搜索框自定义
我们需要设计一个自定义的AppCompatEditText搜索框,具有以下功能:
- 可以设置背景
- 左边设置图标并且有点击事件
- 默认搜索事件 (意思是? 可能是点击键盘上的搜索按钮触发搜索,或者调用外部搜索回调)
- 右边可以定义搜索按钮 (可能是视图上的一个按钮,点击执行搜索)
- 当有文字输入时,文字右边(搜索按钮的左边)出现删除图标,点击清除文字
- 键盘右下角action设置成搜索按钮 (IME_ACTION_SEARCH)
- 输入文字过滤表情符号
1. 自定义属性定义 (res/values/attrs.xml)
<declare-styleable name="SearchEditText">
<attr name="leftIcon" format="reference" />
<attr name="leftIconVisible" format="boolean" />
<attr name="searchButtonText" format="string" />
<attr name="searchButtonIcon" format="reference" />
<attr name="searchButtonBackground" format="reference" />
<attr name="clearIcon" format="reference" />
<attr name="android:hint" />
<attr name="android:textSize" />
<attr name="android:textColor" />
<attr name="android:textColorHint" />
<attr name="android:inputType" />
<attr name="android:maxLines" />
<attr name="android:background" />
</declare-styleable>
2. SearchEditText 组件代码 (Kotlin)
package com.example.testabcdemo
import android.content.Context
import android.content.res.TypedArray
import android.text.Editable
import android.text.InputFilter
import android.text.Spanned
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.content.ContextCompat
import java.util.regex.Pattern
class SearchEditText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val leftIcon: ImageView
private val editText: AppCompatEditText
private val clearIcon: ImageView
private val searchButton: TextView
private var onSearchClickListener: ((String) -> Unit)? = null
private var onLeftIconClickListener: (() -> Unit)? = null
init {
orientation = HORIZONTAL
gravity = android.view.Gravity.CENTER_VERTICAL
LayoutInflater.from(context).inflate(R.layout.view_search_edittext, this, true)
leftIcon = findViewById(R.id.iv_left_icon)
editText = findViewById(R.id.et_search)
clearIcon = findViewById(R.id.iv_clear)
searchButton = findViewById(R.id.btn_search)
val ta = context.obtainStyledAttributes(attrs, R.styleable.SearchEditText)
setupAttributes(ta)
ta.recycle()
editText.imeOptions = EditorInfo.IME_ACTION_SEARCH
editText.inputType = EditorInfo.TYPE_CLASS_TEXT
editText.maxLines = 1
editText.isSingleLine = true
editText.filters = arrayOf(EmojiInputFilter())
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
clearIcon.visibility = if (s.isNullOrEmpty()) View.GONE else View.VISIBLE
}
})
clearIcon.setOnClickListener {
editText.text?.clear()
editText.requestFocus()
showSoftInput()
}
searchButton.setOnClickListener {
performSearch()
}
editText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
performSearch()
true
} else false
}
leftIcon.setOnClickListener {
onLeftIconClickListener?.invoke()
}
clearIcon.visibility = if (editText.text.isNullOrEmpty()) View.GONE else View.VISIBLE
}
private fun setupAttributes(ta: TypedArray) {
val leftIconRes = ta.getResourceId(R.styleable.SearchEditText_leftIcon, 0)
if (leftIconRes != 0) {
leftIcon.setImageResource(leftIconRes)
}
val leftIconVisible = ta.getBoolean(R.styleable.SearchEditText_leftIconVisible, true)
leftIcon.visibility = if (leftIconVisible) View.VISIBLE else View.GONE
val clearIconRes = ta.getResourceId(R.styleable.SearchEditText_clearIcon, 0)
if (clearIconRes != 0) {
clearIcon.setImageResource(clearIconRes)
} else {
clearIcon.setImageDrawable(
ContextCompat.getDrawable(context, android.R.drawable.ic_menu_close_clear_cancel)
)
}
val searchButtonText = ta.getString(R.styleable.SearchEditText_searchButtonText)
val searchButtonIcon = ta.getResourceId(R.styleable.SearchEditText_searchButtonIcon, 0)
if (searchButtonIcon != 0) {
searchButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, searchButtonIcon, 0)
searchButton.text = ""
} else {
searchButton.text = searchButtonText ?: "搜索"
}
val searchButtonBg = ta.getResourceId(R.styleable.SearchEditText_searchButtonBackground, 0)
if (searchButtonBg != 0) {
searchButton.setBackgroundResource(searchButtonBg)
} else {
searchButton.setBackgroundResource(android.R.drawable.editbox_background)
}
val hint = ta.getString(R.styleable.SearchEditText_android_hint)
editText.hint = hint
val textSize = ta.getDimensionPixelSize(R.styleable.SearchEditText_android_textSize, -1)
if (textSize != -1) {
editText.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, textSize.toFloat())
}
val textColor = ta.getColor(R.styleable.SearchEditText_android_textColor, -1)
if (textColor != -1) {
editText.setTextColor(textColor)
}
val textColorHint = ta.getColor(R.styleable.SearchEditText_android_textColorHint, -1)
if (textColorHint != -1) {
editText.setHintTextColor(textColorHint)
}
val inputType = ta.getInt(R.styleable.SearchEditText_android_inputType, -1)
if (inputType != -1) {
editText.inputType = inputType
}
val maxLines = ta.getInt(R.styleable.SearchEditText_android_maxLines, 1)
editText.maxLines = maxLines
val background = ta.getDrawable(R.styleable.SearchEditText_android_background)
if (background != null) {
this.background = background
}
}
private fun performSearch() {
val query = editText.text.toString()
onSearchClickListener?.invoke(query)
hideSoftInput()
}
private fun hideSoftInput() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0)
}
private fun showSoftInput() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
fun setOnSearchClickListener(listener: (String) -> Unit) {
onSearchClickListener = listener
}
fun setOnLeftIconClickListener(listener: () -> Unit) {
onLeftIconClickListener = listener
}
fun getText(): String = editText.text.toString()
fun setText(text: String) {
editText.setText(text)
editText.setSelection(text.length)
}
fun setHint(hint: String) {
editText.hint = hint
}
fun setLeftIcon(iconRes: Int) {
leftIcon.setImageResource(iconRes)
leftIcon.visibility = View.VISIBLE
}
fun setLeftIconVisible(visible: Boolean) {
leftIcon.visibility = if (visible) View.VISIBLE else View.GONE
}
fun setSearchButtonText(text: String) {
searchButton.text = text
}
fun setSearchButtonIcon(iconRes: Int) {
searchButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, iconRes, 0)
searchButton.text = ""
}
private class EmojiInputFilter : InputFilter {
private val emojiPattern = Pattern.compile(
"[\uD83C-\uDBFF\uDC00-\uDFFF]|" +
"[\u2600-\u27BF]|" +
"[\uE000-\uF8FF]|" +
"[\uFE00-\uFE0F]|" +
"[\u1F300-\u1F5FF]|" +
"[\u1F600-\u1F64F]|" +
"[\u1F680-\u1F6FF]|" +
"[\u1F700-\u1F77F]|" +
"[\u1F780-\u1F7FF]|" +
"[\u1F800-\u1F8FF]|" +
"[\u1F900-\u1F9FF]|" +
"[\u1FA00-\u1FA6F]|" +
"[\u1FA70-\u1FAFF]"
)
override fun filter(
source: CharSequence,
start: Int,
end: Int,
dest: Spanned?,
dstart: Int,
dend: Int
): CharSequence? {
val builder = StringBuilder()
for (i in start until end) {
val c = source[i]
if (!isEmoji(c)) {
builder.append(c)
}
}
return if (builder.length == end - start) null else builder.toString()
}
private fun isEmoji(char: Char): Boolean {
if (Character.isHighSurrogate(char) || Character.isLowSurrogate(char)) {
return true
}
return emojiPattern.matcher(char.toString()).matches()
}
}
}
3. 布局文件 (res/layout/view_search_edittext.xml)
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.LinearLayout">
<!-- 左侧图标 -->
<ImageView
android:id="@+id/iv_left_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="12dp"
android:paddingEnd="8dp"
android:src="@drawable/baseline_search_24"
android:visibility="visible" />
<!-- 输入框 -->
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@null"
android:paddingVertical="10dp"
android:paddingStart="0dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textSize="14sp" />
<!-- 清除按钮 -->
<ImageView
android:id="@+id/iv_clear"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:src="@drawable/baseline_clear_24"
android:visibility="gone"
android:contentDescription="clear" />
<!-- 搜索按钮 -->
<TextView
android:id="@+id/btn_search"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackground"
android:gravity="center"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:layout_margin="1dp"
android:text="搜索"
android:textColor="@android:color/white"
android:textSize="14sp" />
</merge>
4. 布局文件 (res/drawable/bg_search_button.xml)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<gradient
android:angle="180"
android:endColor="#ffff2424"
android:startColor="#ffff8298" />
</shape>
5. 布局文件 (res/drawable/bg_rounded_edittext.xml)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="1dp"
android:color="#ffe0e0e0" />
<solid android:color="#fffefefe" />
<corners
android:bottomLeftRadius="4dp"
android:bottomRightRadius="4dp"
android:topLeftRadius="4dp"
android:topRightRadius="4dp" />
</shape>
6. 在布局中使用
<com.example.testabcdemo.SearchEditText
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="@drawable/bg_rounded_edittext"
android:hint="请输入关键词"
android:textColor="@android:color/black"
android:textColorHint="#888888"
android:textSize="14sp"
app:clearIcon="@drawable/baseline_clear_24"
app:layout_constraintTop_toTopOf="parent"
app:leftIcon="@drawable/baseline_search_24"
app:leftIconVisible="true"
app:searchButtonBackground="@drawable/bg_search_button"
app:searchButtonText="搜索" />
7. 在 Activity/Fragment 中使用
val searchEditText = findViewById<SearchEditText>(R.id.search_view)
searchEditText.setOnSearchClickListener { query ->
Toast.makeText(this, "搜索: $query", Toast.LENGTH_SHORT).show()
}
searchEditText.setOnLeftIconClickListener {
Toast.makeText(this, "左侧图标被点击", Toast.LENGTH_SHORT).show()
}
8. 效果展示
