Android中setContentView加載布局的原理是什么?相信很多沒有經(jīng)驗(yàn)的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。
成都創(chuàng)新互聯(lián)主營甘井子網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,app軟件定制開發(fā),甘井子h5小程序開發(fā)搭建,甘井子網(wǎng)站營銷推廣歡迎甘井子等地區(qū)企業(yè)咨詢
Activiy setContentView源碼分析
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. */ public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
在Activity中setContentView最終調(diào)用了getWindow()
的setContentView·方法,getWindow()
返回的是一個Window類,它表示一個窗口的概念,我們的Activity就是一個Window,Dialog和Toast也都是通過Window來展示的,這很好理解,它是一個抽象類,具體的實(shí)現(xiàn)是PhoneWindow,加載布局的相關(guān)邏輯都幾乎都是它處理的。
@Override 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)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 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 是否為空,當(dāng)然第一次啟動時mContentParent 時為空的,然后執(zhí)行installDecor();
方法。
mContentParent不為空是通過hasFeature(FEATURE_CONTENT_TRANSITIONS)
判斷是否有轉(zhuǎn)場動畫,當(dāng)沒有的時候就把通過mContentParent.removeAllViews();
移除mContentParent節(jié)點(diǎn)下的所有View.再通過inflate將我們的把布局填充到mContentParent,最后就是內(nèi)容變化的回調(diào)。至于mContentParent 是什么東東,先留個懸念,稍后再說。
private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } if (mContentParent == null) { mContentParent = generateLayout(mDecor); // Set up decor part of UI to ignore fitsSystemWindows if appropriate. mDecor.makeOptionalFitsSystemWindows(); final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( R.id.decor_content_parent); if (decorContentParent != null) { mDecorContentParent = decorContentParent; mDecorContentParent.setWindowCallback(getCallback()); if (mDecorContentParent.getTitle() == null) { mDecorContentParent.setWindowTitle(mTitle); } final int localFeatures = getLocalFeatures(); for (int i = 0; i < FEATURE_MAX; i++) { if ((localFeatures & (1 << i)) != 0) { mDecorContentParent.initFeature(i); } } mDecorContentParent.setUiOptions(mUiOptions); if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 || (mIconRes != 0 && !mDecorContentParent.hasIcon())) { mDecorContentParent.setIcon(mIconRes); } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 && mIconRes == 0 && !mDecorContentParent.hasIcon()) { mDecorContentParent.setIcon( getContext().getPackageManager().getDefaultActivityIcon()); mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK; } if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 || (mLogoRes != 0 && !mDecorContentParent.hasLogo())) { mDecorContentParent.setLogo(mLogoRes); } // Invalidate if the panel menu hasn't been created before this. // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu // being called in the middle of onCreate or similar. // A pending invalidation will typically be resolved before the posted message // would run normally in order to satisfy instance state restoration. PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) { invalidatePanelMenu(FEATURE_ACTION_BAR); } } else { //設(shè)置標(biāo)題 mTitleView = (TextView) 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); } } } //......初始化屬性變量 } }
在上面的方法中主要工作就是初始化mDecor和mContentParent ,以及一些屬性的初始化
protected DecorView generateDecor(int featureId) { // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity. Context context; if (mUseDecorContext) { Context applicationContext = getContext().getApplicationContext(); if (applicationContext == null) { context = getContext(); } else { context = new DecorContext(applicationContext, getContext().getResources()); if (mTheme != -1) { context.setTheme(mTheme); } } } else { context = getContext(); } return new DecorView(context, featureId, this, getAttributes()); }
generateDecor初始化一個DecorView對象,DecorView繼承了FrameLayout,是我們要顯示布局的頂級View,我們看到的布局,標(biāo)題欄都是它里面。
然后將mDecor作為參數(shù)調(diào)用generateLayout初始化mContetParent
protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. //獲取主題樣式 TypedArray a = getWindowStyle(); //......省略樣式的設(shè)置 // Inflate the window decor. int layoutResource; //獲取feature并根據(jù)其來加載對應(yīng)的xml布局文件 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) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleIconsDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_title_icons; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); // System.out.println("Title Icons!"); } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) { // Special case for a window with only a progress bar (and title). // XXX Need to have a no-title version of embedded windows. layoutResource = R.layout.screen_progress; // System.out.println("Progress!"); } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { // Special case for a window with a custom title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogCustomTitleDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_custom_title; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { // If no other features and not embedded, only need a title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleDecorLayout, res, true); layoutResource = res.resourceId; } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { layoutResource = a.getResourceId( R.styleable.Window_windowActionBarFullscreenDecorLayout, R.layout.screen_action_bar); } else { layoutResource = R.layout.screen_title; } // System.out.println("Title!"); } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { layoutResource = R.layout.screen_simple_overlay_action_mode; } else { // Embedded, so no decoration is needed. layoutResource = R.layout.screen_simple; // System.out.println("Simple!"); } mDecor.startChanging(); mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { ProgressBar progress = getCircularProgressBar(false); if (progress != null) { progress.setIndeterminate(true); } } if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { registerSwipeCallbacks(); } // 給頂層窗口設(shè)置標(biāo)題和背景 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獲取主題樣式進(jìn)行初始化,然后通過getLocalFeatures獲取設(shè)置的不同features加載不同的布局,例如我們通常在Activity 加入requestWindowFeature(Window.FEATURE_NO_TITLE);
來隱藏標(biāo)題欄,不管根據(jù)Feature最終使用的是哪一種布局,里面都有一個android:id="@android:id/content"
的FrameLayout,我們的布局文件就添加到這個FrameLayout中了。我們看一下一個簡單的布局
通過上面的分析,你應(yīng)該明白了requestWindowFeature為什么必須在setContentView之前設(shè)置了,如果在之后設(shè)置,那么通過上面的分析在setContentView執(zhí)行時已經(jīng)從本地讀取features,而此時還沒有設(shè)置,當(dāng)然就無效了。
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
通過上面findViewById獲取該對象。不過在獲取ViewGroup之前還有一個重要的方法
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) { mStackId = getStackId(); if (mBackdropFrameRenderer != null) { loadBackgroundDrawablesIfNeeded(); mBackdropFrameRenderer.onResourcesLoaded( this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable, mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState), getCurrentColor(mNavigationColorViewState)); } mDecorCaptionView = createDecorCaptionView(inflater); final View root = inflater.inflate(layoutResource, null); if (mDecorCaptionView != null) { if (mDecorCaptionView.getParent() == null) { addView(mDecorCaptionView, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mDecorCaptionView.addView(root, new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); } else { // Put it below the color views. addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } mContentRoot = (ViewGroup) root; initializeElevation(); }
這個比較好理解,root就是在上面判斷的根據(jù)不同的features,加載的布局,然后將該布局通過addView添加到DecorView.到這里初始都成功了.
mLayoutInflater.inflate(layoutResID, mContentParent);
在回到最初setContentView中的一句代碼,如上,我們也就好理解了,它就是將我們的布局文件inflate到mContentParent中。到這里Activity的加載布局文件就完畢了。
AppCompatActivity的setContentView分析
由于AppCompatActivity的setContentView加載布局的與Activity有很多不同的地方,而且相對Activity稍微復(fù)雜點(diǎn),在這里也簡單分析一下。
@Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); }
通過名字也就知道把加載布局交給了一個委托對象。
@NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; }
AppCompatDelegate時一個抽象類,如下圖他有幾個子類實(shí)現(xiàn)
為啥有那么多子類呢,其實(shí)通過名字我們也能猜到,是為了兼容。為了證明這點(diǎn),我們看看create方法
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) { final int sdk = Build.VERSION.SDK_INT; if (BuildCompat.isAtLeastN()) { return new AppCompatDelegateImplN(context, window, callback); } else if (sdk >= 23) { return new AppCompatDelegateImplV23(context, window, callback); } else if (sdk >= 14) { return new AppCompatDelegateImplV14(context, window, callback); } else if (sdk >= 11) { return new AppCompatDelegateImplV11(context, window, callback); } else { return new AppCompatDelegateImplV9(context, window, callback); } }
這里就很明顯了,根據(jù)不同的API版本初始化不同的delegate。通過查看代碼setContentView方法的實(shí)現(xiàn)是在AppCompatDelegateImplV9中
@Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mOriginalWindowCallback.onContentChanged(); }
有了分析Activity的加載經(jīng)驗(yàn),我們就很容易明白contentParent和Activity中的mContentParent是一個東東,ensureSubDecor就是初始mSubDecor,然后removeAllViews,再將我們的布局填充到contentParent中。最后執(zhí)行回調(diào)。
private void ensureSubDecor() { if (!mSubDecorInstalled) { mSubDecor = createSubDecor(); //省略部分代碼 onSubDecorInstalled(mSubDecor); } } private ViewGroup createSubDecor() { TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); //如果哦們不設(shè)置置AppCompat主題會報(bào)錯,就是在這個地方 if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) { a.recycle(); throw new IllegalStateException( "You need to use a Theme.AppCompat theme (or descendant) with this activity."); } //省略..... 初始化一下屬性 ViewGroup subDecor = null; //PhtoWindowgetDecorView會調(diào)用installDecor,在Activity已經(jīng)介紹過,主要工作就是初始化mDecor,mContentParent。 mWindow.getDecorView(); //省略 //根據(jù)設(shè)置加載不同的布局 if (!mWindowNoTitle) { if (mIsFloating) { // If we're floating, inflate the dialog title decor subDecor = (ViewGroup) inflater.inflate( R.layout.abc_dialog_title_material, null); // Floating windows can never have an action bar, reset the flags mHasActionBar = mOverlayActionBar = false; } else if (mHasActionBar) { /** * This needs some explanation. As we can not use the android:theme attribute * pre-L, we emulate it by manually creating a LayoutInflater using a * ContextThemeWrapper pointing to actionBarTheme. */ TypedValue outValue = new TypedValue(); mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true); Context themedContext; if (outValue.resourceId != 0) { themedContext = new ContextThemeWrapper(mContext, outValue.resourceId); } else { themedContext = mContext; } // Now inflate the view using the themed context and set it as the content view subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); mDecorContentParent = (DecorContentParent) subDecor .findViewById(R.id.decor_content_parent); mDecorContentParent.setWindowCallback(getWindowCallback()); /** * Propagate features to DecorContentParent */ if (mOverlayActionBar) { mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY); } if (mFeatureProgress) { mDecorContentParent.initFeature(Window.FEATURE_PROGRESS); } if (mFeatureIndeterminateProgress) { mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS); } } } else { if (mOverlayActionMode) { subDecor = (ViewGroup) inflater.inflate( R.layout.abc_screen_simple_overlay_action_mode, null); } else { subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null); } if (Build.VERSION.SDK_INT >= 21) { // If we're running on L or above, we can rely on ViewCompat's // setOnApplyWindowInsetsListener ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { final int top = insets.getSystemWindowInsetTop(); final int newTop = updateStatusGuard(top); if (top != newTop) { insets = insets.replaceSystemWindowInsets( insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); } // Now apply the insets on our view return ViewCompat.onApplyWindowInsets(v, insets); } }); } else { // Else, we need to use our own FitWindowsViewGroup handling ((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener( new FitWindowsViewGroup.OnFitSystemWindowsListener() { @Override public void onFitSystemWindows(Rect insets) { insets.top = updateStatusGuard(insets.top); } }); } } if (subDecor == null) { throw new IllegalArgumentException( "AppCompat does not support the current theme features: { " + "windowActionBar: " + mHasActionBar + ", windowActionBarOverlay: "+ mOverlayActionBar + ", android:windowIsFloating: " + mIsFloating + ", windowActionModeOverlay: " + mOverlayActionMode + ", windowNoTitle: " + mWindowNoTitle + " }"); } if (mDecorContentParent == null) { mTitleView = (TextView) subDecor.findViewById(R.id.title); } // Make the decor optionally fit system windows, like the window's decor ViewUtils.makeOptionalFitsSystemWindows(subDecor); //contentView 是我們布局填充的地方 final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); //這個就是和我們Activity中的介紹的mDecor層級中的mContentParent是一個東西, final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. //清除windowContentView的id windowContentView.setId(View.NO_ID); //將contentView的id設(shè)置成android.R.id.content,在此我們應(yīng)該明白了,contentView 就成為了Activity中的mContentParent,我們的布局加載到這個view中。 contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } } // Now set the Window's content view with the decor //將subDecor 填充到DecorView中 mWindow.setContentView(subDecor); //省略部分代碼 return subDecor; }
上面的處理邏輯就是先初始化一些主題樣式,然后通過mWindow.getDecorView()
初始化DecorView.和布局,然后createSubDecor根據(jù)主題加載不同的布局subDecor,通過findViewById獲取contentView( AppCompat根據(jù)不同主題加載的布局中的View R.id.action_bar_activity_content)
和windowContentView (
DecorView中的View android.R.id.content
)控件。獲取控件后將windowContentView 的id清空,并將 contentView的id由R.id.action_bar_activity_content更改為android.R.id.content。最后通過 mWindow.setContentView(subDecor);
將subDecor添加到DecorView中。
//調(diào)用兩個參數(shù)方法 @Override public void setContentView(View view) { setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } //此處處理和在Activity中分析的setContentView傳資源ID進(jìn)行加載布局是一樣的,不同的是此時mContentParent 不為空,先removeAllViews(無轉(zhuǎn)場動畫情況)后再直接執(zhí)行mContentParent.addView(view, params);即將subDecor添加到mContentParent @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) { 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; }
關(guān)于subDecor到底是什么布局,我們隨便看一個布局R.layout.abc_screen_toolbar
,有標(biāo)題(mWindowNoTitle為false)并且有ActionBar(mHasActionBar 為true)的情況加載的布局。
不管哪個主題下的布局,都會有一個id 為 abc_screen_content_include最好將id更改為androd.R,content,然后添加到mDecor中的mContentParent中。我們可以同SDK中tools下hierarchyviewer工具查看我們的布局層級結(jié)構(gòu)。例如我們AppCompatActivity中setContentView傳入的布局文件,是一個線程布局,該布局下有一個Button,則查看到層級結(jié)構(gòu)
看完上述內(nèi)容,你們掌握Android中setContentView加載布局的原理是什么的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!