自定义组件开发一 View 的绘制流程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011733020/article/details/80211375

Activity 的组成结构

Activity对于安卓开发来说,是熟悉的不能再熟悉的,它是安卓四大组件之一,用来做界面显示用的,那么我相信,并不是所有的朋友都对Activity的组成结构有清晰的认识,这里简单聊聊Activity的组成。
实际界面展示的是Activity中的Window来完成的,Activity中有一个PhoneWindow对象,它继承自Window,里面有一个顶级的RootView 叫DecorView,DecorView 是我们创建的所有的View的根,DecorView 由三部分组成 ActionBar、标题区和内容区,其中内容区是我们用的比较多的,比如我们可以通过android.R.id.content 获取到内容区,是一个FrameLayout。
DecorView 继承自FrameLayout,我们在Activity 中调用SetContentView(layoutResID)

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

其实就是传递给PhoneWindow

362     @Override
363     public void More ...setContentView(int layoutResID) {
364         // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
365         // decor, when theme attributes and the like are crystalized. Do not check the feature
366         // before this happens.
367         if (mContentParent == null) {
368             installDecor();
369         } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
370             mContentParent.removeAllViews();
371         }
372 
373         if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
374             final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
375                     getContext());
376             transitionTo(newScene);
377         } else {
378             mLayoutInflater.inflate(layoutResID, mContentParent);
379         }
380         final Callback cb = getCallback();
381         if (cb != null && !isDestroyed()) {
382             cb.onContentChanged();
383         }
384     }

mContentParent 就是android.R.id.content 表示的那个内容区容器。最终我们setContentView 传进来的View就添加到了mContentParent 上面。

这里写图片描述

这张图可以形象表面,上面所说的各部分关系:

Activity 类似于一个框架,负责容器生命周期及活动,窗口通过 Window 来管理;
Window 负责窗口管理(实际是子类 PhoneWindow),窗口的绘制和渲染交给 DecorView完成;
DecorView 是 View 树的根,开发人员为 Activity 定义的 layout 将成为 DecorView 的子视图 ContentParent 的子视图;
layout.xml 是开发人员定义的布局文件,最终 inflate 为 DecorView 的子组件;

另外PhoneWinodw 中有两个比较重要的对象WindowManager和ViewRootImpl。
WindowManager 处理触摸事件,ViewRootImpl去完成UI的绘制。
这里写图片描述

View的绘制

ViewRootImpl 负责 Activity 整个 UI 的绘制,而绘制是 ViewRootImpl 的
performTraversals()方法。
里面有三个方法:
这里写图片描述

performMeasure():测量组件的大小;
performLayout():用于子组件的定位(放在窗口的什么地方);
performDraw():将组件的外观绘制出来;

测量组件

performMeasure()方法负责组件自身尺寸的测量,我们知道,在 layout 布局文件中,每一个
组件都必须设置 layout_width 和 layout_height 属性。
属性值有三种可选模式:wrap_content、match_parent 和数值,
performMeasure()方法根据设置的模式计算出组件的宽度和高度。大多数情况下模式为 match_parent 和数值的时候是不需要计算的,传过来的就是父容器自己计算好的尺寸或是一个指定的精确值,只有当模式为 wrap_content 的时候才需要根据内容进行尺寸的测量。

private void performMeasure(int childWidthMeasureSpec, int
childHeightMeasureSpec) {
    try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
    }
}

performMeasure方法测量组件调用了View的 measure方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ……
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ……
}

onMeasure()一般我们需要重写这个方法去给View的子类指定宽度和高度。

如果测量的是容器的尺寸,而容器的尺寸又依赖于子组件的大小,所以必须先测量容器中子
组件的大小,不然,测量出来的宽度和高度永远为 0

确定组件的位置

performLayout()方法用于确定子组件的位置,所以,该方法只针对 ViewGroup 容器类。作为容器,必须为容器中的子 View 精确定义位置和大小。

private void performLayout(WindowManager.LayoutParams lp,
                        int desiredWindowWidth, int desiredWindowHeight){
    ……
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ……
    for (int i = 0; i < numValidRequests; ++i) {
        final View view = validLayoutRequesters.get(i);
        view.requestLayout();
    }
}

代码中的 host 是 View 树中的根视图(DecroView),也就是最外层容器,容器的位置安排在左上角(0, 0),其大小默认会填满 mContentParent 容器。我们重点来看一下 layout()方法的源码

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight; 
    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);
    ……
    }
    ……
}

