[github高级控件]带你走近->自定义标签云

在项目中经常遇到一些标签云的效果,比如城市的选择,景点类型选择,酒店房型选择 这些常见使用标签云效果就比较好了。
这里写图片描述

这里写图片描述

今天一步步的写一个标签云view,借鉴于github上TagCloudView

1.准备工作

attr.xml

<resources>
    <declare-styleable name="TagTextViewStyle">
        <attr name="t_textSize" format="dimension"/>
        <attr name="t_textColor" format="color"/>
        <attr name="t_itemBorder" format="dimension"/>
        <attr name="t_viewBorder" format="dimension"/>
        <attr name="t_tagBackground" format="reference"/>
        <attr name="t_singleLine" format="boolean"/>
        <attr name="t_imageWidth" format="dimension"/>
        <attr name="t_imageHeight" format="dimension"/>
        <attr name="t_rightArrow" format="integer"/>
        <attr name="t_showArrow" format="boolean"/>
        <attr name="t_showMore" format="boolean"/>
        <attr name="t_moreTextStr" format="string"/>
        <attr name="t_moreTextWidth" format="dimension"/>
    </declare-styleable>

</resources>

然后定义一个TagTextView,继承与ViewGroup,接下来就从attr中获取这些属性

 TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.TagTextViewStyle, defStyleAttr, defStyleAttr);
        mTextSize = typedArray.getDimensionPixelSize(R.styleable.TagTextViewStyle_t_textSize, 12);
    //更多属性自行实现...

2.重写onMeasure

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //1,计算子view

        //2,计算tag view 实际需要高度

        //3,根据高度设置

    }


计算子view

MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由size和mode组成,它有三种模式:UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;AT_MOST(至多),子元素至多达到指定大小的值

这里不指定具体的mode和size作特殊处理,只是使用到size和mode

      //计算 ViewGroup 上级容器为其推荐的宽高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
        mHeightSize = MeasureSpec.getSize(heightMeasureSpec);

        // 计算出所有的childView的宽和高  
        measureChildren(widthMeasureSpec, heightMeasureSpec);

measureChildren一行代码就计算了所以的childview的宽,高,这里不具体分析,可以自行查看源码分析measureChildren原理。


计算TagTextView的高度并确定item的位置

如果所有标签没有超过1行,那么TagTextView的高度就是item的高度;如果超过一行,那么高度h=item高度*行数+所有间距

这里实现2中模式:多行模式和单行模式
先介绍怎么实现多行模式

 private int getMultiTotalHeight(int totalWidth, int totalHeight) {
        int childWidth;
        int childHeight;
    //遍历所有的子view
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            //总的宽度 = 所有子view宽+item间距
            totalWidth += childWidth + mItemBorder;
            //设置第一行高度 因为layout t = totalHeight - childHeight
            //要让totalHeight - childHeight不为负数 所以先设置一行高度
            if (i == 0) {
                totalHeight = childHeight + mItemBorder;
            }
            //2种情况所有item宽度大于viewgroup宽度,这时候需要换行;不大于viewgroup宽度,继续
            //所有的totalWidth=item宽度+之间的边距,加上左边第一个item与viewGroup边距mViewBorder,加上右边mItemBorder边距,大于viewGroup的宽,需要换行
            if (totalWidth + mItemBorder + mViewBorder > mWidthSize) {
                totalWidth = mItemBorder;//换行设置间距
                //高度 = 原高度 + item高度 + item间距
                totalHeight += childHeight + mItemBorder;
                //layout确定子view在view group中的位置
                child.layout(
                        totalWidth + mViewBorder,
                        totalHeight - childHeight,
                        totalWidth + childWidth + mViewBorder,
                        totalHeight);
                totalWidth += childWidth;
            } else {
                //横排:起始 间隔viewboder距离开始,到总的width(items+item间距)+view间距
                //竖排:起始 离顶部viewboder间距开始,到总的height
                child.layout(totalWidth - childWidth + mViewBorder,
                        totalHeight - childHeight,
                        totalWidth + mViewBorder,
                        totalHeight);
            }
        }

        return totalHeight + mItemBorder;
    }

设置高度

 //子view计算的宽,高度去决定测量的宽高值
  setMeasuredDimension(mWidthSize, heightMode == MeasureSpec.EXACTLY ? mHeightSize : totalHeight);

做完上面的步骤,view的测量,布局都已经完成,接下来只需要把item view放进来。

添加item到ViewGroup

 public void setTags(List<String> tagDatas) {
        if (tagDatas == null) {
            return;
        }
        mTags = tagDatas;
        //先清空所有的view
        removeAllViews();
        String tag;
        for (int i = 0; i < mTags.size(); i++) {
            tag = tagDatas.get(i);
            //加载布局文件
            TextView tagView = (TextView) mInflater.inflate(R.layout.layout_item, null);
            tagView.setText(tag);
            tagView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
            tagView.setBackgroundResource(mTagBackground);
            tagView.setTextColor(mTextColor);
            LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            tagView.setLayoutParams(params);
          //添加view
            addView(tagView);  
        }
    //刷新
        postInvalidate();
    }

