setContentView源码解析

来源:互联网 发布:c语言一维数组最大长度 编辑:程序博客网 时间:2024/05/17 17:45

1.两个疑问

  • requestWindowFeature方法为什么要在setContentView方法之前调用
  • 为什么在我们的根布局前面前面还会有FrameLayout,而这个我们并没有写在布局里面(不知怎么引用本地图片,借网上的一用)

2.Android6.0(API 23) Activity的setContentView方法

2-0 从getWindow说起

在我们自己的MainActivity点进去后到Activity类,发现有三个重载的方法

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();                                       }                                                                     

因为我们在项目中最常用到的是第一个,也就是传一个布局文件进去的这个重载方法,所以接下来就重点分析这个方法

在Activity类里面是又去调用了getWindow().setContentView(layoutResID),这个getWindow是什么东西呢,我们点进去发现是返回一个mWindow,我们再找到mWindow赋值的地方

public Window getWindow() {     return 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) {    attachBaseContext(context);                mFragments.attachHost(null /*parent*/);    // 这里初始化                                       mWindow = new PhoneWindow(this);       mWindow.setCallback(this);    .....    .....}

可以看到是在attach方法里面对mWindow进行实例化,也就是说我们的这个getWindow实际上也就是个PhoneWindow类

注意:

  • 看完这一步可能有人有疑问,getWindow().setContentView(layoutResID)这个方法我直接点setContentView跳转进去看不就可以了,还要这么麻烦。那看一下直接点击去是长怎样的,代码如下:

    public abstract void setContentView(@LayoutRes int layoutResID);public abstract void setContentView(View view);public abstract void setContentView(View view, ViewGroup.LayoutParams params);

    什么都没有实现,这是三个抽象方法,实际上,这是跳转到了Window这个抽象类的setContentView,我们无法知道是在哪个子类里具体实现了Activity调用的setContentView方法

  • 在Android4.4的源码里,这里的mWindow类并不是这样直接初始化的,而是通过PolicyManager来makeNewWindow来创建的,这一个地方有做更改

2-1 窗口Window类的一些关系

找到是PhoneWindow之后,我们再来看一下他们的关系

这里有几点说明一下:

  • 在Activity类声明了一个Window抽象类的成员变量mWindow,再在attach方法中把这个抽象类实例化成PhoneWindow类
  • PhoneWindow类有个内部类DecorView,这个DecorView实际上是所有Activity的根View
  • DecorView是继承FrameLayout的自定义View,用作装饰FrameLayout(Decor实际就是装饰的意思)
  • 在Android API 24以后,这个DecorView已经不再作为内部类

2-2 PhoneWindow类的setContentView方法

现在回到PhoneWindow类,来看这个类里的setContenView做了什么

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();        }}

这里主要做了两件事情:

  • 当mContentParent为空时去调用installDecor方法

  • 调用mLayoutInflater.inflate把我们传入的布局文件,附着到mContentParent到这个root布局上

    这两个地方也是setContentView最主要需要分析的地方

hasFeature(FEATURE_CONTENT_TRANSITIONS)相关的判断是在5.0以后加入的,在5.0以前并没有,这些判断主要是为了给页面UI提供变换(transition)和转场(Scene)动画效果,不是我们今天要分析的重点

2-3 PhoneWindow类的installDecor方法

接着我们来看installDecor这个方法

private void installDecor() {        if (mDecor == null) {            mDecor = generateDecor();            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);            mDecor.setIsRootNamespace(true);            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);            }        }        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);            ....            ....            // 在这个判断里面设置标题转场动画等属性        }}                    

这个方法非常长,挑我们需要关注的地方,主要两个,一个是generateDecor方法,一个是generateLayout方法

generateDecor方法内容,如下:

protected DecorView generateDecor() {        return new DecorView(getContext(), -1);}

在这里创建一个DecorView的实例

回到installDecor方法继续看mContentParent是怎么赋值的,它是通过调用generateLayout的返回值

继续看generateLayout方法主要做的事情,如下:

