Android view 工作流程《下》

本文详细解析了Android系统中视图的测量、布局与绘制流程,包括ViewRoot的performTraversals方法执行过程,以及View类中的measure、layout和draw方法的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

遍历View树performTraversals()执行过程
view树遍历概述
还是回到ViewRoot.Java,我们直接看performTraversals(),该函数就是Android系统View树遍历工作的核心。一眼看去,发现这个函数挺长的,但是逻辑是非常清晰的,其执行过程可简单概括为根据之前所有设置好的状态,判断是否需要计算视图大小(measure)、是否需要重新安置视图的位置(layout),以及是否需要重绘(draw)视图,可以用以下图来表示该流程。
这里写图片描述

private void performTraversals() {  
        // cache mView since it is used so much below...  
//1 处理mAttachInfo的初始化,并根据resize、visibility改变的情况,给相应的变量赋值。  
        final View host = mView;  
        final View.AttachInfo attachInfo = mAttachInfo;  
        final int viewVisibility = getHostVisibility();  
        boolean viewVisibilityChanged = mViewVisibility != viewVisibility  
                || mNewSurfaceNeeded;  
        float appScale = mAttachInfo.mApplicationScale;  
        WindowManager.LayoutParams params = null;  
        if (mWindowAttributesChanged) {  
            mWindowAttributesChanged = false;  
            surfaceChanged = true;  
            params = lp;  
        }  
        Rect frame = mWinFrame;  
        if (mFirst) {  

            // For the very first time, tell the view hierarchy that it  
            // is attached to the window.  Note that at this point the surface  
            // object is not initialized to its backing store, but soon it  
            // will be (assuming the window is visible).  
            attachInfo.mSurface = mSurface;  
            attachInfo.mUse32BitDrawingCache = PixelFormat.formatHasAlpha(lp.format) ||  
                    lp.format == PixelFormat.RGBX_8888;  
            attachInfo.mHasWindowFocus = false;  
            attachInfo.mWindowVisibility = viewVisibility;  
            ......  
        }   
//2 如果mLayoutRequested判断为true,那么说明需要重新layout,不过在此之前那必须重新measure。  
        if (mLayoutRequested) {  
            // Execute enqueued actions on every layout in case a view that was detached  
            // enqueued an action after being detached  
            getRunQueue().executeActions(attachInfo.mHandler);  

            if (mFirst) {  
                ......  
            }   
        }  
//3 判断是否有子视图的属性发生变化,ViewRoot需要获取这些变化。  
        if (attachInfo.mRecomputeGlobalAttributes) {  
            ......  
        }  

        if (mFirst || attachInfo.mViewVisibilityChanged) {  
            ......  
        }  


//4 根据上面得到的变量数值,确定我们的view需要多大尺寸才能装下。于是就得measure了,有viewgroup的weight属性还得再做些处理  
                 // Ask host how big it wants to be  
                host.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
                mLayoutRequested = true;  
            }  
        }  
//5 measure完毕,接下来可以layout了。  
        final boolean didLayout = mLayoutRequested;  
        boolean triggerGlobalLayoutListener = didLayout  
                || attachInfo.mRecomputeGlobalAttributes;  
        if (didLayout) {  
            host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);  

        }  

//6 如果mFirst为true,那么会进行view获取焦点的动作。  
        if (mFirst) {  
            mRealFocusedView = mView.findFocus();  
        }  

        boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw();  
//7 终于,来到最后一步,前面的工作可以说都是铺垫,都是为了draw而准备的。  
        if (!cancelDraw && !newSurface) {  
            mFullRedrawNeeded = false;  
            draw(fullRedrawNeeded);  
}  

下面我们就来会会view那三部曲吧。
计算视图大小(measure)的过程
整个view视图的Measure过程就是一个量体裁衣,按需分配的过程。看一下以下的递归过程:
这里写图片描述
从上图可以看出,measure过程始于ViewRoot的host.measure(),调的就是view类的measure()函数,该函数然后回调onMeasure。如果host对象是一个ViewGroup实例,一般会重载onMeasure,如果没有的话,则会执行view类中默认的onMeasure。合理的情况是编程人员重载onMeasure并逐一对里面的子view进行measure。我们可以看一下view的measure方法:

