學(xué)習(xí)筆記—Android 控件架構(gòu)與自定義控件

《Android 群英傳》第三章 “ Android 控件架構(gòu)與自定義控件詳解 ” 學(xué)習(xí)筆記

Android控件架構(gòu)

在 Android 中,控件被分為兩類,即 ViewGroup 與 View。在界面上,控件其實(shí)是一個(gè)矩形。ViewGroup 可作為父控件包含一個(gè)或多個(gè)View。通過(guò)ViewGroup,整個(gè)界面上的控件形成了一個(gè)樹形結(jié)構(gòu)(控件樹)。上層控件負(fù)責(zé)下層子控件的測(cè)量與繪制,并傳遞交互事件。在控件樹頂部,有一個(gè) ViewParent 對(duì)象,它負(fù)責(zé)統(tǒng)一調(diào)度和分配所有的交互管理事件,對(duì)整個(gè)視圖進(jìn)行整體控制。

View 樹結(jié)構(gòu)
Android UI 界面架構(gòu)圖

關(guān)于 setContentView()

每個(gè) Activity 都包含一個(gè) Window 對(duì)象,在 Android中Window 對(duì)象通常由 PhoneWindow 來(lái)實(shí)現(xiàn)。 PhoneWindow 將一個(gè) DecorView 設(shè)置為整個(gè)應(yīng)用的根 View。DecorView 作為窗口的頂層視圖,封裝了一些窗口操作的通用方法。DecorView 將要顯示的內(nèi)容呈現(xiàn)在 PhoneWindow 上,這里面的所有 View 的監(jiān)聽(tīng)事件,都通過(guò) WindowManagerService 來(lái)進(jìn)行接收。
DecorView 分為兩部分,TitleView 與 ContentView。ContentView 實(shí)際是一個(gè) ID 為content 的 FrameLayout,我們通過(guò) setContentView() 方法設(shè)置的布局就會(huì)顯示在這個(gè) FrameLayout 中。
當(dāng)onCreate() 方法中調(diào)用 setContentView() 方法后,ActivityManagerService 會(huì)回調(diào) onResume() 方法,此時(shí)系統(tǒng)才會(huì)把整個(gè) DecorView 添加到 PhoneWindow 中,并顯示出來(lái),從而完成界面繪制。

// 來(lái)自源碼 
// 得到 Window 對(duì)象,設(shè)置布局(疑問(wèn):得到的Window對(duì)象是PhoneWindow對(duì)象嗎?)
// 初始化ActionBar
public void setContentView(@LayoutRes int layoutResID) {    
    getWindow().setContentView(layoutResID);   
    initWindowDecorActionBar();
}

