浅谈Android之Activity 窗口显示流程介绍(二)

来源:互联网 发布:免费制作网络结婚证 编辑:程序博客网 时间:2024/06/16 16:56

7.3 Activity Décorview布局(layout)

Measure确定Décor View以及child views的大小,layout则是确定child view在其parent view中的显示区域,只有layout结束,view的left,right,top,bottom值才会被设置,getWidth和getHeight两个函数才会返回view最终的宽高值

 

对FrameLayout来说,由于child view的大小确定了,那么再确定它们在parentview中的显示区域其实通过parent view的padding值,child view的margin值以及宽高就可以计算出

 

当然RelativeLayout和LinearLayout会相对复杂点,因为childviews之间会存在布局关联,本文只对FrameLayout的实现做简单介绍,至于RelativeLayout和LinearLayout大家可自行看代码分析

 

在ViewRootImpl中调用performMeasure完成对Décor View的measure后,接着调用

PerformLayout触发布局操作,其内部主要调用:

host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

Host变量保存的就是Décor View,layout的四个变量依次对应left,top,right,bottom

 

FrameLayout和ViewGroup都没有对layout做处理,接着直接看View中的默认实现:

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);

            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;

}

这个函数先调用setFrame保存位置数据,然后调用onLayout进行布局操作,最后看这个view是否有设置layoutchangelistener,如果有,调用回调通知布局已经发生改变

 

先看setFrame:

//View.java

protected boolean setFrame(int left, int top, int right, int bottom) {

        boolean changed = false;

 

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {

            changed = true;

            int drawn = mPrivateFlags & PFLAG_DRAWN;

 

            int oldWidth = mRight - mLeft;

            int oldHeight = mBottom - mTop;

            int newWidth = right - left;

            int newHeight = bottom - top;

            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

 

            // Invalidate our old position

            invalidate(sizeChanged);

 

            mLeft = left;

            mTop = top;

            mRight = right;

            mBottom = bottom;

            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

 

            mPrivateFlags |= PFLAG_HAS_BOUNDS;

            if (sizeChanged) {

                sizeChange(newWidth, newHeight, oldWidth, oldHeight);

            }

 

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {

                mPrivateFlags |= PFLAG_DRAWN;

                invalidate(sizeChanged);

                invalidateParentCaches();

            }

            // Reset drawn bit to original value (invalidate turns it off)

            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;

            notifySubtreeAccessibilityStateChangedIfNeeded();

        }

        return changed;

    }

如果left,right,top,bottom值都没变,那就说明布局没变,返回changed为false,如果变了,则将对应的值都保存到mLeft,mRight,mTop, mBottom,然后调用invalidate通知view进行重绘

 

接着看onLayout的实现:

//FrameLayout.java

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

        layoutChildren(left, top, right, bottom, false /* no force left gravity */);

}

直接调用layoutChildren:

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();

 

        mForegroundBoundsChanged = true;

       

        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();

                final int height = child.getMeasuredHeight();

 

                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;

 

                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;

                }

 

                switch (verticalGravity) {

                    case Gravity.TOP:

                        childTop = parentTop + lp.topMargin;

                        break;

                    case Gravity.CENTER_VERTICAL:

                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +

                        lp.topMargin - lp.bottomMargin;

                        break;

                    case Gravity.BOTTOM:

                        childTop = parentBottom - height - lp.bottomMargin;

                        break;

                    default:

                        childTop = parentTop + lp.topMargin;

                }

 

                child.layout(childLeft, childTop, childLeft + width, childTop + height);

            }

        }

}

逻辑很简单,通过拿到child view的measure width和measure height,然后根据gravity和margin值计算出child view在parent view中的left,right,top,bottom值,最后调用

child.layout

 

如果child view是ViewGroup,则重走本节流程,依次反复执行,直到全部child view都layout完成为止

 

7.4 Activity Décorview绘制(draw)

Layout结束后,每一个child view在其parent view的位置都已经固定,接下去就可以开始绘制child view的图形数据了

 

在ViewRootImpl中调用performLayout完成布局后,接着调用performDraw,其最终会调用drawSoftware,该函数会通过mSurface拿到Canvas,接着调用mView.draw()并传入canvas开始Décor view的绘制

 

由于Decor view的所有child views是共用Decor view这一块top canvas的,对每一个view来说,它们在绘制其child view或者content data时,最简单的当然是使用相对坐标,也就是将其左上角设置为坐标原点,这样就可以让view只需专注于child view或者content data的绘制,而无需关心坐标原点的调整以及恢复

 

如果把Décor view作为世界地图,那top canvas肯定拥有初始的单位矩阵, childview或者content data就是世界中的显示元素,元素显示位置和方式的调整,都是通过矩阵(matrix)的配置来实现的

 

