View绘制实战——热门标签

上一篇文章已经对View绘制的流程进行了讲解,并用其解决了ListView的嵌套问题。今天我们再次通过热门标签这种常用的自定义控件来演示View绘制的综合运用。

本篇文章讲了热门标签这一简单自定控件的三种实现。其中,前两种基于onMeasure、onLayout方法,最后一种基于LinearLayout布局。

这样做的目的是在加强对View绘制理解的同时,注意实现过程的多样性,增强对知识的灵活运用能力。自定义控件的实现方式可能存在多种,不能拘泥于某一种实现,要能够扩散思维,从而找到最佳或者最适合自己的实现方式。比如,我们之前讲过的圆角图形,就可以通过BitmapShader和Xfermode两种方式来实现。

言归正传,先上效果图,图中为第一种(上不)和第三种(下部)方式实现效果。

这里写图片描述

从图中看上下两部分效果一样,这也是我们的目的,但实现过程却有一定差异。

第一种实现方式

这种实现方式需要继承自ViewGroup,核心点为:

  • 在onMeasure方法测量出控件本身的宽高
  • 在onLayout方法里计算控件的位置并摆放
  • 设置字符串数据,添加View到ViewGroup
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalWidth = MeasureSpec.getSize(widthMeasureSpec);//获得总宽度
        mLeftPadding = getPaddingLeft();//左侧padding
        mTopPadding = getPaddingTop();//右侧padding
        mRightPadding = getPaddingRight();//上部padding
        mBottomPadding = getPaddingRight();//上部padding
        int x = mLeftPadding;
        int lineNum = 0;//行数
        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
            int width = view.getMeasuredWidth();
            height = view.getMeasuredHeight();

            if (0 == i || (x + width > mTotalWidth - mRightPadding)) {
                lineNum++;//新一行
                x = mLeftPadding;
            }
            x = x + width + ROW_SPACE;
        }
        setMeasuredDimension(mTotalWidth,
                mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height + mBottomPadding);
    }

onMeasure方法

  • 首先通过MeasureSpec获得自身宽度以及各方向的padding,x记录子View的起始位置,默认为mLeftPadding。

  • 然后,for循环子View,获得每个子View的宽高,如果是第一个子View或者剩余宽度不够存当前子View(x+width为假如子View放到该行时右边的x坐标,如果这个坐标大于mTotalWidth - mRightPadding即控件右边可用控件的x坐标)lineNum+1,然后把x恢复到mLeftPadding。

  • 接着,x坐标变为下一个子View的起始位置,即x+width+ROW_SPACE;

  • 最后,调用setMeasuredDimension设置获得的大小。宽度不管mode是啥,设置为原有的宽度。高度为 mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height + mBottomPadding,即上下padding,加上行数乘以行高,加上(行数-1)乘以行间距。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int x = mLeftPadding;
        int lineNum = 0;//行数
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            if (0 == i || (x + width > mTotalWidth - mRightPadding)) {
                lineNum++;//新一行
                x = mLeftPadding;
            }
            view.layout(x, mTopPadding + (lineNum - 1) * (height + LINE_SPACE),
                    x + width, mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height);
            x = x + width + ROW_SPACE;
        }
    }

onLayout方法跟onMeasure方法有点类似:

  • 首先,x记录子View的起始位置,默认为mLeftPadding。

  • 然后,for循环子View,获得每个子View的宽高,如果是第一个子View或者剩余宽度不够存当前子View(x+width为假如子View放到该行时右边的x坐标,如果这个坐标大于mTotalWidth - mRightPadding即控件右边可用控件的x坐标)lineNum+1,然后把x恢复到mLeftPadding。

  • 接着,调用 view.layout设置摆放子View本身。left为x,top为mTopPadding + (lineNum - 1) * (height + LINE_SPACE),right为x + width,bottom为mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height。

  • 最后,x坐标变为下一个子View的起始位置,即x+width+ROW_SPACE;

    /***
     * 设置标签数据
     * @param tagStrList
     */
    public void setViewData(List<String> tagStrList) {
        removeAllViews();//清除所有view
        if (null == tagStrList || tagStrList.size() < 1) {
            return;
        }
        LayoutInflater m_Inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        for (int i = 0; i < tagStrList.size(); i++) {
            TextView aTextView = (TextView) m_Inflater.inflate(
                    R.layout.item_tag, null);
            if (aTextView != null) {
                aTextView.setText(tagStrList.get(i));
                addView(aTextView);
            }
        }
    }

setViewData方法为设置数据,然后利用m_Inflater来获得一个TextView,给这个TextView设置要显示的内容,添加到布局中。这个TextView其实也可以用代码方式创建,但是一个情况下这个TextView都需要设定一些特定的样式及背景,用XML的方式配置比较直观。

第二种实现方式

