Android
webView
ChromeCustomTabs

WebViewでページ内検索

先日、Qiitappにページ内検索機能を追加しました。

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

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

スクリーンショット

Chrome Custom Tabs版

ss_chromecustomtabs.png 

WebView版(Qiitappとは異なります)

ss_webview.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

0.全体

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

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.xmlactionModeCloseButtonStyleにtintを指定して色を変更します。

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とマッチ数表示用のTextViewLinearLayoutで囲んでActionMode#setCustomViewに指定します(ここではそのレイアウトのみ)。

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>
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>
values/colors.xml
<resources>
    ...
    <color name="colorCursor">#4285F4</color>
    ...
</resources>
drawable/cursor.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <size android:width="2dp" />
    <solid android:color="@color/colorCursor" />
</shape>

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

3.OptionsMenu

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

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>
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を登録する必要があります。
あとはこれらをActionModeTextWatcherなどのイベントに合わせて呼ぶだけです。

Activity#onCreateなど
mWebView.setFindListener(this);  // FindListenerの設定
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;
}
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);
}
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;
}
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にあります。