Have you ever tried to customize the appearance or behaviour of aSearchView? Probably yes. Some customizations are pretty easy to do, but others are not that much straight forward. If you want to have full control, then writing a custom view to replace aSearchView is one way to go. Writing such a view is not only educational but also fun!
您是否尝试过自定义SearchView的外观或行为? 可能是。 有些自定义非常容易实现,但是其他自定义则不那么简单。 如果要完全控制,那么编写自定义视图来替换SearchView是一种方法。 写下这样的观点不仅具有教育意义,而且很有趣!
Note: the custom view (from the hereafter SearchEditText) we are going to write will not have all the options available by a SearchView, but adding more options would not be difficult when you get the hang of it.
注意:我们将要编写的自定义视图(此后为SearchEditText )将不具有SearchView可用的所有选项,但是,当您掌握了更多内容时,添加更多选项并不困难。
There are several things we need to do to transform an EditText into aSearchEditText. In a nutshell, we need to:
要将EditText转换为SearchEditText需要做几件事。 简而言之,我们需要:
Inherit from AppCompatEditText,
从AppCompatEditText继承,
Add a Search icon to the left (or right) of SearchEditText that when clicked, passes the search query to the registered listener,
在SearchEditText的左侧(或右侧)添加一个Search图标,单击该图标可将搜索查询传递给已注册的侦听器,
Add a Clear icon to the right (or left) of SearchEditText so that when clicked, clears the text of SearchEditText,
在SearchEditText的右侧(或左侧)添加一个Clear图标,以便在单击时清除SearchEditText的文本,
Set the imeOptions of SearchEditText to IME_ACTION_SEARCH so that when the keyboard appears, instead of an enter button, there is a search button.
设置imeOptions的SearchEditText到IME_ACTION_SEARCH这样,当键盘出现的,而不是输入按钮,还有一个搜索按钮。
In the next section, I will show you the full source code of SearchEditText, and in the remaining ones, I will walk you through different parts of SearchEditText to see what each part does.
在下一部分中,我将向您展示SearchEditText的完整源代码,而在其余部分中,我将SearchEditText您浏览SearchEditText不同部分,以了解每个部分的作用。
The full source code of SearchEditText is as follows:
SearchEditText的完整源代码如下:
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, I’ve used two extension functions to easily set the right and left drawables of an EditText. These two functions are defined as below:
在上面的代码中,我使用了两个扩展功能来轻松设置EditText左右可绘制对象。 这两个函数定义如下:
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 ) }There is no secret in inhering from AppCompatEditText. It is as follows:
从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 I’ve written a constructor and passed the required parameters to the constructor of AppCompatEditText. The important point here is that the default value of defStyle is androidx.appcompat.R.attr.editTextStyle. When inheriting from a LinearLayout, FrameLayout, and some other views we usually set 0 as the default value of defStyle. If we use 0 here, then our SearchEditText behaves like a TextView, not an EditText.
如您所见,我已经编写了一个构造函数,并将所需的参数传递给AppCompatEditText的构造函数。 这里的defStyle是androidx.appcompat.R.attr.editTextStyle的默认值为androidx.appcompat.R.attr.editTextStyle 。 从LinearLayout , FrameLayout和其他一些视图继承时,我们通常将0设置为defStyle的默认值。 如果在此处使用0,则我们的SearchEditText行为类似于TextView ,而不是EditText 。
The next thing we need to do is to react to the text change events of our SearchEditText. We need to handle the text change events for two reasons:
我们需要做的下一件事是对SearchEditText的文本更改事件作出React。 我们需要处理文本更改事件有两个原因:
to show or hide the Clear icon based on whether the text of SearchEditText is empty or not,
根据SearchEditText的文本是否为空来显示或隐藏“清除”图标,
to inform the listener, if any, that the text of SearchEditText has changed.
通知侦听器(如有) SearchEditText的文本已更改。
The following code shows the definition of setTextChangeListener function.
以下代码显示了setTextChangeListener函数的定义。
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 the text change events I’ve used the doAfterTextChanged extension function provided byandroidx.core:core-ktx library.
为了处理文本更改事件,我使用了androidx.core:core-ktx库提供的doAfterTextChanged扩展功能。
When user presses an action key on the keyboard we check to see if the action is IME_ACTION_SEARCH. If that’s the case then we inform the listener about this action and pass it the text of SearchEditText. The following code shows how it’s done.
当用户在键盘上按下操作键时,我们将检查操作是否为IME_ACTION_SEARCH 。 如果是这种情况,那么我们会将此操作通知给侦听器,并向其传递SearchEditText的文本。 以下代码显示了它是如何完成的。
private fun setOnEditorActionListener() { setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_SEARCH) { queryTextListener?.onQueryTextSubmit(text.toString()) true } else { false } } }Last but not least we should take care of click events of Search and Clear icons. The tricky part here is that the drawables of an EditText don’t respond to click events, meaning that there is no official listener to register to get notified when they are clicked. So we need to find a way to detect if they are clicked.
最后但并非最不重要的一点是,我们应该注意“搜索”和“清除”图标的单击事件。 这里最棘手的部分是EditText的可绘制对象不响应单击事件,这意味着没有官方的侦听器可以注册以在单击它们时获得通知。 因此,我们需要找到一种方法来检测它们是否被点击。
To do so we register a OnTouchListener on the SearchEditText. When a touch event occurs we use two helper functions leftDrawableClicked and rightDrawableClicked to see if the left or right drawable is clicked.
为此,我们在OnTouchListener上注册一个SearchEditText 。 发生触摸事件时,我们使用两个辅助函数leftDrawableClicked和rightDrawableClicked来查看是否单击了左侧或右侧可绘制对象。
Take a look at the following 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 } }The leftDrawableClicked and RightDrawableClicked functions are pretty straight forward. Take leftDrawableClicked for instance. For the left drawable first we compute startOfDrawable and endOfDrawable then we check to see if the x coordinate of the touched point is within the range [startofDrawable, endOfDrawable]. If that’s the case it means the left drawable is clicked. rightDrawableClicked function works in a similar manner.
leftDrawableClicked和RightDrawableClicked函数非常简单。 以leftDrawableClicked为例。 对于左边的可绘制对象,首先计算startOfDrawable和endOfDrawable然后检查触摸点的x坐标是否在[startofDrawable, endOfDrawable]范围内。 如果是这种情况,则意味着单击了左侧可绘制对象。 rightDrawableClicked函数以类似的方式工作。
Based on whether the left or right drawable is clicked we take proper action. If the left drawable (Search icon) is clicked we inform the listener by calling its onQueryTextSubmit function. If the right drawable is clicked we clear the text of SearchEditText.
根据是单击左侧还是右侧可绘制对象,我们将采取适当的措施。 如果单击左侧可绘制对象(搜索图标),我们将通过调用其onQueryTextSubmit函数来通知侦听器。 如果单击了正确的drawable,我们将清除SearchEditText的文本。
In this post, we took some simple steps to turn an EditText into a SearchEditText. As mentioned before, SearchEditText does not support all the options provided by a SearchView but adding more options would not be difficult at all. Trust me!
在本文中,我们采取了一些简单的步骤将EditText转换为SearchEditText 。 如前所述, SearchEditText不支持SearchView提供的所有选项,但是添加更多选项根本不会很困难。 相信我!
P.S: You can access the source code of SearchEditText from this GitHub repository.
PS:您可以访问的源代码SearchEditText从这个 GitHub的仓库。
翻译自: https://medium.com/@masood.fallahpoor/turning-an-edittext-into-a-searchedittext-ddef220f6b43