Android标签容器的开发

详细的完整工程可以看这里github链接查看

 

        

        这两天自定义ViewGroup开发了一个标签容器,可以展示一大堆标签。

        这个组件的关键其实就在于计算换行,因为给到的标签肯定是个列表,那条目的文本长度不一,什么时候该换行什么时候该在一行展示可以重点看下onMeasure()中计算高度的代码。其他的我觉得就没啥难度和挑战了!!!

        老司机仔细看都看得懂的。然后一些可变属性都提取出来了在attrs.xml里方便复用。

        点击tag可以触发点击了tag的回调

        长按tag可以触发选中或者取消选中tag的回调

下面只贴关键的代码

可配置属性声明

    <declare-styleable name="TagContainerView">
        <attr name="tag_vertical_margin" format="dimension" />
        <attr name="tag_horizontal_margin" format="dimension" />
        <attr name="tag_text_color" format="color" />
        <attr name="tag_padding_vertical" format="dimension" />
        <attr name="tag_padding_horizontal" format="dimension" />
        <attr name="tag_text_size" format="dimension" />
        <attr name="tag_bg_drawable" format="reference" />
        <attr name="tag_bg_drawable_selected" format="reference" />
        <attr name="tag_text_color_selected" format="color" />
    </declare-styleable>

自定义的TagContainerView见下

package com.openld.seniorui.testtagcontainer

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.text.TextUtils
import android.util.AttributeSet
import android.view.DragEvent
import android.view.Gravity
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.NonNull
import com.openld.seniorui.R
import com.openld.seniorutils.utils.DisplayUtils

/**
 * author: lllddd
 * created on: 2022/6/15 20:42
 * description:Tag容器,能够自动换行
 */
