view 的onLayout過程源碼分析及應用

上一節我們講了view 的測量過程,接着上節講講onLayout view的擺放過程。

講解順序:

1.控件擺放原理分析

2.原生控件源碼擺放過程分析

3.如何在自定義的viewGroup中根據需求擺放控件

1.控件擺放原理分析

首先說控件的擺放是建立在控件的測量的基礎之上的,試想,如果你連子控件的大小,以及對應的父控件的大小都不知道,你如何去擺放,即使擺放了也沒有實際意義,因爲父類不會隨着子類的改變而改變,所以首先父控件和子控件的大小我們現在已經知道了,具體如何擺放方式其實在測量的時候也已經決定了。

首先我們先假定一種擺放方式:將控件橫向一次從左到右擺放,如果一行擺放不下再另起一行擺放,viewGroup佈局信息是這樣的

  <com.example.macbook.demoproject.measure.MyGroupMeasureVeiw
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        android:padding="10dp">

計算父類寬高規則是這樣的:

父類寬度規則是:如果所有控件寬度方向之和大於設備寬度那麼寬度就是設備寬度,否則是寬度之和,包括margin值。

父類高度規則是:子控件所在每行的最大高度之和 包括margin值 就是父控件高度,既(每行最大高度包含上下margin值, 的累加值)。

代碼還是上次使用的代碼:

public class MyGroupMeasureVeiw extends ViewGroup {
    private static final String TAG = "MyGroupMeasureVeiw";
    Context context;
    int widthL;
    /**
     * 父類最大寬度
     */
    int maxViewGroupWidth = 0;

