我在爬虫小分队的见闻Android自定义ViewAndroid

我奶奶都能懂的UI绘制流程(上)!

2017-07-19  本文已影响1051人  吴愣

1.前言

从今天开始,慢慢整理Android高级UI的知识,涉及到各种酷炫狂拽吊炸天的特效。

之前写过一篇Window一本满足算是这个专题的预备知识,本文就基于这篇文章,继续往下探索UI的绘制流程。按照国际惯例,我们开始源码的分析,毕竟只有把原理给搞清楚了,才能进行各种天马行空的创作。

2.setContentView

在关于Window的学习中,我们知道每个Activity都有自己的一个Window负责界面的显示。PhoneWindow是抽象Window唯一的实现类。调用Activity的setContentView()实际上是调用了PhoneWindow的setContentView()

  public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } 
      ...
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
      ...
    }

最开始的时候,判断mContentParent是否为空,为空则执行installDecor(),从名字上可以看出这个方法与DecorView的初始化有关。接下来通过FEATURE_CONTENT_TRANSITIONS判断是否需要执行过场动画,需要则执行,不需要则直接通过mLayoutInflater将XML资源加载到mContentParent中。

关于mContentParent和mDecor的关系,直接看官方注释,我就不翻译了。

    // 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.
    private ViewGroup mContentParent;
if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
         ...
        }

接着来看看先前猜测的installDecor()方法到底做了些啥

 private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            ...
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
            ...

当mDecor为空时generateDecor()会直接新建一个DecorView对象并将其返回,注意,DecorView本质上就是一个FrameLayout。

接着当mContentParent为空时,执行generateLayout(mDecor)并将返回值赋给mContentParent,这是一个重量级的方法,主要包含5块内容

第一步,通过getWindowStyle()获取当前Window的TypedArray 。熟悉自定义控件的同学对TypedArray一定是相当熟悉的,他可以用来获取布局xml中的信息 。

TypedArray a = getWindowStyle();

第二步,通过获取到的TypedArray对Feature状态位进行设置,比如判断当前Window是否为悬浮状态,是否全屏,是否显示ActionBar,是否透明等等

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

      ....省略剩下9999种判断...

第三步,通过设置好的Feature获取对应的layoutResource,这些layoutResource都是Android系统原先就提供好的。

   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!");
 
           ....省略中间9999种判断...
 
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }

我们来看看最简单的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>

ViewStub是用来延迟加载的一种组件,是用来动态显示bar的,而id为content的这个FrameLayout就是我们真正加载布局的地方了。一定要记住android:id="@android:id/content",其他类型的布局或许样式不同,但真正加载用户布局的id始终都为content

第四步,将获取到的layoutResource进行渲染,添加到decor中。要注意,这个时候用户的布局还没有加载到content中,此时只是将原始的layoutResource加载到decor中

        View in = mLayoutInflater.inflate(layo utResource, null);
        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        mContentRoot = (ViewGroup) in;

第五步,获取layoutResource中id为ID_ANDROID_CONTENT的ViewGroup,并将其返回。这个ViewGroup就是真正加载用户布局的地方。

 public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
 ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
 ...
 return contentParent;

到此为止,generateLayout(mDecor)完成了自己的历史使命,mContentParent 成为了真正加载用户布局的FrameLayout。回到setContentView()方法中,现在容器已经准备好了,我们可以放心的开始加载用户布局。

1_Activity加载UI-类图关系和视图结构.png

3.LayoutInflater

setContentView()的最后,用户布局开始进行加载

mLayoutInflater.inflate(layoutResID, mContentParent);

inflate()方法第一次重载后如下

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

注意第三个参数为root != null,由于我们将mContentParent作为root传入,所以此时第三个参数为true

下一次重载中,会获取一个XmlResourceParser用于解析用户传入的布局资源,之后将这个XmlPullParser 作为参数进行最后一次重载。这个方法内容比较多,我们一点一点看

首先,根据XmlResourceParser获取到AttributeSet ,这个set中保存了xml布局中的配置信息。

 final AttributeSet attrs = Xml.asAttributeSet(parser);

接着,通过XML解析获取根节点,此时name就是根节点标签的名字。不熟悉XML解析的同学自行百度。(主要有PULL,SAX,DOM三种解析方式)

                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();

获取到根节点的标签后,首先要判断是否为TAG_MERGE。如果是且root为空则抛出异常,否则进行合并渲染。

这里稍微解释一下TAG_MERGE。在我们写布局的时候,会使用<include/>标签来引入某个布局,<merge/>标签的作用就体现在此,因为父布局已经存在一个ViewGroup了,所以使用<merge/>时,子布局可以不写最外层的ViewGroup。这样就做到了减少图层的效果。

