1. 程式人生 > >android自定義流式佈局解析與原始碼

android自定義流式佈局解析與原始碼

  今天給大家解析一下自定義流式佈局的編寫,以及分析一下寫程式碼過程遇到的難點。該佈局支援水平垂直方向和子view gravity選擇,先看一下執行的效果,左邊是垂直佈局,右邊是水平佈局,套一個scrollview就支援滑動了

垂直佈局 水平佈局

說一下遇到的兩個難點:

  • 自定義LayoutParams類

編寫過程中需要自定義一個LayoutParams,這個LayoutParams類是要繼承子父類的LayoutParams,在編碼過程中需要將子view的ViewGroup.LayoutParams轉成自定義的LayoutParams

    LayoutParams childLayoutParams 
= (LayoutParams) child.getLayoutParams();

這行程式碼如果沒有進行處理就會報錯,這時候就需要重寫幾個函式

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    @Override
public LayoutParams generateLayoutParams(AttributeSet attributeSet) { return new LayoutParams(getContext(), attributeSet); } @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); }

為什麼需要重寫這幾個函式呢?我們去看看ViewGroup的原始碼就知道了

    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {
        ....
        //注意該params為child的LayoutParams
        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }
        ....
    }

在新增每個子view的時候都會先檢測該子view的params是不是該父view的LayoutParams型別,如果不是,則會呼叫generateLayoutParams函式將ViewGroup.LayoutParams型別轉換為自定義的LayoutParams型別。而generateDefaultLayoutParams函式則是子view無layoutparmas的時候生成一個預設layoutparams。複寫這四個函式之後,上面那行程式碼就不會出異常了。

  • 自定義attr
<declare-styleable name="FlowLayout">
        <attr name="orientation" format="enum">
            <enum name="vertical" value="0"/>
            <enum name="horizontal" value="1"/>
        </attr>
        <attr name="childGravity">
            <flag name="top" value="0x30"/>
            <flag name="bottom" value="0x50"/>
            <flag name="left" value="0x03"/>
            <flag name="right" value="0x05"/>
            <flag name="center" value="0x11"/>
        </attr>
        <attr name="verticalSpacing" format="dimension"/>
        <attr name="horizontalSpacing" format="dimension"/>
    </declare-styleable>

關於gravity的使用,自己看了一下原始碼,具體瞭解了一下原理,它總共用了8位二進位制來表示,前面4位用來表示y軸,後4位用來表示x軸,具體可以看Gravity類,這個類中有兩個掩碼HORIZONTAL_GRAVITY_MASK(00000111)和VERTICAL_GRAVITY_MASK(01110000),接著如果把所有的gravity變數都用二進位制來表示,就很明瞭地知道這些變數與和或之後的結果是另外哪個gravity變數。
  

原始碼分析

最後就來直接看看程式碼,整個類最複雜的就是onMeasure函數了,這個函式中需要做大量的計算和處理,先貼出來程式碼:

FlowLayout.class類

public class FlowLayout extends ViewGroup{
    public static final int VERTICAL = 0;
    public static final int HORIZONTAL = 1;
    private final int CENTER = 1;
    private final int TOP = 2;
    private final int BOTTOM =3;
    private final int LEFT = 4;
    private final int RIGHT = 5;

    //預設間隙
    private int verticalSpacing = 10;
    private int horizontalSpacing = 10;
    //佈局方向
    private int orientation = HORIZONTAL;
    //子view放置gravity
    private int childGravity = 1;

    public FlowLayout(Context context) {
        this(context, null);
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getAttrValue(attrs);
    }

    private void getAttrValue(AttributeSet attrs){
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.FlowLayout);

        verticalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_verticalSpacing, 10);
        horizontalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_horizontalSpacing, 10);
        orientation = typedArray.getInt(R.styleable.FlowLayout_orientation, HORIZONTAL);

        int gravity = typedArray.getInt(R.styleable.FlowLayout_childGravity, Gravity.TOP);
        if (orientation == HORIZONTAL) {
            gravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
            if (gravity == Gravity.TOP)
                childGravity = TOP;
            else if (gravity == Gravity.BOTTOM)
                childGravity = BOTTOM;
            else
                childGravity = CENTER;
        }else{
            gravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
            if (gravity == Gravity.LEFT)
                childGravity = LEFT;
            else if (gravity == Gravity.RIGHT)
                childGravity = RIGHT;
            else
                childGravity = CENTER;
        }

        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (getChildCount() <= 0){
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }

        int paddingTop = getPaddingTop();
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        int width;
        int height;

        int childWidth;
        int childHeight;

        //該行最大子view大小
        int maxChildSize = 0;
        //剩餘大小
        int lastSize;

        //水平佈局,寬度固定,高度變化
        if (orientation == HORIZONTAL) {
            width = MeasureSpec.getSize(widthMeasureSpec);
            height = 0;
            lastSize = width - paddingLeft - paddingRight;

            //如果第一個子view的大小已經超過容器大小
            if (lastSize < getChildAt(0).getLayoutParams().width)
                throw new ChildSizeTooLongException("the 0 child's width too long");
        }
        //垂直佈局,高度固定,寬度變化
        else{
            width = 0;
            height = MeasureSpec.getSize(heightMeasureSpec);
            lastSize = height - paddingTop - paddingBottom;

            //如果第一個子view的大小已經超過容器大小
            if (lastSize < getChildAt(0).getLayoutParams().height)
                throw new ChildSizeTooLongException("the 0 child's height too long");
        }

        //每行的第一個item的序號
        int firstItemOfLine = 0;

        //x,y座標
        int x = paddingLeft;
        int y = paddingTop;

        int childSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            childHeight = lp.height;
            childWidth = lp.width;

            if (childHeight <= 0 || childWidth <= 0) {
                child.measure(childSpec, childSpec);
                childWidth = child.getMeasuredWidth();
                childHeight = child.getMeasuredHeight();
            }
            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));

            if (orientation == HORIZONTAL) {
                lastSize = lastSize - childWidth - horizontalSpacing;
            }else{
                lastSize = lastSize - childHeight - verticalSpacing;
            }

            //需要換行
            if (lastSize < 0) {
                if (orientation == HORIZONTAL) {
                    //根據gravity將上一行的子view放置在正確的位置上
                    for (int j=firstItemOfLine; j<i; j++){
                        View lineChild = getChildAt(j);
                        LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                        if (childGravity == TOP){
                            //預設,無需處理
                        }else if (childGravity == BOTTOM){
                            childLayoutParams.y += maxChildSize - lineChild.getMeasuredHeight();
                        }else if (childGravity == CENTER){
                            childLayoutParams.y += (maxChildSize - lineChild.getMeasuredHeight())/2;
                        }
                    }

                    //將大小重置
                    lastSize = width - paddingLeft - paddingRight - childWidth;

                    //換行之後該行的第一個view大小超過整體父view大小
                    if (lastSize < 0)
                        throw new ChildSizeTooLongException("the " + i + " child's width too long");

                    //高換行
                    height += maxChildSize + verticalSpacing;
                    //換行之後的第一行座標
                    x = paddingLeft;
                    y += maxChildSize + verticalSpacing;
                    //將最大高度值置為這第一個view的高度
                    maxChildSize = childHeight;
                }else{
                    //根據gravity將上一行的子view放置在正確的位置上
                    for (int j=firstItemOfLine; j<i; j++){
                        View lineChild = getChildAt(j);
                        LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                        if (childGravity == LEFT){
                            //預設,無需處理
                        }else if (childGravity == RIGHT){
                            childLayoutParams.x += maxChildSize - lineChild.getMeasuredWidth();
                        }else if (childGravity == CENTER){
                            childLayoutParams.x += (maxChildSize - lineChild.getMeasuredWidth())/2;
                        }
                    }

                    //將大小重置
                    lastSize = height - paddingTop - paddingBottom - childHeight;

                    //換行之後該行的第一個view大小超過整體父view大小
                    if (lastSize < 0)
                        throw new ChildSizeTooLongException("the " + i + " child's height too long");

                    //寬換列
                    width += maxChildSize + horizontalSpacing;
                    //換列之後的第一列座標
                    x += maxChildSize + horizontalSpacing;
                    y = paddingTop;
                    //將最大寬度值置為這第一個view的寬度
                    maxChildSize = childWidth;
                }

                //換行之後的第一個item序號
                firstItemOfLine= i;
            }
            //不需要換行
            else {
                if (orientation == HORIZONTAL) {
                    //計算出這一行子view中高度最大的view
                    maxChildSize = maxChildSize > childHeight ? maxChildSize : childHeight;
                }else{
                    //計算出這一列子view中寬度最大的view
                    maxChildSize = maxChildSize > childWidth ? maxChildSize : childWidth;
                }
            }
            lp.setXY(x, y);
            if (orientation == HORIZONTAL) {
                x += childWidth + horizontalSpacing;
            }else{
                y += childHeight + verticalSpacing;
            }
        }
        if (orientation == HORIZONTAL) {
            height += maxChildSize;
            height += + paddingBottom + paddingTop;
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
            //不要忘記最後一行
            for (int i=firstItemOfLine; i<getChildCount(); i++){
                View lineChild = getChildAt(i);
                LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                if (childGravity == TOP){
                    //預設,無需處理
                }else if (childGravity == BOTTOM){
                    childLayoutParams.y += maxChildSize - lineChild.getMeasuredHeight();
                }else if (childGravity == CENTER){
                    childLayoutParams.y += (maxChildSize - lineChild.getMeasuredHeight())/2;
                }
            }
        }else{
            width += maxChildSize;
            width += paddingLeft + paddingRight;
            //不要忘記最後一列
            for (int i=firstItemOfLine; i<getChildCount(); i++){
                View lineChild = getChildAt(i);
                LayoutParams childLayoutParams = (LayoutParams) lineChild.getLayoutParams();
                if (childGravity == LEFT){
                    //預設,無需處理
                }else if (childGravity == RIGHT){
                    childLayoutParams.x += maxChildSize - lineChild.getMeasuredWidth();
                }else if (childGravity == CENTER){
                    childLayoutParams.x += (maxChildSize - lineChild.getMeasuredWidth())/2;
                }
            }
        }
        setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
        }
    }
    /**
     * 設定佈局方向
     * @param orientation {@link #HORIZONTAL}or{@link #VERTICAL}
     */
    public void setOrientation(int orientation){
        if (orientation!=HORIZONTAL && orientation!=VERTICAL)
            throw new IllegalArgumentException("orientation error");
        this.orientation = orientation;
        invalidate();
    }
    public int getOrientation(){
        return orientation;
    }
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attributeSet) {
        return new LayoutParams(getContext(), attributeSet);
    }
    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }
    public static class LayoutParams extends ViewGroup.LayoutParams{
        public int x;
        public int y;
        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }
        public LayoutParams(int width, int height) {
            super(width, height);
        }
        public void setXY(int x, int y){
            this.x = x;
            this.y = y;
        }
    }
    public static class ChildSizeTooLongException extends RuntimeException{
        public ChildSizeTooLongException(String message){
            super(message);
        }
    }
}