    public MyGroupMeasureVeiw(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        int d = (int) context.getResources().getDisplayMetrics().xdpi;
        widthL = context.getResources().getDisplayMetrics().widthPixels;
        int dsdd = context.getResources().getDisplayMetrics().heightPixels;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int childCount = getChildCount();
        if (childCount == 0) {//如果沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
            setMeasuredDimension(0, 0);
        } else {
            int height = getTotleHeight();
            int width = maxViewGroupWidth != 0 ? maxViewGroupWidth : getMaxChildWidth();
            //如果寬高都是包裹內容最大值模式
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我們將高度設置爲所有子View的高度相加,寬度設爲子View中最大的寬度
                setMeasuredDimension(width, height);
            } else if (heightMode == MeasureSpec.AT_MOST) {
                //如果只有高度是包裹內容最大值模式
                //寬度設置爲ViewGroup自己的測量寬度,高度設置爲所有子View的高度總和
                setMeasuredDimension(widthSize, height);
            } else if (widthMode == MeasureSpec.AT_MOST) {
                //如果只有寬度是包裹內容
                //寬度設置爲子View中寬度最大的值,高度設置爲ViewGroup自己的測量值
                setMeasuredDimension(width, heightSize);
            }
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "getTotleHeight onLayout ch-->: l" + l + "t: " + t + "r: " + r + "b: " + b);
        //記錄當前的高度位置
        int childCount = getChildCount();
        /**
         * 保存一行中高度最大的一個view的高度包括margin值
         */
        int maxHeight = 0;
        /**
         * y軸方向每次需要累加的view的高度,用於計算Y值得大小
         */
        int maxYHeight = 0;
        /**
         * view x軸方向最大寬度用於判斷view是否需要換行
         */
        int maxWidth = 0;
        /**
         * 左邊距離
         */
        int x = 0;
        /**
         * 右邊距離
         */
        int y = 0;
        /**
         * 右側距離
         */
        int xw = 0;
        /**
         *
         */
        int yh = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            /**
             * 獲取當前view的高度
             */
            int height = childView.getMeasuredHeight();
            int heightds = childView.getHeight();
            /**
             * 獲取當前view的寬度
             */
            int width = childView.getMeasuredWidth();
            /**
             * 獲取當前view的寬度包含margin值因爲在實際的控件擺放中是需要將margin值空出來的
             */
            int viewWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            x += lp.leftMargin;
            y = lp.topMargin + maxYHeight;
            xw = x + width;
            yh = y + height;
            if (maxWidth <= widthL && (maxWidth + viewWidth) >= widthL) { //view在本行放滿了累加本行最大高度
                /**
                 * 換行只和y軸方向座標有關x方向恢復默認
                 */
                x = lp.leftMargin;
                xw = x + width;

                maxWidth = viewWidth;
                maxYHeight += maxHeight;
                y = maxYHeight + lp.topMargin;
                yh = y + height;
                Log.d(TAG, "getTotleHeight: maxHeight:" + y);
            } else {
                maxWidth += viewWidth;
                //獲取一行中高度最大的view的高度
                int hHeight = height + lp.topMargin + lp.bottomMargin;
                if (maxHeight < hHeight) {
                    maxHeight = hHeight;
                }
                Log.d(TAG, "getTotleHeight: maxWidth:" + maxWidth);
            }
            Log.d(TAG, "getTotleHeight onLayout: l" + x + "t: " + y + "r: " + xw + "b: " + yh);
            childView.layout(x, y, xw, yh);
            /**
             *view擺放成功後將 marginLeft + width + marginRight 得到X 保存一個完整的view所佔的寬度
             */
            x = xw + lp.rightMargin;
        }
    }

    /***
     * 獲取子View中寬度最大的值
     */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            /**
             * 獲取當前view的寬度
             */
            int width = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //view 寬度累計大於屏幕寬度,將寬度設置爲設備寬度
            if (maxWidth <= widthL && (maxWidth + width) >= widthL) {
                maxWidth = widthL;
                Log.d(TAG, "getTotleHeight: maxHeight:" + maxWidth);
                return maxWidth;
            } else {
                maxWidth += width;
                Log.d(TAG, "getTotleHeight: maxWidth:" + maxWidth);
            }
        }
        return maxWidth;
    }

    /***
     * 將所有子View擺放的行的高度相加
     **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int maxHeight = 0;
        int maxYHeight = 0;
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();

            /**
             * 獲取當前view的高度
             */
            int height = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            /**
             * 獲取當前view的寬度
             */
            int width = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //view在本行放滿了累加本行最大高度,用於計算viewgroup高度
            maxWidth += width;
            //獲取一行中高度最大的view的高度
            if (maxHeight < height) {
                maxHeight = height;
            }
            if (maxViewGroupWidth < widthL) {
                maxViewGroupWidth = maxWidth;
            }
            if (maxWidth <= widthL && (maxWidth + width) >= widthL) {
                maxViewGroupWidth = widthL;
                maxYHeight += maxHeight;
                maxHeight = 0;
                maxWidth = 0;
                Log.d(TAG, "getTotleHeight: maxHeight:" + maxYHeight);
            }
            if (i==childCount-1) {
                maxYHeight+=maxHeight;
            }
            Log.d(TAG, "getTotleHeight: maxWidth:" + maxHeight);
        }


        return maxYHeight;
    }

    public void scrollPage(int index) {
        scrollTo(index * getWidth(), 0);
    }
}

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_dark">

    <com.example.macbook.demoproject.measure.MyGroupMeasureVeiw
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@android:color/white"
            android:gravity="center"
            android:padding="20dp"
            android:text="打ffffffffffffff快點1"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="15sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@android:color/white"
            android:padding="20dp"
            android:text="打fffffffffdsdfvbfd快點2"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="15sp" />

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快點3"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快dfdsdaaaaaaaaaaaadad點4"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快點5"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快dsddadf點6"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快dsddadf點7"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快dsddadf點8"-->
        <!--android:background="@android:color/white"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->


        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:gravity="center"-->
        <!--android:padding="20dp"-->
        <!--android:text="打ffffffffffffff快點1"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打fffffffffdsdfvbfd快點2"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快點3"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快dfdsdaaaaaaaaaaaadad點4"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快點5"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->

        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:padding="20dp"-->
        <!--android:text="打快dsddadf點6"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->
        <!--<TextView-->
        <!--android:layout_width="wrap_content"-->
        <!--android:layout_height="wrap_content"-->
        <!--android:layout_margin="5dp"-->
        <!--android:background="@android:color/white"-->
        <!--android:gravity="center"-->
        <!--android:padding="20dp"-->
        <!--android:text="打ffffffffffffff快點1"-->
        <!--android:textColor="@android:color/holo_red_dark"-->
        <!--android:textSize="15sp" />-->


    </com.example.macbook.demoproject.measure.MyGroupMeasureVeiw>

</LinearLayout>

