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!