Have you ever tried to customize the appearance or behavior of the standard SearchView component? I guess so. In this case, I think that you will agree that not all of its settings are flexible enough to satisfy all the business requirements of a single task. One of the ways to solve this problem is to write your own βcustomβ SearchView, which we will do today. Go!
Note: the created view (hereinafter - SearchEditText ) will not have all the properties of the standard SearchView. If necessary, you can easily add additional options for specific needs.
Action plan
There are several things we need to do to "turn" an EditText into a SearchEditText. In short, we need:
- Inherit SearchEditText from AppCompatEditText
- Add a "Search" icon in the left (or right) corner of SearchEditText, when clicking on which the entered search query will be transmitted to the registered listener
- Add a "Cleanup" icon in the right (or left) corner of SearchEditText, when you click on which, the entered text in the search bar will be cleared
- Set the imeOptions SearchEditText parameter to the value IME_ACTION_SEARCH, so that when the keyboard appears, the text input button will act as the "Search" button
SearchEditText in all its glory!
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?)
}
}
In the above code, two extension functions were used to set the right and left image of the EditText. These two functions look like this:
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
)
}
Inheriting from AppCompatEditText
class SearchEditText
@JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyle: Int = androidx.appcompat.R.attr.editTextStyle
) : AppCompatEditText(context, attributeSet, defStyle)
As you can see, from the written constructor, we pass all the necessary parameters to the AppCompatEditText constructor. The important point here is that the default defStyle is android.appcompat.R.attr.editTextStyle. Inheriting from LinearLayout, FrameLayout and some other views, we tend to use 0 as the default for defStyle. However, in our case, this is not suitable, otherwise our SearchEditText will behave like a TextView, and not like an EditText.
Processing text changes
The next thing we need to do is "learn" how to respond to text change events in our SearchEditText. We need this for two reasons:
- show or hide the clear icon depending on whether the text is entered
- notifying the listener to change the text in SearchEditText
Let's look at the listener code:
private fun setTextChangeListener() {
doAfterTextChanged {
if (it.isNullOrBlank()) {
setRightDrawable(0)
} else {
setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)
}
queryTextListener?.onQueryTextChange(it.toString())
}
}
To handle text change events, the doAfterTextChanged extension function from androidx.core: core-ktx was used.
Handle the click of the enter button on the keyboard
When the user presses the enter key on the keyboard, a check is made to see if the action is IME_ACTION_SEARCH. If so, then we inform the listener about this action and pass it the text from SearchEditText. Let's see how this happens.
private fun setOnEditorActionListener() {
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
queryTextListener?.onQueryTextSubmit(text.toString())
true
} else {
false
}
}
}
Handling icon clicks
And finally, the last, but not least, question - how to handle clicking on search icons and clear text. The catch here is that, by default, the drawables of the standard EditText do not respond to click events, which means that there is no official listener that could handle them.
To solve this problem, an OnTouchListener was registered in the SearchEditText. On touch, using the functions leftDrawableClicked and rightDrawableClicked, we can now handle clicking on icons. Let's take a look at the 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
}
}
There is nothing complicated about the leftDrawableClicked and RightDrawableClicked functions. Take the first one, for example. For the left icon, we first calculate startOfDrawable and endOfDrawable and then check if the x-coordinate of the touch point is in the range [startofDrawable, endOfDrawable]. If yes, it means that the left icon was pressed. The rightDrawableClicked function works in a similar way.
Depending on whether the left or right icon is pressed, we carry out certain actions. When we click on the left icon (search icon), we inform the listener about this by calling its onQueryTextSubmit function. When you click on the right one, we clear the SearchEditText text.
Output
In this article, we looked at the option of "turning" a standard EditText into a more advanced SearchEditText. As mentioned earlier, the out-of-the-box solution does not support all of the options provided by SearchView, however, you can improve it at any time by adding additional options at your discretion. Go for it!