瞭解view的繪製流程和基礎概念

本文大綱:

  1. android中view的加載繪製流程
  2. 自定義view中的基礎概念
  3. 父View和子View的聯繫

1.android中view的加載繪製流程

 

1.1 andriod視圖結構:

說明:

    上圖給出了android一個Activity中的視圖結構,從外向內依次是 phoneWindow-->DecorView-->ActionBar+ContentLayout. 其中ContentLayout是一個FrameLayout,它的id是content, 我們自己設置的佈局就是contentLayout的子View, 下面把它叫做contentView, 調用setContentView() 其實就是將自己的佈局添加到contentLayout中去.

1.2 視圖加載流程:

 

繪製流程從源碼說起,入口就是 進入Activity之後的setContentView() (源碼爲android API28)

 

*  如果我們繼承AppCompatActivty(現在很多都是這樣):

入口:

AppCompatActivity#setContentView

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

AppCompatActivity#getDelegate

@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

 AppCompatDelegate.create

public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}

AppCompatDelegateImpl#setContentView

@Override
public void setContentView(View v) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    contentParent.addView(v);
    mOriginalWindowCallback.onContentChanged();
}

 

順藤摸瓜,找到了線索,contentParent就是上面說到的contentLayout, 我們自己的佈局就是v, 其中的邏輯清晰可見。'

 

*  如果我們直接繼承Activity:

入口

Activity#setContentView

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

Activity#getWindow

public Window getWindow() {
    return mWindow;
}

 

Activity#attach 中找到了mWindow的賦值

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
.....
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
......
}

PhoneWindow#setContentView

@Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            // ①創建 decorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            // ②添加我們自己的view
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

我們也找到了相似的邏輯,①處 如果decorView還沒有被創建,則創建一個,其中包括decorView的創建和其中的toolbar, contentLayout的創建邏輯。 ②處mContentParent就是之前說到的contentLayout, 它在installDecor() 中會調用 'mContentParent = generateLayout(mDecor);’ 被創建,接下來就是將我們自己的view添加到mContentParent中去。

 

2. 自定義view中的基礎概念

2.1.自定義View的基本方法:

每個View都要依次經歷方法:

  • 測量:onMeasure()決定View的大小;
  • 佈局:onLayout()決定View在ViewGroup中的位置;
  • 繪製:onDraw()決定繪製這個View。

2.2.自定義View的分類:

  • 單一視圖,無子view:View

此時一般需要重寫onMeasure() 和 onDraw()

  • 視圖組,包含子view:ViewGroup

此時一般需要重寫onMeasure() 和 onLayout()

 

注意繪製的順序,都是從父佈局自上而下繪製的,只有葉子節點纔會只繪製自身,父節點不止繪製自身,還要繪製子view

2.3.Android座標系

對於整個屏幕而言:

  • 屏幕左上角爲座標原點
  • 向右爲X增大方向

對於View的座標位置,一般是相對於父佈局而言的:

  • 父容器的左上角爲view的座標原點
  • 向右爲X增大方向

//View中獲取位置的方法

getTop(); //獲取子View左上角距父View頂部的距離
getLeft(); //獲取子View左上角距父View左側的距離
getBottom(); //獲取子View右下角距父View頂部的距離
getRight(); //獲取子View右下角距父View左側的距離

與MotionEvent中 get()和getRaw()的區別

//get() :觸摸點相對於其所在組件座標系的座標
event.getX();
event.getY();
//getRaw() :觸摸點相對於屏幕默認座標系的座標
event.getRawX();
event.getRawY();

 

2.4.寬高的獲取

getWidth && getHeight 在onLayout() 之後才能獲取值

public final int getHeight() {
    return mBottom - mTop;
}
public final int getWidth() {
    return mRight - mLeft;
}

getMeasuredHeight && getMeasuredWidth 在onMeasure之後可以獲取值

public final int getMeasuredHeight() {
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}
public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

 

2.5.View的測量規格 MeasureSpec

MeasureSpec定義:

    首先MeasureSpec是一個int值,包含測量模式mode(佔2位)和 測量大小size(佔30位)兩種信息。

mode有三種:

  • UNSECIFIED 父控件不對你有任何限制,你想要多大給你多大,想上天就上天。這種情況一般用於系統內部,表示一種測量狀態。(這個模式主要用於系統內部多次Measure的情形,並不是真的說你想要多大最後就真有多大)
  • EXACTLY 父控件已經知道你所需的精確大小,你的最終大小應該就是這麼大。
  • AT_MOST 你的大小不能大於父控件給你指定的size,但具體是多少,得看你自己的實現

MeasureSpec的意義

    可以看到在重寫 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 會帶有MeasureSpec類型的參數,這個參數就是從父佈局傳進來的。

    MeasureSpec是父佈局用來對子view的控制的參數,子view通過父佈局傳進來的MeasureSpec參數結合自己的內容大小最終來確定自己的大小。後面會詳細解釋。

 

3. 父View和子View的聯繫

要真正理解自定義view, 必須要知道父佈局和子view是怎麼產生聯繫的。這樣才能知道整顆View樹是怎樣繪製的。

 

3.1 先從onMeasure說起(父佈局和子佈局的代碼只代表邏輯):

