EditTextをSearchEditTextに変換する

画像



標準のSearchViewコンポーネントの外観や動作をカスタマイズしようとしたことがありますか?たぶんそうだ。この場合、すべての設定が1つのタスクのすべてのビジネス要件を満たすのに十分な柔軟性を備えているわけではないことに同意していただけると思います。この問題を解決する方法の1つは、独自の「カスタム」SearchViewを作成することです。これは、本日行います。行く!



注:作成されたビュー(以下、SearchEditText)には、標準のSearchViewのすべてのプロパティが含まれるわけではありません。必要に応じて、特定のニーズに合わせてオプションを簡単に追加できます。



行動計画



EditTextをSearchEditTextに「変換」するために必要なことがいくつかあります。つまり、次のものが必要です。



  • AppCompatEditTextからSearchEditTextを継承します
  • 入力した検索クエリが登録済みのリスナーに送信されるクリック時に、SearchEditTextの左(または右)コーナーに「検索」アイコンを追加します
  • SearchEditTextの右(または左)コーナーに「クリーンアップ」アイコンを追加します。これをクリックすると、検索バーに入力されたテキストがクリアされます。
  • imeOptions SearchEditTextパラメーターをIME_ACTION_SEARCHに設定して、キーボードが表示されたときにテキスト入力ボタンが「検索」ボタンとして機能するようにします。


SearchEditTextの栄光!



import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View.OnTouchListener
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.widget.doAfterTextChanged

class SearchEditText
@JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyle: Int = androidx.appcompat.R.attr.editTextStyle
) : AppCompatEditText(context, attributeSet, defStyle) {

    init {
        setLeftDrawable(android.R.drawable.ic_menu_search)
        setTextChangeListener()
        setOnEditorActionListener()
        setDrawablesListener()
        imeOptions = EditorInfo.IME_ACTION_SEARCH
    }

    companion object {
        private const val DRAWABLE_LEFT_INDEX = 0
        private const val DRAWABLE_RIGHT_INDEX = 2
    }

    private var queryTextListener: QueryTextListener? = null

    private fun setTextChangeListener() {
        doAfterTextChanged {
            if (it.isNullOrBlank()) {
                setRightDrawable(0)
            } else {
                setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)
            }
            queryTextListener?.onQueryTextChange(it.toString())
        }
    }
    
    private fun setOnEditorActionListener() {
        setOnEditorActionListener { _, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                queryTextListener?.onQueryTextSubmit(text.toString())
                true
            } else {
                false
            }
        }
    }
    
    private fun setDrawablesListener() {
        setOnTouchListener(OnTouchListener { view, event ->
            view.performClick()
            if (event.action == MotionEvent.ACTION_UP) {
                when {
                    rightDrawableClicked(event) -> {
                        setText("")
                        return@OnTouchListener true
                    }
                    leftDrawableClicked(event) -> {
                        queryTextListener?.onQueryTextSubmit(text.toString())
                        return@OnTouchListener true
                    }
                    else -> {
                        return@OnTouchListener false
                    }
                }
            }
            false
        })
    }

    private fun rightDrawableClicked(event: MotionEvent): Boolean {

        val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]

        return if (rightDrawable == null) {
            false
        } else {
            val startOfDrawable = width - rightDrawable.bounds.width() - paddingRight
            val endOfDrawable = startOfDrawable + rightDrawable.bounds.width()
            startOfDrawable <= event.x && event.x <= endOfDrawable
        }

    }

    private fun leftDrawableClicked(event: MotionEvent): Boolean {

        val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]

        return if (leftDrawable == null) {
            false
        } else {
            val startOfDrawable = paddingLeft
            val endOfDrawable = startOfDrawable + leftDrawable.bounds.width()
            startOfDrawable <= event.x && event.x <= endOfDrawable
        }

    }

    fun setQueryTextChangeListener(queryTextListener: QueryTextListener) {
        this.queryTextListener = queryTextListener
    }

    interface QueryTextListener {
        fun onQueryTextSubmit(query: String?)
        fun onQueryTextChange(newText: String?)
    }

}


上記のコードでは、2つの拡張関数を使用してEditTextの左右の画像を設定しました。これらの2つの関数は次のようになります。



import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat

private const val DRAWABLE_LEFT_INDEX = 0
private const val DRAWABLE_TOP_INDEX = 1
private const val DRAWABLE_RIGHT_INDEX = 2
private const val DRAWABLE_BOTTOM_INDEX = 3

fun TextView.setLeftDrawable(@DrawableRes drawableResId: Int) {

    val leftDrawable = if (drawableResId != 0) {
        ContextCompat.getDrawable(context, drawableResId)
    } else {
        null
    }
    val topDrawable = compoundDrawables[DRAWABLE_TOP_INDEX]
    val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]
    val bottomDrawable = compoundDrawables[DRAWABLE_BOTTOM_INDEX]

    setCompoundDrawablesWithIntrinsicBounds(
        leftDrawable,
        topDrawable,
        rightDrawable,
        bottomDrawable
    )

}

