常用的自定义View例子一( FlowLayout) 在Android开发中,我们经常会遇到流布式的布局,经常会用来一些标签的显示,比如qq中个人便签,搜索框下方提示的词语,这些是指都是流布式的布局,今天我就我们日常开放中遇到的流布式布局坐一些总结
转载请注明博客地址: http://blog.csdn.net/gdutxiaoxu/article/details/51765428
**源码下载地址: https://github.com/gdutxiaoxu/CustomViewDemo.git **
1. 先给大家看一下效果
仔细观察,我们可以知道图二其实是图一效果的升级版,图一当我们控件的宽度超过这一行的时候,剩余的宽度它不会自动分布到每个控件中,而图二的效果当我们换行的时候,如控件还没有占满这一行的时候,它会自动把剩余的宽度分布到每个控件中
2.废话不多说了,大家来直接看来看一下图一的源码 1)代码如下 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 156 157 public class SimpleFlowLayout extends ViewGroup { private int verticalSpacing = 20 ; public SimpleFlowLayout (Context context ) { super (context); } @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); int widthUsed = paddingLeft + paddingRight; int heightUsed = paddingTop + paddingBottom; int childMaxHeightOfThisLine = 0 ; int childCount = getChildCount(); for (int i = 0 ; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { int childUsedWidth = 0 ; int childUsedHeight = 0 ; measureChild(child,widthMeasureSpec,heightMeasureSpec); childUsedWidth += child.getMeasuredWidth(); childUsedHeight += child.getMeasuredHeight(); Rect marginRect = getMarginRect(child); int leftMargin=marginRect.left; int rightMargin=marginRect.right; int topMargin=marginRect.top; int bottomMargin=marginRect.bottom; childUsedWidth += leftMargin + rightMargin; childUsedHeight += topMargin + bottomMargin; if (widthUsed + childUsedWidth < widthSpecSize) { widthUsed += childUsedWidth; if (childUsedHeight > childMaxHeightOfThisLine) { childMaxHeightOfThisLine = childUsedHeight; } } else { heightUsed += childMaxHeightOfThisLine + verticalSpacing; widthUsed = paddingLeft + paddingRight + childUsedWidth; childMaxHeightOfThisLine = childUsedHeight; } } } heightUsed += childMaxHeightOfThisLine; setMeasuredDimension(widthSpecSize, heightUsed); } @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); int childStartLayoutX = paddingLeft; int childStartLayoutY = paddingTop; int widthUsed = paddingLeft + paddingRight; int childMaxHeight = 0 ; int childCount = getChildCount(); for (int i = 0 ; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { int childNeededWidth, childNeedHeight; int left, top, right, bottom; int childMeasuredWidth = child.getMeasuredWidth(); int childMeasuredHeight = child.getMeasuredHeight(); Rect marginRect = getMarginRect(child); int leftMargin=marginRect.left; int rightMargin=marginRect.right; int topMargin=marginRect.top; int bottomMargin=marginRect.bottom; childNeededWidth = leftMargin + rightMargin + childMeasuredWidth; childNeedHeight = topMargin + topMargin + childMeasuredHeight; if (widthUsed + childNeededWidth <= r - l) { if (childNeedHeight > childMaxHeight) { childMaxHeight = childNeedHeight; } left = childStartLayoutX + leftMargin; top = childStartLayoutY + topMargin; right = left + childMeasuredWidth; bottom = top + childMeasuredHeight; widthUsed += childNeededWidth; childStartLayoutX += childNeededWidth; } else { childStartLayoutY += childMaxHeight + verticalSpacing; childStartLayoutX = paddingLeft; widthUsed = paddingLeft + paddingRight; left = childStartLayoutX + leftMargin; top = childStartLayoutY + topMargin; right = left + childMeasuredWidth; bottom = top + childMeasuredHeight; widthUsed += childNeededWidth; childStartLayoutX += childNeededWidth; childMaxHeight = childNeedHeight; } child.layout(left, top, right, bottom); } } } private Rect getMarginRect (View child) { LayoutParams layoutParams = child.getLayoutParams(); int leftMargin = 0 ; int rightMargin = 0 ; int topMargin = 0 ; int bottomMargin = 0 ; if (layoutParams instanceof MarginLayoutParams) { MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams; leftMargin = marginLayoutParams.leftMargin; rightMargin = marginLayoutParams.rightMargin; topMargin = marginLayoutParams.topMargin; bottomMargin = marginLayoutParams.bottomMargin; } return new Rect (leftMargin, topMargin, rightMargin, bottomMargin); } }
2)思路解析
首先我们重写onMeasure方法,在OnMeasure方法里面我们调用measureChild()这个方法去获取每个孩子的宽度和高度,每次增加一个孩子我们执行 widthUsed += childUsedWidth;
添加完一个孩子以后我们判断widthUsed是够超出控件本身的最大宽度widthSpecSize, 若没有超过执行
widthUsed += childUsedWidth;
if (childUsedHeight > childMaxHeightOfThisLine) {
childMaxHeightOfThisLine = childUsedHeight;
}
超过控件的宽度执行
heightUsed += childMaxHeightOfThisLine + verticalSpacing;
widthUsed = paddingLeft + paddingRight + childUsedWidth;
childMaxHeightOfThisLine = childUsedHeight;
最后调用 setMeasuredDimension(widthSpecSize, heightUsed);这个方法去设置它的大小 3.在OnLayout方法里面,所做的工作就是去摆放每一个孩子的位置 ,判断需不需要换行,不需要更改left值,需要换行,更改top值
3)注意事项 讲解之前,我们先来了解一下一个基本知识
从这张图片里面我们可以得出这样结论
Width=控件真正的宽度(realWidth)+PaddingLeft+PaddingRight
margin是子控件相对于父控件的距离
注意事项
为了支持控件本身的padding属性,我们做了处理,主要代码如下1 2 3 4 5 6 7 8 9 int widthUsed = paddingLeft + paddingRight; int heightUsed = paddingTop + paddingBottom; ---------- if (widthUsed + childUsedWidth < widthSpecSize) { widthUsed += childUsedWidth; if (childUsedHeight > childMaxHeightOfThisLine) { childMaxHeightOfThisLine = childUsedHeight; } }
为了支持子控件的margin属性,我们同样也做了处理1 2 3 4 5 6 7 8 Rect marginRect = getMarginRect(child); int leftMargin=marginRect.left; int rightMargin=marginRect.right; int topMargin=marginRect.top; int bottomMargin=marginRect.bottom; childUsedWidth += leftMargin + rightMargin; childUsedHeight += topMargin + bottomMargin;
即我们在计算孩子所占用的宽度和高度的时候加上margin属性的高度,接着在计算需要孩子总共用的宽高度的时候加上每个孩子的margin属性的宽高度,这样自然就支持了孩子的margin属性了
4.缺陷 如下图所见,在控件宽度参差不齐的情况下,控件换行会留下一些剩余的宽度,作为想写出鲁棒性的代码的我们会觉得别扭,于是我们相处了解决办法。
解决方法见下面
图二源码解析
废话不多说,先看源码 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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 public class PrefectFlowLayout extends ViewGroup { public PrefectFlowLayout (Context context) { super (context); } public PrefectFlowLayout (Context context, AttributeSet attrs) { super (context, attrs); } public PrefectFlowLayout (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); } private int parentWidthSize; private int horizontalSpacing = 16 ; private int verticalSpacing = 16 ; private Line currentLine; private List<Line> mLines = new ArrayList <>(); private int userWidth = 0 ; private class Line { private List<View> children; private int height; private int lineWidth = 0 ; public Line () { children = new ArrayList <>(); } private void addChild (View child) { children.add(child); if (child.getMeasuredHeight() > height) { height = child.getMeasuredHeight(); } lineWidth += child.getMeasuredWidth(); } public int getHeight () { return height; } public int getChildCount () { return children.size(); } public void onLayout (int l, int t) { lineWidth += horizontalSpacing * (children.size() - 1 ); int surplusChild = 0 ; int surplus = parentWidthSize - lineWidth; if (surplus > 0 ) { surplusChild = (int ) (surplus / children.size()+0.5 ); } for (int i = 0 ; i < children.size(); i++) { View child = children.get(i); child.getLayoutParams().width=child.getMeasuredWidth()+surplusChild; if (surplusChild>0 ){ child.measure(MeasureSpec.makeMeasureSpec( child.getMeasuredWidth()+surplusChild,MeasureSpec.EXACTLY) ,MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY)); } child.layout(l, t, l + child.getMeasuredWidth(), t + child.getMeasuredHeight()); l += child.getMeasuredWidth() + horizontalSpacing; } } } @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { mLines.clear(); currentLine = null ; userWidth = 0 ; int widthMode = MeasureSpec.getMode(widthMeasureSpec); parentWidthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); int heigthMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); int childWidthMode, childHeightMode; if (widthMode == MeasureSpec.EXACTLY) { childWidthMode = MeasureSpec.AT_MOST; } else { childWidthMode = widthMode; } if (heigthMode == MeasureSpec.EXACTLY) { childHeightMode = MeasureSpec.AT_MOST; } else { childHeightMode = heigthMode; } int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidthSize, childWidthMode); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, childHeightMode); currentLine = new Line (); for (int i = 0 ; i < getChildCount(); i++) { View child = getChildAt(i); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); int childMeasuredWidth = child.getMeasuredWidth(); userWidth += childMeasuredWidth; if (userWidth <= parentWidthSize) { currentLine.addChild(child); userWidth += horizontalSpacing; if (userWidth > parentWidthSize) { newLine(); } } else { if (currentLine.getChildCount() == 0 ) { currentLine.addChild(child); } newLine(); currentLine.addChild(child); userWidth = child.getMeasuredWidth()+horizontalSpacing; } } if (!mLines.contains(currentLine)) { mLines.add(currentLine); } int totalHeight = 0 ; for (Line line : mLines) { totalHeight += line.getHeight() + verticalSpacing; } setMeasuredDimension(parentWidthSize + getPaddingLeft() + getPaddingRight(), resolveSize(totalHeight + getPaddingTop() + getPaddingBottom(), heightMeasureSpec)); } private void newLine () { mLines.add(currentLine); currentLine = new Line (); userWidth = 0 ; } @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { l += getPaddingLeft(); t += getPaddingTop(); for (int i = 0 ; i < mLines.size(); i++) { Line line = mLines.get(i); line.onLayout(l, t); t += line.getHeight() + verticalSpacing; } } private int getMode (int mode) { int childMode = 0 ; if (mode == MeasureSpec.EXACTLY) { childMode = MeasureSpec.AT_MOST; } else { childMode = mode; } return childMode; } }
2.思路解析
对比图一的实现思路,我们封装了Line这个内部类,看到这个名字,相信大家都猜到是什么意思了,其实就是一个Line实例对象代表一行,Line里面的List children用来存放孩子
private List<View> children;//一行里面所添加的子View集合
Line里面还封装了void onLayout(int l, int t)方法,即自己去拜访每个孩子的位置, 实现剩余的宽度平均分配,主要体现在这几行代码
1 2 3 4 5 6 7 8 9 10 if (surplus > 0) { //如果有剩余宽度,则将剩余宽度平分给每一个子控件 surplusChild = (int) (surplus / children.size()+0.5); } ------- //重新分配每个孩子的大小 child.measure(MeasureSpec.makeMeasureSpec( child.getMeasuredWidth()+surplusChild,MeasureSpec.EXACTLY) ,MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY));
今天就写到这里了,有时间再来补充,最近考试比较忙,已经好久没有更新博客了。 源码下载地址: https://github.com/gdutxiaoxu/CustomViewDemo.git