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)

扫描二维码关注公众号,回复: 3280650 查看本文章
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中的,敬请关注!

猜你喜欢

转载自blog.csdn.net/boyeleven/article/details/82759753