[关闭]
@xujun94 2016-06-29T18:29:29.000000Z 字数 11013 阅读 1051

常用的自定义View例子一

在Android开发中,我们经常会遇到流布式的布局,经常会用来一些标签的显示,比如qq中个人便签,搜索框下方提示的词语,这些是指都是流布式的布局,今天我就我们日常开放中遇到的流布式布局坐一些总结

转载请注明博客地址:http://blog.csdn.net/gdutxiaoxu/article/details/51765428
**源码下载地址:https://github.com/gdutxiaoxu/CustomViewDemo.git **
效果图

1. 先给大家看一下效果



仔细观察,我们可以知道图二其实是图一效果的升级版,图一当我们控件的宽度超过这一行的时候,剩余的宽度它不会自动分布到每个控件中,而图二的效果当我们换行的时候,如控件还没有占满这一行的时候,它会自动把剩余的宽度分布到每个控件中

2.废话不多说了,大家来直接看来看一下图一的源码

1)代码如下

  1. /**
  2. * 博客地址:http://blog.csdn.net/gdutxiaoxu
  3. * @author xujun
  4. * @time 2016/6/20 23:49.
  5. */
  6. public class SimpleFlowLayout extends ViewGroup {
  7. private int verticalSpacing = 20;
  8. public SimpleFlowLayout(Context context ) {
  9. super(context);
  10. }
  11. /**
  12. * 重写onMeasure方法是为了确定最终的大小
  13. * @param widthMeasureSpec
  14. * @param heightMeasureSpec
  15. */
  16. @Override
  17. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  18. int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
  19. int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
  20. int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
  21. int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
  22. int paddingLeft = getPaddingLeft();
  23. int paddingRight = getPaddingRight();
  24. int paddingTop = getPaddingTop();
  25. int paddingBottom = getPaddingBottom();
  26. //处理Padding属性,让当前的ViewGroup支持Padding
  27. int widthUsed = paddingLeft + paddingRight;
  28. int heightUsed = paddingTop + paddingBottom;
  29. int childMaxHeightOfThisLine = 0;
  30. int childCount = getChildCount();
  31. for (int i = 0; i < childCount; i++) {
  32. View child = getChildAt(i);
  33. if (child.getVisibility() != GONE) {
  34. // 已用的宽度
  35. int childUsedWidth = 0;
  36. // 已用的高度
  37. int childUsedHeight = 0;
  38. // 调用ViewGroup自身的方法测量孩子的宽度和高度,我们也可以自己根据MeasureMode来测量
  39. measureChild(child,widthMeasureSpec,heightMeasureSpec);
  40. childUsedWidth += child.getMeasuredWidth();
  41. childUsedHeight += child.getMeasuredHeight();
  42. //处理Margin,支持孩子的Margin属性
  43. Rect marginRect = getMarginRect(child);
  44. int leftMargin=marginRect.left;
  45. int rightMargin=marginRect.right;
  46. int topMargin=marginRect.top;
  47. int bottomMargin=marginRect.bottom;
  48. childUsedWidth += leftMargin + rightMargin;
  49. childUsedHeight += topMargin + bottomMargin;
  50. //总宽度没有超过本行
  51. if (widthUsed + childUsedWidth < widthSpecSize) {
  52. widthUsed += childUsedWidth;
  53. if (childUsedHeight > childMaxHeightOfThisLine) {
  54. childMaxHeightOfThisLine = childUsedHeight;
  55. }
  56. } else {//总宽度已经超过本行
  57. heightUsed += childMaxHeightOfThisLine + verticalSpacing;
  58. widthUsed = paddingLeft + paddingRight + childUsedWidth;
  59. childMaxHeightOfThisLine = childUsedHeight;
  60. }
  61. }
  62. }
  63. //加上最后一行的最大高度
  64. heightUsed += childMaxHeightOfThisLine;
  65. setMeasuredDimension(widthSpecSize, heightUsed);
  66. }
  67. @Override
  68. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  69. int paddingLeft = getPaddingLeft();
  70. int paddingRight = getPaddingRight();
  71. int paddingTop = getPaddingTop();
  72. int paddingBottom = getPaddingBottom();
  73. /**
  74. * 为了 支持Padding属性
  75. */
  76. int childStartLayoutX = paddingLeft;
  77. int childStartLayoutY = paddingTop;
  78. int widthUsed = paddingLeft + paddingRight;
  79. int childMaxHeight = 0;
  80. int childCount = getChildCount();
  81. //摆放每一个孩子的高度
  82. for (int i = 0; i < childCount; i++) {
  83. View child = getChildAt(i);
  84. if (child.getVisibility() != GONE) {
  85. int childNeededWidth, childNeedHeight;
  86. int left, top, right, bottom;
  87. int childMeasuredWidth = child.getMeasuredWidth();
  88. int childMeasuredHeight = child.getMeasuredHeight();
  89. Rect marginRect = getMarginRect(child);
  90. int leftMargin=marginRect.left;
  91. int rightMargin=marginRect.right;
  92. int topMargin=marginRect.top;
  93. int bottomMargin=marginRect.bottom;
  94. childNeededWidth = leftMargin + rightMargin + childMeasuredWidth;
  95. childNeedHeight = topMargin + topMargin + childMeasuredHeight;
  96. // 没有超过本行
  97. if (widthUsed + childNeededWidth <= r - l) {
  98. if (childNeedHeight > childMaxHeight) {
  99. childMaxHeight = childNeedHeight;
  100. }
  101. left = childStartLayoutX + leftMargin;
  102. top = childStartLayoutY + topMargin;
  103. right = left + childMeasuredWidth;
  104. bottom = top + childMeasuredHeight;
  105. widthUsed += childNeededWidth;
  106. childStartLayoutX += childNeededWidth;
  107. } else {
  108. childStartLayoutY += childMaxHeight + verticalSpacing;
  109. childStartLayoutX = paddingLeft;
  110. widthUsed = paddingLeft + paddingRight;
  111. left = childStartLayoutX + leftMargin;
  112. top = childStartLayoutY + topMargin;
  113. right = left + childMeasuredWidth;
  114. bottom = top + childMeasuredHeight;
  115. widthUsed += childNeededWidth;
  116. childStartLayoutX += childNeededWidth;
  117. childMaxHeight = childNeedHeight;
  118. }
  119. child.layout(left, top, right, bottom);
  120. }
  121. }
  122. }
  123. private Rect getMarginRect(View child) {
  124. LayoutParams layoutParams = child.getLayoutParams();
  125. int leftMargin = 0;
  126. int rightMargin = 0;
  127. int topMargin = 0;
  128. int bottomMargin = 0;
  129. if (layoutParams instanceof MarginLayoutParams) {
  130. MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
  131. leftMargin = marginLayoutParams.leftMargin;
  132. rightMargin = marginLayoutParams.rightMargin;
  133. topMargin = marginLayoutParams.topMargin;
  134. bottomMargin = marginLayoutParams.bottomMargin;
  135. }
  136. return new Rect(leftMargin, topMargin, rightMargin, bottomMargin);
  137. }
  138. }

