Android開發&簡單的ViewGroup——FrameLayout

    開門見山的說,一般android開發中,FrameLayout更多的是作爲圖層功能,或者碎片佔位符;如時下的身份證掃描界面,可以利用FrameLayout實現兩級圖層;再有就是一些自定義的控件,往往是FrameLayout的子類;
    其實對於我來說,這個從出生起一直生存到現在的空間,直接使用的次數並不算多,因爲它的功能實在是太過簡單,雖然方便定製視圖,但造輪子的事情早已有無數的先輩們替我們完成了,因此。。。

一、 簡單的FrameLayout

FrameLayout存在的意義彷彿只是提供一個容器,憑藉自己的想象,可以放置任何控件,甚至大小什麼都無所謂,反正超過屏幕的部分會消失掉,不過也正是如此,才體現出了ViewGroup的含義——可以放View的容器;當然,嵌套ViewGroup也是可以的,畢竟ViewGroup也是View的實現類,雖然兩者“長相”相差有點大。

二、 ViewGroup 與View

簡單一提,View和ViewGroup之間的關係錯綜複雜,但他們有一個很直觀的相同的特點——都能被看到;
View表示的是視圖,能被看到,才能更好的表達信息,才能夠與用戶交互,否則就又要回歸單片機的世界了。
這裏就牽扯出重要的部分,view的呈現機制,見得很多地方將“能夠理解View機制”作爲一箇中級菜鳥的合格證,不可否認,純粹做一個碼農,利用現有的控件或者大神寫好的類,不理解這些東西也活的下去,確實如此,即便懂了這些,更深層次的底層實現我們還是一無所知,所以,興趣纔是前提。

三、 View的呈現機制

依靠計算機來理解人的思維,並顯示到屏幕上,是個有些無從下手的問題,還好,最難的部分已經有人做完了,我們所做的只是拿着一堆的七巧板,來拼湊出讓人耳目一新的效果(或許一個良好的UI設計師才適合做移動開發)。好在程序員只需要做一個簡單的工作:用醜陋的界面實現交互的功能,剩下的部分就讓其他人去完成吧。
提及View,不得不推一下那些總結出結論的前輩,沒有他們的努力,在不知道結論的情況下我是不怎麼喜歡去瞅源碼的(時間好緊張。。。)。可以先看一下一系列的文章:深入瞭解View(一)
反正就四篇,即便看不懂,先記住結論也是可以的,有一點需要說明,因爲上述博客太過經典,所以有些滯後,而sdk版本更迭是很快的,因此有些源碼可能會與自己手頭的文檔有些差異,不過沒關係,反正記住了結論,就可以參照源碼自己分析了。
言歸正傳,一個View想要顯示在屏幕上,需要的過程是複雜的,不過這已經被人爲的抽象成了三個階段(當前分析參照API23)

  1. measure 測量:確定view的大小
  2. layout 定位:確定view所在的座標位置
  3. draw 繪製:在指定的座標位置繪製出view

又是一個老生常談的部分,沒辦法,黔驢技窮了,要是不說這個,還真就無從開始了,不過還好,只要看完上述一系列的四篇博客,至少可以去翻源碼了,主要總結一下也不復雜:measure、layout和draw都是由一個類調用的,恩,一稱爲ViewRoot,在該類中由performTraversals()發起這三個過程,先盜用百度圖片:

這裏寫圖片描述

若單說view的話,只從DecorView開始就行了;DecorView可看做一個縱向排列的LinearLayout,包含兩個子佈局TitleView以及ContentView,TitleView我們一般都讓它消失,因此只看ContentView就行,該View不僅是一個佈局,並且還是正好要討論的FrameLayout,再好不過。
先簡單的敘述一下measure,layout,draw好了,反正看完博客自己也可以理解,就不過多××了。

【measure過程】
1、View系統的繪製流程會從ViewRoot的performTraversals()開始,測量根view(這裏從FrameLayout開始考慮比較簡單)
2、對於根View,會調用View的measure(),final類型,無法重寫,measure()中會調用onMeasure()
3、ViewGroup的子類中一般會重寫onMeasure(),完成兩個操作:測量自身,測量子佈局(調用子View的measure(),返回第2步遞歸調用)。而對於View的子類(不是ViewGroup的子類)而言,只完成一個操作:測量自身。
4、View的子類(非ViewGroup子類)中onMeasure()會調用setMeasureDimension()爲成員變量mMeasuredWidth和mMeasuredHeight賦值,完成測量自身的工作;