正常效果圖:                                                                               如果我們將onLayout中的代碼註釋掉效果是這樣的:

我們看到,不管子類有沒有擺放,其實父類大小已經確定了,這就是我前邊說的,父類大小及子類的擺放模式其實在計算父類的大小的時候就已經確定了,我們在onLayout中能做的就是根據之前的擺放模式設置控件的,x,y座標及寬高,這樣控件就會顯示在相應的位置上了。

用我自己的理解形象一點吧,假如你家客廳現在要鋪設地磚,現在客廳寬長已經定了,地磚的擺放就像我上邊說的那樣擺放,地磚的固定長寬就是控件的長寬,磚與磚之間的縫隙就是margin值,那麼第一塊磚的四個值分別是:x: 左margin,y:上margin,xw:寬度加左margin,yh:高度加上margin 四個值,看圖理解下。

這就是view擺放的基本原理。

2.原生控件源碼擺放過程分析

就拿我們最常用的LinearLayout類 的onLayout進行分析

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

這裏先判斷view 是橫向的還是垂直的,我們只看橫向的。

layoutHorizontal(l, t, r, b);
void layoutHorizontal(int left, int top, int right, int bottom) {
        /**
         * 這裏有四個參數,可以看做是左上角座標值(left(x),top(y)) 右下角座標(right(x),bottom(y)).
         * 其實就是父view的繪製區域
         */
        final boolean isLayoutRtl = isLayoutRtl();
        final int paddingTop = mPaddingTop;

        int childTop;
        int childLeft;

        // Where bottom of child should go
        /**
         * 父類的高度
         */
        final int height = bottom - top;
        /**
         * 父類的可供子類使用的y方向的最大高度點
         */
        int childBottom = height - mPaddingBottom;

        // Space available for child
        /**
         * 可供子類使用的總高度
         */
        int childSpace = height - paddingTop - mPaddingBottom;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;

        final boolean baselineAligned = mBaselineAligned;

        final int[] maxAscent = mMaxAscent;
        final int[] maxDescent = mMaxDescent;

        final int layoutDirection = getLayoutDirection();
        /**
         * mTotalLength measure過程中計算出的寬度
         * 通過對齊方式計算出x方向座標
         */
        switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) {
            case Gravity.RIGHT:
                // mTotalLength contains the padding already
                childLeft = mPaddingLeft + right - left - mTotalLength;
                break;

            case Gravity.CENTER_HORIZONTAL:
                // mTotalLength contains the padding already
                childLeft = mPaddingLeft + (right - left - mTotalLength) / 2;
                break;

            case Gravity.LEFT:
            default:
                childLeft = mPaddingLeft;
                break;
        }

        int start = 0;
        int dir = 1;
        //In case of RTL, start drawing from the last child.
        /**
         * 佈局是否是從右到左顯示,也就是rtl模式
         * 這種情況我們可以忽略,一般用不到
         */
        if (isLayoutRtl) {
            start = count - 1;
            dir = -1;
        }

        for (int i = 0; i < count; i++) {
            /**
             * 忽略rtl 正常childIndex 是從0開始的
             */
            final int childIndex = start + dir * i;
            final View child = getVirtualChildAt(childIndex);
            if (child == null) {
                childLeft += measureNullChild(childIndex);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                int childBaseline = -1;

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                 /*
                 baselineAligned用於設置控件的對齊方式是內容對齊還是方位對齊
                 基線對齊
                  */
                if (baselineAligned && lp.height != LayoutParams.MATCH_PARENT) {
                    childBaseline = child.getBaseline();
                }

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                /**
                 * 計算view y方向左邊
                 */
                switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
                    case Gravity.TOP:
                        childTop = paddingTop + lp.topMargin;
                        if (childBaseline != -1) {
                            childTop += maxAscent[INDEX_TOP] - childBaseline;
                        }
                        break;

                    case Gravity.CENTER_VERTICAL:
                        // Removed support for baseline alignment when layout_gravity or
                        // gravity == center_vertical. See bug #1038483.
                        // Keep the code around if we need to re-enable this feature
                        // if (childBaseline != -1) {
                        //     // Align baselines vertically only if the child is smaller than us
                        //     if (childSpace - childHeight > 0) {
                        //         childTop = paddingTop + (childSpace / 2) - childBaseline;
                        //     } else {
                        //         childTop = paddingTop + (childSpace - childHeight) / 2;
                        //     }
                        // } else {
                        childTop = paddingTop + ((childSpace - childHeight) / 2)
                                + lp.topMargin - lp.bottomMargin;
                        break;

                    case Gravity.BOTTOM:
                        childTop = childBottom - childHeight - lp.bottomMargin;
                        if (childBaseline != -1) {
                            int descent = child.getMeasuredHeight() - childBaseline;
                            childTop -= (maxDescent[INDEX_BOTTOM] - descent);
                        }
                        break;
                    default:
                        childTop = paddingTop;
                        break;
                }
                /**
                 * view之間的分割線寬度
                 */
                if (hasDividerBeforeChildAt(childIndex)) {
                    childLeft += mDividerWidth;
                }
                /**
                 * 將控件本身距離左邊margin加上,得到view左側 x方向起點
                 */
                childLeft += lp.leftMargin;
                /**
                 * 開始擺放view
                 * getLocationOffset 一般性忽略不計
                 */
                setChildFrame(child, childLeft + getLocationOffset(child), childTop,
                        childWidth, childHeight);
                /**
                 * 將x座標移動到view的右邊,以便下一個控件使用
                     getNextLocationOffset 同樣忽略
                 */
                childLeft += childWidth + lp.rightMargin +
                        getNextLocationOffset(child);

                i += getChildrenSkipCount(child, childIndex);
            }
        }
    }

