kotlin - 自定义ViewGroup实现搜索框:编辑框、搜索按钮、标签列表

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()
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王宁-Android

你的鼓励是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值