为妈妈开发一个《圣经》语录搜索APP,填空再也不用愁啦!

3,675 阅读4分钟

前言

我们这的教会中有一本小册子,此册子非比册子,全书是用来填空的,就和我们做的填空题一样,而它的答案是来自《圣经》一书中的内容,看过《圣经》一书的人应该知道内容非常多,分为新约和旧约,而要在其中找出某段话在哪一章,除非是熟读《圣经》的人,否则只能靠百度了,但是百度有时候也不能快速的找出来。

所以最后为了简单高效的填完这这个册子,准备做一个针对《圣经》搜索的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,我们需要了解他几个类的作用。

  1. Document:

    代表一个HTML全文

  2. Elements

    代表多个HTML标签元素,他继承了ArrayList,存放的内容是Element

  3. 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);
            }
        });
    }
}


效果

下载地址:houxinlin.com/圣经搜索.apk