多行显示tag就在这里写完了,运行,看下标签云的效果

写完多行模式,接下来写单行模式,我们会问,多行模式下不超过viewgroup宽度不就是单行吗,为什么还要定义单行模式?
这里的单行模式是指不管有多少item都值显示单行,超出的item不显示,用”…”代替。

单行模式与多行模式基本类似,主要区别在于获取totalHeight和addView部分。接下来对这两个部分进行实现。

单行模式实现

初始化单行模式view

 private void initSingleLineView(int widthMeasureSpec, int heightMeasureSpec) {
     //判断是否是单行模式
        if (!mSingleLine) {
            return;
        }
        //是否显示向右箭头
        if (mShowArrow) {
            mArrowIv = new ImageView(getContext());
            mArrowIv.setImageResource(mArrowResId);
            mArrowIv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
            mArrowIv.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            measureChild(mArrowIv, widthMeasureSpec, heightMeasureSpec);
            mArrowIconWidth = mArrowIv.getMeasuredWidth();
            mImageHeight = mArrowIv.getMeasuredHeight();
            addView(mArrowIv);
        }
    //是否显示更多item
        if (mShowMore) {
            mMoreTextTv = (TextView) mInflater.inflate(R.layout.layout_item, null);
            mMoreTextTv.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
            mMoreTextTv.setTextColor(mTextColor);
            mMoreTextTv.setBackgroundResource(mTagBackground);
            @SuppressLint("DrawAllocation")
            LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            mMoreTextTv.setLayoutParams(layoutParams);
            mMoreTextTv.setText(mMoreTextStr == null || mMoreTextStr.equals("") ? "..." : mMoreTextStr);
            measureChild(mMoreTextTv, widthMeasureSpec, heightMeasureSpec);
            mMoreTextWidth = mMoreTextTv.getMeasuredWidth();
            addView(mMoreTextTv);
        }

    }

计算单行模式高度及确定位置2

单行模式看起来代码比较多,其实并不复杂,只是比多行模式多了一个arrow icon和查看更多item

这里写图片描述

private int getSingleTotalHeight(int totalWidth, int totalHeight) {
        int childWidth;
        int childHeight = 0;

        totalWidth += mViewBorder;//设置左边距
    //上图红色边框的总宽度
        int textTotalWidth = getTextTotalWidth();
        //items 总宽度小于 viewgroup - 箭头图片width 不用显示更多item
        if (textTotalWidth < mWidthSize - mArrowIconWidth) {
            mMoreTextStr = null;
            mMoreTextWidth = 0;
        }
        //计算item中宽度并确定布局
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();

            if (i == 0) {
                totalWidth += childWidth;
                totalHeight = childHeight + mViewBorder;
            } else {
                totalWidth += childWidth + mItemBorder;
            }
    //左边距+items宽+item边距+更多item宽+arrow icon宽+右边距 < viewgroup宽
            if (mViewBorder + totalWidth + mItemBorder +   mMoreTextWidth + mArrowIconWidth+ mViewBorder  < mWidthSize) {
              //横排:起始 间隔mItemBorder距离开始,到总的width(items+item间距)+mItemBorder间距
                //竖排:起始 离顶部viewboder间距开始,到总的height
                child.layout(
                        totalWidth - childWidth + mItemBorder,
                        totalHeight - childHeight,
                        totalWidth + mItemBorder,
                        totalHeight);
            } else {
            //超出部分不显示 ,前面已经加上了,所以这里需要减去
                totalWidth -= childWidth + mViewBorder;
                break;
            }
        }
        //更多item
        if (mMoreTextTv != null) {
            //起始位置 从最后一个item+item边距位置x开始 
            //结束位置 x+moretextWidth
            mMoreTextTv.layout(
                    mViewBorder+ totalWidth + mItemBorder,
                    totalHeight - childHeight,
                    totalWidth + mViewBorder + mItemBorder + mMoreTextWidth,
                    totalHeight);
        }

        totalHeight += mViewBorder;
        //箭头 与上面类似
        if (mArrowIv != null) {
            mArrowIv.layout(
                    mWidthSize - mArrowIconWidth - mViewBorder,
                    (totalHeight - mImageHeight) / 2,
                    mWidthSize - mViewBorder,
                    (totalHeight - mImageHeight) / 2 + mImageHeight);
        }

        return totalHeight;
    }

这样单行模式也就完成了,剩下的就是加入监听事件,优化等,功能扩展,这里不多介绍

完整demo下载地址:TagTextView

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值