前言
我们这的教会中有一本小册子,此册子非比册子,全书是用来填空的,就和我们做的填空题一样,而它的答案是来自《圣经》一书中的内容,看过《圣经》一书的人应该知道内容非常多,分为新约和旧约,而要在其中找出某段话在哪一章,除非是熟读《圣经》的人,否则只能靠百度了,但是百度有时候也不能快速的找出来。
所以最后为了简单高效的填完这这个册子,准备做一个针对《圣经》搜索的APP,一开始想下载《圣经》全文的txt,然后在其中搜索,奈何这玩意儿还不好下载,不是广告就是在广告的路上,下载了几个和《圣经》书中的还不对应,但是无意中发现有些《圣经》在线阅读网站就支持搜索。
那这回就省了好多事了,直接通过OKHTTP发起请求,然后通过JSOUP去解析HTML内容即可。这里是因为网站是后端生成的数据,渲染完成后将整个HTML返回给浏览器,没有json数据,所以只能解析HTML来获取数据了。
OKHTTP
不管在后端还是移动端中,都可能会用到OKHTTP,他是由Square公司贡献的,优点非常多,如缓存响应数据,以此来减少重复的网络请求,共享Socket,减少对服务器的请求次数,它的API也简单好用,下面是发起一个GET请求并获取响应的数据,对于本程序,下面这个代码就够用了。
OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
下面是发起一个POST请求,并且携带json参数。
public static final MediaType JSON
= MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
JSOUP
jsoup是用来解析HTML文档,当我们通过OkHttp发起请求后,拿到响应数据,需要通过他来从指定标签中取到我们要的数据,他还可以直接通过选择器来选择指定的标签。
要使用jsoup,我们需要了解他几个类的作用。
-
Document:
代表一个HTML全文
-
Elements
代表多个HTML标签元素,他继承了ArrayList,存放的内容是Element
-
Element
代表某个HTML标签,通过Element,就可以获取这个标签中的属性和内容。
要获得Document,可以通过Jsoup的静态方法parse,参数是html全文字符,其实Jsoup也可以直接发起GET请求,不需要使用OKHTTP,他有个重载方法是parse(URL url, int timeoutMillis),可以发起请求,并转换为Document,但是测试的时候,不如OKHTTP快,而我们的程序可能在短时间发起很多请求,所以就不去使用了。
Document parse = Jsoup.parse(body);
Elements select = parse.select(".bookbody>div");
分析HTML格式
这个格式很简单,父容器类名是.bookbody,子容器是没有任何名称的div,每个div就是一行,行中有两个标签,a标签是章节信息,span是内容,所以,我们只要通过JSOUP拿到.bookbody下的所有div,依次遍历他,从每个item中取出a标签和span标签的内容就可以了。
Android 界面设计
因为这里使用了dataBinding,所以根节点是layout。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center|left"
android:orientation="horizontal">
<TextView
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="圣经搜索"
android:textColor="#373434"
android:textSize="26sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.hxl.shengjing.LoadImageView
android:id="@+id/iv_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="10dp"></com.example.hxl.shengjing.LoadImageView>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ListView
android:id="@+id/lv_result"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:divider="@null"></ListView>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70dp">
<EditText
android:id="@+id/et_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/shape_editview"
android:hint="输入关键段落"
android:padding="10dp"
android:textColor="#000000"
android:textSize="18sp"></EditText>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="15dp"></corners>
<solid android:color="#ffffff"></solid>
<stroke android:width="3dp" android:color="#03a9f4"></stroke>
</shape>
大概效果如下:
没有搜索按钮是因为每输入一个字就会进行搜索。
还有一个自定义View,加载动画。
public class LoadImageView extends AppCompatImageView {
private int rotation = 0;
private int speed = 2;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
rotation = rotation >= 360 ? 0 : rotation + speed;
LoadImageView.this.setRotation(rotation);
mHandler.postDelayed(this, 10);
}
};
public LoadImageView(@NonNull Context context) {
this(context, null);
}
public LoadImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setBackgroundResource(R.drawable.ic_loading);
mHandler.postDelayed(mRunnable, 10);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeCallbacks(mRunnable);
}
}
逻辑设计
public class MainActivity extends AppCompatActivity {
private static final String TAG = "TAG";
ActivityMainBinding mActivityMainBinding;
private SearchAdapter mSearchAdapter;
private List<ItemBean> mItemBeanList = new ArrayList<>();
private OkHttpClient mOkHttpClient = new OkHttpClient();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
StatusBarUtil.setLightMode(this);
StatusBarUtil.setTransparent(this);
mSearchAdapter = new SearchAdapter(mItemBeanList, this);
mActivityMainBinding.lvResult.setAdapter(mSearchAdapter);
mActivityMainBinding.ivLoading.setVisibility(View.GONE);
mActivityMainBinding.etContent.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
search(editable.toString());
}
});
}
private synchronized void search(String editable) {
mActivityMainBinding.ivLoading.setVisibility(View.VISIBLE);
Request.Builder builder = new Request.Builder()
.url("http://www.xxxxxxx.com/search?key=" + URLEncoder.encode(editable))
.get();
mOkHttpClient.newCall(builder.build()).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
parse(response.body().string());
}
});
}
private boolean isEmpty(ItemBean itemBean) {
return "".equals(itemBean.getZhangjie()) || "".equals(itemBean.getContent());
}
private void parse(String body) {
Document parse = Jsoup.parse(body);
Elements select = parse.select(".bookbody>div");
mItemBeanList.clear();
for (Element element : select) {
ItemBean itemBean = new ItemBean(element.select("a").html(), element.select("span").html());
if (!isEmpty(itemBean))
mItemBeanList.add(itemBean);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mSearchAdapter.notifyDataSetChanged();
mActivityMainBinding.ivLoading.setVisibility(View.GONE);
}
});
}
}