View繪製體系(一)——從setContentView聊起

View繪製體系(一)——從setContentView聊起

前言

對於Android開發者來說,View的繪製是非常基礎且重要的部分,而Activity繪製View的流程,我們都是從setContentView開始去設置我們自定義的佈局的,所以我準備從setContentView爲起點來聊下View的繪製流程。

setContentView

在Activity中,我們經常會調用到setContentView這個方法來設置對應的佈局文件,在這裏我想從源碼的角度去分析下setContentView內部是如何實現的。需要注意的是,setContentView並沒有繪製View,只是創建了View。

我們先來看下Activity類中的setContentView方法:

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
public void setContentView(View view) {
    getWindow().setContentView(view);
    initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
    getWindow().setContentView(view, params);
    initWindowDecorActionBar();
}

可以看到setContentView的三個重載其內部都是調用的getWindow().setContentView對應參數的三個重載方法,然後調用initWindowDecorActionBar()來初始化標題欄,那麼我們就看看getWindow()方法。

public Window getWindow() {
    return mWindow;
}

根據上面代碼,可以看到mWindowActivity中的一個變量,保存與Activity對應的Window對象,Window是個抽象類,所以我們要找到該對象初始化的地方,在Activity中的attach方法裏面:

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) {
        attachBaseContext(context);
    //...
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    //...
}

由此可以看到PhoneWindowWindow的實現子類,而ActivitysetContentView實質是調用了PhoneWindowsetContentView方法,那麼就來看下這個類,我們先來看下PhoneWindow.setContentView(int layoutResID)這個方法:

public void setContentView(int layoutResID) {
    // 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) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
	    //FEATURE_CONTENT_TRANSITIONS表示開啓了動畫Transition效果
	    //移除mContentParent中所有的子View
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
	    //開啓Transiton後做相應的處理,不做具體分析
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
	    //一般情況來到這裏,加載佈局
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

需要注意的變量是mContentParent這個,我們來看下這個變量是什麼:

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;

從註釋可以看出mContentParent是放置Window內容的一個容器,它是mDecor自身或者mDecor的子View,而mDecorWindow對象的頂層View,放置Window的所有裝飾元素,DecorView繼承FrameLayout,是一個容器。

繼續回到setContentView,首先先判斷mContentParent是否爲空,如果爲空的話就調用installDecor()方法去執行初始化操作,否則判斷是否開啓了Transition效果,如果開啓了就移除mContentParent的所有子View。我們先來分析下installDecor()方法的具體實現:

private void installDecor() {
	//省略了一些與無關分析代碼
    if (mDecor == null) {
        mDecor = generateDecor(-1);
    } else {
        mDecor.setWindow(this);
    }
    
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                R.id.decor_content_parent);
                
        if (decorContentParent != null) {
            mDecorContentParent = decorContentParent;
            if (mDecorContentParent.getTitle() == null) {
                mDecorContentParent.setWindowTitle(mTitle);
        } else {
            mTitleView = findViewById(R.id.title);
            if (mTitleView != null) {
                if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                    final View titleContainer = findViewById(R.id.title_container);
                    if (titleContainer != null) {
                        titleContainer.setVisibility(View.GONE);
                    } else {
                        mTitleView.setVisibility(View.GONE);
                    }
                    mContentParent.setForeground(null);
                } else {
                    mTitleView.setText(mTitle);
                }
            }
        }
        // Only inflate or create a new TransitionManager if the caller hasn't
        // already set a custom one.
        if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
            //省略跟Transition有關的代碼
        }
    }
}

installDecor方法中我們需要關注的是mDecormContentParent的初始化,先來看看generateDecor(-1)的具體實現:

protected DecorView generateDecor(int featureId) {
    //省略context的設置
    return new DecorView(context, featureId, this, getAttributes());
}

可見在installDecor()方法中調用DecorView的構造方法初始化了一個DecorView,再來看下mContentParent的初始化方法generateLayout(mDecor)

