Android自定义View系列:标签LabelView实战篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shayubuhuifei/article/details/84527374

前言部分

本文主要介绍如何自定义一个常见的labels标签,功能上主要支持,单选、多选、点击三种模式。因为这个使用率很高,并且这个是比较典型学习自定义ViewGroup的例子,所以特意动手实践,加深对Android的认识。这个项目主要是为了自己学习使用,所以并不是很完善,先上一个效果图,了解一下:

在这里插入图片描述

内容部分

  1. ViewGroup的定义主要还是分布在两个部分,一个是测量,另一个是布局。layout子view是作为容器最基本的工作。

  2. 测量的部分主要还是遵循view的三种测量模式来不同处理。介绍测量规则的博客很多,这里不多解释。

    {@link android.view.View.MeasureSpec#UNSPECIFIED},
    {@link android.view.View.MeasureSpec#AT_MOST} or
    {@link android.view.View.MeasureSpec#EXACTLY}
    
  3. 布局部分主要是需要我们特殊处理的地方,因为我们的每个labels的大小是根据内容决定的,所以我们要自己根据view的尺寸进行摆放。

代码实现

首先介绍onMeasure方法中的实现,根绝子view的尺寸来决定容器view的测量情况。

private void measureMyChild(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //宽度
        maxWidth = MeasureSpec.getSize(widthMeasureSpec);
        int count = getChildCount();
        //总高度
        int contentHeight = 0;
        //记录最宽的行宽
        int maxLineWidth = 0;
        // 每行宽度
        int startLayoutWidth = 0;
        //一行中子控件最高的高度,用于决定下一行高度应该在目前基础上累加多少
        int maxChildHeight = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            LogUtil.i("onLayout--getPaddingRight:" +
                    child.getPaddingRight() +
                    "getPaddingLeft:" + child.getPaddingLeft());

            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //测量的宽高
            int childMeasureWidth = child.getMeasuredWidth();
            int childMeasureHeight = child.getMeasuredHeight();
            LogUtil.i("onLayout--width:" + maxWidth + "startLayoutWidth:");
            if (startLayoutWidth + childMeasureWidth < maxWidth) {
                //如果一行没有排满,继续往右排列
                startLayoutWidth += childMeasureWidth + margiHorizontal;
            } else {
                // 初始化为0
                maxChildHeight = 0;
                startLayoutWidth = 0;

            }
            if (childMeasureHeight > maxChildHeight) {
                maxChildHeight = childMeasureHeight;
            }
            //获取总的高度
            contentHeight += maxChildHeight + margiVertical;
            //获取最长的行总的宽度
            maxLineWidth = Math.max(maxLineWidth, startLayoutWidth);
        }

        //如果没有子元素,就设置宽高都为0(简化处理)
        if (getChildCount() == 0) {
            setMeasuredDimension(0, 0);
        } else
            //宽和高都是AT_MOST,则设置宽度最宽的字元素的宽度的和;高度设置为最高的元素的高度;
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(maxLineWidth, contentHeight);
            }
            //如果宽度是wrap_content,则宽度为最宽的一行的宽度
            else if (widthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(maxLineWidth, heightSize);
            }
            //如果高度是wrap_content,则高度为最高的字元素的高度
            else if (heightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthSize, contentHeight);
            }

    }

宽度的测量过程是,最大的宽度直接是取的容器view的尺寸。然后我们计算每一行的子view尺寸,通过求和来和容器view的最大宽度进行比较(公式:startLayoutWidth + childMeasureWidth < maxWidth)累加的宽度加上下一个字view的宽度和最大宽度比较来决定是否进行换行。

高度的测量过程是,先把所有的子view的高度进行相加求和,在根据容器view的mode来进行尺寸选择。这里内容都是常规的测量原则。主要区别还说在于宽高的取值。

宽高的取值我们的原则是,高度我们取值为最高的字元素;宽度我们取值为字元素相加,最宽的一行字元素。

