Conversion de EditText en SearchEditText

image



Avez-vous déjà essayé de personnaliser l'apparence ou le comportement du composant SearchView standard? Je suppose. Dans ce cas, je pense que vous conviendrez que tous ses paramètres ne sont pas suffisamment flexibles pour satisfaire toutes les exigences métier d'une seule tâche. L'un des moyens de résoudre ce problème est d'écrire votre propre SearchView «personnalisé», ce que nous allons faire aujourd'hui. Aller!



Remarque: la vue créée (ci-après - SearchEditText ) n'aura pas toutes les propriétés du SearchView standard. Si nécessaire, vous pouvez facilement ajouter des options supplémentaires pour des besoins spécifiques.



Plan d'action



Il y a plusieurs choses que nous devons faire pour "transformer" un EditText en SearchEditText. En bref, nous avons besoin de:



  • Hériter de SearchEditText de AppCompatEditText
  • Ajoutez une icône "Rechercher" dans le coin gauche (ou droit) de SearchEditText, en cliquant sur lequel la requête de recherche saisie sera transmise à l'auditeur enregistré
  • Ajoutez une icône "Nettoyage" dans le coin droit (ou gauche) de SearchEditText, lorsque vous cliquez sur lequel, le texte saisi dans la barre de recherche sera effacé
  • Définissez le paramètre imeOptions SearchEditText sur IME_ACTION_SEARCH, de sorte que lorsque le clavier apparaît, le bouton de saisie de texte se comporte comme le bouton "Rechercher"


SearchEditText dans toute sa splendeur!



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?)
    }

}


Dans le code ci-dessus, deux fonctions d'extension ont été utilisées pour définir l'image droite et gauche de EditText. Ces deux fonctions ressemblent à ceci:



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
    )

}


Héritage de AppCompatEditText



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


Comme vous pouvez le voir, à partir du constructeur écrit, nous transmettons tous les paramètres nécessaires au constructeur AppCompatEditText. Le point important ici est que le defStyle par défaut est android.appcompat.R.attr.editTextStyle. Héritant de LinearLayout, FrameLayout et d'autres vues, nous avons tendance à utiliser 0 comme valeur par défaut pour defStyle. Cependant, dans notre cas, cela ne convient pas, sinon notre SearchEditText se comportera comme un TextView, et non comme un EditText.



Traitement des modifications de texte



La prochaine chose que nous devons faire est «d'apprendre» comment répondre aux événements de changement de texte dans notre SearchEditText. Nous en avons besoin pour deux raisons:



  • afficher ou masquer l'icône d'effacement selon que le texte est saisi
  • notification à l'auditeur de modifier le texte dans SearchEditText


Regardons le code de l'auditeur:



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


Pour gérer les événements de modification de texte, la fonction d'extension doAfterTextChanged de androidx.core: core-ktx a été utilisée.



Gérez le clic du bouton Entrée sur le clavier



Lorsque l'utilisateur appuie sur la touche Entrée du clavier, une vérification est effectuée pour voir si l'action est IME_ACTION_SEARCH. Si tel est le cas, nous informons l'auditeur de cette action et lui transmettons le texte de SearchEditText. Voyons comment cela se produit.



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


Gestion des clics sur les icônes



Et enfin, la dernière question, mais non la moindre: comment gérer le clic sur les icônes de recherche et le texte clair. Le hic ici est que, par défaut, les drawables du EditText standard ne répondent pas aux événements de clic, ce qui signifie qu'il n'y a pas d'auditeur officiel qui pourrait les gérer.



Pour résoudre ce problème, un OnTouchListener a été enregistré dans SearchEditText. Au toucher, en utilisant les fonctions leftDrawableClicked et rightDrawableClicked, nous pouvons désormais gérer le clic sur les icônes. Jetons un coup d'œil au code:



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
    }

}


Les fonctions leftDrawableClicked et RightDrawableClicked n'ont rien de compliqué. Prenons le premier, par exemple. Pour l'icône de gauche, nous calculons d'abord startOfDrawable et endOfDrawable, puis vérifions si la coordonnée x du point de contact est dans la plage [startofDrawable, endOfDrawable]. Si oui, cela signifie que l'icône de gauche a été enfoncée. La fonction rightDrawableClicked fonctionne de la même manière.



Selon que vous appuyez sur l'icône gauche ou droite, nous effectuons certaines actions. Lorsque nous cliquons sur l'icône de gauche (icône de recherche), nous en informons l'auditeur en appelant sa fonction onQueryTextSubmit. Lorsque vous cliquez sur celui de droite, nous effaçons le texte SearchEditText.



Production



Dans cet article, nous avons examiné l'option de «transformer» un EditText standard en un SearchEditText plus avancé. Comme mentionné précédemment, la solution prête à l'emploi ne prend pas en charge toutes les options fournies par SearchView, cependant, vous pouvez l'améliorer à tout moment en ajoutant des options supplémentaires à votre discrétion. Fonce!



PS:

Vous pouvez accéder au code source de SearchEditText à partir de ce référentiel GitHub.



All Articles