if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } 

如果根节点标签不是TAG_MERGE,那么此时获取到的Tag就是xml中真正的根View。

                   // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs)

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

我们将其创建出来。此时View类已经实例化了,但是在xml设置的属性还没有添加进去。

xml中的属性通过XmlResourceParser解析到attrs中,所以此时要通过root.generateLayoutParams(attrs)将attrs转化成LayoutParams 。还记得root是什么吗?在上文中,root就是id为content的那个FrameLayout。是加载用户布局的地方。

我们获取到了LayoutParams,最后只要通过temp.setLayoutParams(params)将params属性设置到View中就OK了。继续往下看代码:

                 // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);
                    ...
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

现在已经获得了用户布局中的根View以及它的属性,接下来就通过rInflateChildren(parser, temp, attrs, true)来渲染其子view,其会重载rInflate(),这个方法长度适中,为了大家能全方位的理解,就一口气展示出来

 void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        ...
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();
            ...
            else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

可以看到,整个过程是处于while循环中,这也是xml解析的一种基本方式。

首先还是通过XmlPullParser 获取到子布局的名称,接着开始判断子布局的类型。如果类型为TAG_INCLUDE并且深度为0,说明<include />是根节点,抛出异常。如果发现类型为TAG_MERGE且深度不为0,说明<merge />不是根节点,抛出异常。

异常判断结束后,重复之前绘制根节点的操作,将子View与子View的子View都一一绘制并添加到他们的父View中。

经过上面这些操作后,用户界面XML中的元素就全部解析并且封装了起来,最后就可以调用root.addView(temp, params)将这个封装完毕的View添加到root中。

到此为止,LayoutInflater.inflate()方法完成了它的历史使命,我们用一张图来总结

LayoutInflater.png

4.AppCompatActivity

文章前面已经将Activity的setContentView()介绍完毕了,但是现在使用AndroidStudio开发时,咱们默认的Activity是谁?是AppCompatActivity,这是一个为了填补Google曾经挖下的各种坑而出现的超级无敌自适应Activity。下面,大家一起来看看AppCompatActivity的setContentView()是怎么操作的。

@Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

这一上来就不一样啊。
getDelegate()是获取代理的方法,会通过mDelegate = AppCompatDelegate.create(this, this)来创建代理对象

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

可以看到,不同的版本会返回不同的代理对象,这些代理对象都继承自AppCompatDelegateImplV9,我们重点看这个类的setContentView()方法。

 @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中的差不多,就是第一行多了ensureSubDecor(),这个方法会调用createSubDecor()来创建一个ViewGroup对象,这是AppCompatActivity中十分关键的一个方法。

createSubDecor()和Activity中的generateLayout(mDecor)十分类似,因为比较重量级,具体的可以结合源码与文章前面对generateLayout(mDecor)的分析来看,在这里我们就分析几处关键的地方。

首先,获取到TypedArray 对象。

 TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

要注意,此处的主题是R.styleable.AppCompatTheme。系统需要通过这个主题来对一些View进行兼容性的改造。这也就是为什么在使用AppCompatActivity时,主题必须设置为AppCompat类型,否则就会抛出异常。

接下来,获取DecorView

// Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();

getDecorView()会调用PhoneWindow的installDecor(),这个方法之前详细介绍过,很重要,忘记了就往前翻翻。

继续下潜,有很长一段代码都是用来判断subDecor需要加载什么系统布局,这个过程和Activity中的类似,我们依然以simple布局为例

 subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.FitWindowsLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/action_bar_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:fitsSystemWindows="true">

    <android.support.v7.widget.ViewStubCompat
        android:id="@+id/action_mode_bar_stub"
        android:inflatedId="@+id/action_mode_bar"
        android:layout="@layout/abc_action_mode_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <include layout="@layout/abc_screen_content_include" />

</android.support.v7.widget.FitWindowsLinearLayout>

这些组件都是在v7包中用来做版本适配的,再来看看Include进来的这个布局

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <android.support.v7.widget.ContentFrameLayout
            android:id="@id/action_bar_activity_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:foregroundGravity="fill_horizontal|top"
            android:foreground="?android:attr/windowContentOverlay" />
</merge>

这个ContentFrameLayout就相当于Activity中的FrameLayout,所以我们一定要把它的id记住,action_bar_activity_content,action_bar_activity_content,action_bar_activity_content,说三遍。

终于,材料已经准备完毕,是时候来享受真正的大餐了。

final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);

final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);

contentView 是subDecor中id为action_bar_activity_content的ContentFrameLayout,
windowContentView是mWindow中id为content的FrameLayout。

windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
...
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);

将windowContentView的id设为NO_ID,再将contentView的id设为content。这个时候,R.id.action_bar_activity_content就完成了它的任务。

最后,将subDecor添加到mWindow中,大功告成!

是不是感觉茅塞顿开了?这招偷梁换柱简直漂亮!我们上一张图来感受此时下整体的结构。

AppCompatActivity的View布局结构.png

5.ViewRootImpl

仔细回忆下之前的过程,在setContentView()方法中,界面布局的xml资源已经解析并生成了view,而view也添加到了window上,但此时view并没有绘制出来,对用户而言还是不可见的。

接下来,我们就来学习View的绘制流程。在开始前,强烈建议大家先去复习下有关Window的爱恨情仇!以及Activity启动流程简直丧心病狂!,不然等会懵逼的可能性会很大。

故事要从Activity启动流程简直丧心病狂!的结尾开始,上回说到,在ActivityThread中调用了handleLaunchActivity()开始真正启动一个活动,今天咱们就来仔细分析下这个方法。

Activity a = performLaunchActivity(r, customIntent);

首先,调用performLaunchActivity()实例化了Activity,这个方法主要做了三件事
第一,通过反射获取到Activity的实例

 ClassLoader cl = r.packageInfo.getClassLoader();
 activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);

第二,调用Activity.attach()初始化window

 activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor);
final void attach(...){
...
mWindow = new PhoneWindow(this, window);
...
}

第三,通过mInstrumentation最终回调了Activity的onCreate方法

mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState)
 public void callActivityOnCreate(Activity activity, Bundle icicle,
            PersistableBundle persistentState) {
        ...
        activity.performCreate(icicle, persistentState);
        ...
    }
final void performCreate(Bundle icicle) {
       ...
        onCreate(icicle);
       ...
    }

由于setContentView()是在onCreate()中执行的,所以现在我们就获取了view并添加到了window上,接下来要开始绘制了,很显然,留给我们进行绘制的只剩下onResume

现在回到handleLaunchActivity()方法中,继续往下看,果然这里会调用handleResumeActivity()

 handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);

进入这个方法,看看会发生什么。

                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                ...
                wm.addView(decor, l);

也就是说,在handleResumeActivity()中,我们获取到了DecorView以及WindowManager,并将decor添加到了wm中。

下面这些内容在有关Window的爱恨情仇!中有详细的介绍,这里简要的说一下

WindowManager.addView()的作用就是通过AIDL将window显示到屏幕上,再调用ViewRootImpl进行view的绘制

addView()中,会实例化ViewRootImpl对象并调用它的setView()方法

root = new ViewRootImpl(view.getContext(), display);
...
root.setView(view, wparams, panelParentView); 

ViewRootImpl.setView()主要做了三件事,第一是通过下面的代码将window添加到屏幕上

res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);

第二是调用requestLayout()进行view的绘制
第三是调用view.assignParent(this)将decorView的parent设置为当前的ViewRootImpl

事件一在有关Window的爱恨情仇!介绍过了,过程比较复杂,请移步。

今天我们主要介绍事件二、三。
首先看比较简单的事件三,这里就是直截了当的将ViewRootImpl设置为decorView的parent

 void assignParent(ViewParent parent) {
        if (mParent == null) {
            mParent = parent;
        }
         ...
    }

这么做的意义是什么呢?大家知道,在View中调用requestLayout()会使得界面重绘,来看看这个方法

public void requestLayout() {  
      ...

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
      ...
    }

原来如此,View.requestLayout()会不断回调其parent的requestLayout()方法,最后到达decorView时,就会调用ViewRootImpl的requestLayout()

也就是说,ViewRootImpl.requestLayout()是view绘制的起源,我们来事件二仔细感受一下

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

该方法会调用scheduleTraversals()

 void scheduleTraversals() {
       ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
       ...
    }

接着在mChoreographer中执行mTraversalRunnable,这是一个Runnable 对象,唯一的作用就是调用doTraversal()

 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal()又会调用performTraversals(),这个方法那是相当长,一看就是有特殊癖好的变态工程师写的,我们主要看其中与UI绘制有关的部分。从前往后慢慢找,依次可以看到他们:

 // Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performDraw();

哇!终于看到这三兄弟了!大家一起来松口气,咱们今天就说到这,虽然还没开始View的绘制,但前面的准备工作都完成啦!最后方式一张流程图进行来梳理一下吧。

DecorView添加至窗口的过程.png

6.总结

什么?你说让我奶奶懂一个给你看看?

哈哈,哈哈,哈哈哈……

完结撒花~

上一篇下一篇

猜你喜欢

热点阅读