【layout過程】
1、ViewRoot的performTraversals()會在measure結束後繼續執行,並調用View的layout()來執行定位過程。
2、View類的layout()會調用onLayout(),View類的onLayout()爲空,不進行任何操作;onLayout()主要確定視圖在佈局中的位置,因此一般由具體的ViewGroup的子類來重寫(且在ViewGroup類中,onLayout爲抽象方法)。(View類的layout()只會完成定位自身的工作,ViewGroup的onLayout()來讓子View調用layout())
3、也就是說:若調用layout方法的爲View的子類,那整個layout過程就已經結束,如果調用layout方法的爲ViewGroup子類,則會在ViewGroup子類中調用onLayout方法,該方法會調用所有子view的layout方法,即返回第2步,遞歸調用

【draw過程】
ViewRoot的performTraversals()會在layout結束後繼續執行;會先調用最上層視圖的draw方法(View類中重載了兩個draw方法,一個爲public權限,一個爲默認權限,首先系統會調用default的draw(),在其中會調用computeScroll()。
接下來,一般情況來看(忽略滾動條的繪製或保存canvas等比較麻煩的部分):
若對象爲View的子類,不是ViewGroup的子類:default的draw()會調用public的draw()。
若對象爲ViewGroup的子類,且ViewGroup自身沒有可視部分,則default的draw()會直接調用dispatchDraw(),即一些ViewGroup子類中並不會調用public的draw()和onDraw()。
一般來說,public的draw()不會被重寫,該方法會依次進行如下操作:
* 1、繪製background
* 2、If necessary, save the canvas’ layers to prepare for fading(暫不考慮)
* 3、Draw view’s content,此時即調用onDraw(),View類的該方法爲空,一般View的子類需要重寫該方法,進行內容的繪製。
* 4、draw the children,調用dispatchDraw(),該方法在View類中爲空方法,在ViewGroup子類中則有具體的實現。dispatchDraw()中會調用ViewGroup子類中的drawChild(),drawChild()中會讓子View調用View類非public的draw(),即返回draw流程開始部分。
* 5、If necessary, draw the fading edges and restore layers
* 6、Draw decorations (scrollbars for instance),即前景onDrawForeground 等(忽略此步驟即可)

當然肯定還有一些東西沒有考慮到,比如padding,margin,滾動等等,不過熟悉基本調用過程就可以自定義view了。然後參照一下FrameLayout的源碼,看一下一個ViewGroup如何對自身和子view完成這三個流程。

四、FrameLayout代碼解析

再次說明,這裏參照的SDK版本爲23,每一個版本代碼都會有變化,因此最好找個儘可能“古老”的版本,至少邏輯會清楚一些。接下來代碼會將FrameLayout中重要部分全部註釋出來,有很多變量源碼中也找不到引用,因此這裏只是將一些有View呈現有關的方法列出,一些成員變量即便沒有引用的出處,也給出了大概表示的含義。

package com.android.senior.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by Administrator on 17-1-7.
 * <p/>
 * 註釋FrameLayout的代碼
 */
public class FrameLayout extends android.widget.FrameLayout {
    //默認的gravity
    private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;

    /**
     * 構造函數
     */
    public FrameLayout(
            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        //獲取屬性集合
        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.FrameLayout, defStyleAttr, defStyleRes);

        //獲取xml中FrameLayout的measureAllChildren屬性,屬性爲true則會在測量等過程考慮所有的即使是View.Gone的子View,默認爲false
        if (a.getBoolean(com.android.internal.R.styleable.FrameLayout_measureAllChildren, false)) {
            setMeasureAllChildren(true);
        }

        a.recycle();
    }

    /**
     * 設置是否考慮所有的子view,即連同View.GONE的子view都進行測量,這裏只是修改了一個標誌位
     */
    @android.view.RemotableViewMethod
    public void setMeasureAllChildren(boolean measureAll) {
        mMeasureAllChildren = measureAll;
    }

    /**
     * 這裏及接下來的三個方法可以先簡單的看做時獲取FrameLayout的padding值的(其實該值需要參照xml中FrameLayout的padding屬性值和background(背景圖片或背景資源xml文件——R.drawable.*)中的padding值)
     */
    int getPaddingLeftWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
                mPaddingLeft + mForegroundPaddingLeft;
    }

    int getPaddingRightWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
                mPaddingRight + mForegroundPaddingRight;
    }

    private int getPaddingTopWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
                mPaddingTop + mForegroundPaddingTop;
    }

    private int getPaddingBottomWithForeground() {
        return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
                mPaddingBottom + mForegroundPaddingBottom;
    }

    /**
     * 系統會在View的measure方法中調用onMeasure方法,該方法完成兩個操作:
     * 測量FrameLayout自身的大小
     * 測量每一個子View的大小
     * <p/>
     * 在該方法調用之後,纔可以通過getMeasuredWidth()和getMeasuredHeight()方法獲取FrameLayout真實的大小
     * {@inheritDoc}
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //獲取所有子View的個數,該方法返回ViewGroup的成員變量mChildrenCount,該成員變量在ViewGroup的addInArray方法中更改值的大小。
        int count = getChildCount();

        //當傳入的寬高對象中mode不都是EXACTLY時,該變量measureMatchParentChildren爲true;即表示該FrameLayout的寬高還是有待測量的,不等於父佈局的寬高(先不考慮margin和padding)
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                        MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;

        //mMatchParentChildren表示View的集合,該集合中每個寬高屬性中至少有一個爲MATCH_PARENT
        mMatchParentChildren.clear();

        //maxHeight表示該FrameLayout最大的高度,maxWidth同理;childState屬性暫時不考慮
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        //對所有的子View進行遍歷
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //mMeasureAllChildren默認爲false,在構造函數中進行了賦值操作,因此不爲Gone則進入if中
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //先對子View進行一次測量,此次測量出的子View寬高是沒有除去子view的margin以及FrameLayout的padding的,因此肯定不準確;在該measureChildWithMargins方法中會讓子View去調用自身的measure方法;這裏可以成爲估測
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                //獲取子View的LayoutParams對象,該對象中包括了layout_width等屬性的值
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //取(maxWidth,子View的width加上子view的左右margin值)中較大的值,maxHeight同理
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        //如果此FrameLayout的寬高並不能直接確定,且子View的寬高有一個是MATCH_PARENT,則將該子View放到mMatchParentChildren集合中,該集合表示match自身佈局的子View集合
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too;負責padding的處理,將上面計算出的maxWidth和maxHeight再加上自身定義的padding值(paddingBottom+paddingTop+maxHeight,paddingLeft+paddingRight+maxWidth)
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width;結合minHeight等屬性來選擇出最大的一個maxHeight和maxWidth值
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width;結合background等屬性來選擇出最大的一個maxHeight和maxWidth值
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        //提供一個maxWidth值和widthMeasureSpec對象,Spec對象中可以提取Mode屬性,根據該屬性來決定FrameLayout自身應當取的width值;測量height同理;然後調用setMeasuredDimension方法,該方法調用之後,通過getMeasuredWidth()和getMeasuredHeight()方法才能獲取到FrameLayout正確的寬高值。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        //如果FrameLayout的寬高並不能直接確定(如果可以確定的話mMatchParentChildren的size肯定爲0),且有超過一個子View在集合中
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);

                //獲取MarginLayoutParams對象,該對象在ViewGroup中定義,所有自定義的ViewGroup對象中的LayoutParams對象基本都是該對象的子類
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                //如果子View寬爲MATCH_PARENT,則進行如下操作(注:mMatchParentChildren集合中所有的子View寬高至少有一個屬性爲MATCH_PARENT)
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    //這裏getMeasuredWidth()就 可以獲取到該FrameLayout正確的寬度;然後減去FrameLayout可能有的paddingLeft和paddingRight值,在減去子view中可能定義的layout_marginLeft,layout_marginRight值,剩下的部分就是子View的寬度;如果FrameLayout測量正確且寬度爲MATCH_PARENT,則此處就完成了子view寬度的測量。
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    //寬度值MATCH_PARENT屬於EXACTLY模式,並且求出了子view的width,因此可以生成寬度的Spec對象。
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    //如果寬度不是MATCH_PARENT,那麼高度肯定是 MATCH_PARENT;此時要計算子View的寬度,就只能依賴view的measure過程中最最核心的方法——getChildMeasureSpec();該方法根據父佈局的widthMeasureSpec,多餘的FrameLayout的padding、子View的margin,以及子佈局估測的寬度來綜合得出一個合理的子view的width值
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                                    lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                //childHeightMeasureSpec計算方法與子view的width的計算方法類似
                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                                    lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                //然後讓子view去調用一次measure方法
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

    /**
     * ViewGroup類中onLayout爲抽象方法,因此所有的ViewGroup的實現類必須重寫該方法
     * <p/>
     * View類中onLayout的方法說明如下:
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * 即:所有的ViewGroup實現類,都必須重寫onLayout方法,並在該方法中調用所有子View的layout方法;
     * 更多的是,在View的layout方法中,已經通過setFrame方法確定了自身的位置
     * <p/>
     * 注:layout過程是一個確定位置的過程,即父佈局有所有權來決定子佈局應當所處的位置,這與measure過程是不同的
     * measure過程需要父佈局給的限定大小,以及Spec的mode,再加上子view的mode來綜合確定。
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //根據該方法的說明,在調用onLayout方法前,就已經在layout方法中調用了setFrame爲ViewGroup(這裏是FrameLayout)自身進行了layout過程,即left,top,right,bottom已經被賦值了。
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    /**
     * 尤其重要的是,FrameLayout的left,top,right,bottom值是相對於FrameLayout的父佈局而言的;對子view調用layout方法時,傳入的座標系則是以FrameLayout而言的,這點需要進行轉換
     *
     * @param left             FrameLayout的左側位置
     * @param top              FrameLayout的頂部位置
     * @param right            FrameLayout的右側位置
     * @param bottom           FrameLayout的底部位置
     * @param forceLeftGravity 是否強行設置子view的排列方式Gravity爲left
     */
    void layoutChildren(int left, int top, int right, int bottom,
                        boolean forceLeftGravity) {
        //子view的個數
        final int count = getChildCount();

        //這裏進行座標系的變換:FrameLayout的左上角現在變爲了(0,0),因此FrameLayout的paddingLeft值會限定子View所處的位置,即子view的left值一般情況下是小於FrameLayout的paddingLeft值的(有一種情況除外,即子view的marginLeft屬性可能設置爲負數),因此這裏parentLeft表示:相對於子view來說,最左側的座標值。 paddingRight屬性值
        final int parentLeft = getPaddingLeftWithForeground();

        //同理,進行座標系轉換,parentRight表示子View右側座標最大的值,計算方法是:FrameLayout的右側座標減去左側座標減去paddingRight值,即width減去paddingRight(正常情況下)
        final int parentRight = right - left - getPaddingRightWithForeground();

        //這裏parentTop和parentBottom同上
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        //開始layout子view的過程
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //如果子view的visibility爲View.GONE,則不用考慮,直接當做沒有這個子View
            if (child.getVisibility() != GONE) {
                //子view獲取LayoutParams,這個對象可以當做在xml文件中添加元素時設置的,padding,margin,layout_width等值的集合。
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                //因爲這個方法屬於layout過程,measure過程會先執行,因此這裏可以直接獲取子view的寬高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                //子view的左側,上側位置;這裏只需要這兩個屬性就行,右下座標可以結合width,height屬性得到
                int childLeft;
                int childTop;

                //gravity:子view的放置方式,這裏與layout_gravity要區分開;layout_gravity表示FrameLayout在父佈局中如何放置;gravity表示子view在FrameLayout中如何放置;
                int gravity = lp.gravity;
                if (gravity == -1) {
                    //這裏很明顯,如果gravity爲-1,則設置gravity爲默認值;這裏的-1從何而來暫不考慮,姑且當做如果FrameLayout沒有設置gravity屬性,則lp.gravity默認爲-1
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                //layoutDirection表示習慣的閱讀方式,一般習慣時從左向右讀,但有些情況下(如古代詩文的閱讀方式,或臺灣文字閱讀方式),閱讀時從右向左的;該屬性值可以通過在manifest文件中application節點下設置android:supportsRtl 來改變
                final int layoutDirection = getLayoutDirection();

                //結合gravity和layoutDirection得出真實的佈局方式absoluteGravity(水平方向)
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);

                //獲取豎直方向上的佈局方式
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        //如果是水平方向上時Gravity.CENTER_HORIZONTAL佈局方式,則子view左側座標就是——parentLeft(子view初始處於的最左側的座標)【加上】(parentRight - parentLeft - width)/2(這個表示ziview可以處於的最右側座標減去最左側座標再減去子view的寬度,得到橫向上除內容部分空白區域的寬度,然後除以2得到左側空白的寬度)【加上】lp.leftMargin(子view的marginLeft的值,該值可能爲負數,爲負數則表示向左偏移的座標數)【減去】lp.rightMargin(子view的marginRight的值);其實從這裏可以看到,leftMargin和rightMargin在這種情況下不是相對左右側的距離,而是偏移量
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                                lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            //如果是Gravity.RIGHT佈局方式,並且沒有強行限制forceLeftGravity,則子view左側座標爲parentRight減去子view的寬度,再留出marginLeft的空白
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        //其他情況就相對簡單,parentLeft加上子view的marginLeft的值;這裏需要說明:安卓中子view的寬度和前端中div寬度是不同的,這裏border只是背景的一種效果,padding只是限制內容區域或者子view應當偏離的值,這兩者不會影響自身的width(會影響子view的大小),而margin只是設置自身在父佈局中偏離的值,同樣不會修改自身width,事實上在measure過程中,一個view的大小就已經確定了
                        childLeft = parentLeft + lp.leftMargin;
                }

                //計算childTop和計算childLeft類似;
                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                                lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                //然後通過childLeft和childTop就可以確定子view所處的座標了
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

    /**
     * 在FrameLayout中沒有draw過程,因此draw過程會調用父類ViewGroup中默認的dispatchDraw方法
     *
     * 一般而言,帶有進度條ScrollBar的view繪製流程會很複雜,view的繪製流程參照View類的public的draw方法可以看到很清楚。
     *
     * 到此爲止,FrameLayout中measure,layout,draw三個view流程全部完成。
     */

    /**
     * 還有一個重要的部分,也是每個ViewGroup子類都會有的部分——定義自己的LayoutParams
     * <p/>
     * 所有ViewGroup子類中的LayoutParams都繼承自ViewGroup類的MarginLayoutParams,因此動態添加布局時,可以使用MarginLayoutParams對象,這樣就不用在不同的ViewGroup中設置不同的LayoutParams對象了。
     * <p/>
     * Per-child layout information for layouts that support margins.
     * See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes}
     * for a list of all child view attributes that this class supports.
     *
     * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
     */
    public static class LayoutParams extends MarginLayoutParams {
        /**
         * 在這裏可以看到,默認的gravity屬性值確實是-1
         * <p/>
         * The gravity to apply with the View to which these layout parameters
         * are associated.
         *
         * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
         * @see android.view.Gravity
         */
        public int gravity = -1;

        /**
         * {@inheritDoc}
         */
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.FrameLayout_Layout);
            //在未設置gravity屬性值時,該方法返回值也是-1
            gravity = a.getInt(com.android.internal.R.styleable.FrameLayout_Layout_layout_gravity, -1);
            a.recycle();
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(int width, int height) {
            super(width, height);
        }

        /**
         * Creates a new set of layout parameters with the specified width, height
         * and weight.
         *
         * @param width   the width, either {@link #MATCH_PARENT},
         *                {@link #WRAP_CONTENT} or a fixed size in pixels
         * @param height  the height, either {@link #MATCH_PARENT},
         *                {@link #WRAP_CONTENT} or a fixed size in pixels
         * @param gravity the gravity
         * @see android.view.Gravity
         */
        public LayoutParams(int width, int height, int gravity) {
            super(width, height);
            this.gravity = gravity;
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        /**
         * {@inheritDoc}
         */
        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
        }

        /**
         * Copy constructor. Clones the width, height, margin values, and
         * gravity of the source.
         *
         * @param source The layout params to copy from.
         */
        public LayoutParams(LayoutParams source) {
            super(source);

            this.gravity = source.gravity;
        }
    }
}

由於代碼過多,因此可能有些東西會出錯,不過對於一個真實的ViewGroup的實現類而言,除了draw過程有些“水”,measure和layout過程都考慮到了很多方面,包括margin,padding,gravity,rtl等等;其實參照源碼可以更好的理解,一些關鍵的英文註釋會指明方法的功能等。

五、Broad vision

在大多時候,需要查看多個文檔才能多篇博客,才能真正理解,即便是百年難得一用的FrameLayout,在配合上自定義的事件動畫時,也可以有好的效果。類似這種Android View框架的measure機制以前感覺很神奇的問題,總有人知道爲什麼。總要多看看,當然,前提是喜歡 Android View刷新機制 。還有,發現不管學多少,都只是起點ViewGroup詳解

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