這裏就是橫向線性佈局,對子view擺放的策略,唯一和我講的例子中的區別就是他不需要考慮一行使用空間完了,如何換行重新計算view的x y 方向的值。

總結:對於這個,其實只需要考慮x方向的變化,一般性y方向是不會變化的,我們只需要計算或者記錄一次,就不用管了,x方向也是計算第一個控件的左側x值,然後在其擺放完成後將x值改爲控件的xw 既該控件的右側的座標值,那麼下一個控件只需要要將該值加上自身的左側的margin值即可得到左側的擺放位置,以此類推。

3.如何在自定義的viewGroup中根據需求擺放控件

要求還是之前我們的要求,控件從左到右依次擺放,如果寬度方向剩餘空間不足以,擺放下控件,就在y方向換行,繼續擺放。

我們直接看下onLayout 方法

   @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "getTotleHeight onLayout ch-->: l:" + l + "t: " + t + "r: " + r + "b: " + b);
        //記錄當前的高度位置
        int childCount = getChildCount();
        /**
         * 保存一行中高度最大的一個view的高度包括margin值
         */
        int maxHeight = 0;
        /**
         * y軸方向每次需要累加的view的高度,用於計算Y值得大小
         */
        int maxYHeight = 0;
        /**
         * view x軸方向最大寬度用於判斷view是否需要換行
         */
        int maxWidth = 0;
        /**
         * 左邊距離
         */
        int x = 0;
        /**
         * 右邊距離
         */
        int y = 0;
        /**
         * 右側距離
         */
        int xw = 0;
        /**
         *
         */
        int yh = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            /**
             * 獲取當前view的高度
             */
            int height = childView.getMeasuredHeight();
            int heightds = childView.getHeight();
            /**
             * 獲取當前view的寬度
             */
            int width = childView.getMeasuredWidth();
            /**
             * 獲取當前view的寬度包含margin值因爲在實際的控件擺放中是需要將margin值空出來的
             */
            int viewWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            x += lp.leftMargin;
            y = lp.topMargin + maxYHeight;
            xw = x + width;
            yh = y + height;
            if (maxWidth <= widthL && (maxWidth + viewWidth) >= widthL) { //view在本行放滿了累加本行最大高度
                /**
                 * 換行只和y軸方向座標有關x方向恢復默認
                 */
                x = lp.leftMargin;
                xw = x + width;

                maxWidth = viewWidth;
                maxYHeight += maxHeight;
                y = maxYHeight + lp.topMargin;
                yh = y + height;
                Log.d(TAG, "getTotleHeight: maxHeight:" + y);
            } else {
                maxWidth += viewWidth;
                //獲取一行中高度最大的view的高度
                int hHeight = height + lp.topMargin + lp.bottomMargin;
                /**
                 * 將控件中高度最大的一個作爲整行的高度
                 */
                if (maxHeight < hHeight) {
                    maxHeight = hHeight;
                }
                Log.d(TAG, "getTotleHeight: maxWidth:" + maxWidth);
            }
            Log.d(TAG, "getTotleHeight onLayout: l" + x + "t: " + y + "r: " + xw + "b: " + yh);
            childView.layout(x, y, xw, yh);
            /**
             *view擺放成功後將 marginLeft + width + marginRight 得到X 保存一個完整的view所佔的寬度
             * 便於下一個控件累加x
             */
            x = xw + lp.rightMargin;
        }
    }