protected ViewGroup generateLayout(DecorView decor) {
    // 獲取manifest文件中Activity的theme設置
    TypedArray a = getWindowStyle();

    //通過獲取到的theme配置設置對應的flag
    mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
    int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
            & (~getForcedWindowFlags());
    if (mIsFloating) {
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        setFlags(0, flagsToUpdate);
    } else {
        setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
    }
    //...
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
        // Don't allow an action bar if there is no title.
        requestFeature(FEATURE_ACTION_BAR);
    }
    //...
    

    // 獲取代碼requestWindowFeature()中指定的Features, 並設置對應的佈局文件
    int layoutResource;
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
       //...
    } else {
        // 無任何修飾時的佈局文件
        layoutResource = R.layout.screen_simple;
	}
	
    mDecor.startChanging();
    //加載對應佈局,並添加到mDecorView中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
	//加載對應contentParent佈局
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }

    // Remaining setup -- of background and title -- that only applies
    // to top-level windows.
    if (getContainer() == null) {
        final Drawable background;
        if (mBackgroundResource != 0) {
            background = getContext().getDrawable(mBackgroundResource);
        } else {
            background = mBackgroundDrawable;
        }
        mDecor.setWindowBackground(background);

        final Drawable frame;
        if (mFrameResource != 0) {
            frame = getContext().getDrawable(mFrameResource);
        } else {
            frame = null;
        }
        mDecor.setWindowFrame(frame);

        mDecor.setElevation(mElevation);
        mDecor.setClipToOutline(mClipToOutline);

        if (mTitle != null) {
            setTitle(mTitle);
        }

        if (mTitleColor == 0) {
            mTitleColor = mTextColor;
        }
        setTitleColor(mTitleColor);
    }

    mDecor.finishChanging();

    return contentParent;
}

上面代碼省略了一些重複性設置的部分,可以分成以下三步來分析:

  • getWindowStyle():獲取在manifest文件中設置的Activitytheme屬性,並通過setFlags或者requestFeature進行相對應的設置
  • getLocalFeatures():獲取代碼中所有通過requestFeature設置的屬性,並通過Feature的不同給layoutResource設置不同的佈局文件
  • 加載mDecorcontentParentmDecor.onResourcesLoaded方法內部調用了addView方法向mDecor中添加了layoutResource對應的佈局。然後在mDecorView通過ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT)找到對應的cotentParent視圖,並對其進行相關的backgroundtitle等設置後,返回contentParent

從上述分析,我們可以得到installDecor的作用是給mDecor設置佈局文件,並獲取到其中idID_ANDROID_CONTENT,即contentView,賦值給mContentParent,我們可以看下R.layout.screen_simple的佈局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看到有id爲content的FrameLayout容器,即mContentParent,上面的ViewStub從id可以看出是ActionMode菜單視圖。這是最簡單的佈局screen_simple,其它的一些佈局與其類似,只是多了一些其他的控件。

通過上面的分析,我們可以知道在installDecor方法中調用generateDecorDecorView進行了初始化,然後調用generateLayout方法,獲取了manifest文件中的android:theme屬性和代碼中requestFeature的設置(所以requestFeature需要在setContentView之前調用),選擇對應的佈局文件,加載mDecormContentParent兩個視圖。

再回到setContentView中來:

public void setContentView(int layoutResID) {
    // 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) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
	    //FEATURE_CONTENT_TRANSITIONS表示開啓了動畫Transition效果
	    //移除mContentParent中所有的子View
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
	    //開啓Transiton後做相應的處理,不做具體分析
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
	    //一般情況來到這裏,加載佈局
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    //通知調用onApplyWindowInsets分發insets,該方法與狀態欄相關
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    //設置用戶顯示設置content view的佈局
    mContentParentExplicitlySet = true;
}

installDecor初始化後,調用mLayoutInflater.inflate(layoutResID, mContentParent);將我們寫的Activity的佈局加載到mContentParent容器中。

繼續往下看,getCallBack()返回的是Window對應的Activity,因爲在Activity的attach方法中,調用了mWindow.setCallback(this)。後續就是判斷Activity不爲空且未被銷燬時,調用其onContentChanged()方法通知Activity內容發生改變(在Activity類中該方法是個空實現)。

對於setContentView的另外兩個重載方法,如下:

public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}


public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        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 {
        mContentParent.addView(view, params);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

這兩個重載跟setContentView(int)的流程幾乎是一模一樣的,只是由於傳入的參數中有View對象,所以不需要加載,而是直接調用addView添加到mContentParent中。

總結

setContentView的總流程圖如下(省略了無關的部分):

setContentView

ps:關於setContentView的繪製流程,就分析到這裏了,後續將會介紹inflate方法是如何將我們自定義的佈局加載到mContentParent中的,敬請關注!

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