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

什么是流式布局呢?也不知道哪个高手把它称之为流失布局,叫什么不重要,重要的是要知道怎么实现,今天就实现下这个功能,先看下图什么就知道是什么是流式布局了,做过电商的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) {
        /**
         * 不调用父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}
 */
@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" 所以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天半的时间写完了!

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值