测量部分的内容就是这些,主要还说需要我们对测量规则进行了解。然后根据我们自己的需求来进行选择。


下面介绍布局子view的部分代码

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        LogUtil.i("onLayout--width:" + maxWidth);
        final int count = getChildCount();
        int childMeasureWidth = 0;
        int childMeasureHeight = 0;
        // 开始的X位置
        int startLayoutWidth = getPaddingLeft();
        // 开始的Y位置
        int startLayoutHeight = getPaddingTop();
        //一行中子控件最高的高度,用于决定下一行高度应该在目前基础上累加多少
        int maxChildHeight = 0;
        for (int i = 0; i < count; i++) {
            int position = i;
            TextView child = (TextView) getChildAt(i);
            //注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
            childMeasureWidth = child.getMeasuredWidth() + child.getPaddingLeft() + child.getPaddingRight();
            childMeasureHeight = child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom();
            LogUtil.i("onLayout--width:" + maxWidth + "startLayoutWidth:" + startLayoutWidth);
            if (startLayoutWidth + childMeasureWidth < maxWidth - getPaddingRight()) {
                //如果一行没有排满,继续往右排列
                left = startLayoutWidth;
                right = left + childMeasureWidth;
                top = startLayoutHeight;
                bottom = top + childMeasureHeight;
            } else {
                //排满后换行
                startLayoutWidth = getPaddingLeft();
                startLayoutHeight += maxChildHeight + margiVertical;
                maxChildHeight = 0;

                left = startLayoutWidth;
                right = left + childMeasureWidth;
                top = startLayoutHeight;
                bottom = top + childMeasureHeight;
            }
            //宽度累加
            startLayoutWidth += childMeasureWidth + margiHorizontal;
            if (childMeasureHeight > maxChildHeight) {
                maxChildHeight = childMeasureHeight;
            }
            //确定子控件的位置,四个参数分别代表(左上右下)点的坐标值
            child.layout(left, top, right, bottom);
            initListener(child, position);
        }

    }

上面内容主要分为两部分,一部分是宽度的布局,一部分是高度的布局。

宽度布局:主要是根据子view的宽度和容器view的宽度的比较,来决定什么时候进行换行。这里需要注意的一些边界值,如我们经常使用的margin和padding值。

高度布局:主要是根据最高的子view的高度决定每一行的高度,这样可以让我们每一行都保持一样的高度。


完成上面两个大的步骤,基本上这个view也就完成的差不多了。

额外注意的点:

因为标签是通过创建textview设置属性添加到容器中,所以这里设置文字颜色变化的方法和在xml中有些区别:

//设置每一个标签
    private void drawTextView() {
        for (String text : textList) {
            TextView label = new TextView(context);
            label.setPadding(30, 30, 0, 0);
            label.setTextSize(TypedValue.COMPLEX_UNIT_PX, 40);
            label.setBackgroundResource(R.drawable.selector_text_bg);
            label.setText(text);
            label.setTextColor(createColorStateList("#ffffffff", "#ff44e6ff"));
            addView(label);
        }
    }

//工具方法
 private static ColorStateList createColorStateList(String selected, String normal) {
        int[] colors = new int[]{Color.parseColor(selected), Color.parseColor(normal)};
        int[][] states = new int[2][];
        states[0] = new int[]{android.R.attr.state_selected};
        states[1] = new int[]{};
        ColorStateList colorList = new ColorStateList(states, colors);
        return colorList;
    }

主要是介绍ColorStateList的使用,通过映射关系来进行颜色变化。

以上的步骤也可以通过填充xml中的textview来实现,这种实现方式你可以更轻松的设置你的textview。以前定义过的一个蚂蚁森林能量球效果,就说这种方式实现。蚂蚁森林效果

结束语

读万卷书,行万里路。虽然这些东西早就有人实现了,我们也许也使用过,但是亲自实践的必要性还是在的。

你的鼓励是我前进的动力。

猜你喜欢

转载自blog.csdn.net/shayubuhuifei/article/details/84527374
今日推荐