protected ViewGroup generateLayout(DecorView decor) {        // Apply data from current theme.        // 获取当前设置的样式文件        TypedArray a = getWindowStyle();        // 根据获取的样式进行一堆属性设置        ....        ....        // Inflate the window decor.        int layoutResource;        // 我们在代码调用requestWindowFeature的时候会通过下面的getLocalFeatures得到        int features = getLocalFeatures();        // System.out.println("Features: 0x" + Integer.toHexString(features));        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {            // 对ActionBar样式或者自定义标题的一些装饰        } else {            // Embedded, so no decoration is needed.            // 如果不需要ActionBar等标题,则使用R.layout.screen_simple这个布局            layoutResource = R.layout.screen_simple;            // System.out.println("Simple!");        }        mDecor.startChanging();        // 给layoutResource赋值后,把它转换为View        View in = mLayoutInflater.inflate(layoutResource, null);        // 把上面得到的可能包括ActionBar,标题等在内的View给添加进去DecorView        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));        mContentRoot = (ViewGroup) in;        // contentParent实际上就是需要赋值给mContentParent的变量        // 可以看到他对应的是上面layoutResource里面id为content的布局        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);        if (contentParent == null) {            throw new RuntimeException("Window couldn't find content container view");        }         // 再设置一堆样式属性        ....        ....}

基本上面的代码已经通过注释说明了

可以知道这个generateLayout实质就是根据设置的风格来设置相应的根布局,比如如果上面设置了无标题就会调用R.layout.screen_simple这个布局文件。接着DecorView再把根布局添加进来,最后返回根据根布局里面id为content的FrameLayout

我们来看下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>

通过上面布局文件我们知道父容器是一个LinearLayout,接着是两个子控件ViewStub和FrameLayout,其中FrameLayout的id是“content”。结合上面generateLayout的代码,这个根布局的作用就很清晰了。

另外这几个控件有没有很熟悉,再来看一下一开始的View树状图:

分析到这里我们基本可以很好的回答开头提出的两个问题了:

  • requestWindowFeature如果在setContentView之后再去调用的话,因为这时mContentParent已经赋值,也就不会执行installDecor方法,这时layoutresource也已经确定,相应地不可能再执行得到设置标题栏这些属性所在的方法。另外在requestWindowFeature这个方法里面,系统也有做了判断,如下:

    @Overridepublic boolean requestFeature(int featureId) {      if (mContentParent != null) {          throw new AndroidRuntimeException("requestFeature() must be called before adding content");      }        ...      ...}

    如果mContentParent不为空,直接抛出一个AndroidRuntimeException异常

  • 在我们布局树里面出现的PhoneWindow$DecorView,这就是DecorView。接着出现的LinearLayout,ViewStub,FrameLayout就是在上面generateLayout方法里面的layoutresource,因为这个layoutresource是通过判断是设置了哪种属性之后被赋值的布局文件,所以这个也不会太确定,不过可以肯定的是,必须会有一个id为content,这个ViewGroup一般都是FrameLayout,用来作为我们开发时候的根布局

2-4 Window类内部接口Callback的onContentChanged方法

最后再来看看PhoneWindow里setContentView最后剩下的一点代码,如下:

final Callback cb = getCallback();if (cb != null && !isDestroyed()) {    cb.onContentChanged();}

这个Callback的onContentChanged究竟做了什么

首先看看getCallback,这个方法是直接调用父类Window类的getCallback,它返回了mCallback。

public final Callback getCallback() {        return mCallback;}

mCallback又是在哪里被赋值的呢?可以看到只有一个地方对它进行了赋值,这个方法就是setCallBack方法

public void setCallback(Callback callback) {        mCallback = callback;}

那么setCallBack又是在哪里被调用的呢?上面我们在分析窗口Window类的关系的地方,可以知道在Activity里面有声明Window类,它的成员变量是mWindow,也就是一开始分析的getWindow的返回值。

事实上,就在mWindow赋值的地方,mWindow被实例化之后,紧接着立刻做了一件事,也就是调用setCallback,代码可以参见如下:

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) {    attachBaseContext(context);                mFragments.attachHost(null /*parent*/);    // 这里初始化                                       mWindow = new PhoneWindow(this);    // 这里调用setCallback    mWindow.setCallback(this);    .....    .....}

到这里,我们也就明白了cb.onContentChanged()调用的onContentChanged实际就是Activity里面的onContentChanged。

我们再来看看Activity里onContentChanged做了什么:

public void onContentChanged() { }                                

咦,什么也没有。没错,Activity里面的实现是空的。那它用来干什么呢?既然父类是空实现,那么它肯定是用来给子类初始化各自不同的内容了,而这个方法里的内容,系统会帮我们去回调。

举个例子:ListActivity这个方法重写了onContentChanged方法,代码如下:

      @Override      public void onContentChanged() {          super.onContentChanged();          View emptyView = findViewById(com.android.internal.R.id.empty);          mList = (ListView)findViewById(com.android.internal.R.id.list);          if (mList == null) {              throw new RuntimeException(                      "Your content must have a ListView whose id attribute is " +                      "'android.R.id.list'");          }          if (emptyView != null) {              mList.setEmptyView(emptyView);          }          mList.setOnItemClickListener(mOnClickListener);          if (mFinishedStart) {              setListAdapter(mAdapter);          }          mHandler.post(mRequestFocus);          mFinishedStart = true;      }

上面的代码无非也就是一些初始化的操作,比如findViewById之类的。实际上在国外的一些开源框架和安卓系统里面Activity的子类,这些初始化操作都是放在了onContentChanged里去操作的

2-5 setContentView源码分析总结

通过以上的一系列分析,我们知道了setContenView做的事情主要有以下这些:

  • 首先创建一个DecorView,作为所有窗口的根View
  • 接着根据feature或style等主题的属性,创建不同的layoutresource
  • 确定了layoutresource后,通过findviewById找到id为content的控件,作为ViewGroup后返回
  • 最后把我们开发中的布局都添加到上一步返回回来mContentParent的ViewGroup里
原创粉丝点击