了解了自定义View的流程,那么接下来最好就是对着一个自定义View去分析,加深一下对步骤的理解,这里拿FlowLayout去分析。这样一个自定义View 是继承的ViewGroup,因为它里面还有一个个的子View 填充,因此我们分析出这是继承自ViewGroup。然后接着分析,这个布局会自动判断 当宽度达到屏幕的宽度时,自动换到下一行,那接下来我们就去实现这样一个布局。
第一步:定义FlowLayout继承ViewGroup,并实现构造方法,以及父类的抽象方法
public class FlowLayout extends ViewGroup { public FlowLayout(Context context) { super(context); // TODO Auto-generated constructor stub } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub } }第二部 根据 MeasureSpec (测量规格)拿到 mode(测量模式) 和size(测量大小),然后根据这个大小去measure里面的child。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 1获取with height 以及mode int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom() - getPaddingTop(); int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); restoreLine(); int count = getChildCount(); // 2 测量子View for (int i = 0; i < count; i++) { View child = getChildAt(i); int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode); int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode); child.measure(widthSpec, heightSpec); if(mLine==null){ mLine=new Line(); } //将childview 添加到每一行中 int childWidth=child.getMeasuredWidth(); //当前行已经占用的宽度 mUsedWidth+=childWidth; if(mUsedWidth<widthSize){ //当前行还没有达到上限,那么该child就添加进这一行 mLine.addView(child); mUsedWidth+=mHorizontalSpacing; //添加上两个子View之间水平方向的间隔 }else{ //说明长度超出了当前的最大宽度 if(mLine.getViewCount()==0){ //表示当前行中还没有元素,添加的第一个元素 长度就超过了最大宽度,那么也要把该child 添加进去保证有数据 mLine.addView(child); }else{ //表示当前行中已经有元素,那么换一行,添加进去 newLine(); mLine.addView(child); //改变已使用的宽度 mUsedWidth+=mHorizontalSpacing+childWidth; } } } //前面只有换行的时候才将Line 添加到lines 集合中,这里要判断一下最后一行,将最后一行也添加进去 if(mLine!=null&&mLine.getViewCount()>0&&!mLines.contains(mLine) ){ //表示有数据 mLines.add(mLine); } // 设置测量的宽高setMeasuredDimension int totoalHeight=0; for(int i=0;i<mLines.size();i++){ totoalHeight+=mLines.get(i).mHeight;//N行的高度 } //加上 行间距 totoalHeight+=(mLines.size()-1)*mVerticalSpacing; //加上padding totoalHeight+=getPaddingBottom()+getPaddingTop(); // 设置FlowLayout的宽度值 高度值 宽度就是默认的宽度,高度是总的高度 int measuredHeight = resolveSize(totoalHeight, heightMeasureSpec); setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), measuredHeight); }
measure 结束,接下来就是layout 去将view 具体确定view 的位置。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //主要是调用child.layout int count=mLines.size(); int left = getPaddingLeft(); int top = getPaddingTop(); for(int i=0;i<count;i++){ Line line=mLines.get(i); line.layout(left,top); top+=mVerticalSpacing+line.mHeight; } }这个方法主要用来layout 子view。
public void layout(int l, int t) { int left = l; int top = t; // 父布局的宽度 int totoalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); // 当前line 中view的个数 int count = getViewCount(); // 剩余空间平分给每个View int spaceLast = totoalWidth - mWidth - (count - 1) * mHorizontalSpacing; int averageWidth = spaceLast / count; // 平分的宽度 // int splitSpacing = (int) (spaceLast / count + 0.5); for (int i = 0; i < count; i++) { View child = views.get(i); int childHeight = child.getMeasuredHeight(); int childWidth = child.getMeasuredWidth(); childWidth += averageWidth; child.getLayoutParams().width = childWidth; // 改变了原来的宽高,重新测量一次 int widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec( childHeight, MeasureSpec.EXACTLY); child.measure(widthMeasureSpec, heightMeasureSpec); // 布局View child.layout(left, top, left + childWidth, top + childHeight); left += childWidth + mHorizontalSpacing; // 为下一个View的left赋值 } }这个方法主要实现的功能是将每一行剩余的空间,平均分配到该行的每一个View 身上。
这样一个布局在这里就基本实现了。
当然,可以根据自己的需要 进行细微的调整,比如改变子view的 弧度,padding 等等。下面给出具体效果