阅读 309

Android-View体系-绘制流程

前言

在上一篇文章中慢~再听我讲一遍Activity的启动流程以源码执行顺序为流程分析了从应用点击到Activity.onCreate的过程,但是用户还是处于看不到页面的状态,那么之后的哪些操作才会让用户才能看到我们的页面呢?所以此文主要是接上一篇文章继续分析。
一般来说,Activity用于UI界面的显示和处理,但是Activity并不会直接管理ViewTree,而是交由Window来管理,那么要让页面处于显示状态,首先要构建起Window的相关体系,然后创建ViewTree,再然后依次处理ViewTree中的View即可。

一.绘制的前置工作

1.创建Window

Window的字面意思就是窗口,是一种抽象的概念,它居于页面的最顶级,提供可绘制UI和响应输入事件的区域,PhoneWindow是Window的具体实现。

Activity.java
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, IBinder assistToken) {
	...
	//创建PhoneWindow对象
	mWindow = new PhoneWindow(this, window, activityConfigCallback);
	...
	//为window设置windowManager
	mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
	...
}
复制代码

在Activity.attach中完成了PhoneWindow的创建,由此可见,每个Activity都有一个与之对应的window,但是如果这么多Window都要和WMS一一通信,那么既浪费资源,又容易出问题,所以它们统一由WindowManager来管理,WindowManager是一个接口,它的实现是WindowManagerImpl。

2.创建DecorView

DecorView是一个继承自FrameLayout的View,它是整个view的原始容器,包含一个LinearLayout,它有两个子元素TitleView和ContentView,一般在onCreate中setContentView就是把xml布局解析后填充到这个区域。

  PhoneWindow.java
  @Override
    public void setContentView(int layoutResID) {
        ...
        //mContentParent是ContentView的父容器 未初始化为null
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        ...
        //解析xml文件 填充view
        mLayoutInflater.inflate(layoutResID, mContentParent);
        ...
    }
    
    private void installDecor() {
        ...
        if (mDecor == null) {
            //generateDecor 中 返回了DecorView的实例
            mDecor = generateDecor(-1);
            ...
        } 
        ...
        if (mContentParent == null) {
            //通过findviewbyid(R.id.content) 找到ContentView
            mContentParent = generateLayout(mDecor);
            ...
        }
     }
复制代码

handleLaunchActivity方法会调用Activity.onCreate方法,在这个方法里调用setContentView,加载我们的布局到DecorView中。

3.关联DecorView与ViewRootImpl

ViewTree是View的层级关系,需要依附于Window来显示,而且也需要被管理,所以由ViewRoot连接DecorView和WindowManager,并且管理着View的测量、布局、绘制、包括触摸等相关事件。

   ActivityThread.java
    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {
	//调用onResume
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        if (r.window == null && !a.mFinished && willBeVisible) {
            //获取到Window和DecorWindow
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            //得到了WindowManager
            ViewManager wm = a.getWindowManager();
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //addView由WindowManagerGlobal实现
                    wm.addView(decor, l);
                }
            }
        }
    }
    
WindowManager.java
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
        synchronized (mLock) {
            //创建了ViewRootImpl 
            root = new ViewRootImpl(view.getContext(), display);
            ...
            //将ViewTree与ViewRootImpl关联 后续会对他进行操做
            root.setView(view, wparams, panelParentView, userId);
        }
    }
复制代码

上述整体关系的创建和关联流程图如下,有需要的同学可以据此查看源码:

View绘制流程.png

二.绘制的整体流程

绘制的流程从performTraversals开始,依次调用performMeasure,performLayout,performDraw,它们对应着measure,layout,draw三个流程,measure用于计算view在ui界面上的区域大小,layout用于计算view的位置,draw用于绘制view的内容。

ViewRootImpl.java
private void performTraversals() {
    ...
    //根据mWidth/mHeight(窗口的宽高)和lp(LayoutParams)获取当前测量规则
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //执行测量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //执行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //执行绘制流程
    performDraw();
}
复制代码

1.measure流程

1.1.测量规则

先来分析performTraversals方法中getRootMeasureSpec方法,其表面意思获取根view的测量规则,所有view在测量阶段都共同遵循一个规则,即根据父view的MeasureSpec和自身的LayoutParams决定如何测量,但也有例外,比如DecorView是根据窗口的大小和自身的LayoutParams决定的。
LayoutParams指定视图的高度和宽度等参数,它有三种方式:

  1. 具体值 比如10dp
  2. MATCH_PARENT 表示子容器想和父容器一样大
  3. WRAP_CONTENT 表示容器包裹其内容就可以

MeasureSpec有三种模式:

  1. EXACTLY 父容器给出了明确的小大,view的最终大小就是这个值,对应LayoutParams的MATCH_PARENT和具体值
  2. AT_MOST 父容器给出了可用的大小,view不能超过这个值,对应LayoutParams的WRAP_CONTENT
  3. UNSPECIFIED 父容器不对子容器做限制