/** 
   * 该方法在需要确定view所需尺寸大小时调用,父视图会提供宽和高的属性约束。 
   * 具体视图完全可以在onMeasure中改变这些。 
   * @see #onMeasure(int, int) 
   */  
  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
      if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
              widthMeasureSpec != mOldWidthMeasureSpec ||  
              heightMeasureSpec != mOldHeightMeasureSpec) {  

          // 首先清除dimension的设值  
          mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
          // measure 自己, 并设置dimension  
          onMeasure(widthMeasureSpec, heightMeasureSpec);  
          // 抛出未设值flag的异常  
          if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
              throw new IllegalStateException("onMeasure() did not set the"  
                      + " measured dimension by calling"  
                      + " setMeasuredDimension()");  
          }  
          mPrivateFlags |= LAYOUT_REQUIRED;  
      }  
      mOldWidthMeasureSpec = widthMeasureSpec;  
      mOldHeightMeasureSpec = heightMeasureSpec;  
  }  

这里强烈建议去看看viewGroup实例FrameLayout和LinearLayout的onMeasure方法,一定会有所感悟的,尤其是LinerLayout的。这样对于viewGroup的专用标签pading和weight也会有新的体会。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:orientation="vertical"  
    android:layout_width="fill_parent"  
    android:layout_height="100dip"  
    >  
<TextView    
    android:layout_width="fill_parent"   
    android:layout_height="20dip"   
    android:layout_weight="2"  
    android:text="@string/hello"  
    />  
    <ListView  
    android:layout_width="fill_parent"  
    android:layout_height="fill_parent"  
    android:background="#ff00ff00"  
    ></ListView>  
</LinearLayout>  

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:orientation="vertical"  
    android:layout_width="fill_parent"  
    android:layout_height="100dip"  
    >  
<TextView    
    android:layout_width="fill_parent"   
    android:layout_height="60dip"   
    android:layout_weight="2"  
    android:text="@string/hello"  
    />  
    <ListView  
    android:layout_width="fill_parent"  
    android:layout_height="0dip"  
    android:layout_weight="2"  
    android:background="#ff00ff00"  
    ></ListView>  
</LinearLayout>  

请问以上两布局有无不同,能否自行画出?
布局(layout)过程
执行完measure过程,也就是说各个view的大小尺寸已经登记在案,现在它们要确定的是自己应该置身于何处,也就是摆放在哪里。好吧,这个就是layout的职责所在,让父视图按照子视图的大小及布局参数,将子视图放置在合适的位置上。

同样需要看下以下流程图:
这里写图片描述

public void layout(int l, int t, int r, int b) {  
        int oldL = mLeft;  
        int oldT = mTop;  
        int oldB = mBottom;  
        int oldR = mRight;  
//调用setFrame()函数给当前视图设置参数中指定的位置  
        boolean changed = setFrame(l, t, r, b);  
        if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
            if (ViewDebug.TRACE_HIERARCHY) {  
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
            }  
//回调onLayout()函数  
            onLayout(changed, l, t, r, b);  
            mPrivateFlags &= ~LAYOUT_REQUIRED;  
//4.0新增监听可捕获layout变化  
            if (mOnLayoutChangeListeners != null) {  
                ArrayList<OnLayoutChangeListener> listenersCopy =  
                        (ArrayList<OnLayoutChangeListener>) 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);  
                }  
            }  
        }  
//layout完成,清楚标签  
        mPrivateFlags &= ~FORCE_LAYOUT;  
    }  

view中的该layout函数流程大概如下:

1,调用setFrame()将位置参数保存,这些参数会保存到view内部变量(mLeft,mTop,mRight,mButtom)。如果有数值的变化那么会调用invalidate()重绘那些区域。

2,回调onLayout(),View中定义的onLayout()函数默认什么都不做,View系统提供onLayout函数的目的是为了使系统包含有子视图的父视图能够在onLayout()函数对子视图进行位置分配,正因为这样,如果是viewGroup类型,就必须重载onLayout(),由此ViewGroup的onLayout为abstract也就很好解释了。

3,清楚mPrivateFlags中的LAYOUT_REQUIRED标记,因为layout的操作已经完成了。

为了对layout过程有更深的体会,有必要针对特定的一种viewGroup进行分析,我们还是把魔爪伸向linearLayout,看看它的onMeasure,onMeasure中会根据mOrientation变量,进行不同的layout,我们看一种就行了:

