kotlin - 自定义ViewGroup实现搜索框:编辑框、搜索按钮、标签列表,点击选中标签。
/** * Author : wn * Email : maoning20080809@163.com * Date : 2025/3/25 13:39 * Description : 自定义ViewGroup实现搜索框:编辑框、搜索按钮、Label列表 */ class CustomSearchActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.custom_search_main) //设置窗口软输入模式 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE or WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) } override fun dispatchTouchEvent(ev: MotionEvent): Boolean { //点击空白处隐藏键盘 if(ev.action == MotionEvent.ACTION_DOWN){ val view = currentFocus if(view is EditText){ val outRect = Rect() view.getGlobalVisibleRect(outRect) if(!outRect.contains(ev.rawX.toInt(), ev.rawY.toInt())){ view.clearFocus() hideKeyboard(view) } } } return super.dispatchTouchEvent(ev) } private fun hideKeyboard(view : View){ val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(view.windowToken, 0) } }
custom_search_main.xml布局
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:id="@+id/gradient_item_layout" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.example.androidkotlindemo2.customview5.customviewgroup.CustomSearchLayout android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
CustomSearchLayout 类
/** * Author : wn * Email : maoning20080809@163.com * Date : 2025/3/25 12:31 * Description : */ class CustomSearchLayout : ViewGroup{ constructor(context: Context) : this(context, null) constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0) constructor(context: Context, attributeSet: AttributeSet? , defStyleAttr : Int) : super(context, attributeSet, defStyleAttr) //搜索输入框 private val searchEditText = CustomEditText(context).apply { imeOptions = EditorInfo.IME_ACTION_SEARCH setOnEditorActionListener { v, actionId, event -> if(actionId == EditorInfo.IME_ACTION_SEARCH){ initLabel() true } else { false } } } //搜索按钮 private val searchButton = CustomButton(context) private val resultContainer = CustomFlowLayout(context) // 模拟数据 private val testData = listOf( "苹果", "香蕉", "橙子", "西瓜", "葡萄", "菠萝蜜", "芒果", "草莓", "樱桃", "梨", "桃子", "李子", "猕猴桃", "哈密瓜", "椰子", "柠檬", "蓝莓", "石榴", "火龙果", "荔枝", "山竹", "木瓜", "杨桃", "枇杷", "桑葚", "无花果", "柿子", "柚子", "金桔", "龙眼" ) init { //添加子view addView(searchEditText) addView(searchButton) addView(resultContainer) searchButton.setOnClickListener { val searchContent = searchEditText.text.toString() if(TextUtils.isEmpty(searchContent)){ Toast.makeText(context, "请输入搜索内容", Toast.LENGTH_LONG).show() } else { Toast.makeText(context, "${searchEditText.text.toString()}", Toast.LENGTH_LONG).show() } } initLabel() } /** * 初始化标签 */ private fun initLabel(){ val query = searchEditText.text.toString().trim() val filteredData = if(query.isEmpty()){ testData } else { testData.filter { it.contains(query) } } showResults(filteredData) } private fun showResults(results : List<String>){ val dp4 = DpToPxUtils.dpToPx(4) resultContainer.removeAllViews() results.forEachIndexed {index, text -> val customLabelTextView = CustomLabelTextView(context) customLabelTextView.text = text customLabelTextView.setCustomTextColor(getLabelColor()) val params = MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) params.setMargins(dp4, dp4, dp4, dp4) customLabelTextView.layoutParams = params resultContainer.addView(customLabelTextView) customLabelTextView.setOnClickListener { LogUtils.i("AAA", "点击文本aa ${text}") searchEditText.setContent(text) processSelectedLabelBackground(index) } } } /** * 选中标签的背景颜色 */ private fun processSelectedLabelBackground(position :Int){ for(i in 0 until resultContainer.size){ val childView = resultContainer.get(i) as CustomLabelTextView if(i == position){ childView.setSelectedBackground(Color.RED) } else { childView.setNormalBackground() } } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val dp48 = DpToPxUtils.dpToPx(48) val dp60 = DpToPxUtils.dpToPx(60) val width = MeasureSpec.getSize(widthMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec) //测量搜索框 val editTextWidthSpec = MeasureSpec.makeMeasureSpec((width * 0.7).toInt(), MeasureSpec.EXACTLY) //测量按钮 val buttonWidthSpec = MeasureSpec.makeMeasureSpec((width * 0.25).toInt(), MeasureSpec.EXACTLY) val heightSpec = MeasureSpec.makeMeasureSpec(dp48, MeasureSpec.EXACTLY) searchEditText.measure(editTextWidthSpec, heightSpec) searchButton.measure(buttonWidthSpec, heightSpec) //测量结果容器 val remainingHeight = height - dp60 //顶部留出空间 val resultContainerSpec = MeasureSpec.makeMeasureSpec(remainingHeight, MeasureSpec.EXACTLY) resultContainer.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), resultContainerSpec) setMeasuredDimension(width, height) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { val width = r - l val dp8 = DpToPxUtils.dpToPx(8) //布局搜索框和按钮 val editTextLeft = dp8 val editTextTop = dp8 searchEditText.layout(editTextLeft, editTextTop, editTextLeft + searchEditText.measuredWidth, editTextTop + searchEditText.measuredHeight) val buttonLeft = width - searchButton.measuredWidth - dp8 searchButton.layout(buttonLeft, editTextTop, buttonLeft + searchButton.measuredWidth, editTextTop +searchButton.measuredHeight) //布局结果容器 val resultTop = editTextTop + searchEditText.measuredHeight + dp8 resultContainer.layout(0, resultTop, width, resultTop + resultContainer.measuredHeight) } /** * 获取标签颜色 */ private fun getLabelColor() : Int{ var arrayColor = arrayOf<Int>(Color.LTGRAY, Color.GREEN, Color.BLUE, Color.MAGENTA, Color.BLACK) var random = Random.nextInt(arrayColor.size) return arrayColor[random] } }
CustomEditText 类
/** * Author : wn * Email : maoning20080809@163.com * Date : 2025/3/25 11:26 * Description : */ class CustomEditText : AppCompatEditText{ constructor(context: Context) : this(context, null) constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0) constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int) : super(context, attributeSet, defStyleAttr) init { val dp16 = DpToPxUtils.dpToPx(16) val dp12 = DpToPxUtils.dpToPx(12) background = createEditTextBackground() setPadding(dp16, dp12, dp16, dp12) hint = "请输入搜索内容" textSize = 16f //设置点击监听器 setOnClickListener { //设置属性,才能弹出软键盘 isFocusable = true isFocusableInTouchMode = true requestFocus() showSoftInput() } //设置焦点变换监听器 setOnFocusChangeListener { v, hasFocus -> LogUtils.i("AAA", "hasFocus() ${hasFocus}") if(hasFocus){ showSoftInput() } } } private fun createEditTextBackground() : Drawable{ val shape = GradientDrawable() shape.shape = GradientDrawable.RECTANGLE shape.cornerRadius = DpToPxUtils.dpToPx(8).toFloat() shape.setStroke(DpToPxUtils.dpToPx(1), Color.GRAY) shape.setColor(Color.WHITE) return shape } private fun showSoftInput(){ val im = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager LogUtils.i("AAA", "showSoftInput() : ${text?.length?:0} , ${im}") //post { im.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) setSelection(text?.length?:0) //} } fun setContent(content : String){ this.setText(content) invalidate() } }
CustomButton 类
/** * Author : wn * Email : maoning20080809@163.com * Date : 2025/3/25 11:33 * Description : */ class CustomButton : AppCompatButton { constructor(context: Context) : this(context, null) constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0) constructor(context: Context, attributeSet: AttributeSet? , defStyleAttr : Int) : super(context, attributeSet, defStyleAttr) init { background = createButtonBackground() val dp24 = DpToPxUtils.dpToPx(24) val dp12 = DpToPxUtils.dpToPx(12) setPadding(dp24, dp12, dp24, dp12) text = "搜索" setTextColor(Color.WHITE) //居中显示 gravity = Gravity.CENTER textSize = 16f } private fun createButtonBackground() : Drawable{ val shape = GradientDrawable() shape.shape = GradientDrawable.RECTANGLE shape.cornerRadius = DpToPxUtils.dpToPx(8).toFloat() shape.setColor(ContextCompat.getColor(context, R.color.colorPrimary)) return shape } }
CustomFlowLayout类
/** * Author : wn * Email : maoning20080809@163.com * Date : 2025/3/25 11:43 * Description : */ class CustomFlowLayout(context: Context, attributeSet: AttributeSet? = null) : ViewGroup(context, attributeSet) { private val childPositions = mutableListOf<Pair<Int, Int>>() init { setOnClickListener { LogUtils.i("点击CustomFlowLayout") } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val width = MeasureSpec.getSize(widthMeasureSpec) var height = 0 var lineHeight = 0 var currentWidth = paddingLeft childPositions.clear() for (i in 0 until childCount){ val child = getChildAt(i) if(child.visibility == View.GONE) continue measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0) val lp = child.layoutParams as MarginLayoutParams val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin if(currentWidth + childWidth > width - paddingRight){ //换行 height += lineHeight currentWidth = paddingLeft lineHeight = 0 } childPositions.add(Pair(currentWidth + lp.leftMargin, height + lp.topMargin)) currentWidth += childWidth lineHeight = maxOf(lineHeight, childHeight) } height += lineHeight + paddingBottom setMeasuredDimension(width, resolveSize(height, heightMeasureSpec)) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { for(i in 0 until childCount){ if(i >= childPositions.size) break val child = getChildAt(i) if(child.visibility == View.GONE) continue val (left, top) = childPositions[i] val right = left + child.measuredWidth val bottom = top + child.measuredHeight child.layout(left, top, right, bottom) } } override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams { return MarginLayoutParams(context, attrs) } override fun generateDefaultLayoutParams(): LayoutParams { return MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) } override fun generateLayoutParams(p: LayoutParams?): LayoutParams { return MarginLayoutParams(p) } }
CustomLabelTextView类:
/** * Author : wn * Email : maoning20080809@163.com * Date : 2025/3/25 11:38 * Description : 显示标签 */ class CustomLabelTextView : AppCompatTextView { constructor(context: Context) : this(context, null) constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0) constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr : Int) : super(context, attributeSet, defStyleAttr) init { val dp8 = DpToPxUtils.dpToPx(8) val dp4 = DpToPxUtils.dpToPx(4) background = createBackground(Color.LTGRAY) setPadding(dp8, dp4, dp8, dp4) textSize = 14f gravity = Gravity.CENTER setTextColor(Color.BLACK) } private fun createBackground(color :Int) : Drawable{ val shape = GradientDrawable() shape.shape = GradientDrawable.RECTANGLE shape.cornerRadius = DpToPxUtils.dpToPx(4).toFloat() shape.setStroke(DpToPxUtils.dpToPx(1), color) shape.setColor(Color.parseColor("#F5F5F5")) return shape } fun setCustomTextColor(color : Int){ this.setTextColor(color) } /** * 设置默认背景 */ fun setNormalBackground(){ this.background = createBackground(Color.LTGRAY) invalidate() } /** * 设置选中背景 */ fun setSelectedBackground(color : Int){ this.setBackgroundColor(color) invalidate() } }
/** * Author : wn * Email : maoning20080809@163.com * Date : 2025/3/25 11:29 * Description : */ object DpToPxUtils { fun dpToPx(dp : Int) : Int{ return (dp * MyApp.myApp.resources.displayMetrics.density).toInt() } }