原创-转载请注明出处。
当我们给Activity设置布局时,都是直接调用setContentView来完成的,但具体Android是怎么把布局加载到window,又是怎么通过findViewById获取view对象的,我们可能并没有太关心,下面就结合源码来分析下这个过程。
Android setContentView
打开Activity的源码发现,setContentView有三个重载方法,
public void setContentView(int layoutResID);
public void setContentView(View view);
public void setContentView(View view, ViewGroup.LayoutParams params)
我们就来看下最常用的第一个方法:
public void setContentView(int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
这个方法调用了,Window类中的setContentView()方法,其他方法也是调用了Window类中的setContentView(),但是Window是一个抽象类,在Activity的attach方法中被初始化,其实是一个PhoneWindow实例,所以这个setContentView方法在PhoneWindow中实现。
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); } final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }
首先判断mContentParent是否为空,如果为空的话则调用installDecor()方法,其次判断是否设置了FEATURE_CONTENT_TRANSITIONS属性,如果没有的话则移除所有view(从这里我们可以得出setContentView可以调用多次,反正会removeAllViews),然后调用LayoutInflater.inflate(),将我们设置的布局文件添加到mContentParent中。接着获取了一个Callback对象,那这个是在Activity的attach方法中设置的一个回调
mWindow.setCallback(this);
所以可以得出在Activity中一定有一个onContentChanged回调,我们来看下这个回调
public void onContentChanged() {}
额,空空如也。但是我们可以在自己的Activity中重写这个回调,用于在setContentView之后做一些事情,比如findViewById,但貌似实际场景也不需要。。。
好了,现在我们回到上面提到的installDecor()方法,好长,我们捡重要的看吧。
private void installDecor() { //初始化decorView if (mDecor == null) { mDecor = generateDecor(); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } //初始化mContentParent if (mContentParent == null) { mContentParent = generateLayout(mDecor); ...... //设置一堆属性值 } }
看下PhoneWindow中的generateDecor()方法
protected DecorView generateDecor() { return new DecorView(getContext(), -1); }
只是单纯的new了一个DecorView实例。这个DecorView是什么鬼。其实它是PhoneWindow的一个内部类,是整个window界面最顶层的view。包含ActionBar,内容块等。好了,现在我们缕一下Window,PhoneWindow,decorView的关系
1.Window类是一个抽象类,提供了绘制窗口的一组通用API。可以将之理解为一个载体,各种View在这个载体上显示。
2.PhoneWindow是Window的一个子类,是Window的具体实现,包含一个内部类DecorView,PhoneWindow是将decorView进行了一定包装,并提供一些方法用于操作窗口。
3。DecorView继承自FrameLayout,是窗口的根view。
好了,接着看mContentParent的初始化,generateLayout(mDecor).这里传入了上一部初始化好的DecorView. 又是一个长方法,我们还是挑出重要的部分。
protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. TypedArray a = getWindowStyle(); //...... //根据定义的style设置一些值,比如是否显示ActionBar, // Inflate the window decor. int layoutResource; int features = getLocalFeatures(); //...... //根据设定好的features值选择不同的窗口修饰布局文件, //得到layoutResource值,系统定义了不同的layout,比如 //R.layout.screen_custom_title,R.layout.screen_simple //把选中的窗口修饰布局文件添加到DecorView对象里,并且指定contentParent值 View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); mContentRoot = (ViewGroup) in; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } //...... //继续一堆属性设置,返回contentParent return contentParent; }
根据不同的features值,设定layoutResource,最终添加到decorView中,所以我们通过在xml中设置的theme,还有在代码中设置的requestWindowFeature,都是用来设置features值,这也是为什么requestWindowFeature方法必须在setContentView之前的原因。
这样看来,如果我们设置我们的Theme为NoTitleBar,最终layoutResource的值为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>
来看下去处标题栏后的视图树
setContentView
所以installDecor主要是初始化了PhoneWindow中的DecorView.和contentParent,之后在setContentView()中通过mLayoutInflater.inflate(layoutResID, mContentParent);将layoutResId,add到初始化好的contentParent中。
大家是否好奇状态栏怎么被加载进DecorView的,我们来看下DecorView中的updateColorViewInt方法
private View updateColorViewInt(View view, int sysUiVis, int systemUiHideFlag, int translucentFlag, int color, int height, int verticalGravity, String transitionName, int id, boolean hiddenByWindowFlag) { ...... if (view == null) { if (show) { view = new View(mContext); view.setBackgroundColor(color); view.setTransitionName(transitionName); view.setId(id); addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, height, Gravity.START | verticalGravity)); } } else { ...... } return view; }
可以看到直接new了一个view,这个view就是状态栏,然后将状态栏添加到了DecorView,其实这个状态栏只是一个单纯的占位view。被updateColorViews方法调用,比如当我们调用setStatusBarColor时就是调用了updateColorViews这个方法。这里先不做过多介绍。
findViewById
那么将layout添加进decorView中后,我们是怎么通过findViewById找到View的呢?
看下Activity的findViewById方法
/** * Finds a view that was identified by the id attribute from the XML that * was processed in {@link #onCreate}. * * @return The view if found or null otherwise. */ public View findViewById(int id) { return getWindow().findViewById(id); }
又是到了window中,看下window中的方法
public View findViewById(int id) { return getDecorView().findViewById(id); }
是调用了getDecorView的findViewById,也就是调用了view的findViewById,我们来看下view类中
public final View findViewById(int id) { if (id < 0) { return null; } return findViewTraversal(id); } protected View findViewTraversal(int id) { if (id == mID) { return this; } return null; }
到这我们就疑惑了,直接判断了id是否为view的id,是的话就返回。怎么也应该有一个循环或者递归查找啊,什么都没有。
这时我们来看下,mID是怎么初始化的
.... case com.android.internal.R.styleable.View_id: mID = a.getResourceId(attr, NO_ID); break; ...
喔,这个id就是我们在xml中设置的id。那会不会在ViewGroup中进行查找的呢?来看下
protected View findViewTraversal(int id) { if (id == mID) { return this; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) { v = v.findViewById(id); if (v != null) { return v; } } } return null; }
果然, ViewGroup重写了View的findViewTraversal()方法,遍历了自己的child的findViewById方法,如果找到了返回View自身。
ok,到现在我们就理解了view是怎么findViewById的了。
作者:程序猿Jeffrey
链接:https://www.jianshu.com/p/82e6070924ab