
Você já tentou personalizar a aparência ou o comportamento do componente SearchView padrão? Eu acho. Nesse caso, acho que você concordará que nem todas as configurações são flexíveis o suficiente para satisfazer todos os requisitos de negócios de uma única tarefa. Uma das maneiras de resolver esse problema é escrever seu próprio SearchView “personalizado”, o que faremos hoje. Ir!
Nota: a visualização criada (doravante - SearchEditText ) não terá todas as propriedades do SearchView padrão. Se necessário, você pode facilmente adicionar opções adicionais para necessidades específicas.
Plano de ação
Há várias coisas que precisamos fazer para "transformar" um EditText em um SearchEditText. Resumindo, precisamos:
- Herdar SearchEditText de AppCompatEditText
- Adicione um ícone "Pesquisar" no canto esquerdo (ou direito) de SearchEditText, ao clicar no qual a consulta de pesquisa inserida será transmitida ao ouvinte registrado
- Adicione um ícone "Limpar" no canto direito (ou esquerdo) de SearchEditText, quando você clicar nele, o texto inserido na barra de pesquisa será apagado
- Defina o parâmetro imeOptions SearchEditText com o valor IME_ACTION_SEARCH, de modo que quando o teclado aparecer, o botão de entrada de texto funcionará como o botão "Pesquisar"
SearchEditText em toda a sua glória!
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?)
}
}
No código acima, duas funções de extensão foram usadas para definir a imagem direita e esquerda do EditText. Essas duas funções têm a seguinte aparência:
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
)
}
Herdando de AppCompatEditText
class SearchEditText
@JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyle: Int = androidx.appcompat.R.attr.editTextStyle
) : AppCompatEditText(context, attributeSet, defStyle)
Como você pode ver, a partir do construtor escrito, passamos todos os parâmetros necessários para o construtor AppCompatEditText. O ponto importante aqui é que o defStyle padrão é android.appcompat.R.attr.editTextStyle. Herdando de LinearLayout, FrameLayout e algumas outras visualizações, tendemos a usar 0 como o padrão para defStyle. No entanto, em nosso caso, isso não é adequado, caso contrário, nosso SearchEditText se comportará como um TextView, e não como um EditText.
Processando alterações de texto
A próxima coisa que precisamos fazer é "aprender" como responder aos eventos de mudança de texto em nosso SearchEditText. Precisamos disso por dois motivos:
- mostra ou esconde o ícone para limpar dependendo se o texto foi inserido
- notificando o ouvinte para alterar o texto em SearchEditText
Vejamos o código do listener:
private fun setTextChangeListener() {
doAfterTextChanged {
if (it.isNullOrBlank()) {
setRightDrawable(0)
} else {
setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)
}
queryTextListener?.onQueryTextChange(it.toString())
}
}
Para lidar com eventos de mudança de texto, a função de extensão doAfterTextChanged de androidx.core: core-ktx foi usada.
Lidar com o clique do botão Enter no teclado
Quando o usuário pressiona a tecla Enter no teclado, é feita uma verificação para ver se a ação é IME_ACTION_SEARCH. Nesse caso, informamos o ouvinte sobre essa ação e passamos o texto de SearchEditText para ele. Vamos ver como isso acontece.
private fun setOnEditorActionListener() {
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
queryTextListener?.onQueryTextSubmit(text.toString())
true
} else {
false
}
}
}
Tratamento de cliques em ícones
E, finalmente, a última, mas não menos importante, questão - como lidar com o clique em ícones de pesquisa e texto claro. O problema aqui é que, por padrão, os drawables do EditText padrão não respondem aos eventos de clique, o que significa que não há um ouvinte oficial que poderia lidar com eles.
Para resolver este problema, um OnTouchListener foi registrado no SearchEditText. No toque, usando as funções leftDrawableClicked e rightDrawableClicked, agora podemos clicar nos ícones. Vamos dar uma olhada no 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
}
}
Não há nada complicado sobre as funções leftDrawableClicked e RightDrawableClicked. Veja o primeiro, por exemplo. Para o ícone esquerdo, primeiro calculamos startOfDrawable e endOfDrawable e, em seguida, verificamos se a coordenada x do ponto de contato está no intervalo [startofDrawable, endOfDrawable]. Se sim, significa que o ícone esquerdo foi pressionado. A função rightDrawableClicked funciona de maneira semelhante.
Dependendo se o ícone da esquerda ou da direita é pressionado, realizamos algumas ações. Quando clicamos no ícone esquerdo (ícone de pesquisa), informamos o ouvinte sobre isso chamando sua função onQueryTextSubmit. Quando você clica no botão certo, limpamos o texto SearchEditText.
Resultado
Neste artigo, vimos a opção de "transformar" um EditText padrão em um SearchEditText mais avançado. Conforme mencionado anteriormente, a solução pronta para uso não oferece suporte a todas as opções fornecidas pelo SearchView, no entanto, você pode melhorá-la a qualquer momento adicionando opções adicionais a seu critério. Vá em frente!