父佈局#onMeasure:

public void onMeasure(int parentWidthMeasureSpec, int parentHeightMeasureSpec){
        // 1. 測量子佈局(可能有多個,這裏假設只有一個子佈局)
        // 1.1 計算子佈局的測量規格   ①
        int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight, lp.width);
        int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom, lp.height);
        // 1.2 測量子佈局
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        // 2. 測量自己
        // 2.1 解析父佈局的測量規格
        int widthSpecMode = MeasureSpec.getMode(parentWidthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(parentWidthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(parentHeightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(parentHeightMeasureSpec);
        // 2.2 根據父佈局的測量規格結合自己的內容設置最終大小
        int resultWidth;
        int resultHeight;
        int contentWidth=child.getMeasuredWidth();  // 假設這是我內容的寬
        int contentHeight=child.getMeasureHeight(); // 假設這是我內容的高
        switch(widthSpecMode){
            case MeasureSpec.EXACTLY:
                resultWidth=widthSpecSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                resultWidth=Math.min(widthSpecSize,contentWidth);
                break;
        }
        switch(heightSpecMode){
            case MeasureSpec.EXACTLY:
                resultHeight=heightSpecSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                resultHeight=Math.min(heightSpecSize,contentHeight);
                break;
        }
        // 3 最後別忘了設置 自己的大小 只有設置了此方法 長和寬纔有效
        setMeasuredDimension(resultWidth,resultHeight);
}

子佈局#onMeasure(假設它沒有子佈局):

public void onMeasure(int parentWidthMeasureSpec, int parentHeightMeasureSpec){
        // 2. 測量自己
        // 2.1 解析父佈局的測量規格  ②
        int widthSpecMode = MeasureSpec.getMode(parentWidthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(parentWidthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(parentHeightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(parentHeightMeasureSpec);
        // 2.2 根據父佈局的測量規格結合自己的內容設置最終大小
        int resultWidth;
        int resultHeight;
        int contentWidth=child.getMeasuredWidth();  // 假設這是我內容的寬
        int contentHeight=child.getMeasureHeight(); // 假設這是我內容的高
        switch(widthSpecMode){
            case MeasureSpec.EXACTLY:
                resultWidth=widthSpecSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                resultWidth=Math.min(widthSpecSize,contentWidth);
                break;
        }
        switch(heightSpecMode){
            case MeasureSpec.EXACTLY:
                resultHeight=heightSpecSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                resultHeight=Math.min(heightSpecSize,contentHeight);
                break;
        }
        // 3 最後別忘了設置 自己的大小 只有設置了此方法 長和寬纔有效
        setMeasuredDimension(resultWidth,resultHeight);
}

 

分析一下父佈局和子view的聯繫:

①處父佈局計算的MeasureSpec, 並將它作用到②處子佈局中,子佈局根據他來決定自身大小。

 

①處計算MeasureSpec的邏輯:

ViewGroup#getChildMeasureSpec

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
  • 父佈局在計算MeasureSpec的時候已經考慮到了子view的LayoutParam屬性,包括 具體的值,match_parent,wrap_content,所以子view在拿到父佈局傳遞的MeasureSpec的時候不用在考慮LayoutParams屬性,只需要考慮自身內容的大小。
  • 最終計算的mode對應關係:

總結:

    • 子控件如果設置了確定值,如 width="20dp" ,則它的大小就固定爲20dp, 模式爲EXACTLY
    • 子控件如果設置match_parent, 那麼他的模式跟隨父親給的模式。當父親給的模式爲AT_MOST時,它的最終大小不能超過父親給的大小;當父親給的模式是EXACTLY時,它的最終大小是父親給的大小。
    • 子控件如果設置了wrap_content,那麼它的模式爲AT_MOST,它的最終大小不能超過父親給的大小。

 

3.2 再說onLayout()

onLayout就簡單了。

onLayout作用是用來擺放子view的,如果沒有子view,那麼一般不需要重寫。所以這個一般是父佈局自己做的事情,子view無需關心

父佈局#onLayout:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // l,t,r,b 是自己距離自己的父佈局的上下左右的距離,如果只考慮內部的佈局,可以不使用
    
    // 1.確定子view的座標
    int top;
    int left;
    int right;
    int bottom;
    // 計算上下左右的座標(相對於自己的內部)
   //... 
   //2. 擺放子佈局
   child.layout(left, top, right, bottom);
}

 

總結:

  • onLayout關鍵就是確定子view上下左右的座標

3.3 onDraw()

onDraw() 是用來繪製自身的,一般父佈局不需要繪製自身,所以這個一般是子view(沒有父佈局)的事情。

子view#onDraw()

protected void onDraw(Canvas canvas) {
    // 座標系相對於view自身
    // 使用畫筆 畫布 形狀 
}

總結:

  • onDraw()方法可以玩出很多樣式,直接繼承View一般都要重寫onDraw(),這方面需要學習畫筆畫布方面的知識。

注意:

作爲父佈局ViewGroup默認是不會調用onDraw()方法的,除非兩種情況:

    1. 父佈局設置了背景顏色
    2. 父佈局設置了setWillNotDraw(false)

 

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