瀑布流标签

来源:互联网 发布:淘宝魔术手 编辑:程序博客网 时间:2024/06/08 10:41

这里写图片描述

实现背景

打造Android中的流式布局和热门标签
看到鸿洋大神的这个视频教程有感而发,于是自己重新写了一遍这个viewgroup,大体思路一致,不过关键地方改成了我自己比较容易理解的算法:

  1. 对于标签直接的间距,鸿洋大神用的是给标签设置margin的方式,而我用的是对ViewGroup的自定义水平间距、垂直间距的方式;

  2. onLayout中,鸿洋大神先是判断标签的换行情况,然后再将每个标签依次摆放,其实判断标签换行的这个操作在onMeasure里面就已经执行过了,如果在onLayout再次判断换行计算宽高什么的,各种计算感觉很麻烦,容易出错,所以理解感觉好费劲,之前一直觉得他那种方法很烧脑,毕竟对于我这种菜鸟来说是跟不上大神的思维的;(我这算不算王婆卖瓜自卖自夸?)

使用步骤

先看这个自定义ViewGroup的用法:

  • 在attr.xml中声明如下自定义属性:
<?xml version="1.0" encoding="utf-8"?><resources>    <attr name="horizontal_diver" format="dimension"/>    <attr name="vertical_diver" format="dimension"/>    <declare-styleable name="FlowTagLayout">        <attr name="horizontal_diver"/>        <attr name="vertical_diver"/>    </declare-styleable></resources>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 布局文件中声明xmlns
xmlns:flowtag="http://schemas.android.com/apk/res-auto"
  • 1
  • 1
  • 然后就能直接使用了
    <com.passerby.androidadvanced.customviewgroup.FlowTagLayout        android:id="@+id/flowTagLayout"        android:layout_width="match_parent"        android:layout_height="wrap_content"        flowtag:horizontal_diver="5dp"        flowtag:vertical_diver="5dp">    </com.passerby.androidadvanced.customviewgroup.FlowTagLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • java代码中直接addView即可
        for (String tag : tagArr) {            TextView textView = makeTextView();            textView.setText(tag);            mFlowTagLayout.addView(textView);        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

是不是很简单。

源码解析

对于瀑布流标签的实现算法,鸿洋大神在视频教程里面已经说明的很清楚了,大家如果觉得文字性的东西比较难懂可以先去看他的视频,我要讲的算法跟他差不多,不过我会更多的描述跟自定义view有关的知识。
源码如下:

package com.passerby.androidadvanced.customviewgroup;import android.content.Context;import android.content.res.TypedArray;import android.text.TextUtils;import android.util.AttributeSet;import android.util.Log;import android.view.View;import android.view.ViewGroup;import com.passerby.androidadvanced.R;import java.util.ArrayList;import java.util.List;/** * Created by mac on 16/1/28. */public class FlowTagLayout extends ViewGroup {    private List<List<View>> mChildViews;    private List<Integer> mLinesHeight;    private int mHorizontalDiver, mVerticalDiver;    public FlowTagLayout(Context context) {        this(context, null);    }    public FlowTagLayout(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public FlowTagLayout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        mChildViews = new ArrayList<>();        mLinesHeight = new ArrayList<>();        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FlowTagLayout, defStyleAttr, 0);        int count = a.getIndexCount();        for (int i = 0; i < count; i++) {            int index = a.getIndex(i);            switch (index) {                case R.styleable.FlowTagLayout_horizontal_diver:                    mHorizontalDiver = (int) a.getDimension(i, 0);                    break;                case R.styleable.FlowTagLayout_vertical_diver:                    mVerticalDiver = (int) a.getDimension(i, 0);                    break;            }        }        a.recycle();    }    //只需要计算AT_MOST模式下的宽高;如果是EXACTLY模式,则直接根据ViewParent传进来的值进行设置就好    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int widthMode = MeasureSpec.getMode(widthMeasureSpec);        int widthSize = MeasureSpec.getSize(widthMeasureSpec);        int heightMode = MeasureSpec.getMode(heightMeasureSpec);        int heightSize = MeasureSpec.getSize(heightMeasureSpec);        mChildViews.clear();        mLinesHeight.clear();        //AT_MOST模式下计算出来的宽和高        int atmostWidth = 0, atmostHeight = 0;        int lineWidth = 0, lineHeight = 0;        List<View> lineChilds = new ArrayList<>();        int childCount = getChildCount();        for (int i = 0; i < childCount; i++) {            View child = getChildAt(i);            measureChild(child, widthMeasureSpec, heightMeasureSpec);            int childWidth = child.getMeasuredWidth() + mHorizontalDiver;            int childHeight = child.getMeasuredHeight() + mVerticalDiver;            if (lineWidth + childWidth < widthSize) {                //如果不需要换行,则直接将该child的宽度累加到改行的宽度中                lineWidth += childWidth;                //以高度最大的child的高度作为本行的高度                lineHeight = Math.max(lineHeight, childHeight);                lineChilds.add(child);            } else {                //如果需要换行,则计算出上一行的宽度跟上一行之前的宽度的较大值                atmostWidth = Math.max(atmostWidth, lineWidth);                //保存新行的第一个child的宽度                lineWidth = childWidth;                //叠加上一行的高度                atmostHeight += lineHeight;                //保存上一行的高度                mLinesHeight.add(lineHeight);                //保存上一行的所有child                mChildViews.add(lineChilds);                //新起一行                lineChilds = new ArrayList<>();                //保存新行的第一个child                lineChilds.add(child);            }            //处理最后一个            if (i == childCount - 1) {                atmostWidth = Math.max(atmostWidth, lineWidth);                atmostHeight += lineHeight;                mLinesHeight.add(lineHeight);                mChildViews.add(lineChilds);            }        }        int finalWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : atmostWidth;        int finalHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : atmostHeight;        v(String.format("width=%s,height=%s", finalWidth, finalHeight));        setMeasuredDimension(finalWidth, finalHeight);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int left = 0, top = 0;        int lineCount = mChildViews.size();        for (int i = 0; i < lineCount; i++) {            List<View> childs = mChildViews.get(i);            int childCount = childs.size();            for (int j = 0; j < childCount; j++) {                View child = childs.get(j);                int childWidth = child.getMeasuredWidth();                int childHeight = child.getMeasuredHeight();                child.layout(left + mHorizontalDiver,                        top + mVerticalDiver,                        left + mHorizontalDiver + childWidth,                        top + mVerticalDiver + childHeight);                left += mHorizontalDiver + childWidth;            }            //重置left到最左            left = 0;            //累加每行的高度,不能重置            top += mLinesHeight.get(i);        }    }    protected void v(String msg) {        final String text = msg;        if (!TextUtils.isEmpty(text)) {            Log.v(getClass().getCanonicalName(), text);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155

注释写的很清楚了,我只挑一部分说明。

代码39~54行是从布局文件中读取自定义属性的值。因为声明自定义属性的时候就是用的dimension格式,所以我们读取的时候直接用getDimension()来获取值即可;如果声明自定义属性的时候用的是Integer,那你用getInteger()来获取值之后,还需要将它转成px,因为自定义view计算各种距离、文字大小都是基于px的。

如果有人之前用paint.drawText()的时候感觉画出来的文字大小不对,那看到这你应该能知道原因了。

说完构造方法,就剩下onMeasure和onLayout了。在onMeasure中我分别使用 mChildViews、mLinesHeight这两个ArrayList来保存所有的子view以及每行标签的高度。

对于mChildViews这个集合,你可以把它想象成一个二维数组,mChildViews取出来的每个元素仍然是一个ArrayList,表示的是某一行的所有标签的view。
对于mLinesHeight,它的作用是为了便于在onLayout中遍历mChildViews的每行标签时提供改行高度的位置参考。

代码105~111行,为什么这里要单独处理最后一个?说准确一点其实这里处理的是最后一行。

你可以假设现在这个ViewGroup里面的标签行数只有一行,也就是说没有达到换行的条件,即没有走到onMeasure中的else那个分支,我们上面累加每行标签的高度、以及保存每行标签子view的时候都是在那个else里面完成的,现在标签数目没有足够换行,那不是意味着这行的行高以及所有的子view都没有被添加进来吗?

这将导致onMeasure方法最后计算出来的宽高并不准确,对于后面的onLayout方法当然也无法正确的被执行。

代码113~118行,计算宽高完成之后不要忘了设置计算出来的值,那两种计算模式应该不用多讲了吧。

再看onLayout,其实很简单了,因为复杂的操作以及在onMeasure中完成了。

两个for循环就是遍历之前存的mChildViews,你可以把它想象成一个二维数组,先取出第0行,然后遍历这行的所有标签,用child.layout来完成该标签的布局。以此类推。

child.layout方法的完整声明如下

public void layout(int l, int t, int r, int b)

  • l : 子view的左边相对于父容器左边的距离
  • t : 子view的顶部相对于父容器顶部的距离
  • r :子view的右边相对于父容器左边的距离
  • b:子view的底部相对于父容器顶部的距离

基本上就说完了,大家如果不明白的话,建议去看看上面的那个视频教程,如果还不明白,建议自己动手试试。

欢迎大家交流意见。

资源下载

0 0