這裏備註寫的比較詳細,關鍵代碼講下,

int viewWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; 得到到的是一個view所佔的空間,它包含左右方向的margin,這點要注意,如果忽略掉了margin,就會導致本該換行的控件沒有換行顯示不全。
childView.layout(x, y, xw, yh); 這個就是具體的根據我們提供的座標大小去擺放view,注意四個參數和我上邊畫圖是一樣的,可以再看下有助於理解。
x = xw + lp.rightMargin; 這句話其實和我們解讀源碼裏最後一句的作用是一樣的,都是保存當前view右側的x方向的座標值,便於下一個控件使用。

現在看下使用xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_dark">

    <com.example.macbook.demoproject.measure.MyGroupMeasureVeiw
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        android:padding="10dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@android:color/white"
            android:gravity="center"
            android:padding="20dp"
            android:text="打ffffffffffffff快點1"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="15sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@android:color/white"
            android:padding="20dp"
            android:text="打fffffffffdsdfvbfd快點2"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="15sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@android:color/white"
            android:padding="20dp"
            android:text="打快點3"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="15sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@android:color/white"
            android:padding="20dp"
            android:text="打快dfdsdaaaaaaaaaaaadad點4"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="15sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:background="@android:color/white"
            android:padding="20dp"
            android:text="打快點5"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="15sp" />

        <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:background="@android:color/white"
        android:padding="20dp"
        android:text="打快dsddadf點6"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="15sp" />

        <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:background="@android:color/white"
        android:padding="20dp"
        android:text="快7"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="15sp" />

        <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:padding="20dp"
        android:text="打快dsddadf點8"
        android:background="@android:color/white"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="15sp" />


        <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:background="@android:color/white"
        android:gravity="center"
        android:padding="20dp"
        android:text="打ffffffffffffff快點1"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="15sp" />

        <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:background="@android:color/white"
        android:padding="20dp"
        android:text="打fffffffffdsdfvbfd快點2"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="15sp" />

    </com.example.macbook.demoproject.measure.MyGroupMeasureVeiw>

</LinearLayout>

顯示效果

完整的類

public class MyGroupMeasureVeiw extends ViewGroup {
    private static final String TAG = "MyGroupMeasureVeiw";
    Context context;
    int widthL;
    /**
     * 父類最大寬度
     */
    int maxViewGroupWidth = 0;