接下来结合源码看一下例子:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        //LayoutParams.MATCH_PARENT对应MeasureSpec的EXACTLY模式
        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        //LayoutParams.WRAP_CONTENT对应MeasureSpec的AT_MOST模式
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            //这种情况就是填了具体值 组建规则时直接用具体的值
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
}
//将模式和数值结合按规则结合为一个32位的int值 节约内存
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
}   
复制代码

通过判断rootDimension(view的宽度/高度信息)对应LayoutParams的三种模式,根据三种模式配对出MeasureSpec的组建方式。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        ...
        //mView(DecorView)开始measure
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
}
复制代码
1.2.View的测量

View的measure方法中会去调用View的onMeasure方法,View的onMeasure方法如下所示。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码

setMeasuredDimension会设置View的宽和高而且必须在onMeasure里设置,接下来看getDefaultSize

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
复制代码

在这个方法中View的大小其实已经取决于父类传递的MesureSpec中的specSize了,而且这里的AT_MOST和EXACTLY取的值一样,那么wrap_content其实是失去了该有的效果的,所以如果直接继承view要重写onMeasure方法判断wrap_content模式添加默认的值。

1.3.ViewGroup的测量

对于DecorView来说,实际执行测量工作的是FrameLayout的onMeasure,但是在分析FrameLayout.onMeasure前,我们先来看一下VIewGroup的测量流程,VIewGroup是一个抽象类,没有实现onMeasure(因为每个子类的实现方式不同,比如LinearLayout、RelativeLayout),而是由子类实现,但是它提供了measureChildren,代码如下:

ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        //遍历所有的子view调用measureChild方法
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
}
    
protected void measureChild(View child, int parentWidthMeasureSpec,
         int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
              mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
              mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码

measureChild方法的大致意思是 取出子view的LayoutParams,然后通过getChildMeasureSpec创建子View的MeasureSpec,再将MeasureSpec传递给子View进行测量,这样一套下来就全部都经历了测量的流程。
接下来看一下FrameLayout的onMeasure:

FrameLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	...
        for (int i = 0; i < count; i++) {
            //循环所有的子view
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //取得最大值
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                ...
            }
        }
	...
        //记录结果
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
}
复制代码

上面的代码中measureChildWithMargins对所有的子view测量了一遍,如果有padding和margin也会参与计算,经过了以上计算后,我们得到了maxHeight和maxWidth的最终值,使用setMeasuredDimension保存到mMeasuredWidth与mMeasuredHeight成员变量中,它们不仅包含了size还包含了state,所以会先调用resolveSizeAndState处理这两个值。

2. layout流程

经过performMeasure流程,ViewTree中各个元素的大小已经被记录下来,接下来会进入另一个遍历的流程,即Layout,它的作用是ViewGroup用来确定子元素的位置信息。

public void layout(int l, int t, int r, int b) {
    ...
    //记录四个位置setFrame,l,t,r,b代表此View与父View的左上右下边框的距离。
    boolean changed = isLayoutModeOptical(mParent)?setOpticalFrame(l,t,r,b):setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        ...
    }
    ...
}
复制代码

DecorView的layout实际上调用了View的layout方法,它的意思就是通过setFrame方法来设定View的四个顶点的位置,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了,接着会调用onLayout方法,我们来看看FrameLayout的onLayout方法:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }
    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();//child在measure中测量到的宽度
                final int height = child.getMeasuredHeight();//measure高度
                int childLeft;
                int childTop;
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                //计算childLeft
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }
                //计算childTop,类似与计算childLeft
                ...
                }
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }
复制代码

上述代码可见,循环所有子View然后调用其layout方法传入此View的layout信息,这里主要计算了childLeft和childTop这两个值,对于一个矩形,得到left,top,width和height就已经知道了它的大小。layout结束后,view的大小和位置就已经确定了。

3. draw流程

一个对象的layout确定后,才可以执行draw,代码如下:

    @CallSuper
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

		//绘制背景
        // Step 1, draw the background, if needed
        int saveCount;

        drawBackground(canvas);

        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
             //绘制自己的内容
            // Step 3, draw the content
            onDraw(canvas);
		   //绘制childen
            // Step 4, draw the children
            dispatchDraw(canvas);
            drawAutofilledHighlight(canvas);
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
		   //第六步,绘制装饰(前景/滚动条)
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
            if (isShowingLayoutBounds()) {
                debugDrawFocus(canvas);
            }
            // we're done...
            return;
        }
        ...
}
复制代码

整体上做了以下几步:

  1. 绘制背景
  2. 绘制自己的内容
  3. 绘制childen
  4. 绘制装饰(前景/滚动条)

总结

在这一部分中,要先梳理好整个View的框架,比如ACtivity,Window,ViewRoot,WindowManagerImpl之间的关系,然后再看整个ViewTree的遍历以及处理会比较好理解。

最后

如果本文提供了有效信息还烦请点个小赞,如果有什么出入可以评论或私信我,感谢。

文章分类
Android
文章标签