fun TextView.setRightDrawable(@DrawableRes drawableResId: Int) {

    val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]
    val topDrawable = compoundDrawables[DRAWABLE_TOP_INDEX]
    val rightDrawable = if (drawableResId != 0) {
        ContextCompat.getDrawable(context, drawableResId)
    } else {
        null
    }
    val bottomDrawable = compoundDrawables[DRAWABLE_BOTTOM_INDEX]

    setCompoundDrawablesWithIntrinsicBounds(
        leftDrawable,
        topDrawable,
        rightDrawable,
        bottomDrawable
    )

}


AppCompatEditTextから継承



class SearchEditText
@JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyle: Int = androidx.appcompat.R.attr.editTextStyle
) : AppCompatEditText(context, attributeSet, defStyle)


ご覧のとおり、記述されたコンストラクターから、必要なすべてのパラメーターをAppCompatEditTextコンストラクターに渡します。ここで重要な点は、デフォルトのdefStyleがandroid.appcompat.R.attr.editTextStyleであるということです。LinearLayout、FrameLayout、およびその他のいくつかのビューから継承して、defStyleのデフォルトとして0を使用する傾向があります。ただし、この場合、これは適切ではありません。そうしないと、SearchEditTextはEditTextではなく、TextViewのように動作します。



テキスト変更の処理



次に行う必要があるのは、SearchEditTextでテキスト変更イベントに応答する方法を「学習」することです。これが必要な理由は2つあります。



  • テキストが入力されているかどうかに応じて、クリアアイコンを表示または非表示にします
  • SearchEditTextのテキストを変更するようリスナーに通知する


リスナーコードを見てみましょう。



private fun setTextChangeListener() {
    doAfterTextChanged {
        if (it.isNullOrBlank()) {
            setRightDrawable(0)
        } else {
            setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)
        }
        queryTextListener?.onQueryTextChange(it.toString())
    }
}


テキスト変更イベントを処理するために、androidx.core:core-ktxのdoAfterTextChanged拡張関数が使用されました。



キーボードのEnterボタンのクリックを処理します



ユーザーがキーボードのEnterキーを押すと、アクションがIME_ACTION_SEARCHであるかどうかが確認されます。その場合、リスナーにこのアクションについて通知し、SearchEditTextからのテキストを渡します。これがどのように起こるか見てみましょう。



private fun setOnEditorActionListener() {
    setOnEditorActionListener { _, actionId, _ ->
        if (actionId == EditorInfo.IME_ACTION_SEARCH) {
            queryTextListener?.onQueryTextSubmit(text.toString())
            true
        } else {
            false
        }
    }
}


アイコンクリックの処理



そして最後に、最後に重要な質問です。検索アイコンとクリアテキストのクリックを処理する方法です。ここでの落とし穴は、デフォルトでは、標準のEditTextのドローアブルがクリックイベントに応答しないことです。つまり、それらを処理できる公式のリスナーは存在しません。



この問題を解決するために、OnTouchListenerがSearchEditTextに登録されました。タッチすると、関数leftDrawableClickedおよびrightDrawableClickedを使用して、アイコンのクリックを処理できるようになりました。コードを見てみましょう:



private fun setDrawablesListener() {
    setOnTouchListener(OnTouchListener { view, event ->
        view.performClick()
        if (event.action == MotionEvent.ACTION_UP) {
            when {
                rightDrawableClicked(event) -> {
                    setText("")
                    return@OnTouchListener true
                }
                leftDrawableClicked(event) -> {
                    queryTextListener?.onQueryTextSubmit(text.toString())
                    return@OnTouchListener true
                }
                else -> {
                    return@OnTouchListener false
                }
            }
        }
        false
    })
}

private fun rightDrawableClicked(event: MotionEvent): Boolean {

    val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]

    return if (rightDrawable == null) {
        false
    } else {
        val startOfDrawable = width - rightDrawable.bounds.width() - paddingRight
        val endOfDrawable = startOfDrawable + rightDrawable.bounds.width()
        startOfDrawable <= event.x && event.x <= endOfDrawable
    }

}

private fun leftDrawableClicked(event: MotionEvent): Boolean {

    val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]

    return if (leftDrawable == null) {
        false
    } else {
        val startOfDrawable = paddingLeft
        val endOfDrawable = startOfDrawable + leftDrawable.bounds.width()
        startOfDrawable <= event.x && event.x <= endOfDrawable
    }

}


関数leftDrawableClickedとRightDrawableClickedについて複雑なことは何もありません。たとえば、最初のものを取り上げます。左側のアイコンの場合、最初にstartOfDrawableとendOfDrawableを計算してから、タッチポイントのx座標が[startofDrawable、endOfDrawable]の範囲内にあるかどうかを確認します。はいの場合、左のアイコンが押されたことを意味します。rightDrawableClicked関数も同様に機能します。



左アイコンと右アイコンのどちらが押されたかに応じて、特定のアクションを実行します。左側のアイコン(検索アイコン)をクリックすると、onQueryTextSubmit関数を呼び出してリスナーに通知します。右側をクリックすると、SearchEditTextテキストがクリアされます。



出力



この記事では、標準のEditTextをより高度なSearchEditTextに「変換」するオプションについて説明しました。前述のように、すぐに使用できるソリューションはSearchViewが提供するすべてのオプションをサポートしているわけではありませんが、任意のオプションを追加することでいつでも改善できます。頑張れ!



PS:

このGitHubリポジトリからSearchEditTextのソースコードにアクセスできます。



All Articles