后续View绘制代码的分析,为了简化逻辑以便于理解,我们假定系统不支持硬件加速,也就是mAttachInfo.mHardwareAccelerated为false

 

Draw(Canvas canvas)是View绘制的入口函数,它跟measure和layout一样,基于view tree做完整的递归调用,它会按顺序做如下事情

1)  绘制view的background

2)  调用onDraw绘制view的content

3)  调用dispatchDraw绘制view的children

4)  绘制scrollbar等

 

其中1, 2, 4都是基于传入的canvas做界面绘制,那canvas的坐标切换肯定是由第3步

dispatchDraw来完成了

 

dispatchDraw的默认实现是空的,也就是啥也没做,只有ViewGroup重新实现了改方法,也就是说,这个方法,只会ViewGroup有效

 

接着我们看其在ViewGroup中的实现:

//ViewGroup.java

protected void dispatchDraw(Canvas canvas) {

        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);

        final int childrenCount = mChildrenCount;

        final View[] children = mChildren;

        int flags = mGroupFlags;

        ……

        int clipSaveCount = 0;

        final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;

        if (clipToPadding) {

            clipSaveCount = canvas.save();

            canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,

                    mScrollX + mRight - mLeft - mPaddingRight,

                    mScrollY + mBottom - mTop - mPaddingBottom);

        }

 

        // We will draw our child's animation, let's reset the flag

        mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;

        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;

 

        ……

 

        for (int i = 0; i < childrenCount; i++) {

            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;

            final View child = (preorderedList == null)

                    ? children[childIndex] : preorderedList.get(childIndex);

            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {

                more |= drawChild(canvas, child, drawingTime);

            }

        }

       ……

}

 

由于mGroupFlags在initViewGroup时被设置了FLAG_CLIP_TO_PADDING,所以这里

clipToPadding肯定为true,接着调用canvas .clipRect根据padding和scroll值来对canvas的操作区域进行裁剪,接着遍历所有childview,依次调用drawChild

 

接着看drawChild的实现:

//ViewGroup.java

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {

        return child.draw(canvas, this, drawingTime);

}

 

直接调用child.draw函数,这个函数也在View.java默认被实现,跟上头提过的draw(canvas)函数不同的是,它有三个参数,除了canvas外,还传入了parent viewgroup和drawing time:

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

        boolean usingRenderNodeProperties = mAttachInfo != null &&mAttachInfo.mHardwareAccelerated;

        boolean more = false;

        final boolean childHasIdentityMatrix = hasIdentityMatrix();

        final int flags = parent.mGroupFlags;

        ……

        final boolean hasNoCache = cache == null || hasDisplayList;

        final boolean offsetForScroll = cache == null && !hasDisplayList &&

                layerType != LAYER_TYPE_HARDWARE;

 

        int restoreTo = -1;

        if (!usingRenderNodeProperties || transformToApply != null) {

            restoreTo = canvas.save();

        }

        if (offsetForScroll) {

            canvas.translate(mLeft - sx, mTop - sy);

        } else {

            if (!usingRenderNodeProperties) {

                canvas.translate(mLeft, mTop);

            }

            if (scalingRequired) {

                if (usingRenderNodeProperties) {

                    // TODO: Might not need this if we put everything inside the DL

                    restoreTo = canvas.save();

                }

                // mAttachInfo cannot be null, otherwise scalingRequired == false

                final float scale = 1.0f / mAttachInfo.mApplicationScale;

                canvas.scale(scale, scale);

            }

        }

        ……

        if (!usingRenderNodeProperties) {

            // apply clips directly, since RenderNode won't do it for this draw

            if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN

                    && cache == null) {

                if (offsetForScroll) {

                    canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop));

                } else {

                    if (!scalingRequired || cache == null) {

                        canvas.clipRect(0, 0, mRight - mLeft, mBottom - mTop);

                    } else {

                        canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());

                    }

                }

            }

 

            if (mClipBounds != null) {

                // clip bounds ignore scroll

                canvas.clipRect(mClipBounds);

            }

        }

        ……

            if (!layerRendered) {

                if (!hasDisplayList) {

                    // Fast path for layouts with no backgrounds

                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {

                        mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                        dispatchDraw(canvas);

                    } else {

                        draw(canvas);

                    }

                } else {

                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                    ((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);

                }

            }

        } else if (cache != null) {

            ……

        }

 

        if (restoreTo >= 0) {

            canvas.restoreToCount(restoreTo);

        }

        ……

        mRecreateDisplayList = false;

        return more;

    }

 

drawingTime和parent viewgroup的作用在下一节再介绍

 

由于在layout的时候,view在其parent view中的top-left坐标已经确定,所以这里的坐标切换相对来说就非常简单,首先调用canvas.save()备份canvas当前的matrix/clip数据,接着调用canvas.translate(mLeft,mTop)进行原点切换,最后调用draw(canvas)让view在当前canvas上进行内容绘制,绘制成功后,调用canvas.restoreToCount(restoreTo)恢复之前canvas备份的matrix/clip数据

 

