掐指一算,本来今天该介绍View的测量了,可是要说View的测量,那就要从setContentView谈起了,setContentView本身涉及到的东西也是挺多的,所以今天我们就先来看看这个setContentView到底做了什么事。上篇文章我们介绍了LayoutInflater加载一个布局文件的原理,如果小伙伴们还没看过,请移步这里View绘制详解,从LayoutInflater谈起。
现在使用Android Studio,我们的Activity都是间接继承自Activity类的,所有Activity的onCreate方法中我们都会加上一句setContentView,然后传入我们的布局的资源id,这行代码我们都知道是用来设置布局文件的,那么这个设置过程到底是什么样的?我们找到Activity的setContentView方法,如下:
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
小伙伴们注意看这个方法的注释,注释说这个方法通过一个资源id来设置activity的布局,并将这个布局添加到activity的顶层View,那要怎么添加呢?就是下面的getWindow().setContentView(layoutResID);方法了,这里我们就要引入一个新的东西了,就是Window。Window表示一个窗口,它是一个抽象类,它的具体实现是由PhoneWindow来完成的。在我们的应用中,所有的视图实际上都是附加在Window上,所以这个Window才是View的真正管理者。那我们要怎么理解Activity、Window和View之间的关系呢?我在网上看到这样一句话,感觉总结的还挺形象,和大家分享一下:
以下这段话来自:http://blog.csdn.net/u011733020/article/details/49465707
Activity就像是一扇贴着窗花的窗口,Window就像窗口上面的玻璃,而View对象就像一个个贴在玻璃上的窗花。
OK,那我们这里就来看看是怎么往玻璃上贴窗花的。
这里首先调用了window的setContentView方法,我们先来看看getWindow获取了一个什么东西:
public Window getWindow() { return mWindow; }
那这个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) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this, window); ...... ...... ...... }
小伙伴们看到,mWindow实际上就是一个PhoneWindow的实例,那我们就去这个PhoneWindow中看一下setContentView到底是干嘛了。找到PhoneWindow中的setContentView方法,如下:
@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; }小伙伴们注意第6行,如果mContentParent==null,则调用installDecor()方法,那么这个mContentParent是什么?我们找到该变量定义的地方,如下:
// 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;
这里有两行注释,它说这个window上的View实际上就是放在这个mContentParent中,这个mContentParent要么就是一个DecorView,要么就是一个DecorView的一个子类。这个DecorView我在 三个案例带你看懂LayoutInflater中inflate方法两个参数和三个参数的区别一文中已经提到过,DecorView是我们页面中的一个顶级View,ActionBar和setContentView所设置的布局就是设置在DecorView中。在我们的sdk中有这样一个文件..\platforms\android-24\data\res\layout\screen_title.xml,这个文件实际就是我们常用的一个Activity的布局文件,如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:fitsSystemWindows="true"> <!-- Popout bar for action modes --> <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:layout_width="match_parent" android:layout_height="?android:attr/windowTitleSize" style="?android:attr/windowTitleBackgroundStyle"> <TextView android:id="@android:id/title" style="?android:attr/windowTitleStyle" android:background="@null" android:fadingEdge="horizontal" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout>
小伙伴们看到,这里有两个FrameLayout,第一个是一个Text View,这个毫无疑问就是我们显示ActionBar的地方,第二个的id就是content,这个就是我们setContentVIew就是给这里设置布局。
如此看来我们往页面添加的View最终都是要添加到mContentParent中,我们继续往下看installDecor方法:
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 { 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); } } } if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) { mDecor.setBackgroundFallback(mBackgroundFallbackResource); } // Only inflate or create a new TransitionManager if the caller hasn't // already set a custom one. if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) { if (mTransitionManager == null) { final int transitionRes = getWindowStyle().getResourceId( R.styleable.Window_windowContentTransitionManager, 0); if (transitionRes != 0) { final TransitionInflater inflater = TransitionInflater.from(getContext()); mTransitionManager = inflater.inflateTransitionManager(transitionRes, mContentParent); } else { mTransitionManager = new TransitionManager(); } } mEnterTransition = getTransition(mEnterTransition, null, R.styleable.Window_windowEnterTransition); mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION, R.styleable.Window_windowReturnTransition); mExitTransition = getTransition(mExitTransition, null, R.styleable.Window_windowExitTransition); mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION, R.styleable.Window_windowReenterTransition); mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null, R.styleable.Window_windowSharedElementEnterTransition); mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition, USE_DEFAULT_TRANSITION, R.styleable.Window_windowSharedElementReturnTransition); mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null, R.styleable.Window_windowSharedElementExitTransition); mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition, USE_DEFAULT_TRANSITION, R.styleable.Window_windowSharedElementReenterTransition); if (mAllowEnterTransitionOverlap == null) { mAllowEnterTransitionOverlap = getWindowStyle().getBoolean( R.styleable.Window_windowAllowEnterTransitionOverlap, true); } if (mAllowReturnTransitionOverlap == null) { mAllowReturnTransitionOverlap = getWindowStyle().getBoolean( R.styleable.Window_windowAllowReturnTransitionOverlap, true); } if (mBackgroundFadeDurationMillis < 0) { mBackgroundFadeDurationMillis = getWindowStyle().getInteger( R.styleable.Window_windowTransitionBackgroundFadeDuration, DEFAULT_BACKGROUND_FADE_DURATION_MS); } if (mSharedElementsUseOverlay == null) { mSharedElementsUseOverlay = getWindowStyle().getBoolean( R.styleable.Window_windowSharedElementsUseOverlay, true); } } } }
这个方法略长,但是却并不难,首先如果mDecor为null,就先生成一个,然后设置相关参数,第5行代码表示只有当mDecor的子View都没有获取焦点的时候mDecor才获取焦点,这个如果小伙伴们知道ListView中如何屏蔽Button的点击事件的话,应该对这个属性会很熟悉。第6行代码表示设置这个mDecor为整个Activity的根节点。第13行,如果mContentParent为null,则根据mDecor生成一个mContentParent,这个具体生成的方法我们一会再看。第19行获取一个DecorContentParent对象,这个东西实际上是一个接口,主要用来为mDecor提供不同的title等。第23行,如果decorContentParent 不为null,则给其分别设置回调、ICON、Logo等属性;否则如果decorContentParent 不为null,则从62行开始,先判断窗口是否包含FEATURE_NO_TITLE属性,如果包含,则隐藏窗口的TextView,否则给窗口的TextVIew设置要显示的Title。接下来从98行到115行都是设置各种出入场动画。小伙伴们看到这就是所谓的installDecor方法,其实很简单,就是大量的准备工作。OK,最后我们再来瞅一眼generateLayout方法,这个方法有300多行,我这里贴出一部分,如下:
protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. TypedArray a = getWindowStyle(); if (false) { System.out.println("From style:"); String s = "Attrs:"; for (int i = 0; i < R.styleable.Window.length; i++) { s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "=" + a.getString(i); } System.out.println(s); } 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); } .... .... mDecor.startChanging(); mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); return contentParent; }
大家注意,从第16行开始,都是读取各种feature和flag设置给窗口(这些东西都是我们在setContentView之前所设置的那些东西),36行的代码通过inflater将布局资源加载成一个View树。第38行将布局中的id为content的View查找到赋值给cotentParent,并将之返回(id为content的View就是我们文章开始贴出来的源码中的第二个FrameLayout)。第36行这个加载过程也很有趣,值得一看:
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(); }
13行将布局文件加载出来,19行将加载得到的View树添加到mDecorCaptionView中。
OK,至此我们setContentView基本上分析完了。最后我们再来回顾看一下setContentView方法,我再在下面贴出:
@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; }
首先我们的所有View最终都是添加在mContenParent上的,如果mContentParent为null,则安装installDecor,第17行通过inflater的inflate方法将布局资源加载成View树并添加到mContentParent中。完成之后调用Window的onContentChanged方法,表示View已经添加完成,我们已经可以通过findViewById来查找相应的View了。这里大家有一个要注意的地方,那就是第一次运行的时候我们需要将mDecor初始化,也需要将mContentParent初始化,以后都不需要了,以后只要将mContentParent上的View移除掉并重新绘制即可。
有问题欢迎留言讨论。
以上。