void layoutVertical() {  
        final int paddingLeft = mPaddingLeft;  
        int childTop;  
        int childLeft;         
        // 获得子视图可以用的宽度,顺便也把子视图左边沿的位置计算出来。  
        final int width = mRight - mLeft;  
        int childRight = width - mPaddingRight;       
        // Space available for child  
        int childSpace = width - paddingLeft - mPaddingRight;  

        final int count = getVirtualChildCount();  
//根据父视图中的gravity属性,决定子视图的起始位置。  
        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;  
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;  
        switch (majorGravity) {  
           case Gravity.BOTTOM:  
               // mTotalLength contains the padding already  
               childTop = mPaddingTop + mBottom - mTop - mTotalLength;  
               break;  
               // mTotalLength contains the padding already  
           case Gravity.CENTER_VERTICAL:  
               childTop = mPaddingTop + (mBottom - mTop - mTotalLength) / 2;  
               break;  
           case Gravity.TOP:  
           default:  
               childTop = mPaddingTop;  
               break;  
        }  
//遍历所有子视图,为它们分配位置.setChildFrame()结果还是会调用child.layout()为子视图设置布局位置.  
        for (int i = 0; i < count; i++) {  
            final View child = getVirtualChildAt(i);  
            if (child == null) {  
                childTop += measureNullChild(i);  
    ......  
                childTop += lp.topMargin;  
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),  
                        childWidth, childHeight);  
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);  
                i += getChildrenSkipCount(child, i);  
            }  
        }  
    }  

绘制(draw)过程
这是见证奇迹的时刻,draw过程就是要把view对象绘制到屏幕上,如果它是一个viewGroup,则需要递归绘制它所有的子视图。视图中有哪些元素是需要绘制的呢?

1,view背景。所有view都会有一个背景,可以是一个颜色值,也可以是一张背景图片

2,视图本身内容。比如TextView的文字

3,渐变边框。就是一个shader对象,让视图看起来更具有层次感

4,滚动条。

按照惯例,还是先看一下绘制的总体流程吧:
这里写图片描述
眼尖的同学应该发现了,上面这张图比前面的measure和layout多了一步:draw()。在performTraversals()函数中调用的是viewRoot的draw()函数,在该函数中进行一系列的前端处理后,再调用host.draw()。

一般情况下,View对象不应该重载draw()函数,因此,host.draw()就是view.draw()。该函数内部过程也就是View系统绘制过程的核心过程,该函数中会依次绘制前面所说哦四种元素,其中绘制视图本身的具体实现就是回调onDraw()函数,应用程序一般也会重载onDraw()函数以绘制所设计的View的真正界面内容。

Google源码的注释太nice了,我加任何说辞都显得多余,就不画蛇添足了:

public void draw(Canvas canvas) {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);  
        }  

        final int privateFlags = mPrivateFlags;  
        final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&  
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);  
        mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;  

        /* 
         * Draw traversal performs several drawing steps which must be executed 
         * in the appropriate order: 
         * 
         *      1. Draw the background 
         *      2. If necessary, save the canvas' layers to prepare for fading 
         *      3. Draw view's content 
         *      4. Draw children 
         *      5. If necessary, draw the fading edges and restore layers 
         *      6. Draw decorations (scrollbars for instance) 
         */  

        // Step 1, draw the background, if needed  
        int saveCount;  

        if (!dirtyOpaque) {  
            final Drawable background = mBGDrawable;  
            if (background != null) {  
                final int scrollX = mScrollX;  
                final int scrollY = mScrollY;  

                if (mBackgroundSizeChanged) {  
                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);  
                    mBackgroundSizeChanged = false;  
                }  

                if ((scrollX | scrollY) == 0) {  
                    background.draw(canvas);  
                } else {  
                    canvas.translate(scrollX, scrollY);  
                    background.draw(canvas);  
                    canvas.translate(-scrollX, -scrollY);  
                }  
            }  
        }  

        // skip step 2 & 5 if possible (common case)  
        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  
            if (!dirtyOpaque) onDraw(canvas);  

            // Step 4, draw the children  
            dispatchDraw(canvas);  

            // Step 6, draw decorations (scrollbars)  
            onDrawScrollBars(canvas);  

            // we're done...  
            return;  
        }  

        /* 
         * Here we do the full fledged routine... 
         * (this is an uncommon case where speed matters less, 
         * this is why we repeat some of the tests that have been 
         * done above) 
         */  

        boolean drawTop = false;  
        boolean drawBottom = false;  
        boolean drawLeft = false;  
        boolean drawRight = false;  

        float topFadeStrength = 0.0f;  
        float bottomFadeStrength = 0.0f;  
        float leftFadeStrength = 0.0f;  
        float rightFadeStrength = 0.0f;  

        // Step 2, save the canvas' layers  
        int paddingLeft = mPaddingLeft;  

        final boolean offsetRequired = isPaddingOffsetRequired();  
        if (offsetRequired) {  
            paddingLeft += getLeftPaddingOffset();  
        }  

        int left = mScrollX + paddingLeft;  
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;  
        int top = mScrollY + getFadeTop(offsetRequired);  
        int bottom = top + getFadeHeight(offsetRequired);  

        if (offsetRequired) {  
            right += getRightPaddingOffset();  
            bottom += getBottomPaddingOffset();  
        }  

        final ScrollabilityCache scrollabilityCache = mScrollCache;  
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;          
        int length = (int) fadeHeight;  

        // clip the fade length if top and bottom fades overlap  
        // overlapping fades produce odd-looking artifacts  
        if (verticalEdges && (top + length > bottom - length)) {  
            length = (bottom - top) / 2;  
        }  

        // also clip horizontal fades if necessary  
        if (horizontalEdges && (left + length > right - length)) {  
            length = (right - left) / 2;  
        }  

        if (verticalEdges) {  
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));  
            drawTop = topFadeStrength * fadeHeight > 1.0f;  
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));  
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;  
        }  

        saveCount = canvas.getSaveCount();  

        int solidColor = getSolidColor();  
        if (solidColor == 0) {  
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;  

            if (drawTop) {  
                canvas.saveLayer(left, top, right, top + length, null, flags);  
            }  

            if (drawBottom) {  
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);  
            }  
        } else {  
            scrollabilityCache.setFadeColor(solidColor);  
        }  

        // Step 3, draw the content  
        if (!dirtyOpaque) onDraw(canvas);  

        // Step 4, draw the children  
        dispatchDraw(canvas);  

        // Step 5, draw the fade effect and restore layers  
        final Paint p = scrollabilityCache.paint;  
        final Matrix matrix = scrollabilityCache.matrix;  
        final Shader fade = scrollabilityCache.shader;  

        if (drawTop) {  
            matrix.setScale(1, fadeHeight * topFadeStrength);  
            matrix.postTranslate(left, top);  
            fade.setLocalMatrix(matrix);  
            canvas.drawRect(left, top, right, top + length, p);  
        }  
