代码改变世界

AndroidReader

  ZuelArlis  阅读(16)  评论(0编辑  收藏  举报

# 二.需求分析与设计

## 项目背景

很多人都用过小说阅读器,现如今软件市场中也有很多种类的阅读器,但这些阅读器大多都有两个共同的缺点:

1. 收费。先免费给你看前面十几章的内容,在你看到关键章节时告诉你接下来要收费了。虽然说收费无可厚非,但在看得兴起时冒出来这个多少有些扫兴,对于学生党而言,白嫖的永远是最香的
2. 广告。比起收费,这个应该更难让人忍受。进入App时弹广告就算了,有些阅读器更过分,会在你看书时突然弹出一个广告,一不小心就会点进去

除此之外,有些App的小说章节并不齐全,有时候会出现无法获取该章节的问题,导致读者无法了解该章节的具体内容。

我们在github上找到了追书神器的开源API,发现它具有较齐全的小说内容、排行榜信息、书籍分类信息等,因此我们决定借助此API来实现一个属于FTE的小说阅读器,鉴于API不是自己的,因此仅供FTE内部使用。

## 运行环境

经测试,本App在Android8.0、7.0、5.1版本均可正常使用,只需安装提供的APK即可体验FTEReader

## 功能需求与设计

作为一个小说阅读器,我们认为主要得有以下几点功能:

1. 阅读,这个是最主要的
2. 本地书架
3. 在线书城

我们把要实现的功能具体化,设计了如下功能:

1. 阅读界面:
- 分章节阅读,滑动翻页切换不同章节
- 书架的书籍阅读章节记忆功能,即再次阅读时自动跳转上一次阅读章节
- 切换夜间模式,保护眼睛
- 调整字体大小
- 章节跳转
- 阅读时提示当前系统时间、电量

2. 本地书架:

- 能够阅读相应书籍
- 添加、删除功能

3. 在线书城:

- 按排行榜找书
- 按分类找书
- 直接搜索书名来找书(搜索需要能够模糊搜索、推荐搜索以及记录搜索历史)

在线书城一个重要的特点是要分男/女生来推荐,这个功能也得实现

4. 在线书籍:

- 提供读者书籍相关信息,吸引读者阅读
- 提供本书相关的推荐

5. 用户体验度:

一个好的App不仅要有齐全的功能,还需要考虑给用户最好的体验,我们考虑了如下几点:

- 流畅程度。在进行网络访问操作时需要额外注意
- 美观。优美的界面会让读者更愿意使用我们的App
- 节省流量。作为需要联网的App,肯定也不能让读者消耗太多流量

6. App健壮性:

由于App需要联网,而网络状况是未知的,为了防止App崩溃,我们处理了各种网络故障的情况,并有相应的提示

## 数据需求

我们需要获取的数据有:

1. 书籍排行榜信息
2. 书籍分类信息
3. 书籍介绍信息
4. 书籍具体章节以及内容信息

这些数据都在API中有具体的接口来供我们访问获取,因此我们不必考虑数据的问题
# 三.应用类图/时序图/架构图等

项目结构图:

 

我们将整个代码分为了6部分:

1. control:包含了创建、操作数据库的函数。

![在这里插入图片描述](https://img-blog.csdnimg.cn/49f9dfbf5b8a4fa6b6794a631097a630.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_11,color_FFFFFF,t_70,g_se,x_16#pic_center)

2. fragment:

![在这里插入图片描述](https://img-blog.csdnimg.cn/c0393cc659544bc5a6151c53285a8ef7.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_10,color_FFFFFF,t_70,g_se,x_16#pic_center)


3. recylerView:包含和recylerview相关的对象、适配器和基础的viewholder
![在这里插入图片描述](https://img-blog.csdnimg.cn/652e63dcf6c847ddbd4a4b4d74c177bc.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_10,color_FFFFFF,t_70,g_se,x_16#pic_center)

 

4. object:包含各种api返回对象类、数据库对象类等等

![在这里插入图片描述](https://img-blog.csdnimg.cn/1141cbd79aa344108c3136d05567ce38.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_11,color_FFFFFF,t_70,g_se,x_16#pic_center)


5. service:包含api服务相关函数

![在这里插入图片描述](https://img-blog.csdnimg.cn/291022ce23b74d2abec65ce30ddc5fe0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_9,color_FFFFFF,t_70,g_se,x_16#pic_center)


6. activity:该app所有的activity


![在这里插入图片描述](https://img-blog.csdnimg.cn/e400f968cf7a4d46a579ecb1057301a4.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_9,color_FFFFFF,t_70,g_se,x_16#pic_center)
# 四.所采用的技术和采用该技术的原因

### 1.ViewPager+Fragment的使用

**原因**

每一个Fragment对应一个界面,使用此技术来显示界面,能够更流畅地在各个界面之间完成切换,
比如主界面的书架、排行榜、分类就是利用3个Fragment来显示的
再增加RadioGroup作为导航栏,就能够实现类似于微信主界面一样的效果了,各个界面之间切换流畅,具有美感

**方法**

使用的方法就是在布局中添加ViewPager:

```xml
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never"
android:scrollbars="none"/>
```

再在Activity中设置Adapter、FragmentList:

```java
private ViewPager viewPager;
private List<Fragment> fragmentList = new ArrayList<>();
private TabFragmentStatePagerAdapter fragmentPagerAdapter;

//初始化Fragment
//...
fragmentList.add(bookShelfFragment);
fragmentList.add(rankingFragment);
fragmentList.add(categoryFragment);
```

设置ViewPager:

```java
viewPager.setOnPageChangeListener(new MyPagerChangeListener());
fragmentPagerAdapter = new TabFragmentStatePagerAdapter(getSupportFragmentManager(), fragmentList);
viewPager.setAdapter(fragmentPagerAdapter);
viewPager.setCurrentItem(0);
viewPager.setOffscreenPageLimit(2);
```

### 2.RadioGroup实现导航栏

**原因**

只有单纯的滑动切换Fragment给用户的提示信息还不够,界面也不够美观,Fragment的分离也不够明显。实现一个导航栏能够让用户明确每一个Fragment的作用。这也是当前许多主流App的做法。

**方法**

利用RadioButton的`android:button="@null"`这个属性来把圆形按钮去掉,再利用`android:drawableTop="@mipmap/bookshelf"`属性来给RadioButton显示不同的图片:

```xml
<RadioButton
style="@style/BottomRB"
android:drawableTop="@mipmap/bookshelf"
/>
```

```xml
<style name="BottomRB">
<item name="android:button">@null</item>
<item name="android:textColor">@drawable/selector_tab_text_color</item>
</style>
```

在RadioButton被选中时,还需要设置字体的颜色以及改变图片颜色,以达到被选中的效果。这可以在RadioGroup的监听事件中来实现,注意需要设置图片的大小,否则会不适配,设置的方法为:

RadioButton的选中与ViewPager切换帧需要结合起来,才能达到导航栏的效果。

在切换选中的RadioButton时,要修改ViewPager当前显示的帧;在切换ViewPager显示的帧时,要修改被选中的RadioButton。

### 3.网络访问部分使用了名为Retrofit的网络加载框架,其底层使用OKHttp封装,支持rxjava。

**原因**

- 解耦,减少API接口定义和API接口使用的耦合
- 包含许多注解,简化代码,方便使用
- 支持同步、异步、RxJava
- 可以使用不同工具解析数据,如json、xml
- 请求速度快

**方法**

接口类:

```java
@GET("/ranking/gender")
Call<AllRankingObj> getAllRanking();
```
```java
OkHttpClient build = new OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS)
.writeTimeout(2, TimeUnit.SECONDS)
.build();

// 用于API访问
Retrofit retrofitForApi = new Retrofit.Builder()
.baseUrl(ApiUrl)
// 设置json数据解析器
.addConverterFactory(GsonConverterFactory.create())
// RxJava封装OkHttp的Call函数,本质还是利用OkHttp请求数据
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(build)
.build();
```

### 4.设置同步变量控制线程顺序

**原因**

当输入速度特别快时,有的线程清空了list,而有的线程正在调用list里的元素,这样会发生访问异常,需要设置同步变量使得同一时刻只有一个线程在运行。

**方法**

```java
flag = true; //用于同步问题的布尔变量
if (!s.equals("")&&flag) {
new Thread(new Runnable() {
@Override
public void run() {
flag = false;
...
}
}
}
```

### 5.将多个列表放在一个activity下

**原因**

将不同列表放入多个activity里可以使得界面布局简单,但是带来的问题是需要增加许多activity之间的通信,增加了跳转的复杂度,而将列表放入同一个activity里方便跳转时的信息传递。

**方法**

```java
//设置可见
if (!isSubmit) {
if (s.equals("")) {
initialLayout.setVisibility(View.VISIBLE);
searchLayout.setVisibility(View.GONE);
}
else {
initialLayout.setVisibility(View.GONE);
searchLayout.setVisibility(View.VISIBLE);
fuzzyList.setVisibility(View.VISIBLE);
resultList.setVisibility(View.GONE);
}
}
```
### 6.系统广播获取系统电量和时间
**原因**

用户进入阅读界面时是全屏阅读,顶部状态栏会消失,那么需要有个信息栏来简要显示系统电量和时间

**方法**

1. `onCreate` 中注册广播,接受电量和时钟变化,并创建自定义的广播接收者`MyReceiver`类来处理订阅

```java
//注册广播接受者java代码
intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); // 电量变化广播
intentFilter.addAction(ACTION_TIME_TICK);
//创建广播接受者对象
myReceiver = new MyReceiver();
//注册receiver
registerReceiver(myReceiver, intentFilter);
```

1. 自定义广播接收者类,处理电量和时钟变化广播

```java
class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if(Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())){
//获取当前电量
int level = intent.getIntExtra("level", 0);
//电量的总刻度
int scale = intent.getIntExtra("scale", 100);
//把它转成百分比
int percent = level*100/scale;
// TODO: Do Something when battery_changed
}
else if (Intent.ACTION_TIME_TICK.equals(intent.getAction())) {
// TODO:Do Something when time_tick
}
}
}
```

1. `onDestroy` 中注销广播订阅

```java
unregisterReceiver(myReceiver);
```
------

### 7.非注销生命周期的横竖屏切换

**原因**

1. 小说阅读需要适应用户阅读习惯,有时候用户需要横屏阅读,因此需要实现阅读界面横竖屏模式切换
2. 如果不设置上面第1步的代码,那么横竖屏切换会重新注销Activity,即结束当前活动的生命周期并重新开启一个新的声明周期。这样不仅需要时间重新进行网络访问获取小说章节内容而且会影响到用户使用体验。

**方法**

1. 在`AndroidManifest.xml` 中对Activity 设置属性(关键)

```xml
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="portrait"
```

1. 横竖屏切换后台java代码

切换到横屏

```java
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
```

​ 切换到竖屏

```java
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
```

# 五.详细设计


#### 1.数据库创建

```java

@Override//创建数据库
public void onCreate(SQLiteDatabase sqLiteDatabase) {
//书架上的书,含书名、类型(本地还是网络)、阅读进度、资源地址、书的封面
String CREATE_TABLE1 = "CREATE TABLE if not exists "
+ TABLE_NAME1
+ " (_id TEXT PRIMARY KEY, name TEXT, type INTEGER ,progress INTEGER, address TEXT,image BLOB, description TEXT, author TEXT, major TEXT)";
//搜索历史
String CREATE_TABLE2 = "CREATE TABLE if not exists "
+ TABLE_NAME2
+ " (_id INTEGER PRIMARY KEY, content TEXT)";
// 阅读器状态保存
String CREATE_TABLE3 = "CREATE TABLE if not exists "
+ TABLE_NAME3
+ " (user_id INTEGER PRIMARY KEY, hor_or_ver_screen INTEGER, day_or_night_status INTEGER, textSize INTEGER)";
sqLiteDatabase.execSQL(CREATE_TABLE1);
sqLiteDatabase.execSQL(CREATE_TABLE2);
sqLiteDatabase.execSQL(CREATE_TABLE3);

Resources res = MainActivity.getContext().getResources();
Bitmap bitmap = BitmapFactory.decodeResource(res, R.mipmap.bookcover);
ContentValues values = new ContentValues();
ShelfBookObj book = new ShelfBookObj("5816b415b06d1d32157790b1","圣墟",bitmap,"",0,"default",0,"在破败中崛起,在寂灭中复苏。沧海成尘,雷电枯竭,那一缕幽雾又一次临近大地,世间的枷锁被打开了,一个全新的世界就此揭开神秘的一角……","辰东","玄幻");
//开始添加第一条数据_id TEXT PRIMARY KEY, name TEXT, type INTEGER ,progress INTEGER, address TEXT,image BLOB, description TEXT
values.put("name",book.getName());
//...
sqLiteDatabase.insert(TABLE_NAME1,null,values);
values.clear();
bitmap = BitmapFactory.decodeResource(res, R.mipmap.bookcover2);
book = new ShelfBookObj("59ba0dbb017336e411085a4e","元尊",bitmap,"",0,"default",0,"《斗破苍穹》《武动乾坤》之后全新力作,朝堂太子踏上了荆棘重生之路…","天蚕土豆","玄幻");
values.put("name",book.getName());
//...
sqLiteDatabase.insert(TABLE_NAME1,null,values);

// 往阅读器状态表格中保存一条初始阅读器状态
UserStatusObj u = new UserStatusObj(0,1,0,18);
values.clear();
values.put("user_id", 0); // 用户id为0,表示默认状态
values.put("hor_or_ver_screen", u.getHor_or_ver_screen()); // 1表示竖屏,0表示横屏
values.put("day_or_night_status", u.getDay_or_night_status()); // 0表示日间,1表示夜间
values.put("textSize", u.getTextSize()); // 字体大小
sqLiteDatabase.insert(TABLE_NAME3,null,values);
}
```

#### 2.书籍类封装

```java
public class ShelfBookObj {
String bookId; //id
private Bitmap icon; //图标
private String iconURL; //图标网络url
private String name; //书名
private String description; //描述
private String author; //作者
private String major; //一级分类
int type; //0代表网络图片,1代表本地图片
private String address; //本地书籍url
private int readChapter; //阅读到的章节
}//get、set函数太长省略
```

#### 3.视图填充

```java
TextView name = holder.getView(R.id.item_book_name);
name.setText(book.getTitle());
TextView author = holder.getView(R.id.item_book_author);
author.setText(book.getAuthor());
TextView major = holder.getView(R.id.item_book_type);
major.setText(book.getCat());
TextView intro = holder.getView(R.id.item_book_intro);
String introString = book.getShortIntro();
if (introString.length() > 50){
introString = introString.substring(0,49)+"……";
}
intro.setText(introString);
final ImageView cover = holder.getView(R.id.item_book_cover);
new Thread(new Runnable() {
@Override
public void run() {
try {
if (isNetWorkConnected(MainActivity.getContext())) {//联网判断
URL url = new URL(BookService.StaticsUrl + book.getCover());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
if (connection.getResponseCode() == 200) {
InputStream inputStream = connection.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
handler.post(new Runnable() {
@Override
public void run() {
cover.setImageBitmap(bitmap);
}
});
}
}
else{
Looper.prepare();
Toast.makeText(SearchActivity.this,"网络似乎出现了点问题",Toast.LENGTH_SHORT).show();
Looper.loop();
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}).start();
}
```

#### 4.item点击事件(历史记录item、搜索结果item、模糊推荐item)

```java
//搜索结果点击函数
public void onClick(final int position) {
final Intent intent = new Intent(SearchActivity.this, BookDetailActivity.class);
final Bundle bundle = new Bundle();
new Thread(new Runnable() {
@Override
public void run() {
if (isNetWorkConnected(MainActivity.getContext())) {
final BookObj t = getBookService().getBookById(results.get(position).get_id());
if (t != null)
handler.post(new Runnable() {
@Override
public void run() {
bundle.putSerializable("bookobj", t);
intent.putExtras(bundle);
startActivity(intent);
}
});
}
else {
Looper.prepare();
Toast.makeText(SearchActivity.this,"网络似乎出现了点问题",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}
}).start();
}
```

#### 5.历史记录点击

```java
public void recordClick(final String s) {
isSubmit = true;
//设置可见
initialLayout.setVisibility(View.GONE);
searchLayout.setVisibility(View.VISIBLE);
fuzzyList.setVisibility(View.GONE);
resultList.setVisibility(View.VISIBLE);
//添加历史
if (!histories.contains(s)){
histories.add(s);
getInstance(getBaseContext()).addSearchHistory(s);
}
//填充数据
new Thread(new Runnable() {
@Override
public void run() {
if (isNetWorkConnected(MainActivity.getContext())) {
SearchResultObj tt = getBookService().getSearchResultObj(s, 0, 8);
results.clear();
if (tt != null) {//api是否出现问题判断
List<SearchResultObj.book> t = tt.getBookList();
results.addAll(t);
handler.post(new Runnable() {
@Override
public void run() {
recyclerViewAdapter.notifyDataSetChanged();
}
});
}
else {
Looper.prepare();
Toast.makeText(SearchActivity.this,"搜索不到相关内容",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}
else {
Looper.prepare();
Toast.makeText(SearchActivity.this,"网络似乎出现了点问题",Toast.LENGTH_SHORT).show();
Looper.loop();
}
}
}).start();
recyclerViewAdapter.notifyDataSetChanged();
searchView.setQuery(s,true);
}
```

#### 6.搜索事件

```java
@Override
public boolean onQueryTextChange(final String s) {
//设置可见
if (!isSubmit) {
if (s.equals("")) {
initialLayout.setVisibility(View.VISIBLE);
searchLayout.setVisibility(View.GONE);
}
else {
initialLayout.setVisibility(View.GONE);
searchLayout.setVisibility(View.VISIBLE);
fuzzyList.setVisibility(View.VISIBLE);
resultList.setVisibility(View.GONE);
}
}
else {
if (s.equals("")) {
fuzzyList.setVisibility(View.GONE);
resultList.setVisibility(View.VISIBLE);
}
else {
fuzzyList.setVisibility(View.VISIBLE);
resultList.setVisibility(View.GONE);
}
}
//填充数据
if (!s.equals("")&&flag) {
new Thread(new Runnable() {
@Override
public void run() {
flag = false;
if (isNetWorkConnected(MainActivity.getContext())) {
SearchResultObj tt = getBookService().getSearchResultObj(s, 0, 8);
List<String> t = new ArrayList<>();
if (tt != null) {
List<SearchResultObj.book> tss = tt.getBookList();
int size = tss.size();
for (int i = 0; i < size; i++)
t.add(tss.get(i).getTitle());
}
tempFuzzy.clear();
if (t.size() > 0)
tempFuzzy.addAll(t);
handler.post(new Runnable() {
@Override
public void run() {
fuzzyAdapter.notifyDataSetChanged();
}
});
}
else {
Looper.prepare();
Toast.makeText(SearchActivity.this,"网络似乎出现了点问题",Toast.LENGTH_SHORT).show();
Looper.loop();
}
flag = true;
}
}).start();
fuzzyAdapter.notifyDataSetChanged();
}
return false;
}
```

#### 7.数据库的增删改查(各列一例、函数过多不一一详述)

```java
//更新阅读进度
public void updateProgress(int progress, String id) {
SQLiteDatabase db = getWritableDatabase();
ContentValues value = new ContentValues();
value.put("progress", progress);
db.update(TABLE_NAME1, value, "_id=?", new String[] { id });
db.close();
}
```

#### 8.界面UI及后台响应实现

在参考了很多小说阅读器后,我们决定把主界面设计成类似微信的样式,设计3个界面:书架、排行榜、分类。利用底部导航栏来切换界面,当然也可以直接滑动切换,可以从这3个界面跳转到其他界面,如搜索界面、阅读界面。

要实现这样的效果,ViewPager+Fragment的使用是必不可少的,我结合使用了这两个控件及RadioGroup来实现总体界面。

主界面具有顶部提示栏和底部导航栏,中间就是显示具体界面的帧,在滑动时可以看到它们的差别。

要通过RadioGroup来实现底部导航栏的效果,我们可以利用RadioButton的`android:button="@null"`这个属性来把圆形按钮去掉,然后利用`android:drawableTop="@mipmap/bookshelf"`属性来给RadioButton显示不同的图片,在RadioButton被选中时,还需要设置字体的颜色以及改变图片颜色,以达到被选中的效果。RadioButton的选中与ViewPager切换帧需要结合起来,才能达到导航栏的效果。

```java
//处理底部RG事件
bottomRG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
int index = 0;
switch (checkedId) {
case R.id.main_bottom_bookshelf:
index = 0;
//设置图片
break;
case R.id.main_bottom_ranking:
index = 1;
//设置图片
break;
case R.id.main_bottom_category:
index = 2;
//设置图片
break;
}
viewPager.setCurrentItem(index);
}
});
```

 

```java
//设置一个ViewPager的监听事件,左右滑动ViewPager时进行处理
public class MyPagerChangeListener implements ViewPager.OnPageChangeListener {
@Override
public void onPageScrollStateChanged(int arg0) {
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageSelected(int arg0) {
switch (arg0) {
case 0:
title.setText("书架");
radioGroup.setVisibility(View.GONE);
bookshelfRB.setChecked(true);
break;
case 1:
title.setText("排行榜");
radioGroup.setVisibility(View.VISIBLE);
rankingRB.setChecked(true);
break;
case 2:
title.setText("分类");
radioGroup.setVisibility(View.GONE);
categoryRB.setChecked(true);
break;
}
}
}
```

 

#### 排行榜界面的设计

排行榜界面分男女,主界面上显示的是榜单,点击榜单可以进入到相应的具体排行榜。榜单的UI设计利用了CardView和RecyclerView,男生榜单和女生榜单写在同一个Fragment中。具体的排行榜界面分为标题栏、顶部导航栏和具体排行榜,每一个排行榜都写在一个Fragment中,利用传递的不同参数来填充不同的数据。

具体的排行榜是一个Activity,而不同的榜单(周榜、月榜、总榜)则是不同的Fragment,进入具体的排行榜时生成对应的榜单并传递参数:

```java
//排行榜的Fragment
for (int i = 0; i < 3; ++i) {
DetailCategoryFragment fragment = new DetailCategoryFragment();
Bundle bundle1 = new Bundle();
bundle1.putString("title", bundle.getString("title"));
bundle1.putBoolean("isRanking", isRanking);
bundle1.putBoolean("isMale", isMale);
if (i == 0) bundle1.putString("type", "周榜");
else if (i == 1) bundle1.putString("type", "月榜");
else bundle1.putString("type", "总榜");
fragment.setArguments(bundle1);
fragmentList.add(fragment);
}
```

导航栏的效果与主界面相同,不做赘述。

在生成不同的榜单时,则需要根据获取的参数来访问API,填充数据。

获取排行榜书单分为两步:

1. 获取全部的排行榜,根据所需要的榜单来获取到具体榜单的id
2. 根据具体榜单的id访问API,获取书籍信息并处理

**所有的网络访问都需要使用线程去完成**

具体的实现逻辑如下:

1. 首先检查网络连接是否正常

```java
//检查网络连接
ConnectivityManager connect = (ConnectivityManager)getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = connect.getActiveNetworkInfo();
if (info == null || !info.isAvailable()) {
Toast.makeText(getActivity(), "网络连接状况:未连接", Toast.LENGTH_LONG).show();
return;
}
```

2. 使用线程访问网络,获取全部排行榜

```java
AllRankingObj allRankingObj = bookService.getAllRankingObj();
if (allRankingObj == null || allRankingObj.isOk() == false) {
Looper.prepare();
Toast.makeText(getContext(), "获取数据失败", Toast.LENGTH_LONG).show();
Looper.loop();
Log.d("error", "获取全部排行榜失败");
return;
}
```

3. 获取具体榜单的id

```java
//获取具体榜单的id
if (isMale) { //男生
for (AllRankingObj.subClass subClass : allRankingObj.getMaleList()) {
if (subClass.getShortTitle().equals(title)) {
if (title.equals("热搜榜")) rankingid = subClass.getId();
else {
if (type.equals("周榜")) rankingid = subClass.getId();
else if (type.equals("月榜")) rankingid = subClass.getMonthRank();
else if (type.equals("总榜")) rankingid = subClass.getTotalRank();
else {
System.exit(1);
Log.d("error", "榜单名错误!");
}
}
break;
}
}
}
else { //女生
//省略
}
```

4. 在线程中新开一个线程,再次访问网络,获取书籍

```java
//得到id后再获取具体榜单的书籍信息
new Thread(new Runnable() {
@Override
public void run() {
SingleRankingObj singleRankingObj = bookService.getSingleRankingObj(rankingid);
if (singleRankingObj == null || singleRankingObj.isOk() == false) {
Looper.prepare();
Toast.makeText(getContext(), "获取失败", Toast.LENGTH_LONG).show();
Looper.loop();
Log.d("error", "获取单一排行榜失败");
return;
}
List<BookObj> objList = singleRankingObj.getRanking().getBookList();
total = singleRankingObj.getRanking().getTotal();
for (int i = 0; i < objList.size(); ++i) {
BookObj bookObj = objList.get(i);
String intro = bookObj.getShortIntro();
if (intro.length() > 50) intro = intro.substring(0, 50);
intro += "...";
bookObj.setShortIntro(intro);
bookObjList.add(bookObj);
}
//使用handler
}
}).start();
```

5. 获取到书籍后需要填充RecyclerView的Adapter并显示,UI的修改无法在子线程中完成,因此需要使用Handler

```java
handler.post(new Runnable() {
@Override
public void run() {
updateRecyclerView(0, PAGE_COUNT);
progressBar.setVisibility(View.GONE);
//数据加载完毕时取消动画
swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getActivity(), "数据刷新完成", Toast.LENGTH_SHORT).show();
}
});
```


获取到了数据之后,我们还需要把数据显示出来。

首先注意到榜单前3名有金、银、铜牌的标识,这简单地设置visible即可。

书籍条目的其他信息都是简单的填充,没有太多技术性问题。但图片的显示也需要网络访问,并且需要使用线程来异步访问,否则就会太过耗时。

```java
//通过网络获取书籍图标
final String iconURL = BookService.StaticsUrl + bookObj.getCover();
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(iconURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
if (connection.getResponseCode() == 200) {
InputStream inputStream = connection.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
mHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}}).start();
```


#### 分类界面的设计

分类界面也分男生女生,并写在不同的Fragment里,这一点与排行榜界面不同。利用RecyclerView和Button来显示不同的分类。

具体分类界面的设计与具体排行榜界面很相似,因此我把它们的实现写在了同一个Activity中,实现了重用。

主要的不同之处还是体现在获取数据的方式上,获取具体分类的书籍并没有排行榜那么麻烦,根据具体类型即可访问API来获取:

```java
//获取具体分类书单
void getCateBookList() {
new Thread(new Runnable() {
@Override
public void run() {
String gender = (isMale == true) ? "male" : "female";
Log.d("type:", "" + type);
final CategoryObj categoryObj = bookService.getBooksByCategoty(type, title, 0, 30000, gender);
if (categoryObj == null || categoryObj.isOk() == false) {
Looper.prepare();
Toast.makeText(getContext(), "获取数据失败", Toast.LENGTH_LONG).show();
Looper.loop();
Log.d("error", "获取主题书单列表失败");
return;
}
total = categoryObj.getTotal();
for (BookObj bookObj : categoryObj.getBooks()) {
if (bookObj.getShortIntro().length() > 50){
String intro = bookObj.getShortIntro();
intro = intro.substring(0, 50);
intro += "...";
bookObj.setShortIntro(intro);
}
bookObjList.add(bookObj);
}
//handler
}
}).start();
```

 

#### 数据下拉刷新和上拉加载的实现

事实上,因为网络中的书籍太多,我们不可能一次性把API提供的所有书籍都获取出来,这样不仅耗时,而且浪费用户流量,因此分批次加载是必须的。鉴于此,我给排行榜和分类界面的书籍获取设计了下拉刷新和上拉加载。

下拉刷新比较简单,可以利用SwipeRefreshLayout来实现。

在布局中使用一个SwipeRefreshLayout包裹RecyclerView:

```xml
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/detail_category_swipeRefresh"
android:layout_height="match_parent"
android:layout_width="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/detail_category_recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"/>
</android.support.v4.widget.SwipeRefreshLayout>
```

SwipeRefreshLayout自带了下拉操作,因此我们只需要设置下拉刷新的回调事件即可:

```java
//设置下拉显示的动画颜色
swipeRefreshLayout.setColorSchemeColors(Color.RED, Color.BLUE);
//下拉刷新的回调事件
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//数据刷新
initBookList();
}
});
```

initBookList函数重新加载书籍,实现刷新效果。

上拉加载的实现思路则是给RecyclerView增加一个显示的条目类型,即底部提示的View,平时这个View都是隐藏的,在上拉到底部时就会显示出来,并根据加载进度显示“正在加载更多”或者“没有更多数据了”。

为了实现上拉加载,我重新自定义了一个RecyclerView的Adapter,一些重要的变量及函数如下:

```java
private int normalType = 0; // 第一种ViewType,正常的item
private int footType = 1; // 第二种ViewType,底部的提示View

private boolean hasMore = true; // 变量,是否有更多数据
private boolean fadeTips = false; // 变量,是否隐藏了底部的提示

private Handler mHandler = new Handler(Looper.getMainLooper()); //获取主线程的Handler

public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
//根据不同的ViewType绑定不同的布局文件
if (viewType == normalType) {
MyViewHolder holder = MyViewHolder.get(context, viewGroup, R.layout.item_book);
holder.setNormalTypeType(true);
return holder;
}
else {
MyViewHolder holder = MyViewHolder.get(context, viewGroup, R.layout.recycler_footer_view);
holder.setNormalTypeType(false);
return holder;
}
}
```

```java

public void onBindViewHolder(@NonNull final MyViewHolder holder, int position) {
if (data.isEmpty()) return;
if (holder.isNormalType() == true) {//是正常的布局
//显示书籍条目信息
}
else {//是提示加载信息的布局
final ProgressBar progressBar = holder.getView(R.id.footer_view_progessBar);
final TextView tips = holder.getView(R.id.footer_view_text);
tips.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE);
// 只有获取数据为空时,hasMore为false,所以当我们拉到底部时基本都会首先显示“正在加载更多...”
if (hasMore == true) {
// 不隐藏footView提示
fadeTips = false;
if (data.size() > 0) {
// 如果查询数据发现增加之后,就显示正在加载更多数据
tips.setText("正在加载更多数据...");
progressBar.setVisibility(View.VISIBLE);
}
} else {
if (data.size() > 0) {
// 如果查询数据发现并没有增加时,就显示没有更多数据了
tips.setText("没有更多数据了");
// 隐藏提示条
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 隐藏提示条
tips.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
// 将fadeTips设置true
fadeTips = true;
// hasMore设为true是为了让再次拉到底时,会先显示正在加载更多
hasMore = true;
}
}, 500);
}
}
}
//设置监听器
if (onItemClickListener != null) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onItemClickListener.onClick(holder.getAdapterPosition());
}
});
}
}
```

在这里,显示提示加载信息的布局则是在滑到底部时显示出来的提示信息条目。

```java
@Override
public int getItemCount() {
return data.size() + 1;
}

//获取列表中数据源的最后一个位置
public int getLastPosition() {
return data.size();
}
```

需要注意的是,`getItemCount`获取到的是数据data.size() + 1,因为实际的条目还有一个提示条目,而`getLastPosition()`则是实际数据源的最后一个位置。

```java
//更新数据源,并修改hasMore的值,如果有增加数据,hasMore为true,否则为false
public void updateList(List<BookObj> newDatas, boolean hasMore) {
// 在原有的数据之上增加新数据
if (newDatas != null) {
data.addAll(newDatas);
}
this.hasMore = hasMore;
notifyDataSetChanged();
}
```

在Fragment中,需要设置RecyclerView的滑动监听事件来实现上拉刷新:

```java
//设置滑动监听器
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// 在newState为滑到底部时
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// 如果没有隐藏footView,那么最后一个条目的位置就比我们的getItemCount少1
if (recyclerViewAdapter.isFadeTips() == false && lastVisibleItem + 1 == recyclerViewAdapter.getItemCount()) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
// 然后调用updateRecyclerview方法更新RecyclerView
updateRecyclerView(recyclerViewAdapter.getLastPosition(), recyclerViewAdapter.getLastPosition() + PAGE_COUNT);
}
}, 500);
}

// 如果隐藏了提示条,我们又上拉加载时,那么最后一个条目就要比getItemCount要少2
if (recyclerViewAdapter.isFadeTips() == true && lastVisibleItem + 2 == recyclerViewAdapter.getItemCount()) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
// 然后调用updateRecyclerview方法更新RecyclerView
updateRecyclerView(recyclerViewAdapter.getLastPosition(), recyclerViewAdapter.getLastPosition() + PAGE_COUNT);
}
}, 500);
}
}
}

@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 在滑动完成后,拿到最后一个可见的item的位置
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
lastVisibleItem = layoutManager.findLastVisibleItemPosition();
}
});
```

```java
// 辅助函数:判断网络是否连接
public boolean isNetWorkConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isConnected();
}
}
return false;
}
```

 

设置 `RecyclerView` 适配:

```java
// 设置 RecyclerView
private void setRecyclerView() {
// 获取页面的 RecyclerView 控件
recyclerView = view.findViewById(R.id.fragment_male_in_category_recyclerview);

// 设置 RecyclerView 的布局方式
recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 2));

// 设置 Adapter 配置
adapter = new MyRecyclerViewAdapter<CategoryRecyObj>(getActivity(), R.layout.item_category, myCategories) {
@Override
public void convert(MyViewHolder holder, CategoryRecyObj categoryRecyObj) {
TextView categoryName = holder.getView(R.id.item_category_name);
categoryName.setText(categoryRecyObj.getCategoryName());
TextView categoryBookCount = holder.getView(R.id.item_category_count);
categoryBookCount.setText(categoryRecyObj.getBookCount());
}
};

// 每个主类按钮的点击响应处理
adapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() {
@Override
public void onClick(int position) {
//跳转具体分类界面
Intent intent = new Intent(getActivity(), CategoryActivity.class);
Bundle bundle = new Bundle();
bundle.putBoolean("isRanking", false);
bundle.putBoolean("isMale", isMale);
if (isMale) bundle.putString("title", maleCategoriesName[position]);
else bundle.putString("title", femaleCategoriesName[position]);
intent.putExtras(bundle);
startActivity(intent);
}

@Override
public void onLongClick(int position) {

}
});

// 给 RecyclerView 适配 Adapter
recyclerView.setAdapter(adapter);
}
```

***

#### 9. 数据库用户阅读习惯表实现

**用户阅读习惯封装类实现**

```java
public class UserStatusObj {
int user_id;
int hor_or_ver_screen;
int day_or_night_status;
int textSize;
// get && set method
}
```

**数据库表格实现并初始化**

```java
// 阅读器状态保存
String CREATE_TABLE3 = "CREATE TABLE if not exists "
+ TABLE_NAME3
+ " (user_id INTEGER PRIMARY KEY, hor_or_ver_screen INTEGER, day_or_night_status INTEGER, textSize INTEGER)";
sqLiteDatabase.execSQL(CREATE_TABLE3);
// 往阅读器状态表格中保存一条初始阅读器状态
UserStatusObj u = new UserStatusObj(0,1,0,18);
values.clear();
values.put("user_id", 0); // 用户id为0,表示默认状态
values.put("hor_or_ver_screen", u.getHor_or_ver_screen()); // 1表示竖屏,0表示横屏
values.put("day_or_night_status", u.getDay_or_night_status()); // 0表示日间,1表示夜间
values.put("textSize", u.getTextSize()); // 字体大小
sqLiteDatabase.insert(TABLE_NAME3,null,values);
```

**数据库get&&set函数接口**

```java
public UserStatusObj get_User_Status_Obj(int user_id) {
SQLiteDatabase db = getWritableDatabase();
String id = Integer.toString(user_id);
String sql = String.format("SELECT * FROM " + TABLE_NAME3 + " where user_id='%s'" , id);
Cursor cursor = db.rawQuery(sql, null);
UserStatusObj res = null;
while (cursor.moveToNext()) {
int hor_or_ver_screen = cursor.getInt(cursor.getColumnIndex("hor_or_ver_screen"));
int day_or_night_status = cursor.getInt(cursor.getColumnIndex("day_or_night_status"));
int textSize = cursor.getInt(cursor.getColumnIndex("textSize"));
res = new UserStatusObj(user_id, hor_or_ver_screen, day_or_night_status, textSize);
return res;
}
cursor.close();
db.close();
return res;
}
```

```java
// 更新用户阅读器习惯的状态
public void updateStatus(int user_id, UserStatusObj u) {
SQLiteDatabase db = getWritableDatabase();
ContentValues value = new ContentValues();
value.put("hor_or_ver_screen", u.getHor_or_ver_screen()); // 1表示竖屏,0表示横屏
value.put("day_or_night_status", u.getDay_or_night_status()); // 0表示日间,1表示夜间
value.put("textSize", u.getTextSize()); // 字体大小
db.update(TABLE_NAME3, value, "user_id=?", new String[] { Integer.toString(user_id)});
db.close();
}
```

***

#### 10.小说列表项UI设计

按照需求样式设计较为美观的小说列表项,包含居中调整,字体样式、大小调整等。总体是左边封面图,右边详细信息。(UI代码略)

***

#### 11.阅读界面UI及后台响应实现

**UI部分**

- `activity_read_page.xml` 主要由章节内容帧 `ViewPager` +底部信息栏+底部功能栏+显示加载中 `ProgressBar` 实现
- `ViewPager` 的章节内容帧 `fragment_read_page.xml` 由标题+内容两个`TextView` 和 `ScrollView` 上下滑动条组成。
- 底部信息栏包含左部的系统电量+系统时间信息,右部的阅读进度信息。
- 底部功能栏有四个功能,由 `RadioGroup` 实现,包含夜间/日间模式+横竖屏模式+字体大小设置+章节目录功能按钮。
- UI代码略

**后台部分(含关键代码)**

- **获取页面跳转信息** :包括小说id,小说名,当前阅读章节

```java
//从传递的参数中获取章节相关信息
Intent intent = this.getIntent();
Bundle bundle = intent.getExtras();
bookid = bundle.getString("bookid");
bookname = bundle.getString("bookname");
currChapter = bundle.getInt("currentChapter");
```

- **获取页面控件** :通过 `findViewById` 获取页面控件控制
- **从数据库获取用户阅读习惯**

```java
// 从数据库中获取用户的阅读习惯
private void set_Reading_Status() {
// 获取用户状态(默认用户状态)
userStatusObj = DatabaseControl.getInstance(this).get_User_Status_Obj(0);
// 获取默认用户下的横竖屏状态
int hor_or_ver_screen = userStatusObj.getHor_or_ver_screen();
if(hor_or_ver_screen == 1) {
is_vertical_screen = true;
}
else {
is_vertical_screen = false;
// 切换成横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
day_or_night_status = userStatusObj.getDay_or_night_status();
if(day_or_night_status == 1) whole_layout_control.setBackgroundColor(getResources().getColor(R.color.nightBackGround));
textSize = userStatusObj.getTextSize();
}
```

​ 当然,当退出阅读界面的时候,也要更新数据库,写入用户当前的阅读习惯,也要写入用户当前阅读到的章节数:

```java
@Override
protected void onPause() {
System.out.println("onPause");
// 将阅读到的当前章节存入数据库
DatabaseControl.getInstance(this).updateProgress(currChapter, bookid);
userStatusObj.setDay_or_night_status(day_or_night_status);
userStatusObj.setHor_or_ver_screen((is_vertical_screen?1:0));
userStatusObj.setTextSize(textSize);
DatabaseControl.getInstance(this).updateStatus(0,userStatusObj);
super.onPause();
}
```

 

- **获取APP当前显示屏幕宽高**

```java
// 获取屏幕宽高等信息
private void get_screen_info() {
DisplayMetrics dm = getResources().getDisplayMetrics();
SCREEN_HEIGHT = dm.heightPixels;
SCREEN_WIDTH = dm.widthPixels;
}
```

- **底部信息栏实现** : 通过系统广播更新,订阅广播实现获取电量和时间

```java
// 注册底部信息栏的系统接收广播
private void init_info_broadcast() {
//注册广播接受者java代码
intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); // 电量变化广播
intentFilter.addAction(ACTION_TIME_TICK);
//创建广播接受者对象
myReceiver = new MyReceiver();
//注册receiver
registerReceiver(myReceiver, intentFilter);
}
```

```java
// 重载onDestroy()中注销广播
unregisterReceiver(myReceiver);
```

```java
// 注册获取系统广播
// 广播获取系统电量和时间
class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// TODO Auto-generated method stub
// 判断它是否是为电量变化的Broadcast Action
// 电量变化广播
if(Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())){
//获取当前电量
int level = intent.getIntExtra("level", 0);
//电量的总刻度
int scale = intent.getIntExtra("scale", 100);
//把它转成百分比
int percent = level*100/scale;
battery_percent_control.setText(Integer.toString(percent));
}
else if (Intent.ACTION_TIME_TICK.equals(intent.getAction())) {
// 设置时间格式
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm");
// 获取当前时间
Date curdate = new Date(System.currentTimeMillis());
// 按照格式将Date 时间转化成格式字符串
String time = formatter.format(curdate);
// UI设置显示
time_control.setText(time);
}
}

}
```

- **网络访问获取小说章节内容** (初始加载上下10章内容):

封装在一个子函数中:`private void init_fragment() `

主要实现是将网络访问获取的一章章的内容做成 `ViewPager` 的一个个帧

该函数的具体步骤如下:

- 设置等待进度条,隐藏 `ViewPager`
- 清空与 `ViewPager` 绑定的数据 `List`
- 启用子线程进行网络访问
- 子线程中:
- 网络连接判断
- 再启用子子线程进行获取上下十个章节内容
- 等待子子线程网络访问结束
- 对获取的文字数据进行处理
- 按照章节信息生成一个个章节内容帧
- ViewPager适配处理后的帧数据
- RXJAVA2 更新界面UI,显示ViewPager内容,隐藏进度条

关键代码:

子子线程获取章节内容:使用 `BookService`

```java
//获取到:章节总页数totalPage、章节title、每页的内容content
Thread initContentThread = new Thread(new Runnable() {
@Override
public void run() {
// 获取所有章节信息
cptListObj = BookService.getBookService().getChaptersByBookId(bookid);
if(cptListObj != null) {
chapterLinks = cptListObj.getImixToc().getChapterLinks();
totalChapter = chapterLinks.size();
System.out.println("共有章节数目: " + totalChapter);
chaptersContent = new ArrayList<>(totalChapter);

// 缓存当前章节以及上下10章的数据
chapterObjs = new ArrayList<ChapterObj>(totalChapter) {
};
cache_chapter_range_min = currChapter - 10;
cache_chapter_range_max = currChapter + 10;
for (int i = cache_chapter_range_min; i <= cache_chapter_range_max; i++) {
System.out.println("正在下载(zero-based): i---" + i + " min---" + cache_chapter_range_min + "---max---" + cache_chapter_range_max);
//超出章节范围
if (i > totalChapter - 1) {
cache_chapter_range_max = totalChapter - 1;
break;
}
if (i < 0) {
cache_chapter_range_min++;
continue;
}
ChapterObj c = BookService.getBookService().getChapterByLink(chapterLinks.get(i).getLink());

chapterObjs.add(i - cache_chapter_range_min, c);
}
System.out.println(cache_chapter_range_min + "----" + cache_chapter_range_max);
}
else {
Looper.prepare();
// 弹出Toast提示
Toast.makeText(ReadPageActivity.this, "无法获取书籍源", Toast.LENGTH_SHORT).show();
ReadPageActivity.this.finish();
Looper.loop();
}
}
});
initContentThread.start();
```

​ 等待子子线程更新完毕再适配ViewPager

```java
try {
initContentThread.join();
} catch (InterruptedException e) {
Log.d("[Error] ", "线程获取信息失败");
e.printStackTrace();
}
```

​ 处理网络访问得到的章节内容数据,并处理成章节帧

```java
// 根据内容适配各帧
for (int i = cache_chapter_range_min; i <= cache_chapter_range_max; ++i) {
// 解析章节内容
String title = chapterLinks.get(i).getTitle();
String content;
if(chapterObjs.get(i-cache_chapter_range_min) == null) {
content = "章节获取失败了呢!客官";
}
else content = chapterObjs.get(i-cache_chapter_range_min).getIchapter().getBody();
// 为段首添加缩进
content = "\u3000\u3000" + content;
content = content.replaceAll("\n", "\n\u3000\u3000");
// 新建对应章节内容帧
ReadPageFragment fragment = new ReadPageFragment();
// 给帧传数据
Bundle bundle = new Bundle();
bundle.putString("title", title);
bundle.putString("content", content);
bundle.putInt("day_or_night_status", day_or_night_status);
bundle.putInt("textSize", textSize);
fragment.setArguments(bundle);
// 将新加的帧放入队列中
fragmentList.add(fragment);
}

viewPager.setOnPageChangeListener(new MyPagerChangeListener());
fragmentAdapter = new TabFragmentStatePagerAdapter(getSupportFragmentManager(), fragmentList);

// 用rxjava更新主线程
rxjava_update_page(0);
```

- **章节内容帧界面实现** : `ReadPageFragment.java`

主要实现是:

将传递到帧界面的标题,页面内容等适配到各控件

另外还有后续的个性化功能例如夜间/白日功能,字体大小设置功能,也通过在这里设置控件信息来实现章节内容阅读帧的生成

```java
private void init_page_info() {
titile_control.setText(title); // 设置标题
content_control.setText(content); // 设置阅读页内容
content_control.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
if(day_or_night_status == 0) {
whole_layout_control.setBackgroundColor(getResources().getColor(R.color.PapayaWhip));
}
else whole_layout_control.setBackgroundColor(getResources().getColor(R.color.nightBackGround));
}
```

- **前/后翻页实现缓存更多章节**

具体实现是:

监听 `ViewPager` 的滑动事件:

`public class MyPagerChangeListener implements ViewPager.OnPageChangeListener`

记录有当前缓存的章节数目区间:

`[cache_chapter_range_min, cache_chapter_range_max]`

当现在阅读到离缓存界限还剩下`cache_left:5` 章的时候,进行新的网络访问,获取更多的`cache_num:10` 章节的内容。

以往后翻页,获取后10章节内容为例子的具体流程是:

- 往后翻页,当前阅读到的章节数改变,`currChapter++;`
- 当达到缓存界限的时候,触发网络访问更新缓存内容
- 与 `init_fragment` 类似通过子线程和子子线程获取缓存后十章的内容,然后处理文字内容添加段首缩进,转换成章节阅读帧,适配 `ViewPager` 。

关键代码:

```java
Thread getNewChapterThread = new Thread(new Runnable() {
@Override
public void run() {
if(isNetWorkConnected(context)) {
for (int i = 1; i <= cache_num; i++) {
int newChapterNum = cache_chapter_range_max + i;
// 非法章节数(已超出网络给出的章节数)
if (newChapterNum >= totalChapter) continue;
// 合法章节则获取新章节
ChapterObj c = BookService.getBookService().getChapterByLink(chapterLinks.get(newChapterNum).getLink());
chapterObjs.add(newChapterNum - cache_chapter_range_min, c);
}
}
else {
Log.e("ERROR", "网络连接状况:未连接");
}
}
});
getNewChapterThread.start();
```

```java
// 新增N个帧
for(int i = 1; i <= cache_num; i++) {
int newChapterNum = cache_chapter_range_max + 1;
// 已经超出总章节数
if(newChapterNum >= totalChapter) continue;
// 网络连接故障,要获取的章节未缓存成功,不添加新帧
if(newChapterNum - cache_chapter_range_min >= chapterObjs.size()) break;
// 合法的章节数
cache_chapter_range_max++;
// 解析章节内容
String title = chapterLinks.get(newChapterNum).getTitle();
String content;
// TODO: 后面跟init_fragment()类似
```

***

#### 12.阅读界面个性化功能实现

个性化功能的切换主要是阅读帧界面的控件样式变换:例如夜间/日间模式切换与文字大小切换,都是转换帧的格式,因此设置一个`changeFrameStyle()` 子函数来实现重新加载帧。

```java
// 当使用功能按键切换阅读界面阅读习惯时
// 不必要重新进行网络访问
// 直接改变阅读帧的样式
public void changeFrameStyle() {
// 设置等待进度条
progressBar.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.GONE);
// 清空当前的FragmentList
fragmentList.clear();
for(int i = cache_chapter_range_min; i<= cache_chapter_range_max; i++) {
// 解析章节内容
String title = chapterLinks.get(i).getTitle();
String content;
if(chapterObjs.get(i-cache_chapter_range_min) == null) {
content = "章节获取失败了呢!客官";
}
else content = chapterObjs.get(i-cache_chapter_range_min).getIchapter().getBody();
// 为段首添加缩进
content = "\u3000\u3000" + content;
content = content.replaceAll("\n", "\n\u3000\u3000");
// 新建对应章节内容帧
ReadPageFragment fragment = new ReadPageFragment();
// 给帧传数据
Bundle bundle = new Bundle();
bundle.putString("title", title);
bundle.putString("content", content);
bundle.putInt("day_or_night_status", day_or_night_status);
bundle.putInt("textSize", textSize);
fragment.setArguments(bundle);
// 将新加的帧放入队列中
fragmentList.add(fragment);
}
rxjava_update_page(2);
}
```

 

**夜间/日间模式**

简单根据当前的夜间/日间模式,调用`changeFrameStyle`更新阅读帧样式。另外还要修改图标与图标文字。代码略。

**横屏/竖屏模式**

```java
// 横屏竖屏功能切换
horizontal_and_vertical_rb_control.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 当前竖屏状态
if(is_vertical_screen) {
// 记录状态数据转变,更新默认用户
is_vertical_screen = false;
horizontal_and_vertical_rb_control.setTextColor(Color.BLACK);
// 切换成横屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
Drawable drawable = getResources().getDrawable(R.mipmap.vertical_screen);
drawable.setBounds(0, 0, 70, 70);
horizontal_and_vertical_rb_control.setCompoundDrawables(null, drawable, null,null);
horizontal_and_vertical_rb_control.setText("竖屏");
horizontal_and_vertical_rb_control.setTextColor(Color.BLACK);
}
// 当前横屏状态
else {
// 记录状态数据转变
is_vertical_screen = true;
horizontal_and_vertical_rb_control.setTextColor(Color.BLACK);
System.out.println("改成竖屏");
System.out.println(DatabaseControl.getInstance(context).get_Hor_Or_Ver_Screen_Status(0));
// 切换成竖屏状态
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
Drawable drawable = getResources().getDrawable(R.mipmap.horizontal_screen);
drawable.setBounds(0, 0, 70, 70);
horizontal_and_vertical_rb_control.setCompoundDrawables(null, drawable, null,null);
horizontal_and_vertical_rb_control.setText("横屏");
horizontal_and_vertical_rb_control.setTextColor(Color.BLACK);
}
}
});
```

**文字字体设置**

弹出一个自定义文字字体大小设置Dialog。

设计这个弹窗Dialog

![在这里插入图片描述](https://img-blog.csdnimg.cn/f9a773a73cad468bb3dac9622ee0ef58.png)

 

 

根据当前APP应用屏幕宽高调整Dialog的宽高。

点击弹窗里+/-大小改变按钮,简单调用`changeFrameStyle` 更改帧样式。

代码省略。

**章节目录展示和跳转功能**

弹出一个自定义章节目录显示Dialog

设计这个弹窗Dialog

![](https://img-blog.csdnimg.cn/img_convert/bbeab2fc8edc799c02c9bc294c240ee0.png)

根据当前APP屏幕宽高调整Dialog的宽高。也即适配横竖屏。

主要构成是 `ScrollView` + `RecyclerView`

设置点击目录item跳转到相应的章节。

关键代码:

```java
// 目录功能
catalog_rb_control.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 弹出一个目录选择框
final Dialog dialog = new Dialog(context);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setContentView(R.layout.catalog_dialog);
// 隐藏功能按钮控件框
rg_control.setVisibility(View.GONE);
show_functional_button = false;
bottom_layout_control.setVisibility(View.VISIBLE);
// 字体不变红色
catalog_rb_control.setTextColor(Color.BLACK);
// 设置dialog标题为书名
TextView title = dialog.findViewById(R.id.catalog_title);
title.setText(bookname);
// 书籍目录RecyclerView
RecyclerView dialog_catalog = dialog.findViewById(R.id.catalog_recylerView);
// 书籍目录数据初始化
myCategory = new ArrayList<>();
for(int i = 0; i < chapterLinks.size(); i++)
myCategory.add(chapterLinks.get(i).getTitle());

// 设置 Adapter
adapter = new MyRecyclerViewAdapter<String>(context, R.layout.item_catalog, myCategory) {
@Override
public void convert(MyViewHolder holder, String s) {
TextView title = holder.getView(R.id.catalog_title);
title.setText(s);
}
};
// 设置点击目录跳转到相应章节
adapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() {
@Override
public void onClick(int position) {
// dialog目录框消失
dialog.dismiss();
// 设置等待进度条
viewPager.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
clickable = false;
// 清空当前的FragmentList
fragmentList.clear();
// 设置跳转章节数
currChapter = position;
// 加载帧
init_fragment();
}

@Override
public void onLongClick(int position) {

}
});

final WindowManager.LayoutParams params = dialog.getWindow().getAttributes();
params.width = (int)(SCREEN_WIDTH * 2 / 3);
params.height = (int)(SCREEN_HEIGHT * 2 / 3);
dialog.getWindow().setAttributes(params);

// 适配 Adapter
dialog_catalog.setAdapter(adapter);

// 设置 RecyclerView 布局
dialog_catalog.setLayoutManager(new LinearLayoutManager(context));

// 设置 dialog 属性并显示
dialog.setCancelable(true);
dialog.show();
}
});
```

***

#### 13.建立网络访问接口(以所有排行榜为例)

```java
public interface UrlService {
/*
* 获取所有排行榜
* @param 无
* @return Call<AllRankingObj>
*/
@GET("/ranking/gender")
Call<AllRankingObj> getAllRanking();

...
}
```

#### 14.初始化多个Retrofit实例分别用于API、图片、章节访问、书籍搜索

```java
Retrofit retrofitForApi = new Retrofit.Builder()
.baseUrl(ApiUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(build)
.build();

Retrofit retrofitForStatics = new Retrofit.Builder()
.baseUrl(StaticsUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(build)
.build();

Retrofit retrofitForChapter = new Retrofit.Builder()
.baseUrl(ChapterUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(build)
.build();

Retrofit retrofitForFuzzySearch = new Retrofit.Builder()
.baseUrl(FuzzySearchUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.client(build)
.build();

private UrlService ApiService = retrofitForApi.create(UrlService.class);
private UrlService StaticsService = retrofitForStatics.create(UrlService.class);
private UrlService ChapterService = retrofitForChapter.create(UrlService.class);
private UrlService FuzzySearchService = retrofitForFuzzySearch.create(UrlService.class);
```

#### 15.封装数据(以所有排行榜为例)

```java
public class AllRankingObj {

// 男生
@SerializedName("male")
private List<subClass> maleList;

// 女生
@SerializedName("female")
private List<subClass> femaleList;

@SerializedName("ok")
private boolean ok;

public List<subClass> getMaleList() {
return maleList;
}

public List<subClass> getFemaleList() {
return femaleList;
}

public boolean isOk() {
return ok;
}

public class subClass {
// 周榜id
@SerializedName("_id")
private String id;

// 排行榜全名
@SerializedName("title")
private String title;

// 排行榜大图标
@SerializedName("cover")
private String cover;

@SerializedName("collapse")
private boolean collapse;

// 月榜id
@SerializedName("monthRank")
private String monthRank;

// 总榜id
@SerializedName("totalRank")
private String totalRank;

// 排行榜简称
@SerializedName("shortTitle")
private String shortTitle;

public String getId() {
return id;
}

public String getTitle() {
return title;
}

public String getCover() {
return cover;
}

public boolean isCollapse() {
return collapse;
}

public String getMonthRank() {
return monthRank;
}

public String getTotalRank() {
return totalRank;
}

public String getShortTitle() {
return shortTitle;
}
}
}
```

#### 16.实现获取对象的函数(以所有排行榜为例)

```java
/*
* 获取所有排行榜
* @param 无
* @return AllRankingObj
*/

public AllRankingObj getAllRankingObj() {
Response<AllRankingObj> response = null;
try {
response = ApiService.getAllRanking().execute();
allRankingObj = response.body();
} catch (IOException e) {
e.printStackTrace();
}
return allRankingObj;
}
```

#### 17.封装书籍信息

```java
public class BookObj implements Serializable {
// 书籍id
@SerializedName("_id")
private String id;

// 书籍标题
@SerializedName("title")
private String title;

// 书籍作者
@SerializedName("author")
private String author;

// 书籍介绍
@SerializedName("longIntro")
private String longIntro;

// 书籍简介
@SerializedName("shortIntro")
private String shortIntro;

// 书籍封面图
@SerializedName("cover")
private String cover;

@SerializedName("site")
private String site;

// 书籍一级分类
@SerializedName("majorCate")
private String majorCate;

// 书籍二级分类
@SerializedName("minorCate")
private String minorCate;

@SerializedName("sizetype")
private int sizetype;

@SerializedName("contentType")
private String contentType;

@SerializedName("allowMonthly")
private boolean allowMonthly;

@SerializedName("banned")
private int banned;

// 最近关注人数
@SerializedName("latelyFollower")
private int latelyFollower;

// 字数
@SerializedName("wordCount")
private int wordCount;

// 留存率
@SerializedName("retentionRatio")
private float retentionRatio;

// 最新章节
@SerializedName("lastChapter")
private String lastChapter;

@SerializedName("updated")
private String updated;

// 性别
@SerializedName("gender")
private String[] gender;

// 标签
@SerializedName("tags")
private String[] tags;

public String getId() {
return id;
}

public String getTitle() {
return title;
}

public String getAuthor() {
return author;
}

public String getShortIntro() {
return shortIntro;
}

public void setShortIntro(String shortIntro) {
this.shortIntro = shortIntro;
}

public String getCover() {
return cover;
}

public String getSite() {
return site;
}

public String getMajorCate() {
return majorCate;
}

public String getMinorCate() {
return minorCate;
}

public int getSizetype() {
return sizetype;
}

public String getContentType() {
return contentType;
}

public boolean isAllowMonthly() {
return allowMonthly;
}

public String getLongIntro() {
return longIntro;
}

public String[] getGender() {
return gender;
}

public int getBanned() {
return banned;
}

public int getLatelyFollower() {
return latelyFollower;
}

public float getRetentionRatio() {
return retentionRatio;
}

public String getLastChapter() {
return lastChapter;
}

public String[] getTags() {
return tags;
}

public int getWordCount() {
return wordCount;
}

public String getUpdated() {
return updated;
}
}
```

#### 18.获取书籍相关信息

```java
new Thread(new Runnable() {
@Override
public void run() {
bookObj = BookService.getBookService().getBookById(bookObj.getId());
...
}
}).start();
```

#### 19.推荐书籍的点击事件

```java
recom1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(BookDetailActivity.this, BookDetailActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable("bookobj", bookObjs.get(0));
intent.putExtras(bundle);
startActivity(intent);
}
});
```

#### 20.跳转到阅读界面

```java
// 阅读
readButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(BookDetailActivity.this, ReadPageActivity.class);
Bundle bundle = new Bundle();
bundle.putString("bookid", bookObj.getId());
intent.putExtras(bundle);
startActivity(intent);
}
});
```

#### 21.查看更多同类书籍

```java
// 查看更多同类书籍
moreButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(BookDetailActivity.this, RecomActivity.class);
Bundle bundle = new Bundle();
RecomListObj recomListObj = new RecomListObj(bookObjs);
bundle.putSerializable("list", recomListObj);
intent.putExtras(bundle);
startActivity(intent);
}
});
```

#### 22.推荐书籍列表内容填充

```java
// 内容填充
MyRecyclerViewAdapter<BookObj> adapter = new MyRecyclerViewAdapter<BookObj>(RecomActivity.this, R.layout.item_book, bookObjs) {
@Override
public void convert(MyViewHolder holder, BookObj bookObj) {
final ImageView imageView = holder.getView(R.id.item_book_cover);
TextView bookName = holder.getView(R.id.item_book_name);
TextView bookAuthor = holder.getView(R.id.item_book_author);
TextView bookType = holder.getView(R.id.item_book_type);
TextView bookIntro = holder.getView(R.id.item_book_intro);
bookName.setText(bookObj.getTitle());
bookType.setText(bookObj.getMajorCate());
bookAuthor.setText(bookObj.getAuthor());
String intro = bookObj.getLongIntro();
if (intro.length() > 50) intro = intro.substring(0, 50);
intro += "...";
bookIntro.setText(intro);

//通过网络获取书籍图标
final String iconURL = BookService.StaticsUrl + bookObj.getCover();
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(iconURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
if (connection.getResponseCode() == 200) {
InputStream inputStream = connection.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
mHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}).start();
}
};
```

#### 23.点击推荐书籍,跳转到详情界面

```java
adapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() {
@Override
public void onClick(int position) {
//跳转到书籍详情界面
Intent intent = new Intent(RecomActivity.this, BookDetailActivity.class);
Bundle bundle = new Bundle();
bundle.putSerializable("bookobj", bookObjs.get(position));
intent.putExtras(bundle);
startActivity(intent);
}

@Override
public void onLongClick(int position) {

}
});
```
#### 总体界面布局的设计

在参考了很多小说阅读器后,我们决定把主界面设计成类似微信的样式,设计3个界面:书架、排行榜、分类。利用底部导航栏来切换界面,当然也可以直接滑动切换,可以从这3个界面跳转到其他界面,如搜索界面、阅读界面。

要实现这样的效果,ViewPager+Fragment的使用是必不可少的,我结合使用了这两个控件及RadioGroup来实现总体界面。

主界面具有顶部提示栏和底部导航栏,中间就是显示具体界面的帧,在滑动时可以看到它们的差别。

要通过RadioGroup来实现底部导航栏的效果,我们可以利用RadioButton的`android:button="@null"`这个属性来把圆形按钮去掉,然后利用`android:drawableTop="@mipmap/bookshelf"`属性来给RadioButton显示不同的图片,在RadioButton被选中时,还需要设置字体的颜色以及改变图片颜色,以达到被选中的效果。RadioButton的选中与ViewPager切换帧需要结合起来,才能达到导航栏的效果。

```java
//处理底部RG事件
bottomRG.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
int index = 0;
switch (checkedId) {
case R.id.main_bottom_bookshelf:
index = 0;
//设置图片
break;
case R.id.main_bottom_ranking:
index = 1;
//设置图片
break;
case R.id.main_bottom_category:
index = 2;
//设置图片
break;
}
viewPager.setCurrentItem(index);
}
});
```

 

```java
//设置一个ViewPager的监听事件,左右滑动ViewPager时进行处理
public class MyPagerChangeListener implements ViewPager.OnPageChangeListener {
@Override
public void onPageScrollStateChanged(int arg0) {
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageSelected(int arg0) {
switch (arg0) {
case 0:
title.setText("书架");
radioGroup.setVisibility(View.GONE);
bookshelfRB.setChecked(true);
break;
case 1:
title.setText("排行榜");
radioGroup.setVisibility(View.VISIBLE);
rankingRB.setChecked(true);
break;
case 2:
title.setText("分类");
radioGroup.setVisibility(View.GONE);
categoryRB.setChecked(true);
break;
}
}
}
```

 

#### 排行榜界面的设计

排行榜界面分男女,主界面上显示的是榜单,点击榜单可以进入到相应的具体排行榜。榜单的UI设计利用了CardView和RecyclerView,男生榜单和女生榜单写在同一个Fragment中。具体的排行榜界面分为标题栏、顶部导航栏和具体排行榜,每一个排行榜都写在一个Fragment中,利用传递的不同参数来填充不同的数据。

具体的排行榜是一个Activity,而不同的榜单(周榜、月榜、总榜)则是不同的Fragment,进入具体的排行榜时生成对应的榜单并传递参数:

```java
//排行榜的Fragment
for (int i = 0; i < 3; ++i) {
DetailCategoryFragment fragment = new DetailCategoryFragment();
Bundle bundle1 = new Bundle();
bundle1.putString("title", bundle.getString("title"));
bundle1.putBoolean("isRanking", isRanking);
bundle1.putBoolean("isMale", isMale);
if (i == 0) bundle1.putString("type", "周榜");
else if (i == 1) bundle1.putString("type", "月榜");
else bundle1.putString("type", "总榜");
fragment.setArguments(bundle1);
fragmentList.add(fragment);
}
```

导航栏的效果与主界面相同,不做赘述。

在生成不同的榜单时,则需要根据获取的参数来访问API,填充数据。

获取排行榜书单分为两步:

1. 获取全部的排行榜,根据所需要的榜单来获取到具体榜单的id
2. 根据具体榜单的id访问API,获取书籍信息并处理

**所有的网络访问都需要使用线程去完成**

具体的实现逻辑如下:

1. 首先检查网络连接是否正常

```java
//检查网络连接
ConnectivityManager connect = (ConnectivityManager)getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = connect.getActiveNetworkInfo();
if (info == null || !info.isAvailable()) {
Toast.makeText(getActivity(), "网络连接状况:未连接", Toast.LENGTH_LONG).show();
return;
}
```

2. 使用线程访问网络,获取全部排行榜

```java
AllRankingObj allRankingObj = bookService.getAllRankingObj();
if (allRankingObj == null || allRankingObj.isOk() == false) {
Looper.prepare();
Toast.makeText(getContext(), "获取数据失败", Toast.LENGTH_LONG).show();
Looper.loop();
Log.d("error", "获取全部排行榜失败");
return;
}
```

3. 获取具体榜单的id

```java
//获取具体榜单的id
if (isMale) { //男生
for (AllRankingObj.subClass subClass : allRankingObj.getMaleList()) {
if (subClass.getShortTitle().equals(title)) {
if (title.equals("热搜榜")) rankingid = subClass.getId();
else {
if (type.equals("周榜")) rankingid = subClass.getId();
else if (type.equals("月榜")) rankingid = subClass.getMonthRank();
else if (type.equals("总榜")) rankingid = subClass.getTotalRank();
else {
System.exit(1);
Log.d("error", "榜单名错误!");
}
}
break;
}
}
}
else { //女生
//省略
}
```

4. 在线程中新开一个线程,再次访问网络,获取书籍

```java
//得到id后再获取具体榜单的书籍信息
new Thread(new Runnable() {
@Override
public void run() {
SingleRankingObj singleRankingObj = bookService.getSingleRankingObj(rankingid);
if (singleRankingObj == null || singleRankingObj.isOk() == false) {
Looper.prepare();
Toast.makeText(getContext(), "获取失败", Toast.LENGTH_LONG).show();
Looper.loop();
Log.d("error", "获取单一排行榜失败");
return;
}
List<BookObj> objList = singleRankingObj.getRanking().getBookList();
total = singleRankingObj.getRanking().getTotal();
for (int i = 0; i < objList.size(); ++i) {
BookObj bookObj = objList.get(i);
String intro = bookObj.getShortIntro();
if (intro.length() > 50) intro = intro.substring(0, 50);
intro += "...";
bookObj.setShortIntro(intro);
bookObjList.add(bookObj);
}
//使用handler
}
}).start();
```

5. 获取到书籍后需要填充RecyclerView的Adapter并显示,UI的修改无法在子线程中完成,因此需要使用Handler

```java
handler.post(new Runnable() {
@Override
public void run() {
updateRecyclerView(0, PAGE_COUNT);
progressBar.setVisibility(View.GONE);
//数据加载完毕时取消动画
swipeRefreshLayout.setRefreshing(false);
Toast.makeText(getActivity(), "数据刷新完成", Toast.LENGTH_SHORT).show();
}
});
```


获取到了数据之后,我们还需要把数据显示出来。

首先注意到榜单前3名有金、银、铜牌的标识,这简单地设置visible即可。

书籍条目的其他信息都是简单的填充,没有太多技术性问题。但图片的显示也需要网络访问,并且需要使用线程来异步访问,否则就会太过耗时。

```java
//通过网络获取书籍图标
final String iconURL = BookService.StaticsUrl + bookObj.getCover();
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(iconURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
if (connection.getResponseCode() == 200) {
InputStream inputStream = connection.getInputStream();
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
mHandler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(bitmap);
}
});
}
} catch (Exception e) {
System.err.println(e.getMessage());
}
}}).start();
```

 

#### 分类界面的设计

分类界面也分男生女生,并写在不同的Fragment里,这一点与排行榜界面不同。利用RecyclerView和Button来显示不同的分类。

具体分类界面的设计与具体排行榜界面很相似,因此我把它们的实现写在了同一个Activity中,实现了重用。

主要的不同之处还是体现在获取数据的方式上,获取具体分类的书籍并没有排行榜那么麻烦,根据具体类型即可访问API来获取:

```java
//获取具体分类书单
void getCateBookList() {
new Thread(new Runnable() {
@Override
public void run() {
String gender = (isMale == true) ? "male" : "female";
Log.d("type:", "" + type);
final CategoryObj categoryObj = bookService.getBooksByCategoty(type, title, 0, 30000, gender);
if (categoryObj == null || categoryObj.isOk() == false) {
Looper.prepare();
Toast.makeText(getContext(), "获取数据失败", Toast.LENGTH_LONG).show();
Looper.loop();
Log.d("error", "获取主题书单列表失败");
return;
}
total = categoryObj.getTotal();
for (BookObj bookObj : categoryObj.getBooks()) {
if (bookObj.getShortIntro().length() > 50){
String intro = bookObj.getShortIntro();
intro = intro.substring(0, 50);
intro += "...";
bookObj.setShortIntro(intro);
}
bookObjList.add(bookObj);
}
//handler
}
}).start();
```

 

#### 数据下拉刷新和上拉加载的实现

事实上,因为网络中的书籍太多,我们不可能一次性把API提供的所有书籍都获取出来,这样不仅耗时,而且浪费用户流量,因此分批次加载是必须的。鉴于此,我给排行榜和分类界面的书籍获取设计了下拉刷新和上拉加载。

下拉刷新比较简单,可以利用SwipeRefreshLayout来实现。

在布局中使用一个SwipeRefreshLayout包裹RecyclerView:

```xml
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/detail_category_swipeRefresh"
android:layout_height="match_parent"
android:layout_width="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/detail_category_recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"/>
</android.support.v4.widget.SwipeRefreshLayout>
```

SwipeRefreshLayout自带了下拉操作,因此我们只需要设置下拉刷新的回调事件即可:

```java
//设置下拉显示的动画颜色
swipeRefreshLayout.setColorSchemeColors(Color.RED, Color.BLUE);
//下拉刷新的回调事件
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//数据刷新
initBookList();
}
});
```

initBookList函数重新加载书籍,实现刷新效果。

上拉加载的实现思路则是给RecyclerView增加一个显示的条目类型,即底部提示的View,平时这个View都是隐藏的,在上拉到底部时就会显示出来,并根据加载进度显示“正在加载更多”或者“没有更多数据了”。

为了实现上拉加载,我重新自定义了一个RecyclerView的Adapter,一些重要的变量及函数如下:

```java
private int normalType = 0; // 第一种ViewType,正常的item
private int footType = 1; // 第二种ViewType,底部的提示View

private boolean hasMore = true; // 变量,是否有更多数据
private boolean fadeTips = false; // 变量,是否隐藏了底部的提示

private Handler mHandler = new Handler(Looper.getMainLooper()); //获取主线程的Handler

public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
//根据不同的ViewType绑定不同的布局文件
if (viewType == normalType) {
MyViewHolder holder = MyViewHolder.get(context, viewGroup, R.layout.item_book);
holder.setNormalTypeType(true);
return holder;
}
else {
MyViewHolder holder = MyViewHolder.get(context, viewGroup, R.layout.recycler_footer_view);
holder.setNormalTypeType(false);
return holder;
}
}
```

```java

public void onBindViewHolder(@NonNull final MyViewHolder holder, int position) {
if (data.isEmpty()) return;
if (holder.isNormalType() == true) {//是正常的布局
//显示书籍条目信息
}
else {//是提示加载信息的布局
final ProgressBar progressBar = holder.getView(R.id.footer_view_progessBar);
final TextView tips = holder.getView(R.id.footer_view_text);
tips.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE);
// 只有获取数据为空时,hasMore为false,所以当我们拉到底部时基本都会首先显示“正在加载更多...”
if (hasMore == true) {
// 不隐藏footView提示
fadeTips = false;
if (data.size() > 0) {
// 如果查询数据发现增加之后,就显示正在加载更多数据
tips.setText("正在加载更多数据...");
progressBar.setVisibility(View.VISIBLE);
}
} else {
if (data.size() > 0) {
// 如果查询数据发现并没有增加时,就显示没有更多数据了
tips.setText("没有更多数据了");
// 隐藏提示条
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 隐藏提示条
tips.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
// 将fadeTips设置true
fadeTips = true;
// hasMore设为true是为了让再次拉到底时,会先显示正在加载更多
hasMore = true;
}
}, 500);
}
}
}
//设置监听器
if (onItemClickListener != null) {
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onItemClickListener.onClick(holder.getAdapterPosition());
}
});
}
}
```

在这里,显示提示加载信息的布局则是在滑到底部时显示出来的提示信息条目。

```java
@Override
public int getItemCount() {
return data.size() + 1;
}

//获取列表中数据源的最后一个位置
public int getLastPosition() {
return data.size();
}
```

需要注意的是,`getItemCount`获取到的是数据data.size() + 1,因为实际的条目还有一个提示条目,而`getLastPosition()`则是实际数据源的最后一个位置。

```java
//更新数据源,并修改hasMore的值,如果有增加数据,hasMore为true,否则为false
public void updateList(List<BookObj> newDatas, boolean hasMore) {
// 在原有的数据之上增加新数据
if (newDatas != null) {
data.addAll(newDatas);
}
this.hasMore = hasMore;
notifyDataSetChanged();
}
```

在Fragment中,需要设置RecyclerView的滑动监听事件来实现上拉刷新:

```java
//设置滑动监听器
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// 在newState为滑到底部时
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// 如果没有隐藏footView,那么最后一个条目的位置就比我们的getItemCount少1
if (recyclerViewAdapter.isFadeTips() == false && lastVisibleItem + 1 == recyclerViewAdapter.getItemCount()) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
// 然后调用updateRecyclerview方法更新RecyclerView
updateRecyclerView(recyclerViewAdapter.getLastPosition(), recyclerViewAdapter.getLastPosition() + PAGE_COUNT);
}
}, 500);
}

// 如果隐藏了提示条,我们又上拉加载时,那么最后一个条目就要比getItemCount要少2
if (recyclerViewAdapter.isFadeTips() == true && lastVisibleItem + 2 == recyclerViewAdapter.getItemCount()) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
// 然后调用updateRecyclerview方法更新RecyclerView
updateRecyclerView(recyclerViewAdapter.getLastPosition(), recyclerViewAdapter.getLastPosition() + PAGE_COUNT);
}
}, 500);
}
}
}

@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 在滑动完成后,拿到最后一个可见的item的位置
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
lastVisibleItem = layoutManager.findLastVisibleItemPosition();
}
});
```

# 六.所遇到的问题以及解决办法

1. 书籍的简介过长导致搜索结果列表布局变形,后来设置当简介超过50字时后面的内容以“......”代替,这样布局就正常了。
2. 搜索输入速度太快会导致闪退,原因是新开线程,同步问题,有的线程内清空List,而有的线程正在调用List里的元素,导致出问题,加上同步变量即可,属于操作系统中简单的生产者消费者问题。
3. 数据库类改变类的架构之后没有在数据库的表中加入对应的属性,导致从数据库导入数据到list的时候出了问题,加上新增的属性即可。
4. 用ConstraintLayout作为recylerView的布局格式底下会被导航栏遮挡,改成LinearLayout之后就正常显示了。
5. 网络断开之后会发生闪退,因为api获取不到东西了,需要加上联网判断,先确定有网络连接在通过api获取数据。
6. api崩溃,中间出现过一次api失效,加入api崩溃检测,如果崩溃,弹Toast提示,就不会发生因api失效的莫名闪退,起码知道问题出在哪儿。
7. 主线程使用数据更新UI,但实际上还未获取到数据。
BookService中对每个对象维护一个实例,接口函数中使用Response<T>.body()给对象赋值,确保返回对象时已经获取到数据。可以采用新建一个线程,在新线程中调用BookService提供的函数。
8.用RadioGroup实现导航栏时需要在监听事件中动态改变RadioButton的图片,由于图片大小未知,直接使用会导致大小不适配。可以利用`Drawable.setBounds`函数和`RadioButton.setCompoundDrawables`函数来实现,可以设置使用图片的长和宽:

# 七.运行界面
![在这里插入图片描述](https://img-blog.csdnimg.cn/01875b4d25f64992a6025bb33a29c8a0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_7,color_FFFFFF,t_70,g_se,x_16)

![在这里插入图片描述](https://img-blog.csdnimg.cn/1b752ed54ca94ac89765a90d2a7e625d.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_7,color_FFFFFF,t_70,g_se,x_16)
![在这里插入图片描述](https://img-blog.csdnimg.cn/67d23519a5d747ec846d2f6c33a6fd5b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_10,color_FFFFFF,t_70,g_se,x_16)
![在这里插入图片描述](https://img-blog.csdnimg.cn/440252f0c86c4731ae2fa883182986d4.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Yqq5Yqb5a2mSmF2YeeahOiPnOeLl-W8n-W8nw==,size_7,color_FFFFFF,t_70,g_se,x_16)

 

相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类
点击右上角即可分享
微信分享提示