WebViewでページ内検索

先日、[Qiitapp](https://qiita.com/alzybaad/items/727a8b4f5cde669d8a23)にページ内検索機能を追加しました。

要望はあったのですが、QiitappはAPIから取得したHTMLをローカルに保存して表示するため、[Chrome Custom Tabs](https://developer.chrome.com/multidevice/android/customtabs)は使えません。また、`WebView`でのページ内検索は`Chrome Custom Tabs`のようなUIにはできないだろう、と勝手に思い込んでいて(あまり調べてなかった)、対応に消極的でした。

やってみたところ、割と簡単にまぁまぁなUIにできたので、一例として紹介します。

# スクリーンショット
## Chrome Custom Tabs版
![ss_chromecustomtabs.png](https://qiita-image-store.s3.amazonaws.com/0/22848/828f72d3-e267-96b9-65ad-0c29d8da5482.png)　

## WebView版(Qiitappとは異なります)
![ss_webview.png](https://qiita-image-store.s3.amazonaws.com/0/22848/3a7cf228-c10d-a97d-4e01-3973a52dbd5d.png)

見ての通り、`Chrome Custom Tabs`版と`WebView`版の違いは、スクロールバー部分と`ActionBar`部分です。残念ながら、スクロールバー部分はできていないため、この先出てきません。

# 環境
* Android 5.0(APIレベル21)以上
* Support Library
    * appcompat-v7 26.1.0
    * customtabs 26.1.0

もう少し低くても動くと思います。

# WebView版の実装(XML)
`ActionBar`部分は以下のように分けられます。
![3.png](https://qiita-image-store.s3.amazonaws.com/0/22848/4f9d023f-e816-3157-3f1e-d6f3c28d97a9.png)

## 0.全体
`Contextual Action Bar`を使用しています。
`Contextual Action Bar`の背景を`Chrome Custom Tabs`と同じ白色にするため、`styles.xml`に`actionModebackground`を追加します。

```xml:values/styles.xml
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ...
    <item name="actionModeBackground">@android:color/white</item>
    ...
</style>
```

## 1.actionModeCloseButtonStyle
`Contextual Action Bar`左部の閉じるボタンアイコンはデフォルトで白色のため、上記0の対応の結果見えなくなります。そのため、`Chrome Custom Tabs`と合わせてグレーにします(値は適当)。
`styles.xml`の`actionModeCloseButtonStyle`にtintを指定して色を変更します。

```xml:values/styles.xml
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    ...
    <item name="actionModeCloseButtonStyle">@style/ActionModeCloseMode</item>
    ...
</style>

<style name="ActionModeCloseMode" parent="Widget.AppCompat.Light.ActionButton.CloseMode">
    <item name="android:tint">#9E9E9E</item>
</style>
```

## 2.ActionMode#setCustomView
検索文字列入力用の`EditText`とマッチ数表示用の`TextView`は`LinearLayout`で囲んで`ActionMode#setCustomView`に指定します(ここではそのレイアウトのみ)。

```xml:layout/activity_web_view_cab.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <EditText
        android:id="@+id/query"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        android:hint="@string/find_in_page"
        android:background="@android:color/transparent"
        android:theme="@style/FindInPageEditText" />

    <TextView
        android:id="@+id/count"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
</LinearLayout>
```

```xml:values/styles.xml
<style name="FindInPageEditText">
    <item name="android:maxLines">1</item>
    <item name="android:inputType">text</item>
    <item name="android:imeOptions">actionSearch</item>
    <item name="android:colorControlActivated">@color/colorCursor</item>
    <item name="android:textCursorDrawable">@drawable/cursor</item>
</style>
```

```xml:values/colors.xml
<resources>
    ...
    <color name="colorCursor">#4285F4</color>
    ...
</resources>
```

```xml:drawable/cursor.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <size android:width="2dp" />
    <solid android:color="@color/colorCursor" />
</shape>
```

`EditText`のスタイル設定は以下を参考にしました。

* [EditTextの色変更箇所まとめ](https://qiita.com/konifar/items/47d467e3574c9228fdc8)
* [EditTextのカーソル色変える時の注意点](https://qiita.com/chiiia12/items/c02d00f7c4b70cf95a79)

## 3.OptionsMenu
`Contextual Action Bar`右部の前後の検索結果への移動ボタンは`OptionsMenu`にAndroidのMaterial iconを指定しています(上記1と同じ色を指定)。

```xml:menu/activity_web_view_find_in_page.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/previous"
        android:icon="@drawable/ic_keyboard_arrow_up"
        android:title="@string/previous" />
    <item
        android:id="@+id/next"
        android:icon="@drawable/ic_keyboard_arrow_down"
        android:title="@string/next" />
</menu>
```

```xml:drawable/ic_keyboard_arrow_up.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportHeight="24.0"
    android:viewportWidth="24.0">
    <path
        android:fillColor="#FF9E9E9E"
        android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z" />
</vector>
```

`drawable/ic_keyboard_arrow_down.xml`は省略。

# WebView版の実装(Java)
`WebView`の検索関連のAPIは、`WebView#findAllAsync`, `WebView#findNext`, `WebView#clearMatches`の3つ。名前からわかりますが、それぞれ、全検索、次検索、強調表示の解除です。
全検索は非同期(同期版はAPIレベル16でdeprecated)となっており、検索結果(総数、現在のフォーカス位置)を受け取りたい場合は`WebView.FindListener`を登録する必要があります。
あとはこれらを`ActionMode`や`TextWatcher`などのイベントに合わせて呼ぶだけです。

```java:Activity#onCreateなど
mWebView.setFindListener(this);  // FindListenerの設定
```

```java:ActionMode.Callback
// TextWatcherの指定
// TextView. OnEditorActionListenerの指定
// カスタムビューの作成・ActionMode#setCustomViewの呼び出し
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    mActionMode = mode;

    getMenuInflater().inflate(R.menu.activity_web_view_find_in_page, menu);

    final View view = getLayoutInflater().inflate(R.layout.acitivity_web_view_cab, null);
    mQueryView = view.findViewById(R.id.query);
    mCountView = view.findViewById(R.id.count);
    mDefaultTextColor = mCountView.getCurrentTextColor();
    mQueryView.addTextChangedListener(this);
    mQueryView.setOnEditorActionListener(this);

    mode.setCustomView(view);
    return true;
}

// 前方検索・後方検索
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
    switch (item.getItemId()) {
        case R.id.previous:
            mWebView.findNext(false);
            return true;
        case R.id.next:
            mWebView.findNext(true);
            return true;
        default:
            return false;
    }
}

// ActionMode終了による強調表示の解除
@Override
public void onDestroyActionMode(ActionMode mode) {
    mWebView.clearMatches();

    mActionMode = null;
}
```

```java:TextWatcher
// 検索(検索文字列未指定の場合は検索結果数用のViewを表示しない)
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
    mWebView.findAllAsync(s.toString());
    mCountView.setVisibility(count != 0 ? View.VISIBLE : View.GONE);
}
```

```java:TextView.OnEditorActionListener
// キーボードのSearchボタン押下時の前方検索
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
    if (actionId == EditorInfo.IME_ACTION_SEARCH) {
        mWebView.findNext(true);
        return true;
    }

    return false;
}
```

```java:WebView.FindListener
// 検索終了時に <現在の位置(0起算)>/<マッチ数> を表示
// マッチ数が0の場合は赤色表示
@Override
public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
    if (isDoneCounting) {
        mCountView.setTextColor(numberOfMatches != 0 ? mDefaultTextColor : Color.RED);
        mCountView.setText(getString(R.string.count, numberOfMatches == 0 ? activeMatchOrdinal : activeMatchOrdinal + 1, numberOfMatches));
    }
}
```

# ソース

[GitHub](https://github.com/alzybaad/FindInPageExample)にあります。