    public MyGroupMeasureVeiw(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        int d = (int) context.getResources().getDisplayMetrics().xdpi;
        widthL = context.getResources().getDisplayMetrics().widthPixels;
        int dsdd = context.getResources().getDisplayMetrics().heightPixels;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int childCount = getChildCount();
        if (childCount == 0) {//如果沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
            setMeasuredDimension(0, 0);
        } else {
            int height = getTotleHeight();
            int width = maxViewGroupWidth != 0 ? maxViewGroupWidth : getMaxChildWidth();
            //如果寬高都是包裹內容最大值模式
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我們將高度設置爲所有子View的高度相加,寬度設爲子View中最大的寬度
                setMeasuredDimension(width, height);
            } else if (heightMode == MeasureSpec.AT_MOST) {
                //如果只有高度是包裹內容最大值模式
                //寬度設置爲ViewGroup自己的測量寬度,高度設置爲所有子View的高度總和
                setMeasuredDimension(widthSize, height);
            } else if (widthMode == MeasureSpec.AT_MOST) {
                //如果只有寬度是包裹內容
                //寬度設置爲子View中寬度最大的值,高度設置爲ViewGroup自己的測量值
                setMeasuredDimension(width, heightSize);
            }
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "getTotleHeight onLayout ch-->: l:" + l + "t: " + t + "r: " + r + "b: " + b);
        //記錄當前的高度位置
        int childCount = getChildCount();
        /**
         * 保存一行中高度最大的一個view的高度包括margin值
         */
        int maxHeight = 0;
        /**
         * y軸方向每次需要累加的view的高度,用於計算Y值得大小
         */
        int maxYHeight = 0;
        /**
         * view x軸方向最大寬度用於判斷view是否需要換行
         */
        int maxWidth = 0;
        /**
         * 左邊距離
         */
        int x = 0;
        /**
         * 右邊距離
         */
        int y = 0;
        /**
         * 右側距離
         */
        int xw = 0;
        /**
         *
         */
        int yh = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            /**
             * 獲取當前view的高度
             */
            int height = childView.getMeasuredHeight();
            int heightds = childView.getHeight();
            /**
             * 獲取當前view的寬度
             */
            int width = childView.getMeasuredWidth();
            /**
             * 獲取當前view的寬度包含margin值因爲在實際的控件擺放中是需要將margin值空出來的
             */
            int viewWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            x += lp.leftMargin;
            y = lp.topMargin + maxYHeight;
            xw = x + width;
            yh = y + height;
            if (maxWidth <= widthL && (maxWidth + viewWidth) >= widthL) { //view在本行放滿了累加本行最大高度
                /**
                 * 換行只和y軸方向座標有關x方向恢復默認
                 */
                x = lp.leftMargin;
                xw = x + width;

                maxWidth = viewWidth;
                maxYHeight += maxHeight;
                y = maxYHeight + lp.topMargin;
                yh = y + height;
                Log.d(TAG, "getTotleHeight: maxHeight:" + y);
            } else {
                maxWidth += viewWidth;
                //獲取一行中高度最大的view的高度
                int hHeight = height + lp.topMargin + lp.bottomMargin;
                /**
                 * 將控件中高度最大的一個作爲整行的高度
                 */
                if (maxHeight < hHeight) {
                    maxHeight = hHeight;
                }
                Log.d(TAG, "getTotleHeight: maxWidth:" + maxWidth);
            }
            Log.d(TAG, "getTotleHeight onLayout: l" + x + "t: " + y + "r: " + xw + "b: " + yh);
            childView.layout(x, y, xw, yh);
            /**
             *view擺放成功後將 marginLeft + width + marginRight 得到X 保存一個完整的view所佔的寬度
             * 便於下一個控件累加x
             */
            x = xw + lp.rightMargin;
        }
    }

    /***
     * 獲取子View中寬度最大的值
     */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            /**
             * 獲取當前view的寬度
             */
            int width = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //view 寬度累計大於屏幕寬度,將寬度設置爲設備寬度
            if (maxWidth <= widthL && (maxWidth + width) >= widthL) {
                maxWidth = widthL;
                Log.d(TAG, "getTotleHeight: maxHeight:" + maxWidth);
                return maxWidth;
            } else {
                maxWidth += width;
                Log.d(TAG, "getTotleHeight: maxWidth:" + maxWidth);
            }
        }
        return maxWidth;
    }

    /***
     * 將所有子View擺放的行的高度相加
     **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int maxHeight = 0;
        int maxYHeight = 0;
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();

            /**
             * 獲取當前view的高度
             */
            int height = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            /**
             * 獲取當前view的寬度
             */
            int width = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //view在本行放滿了累加本行最大高度,用於計算viewgroup高度
            maxWidth += width;
            //獲取一行中高度最大的view的高度
            if (maxHeight < height) {
                maxHeight = height;
            }
            if (maxViewGroupWidth < widthL) {
                maxViewGroupWidth = maxWidth;
            }
            if (maxWidth <= widthL && (maxWidth + width) >= widthL) {
                maxViewGroupWidth = widthL;
                maxYHeight += maxHeight;
                maxHeight = 0;
                maxWidth = 0;
                Log.d(TAG, "getTotleHeight: maxHeight:" + maxYHeight);
            }
            if (i==childCount-1) {
                maxYHeight+=maxHeight;
            }
            Log.d(TAG, "getTotleHeight: maxWidth:" + maxHeight);
        }


        return maxYHeight;
    }

    public void scrollPage(int index) {
        scrollTo(index * getWidth(), 0);
    }
}

到此view 的擺放過程就學習完了,下一篇將介紹view的繪製過程onDraw。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章