自定义View(三)--实现一个简单地流式布局
Android中的流式布局也就是常说的瀑布流很是常见,不仅在很多项目中都能见到,而且面试中也有很多面试官问道,那么什么是流式布局呢?简单来说就是如果当前行的剩余宽度不足以摆放下一个控件的时候,则自动将控件填充到下一行中,如一些关键字的标签,热搜词列表等位置出现的比较多。而且控件的类型可以摆放不同的,如一行中可以放置TextView,或者是ImageView,布局应该根据行中最大宽度来自动确定自己的每一行的宽度。
本次的Demo我们要实现如下的效果:
其实思路还是比较简单容易理顺的,看代码的前几遍可能会感觉到懵逼,但是看过几遍就会有种"哦,就是这么回事。"的感觉了。先来说下我的思路吧:
因为是流式布局么,布局布局,当然要继承自ViewGroup了,而且本次Demo中没有用到什么自定义属性,所以简单了不少。老样子,自定义View,肯定先是要自定义一个类,我们起名为FlowLayout,名字是不是很贴切?按照惯例,因为要在xml文件中使用,所以重写其一个参数的和两个参数的构造函数。然后:
重写onMeasure(),在onMeasure()中计算出父布局和子控件的尺寸(还要计算出什么时候一行摆放不下的时候该换行,这部分是关键部分)
我们定义了两个个全局变量List<List<View>> mAllChildViews和List<Integer> mLineHeight用来在onLayout中使用,前者是用来存放所有的子控件的集合,里层的List集合用来存放每一行的子控件;后者是用来存放每一行中的最大高度的那一个子控件。
重写onLayout()方法,在onLayout()方法中先要根据控件的宽高和是否有外边距等来确定每行的childView和每行的最大行高,并将其填充到集合中,然后再遍历mAllChildViews集合,绘制每行的childView,这时候就不需要在判断是否需要换行了!因为之前的操作中我们已经确定了哪些childView是属于一行中的。最后,我们可能会在xml文件中使用margin属性,所以要重写三个generateLayoutParams方法,并返回一个MarginLayoutParams对象。下面我们来看一下关键代码:
1 package com.example.customviewgroup; 2 3 import android.content.Context; 4 import android.util.AttributeSet; 5 import android.util.Log; 6 import android.view.View; 7 import android.view.ViewGroup; 8 9 import java.util.ArrayList; 10 import java.util.List; 11 12 /** 13 * 流式布局 14 */ 15 public class FlowLayout extends ViewGroup { 16 //存储所有行的view视图 按行记录view 每行的view存储到list集合中 17 private List<List<View>> mAllChildViews = new ArrayList<>(); 18 //记录每行的最大高度 19 private List<Integer> mLineHeight = new ArrayList<>(); 20 21 public FlowLayout(Context context) { 22 super(context); 23 } 24 25 public FlowLayout(Context context, AttributeSet attrs) { 26 super(context, attrs); 27 } 28 29 /** 30 * 负责子控件的测量模式和大小 根据所有的子控件的设置确定ViewGroup自己的宽度和高度 31 * 首先得到父容器传入的测量模式和宽高的计算值 循环所有的子view 调用measureChild对 32 * 所有的子view进行测量 然后根据所有的子view测量得出宽度和高度 如果ViewGroup设置为 33 * warp_content时 ViewGroup的宽度和子view的总宽度一致 高度和子view的最大的高度一致 34 * <p/> 35 * 如果viewgroup设置为match_parent时直接根据父viewgroup传入的宽度和高度进行测量设置 36 */ 37 @Override 38 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 39 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 40 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 41 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 42 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 43 // Log.d("tag", "widthSize: "+widthSize); 44 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 45 46 //定义一个不断累加的总高度 当布局中高度的设置为warp_content时使用该高度 47 int totalHeight = 0; 48 //定义不断累加的变量 存放当前行控件的宽度总和 49 int lineWidth = 0; 50 //获取当前行控件中最高的控件的高度总和 51 int lineHeight = 0; 52 //获取viewGroup中子控件总个数遍历 53 int childCount = getChildCount(); 54 for (int i = 0; i < childCount; i++) { 55 View childView = getChildAt(i); 56 //测量子控件的宽度和高度 57 // (因为每个子控件的margin可能不同,所以使用measureChild()方法测量每个子控件 58 // 若没有特定的属性,使用measureChildren()方法即可) 59 measureChild(childView, widthMeasureSpec, heightMeasureSpec); 60 //获取每个子控件的布局参数 61 MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); 62 //获取子控件的实际宽度和高度 63 int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; 64 int chileHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 65 66 //如果加入当前的childView超出最大的宽度 到目前为止最大的宽度给width 累加高度 开启新行 67 if (lineWidth + childWidth > widthSize) { 68 //开启新行(相当于直接将当前控件摆放到下一行,所以行宽从此childView的宽度开始) 69 lineWidth = childWidth; 70 //累加高度 71 totalHeight += lineHeight; 72 lineHeight = chileHeight;//开启记录下一行的高度 73 } else { 74 //累加lineWidth 取得最大的lineHeight 75 lineWidth += childWidth; 76 lineHeight = Math.max(lineHeight, chileHeight); 77 } 78 79 // 如果绘制最后一个,因为totalHeight是从0的位置开始的,所以要再加上一个行高 80 if (i == childCount - 1) { 81 totalHeight += lineHeight; 82 } 83 84 //宽度就设置为与屏幕一致,高度用一个三目运算符,若是高度模式为MeasureSpec.EXACTLY,则与屏幕一致,否则为我们得到的总高度 85 setMeasuredDimension(widthSize, 86 (heightMode == MeasureSpec.EXACTLY) ? heightSize : totalHeight); 87 } 88 } 89 90 @Override 91 protected LayoutParams generateDefaultLayoutParams() { 92 return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 93 } 94 95 @Override 96 public LayoutParams generateLayoutParams(AttributeSet attrs) { 97 return new MarginLayoutParams(getContext(), attrs); 98 } 99 100 @Override 101 protected LayoutParams generateLayoutParams(LayoutParams p) { 102 return new MarginLayoutParams(p); 103 } 104 105 /** 106 * 完成对viewgroup中所有childview的位置的指定 107 */ 108 @Override 109 protected void onLayout(boolean changed, int l, int t, int r, int b) { 110 //先把集合中的内容全部清除掉,然后再加载本次绘制内容(这个地方不太清楚理解的对不对) 111 mAllChildViews.clear(); 112 mLineHeight.clear(); 113 Log.d("onLayout", "mAllChildViews: " + mAllChildViews); 114 Log.d("onLayout", "mLineHeight: "+mLineHeight); 115 //获取当前view的宽度(也就是父布局宽度) 116 int layoutWidth = getWidth(); 117 // Log.d("tag", "layoutWidth: "+layoutWidth); 118 //声明变量 定义每行的宽度和高度 119 int lineWidth = 0; 120 int lineHeight = 0; 121 //声明list集合存储每一行中所有的childView 122 List<View> lineViews = new ArrayList<>(); 123 int childCount = getChildCount(); 124 for (int i = 0; i < childCount; i++) { 125 View childView = getChildAt(i); 126 MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); 127 int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; 128 int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 129 //如果需要换行 130 if (lineWidth + childWidth > layoutWidth) { 131 //记录这一行中所有的view以及最大高度 132 mLineHeight.add(lineHeight); 133 //将每一行的childview视图存储到集合中 然后开启新的arraylist记录下一行 134 mAllChildViews.add(lineViews); 135 //重置行宽度(这里说行宽其实有点不准确,lineWidth其实指的是绘制childView的左上角的点的横坐标) 136 lineWidth = 0; 137 //开启新的一行,则重新new一个来记录行View的集合 138 lineViews = new ArrayList<>(); 139 } 140 //如果不需要换行 则累加行宽,找出本行最大行高 141 lineWidth += childWidth; 142 //lineHeight其实指的是childView的左上角的点的纵坐标 143 lineHeight = Math.max(lineHeight, childHeight); 144 //不换行则添加childView到行集合中 145 lineViews.add(childView); 146 } 147 148 mLineHeight.add(lineHeight); 149 mAllChildViews.add(lineViews); 150 151 //声明绘制的起点坐标 152 int mLeft = 0; 153 int mTop = 0; 154 int lineNums = mAllChildViews.size();//获取总行数 155 for (int i = 0; i < lineNums; i++) { 156 lineViews = mAllChildViews.get(i);//获取每一行中所有的view 157 lineHeight = mLineHeight.get(i);//当前行的最大高度 158 for (int j = 0; j < lineViews.size(); j++) { 159 View childView = lineViews.get(j); 160 MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); 161 //计算childView中的left top right bottom 162 int left_child = mLeft + lp.leftMargin; 163 int top_child = mTop + lp.topMargin; 164 int right_child = left_child + childView.getMeasuredWidth(); 165 int botton_child = top_child + childView.getMeasuredHeight(); 166 childView.layout(left_child, top_child, right_child, botton_child); 167 //指定每个子控件的起始位置 168 mLeft += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; 169 } 170 //下一行开始,重置行宽(x),累加行高(y) 171 mLeft = 0; 172 mTop += lineHeight; 173 } 174 } 175 }
其实Android5.0之后可以使用RecycleView来实现,不用这么麻烦。