@SuppressLint("NewApi")
class TagContainerView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
    ViewGroup(context, attrs, defStyleAttr) {

    // 垂直方向间距
    private var mVerticalMargin: Int = 0

    // 水平方向间距
    private var mHorizontalMargin: Int = 0

    // tag高度
    private var mTagHeight: Int = 0

    // tag水平padding
    private var mTagHorizontalPadding: Int = 0

    // tag垂直padding
    private var mTagVerticalPadding: Int = 0

    // tag文字颜色
    private var mTagTextColor: Int = Color.RED

    // tag选中的文字颜色
    private var mTagTextColorSelected: Int = Color.WHITE

    // tag字体大小
    private var mTagTextSize: Int = 14

    // 组件宽度
    private var mWidth: Int = 0

    // 组件高度
    private var mHeight: Int = 0

    // tag背景Drawable
    private var mBgTagDrawableResId: Int = R.drawable.bg_tag

    // tag选中背景Drawable
    private var mBgTagSelectedDrawableResId: Int = R.drawable.bg_tag_selected

    // 该画笔用来辅助测量tag中的文字
    private var mTagTextPaint: Paint

    // Tag被点击的监听器
    private var mOnTagClickListener: OnTagClickListener? = null

    // Tag被长按的监听器
    private var mOnTagLongClickListener: OnTagLongClickListener? = null

    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)

    constructor(context: Context?) : this(context, null)

    init {
        val a = context!!.obtainStyledAttributes(attrs, R.styleable.TagContainerView)

        mVerticalMargin = a.getDimensionPixelSize(
            R.styleable.TagContainerView_tag_vertical_margin,
            DisplayUtils.dp2px(context, 5)
        )

        mHorizontalMargin = a.getDimensionPixelSize(
            R.styleable.TagContainerView_tag_horizontal_margin,
            DisplayUtils.dp2px(context, 5)
        )

        val tagTextSizePx = a.getDimensionPixelSize(
            R.styleable.TagContainerView_tag_text_size,
            DisplayUtils.sp2px(context, 14)
        )
        mTagTextSize = DisplayUtils.px2sp(context, tagTextSizePx)

        mTagVerticalPadding = a.getDimensionPixelSize(
            R.styleable.TagContainerView_tag_padding_vertical,
            DisplayUtils.dp2px(context, 2)
        )

        mTagHorizontalPadding = a.getDimensionPixelSize(
            R.styleable.TagContainerView_tag_padding_horizontal,
            DisplayUtils.dp2px(context, 10)
        )

        mTagTextColor = a.getColor(
            R.styleable.TagContainerView_tag_text_color,
            context.resources.getColor(R.color.red, null)
        )

        mTagTextColorSelected = a.getColor(
            R.styleable.TagContainerView_tag_text_color_selected,
            context.resources.getColor(R.color.white, null)
        )

        mBgTagDrawableResId =
            a.getResourceId(R.styleable.TagContainerView_tag_bg_drawable, R.drawable.bg_tag)

        mBgTagSelectedDrawableResId = a.getResourceId(
            R.styleable.TagContainerView_tag_bg_drawable_selected,
            R.drawable.bg_tag_selected
        )

        // 这边需要加上一个文字的偏移否则展示有问题
        mTagTextPaint = Paint()
        mTagTextPaint.textSize = mTagTextSize.toFloat()
        val textOffset = mTagTextPaint.fontMetrics.bottom - mTagTextPaint.fontMetrics.top

        mTagHeight = (textOffset + tagTextSizePx + mTagVerticalPadding * 2).toInt()

        a.recycle()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var lines = 1
        if (childCount == 0) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        }

        val width = MeasureSpec.getSize(widthMeasureSpec)
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        // 处理宽
        mWidth = when (widthMode) {
            MeasureSpec.EXACTLY -> {
                width
            }
            MeasureSpec.AT_MOST -> {
                // 此时让横向占满屏幕
                context.resources.displayMetrics.widthPixels
            }
            else -> {
                width
            }
        }

        // 处理高
        when (heightMode) {
            MeasureSpec.EXACTLY -> {
                mHeight = height
            }
            MeasureSpec.AT_MOST -> {
                var tempWidth = 0
                var index = 0
                while (index < childCount) {
                    val child = getChildAt(index)
                    val childParams = child.layoutParams as MarginLayoutParams

                    if (tempWidth <= mWidth - paddingLeft - paddingRight) {//当前行摆得下
                        tempWidth += childParams.width + childParams.leftMargin + childParams.rightMargin
                        index++
                    } else {// 当前行摆不下了
                        index--
                        lines++
                        tempWidth = 0
                    }
                }
                mHeight = paddingTop + paddingBottom + lines * (mTagHeight + 2 * mVerticalMargin)
            }
            else -> {
                mHeight = height
            }
        }

        setMeasuredDimension(mWidth, mHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        for (index in 0 until childCount) {
            val child = getChildAt(index)
            val childParams = child.layoutParams as MarginLayoutParams

            var left: Int
            var top: Int
            var right: Int
            var bottom: Int

            if (index == 0) {// 第一个tag
                left = paddingLeft + childParams.leftMargin
                top = paddingTop + childParams.topMargin
                right = left + childParams.width
                bottom = top + childParams.height
            } else {// 非第一个tag
                val preChild = getChildAt(index - 1)
                val preChildParams = preChild.layoutParams as MarginLayoutParams

                if (preChild.right + preChildParams.rightMargin + childParams.leftMargin + childParams.width + childParams.rightMargin
                    <= mWidth - paddingRight
                ) {// 不需要换行
                    left = preChild.right + preChildParams.rightMargin + childParams.leftMargin
                    top = preChild.top
                    right = left + childParams.width
                    bottom = top + childParams.height
                } else {// 需要换行
                    left = paddingLeft + childParams.leftMargin
                    top = preChild.bottom + preChildParams.bottomMargin + childParams.topMargin
                    right = left + childParams.width
                    bottom = top + childParams.height
                }
            }

            child.layout(left, top, right, bottom)
        }
    }

    override fun onDragEvent(event: DragEvent?): Boolean {
        return super.onDragEvent(event)
    }

    /**
     * 设置tags
     */
    @SuppressLint("UseCompatLoadingForDrawables")
    fun setTags(@NonNull tagList: List<String>) {
        removeAllViews()

        for (index in tagList.indices) {
            val s = tagList[index]
            if (TextUtils.isEmpty(s)) {
                continue
            }

            // 方式1:使用LayoutInflater添加tag
//            val view = LayoutInflater.from(context).inflate(R.layout.tag, this, false)
//            val txtTag = view.findViewById<TextView>(R.id.txt_tag) as TextView
//            txtTag.tag = false
//            txtTag.text = s
//            txtTag.setTextColor(mTagTextColor)
//            txtTag.textSize = mTagTextSize.toFloat()
//            txtTag.gravity = Gravity.CENTER
//            txtTag.background = context.getDrawable(mBgTagDrawableResId)
//            txtTag.setOnClickListener {
//                onTagClick(index, txtTag, s)
//            }
//            txtTag.setOnLongClickListener {
//                onTagLongClick(index, txtTag, s)
//                true
//            }
//            txtTag.setPadding(
//                mTagHorizontalPadding,
//                mTagVerticalPadding,
//                mTagHorizontalPadding,
//                mTagVerticalPadding
//            )
//
//            val params = view.layoutParams as MarginLayoutParams
//            params.width =
//                2 * mTagHorizontalPadding + DisplayUtils.sp2px(
//                    context,
//                    mTagTextPaint.measureText(s).toInt()
//                )
//            params.height = mTagHeight
//
//            params.leftMargin = mHorizontalMargin
//            params.rightMargin = mHorizontalMargin
//            params.topMargin = mHorizontalMargin
//            params.bottomMargin = mHorizontalMargin
//            view.layoutParams = params
//            addView(view)


            // 方式2:动态添加tag
            val txtTag = TextView(context)
            // 标记默认未选中
            txtTag.tag = false
            txtTag.setLines(1)

            val layoutParams = MarginLayoutParams(
                2 * mTagHorizontalPadding + DisplayUtils.sp2px(
                    context,
                    mTagTextPaint.measureText(s).toInt()
                ),
                mTagHeight
            )

            layoutParams.leftMargin = mHorizontalMargin
            layoutParams.rightMargin = mHorizontalMargin
            layoutParams.topMargin = mVerticalMargin
            layoutParams.bottomMargin = mVerticalMargin
            txtTag.layoutParams = layoutParams

            txtTag.setPadding(
                mTagHorizontalPadding,
                mTagVerticalPadding,
                mTagHorizontalPadding,
                mTagVerticalPadding
            )

            txtTag.text = s
            txtTag.gravity = Gravity.CENTER

            txtTag.setTextColor(mTagTextColor)
            txtTag.textSize = mTagTextSize.toFloat()
            txtTag.background = context.getDrawable(mBgTagDrawableResId)
            txtTag.setOnClickListener {
                onTagClick(index, txtTag, s)
            }
            txtTag.setOnLongClickListener {
                onTagLongClick(index, txtTag, s)
                true
            }
            addView(txtTag)
        }
    }

    @SuppressLint("UseCompatLoadingForDrawables")
    private fun onTagLongClick(index: Int, @NonNull tag: TextView, @NonNull s: String) {
        if (mOnTagLongClickListener == null) {
            return
        }

        tag.tag = !(tag.tag as Boolean)
        if (tag.tag as Boolean) {
            tag.background = context.getDrawable(R.drawable.bg_tag_selected)
            tag.setTextColor(mTagTextColorSelected)
        } else {
            tag.background = context.getDrawable(R.drawable.bg_tag)
            tag.setTextColor(mTagTextColor)
        }

        mOnTagLongClickListener!!.onTagLongClicked(index, tag, s, tag.tag as Boolean)
    }

    /**
     * 对tag的点击
     */
    private fun onTagClick(index: Int, @NonNull tag: TextView, @NonNull s: String) {
        if (mOnTagClickListener == null) {
            return
        }

        mOnTagClickListener!!.onTagClicked(index, tag, s)
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }

    /**
     * Tag点击的监听器
     */
    interface OnTagClickListener {
        /**
         * 点击了tag
         *
         * @param index 位置游标
         * @param tag 被点击的tag
         * @param tagStr tag内容
         */
        fun onTagClicked(index: Int, @NonNull tag: TextView, @NonNull tagStr: String)
    }

    /**
     * 设置Tag被点击的监听器
     *
     * @param listener 监听器
     */
    fun setOnTagClickListener(listener: OnTagClickListener) {
        this.mOnTagClickListener = listener
    }

    /**
     * Tag点击的监听器
     */
    interface OnTagLongClickListener {
        /**
         * 点击了tag
         *
         * @param index 位置游标
         * @param tag 被点击的tag
         * @param tagStr tag内容
         * @param isSelected 是否选中
         */
        fun onTagLongClicked(
            index: Int,
            @NonNull tag: TextView,
            @NonNull tagStr: String,
            isSelected: Boolean
        )
    }

    /**
     * 设置Tag被点击的监听器
     *
     * @param listener 监听器
     */
    fun setOnTagLongClickListener(listener: OnTagLongClickListener) {
        this.mOnTagLongClickListener = listener
    }
}