7.5 ViewAnimation原理介绍

通过上一节我们知道,View在parent view中的位置或者展示方式是通过matrix来完成的,

那如果我在view每一次绘制的时候,按照一定的规律更改matrix的值,这样就可以达到view在parent view的一个序列图形显示效果,也就是说一个针对view的动画效果就出来了,这个好像是废话,动态视图不都是这么来的么^_^

 

Android提供了TranslateAnimation等类用来封装对每一帧对应matrix的计算

 

接下去通过代码做下简单介绍,为view设置并开始播放animation

TranslateAnimation tAnim = new TranslateAnimation(0, 400, 0, 0);

tAnim.setDuration(2000); 

           

view.startAnimation(tAnim);

 

创建TranslateAnimation,然后调用startAnimation开发播放

//View.java

public void startAnimation(Animation animation) {

        animation.setStartTime(Animation.START_ON_FIRST_FRAME);

        setAnimation(animation);

        invalidateParentCaches();

        invalidate(true);

}

 

先通过animation.setStartTime(Animation.START_ON_FIRST_FRAME)设置animation的启动时间为Animation.START_ON_FIRST_FRAME,也就是说,animation的第一帧被绘制的时间,即为start time

 

然后调用setAnimation(animation)将该animation设置为viewmCurrentAnimation

最后调用invalidate(true)强制view重画。

 

View重画后,上一节说过,带有parentdrawingTimedraw函数会被调用:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

        ……

        Transformation transformToApply = null;

        boolean concatMatrix = false;

……

        final Animation a = getAnimation();

        if (a != null) {

            more = drawAnimation(parent, drawingTime, a, scalingRequired);

            concatMatrix = a.willChangeTransformationMatrix();

            if (concatMatrix) {

                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;

            }

            transformToApply = parent.getChildTransformation();

        } else {

            ……

        }

        concatMatrix |= !childHasIdentityMatrix;

       ……

        float alpha = usingRenderNodeProperties ? 1 : (getAlpha() * getTransitionAlpha());

        if (transformToApply != null || alpha < 1 || !hasIdentityMatrix() ||

                (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) == PFLAG3_VIEW_IS_ANIMATING_ALPHA) {

            if (transformToApply != null || !childHasIdentityMatrix) {

                int transX = 0;

                int transY = 0;

 

                if (offsetForScroll) {

                    transX = -sx;

                    transY = -sy;

                }

 

                if (transformToApply != null) {

                    if (concatMatrix) {

                        if (usingRenderNodeProperties) {

                            renderNode.setAnimationMatrix(transformToApply.getMatrix());

                        } else {

                            // Undo the scroll translation, apply the transformation matrix,

                            // then redo the scroll translate to get the correct result.

                            canvas.translate(-transX, -transY);

                            canvas.concat(transformToApply.getMatrix());

                            canvas.translate(transX, transY);

                        }

                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;

                    }

 

                    ……

            }

          ……

if (a != null && !more) {

            if (!hardwareAccelerated && !a.getFillAfter()) {

                onSetAlpha(255);

            }

            parent.finishAnimatingView(this, a);

        }

           ……

        return more;

    }

 

drawingTime,其实就是指这次draw操作的触发时间,也就是ViewRootImpl调用

drawSoftware的时间:

//ViewRootImpl.java

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,

            boolean scalingRequired, Rect dirty) {

……

            attachInfo.mDrawingTime = SystemClock.uptimeMillis();

        ……

        return true;

 }

 

至于parent ViewGroup,这边看到,其内部会有一个变量mChildTransformation保存当前某一child viewanimation对应的transformation片段数据

 

draw函数中,先通过调用drawAnimation并传入parentanimationdrawingTime来来得到当前的时间下对应的transformation片段数据,并保存到parent ViewGroup

mChildTransformation中,然后drawAnimation内部还会判断在当前transformation片段数据执行完后,是否还存在animation片段数据?如果有,则会调用invalidate触发view的下一次绘制,这样才能保持animation不会中断

 

draw函数,接着调用parent.getChildTransformation()获取当前的transformation数据,然后获取其Matrix并设置到canvas

 

最后判断more是否为false,如果是false,说明animation已经结束,调用

parent.finishAnimatingView(this,a)执行animation结束相关扫尾代码

 

由于android View都是在主线程的完成绘制的,而主线程的负载又不是均衡的,所以会导致View animation的帧率无法得到保障,当然,对于短时的动画来说影响不大,但是对于长时间并且比较复杂的动画,建议还是使用Surface来绘制,然后开一条线程来按照你想要的帧率来触发重绘

0 0