private void initWindowDecorActionBar() {
        Window window = getWindow();

        // Initializing the window decor can change window feature flags.
        // Make sure that we have the correct set before performing the test below.
        window.getDecorView();

        // 判斷是否顯示ActionBar 
        if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
            return;
        }

        mActionBar = new WindowDecorActionBar(this);
        mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);

        mWindow.setDefaultIcon(mActivityInfo.getIconResource());
        mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
    }```

## View 的測(cè)量
繪制 View 需要知道它的大小和位置。這個(gè)過(guò)程在 onMeasure() 方法中進(jìn)行。同時(shí) MeasureSpec 類是幫助我們測(cè)量 View 的。MeasureSpec 中有三個(gè)int型常量。
    /**
     * Measure specification mode: The parent has not imposed any constraint
     * on the child. It can be whatever size it wants.
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    /**
     * Measure specification mode: The parent has determined an exact size
     * for the child. The child is going to be given those bounds regardless
     * of how big it wants to be.
     */
    public static final int EXACTLY     = 1 << MODE_SHIFT;

    /**
     * Measure specification mode: The child can be as large as it wants up
     * to the specified size.
     */
    public static final int AT_MOST     = 2 << MODE_SHIFT;
看英文注釋,不是很明白。這里引用下書中的解釋:
* EXACTLY 
精確值模式,當(dāng)我們將控件的layout_width 屬性或 layout_height 屬性指定為具體數(shù)值時(shí),比如設(shè)置寬為100dp,或者指定為match_parent 屬性時(shí),測(cè)量模式即為 EXACTLY 模式
*  AT_MOST
最大值模式,當(dāng)控件的layout_width 屬性或 layout_height 屬性指定為 wrap_content 時(shí),這時(shí)候的控件的大小一般隨它的子控件或內(nèi)容的變化而變化。
* UNSPECIFIED
 未指定模式(書中沒(méi)有中文解釋),不指定測(cè)量模式,View 想多大就多大。

View 類默認(rèn)的 onMeasure() 方法只支持 EXACTLY 模式。自定義的 View 需要重寫 onMeasure() 才能使用wrap_content 屬性。

// View 默認(rèn)的 onMeasure 方法
// 得到寬高后調(diào)用setMeasuredDimension
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

    // 得到寬高的測(cè)量模式和大小(來(lái)自 TextView 的 onMeasure() 方法)
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    // ...
自定義一個(gè)View,重寫onMeasure() 方法,測(cè)量寬高

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    setMeasuredDimension(getWidthSize(widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// 通過(guò)測(cè)量模式返回寬度大小(疑問(wèn):為何 onMeasure() 方法會(huì)被多次執(zhí)行???)
private int getWidthSize(int widthMeasureSpec) {
    int mode = MeasureSpec.getMode(widthMeasureSpec);
    int size = MeasureSpec.getSize(widthMeasureSpec);

    int width;

    if (mode == MeasureSpec.EXACTLY) {
        width = size;
    } else {
        width = 200;
        if (mode == MeasureSpec.AT_MOST) {
            width = Math.min(width, size);
        }
    }
    Log.i("size", size + "");
    Log.i("width", width + "");

    return width;
}

## View 的繪制
當(dāng)測(cè)量好 View 后,可以重寫 onDraw() 方法,在 Canvas 對(duì)象上繪制圖形。Canvas 就像一張畫布,使用 Paint 就可以在上面畫東西了。

## ViewGroup 的測(cè)量
當(dāng) ViewGroup 的大小為 wrap_content 時(shí),ViewGroup 就需要對(duì)子 View 進(jìn)行遍歷,以便獲取所有子 View 的大小,從而決定自身的大小。其他模式則通過(guò)具體的指定值來(lái)設(shè)置大小。
ViewGroup 通過(guò)遍歷子 View,從而調(diào)用子 View 的 onMeasure() 來(lái)獲取每一個(gè)子 View的測(cè)量結(jié)果。
當(dāng)子 View 測(cè)量完畢后,還需要放置 View 在界面的位置,這個(gè)過(guò)程是 View 的 Layout過(guò)程。ViewGroup 在執(zhí)行 Layout 過(guò)程中,同樣使用遍歷調(diào)用子 View 的Layout 方法,確定它的顯示位置。從而決定布局位置。
自定義 ViewGroup 是,一般要重寫 onLayout() 方法來(lái)控制子 View 顯示位置的邏輯。支持 wrap_content 屬性,還需要重寫 onMeasure 來(lái)決定自身的大小。

閱讀 LinearLayout 的部分源碼,理解測(cè)量過(guò)程

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 判斷布局方向,調(diào)用不同的測(cè)量方法
if (mOrientation == VERTICAL) {

        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}    

在 measureVertical(widthMeasureSpec, heightMeasureSpec) 發(fā)現(xiàn)如下代碼
    final int count = getVirtualChildCount();
    
     //         .... 省略N行

    // See how tall everyone is. Also remember max width.
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);

        if (child == null) {
            mTotalLength += measureNullChild(i);
            continue;
        }

        if (child.getVisibility() == View.GONE) {
           i += getChildrenSkipCount(child, i);
           continue;
        }

        if (hasDividerBeforeChildAt(i)) {
            mTotalLength += mDividerHeight;
        }

        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

        totalWeight += lp.weight;

// .... 省略N行

對(duì) Child View 進(jìn)行遍歷,去測(cè)量 weight 等。接著

measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);


通過(guò)方法名我們也能理解這是在 Layout 之前測(cè)量 子 View。接著跟下去

protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

發(fā)現(xiàn)這里正是在獲取子 View 寬高的 MeasureSpec,然后回調(diào)子 View 的measure() 方法,直接跳轉(zhuǎn)去看這個(gè)方法在干什么。(猜測(cè)應(yīng)該會(huì)去調(diào)用 onMeasure 方法了)

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
// 這里的確發(fā)生了 調(diào)用 子 View 的 onMeasure() 去測(cè)量大小
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;
}```

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容