android 自定义控件实现流式布局

来源:互联网 发布:电脑淘宝登录不了 编辑:程序博客网 时间:2024/04/19 23:49

什么是流式布局呢?也不知道哪个高手把它称之为流失布局,叫什么不重要,重要的是要知道怎么实现,今天就实现下这个功能,先看下图什么就知道是什么是流式布局了,做过电商的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()方法中添加几行代码:

@Overrideprotected 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()方法

@Overrideprotected 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) {        /**         * 不调用父viewonMeasure()方法而是直接调用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} */@Overridepublic 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等值,现在看第二个方法

@Overridepublic 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" 所以modeexactly,所以这个宽度是一个确定的值        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天半的时间写完了!

2 0
原创粉丝点击