2)思路解析

  1. 首先我们重写onMeasure方法,在OnMeasure方法里面我们调用measureChild()这个方法去获取每个孩子的宽度和高度,每次增加一个孩子我们执行 widthUsed += childUsedWidth;
  2. 添加完一个孩子以后我们判断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)注意事项

讲解之前,我们先来了解一下一个基本知识

从这张图片里面我们可以得出这样结论

  1. Width=控件真正的宽度(realWidth)+PaddingLeft+PaddingRight
  2. margin是子控件相对于父控件的距离

注意事项

  1. 为了支持控件本身的padding属性,我们做了处理,主要代码如下
  1. int widthUsed = paddingLeft + paddingRight;
  2. int heightUsed = paddingTop + paddingBottom;
  3. ----------
  4. if (widthUsed + childUsedWidth < widthSpecSize) {
  5. widthUsed += childUsedWidth;
  6. if (childUsedHeight > childMaxHeightOfThisLine) {
  7. childMaxHeightOfThisLine = childUsedHeight;
  8. }
  9. }
  1. 为了支持子控件的margin属性,我们同样也做了处理
  1. Rect marginRect = getMarginRect(child);
  2. int leftMargin=marginRect.left;
  3. int rightMargin=marginRect.right;
  4. int topMargin=marginRect.top;
  5. int bottomMargin=marginRect.bottom;
  6. childUsedWidth += leftMargin + rightMargin;
  7. childUsedHeight += topMargin + bottomMargin;

即我们在计算孩子所占用的宽度和高度的时候加上margin属性的高度,接着在计算需要孩子总共用的宽高度的时候加上每个孩子的margin属性的宽高度,这样自然就支持了孩子的margin属性了

4.缺陷

如下图所见,在控件宽度参差不齐的情况下,控件换行会留下一些剩余的宽度,作为想写出鲁棒性的代码的我们会觉得别扭,于是我们相处了解决办法。

解决方法见下面

图二源码解析