。。。。。  
        canvas.restoreToCount(saveCount);  
        // Step 6, draw decorations (scrollbars)  
        onDrawScrollBars(canvas);  
    }  

绘制完界面内容后,如果该视图还包含子视图,则调用dispatchDraw()函数,实际上起作用的还是viewGroup的dispatchDraw()函数。需要说明的是应用程序不应该再重载ViewGroup中该方法,因为它已经有了默认而且标准的view系统流程。dispatchDraw()内部for循环调用drawChild()分别绘制每一个子视图,而drawChild()内部又会调用draw()函数完成子视图的内部绘制工作

protected void dispatchDraw(Canvas canvas) {  
        final int count = mChildrenCount;  
        final View[] children = mChildren;  
        int flags = mGroupFlags;  
//1 判断mGroupFlags是否设有FLAG_RUN_ANIMATION标识并且不为0.该layout动画指的是加载或移除子视图时候呈现的动画.  
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {  
            final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;  
            final boolean buildCache = !isHardwareAccelerated();//硬件加速,4.0加入.  
            for (int i = 0; i < count; i++) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {  
                    final LayoutParams params = child.getLayoutParams();  
                    attachLayoutAnimationParameters(child, params, i, count);  
                    bindLayoutAnimation(child);  
            }  

        }  
//2 处理padding属性,如果该viewGroup有设置.  
        int saveCount = 0;  
        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;  
        if (clipToPadding) {  
            saveCount = canvas.save();  
            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,  
                    mScrollX + mRight - mLeft - mPaddingRight,  
                    mScrollY + mBottom - mTop - mPaddingBottom);  

        }  
//3 开始绘制子视图动画之前先清除flag.  
        // We will draw our child's animation, let's reset the flag  
        mPrivateFlags &= ~DRAW_ANIMATION;  
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;  

        boolean more = false;  
        final long drawingTime = getDrawingTime();  
//4 使用佛如循环,使viewGroup的子视图逐个调用drawChild函数.  
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {  
            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);  
                }  
            }  
        } else {  
            for (int i = 0; i < count; i++) {  
                final View child = children[getChildDrawingOrder(count, i)];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
                    more |= drawChild(canvas, child, drawingTime);  
                }  
            }  
        }  

//5 Draw any disappearing views that have animations  
        if (mDisappearingChildren != null) {  
            final ArrayList<View> disappearingChildren = mDisappearingChildren;  
            final int disappearingCount = disappearingChildren.size() - 1;  
            // Go backwards -- we may delete as animations finish  
            for (int i = disappearingCount; i >= 0; i--) {  
                final View child = disappearingChildren.get(i);  
                more |= drawChild(canvas, child, drawingTime);  
            }  
        }  

        if (clipToPadding) {  
            canvas.restoreToCount(saveCount);  
        }  

// mGroupFlags might have been updated by drawChild()  
        flags = mGroupFlags;  

        if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {  
            invalidate(true);  
        }  
    }  

整个view的工作流程那是相当复杂的,这需要静下心来,加以时日细细揣摩才能略有体会

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值