Conversión de EditText a SearchEditText

imagen



¿Alguna vez ha intentado personalizar la apariencia o el comportamiento del componente estándar de SearchView? Supongo que sí. En este caso, creo que estará de acuerdo en que no todas sus configuraciones son lo suficientemente flexibles para satisfacer todos los requisitos comerciales de una tarea en particular. Una de las formas de resolver este problema es escribir su propio SearchView "personalizado", lo que haremos hoy. ¡Vamos!



Nota: la vista creada (en adelante, SearchEditText ) no tendrá todas las propiedades del SearchView estándar. Si es necesario, puede agregar fácilmente opciones adicionales para necesidades específicas.



Plan de ACCION



Hay varias cosas que debemos hacer para "convertir" un EditText en un SearchEditText. En resumen, necesitamos:



  • Heredar SearchEditText de AppCompatEditText
  • Agregue un icono de "Buscar" en la esquina izquierda (o derecha) de SearchEditText, al hacer clic en el cual la consulta de búsqueda ingresada se transmitirá al oyente registrado
  • Agregue un ícono de "Limpieza" en la esquina derecha (o izquierda) de SearchEditText, cuando haga clic en el cual, el texto ingresado en la barra de búsqueda se borrará
  • Establezca el parámetro imeOptions SearchEditText en el valor IME_ACTION_SEARCH, de modo que cuando aparezca el teclado, el botón de entrada de texto actuará como el botón "Buscar"


SearchEditText en todo su esplendor!



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

}


En el código anterior, se usaron dos funciones de extensión para configurar la imagen derecha e izquierda del EditText. Estas dos funciones se ven así:



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
    )

}


Herencia de AppCompatEditText



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


Como puede ver, desde el constructor escrito, pasamos todos los parámetros necesarios al constructor AppCompatEditText. El punto importante aquí es que el defStyle predeterminado es android.appcompat.R.attr.editTextStyle. Heredando de LinearLayout, FrameLayout y algunas otras vistas, tendemos a usar 0 como valor predeterminado para defStyle. Sin embargo, en nuestro caso esto no es adecuado, de lo contrario nuestro SearchEditText se comportará como un TextView y no como un EditText.



Procesando cambios de texto



Lo siguiente que debemos hacer es "aprender" cómo responder a eventos de cambio de texto en nuestro SearchEditText. Necesitamos esto por dos razones:



  • mostrar u ocultar el icono claro dependiendo de si se ingresó el texto
  • notificar al oyente que cambie el texto en SearchEditText


Veamos el código del oyente:



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


Para manejar eventos de cambio de texto, se utilizó la función de extensión doAfterTextChanged de androidx.core: core-ktx.



Maneje el clic del botón Enter en el teclado



Cuando el usuario presiona la tecla Enter en el teclado, se realiza una verificación para ver si la acción es IME_ACTION_SEARCH. Si es así, informamos al oyente sobre esta acción y le pasamos el texto de SearchEditText. Veamos cómo sucede esto.



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


Manejo de clics en iconos



Y finalmente, la última, pero no menos importante, pregunta: cómo hacer clic en los íconos de búsqueda y el texto sin cifrar. El problema aquí es que, de forma predeterminada, los elementos de diseño del EditText estándar no responden a los eventos de clic, lo que significa que no hay un oyente oficial que pueda manejarlos.



Para resolver este problema, se registró un OnTouchListener en SearchEditText. Al tocar, usando las funciones leftDrawableClicked y rightDrawableClicked, ahora podemos manejar hacer clic en los iconos. Echemos un vistazo al código:



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
    }

}


No hay nada complicado en las funciones leftDrawableClicked y RightDrawableClicked. Tome el primero, por ejemplo. Para el icono de la izquierda, primero calculamos startOfDrawable y endOfDrawable y luego verificamos si la coordenada x del punto de contacto está en el rango [startofDrawable, endOfDrawable]. Si es así, significa que se presionó el icono de la izquierda. La función rightDrawableClicked funciona de manera similar.



Dependiendo de si se pulsa el icono de la izquierda o la derecha, realizamos determinadas acciones. Cuando hacemos clic en el icono de la izquierda (icono de búsqueda), informamos al oyente sobre esto llamando a su función onQueryTextSubmit. Al hacer clic en el de la derecha, borramos el texto SearchEditText.



Salida



En este artículo, analizamos la opción de "convertir" un EditText estándar en un SearchEditText más avanzado. Como se mencionó anteriormente, la solución lista para usar no es compatible con todas las opciones proporcionadas por SearchView; sin embargo, puede mejorarla en cualquier momento agregando opciones adicionales a su discreción. ¡Ve a por ello!



PD:

Puede acceder al código fuente de SearchEditText desde este repositorio de GitHub.



All Articles