什么是流式布局呢?也不知道哪个高手把它称之为流失布局,叫什么不重要,重要的是要知道怎么实现,今天就实现下这个功能,先看下图什么就知道是什么是流式布局了,做过电商的app或者网购的人都知道有一个什么选择规格(x,xl,ml)so,
当然这个用其他什么gridview也能实现,如果大小是一样的话,如果大小不一样就不好搞定了,那么如果使用今天讲的流式布局就很好做了,那么还是一开始并不是直接讲这个效果怎么实现,而是把相关的技术点尽自己的能力讲清楚,如果这个懂了,说不定不仅这个流式布局懂了,也许你还懂了其他东西,这就是最好的,这就是为什么不上来贴代码的原因,而是花更多的时间把原理讲清楚!要实现这个效果,就必须懂view的绘制流程,如图:
这就是所谓的绘制流程三步骤,打个比方吧,你team叫你把一个控件放到手机屏幕上,那么要问我要把一个多大的控件放在哪个位置啊,这里就有二个词很重要,多大,哪个位置,多大就是onMeasure(),哪个位置就是onLayout(),控件在屏幕上是显示什么,这就是内容了也就是onDraw(),
上面的图说了onMeasure()方法也就是测量控件大小并不是最终的大小,又可能onLayout()方法中改变了view的大小,现在写个小例子验证下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.flowlayout.MyLinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffff00" android:text="Hello World!" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="今天讲流失布局" android:textColor="#000000" android:background="#ffffff" /> </com.example.flowlayout.MyLinearLayout> </RelativeLayout>
package com.example.flowlayout; import android.content.Context; import android.util.AttributeSet; import android.widget.LinearLayout; /** * Created by admin on 2016/6/13. */ public class MyLinearLayout extends LinearLayout { public MyLinearLayout(Context context) { this(context,null); } public MyLinearLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); } }效果:
你会发现textview宽和高就是包裹内容,我现在在onLayout()方法中添加几行代码:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); TextView tv = (TextView) getChildAt(0);//获取MyLinearLayout控件的第一个子view,这个和xml布局是对应的 tv.layout(0,0,300,300); }效果图:
看到textview的宽和高变成了300,300了吧,和之前的内容包裹是不是不一样了,因为在onLayout()方法中改变了子view的宽和高,按到底这是违背view绘制流程的,但是可以这么做,我们知道android view有二种,一种是view比如TextView,Button,ImageView,就是不能通过addView(View view)添加子view的,另外还有一种View是ViewGroup,就是存储view的容器,但是ViewGroup是继承自View,所以你也可以说android上所有的控件就一种View,
onMeasure()---测量
我们知道绘制流程第一步就是测量,从源码中发现真正的测量是从measure()方法开始的,这个方法在view中而不是在ViewGroup中,所以刚才在自定义LinearLayout写的onMeasure()方法也是继承了View中的onMeasure()方法,那么先看下View中的measure()方法:
* * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } // Suppress sign extension for the low bytes long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec);//重点 mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }从上面方法中的注释标记了红色,意思是说水平和竖直空间需要父view提供,记住这个,往下会用到,从上面的measure()方法看到这是用final修饰的,表示子类不能继承它,也就是说Google让你不想打破它的测量框架,上面有一个很重要的方法 onMeasure(widthMeasureSpec, heightMeasureSpec);一般测量都是继承这个方法,onMeasure()源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }其实onMeasure()方法中也就是调用了setMeasureDimension()方法,它也是接受2个形参,但是这二个形参确实调用了getDefaultSize()方法,
public static int getDefaultSize(int size, int measureSpec) { int result = size; //把size赋值给result int specMode = MeasureSpec.getMode(measureSpec);//获取mode int specSize = MeasureSpec.getSize(measureSpec);//获取size switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }从上面的形参的字面意思知道第一个形参是大小,第二个形参是测量规范,我是从字面意思翻译的,因为spec是规范意思,
所以onMeasure()方法中的2个参数就不是一个具体的值,比如不是什么100,200之类的,其实这100,200是由大小和规范决定的,现在看下getDefaultSize()方法,其中
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
MeasureSpec类源码:
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }上面的几个常量做一个简单的介绍
UNSPECIFIED = 0 << MODE_SHIFT(=30)表示向左移30 最后的结果=0
EXACTLY = 1 << MODE_SHIFT表示左移30=1073741824
AT_MOST = 2 << MODE_SHIFT;表示左移30结果=-2147483648
MODE_MASK = 0x3 << MODE_SHIFT表示左移30结果-1073741824
现在看下getMode()的方法:
public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); }比如measureSpec=100,那么getMode()最后返回的值为0,那么就是 UNSPECIFIED
public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }getSize()最后的返回的值就是measureSpec传入的值,
结合上面2个方法以及getDefaultSize()我们总结一个结论
测量最终的值=size+mode
现在讲下上面涉及到的三个变量也就是mode,
UNSPECIFIED:
表示视图按照自己的意愿设置成任意的大小,没有任何限制,这个一般用在ScollerView上
EXACTLY
这个exactly是精确的意思,意思是说父view传递给子view的大小是精度的,那么子view就应该接受父view传递给它的值是多少就是多少
AT_MOST
表示子view只能接受指定的大小,不能超过这个指定大小的范围,就好像是LinearLayout的宽和高是100,而它的子view TextView只能接受最大值为100,不能超过这个100
现在看下之前自定义的LinearLayout中的onMeasure()方法
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int mode = MeasureSpec.getMode(widthMeasureSpec); Log.e(TAG,"mode------------------->"+mode); }log:
06-13 06:52:30.088 30004-30004/com.example.flowlayout E/MyLinearLayout: mode------------------->1073741824
把1073741824和上面的几个分析的常量对比一下发现mode就是EXACTLY,哪为什么是EXACTLY呢?看下布局文件:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.flowlayout.MyLinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffff00" android:text="Hello World!" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="今天讲流失布局" android:textColor="#000000" android:background="#ffffff" /> </com.example.flowlayout.MyLinearLayout> </RelativeLayout>发现MyLinearLayout宽和高都是match_parent,也就是填充父view的大小,它的父view就是RelativeLayout,而这个 RelativeLayout的宽和高是读取手机的屏幕赋值给RelativeLayout的,所以RelativeLayout的宽和高是一个定值,这就是为什么mode为EXACTLY,如图:
现在我把布局文件改变下,
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:layout_width="wrap_content" android:layout_height="300px" android:orientation="horizontal" android:background="#ff0000" > <com.example.flowlayout.MyLinearLayout android:layout_width="wrap_content" android:layout_height="200px" android:background="#ffff00" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="今天讲流失布局" android:textColor="#000000" /> </com.example.flowlayout.MyLinearLayout> </LinearLayout> </RelativeLayout>现在打印下mode值为
06-13 07:22:11.258 23646-23646/com.example.flowlayout I/MyLinearLayout: mode------------------->-2147483648
这个是不是对应AT_MOST,因为LinearLayout的宽度为wrap_content,它的宽度取决于它孩子view的宽度,所以它不是固定的,那么你MyLinearLayout就是最大取值反正不能超过父view的宽度就行,从上面的分析可以得出一般的结论:
1:当子view的宽和高设置为wrap_content,父view给它的mode为AT_MOST
2:当子view的高和宽设置为match_parent和确切的值的时候 父view给它的mode为EXACTLY
测量的最终是在setMeasuredDimension(int measuredWidth, int measuredHeight)方法中结束最后的测量过程,因为measuredWidth和measuredHeight都是最终的测量后的宽度和高度,从这个形参也知道后缀没带Spec这几个字母,
在这里我自定义一个View,
package com.example.flowlayout; import android.content.Context; import android.util.AttributeSet; import android.view.View; /** * Created by admin on 2016/6/13. */ public class MyView extends View { public MyView(Context context) { super(context); } public MyView(Context context, AttributeSet attrs) { super(context, attrs); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { /** * 不调用父view的onMeasure()方法而是直接调用setMeasuredDimension() */ // super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(200,200); } }布局文件
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="#ff0000" > <com.example.flowlayout.MyLinearLayout android:layout_width="wrap_content" android:layout_height="200px" android:background="#ffff00" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="今天讲流失布局" android:textColor="#000000" /> </com.example.flowlayout.MyLinearLayout> <com.example.flowlayout.MyView android:layout_width="50px" android:layout_height="50px" android:background="#ff999999" ></com.example.flowlayout.MyView> </LinearLayout> </RelativeLayout>我布局文件设置的宽和高都是50px,效果:
发现被骗了一样,是的布局文件是不能当作最终的view的宽和高,是因为我们在MyView的onMeasure()方法中设置了
setMeasuredDimension(200,200);其实在ViewGroup类中还有一个测量子view的方法
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount;//子view的总数 final View[] children = mChildren;//记录所有的子view(是一个数组) for (int i = 0; i < size; ++i) {//遍历所有的子view final View child = children[i];//赋值某一个子view if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {//判断这个View是不是Gone了也就是不可见 measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }现在看下measureChild()方法源码:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }上面通过一系列对父view传递进来的宽和高计算,最终调用的是子view的measure()方法来最终测量宽和高
在这提一个知识点,就是 getMeasuredWidth() getMeasuredHeight()这二个方法,我们只要看其中一个方法源码就行
public final int getMeasuredHeight() { return mMeasuredHeight & MEASURED_SIZE_MASK; }mMeasuredHeight这个值是在measure()方法中对进行赋值,而public static final int MEASURED_SIZE_MASK = 0x00ffffff;是一个定值,所以getMeasureHeight()方法是在测量后才能获取到这个值,好了测量就讲到这里,现在接着讲onLayout()方法
onLayout()
研究onLayout()方法首先要先研究下ViewGroup中的layout()方法开始
/** * {@inheritDoc} */ @Override public final void layout(int l, int t, int r, int b) { if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) { if (mTransition != null) { mTransition.layoutChange(this); } super.layout(l, t, r, b);//调用父view的layout()方法也就是调用view的layout方法 } else { // record the fact that we noop'd it; request layout when transition finishes mLayoutCalledWhileSuppressed = true; } }
发现这个layout()方法也是final修饰的,所以子view不能继承重写这个layout()方法,现在看下view的layout()方法
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);//在这里调用了测量方法 mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b);//给继承了ViewGroup的子类让它自己去控制view的位置 mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }看下onLayout()方法:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }发现它是一个空方法,哪好了画图理解下
其实view的layout的四个参数其实就是2个坐标点而已,如图:
package com.example.flowlayout; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; import android.view.Window; import android.widget.Button; /** * Created by admin on 2016/6/13. */ public class MyView extends Button { private static final String TAG ="MyView" ; public MyView(Context context) { this(context,null); } public MyView(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private float downX = 0; private float downY = 0; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: downX = event.getX(); downY = event.getY(); break; case MotionEvent.ACTION_MOVE: float moveX = event.getX(); float moveY = event.getY(); int l = getLeft(); int t = getTop(); int r = getRight(); int b = getBottom(); int newL = (int) (l+(moveX-downX)); int newT = (int) (t+(moveY-downY)); int newR = (int) (r+(moveX-downX)); int newB = (int) (b+(moveY-downY)); layout(newL,newT,newR,newB); downX = moveX; downY = moveY; break; case MotionEvent.ACTION_UP: break; } return true; } }这个是实现在屏幕上随意拖动
现在讲下View的getwidth()和getHeight()方法,直接上源码
@ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; }widht=mRight-mLeft,从这个简单的算法中就知道要想一个view通过getWidth()获取到宽度,必须是onLayout()方法执行后
在这里忘记了讲下onLayout(l,t,r,b)方法中四个参数,其实就是离父view的left,top,right,bottom
布局文件:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.flowlayout.MyLinearLayout android:layout_width="100px" android:layout_height="100px" android:background="#ff999999" android:text="随意拖动" android:layout_marginLeft="10px" android:layout_marginTop="10px" android:layout_marginRight="10px" android:layout_marginBottom="10px" ></com.example.flowlayout.MyLinearLayout> </RelativeLayout>
在onLayout()方法中打印log;
06-13 12:50:17.293 11451-11451/com.example.flowlayout E/MyLinearLayout: l=10t=10r=110b=110
看出来了吧从log日记中,如图:
好吧,onLayout()方法就讲到这里了,现在讲一个例子引出另外一个技术点
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
>
<com.example.measureviewdemo.MyLinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="阿里巴巴"
android:background="#ff0000"
android:padding="10dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="腾讯"
android:background="#ffff00"
android:padding="10dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="百度"
android:background="#00ff00"
android:padding="10dp"
/>
</com.example.measureviewdemo.MyLinearLayout>
</RelativeLayout>
package com.example.measureviewdemo;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class MyLinearLayout extends ViewGroup {
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLinearLayout(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
发现MyLinearLayout 是继承了ViewGroup,里面什么逻辑代码也没写,运行起来看有啥
发现叼都没有,是因为没有实现onMeasure()和onLayout()方法,因为我是继承了ViewGroup,现在实现下这个二个方法中的逻辑
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height=0;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int count = getChildCount();//获取所有的子view
for (int i=0;i<count;i++) { //遍历所有的子view
View view= getChildAt(i); //获取某一个子view
measureChild(view, widthMeasureSpec, heightMeasureSpec); //测量子view
int childWidth = view.getMeasuredWidth(); //测量后获取子view的宽度
int childHeight = view.getMeasuredHeight();//测量后获取子view的高度
//得到最大宽度,并且累加高度
height = childHeight;
width+= Math.max(childWidth, width);
}
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize: width, (heightMode == MeasureSpec.EXACTLY) ? heightSize: height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
int viewWidth = 0;//记录每个子view的宽度累加
for (int i=0;i<count;i++) {
View child = getChildAt(i);
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
child.layout(viewWidth, 0, childWidth+viewWidth, childHeight);
viewWidth+=childWidth;
}
}
效果:
完成的把这三个子view显示出来了,但是我现在布局文件中对这三个textview添加一个属性 android:layout_marginLeft="20px" 但是你发现运行起来的效果和上面的效果没任何区别,按到底高度不变,宽度要加3*20也就是width+60呢?这就是因为子view在计算宽度的时候没有加上Margin的值,也就是所谓的LayoutParams,这个类中封装了子view离父view的一些margin值,ViewGroup中有三个关于Marign的方法要复写,
/** * Returns a new set of layout parameters based on the supplied attributes set. * * @param attrs the attributes to build the layout parameters from * * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one * of its descendants */ public LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } /** * Returns a safe set of layout parameters based on the supplied layout params. * When a ViewGroup is passed a View whose layout params do not pass the test of * {@link #checkLayoutParams(android.view.ViewGroup.LayoutParams)}, this method * is invoked. This method should return a new set of layout params suitable for * this ViewGroup, possibly by copying the appropriate attributes from the * specified set of layout params. * * @param p The layout parameters to convert into a suitable set of layout parameters * for this ViewGroup. * * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one * of its descendants */ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return p; } /** * Returns a set of default layout parameters. These parameters are requested * when the View passed to {@link #addView(View)} has no layout parameters * already set. If null is returned, an exception is thrown from addView. * * @return a set of default layout parameters or null */ protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); }现在在自定义ViewGroup类中把上面的三个方法都重写一下,然后onMeasure()和onLayout()逻辑都做一些改变,代码如下:
package com.example.flowlayout; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; /** * Created by admin on 2016/6/13. */ public class MyLinearLayout extends ViewGroup { private static final String TAG = "MyLinearLayout"; public MyLinearLayout(Context context) { this(context,null); } public MyLinearLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = 0; int height=0; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int count = getChildCount();//获取所有的子view for (int i=0;i<count;i++) { //遍历所有的子view View view= getChildAt(i); //获取某一个子view measureChild(view, widthMeasureSpec, heightMeasureSpec); //测量子view MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); int childHeight = view.getMeasuredHeight()+lp.topMargin+lp.bottomMargin; int childWidth = view.getMeasuredWidth()+lp.leftMargin+lp.rightMargin; //得到最大宽度,并且累加高度 height = childHeight; width+= childWidth; } setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize: width, (heightMode == MeasureSpec.EXACTLY) ? heightSize: height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); int viewWidth = 0;//记录每个子view的宽度累加 int left = 0; int right = 0; for (int i=0;i<count;i++) { View view = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams(); int childHeight = view.getMeasuredHeight(); int childWidth = view.getMeasuredWidth(); left +=lp.leftMargin; right += lp.rightMargin; view.layout(viewWidth+right+left, 0, childWidth+viewWidth+left+right, childHeight); viewWidth+=childWidth; } } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } }现在的效果:
ok,我们想要的效果完成了!
现在看下
protected LayoutParams generateLayoutParams(LayoutParams p)
public LayoutParams generateLayoutParams(AttributeSet attrs)
protected LayoutParams generateDefaultLayoutParams()
这三个方法执行了哪一个?可以通过打log的形式:
发现执行的是 public LayoutParams generateLayoutParams(AttributeSet attrs)这个方法,首先看下它的形参AttributeSet ,是不是想到了自定义属性这块知识,而AttributeSet 是我们在xml中布局的封装,而它的重载方法
protected LayoutParams generateLayoutParams(LayoutParams p)一般是你通过new 一个子view添加到ViewGroup中执行的,
而最后一个方法 protected LayoutParams generateDefaultLayoutParams()是给你默认生成的宽和高都是WRAP_CONTENT
/** * Returns a set of default layout parameters. These parameters are requested * when the View passed to {@link #addView(View)} has no layout parameters * already set. If null is returned, an exception is thrown from addView. * * @return a set of default layout parameters or null */ protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); }上面是它的源码,
protected LayoutParams generateLayoutParams(LayoutParams p)和 public LayoutParams generateLayoutParams(AttributeSet attrs) 这二个方法区别可以从源码分析出来,
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return p; }然后点击进入到ViewGroup.LayoutParams静态内部类中,这个源码就不贴了,可以从结构图上看出来,
你会发现它除了width和height,好像没有涉及到什么layout_margin属性的封装,所以你怎么可能获取到什么leftMaring等值,现在看第二个方法
@Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); }这是我重写ViewGroup类中的方法,点击这个进去
public MarginLayoutParams(Context c, AttributeSet attrs) { super(); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout); setBaseAttributes(a, R.styleable.ViewGroup_MarginLayout_layout_width, R.styleable.ViewGroup_MarginLayout_layout_height); int margin = a.getDimensionPixelSize( com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1); if (margin >= 0) { leftMargin = margin; topMargin = margin; rightMargin= margin; bottomMargin = margin; } else { leftMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginLeft, UNDEFINED_MARGIN); if (leftMargin == UNDEFINED_MARGIN) { mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK; leftMargin = DEFAULT_MARGIN_RESOLVED; } rightMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginRight, UNDEFINED_MARGIN); if (rightMargin == UNDEFINED_MARGIN) { mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK; rightMargin = DEFAULT_MARGIN_RESOLVED; } topMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginTop, DEFAULT_MARGIN_RESOLVED); bottomMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginBottom, DEFAULT_MARGIN_RESOLVED); startMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE); endMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE); if (isMarginRelative()) { mMarginFlags |= NEED_RESOLUTION_MASK; } } final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport(); final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion; if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) { mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK; } // Layout direction is LTR by default mMarginFlags |= LAYOUT_DIRECTION_LTR; a.recycle(); }好好看看,是不是有我们想要的东西,我用红色标记一下,
还有一行代码需要解释下,就是这个
MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
直接用源码看就懂了,
public ViewGroup.LayoutParams getLayoutParams() { return mLayoutParams; }这是View类中的方法,
现在知道为什么可以强制转换了么,MarginLayoutParams是ViewGroup.LayoutParams的子类,好了技术点就分析完了,现在把流式布局的代码贴下来,如果上面的技术点都懂了,看这个代码应该不费劲,而且我还会结合图讲解下
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" > <com.example.flowlayout.FlowView android:layout_width="match_parent" android:layout_height="wrap_content" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="藤井莉娜" android:padding="10dp" android:textColor="#000000" android:background="@drawable/tv_bg" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="新垣结衣" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="酒井法子" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="泽尻绘里香" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="苍井优" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="藤原纪香" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="深田恭子" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="吉永小百合" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="国分佐智子" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="黑木明纱" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="长泽雅美" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="稻田千花" android:textColor="#000000" android:background="@drawable/tv_bg" android:padding="10dp" android:layout_marginLeft="20px" android:layout_marginTop="20px" /> </com.example.flowlayout.FlowView> </RelativeLayout>
自定义ViewGroup
package com.example.flowlayout; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; /** * Created by admin on 2016/6/14. */ public class FlowView extends ViewGroup { private static final String TAG ="FlowView" ; public FlowView(Context context) { super(context); } public FlowView(Context context, AttributeSet attrs) { super(context, attrs); } public FlowView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); //因为是 android:layout_width="match_parent" 所以mode是exactly,所以这个宽度是一个确定的值 int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); Log.e(TAG,"mode="+(heightMode==(MeasureSpec.AT_MOST))); int lineWidth = 0;//记录,每一个子view的宽度 int lineHight = 0;//记录每一个子view的高度 int width = 0;//记录当前容器的宽度 int height = 0;//记录当前容器的高度 int count = getChildCount();//获取所有的子view个数 if(count>0){ for(int i=0;i<count;i++){ View childView = getChildAt(i); if(childView!=null){ measureChild(childView,widthMeasureSpec,heightMeasureSpec);//测量子view宽和高 MarginLayoutParams layourParams = (MarginLayoutParams) childView.getLayoutParams(); int childWidth = childView.getMeasuredWidth()+layourParams.rightMargin+layourParams.leftMargin;//每个字view所占的宽 int childHeight = childView.getMeasuredWidth()+layourParams.topMargin+layourParams.bottomMargin;//每个字view所占的高 if(lineWidth+childWidth>widthSize){//lineWidth是记录前面的宽度+当前的子view宽度 height+=lineHight;//换行 记录容器的总高度 width = Math.max(lineWidth,width);//记录每一行最大的宽度 把最大的宽度当做设置给父view的宽度当然这要看mode是啥, lineHight = childHeight;//需要换行的时候 记录第一个子view的高度也就是每一行高度的初始化 lineWidth = childWidth; }else{//小于一行 lineWidth+=childWidth;//记录每个子view累加的宽度判断是否需要换行 lineHight = Math.max(lineHight,childHeight);//这是为了取一个最大值作为一行的高度,因为可能每个子view的高度不一样 } if (i == count -1){ height += lineHight; width = Math.max(width,lineWidth); } } } } //设置容器的宽和高 setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int top = 0;//记录view的向上的坐标 在这里20是为了不贴近标题栏 int left = 0;//记录view向左的坐标 同上 int lineWidth = 0; int lineHeight= 0 ; int count = getChildCount(); if(count>0){ for(int i=0;i<count;i++){ View childView = getChildAt(i); MarginLayoutParams layourParams = (MarginLayoutParams) childView.getLayoutParams(); int childWidth = childView.getMeasuredWidth()+layourParams.rightMargin+layourParams.leftMargin; int childHeight = childView.getMeasuredHeight()+layourParams.topMargin+layourParams.bottomMargin; Log.e(TAG,"childHeight="+childHeight); if(lineWidth+childWidth>getMeasuredWidth()){//换行 left=0; top+=lineHeight; lineWidth = childWidth; lineHeight = childHeight;//重新赋值 }else{ //不换行 lineWidth+=childWidth; lineHeight=Math.max(lineHeight,childHeight);//记录最大子view的高度作为容器的高度 } int newL = left+layourParams.leftMargin; int newT = top+layourParams.topMargin; int newR = newL+childView.getMeasuredWidth(); int newB = newT+childView.getMeasuredHeight(); childView.layout(newL,newT,newR,newB); left+=childWidth;//下一个子view离父view坐标的起点 } } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } }效果:
ok,终于发了1天半的时间写完了!