页面布局见下

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".testtagcontainer.TestTagContainerActivity">

    <com.openld.seniorui.testtagcontainer.TagContainerView
        android:id="@+id/tag_container"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0"
        app:tag_bg_drawable="@drawable/bg_tag"
        app:tag_bg_drawable_selected="@drawable/bg_tag_selected"
        app:tag_horizontal_margin="5dp"
        app:tag_padding_horizontal="10dp"
        app:tag_padding_vertical="2dp"
        app:tag_text_color="@color/red"
        app:tag_text_color_selected="@color/white"
        app:tag_text_size="12sp"
        app:tag_vertical_margin="5dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

页面调用代码见下

package com.openld.seniorui.testtagcontainer

import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.openld.seniorui.R
import com.openld.seniorutils.utils.DisplayUtils
import kotlin.random.Random

class TestTagContainerActivity : AppCompatActivity(), TagContainerView.OnTagClickListener,
    TagContainerView.OnTagLongClickListener {
    private lateinit var mTagContainer: TagContainerView

    private lateinit var mTagList: MutableList<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_tag_container)

        initData()

        initWidgets()

        addListeners()
    }

    private fun initData() {
        mTagList = mutableListOf<String>()
        mTagList.add("我心依狂")
        mTagList.add("人间烟火")
        mTagList.add("泡沫")
        mTagList.add("Fight")
        mTagList.add("归去来")
        mTagList.add("童话")
        mTagList.add("绿色")
        mTagList.add("人世间")
        mTagList.add("如愿")
        mTagList.add("江南Style")
        mTagList.add("溯")
        mTagList.add("漠河舞厅")
        mTagList.add("孤勇者")
        mTagList.add("问明月")
        mTagList.add("奔赴星空")
        mTagList.add("落海")
        mTagList.add("半生雪")
        mTagList.add("水星记")
        mTagList.add("千千万万")
        mTagList.add("四季予你")
        mTagList.add("富士山下")
        mTagList.add("踏山河")
        mTagList.add("房间")
        mTagList.add("半生雪")
        mTagList.add("丑八怪")
        mTagList.add("痴心绝对")
        mTagList.add("独角戏")
        mTagList.add("飘摇")
        mTagList.add("安河桥")
        mTagList.add("星语心愿")
        mTagList.add("走马")
        mTagList.add("方圆几里")
        mTagList.add("月半弯")
        mTagList.add("认真的雪")
        mTagList.add("短发")
        mTagList.add("泡沫")
        mTagList.add("一生所爱")
        mTagList.add("黄昏")
        mTagList.add("千千阙歌")
        mTagList.add("太多")
        mTagList.add("年少有为")
        mTagList.add("再回首")
        mTagList.add("当爱已成往事")
        mTagList.add("我们的纪念")
        mTagList.add("借")
        mTagList.add("麻雀")
        mTagList.add("如果云知道")
        mTagList.add("时光慢旅")
        mTagList.add("当你老了")
        mTagList.add("狼")
        mTagList.add("追光者")
        mTagList.add("离人")
    }

    private fun initWidgets() {
        mTagContainer = findViewById(R.id.tag_container)
        mTagContainer.setTags(mTagList)

        val random = Random(1)

        for (index in 0 until mTagContainer.childCount) {
            val offsetDp = random.nextInt(10)
            val xOffset = DisplayUtils.dp2px(this, offsetDp).toFloat()

            if (offsetDp % 2 == 0) {
                val animator = ObjectAnimator.ofFloat(
                    mTagContainer.getChildAt(index),
                    "translationX",
                    -xOffset,
                    0f,
                    xOffset,
                    0f
                ).apply {
                    duration = 200
                    interpolator = AccelerateDecelerateInterpolator()
                    repeatCount = 4
                    start()
                }
            } else {
                val animator = ObjectAnimator.ofFloat(
                    mTagContainer.getChildAt(index),
                    "translationX",
                    xOffset,
                    0f,
                    -xOffset,
                    0f
                ).apply {
                    duration = 200
                    interpolator = AccelerateDecelerateInterpolator()
                    repeatCount = 4
                    start()
                }
            }
        }
    }

    private fun addListeners() {
        mTagContainer.setOnTagClickListener(this)
        mTagContainer.setOnTagLongClickListener(this)
    }

    override fun onTagClicked(index: Int, tag: TextView, tagStr: String) {
        Toast.makeText(this, "点击了第${index}个tag\n${tagStr}", Toast.LENGTH_SHORT).show()

        val animScaleX = ObjectAnimator.ofFloat(tag, "scaleX", 1.0f, 1.2f, 1.0f)
        animScaleX.repeatCount = 1

        val animScaleY = ObjectAnimator.ofFloat(tag, "scaleY", 1.0f, 1.2f, 1.0f)
        animScaleY.repeatCount = 1

        val set = AnimatorSet().apply {
            duration = 300
            interpolator = AccelerateDecelerateInterpolator()
            playTogether(animScaleX, animScaleY)
            start()
        }

    }

    override fun onTagLongClicked(index: Int, tag: TextView, tagStr: String, isSelected: Boolean) {
        if (isSelected) {
            Toast.makeText(this, "选择了\n${tagStr}", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "取消了\n${tagStr}", Toast.LENGTH_SHORT).show()
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值