在 layout()方法中,在定位之前如果需要重新测量组件的大小,则先调用 onMeasure()方法,接下来执行 setOpticalFrame()或 setFrame()方法确定自身的位置与大小,此时只是保存了相关的值,与具体的绘制无关。随后,onLayout()方法被调用

protected void onLayout(boolean changed, int left, int top, int right, int          bottom) {

}

onLayout()方法在这里的作用是当前组件为容器时,负责定位容器中的子组件

绘制组件

performDraw()方法执行组件的绘制功能,组件绘制是一个十分复杂的过程,不仅仅绘制组
件本身,还要绘制背景、滚动,绘制流程:

private void performDraw() {
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
try {
    draw(fullRedrawNeeded);
} finally {
    mIsDrawing = false;
}
……
}

在 performDraw()方法中调用了 draw()方法:

private void draw(boolean fullRedrawNeeded) {
……
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
 return;
}
……
}

draw()方法又调用了 drawSoftware()方法:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
……
final Canvas canvas;
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;
canvas = mSurface.lockCanvas(dirty);
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
    canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
mView.draw(canvas);
surface.unlockCanvasAndPost(canvas);
……
}

drawSoftware()方法中调用了 mView 的 draw()方法,前面说过,mView 是 Activity 界面中 View树的根(DecroView),也是一个容器(具体来说就是一个 FrameLayout 布局容器)FrameLayout 类的 draw()方法源码:

public void draw(Canvas canvas) {
super.draw(canvas);
……
final Drawable foreground = mForeground;
foreground.draw(canvas);
}

FrameLayout 类的 draw()方法做了两件事,一是调用父类的 draw()方法绘制自己,二是将前景位图画在了 canvas 上。自然,super.draw(canvas)语句是我们关注的重点,FrameLayout 继承自ViewGroup,遗憾的是 ViewGroup 并没有重写 draw()方法,也就是说,ViewGroup 的绘制完全重用了他的父类 View 的 draw()方法,不过,ViewGroup 中定义了一个名为 dispatchDraw()的方法,该方法在 View 中定义,在 ViewGroup 中实现,至于有什么用,暂且卖个关子,我们先扒开 View的 draw()方法源码看看:

public void draw(Canvas canvas) {
    background.draw(canvas);
    if (!dirtyOpaque) onDraw(canvas);
    dispatchDraw(canvas);
    onDrawScrollBars(canvas);
}

View 类的 draw()方法是组件绘制的核心方法,主要做了下面几件事:
绘制背景:background.draw(canvas)
绘制自己:onDraw(canvas)
绘制子视图:dispatchDraw(canvas)
绘制滚动条:onDrawScrollBars(canvas)
background 是一个 Drawable 对象,直接绘制在 Canvas 上,并且与组件要绘制的内容互不干扰,很多时候,这个特征能被某些场景利用,比如后面的“刮刮乐”就是一个很好的范例。
View 只是组件的抽象定义,他自己并不知道自己是什么样子,所以,View 定义了一个空方法 onDraw(),源码如下:

protected void onDraw(Canvas canvas) {

}

和前面讲过的 onMeasure()与 onLayout()一样,onDraw()方法同样是预留给子类扩展的功能
接口,用于绘制组件自身。组件的外观由该方法来决定。
dispatchDraw()方法也是一个空方法,源码如下:

protected void dispatchDraw(Canvas canvas) {

}

该方法服务于容器组件,容器中的子组件必须通过 dispatchDraw()方法进行绘制,所以,View虽然没有实现该方法但他的子类 ViewGroup 实现了该方法:

protected void dispatchDraw(Canvas canvas) {
    ……
    final int count = mChildrenCount;
    final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
        }
    ……
}

在 dispatchDraw()方法中,循环遍历每一个子组件,并调用 drawChild()方法绘制子组件,而子组件又调用 View 的 draw()方法绘制自己:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

组件的绘制也是一个递归的过程,说到底 Activity 的 UI 界面的根一定是容器,根容器绘制
结束后开始绘制子组件,子组件如果是容器继续往下递归绘制,否则将子组件绘制出来……直所有的组件正确绘制为止。
总体来说,UI 界面的绘制从开始到结束要经历几个过程:
测量大小,回调 onMeasure()方法
组件定位,回调 onLayout()方法
组件绘制,回调 onDraw()方法
概括描述组件的绘制过程就是,首先通过PerformMeasure方法测量组件的大小,接PerformLayout来定位组件的位置,最后调用PerformDraw方法来绘制组件。

谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309

猜你喜欢

转载自blog.csdn.net/u011733020/article/details/80211375
今日推荐