码迷,mamicode.com
首页 > 移动开发 > 详细

从Android源码分析View绘制

时间:2015-08-10 19:40:22      阅读:135      评论:0      收藏:0      [点我收藏+]

标签:

    在开发过程中,我们常常会来自定义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则负责绘制其自身。整个遍历过程从上到下,在整个过程中,需要进行大小测量(measure函数)和定位(layout函数),然后再进行绘制。下面我们来看这些工作是如何进行的:

设定大小

    首先在measure方法中确定view的大小。这个方法被定义为final类型,不可重写。在View中有一个静态内部类MeasureSpec封装了父view要传递给子View的布局参数,由size 和 mode共同组成。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的最小尺寸。OK,measure方法至此完毕。然而,我们可以发现真正测量view大小的工作并不在此方法中进行,这里仅仅是一个测量框架,根据各种不同的情况进行判断,完成一些必要的步骤。这些步骤是必须的也是无法被开发者更改的,需要根据情况自定义的工作放在了onMeasure中由开发者完成。这样既保证了绘制流程的执行,又灵活的满足了各种需求,是典型的模板方法模式。

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

 

定位

    当view的大小已经设定完毕,则需要确定view在其父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方法,总共分为六个步骤:

  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的具体意义。

从Android源码分析View绘制

标签:

原文地址:http://www.cnblogs.com/cqumonk/p/4718788.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!