废话不多说,先看源码

  1. /**
  2. * 博客地址:http://blog.csdn.net/gdutxiaoxu
  3. * @author xujun
  4. * @time 2016/6/26 22:54.
  5. */
  6. public class PrefectFlowLayout extends ViewGroup {
  7. public PrefectFlowLayout(Context context) {
  8. super(context);
  9. }
  10. public PrefectFlowLayout(Context context, AttributeSet attrs) {
  11. super(context, attrs);
  12. }
  13. public PrefectFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
  14. super(context, attrs, defStyleAttr);
  15. }
  16. //父容器宽度
  17. private int parentWidthSize;
  18. //水平间距
  19. private int horizontalSpacing = 16;
  20. //垂直间距
  21. private int verticalSpacing = 16;
  22. //当前行
  23. private Line currentLine;
  24. //所有行的集合
  25. private List<Line> mLines = new ArrayList<>();
  26. //当前行已使用宽度
  27. private int userWidth = 0;
  28. /**
  29. * 行对象
  30. */
  31. private class Line {
  32. //一行里面所添加的子View集合
  33. private List<View> children;
  34. //当前行高度
  35. private int height;
  36. //当前行已使用宽度
  37. private int lineWidth = 0;
  38. public Line() {
  39. children = new ArrayList<>();
  40. }
  41. /**
  42. * 添加一个子控件
  43. *
  44. * @param child
  45. */
  46. private void addChild(View child) {
  47. children.add(child);
  48. if (child.getMeasuredHeight() > height) {
  49. //当前行高度以子控件最大高度为准
  50. height = child.getMeasuredHeight();
  51. }
  52. //将每个子控件宽度进行累加,记录使用的宽度
  53. lineWidth += child.getMeasuredWidth();
  54. }
  55. /**
  56. * 获取行的高度
  57. *
  58. * @return
  59. */
  60. public int getHeight() {
  61. return height;
  62. }
  63. /**
  64. * 获取子控件的数量
  65. *
  66. * @return
  67. */
  68. public int getChildCount() {
  69. return children.size();
  70. }
  71. /**
  72. * 放置每一行里面的子控件的位置
  73. *
  74. * @param l 距离最左边的距离
  75. * @param t 距离最顶端的距离
  76. */
  77. public void onLayout(int l, int t) {
  78. //当前行使用的宽度,等于每个子控件宽度之和+子控件之间的水平距离
  79. lineWidth += horizontalSpacing * (children.size() - 1);
  80. int surplusChild = 0;
  81. int surplus = parentWidthSize - lineWidth;//剩余宽度
  82. if (surplus > 0) {
  83. //如果有剩余宽度,则将剩余宽度平分给每一个子控件
  84. surplusChild = (int) (surplus / children.size()+0.5);
  85. }
  86. for (int i = 0; i < children.size(); i++) {
  87. View child = children.get(i);
  88. child.getLayoutParams().width=child.getMeasuredWidth()+surplusChild;
  89. if (surplusChild>0){
  90. //如果长度改变了后,需要重新测量,否则布局中的属性大小还会是原来的大小
  91. child.measure(MeasureSpec.makeMeasureSpec(
  92. child.getMeasuredWidth()+surplusChild,MeasureSpec.EXACTLY)
  93. ,MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY));
  94. }
  95. child.layout(l, t, l + child.getMeasuredWidth(), t + child.getMeasuredHeight());
  96. l += child.getMeasuredWidth() + horizontalSpacing;
  97. }
  98. }
  99. }
  100. // getMeasuredWidth() 控件实际的大小
  101. // getWidth() 控件显示的大小
  102. @Override
  103. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  104. //将之前测量的数据进行清空,以防复用时影响下次测量
  105. mLines.clear();
  106. currentLine = null;
  107. userWidth = 0;
  108. //获取父容器的宽度和模式
  109. int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  110. parentWidthSize = MeasureSpec.getSize(widthMeasureSpec)
  111. - getPaddingLeft() - getPaddingRight();
  112. //获取父容器的高度和模式
  113. int heigthMode = MeasureSpec.getMode(heightMeasureSpec);
  114. int heightSize = MeasureSpec.getSize(heightMeasureSpec)
  115. - getPaddingTop() - getPaddingBottom();
  116. int childWidthMode, childHeightMode;
  117. //为了测量每个子控件,需要指定每个子控件的测量规则
  118. //子控件设置为WRAP_CONTENT,具体测量规则详见,ViewGroup的getChildMeasureSpec()方法
  119. if (widthMode == MeasureSpec.EXACTLY) {
  120. childWidthMode = MeasureSpec.AT_MOST;
  121. } else {
  122. childWidthMode = widthMode;
  123. }
  124. if (heigthMode == MeasureSpec.EXACTLY) {
  125. childHeightMode = MeasureSpec.AT_MOST;
  126. } else {
  127. childHeightMode = heigthMode;
  128. }
  129. //获取到子控件高和宽的测量规则
  130. int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(parentWidthSize, childWidthMode);
  131. int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, childHeightMode);
  132. currentLine = new Line();//创建第一行
  133. for (int i = 0; i < getChildCount(); i++) {
  134. View child = getChildAt(i);
  135. //测量每一个孩子
  136. child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
  137. //获取当前子控件的实际宽度
  138. int childMeasuredWidth = child.getMeasuredWidth();
  139. //让当前行使用宽度加上当前子控件宽度
  140. userWidth += childMeasuredWidth;
  141. if (userWidth <= parentWidthSize) {
  142. //如果当前行使用宽度小于父控件的宽度,则加入该行
  143. currentLine.addChild(child);
  144. //当前行使用宽度加上子控件之间的水平距离
  145. userWidth += horizontalSpacing;
  146. //如果当前行加上水平距离后超出父控件宽度,则换行
  147. if (userWidth > parentWidthSize) {
  148. newLine();
  149. }
  150. } else {
  151. //以防出现一个子控件宽度超过父控件的情况出现
  152. if (currentLine.getChildCount() == 0) {
  153. currentLine.addChild(child);
  154. }
  155. newLine();
  156. //并将超出范围的当前的子控件加入新的行中
  157. currentLine.addChild(child);
  158. //并将使用宽度加上子控件的宽度;
  159. userWidth = child.getMeasuredWidth()+horizontalSpacing;
  160. }
  161. }
  162. //加入最后一行,因为如果最后一行宽度不足父控件宽度时,就未换行
  163. if (!mLines.contains(currentLine)) {
  164. mLines.add(currentLine);
  165. }
  166. int totalHeight = 0;//总高度
  167. for (Line line : mLines) {
  168. //总高度等于每一行的高度+垂直间距
  169. totalHeight += line.getHeight() + verticalSpacing;
  170. }
  171. //resolveSize(),将实际高度与父控件高度进行比较,选取最合适的
  172. setMeasuredDimension(parentWidthSize + getPaddingLeft() + getPaddingRight(),
  173. resolveSize(totalHeight + getPaddingTop() + getPaddingBottom(), heightMeasureSpec));
  174. }
  175. /**
  176. * 换行
  177. */
  178. private void newLine() {
  179. mLines.add(currentLine);//记录之前行
  180. currentLine = new Line();//重新创建新的行
  181. userWidth = 0;//将使用宽度初始化
  182. }
  183. /**
  184. * 放置每个子控件的位置
  185. *
  186. * @param changed
  187. * @param l
  188. * @param t
  189. * @param r
  190. * @param b
  191. */
  192. @Override
  193. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  194. l += getPaddingLeft();
  195. t += getPaddingTop();
  196. for (int i = 0; i < mLines.size(); i++) {
  197. Line line = mLines.get(i);
  198. //设置每一行的位置,每一行的子控件由其自己去分配
  199. line.onLayout(l, t);
  200. //距离最顶端的距离,即每一行高度和垂直间距的累加
  201. t += line.getHeight() + verticalSpacing;
  202. }
  203. }
  204. /**
  205. * 获取子控件的测量规则
  206. *
  207. * @param mode 父控件的测量规则
  208. * @return 子控件设置为WRAP_CONTENT,具体测量规则详见,ViewGroup的getChildMeasureSpec()方法
  209. */
  210. private int getMode(int mode) {
  211. int childMode = 0;
  212. if (mode == MeasureSpec.EXACTLY) {
  213. childMode = MeasureSpec.AT_MOST;
  214. } else {
  215. childMode = mode;
  216. }
  217. return childMode;
  218. }
  219. }

2.思路解析

  1. 对比图一的实现思路,我们封装了Line这个内部类,看到这个名字,相信大家都猜到是什么意思了,其实就是一个Line实例对象代表一行,Line里面的List children用来存放孩子

    private List<View> children;//一行里面所添加的子View集合
    
  2. Line里面还封装了void onLayout(int l, int t)方法,即自己去拜访每个孩子的位置,
    实现剩余的宽度平均分配,主要体现在这几行代码
  1. if (surplus > 0) {
  2. //如果有剩余宽度,则将剩余宽度平分给每一个子控件
  3. surplusChild = (int) (surplus / children.size()+0.5);
  4. }
  5. -------
  6. //重新分配每个孩子的大小
  7. child.measure(MeasureSpec.makeMeasureSpec(
  8. child.getMeasuredWidth()+surplusChild,MeasureSpec.EXACTLY)
  9. ,MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY));

今天就写到这里了,有时间再来补充,最近考试比较忙,已经好久没有更新博客了。

**源码下载地址:https://github.com/gdutxiaoxu/CustomViewDemo.git **

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注