zoukankan      html  css  js  c++  java
  • 从Android源码分析View绘制

        在开发过程中,我们常常会来自定义View。它是用户交互组件的基本组成部分,负责展示图像和处理事件,通常被当做自定义组件的基类继承。那么今天就通过源码来仔细分析一下View是如何被创建以及在绘制过程中发生了什么。

    创建

        首先,View公有的构造函数的重载形式就有四种:

    • View(Context context)    通过代码创建view时使用此构造函数,通过context参数,可以获取到需要的主题,资源等等。
    • View(Context context, AttributeSet attrs)    当通过xml布局文件创建view时会使用此构造函数,调用了3个参数的构造方法。
    • View(Context context, AttributeSet attrs, int defStyleAttr)     通过xml布局文件创建view,并采用在属性中指定的style。这个view的构造函数允许其子类在创建时使用自己的style。调用了下面四参的构造方法。
    • View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)    该构造函数可以通过xml布局文件创建view,可以采用theme属性或者style资源文件指定的style。

        参数:

      • Context : view运行的上下文信息,从中可以获取到当前theme,资源文件等信息。
      • AttributeSet: xml布局文件中view标签下指定的属性集合。
      • defStyleAttr: 当前theme中的一条属性,它包含一条指向theme资源文件中style的引用。默认值为0。
      • defStyleRes: 一个style资源文件的标示,表示style的ID,当值为0或者找不到对应的theme资源时候采用默认值。

        综上所述,单参的构造函数从代码创建view,其余都调用四参的构造函数根据xml布局文件创建view。我们可以在不同的地方指定属性值,例如:

    直接在xml标签中中指定的attrs值,可以从AttributeSet中获取

    • 通过在标签属性“style”中指定的资源文件。
    • 默认的defStyleAttr。
    • 默认的defStyleRes。
    • 当前theme中的默认值。

        构造函数的代码过长,就不在这里贴了,主要进行的工作是:获取各项系统定义的属性,然后根据属性值初始化view的各项成员变量和事件。

        一般情况下,我们自定义view的时候,根据实际情况重写构造函数时,如果只从code创建,则只用实现单参数的即可。如果需要从xml布局文件中创建,则需要实现单参数和一个多参数的就好了,因为多参数的默认调用了四参数的构造函数;然后再获取到自定义的属性进行处理就OK了。

        至此,view的创建以及初始化工作完毕,然后开始绘制view的工作。那么Android系统是如何对view进行绘制的呢?

    绘制

        在activity获取到焦点后,会请求Android Framework根据它的布局文件进行绘制,activity需要提供所绘布局文件的根节点,然后对布局的树结构一边遍历一边进行绘制。我们都知道,ViewGroup是View的子类,它可以拥有若干子view,它的很多操作和view相同,不同的是ViewGroup负责绘制其子节点,而view则负责绘制其自身。整个遍历过程从上到下,在整个过程中,需要进行大小测量(measure函数)和定位(layout函数),然后再进行绘制。下面我们来看这些工作是如何进行的:

    测定尺寸

        在Android中,所有view被组织成树状结构,最顶层measure的主要工作就是负责递归测量出整个view树结构的尺寸大小,每个View的控件的实际宽高都是由父视图和本身视图决定的。

        在研究源码之前,我先从整体上概况一下整个递归调用过程。从根view开始,使用measure方法中计算整个view树的大小,在该方法中调用子view的onMeasure方法。在onMeasure中主要进行两个工作:

    1. 调用setMeasuredDimension设置view自身的尺寸(mMeasureWidth和mMeasuredHeight),具体会在下面看到。
    2. 如果该view是ViewGroup,则需要继续递归调用其onMeasure方法来计算ViewGroup的子view大小。

        根view通常就是一个ViewGroup,需要计算子view尺寸。首先获取到所有子view,然后调用measureChildWithMargins方法来计算子view的尺寸。在这个方法中调用了子view的measure方法。下面我们来看具体源码。

        首先在measure方法中确定view的大小。这个方法被定义为final类型,不可被子类重写。在View中有一个静态内部类MeasureSpec封装了父view要传递给子View的布局参数,由size 和 mode共同组成。size即是大小,mode表示模式。(其实就是一个int值高2位表示mode,低30位表示size). mode总共有三种模式:

    • UNSPECIFIED:父view并未指定子view的大小,可随意根据开发人员需求指定view大小。
    • EXACTLY: 父view严格指定了子view的大小
    • AT_MOST: 子view的大小不超过该值
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);//是否使用视觉边界布局 
            if (optical != isLayoutModeOptical(mParent)) {// 当view和它的父viewGroup就是否采用视觉边界布局不一致时
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }
    
            
            long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
            if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    
            if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                    widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec) {
    
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
                resolveRtlPropertiesIfNeeded();
    
                int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                        mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }
    
                // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                    throw new IllegalStateException("onMeasure() did not set the"
                            + " measured dimension by calling"
                            + " setMeasuredDimension()");
                }
    
                mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
            }
    
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
    
            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                    (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
        }

        方法接收的两个参数widthMeasureSpec和heightMeasureSpec表示view的宽高,由上一层父view计算后传递过来。view大小的测量工作在标红的onMeasure方法中进行。我们在自定义view时往往需要重写该方法,根据传入的view大小以及其内容来设定view最终显示的尺寸。

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

        重写该方法时,我们需要调用setMeasuredDimension这个方法来存储已经测量好的尺寸(这里默认使用getDefalutSize),只有在调用过此方法后,才能通过getMeasuredWidth方法和getMeasuredHeight方法获取到尺寸。同时,我们要保证最后得到的尺寸不小于view的最小尺寸。我们需要注意的是,setMeasuredDimension方法必须在OnMeasure方法中调用,否则会抛出异常。

        OK,measure方法至此完毕。然而,我们可以发现真正测量view大小的工作并不在此方法中进行,这里仅仅是一个测量框架,根据各种不同的情况进行判断,完成一些必要的步骤。这些步骤是必须的也是无法被开发者更改的,需要根据情况自定义的工作放在了onMeasure中由开发者完成。这样既保证了绘制流程的执行,又灵活的满足了各种需求,是典型的模板方法模式。

        由于一个父view下可能有多个子view,所以measure方法不仅仅执行一次,而是在父view(viewGroup)中获取到所有子view,然后遍历调用子view的measure方法。

    定位

        当view的大小已经设定完毕,则需要确定view在其父view中的位置,也就是把子view放在合理的位置上。因为只有ViewGroup才包含子view,所以一般我们说起父view,肯定是在说ViewGroup。完成布局工作主要分为两部分,也是递归实现的:

    1. 在layout方法中调用setFrame设置该View视图位于父视图的坐标。
    2. 如果view是ViewGroup类型,则调用其onLayout方法完成子view布局工作。

        下面来看具体源码,父view调用了子view的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); // 确定view在布局中的位置
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
    
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        }

        该方法接收四个参数是子view相对于父view而言的上下左右位置。然而我们发现其中调用到的onLayout方法默认的实现是空的。这是因为确定view在布局的位置这个操作应该由Layout根据自身特点来完成。任何布局的定义都要重写其onLayout方法,并在其中设定子view的位置。

    绘制

        在进行完测定尺寸和定位之后,终于可以开始绘制了。这里的工作仍是通过递归来完成的。view调用draw方法来进行绘制,里面调用onDraw来绘制自身,如果还有子view则需要调用dispatchDraw来绘制子view。

        绘制需要调用draw方法,总共分为六个步骤:

    1. 绘制背景
    2. 如果需要,保存canvas的层次准备边缘淡化。
    3. 绘制view的内容
    4. 绘制子view
    5. 如果需要,绘制淡化的边缘并存储图层。
    6. 绘制装饰部分,例如滚动条等。
    public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
            mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
            // Step 1, 绘制背景
            int saveCount;
    
            if (!dirtyOpaque) {
                drawBackground(canvas);
            }
    
            // 如果不需要,跳过步骤2和5
            final int viewFlags = mViewFlags;
            boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
            boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
            if (!verticalEdges && !horizontalEdges) {
                // Step 3, 绘制内容
                if (!dirtyOpaque) onDraw(canvas);
    
                // Step 4, 绘制子view
                dispatchDraw(canvas);
    
                // Step 6, 绘制装饰部分
                onDrawScrollBars(canvas);
    
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().dispatchDraw(canvas);
                }
    
                // 完成
                return;
            }
    
        }

        我们选择常规的绘制过程,不介绍2,5步骤。

        第一步,调用drawBackground绘制背景图案:

    private void drawBackground(Canvas canvas) {
            final Drawable background = mBackground;
             // 获取到当前view的背景,是一个drawable对象 if (background == null) {
                return;
            }
    
            if (mBackgroundSizeChanged) {// 判断背景大小是否变化,是则设置背景边界
                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
                mBackgroundSizeChanged = false;
                mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;
            }
    
            // Attempt to use a display list if requested.
            if (canvas.isHardwareAccelerated() && mAttachInfo != null
                    && mAttachInfo.mHardwareRenderer != null) {
                mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
    
                final RenderNode displayList = mBackgroundRenderNode;
                if (displayList != null && displayList.isValid()) {
                    setBackgroundDisplayListProperties(displayList);
                    ((HardwareCanvas) canvas).drawRenderNode(displayList);
                    return;
                }
            }
           // 调用drawable对象的绘制方法完成绘制
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
            if ((scrollX | scrollY) == 0) {
                background.draw(canvas);
            } else {
                canvas.translate(scrollX, scrollY);
                background.draw(canvas);
                canvas.translate(-scrollX, -scrollY);
            }
        }

        第三步,调用onDraw方法绘制view的内容,由于不同的view内容不同,所以需要子类进行重写。

        第四步,绘制子view,这里仍然需要当前layout的dispatchDraw方法来完成对各子view的绘制。

        第六步,绘制滚动条。

        通常情况下,我们自定义view,复写onDraw方法来绘制我们定义的view的内容即可。

    总结

        通过研究view类的源码,我们可以发现,在整个view的绘制流程中我们需要完成测定尺寸,布局定位,绘制这三个步骤。Android在设计过程中,将固定不变的流程设计为不可更改的模板方法,然而需要根据不同情况而定的内容则交给开发者来完成重写,在模板方法中调用即可。这样设计即保证了整个流程的完整,又给开发工作带来了灵活。同时,在类中又根据不同情况定义了不同的flag,来满足不同情况的绘制需求,以后有机会再具体研究这些flag的具体意义。

  • 相关阅读:
    POJ 2723 Get Luffy Out(2-SAT)
    ZOJ 3613 Wormhole Transport
    HDU 4085 Peach Blossom Spring
    NBUT 1221 Intermediary
    NBUT 1223 Friends number
    NBUT 1220 SPY
    NBUT 1218 You are my brother
    PAT 1131. Subway Map (30)
    ZSTU OJ 4273 玩具
    ZSTU OJ 4272 最佳淘汰算法
  • 原文地址:https://www.cnblogs.com/cqumonk/p/4718788.html
Copyright © 2011-2022 走看看