هل سبق لك أن حاولت تخصيص مظهر أو سلوك مكون SearchView القياسي؟ أظن ذلك. في هذه الحالة ، أعتقد أنك ستوافق على أنه ليست كل إعداداتها مرنة بما يكفي لتلبية جميع متطلبات العمل لمهمة واحدة. تتمثل إحدى طرق حل هذه المشكلة في كتابة طريقة عرض البحث "المخصصة" الخاصة بك ، والتي سنفعلها اليوم. اذهب!
ملاحظة: لن يحتوي العرض الذي تم إنشاؤه (المشار إليه فيما يلي - SearchEditText ) على جميع خصائص SearchView القياسي. إذا لزم الأمر ، يمكنك بسهولة إضافة خيارات إضافية لاحتياجات معينة.
خطة عمل
هناك العديد من الأشياء التي يتعين علينا القيام بها "لتحويل" EditText إلى SearchEditText. باختصار نحن بحاجة إلى:
- ترث SearchEditText من AppCompatEditText
- أضف أيقونة "بحث" في الزاوية اليسرى (أو اليمنى) من SearchEditText ، عند النقر على أي استعلام البحث الذي تم إدخاله سينتقل إلى المستمع المسجل
- أضف رمز "تنظيف" في الزاوية اليمنى (أو اليسرى) من SearchEditText ، عند النقر فوق أي منها ، سيتم مسح النص الذي تم إدخاله في شريط البحث
- اضبط معلمة imeOptions SearchEditText على القيمة IME_ACTION_SEARCH ، بحيث عندما تظهر لوحة المفاتيح ، سيعمل زر إدخال النص كزر "بحث"
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?)
}
}
في الكود أعلاه ، تم استخدام وظيفتين ملحق لتعيين الصورة اليمنى واليسرى من 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
)
}
الوراثة من AppCompatEditText
class SearchEditText
@JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyle: Int = androidx.appcompat.R.attr.editTextStyle
) : AppCompatEditText(context, attributeSet, defStyle)
كما ترى ، من المُنشئ المكتوب ، نقوم بتمرير جميع المعلمات الضرورية إلى مُنشئ AppCompatEditText. النقطة المهمة هنا هي أن النمط الافتراضي هو android.appcompat.R.attr.editTextStyle. بالوراثة من LinearLayout و FrameLayout وبعض طرق العرض الأخرى ، نميل إلى استخدام 0 كإعداد افتراضي لـ defStyle. ومع ذلك ، في حالتنا ، هذا غير مناسب ، وإلا فإن SearchEditText الخاص بنا سوف يتصرف مثل TextView ، وليس مثل EditText.
معالجة تغييرات النص
الشيء التالي الذي يتعين علينا القيام به هو "التعرف" على كيفية الرد على أحداث تغيير النص في SearchEditText. نحتاج إلى هذا لسببين:
- إظهار أو إخفاء رمز المسح بناءً على ما إذا تم إدخال النص
- إعلام المستمع بتغيير النص في SearchEditText
لنلقِ نظرة على رمز المستمع:
private fun setTextChangeListener() {
doAfterTextChanged {
if (it.isNullOrBlank()) {
setRightDrawable(0)
} else {
setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)
}
queryTextListener?.onQueryTextChange(it.toString())
}
}
للتعامل مع أحداث تغيير النص ، تم استخدام وظيفة الامتداد doAfterTextChanged من androidx.core: core-ktx.
تعامل مع النقر على زر الإدخال على لوحة المفاتيح
عندما يضغط المستخدم على مفتاح الإدخال على لوحة المفاتيح ، فإنه يتحقق لمعرفة ما إذا كان الإجراء هو IME_ACTION_SEARCH. إذا كان الأمر كذلك ، فإننا نبلغ المستمع بهذا الإجراء ونمرر النص من SearchEditText إليه. دعونا نرى كيف يحدث هذا.
private fun setOnEditorActionListener() {
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
queryTextListener?.onQueryTextSubmit(text.toString())
true
} else {
false
}
}
}
معالجة نقرات الرمز
وأخيرًا ، السؤال الأخير وليس آخرًا - كيفية التعامل مع النقر على أيقونات البحث ومسح النص. المهم هنا هو ، افتراضيًا ، أن العناصر القابلة للرسم في EditText القياسي لا تستجيب لأحداث النقر ، مما يعني أنه لا يوجد مستمع رسمي يمكنه التعامل معها.
لحل هذه المشكلة ، تم تسجيل OnTouchListener في SearchEditText. عند اللمس ، باستخدام الوظائف leftDrawableClicked و rightDrawableClicked ، يمكننا الآن التعامل مع النقر على الرموز. دعنا نلقي نظرة على الكود:
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
}
}
لا يوجد شيء معقد حول الدالتين leftDrawableClicked و RightDrawableClicked. خذ الأول ، على سبيل المثال. بالنسبة للأيقونة اليسرى ، نحسب أولاً startOfDrawable و endOfDrawable ثم نتحقق مما إذا كان إحداثي x لنقطة اللمس في النطاق [startofDrawable، endOfDrawable]. إذا كانت الإجابة بنعم ، فهذا يعني أنه تم الضغط على الرمز الأيسر. تعمل الوظيفة rightDrawableClicked بطريقة مماثلة.
اعتمادًا على الضغط على الأيقونة اليمنى أو اليسرى ، نقوم بتنفيذ بعض الإجراءات. عندما نضغط على الأيقونة اليسرى (أيقونة البحث) ، نبلغ المستمع بذلك عن طريق استدعاء وظيفة onQueryTextSubmit الخاصة به. عند النقر فوق الزر الأيمن ، نقوم بمسح نص SearchEditText.
انتاج |
في هذه المقالة ، نظرنا في خيار "تحويل" EditText قياسي إلى SearchEditText أكثر تقدمًا. كما ذكرنا سابقًا ، لا يدعم الحل الجاهز جميع الخيارات التي يوفرها SearchView ، ومع ذلك ، يمكنك تحسينه في أي وقت عن طريق إضافة خيارات إضافية وفقًا لتقديرك. أذهب خلفها!