onMeasure函式第一步根據佈局方向來確定整個view的寬或高,固定一個值不變,去計算另外一個值大小。第二步,迴圈該view所有子view,先獲取子view寬和高,如果未知就讓子view自己去計算寬和高;接著根據子view的大小去計算是否需要換行,垂直佈局和水平佈局的處理方式有些許差異;最後計算出子view的x和y軸座標,並且賦值到該子view的layoutparams中即可。第三步,根據以上的計算結果最後統計出整個父view的大小並且呼叫setMeasuredDimension方法收尾即可。

onLayout函式非常簡單,就是根據onMeasure函式中的計算結果x和y來佈局所有子view。

問題討論

最後還有一個問題就是

Method 'onMeasure' is too complex to analyze by data flow algorithm

由於在onMeasure函式裡面的計算和處理程式碼有點多,導致在實際onMeasure操作時有些耗時,經測算,在HTC one m8t上面加入1000個子view,onMeasure函式會執行400ms左右,在500個以上就能明顯感覺到卡頓,這個需要怎麼處理,onMeasure函式我已經優化過了,留下的都是一些必要的操作,求指點~~

整體原始碼下載

佈局類FlowLayout.class程式碼位於com.android.libcore_ui.widget.FlowLayout中
測試類FlowLayoutActivity.class程式碼位於com.android.sample.test_widget.FlowLayoutActivity中。

最後可以厚顏無恥的要一顆星麼~(@^_^@)~