这种方式同样需要继承自ViewGroup,可以看做在第一种方式上的改进。因为通过第一种的实现过程可以看出,在onMeasure方法计算控件本身宽高的同时,实际上已经能够确定子View的位置。通过对子View位置的保存,减少了在onLayout中重复计算工作,直接对子View进行摆放即可。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalWidth = MeasureSpec.getSize(widthMeasureSpec);//获得总宽度
        mLeftPadding = getPaddingLeft();//左侧padding
        mTopPadding = getPaddingTop();//右侧padding
        mRightPadding = getPaddingRight();//上部padding
        mBottomPadding = getPaddingRight();//上部padding
        int x = mLeftPadding;
        int lineNum = 0;//行数
        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
            int width = view.getMeasuredWidth();
            height = view.getMeasuredHeight();

            if (0 == i || (x + width > mTotalWidth - mRightPadding)) {
                lineNum++;//新一行
                x = mLeftPadding;
            }
            view.setLeft(x);
            view.setTop(mTopPadding + (lineNum - 1) * (height + LINE_SPACE));
            view.setRight(x + width);
            view.setBottom(mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height);
            x = x + width + ROW_SPACE;
        }
        setMeasuredDimension(mTotalWidth,
                mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height + mBottomPadding);
    }

与第一种方式的onMeasure方法相比,增加了一下几行代码:

            view.setLeft(x);
            view.setTop(mTopPadding + (lineNum - 1) * (height + LINE_SPACE));
            view.setRight(x + width);
            view.setBottom(mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height);

主要是保存当前子View的位置,left为x,top为mTopPadding + (lineNum - 1) * (height + LINE_SPACE),right为x + width,bottom为mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height。

特别注意的是: x = x + width + ROW_SPACE;代表着下一个子View的起始位置,所以应将当前子View一切计算完成后,再调用该行代码。

第三种实现方式

这种方式需要继承自LinearLayout,跟View的绘制知识关联相对较少,核心点为:

  • 设置布局的Orientation为VERTICAL,传入字符串数据
  • 每一行用一个LinearLayout包裹并将这个LinearLayout添加到布局中
  • 通过子View的宽度判断是否需要换行,同时调整相应的布局参数
public void setViewData(List<String> tagStrList) {
        removeAllViews();//清除所有view
        if (null == tagStrList || tagStrList.size() < 1) {
            return;
        }
        setOrientation(VERTICAL);
        LayoutInflater m_Inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        int surplus;
        int i = 0;
        while (i < tagStrList.size()) {
            LinearLayout wrapLayout = new LinearLayout(getContext());
            wrapLayout.setOrientation(HORIZONTAL);
            LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            if (i > 0) {
                params.topMargin = LINE_SPACE;
            }
            surplus = mContentWidth;
            wrapLayout.setLayoutParams(params);
            addView(wrapLayout);
            for (int j = i; j < tagStrList.size(); j++) {
                TextView aTextView = (TextView) m_Inflater.inflate(R.layout.item_tag, null);
                aTextView.setText(tagStrList.get(i));
                aTextView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
                int width = aTextView.getMeasuredWidth();
                if (surplus - width >=0) {
                    if (surplus!=mContentWidth) {
                        LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                        textParams.leftMargin = ROW_SPACE;
                        aTextView.setLayoutParams(textParams);
                    }
                    surplus = surplus - width - ROW_SPACE;
                    wrapLayout.addView(aTextView);
                    i++;
                } else {
                    break;
                }

            }
        }
    }
  • 首先,清除所有子View,设置Orientation为VERTICAL。

  • 然后,while循环添加LinearLayout,这个LinearLayout的rientation为HORIZONTAL。如果不是第一个需要给布局参数params设置topMargin。

  • 接着,for循环给单个LinearLayout添加子View。这些子View需要判断是否为第一个,如果不是第一个的话需要设置leftMargin。surplus代表该行LinearLayout还可存放子View的空间,如果不能够存放下一个子View,则跳出for循环,进入到While循环中,切换到新的一行。

这种方式的实现,主要是利用了LinearLayout布局的特性来完成的,比较容易理解,但可能存在布局多层嵌套的问题,有一定的性能的影响。

另外需要注意的是,mContentWidth是在onSizeChanged中获得的,因此在调用setViewData需要保证View得到绘制。
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mContentWidth = w - getPaddingLeft() - getPaddingRight();
    }
方法调用
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String testStr = "测试测试测试测试测试";
        final List<String> stringList = new ArrayList<>();
        for (int i = 0; i < 12; i++) {
            stringList.add(testStr.substring(0, 1 + (new java.util.Random().nextInt(testStr.length() - 1))));
        }

        TagViewByViewGroup tagViewByViewGroup = findViewById(R.id.tagview_by_viewgroup);
        tagViewByViewGroup.setViewData(stringList);

        final TagViewByLinearLayout tagViewByLinearLayout = findViewById(R.id.tagview_by_linearlayout);
        tagViewByLinearLayout.post(new Runnable() {
            @Override
            public void run() {
                tagViewByLinearLayout.setViewData(stringList);
            }
        });


    }

从字符串中随机截取一段,然后添加到list中。找到对应控件,调用setViewData设置数据。TagViewByLinearLayout利用post方法保证onSizeChanged调用后再设置数据,否则会因为mContentWidth为0,设置不上数据。

以上就完成了热门标签的开发,但仅具备基础功能。如果需要点击事件以及背景切换的只需对子View设置即可,相对简单,本文就不再介绍了。

老规矩,项目地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值