View的工作流程

戏说Android view 工作流程《下》 遍历View树performTraversals()执行过程 view树遍历概述 还是回到ViewRoot.java,我们直接看performTraversals(),该函数就是android系统View树遍历工作的核心。一眼看去,发现这个函数挺长的,但是逻辑是非常清晰的,其执行过程可简单概括为根据之前所有设置好的状态,判断是否需要计算视图大小(measure)、是否需要重新安置视 阅读详情

Android、View视图与坐标系
View的滑动和属性动画
从源码解析View的事件分发机制
View的工作流程
Android自定义view

View的工作流程

  View的工作流程,指的就是measure、layout和draw。其中,measure用来测量View的宽和高,layout用来确定View的位置,draw则用来绘制View。

View的工作流程入口

  上面的内容讲到了Activity的构成,最后讲到了DecorView的创建以及它加载的资源。这个时候DecorView的内容还无法显示,因为它还没有被加载到Window中。接下来我们来看看DecorView如何被加载到Window中。

DecorView被加载到Window中

  当DecorView创建完毕,要加载到Window中时,我们需要先了解一下Activity的创建过程。当我们调用Activity的startActivity方法时,最终是调用ActivityThread的handleLaunchActivity方法来创建Activity的,代码如下所示:

    private void handleLaunchActivity(ActivityClientRecord r, 
        Intent customIntent, String reason) {
       
        Activity a = performLaunchActivity(r, customIntent);//1
        if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, //2
                    r.lastProcessedSeq, reason);
        ...
    }

  上面代码注释 1 处调用 performLaunchActivity 方法来创建 Activity,在这里面会调用到Activity的onCreate方法,从而完成DecorView的创建。接着在上面代码注释2处调用handleResumeActivity方法,代码如下所示:

final void handleResumeActivity(IBinder token, boolean clearHide, 
        boolean isForward, boolean reallyResume, int seq, String reason) {
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;
    r = performResumeActivity(token, clearHide, reason);//1
    if (r != null) {
        final Activity a = r.activity;
        ...
         if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();//2
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();//3
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;

            if (a.mVisibleFromClient && !a.mWindowAdded) {
                 a.mWindowAdded = true;
               wm.addView(decor, l);//4
            }
            ...
}

  在上面代码注释1处的performResumeActivity方法中会调用Activity的onResume方法。接着往下看,注释
2处得到了DecorView。注释3处得到了WindowManager,WindowManager是一个接口并且继承了接口ViewManager。在注释4处调用WindowManager的addView方法,WindowManager 的实现类是
WindowManagerImpl,所以实际调用的是 WindowManagerImpl 的addView方法。具体代码如下所示:

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    
     @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
}

  在 WindowManagerImpl 的 addView 方法中,又调用了 WindowManagerGlobal 的 addView方法,代码如下所示:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ...            
    ViewRootImpl root;
    View panelParentView = null;            
    synchronized (mLock) {
        ...
         root = new ViewRootImpl(view.getContext(), display);//1

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
        
    }
    try {
        root.setView(view, wparams, panelParentView);//2
    } catch (RuntimeException e) {
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }            
}

  在上面代码注释1处创建了ViewRootImpl实例,在注释2处调用了ViewRootImpl的setView方法并将DecorView作为参数传进去,这样就把DecorView加载到了Window中。当然界面仍不会显示出什么来,因为View的工作流程还没有执行完,还需要经过measure、layout以及draw才会把View绘制出来。

ViewRootlmpl的PerformTraveals方法

  前面讲到了将 DecorView 加载到 Window 中,是通过 ViewRootImpl 的 setView 方法。ViewRootImpl还
有一个方法PerformTraveals,这个方法使得ViewTree开始View的工作流程,代码如下所示:

private void performTraversals() {
    ...
    if (!mStopped || mReportNextDraw) {
        if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
           || mHeight != host.getMeasuredHeight() || framesChanged || updatedConfiguration) {
            int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);//1
            int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//2
        }        
    }
    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
        ...
    }  
    if (!cancelDraw && !newSurface) {
        if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).startChangingAnimations();
            }
            mPendingTransitions.clear();
        }
        performDraw();
    }
    ...
}

  这里面主要执行了3个方法,分别是performMeasure、performLayout和performDraw,在其方法的内部又会分别调用View的measure、layout和draw方法。需要注意的是,performMeasure方法中需要传入两个参数,分别是 childWidthMeasureSpec 和 childHeightMeasureSpec。要了解这两个参数,需要了解MeasureSpec。

理解MeasureSpec

  MeasureSpec是View的内部类,其封装了一个View的规格尺寸,包括View的宽和高的信息,它的作用是在Measure流程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。MeasureSpec的代码如下所示:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;

    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) 
                int size,  @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }

    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        if (mode == UNSPECIFIED) {
            return makeMeasureSpec(size, UNSPECIFIED);
        }
        size += delta;
        if (size < 0) {
                Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                        ") spec: " + toString(measureSpec) + " delta: " + delta);
                size = 0;
        }
        return makeMeasureSpec(size, mode);
    }

    
    public static String toString(int measureSpec) {
        int mode = getMode(measureSpec);
        int size = getSize(measureSpec);

        StringBuilder sb = new StringBuilder("MeasureSpec: ");

        if (mode == UNSPECIFIED)
            sb.append("UNSPECIFIED ");
        else if (mode == EXACTLY)
            sb.append("EXACTLY ");
        else if (mode == AT_MOST)
            sb.append("AT_MOST ");
        else
            sb.append(mode).append(" ");
        sb.append(size);
        return sb.toString();
    }
}

  从MeasureSpec的常量可以看出,它代表了32位的int值,其中高2位代表了SpecMode,低30位则代表SpecSize。SpecMode指的是测量模式,SpecSize指的是测量大小。SpecMode有3种模式,如下所示。

  • UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
  • AT_MOST:最大模式,对应于wrap_comtent属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。
  • EXACTLY:精确模式,对应于 match_parent 属性和具体的数值,父容器测量出 View所需要的大小,也就是SpecSize的值。

  对于每一个View,都持有一个MeasureSpec,而该MeasureSpec则保存了该View的尺寸规格。在View的测量流程中,通过makeMeasureSpec来保存宽和高的信息。通过getMode或getSize得到模式和宽、高。MeasureSpec是受自身LayoutParams和父容器的MeasureSpec共同影响的。作为顶层View的DecorView来说,其并没有父容器,那么它的MeasureSpec是如何得来的呢?为了解决这个疑问,我们再回到ViewRootImpl的PerformTraveals方法,代码注释 1 处调用了 getRootMeasureSpec(mWidth,lp.width)方法。下面来查看这方法做了什么:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

  getRootMeasureSpec方法的第一个参数windowSize指的是窗口的尺寸,所以对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定,这一点和普通View是不同的。接着往下看,就会看到根据自身的LayoutParams来得到不同的MeasureSpec。performMeasure方法中需要传入两个参数,即childWidthMeasureSpec和childHeightMeasureSpec,这代表什么我们也应该明白了。接着回到PerformTraveals方法,查看在注释2处的performMeasure方法内部做了什么,
代码如下所示:

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

  其实,就算不看我们也应该知道里面调用了什么——是View的measure方法。接下来我们学习一下View的measure流程。

View的measure流程

  measure 用来测量 View 的宽和高,它的流程分为 View 的 measure 流程和 ViewGroup 的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量,还要遍历地调用子元素的measure()方法。

View的measure流程

  首先来看一下View的onMeasure方法:

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

  接着查看setMeasuredDimension方法,代码如下所示:

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

  这很显然是用来设置View的宽、高的,再回头看看getDefaultSize()方法处理了什么:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

  SpecMode是View的测量模式,而SpecSize是View的测量大小,上面的内容让我们也了解了MeasureSpec。这里很显然根据不同的SpecMode值来返回不同的result值,也就是SpecSize。在AT_MOST和EXACTLY模式
下,都返回SpecSize这个值,即View在这两种模式下的测量宽和高直接取决于SpecSize。也就是说,对于一个直接继承自View的自定义View来说,它的wrap_content 和 match_parent 属性的效果是一样的。因此如果
要实现自定义 View 的wrap_content,则要重写onMeasure方法,并对自定义View的wrap_content属性进行处理。而在 UNSPECIFIED 模式下返回的是 getDefaultSize 方法的第一个参数 size 的值,size 的值从onMeasure
方法来看是getSuggestedMinimumWidth方法或者getSuggestedMinimumHeight方法得到的。我们来查看getSuggestedMinimumWidth方法做了什么。只需要弄懂getSuggestedMinimumWidth方法就可以了,因为这两个方法的原理是一样的。

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : 
            max(mMinWidth, mBackground.getMinimumWidth());
    }

  如果 View 没有设置背景,则取值为 mMinWidth,mMinWidth 是可以设置的,它对应于Android:minWidth这个属性设置的值或者View的setMinimumWidth的值;如果不指定的话,则默认为0。setMinimumWidth方法如下所示:

    public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();
    }

  如果View设置了背景,则取值为max(mMinWidth,mBackground.getMinimumWidth()),也就是取mMinWidth和mBackground.getMinimumWidth()之间的最大值。此前讲了mMinWidth,下面看看mBackground.getMinimumWidth()。这个mBackground是Drawable类型的,Drawable类的getMinimumWidth 方法如下所示:

    public int getMinimumWidth() {
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }

  intrinsicWidth得到的是这个Drawable的固有宽度,如果固有宽度大于0则返回固有宽度,否则返回0。总结一下,getSuggestedMinimumWidth方法就是:如果View没有设置背景,则返回mMinWidth;如果设置了背景,就返回mMinWidth和Drawable的最小宽度之间的最大值。

ViewGroup的measure流程

  讲完了View的measure流程,接下来看看ViewGroup的measure流程。对于ViewGroup,它不只要测量自身,还要遍历地调用子元素的measure()方法。ViewGroup中没有定义onMeasure()方法,但却定义了measureChildren()方法:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

  遍历子元素并调用measureChild方法,measureChild方法如下所示:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

  调用 child.getLayoutParams()方法来获得子元素的 LayoutParams 属性,获取子元素的MeasureSpec 并调用子元素的 measure()方法进行测量。getChildMeasureSpec()方法里写了什么呢?其代码如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    switch (specMode) {
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;//1
            }
            break;

        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

  很显然,这是根据父容器的MeasureSpec模式再结合子元素的LayoutParams属性来得出的子元素的 MeasureSpec 属性。有一点需要注意的是,如果父容器的 MeasureSpec 属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT,那根据上面代码注释1处的代码,我们会发现子元素的MeasureSpec
属性也为AT_MOST,它的SpecSize值为父容器的SpecSize减去padding的值。换句话说,这和子元素设置LayoutParams属性为MATCH_PARENT效果是一样的。为了解决这个问题,需要在LayoutParams属性为WRAP_CONTENT时指定一下默认的宽和高。ViewGroup并没有提供onMeasure方法,而是让其子类来各自实现测量的方法,究其原因就是ViewGroup有不同布局的需要,很难统一。接下来我们简单分析一下ViewGroup的子类LinearLayout的measure流程。现在先来看看它的onMeasure方法,代码如下所示:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

  这个方法的逻辑很简单,如果是垂直方向则调用 measureVertical 方法,否则就调用measureHorizontal方法。接着分析垂直measureVertical()方法的部分源码:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    mTotalLength = 0;
    ...
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
        }
    ...
    if (useLargestChild && (heightMode == MeasureSpec.AT_MOST || 
            heightMode == MeasureSpec.UNSPECIFIED)) {
        mTotalLength = 0;

        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }
    
        if (child.getVisibility() == GONE) {
            i += getChildrenSkipCount(child, i);
            continue;
        }

        final LinearLayout.LayoutParams lp = 
                    (LinearLayout.LayoutParams) child.getLayoutParams();
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + largestChildHeight + 
        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
        }
    }

    mTotalLength += mPaddingTop + mPaddingBottom;

    int heightSize = mTotalLength;
    ...
}

  这里定义了mTotalLength用来存储LinearLayout在垂直方向的高度,然后遍历子元素,根据子元素的
MeasureSpec模式分别计算每个子元素的高度。如果是WRAP_CONTENT,则将每个子元素的高度和margin垂直高度等值相加并赋值给mTotalLength。当然,最后还要加上垂直方向padding的值。如果布局高度设置为MATCH_PARENT或者具体数值,则和View的测量方法是一样的。measure流程就讲到这里了,接下来讲解View的layout和draw流程。

View的layout流程

  layout方法的作用是确定元素的位置。ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。首先我们看看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);
            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;
    }

  layout方法的4个参数l、t、r、b分别是View从左、上、右、下相对于其父容器的距离。接着来查看setFrame方法里做了什么,代码如下所示:

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(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();
        }
        
        mPrivateFlags |= drawn;

        mBackgroundSizeChanged = true;
        if (mForegroundInfo != null) {
            mForegroundInfo.mBoundsChanged = true;
        }

        notifySubtreeAccessibilityStateChangedIfNeeded();
    }
    return changed;
}

  setFrame方法用传进来的l、t、r、b这4个参数分别初始化mLeft、mTop、mRight、mBottom这4个值,这样就确定了该View在父容器中的位置。在调用setFrame方法后,会调用onLayout方法:

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

  onLayout方法是一个空方法,这和onMeasure方法类似。确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout方法。既然这样,我们下面就来查看LinearLayout的onLayout方法:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

  与 onMeasure 方法类似,根据方向来调用不同的方法。这里仍旧查看垂直方向的layoutVertical方法,如下所示:

    void layoutVertical(int left, int top, int right, int bottom) {
        ...
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                
                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

  这个方法会遍历子元素并调用setChildFrame方法。其中childTop值是不断累加的,这样子元素才会依次按照垂直方向一个接一个排列下去而不会是重叠的。接着看setChildFrame方法:

    private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

  在setChildFrame方法中调用子元素的layout方法来确定自己的位置。

View的draw流程

  View的draw流程很简单,下面先来看看View的draw方法。官方注释清楚地说明了每一步的做法,它们分别是:

  1. 如果需要,则绘制背景。
  2. 保存当前canvas层。
  3. 绘制View的内容。
  4. 绘制子View。
  5. 如果需要,则绘制View的褪色边缘,这类似于阴影效果。
  6. 绘制装饰,比如滚动条。

  其中第2步和第5步可以跳过,所以这里不做分析,重点分析其他步骤。

步骤1:绘制背景

  绘制背景调用了View的drawBackground方法,如下所示:

    private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();
        ...
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {//1
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

  从上面代码注释1 处可看出绘制背景考虑了偏移参数 scrollX 和scrollY。如果有偏移值不为0,则会在偏移后的canvas绘制背景。

步骤3:绘制View的内容

  步骤3调用了View的onDraw方法。这个方法是一个空实现,因为不同的View有着不同的内容,这需要我们自己去实现,即在自定义View中重写该方法来实现:

    protected void onDraw(Canvas canvas) {
    
    }
步骤4:绘制子View

  步骤4调用了dispatchDraw方法,这个方法也是一个空实现,如下所示:

    protected void dispatchDraw(Canvas canvas) {

    }

  ViewGroup重写了这个方法,紧接着看看ViewGroup的dispatchDraw方法:

    @Override
    protected void dispatchDraw(Canvas canvas) {
        ...
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
        }
        ...
    }

  源码很长,这里截取了关键的部分,在 dispatchDraw 方法中对子类 View 进行遍历,并调用drawChild方法:

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

  这里主要调用了View的draw方法,代码如下所示:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ...
    
    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // 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 if (cache != null) { 
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            int layerPaintAlpha = mLayerPaint.getAlpha();
            if (alpha < 1) {
                mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            }
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            if (alpha < 1) {
                mLayerPaint.setAlpha(layerPaintAlpha);
            }
        }
    }   
}

  源码很长,我们挑重点的看。在上面代码注释1处判断是否有缓存,如果没有则正常绘制,如果有则利用缓存显示。

步骤6:绘制装饰

绘制装饰的方法为View的onDrawForeground方法:

    public void onDrawForeground(Canvas canvas) {
        onDrawScrollIndicators(canvas);
        onDrawScrollBars(canvas);

        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;

                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }

                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }

            foreground.draw(canvas);
        }
    }

  很明显这个方法用于绘制ScrollBar以及其他的装饰,并将它们绘制在视图内容的上层。

以上内容摘自《Android进阶之光》

树莓派4B玩转ROS2 Humble:Ubuntu Desktop一键安装与配置教程 本文详细介绍了在树莓派4B上安装和配置ROS2 Humble的完整教程,包括硬件准备、Ubuntu Desktop系统安装、一键部署ROS2、网络配置优化以及常见问题解决方案。通过简化安装流程和性能调优技巧,帮助开发者快速搭建机器人开发环境,充分发挥树莓派4B在ROS2应用中的潜力。 阅读详情

相关推荐

从USB 2.0到USB 3.x:Synopsys SVT USB VIP配置避坑与接口选择指南

本文详细解析了从USB 2.0到USB 3.x的Synopsys SVT USB VIP配置要点,涵盖协议版本选择、物理接口匹配及UVM层次化架构实现。重点介绍了layering sequence的配置技巧和常见问题解决方案,帮助验证工程师高效避坑并优化验证流程。

weixin_42653691的博客 239

View|工作流程

前言 一、基础知识 在学习View的工作原理之前,需要先学习一些基本的概念。 2.1 ViewRoot Q1: ViewRoot是什么? 对应ViewRootImpl类。 连接WindowManagerService和DecorView的纽带。 Q2:ViewRoot在View绘制中有什么作用? View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。 注意:ViewRoot并不属于View树的一份子。从源码实现上来看,它既非Vi

Yuki-r的博客 1261

华为2019秋招笔试Java实现(2)

华为2019秋招笔试Java实现(2) 笔试日期2019年8月14日 第二题(200分) 题目描述 某程序员小A,有一种表格数据需要存储在文件中。表格中的每一行,由若干个字段组成,每个字段可以是整数或字符串(字符串只包含数字、字母以及特殊字符!@#%^&*()",)。小A设计了一种存储格式,采用文本的方式对表格数据进行存储。文本文件中的每一行,代表了表格数据中的一行数据。具体格式描述如下: 1、采用逗号分隔不同的字段(逗号前后无空格); 2、数字直接采用10进制的文本存储; 3、字符串的存储规则如下

hewhys的博客 1549

View的工作流程1

当然,这个时候界面仍然不会显示出什么,因为View的工作流程还没有执行完,还需要经过measure,layout以及draw才会把view绘制出来。这里调用performLaunchActivity方法来创建Activity,在这个方法里会调用Activity的onCreate方法,从而完成DecorView的创建。在这个方法里,performeResumeActivity会有调用,而在performResumeActivity里,会调用Activity的onResume方法。View的工作流程入口。

Crazy Android 316

第14.1章 高翔博士2025年11月最新开源框架Lightning-LM源码学习解读(一):激光-IMU定位+建图算法各模块简介

Lightning-LM:高效激光SLAM定位系统 该框架基于模块化设计,通过激光-IMU融合实现快速建图与定位,核心特点包括: 极速性能:纯CPU运行(建图1.2核/定位0.8核) 模块化架构:包含激光里程计(LIO)、地图管理、回环检测等独立模块 动态场景支持:地图分块加载、动静态图层分离 多模式适配:支持ROS2环境下的在线/离线运行 关键模块包括前端里程计(core/lio)、2D/3D地图转换(core/g2p5)、轻量优化库(core/miao)等,通过系统控制器(core/system)协调流

行知SLAM 2439

通用抓取算法AnyGrasp(Graspnet)——本地部署并测试自定义输入数据

背景:GraspNet在数据采集方面,首先将相机固定到机械臂末端,对同一个场景拍摄256个不同的角度,同时场景中的每个物体都会提前采集好3D模型,之后将其投射到仿真环境中,在仿真环境中,对抓取位姿进行打标签,如此完成数据的采集。在运行官方的测试示例时,使用的是路径下的输入数据,可以分析下。color.png(输入):rgb图像(输出):输出的生成抓取位姿的图像depth.png(输入):和当前rgb图像对应的深度图像(保存格式是单通道uint16)meta.mat。

boss-dog 1330

Android——View的工作流程

文章目录View的工作流程View的工作流程入口DecorView被加载到Window中MeasureSpecView的measure流程View的measure流程ViewGroup的measure流程View的layout流程View的draw流程步骤1:绘制背景步骤3:绘制View的内容步骤4:绘制子View步骤6:绘制装饰 View的工作流程 View的工作流程,指的就是measure、l...

PdKingLiu 649

基于STM32单片机设计的红外人脸测温仪(QT上位机)

基于STM32单片机设计的红外人脸测温仪(QT上位机)是一种结合红外测温和人脸识别技术的设备,可以快速、非接触地测量人体体温并识别身份,广泛应用于人员密集场所的体温筛查和出入管理。基于STM32单片机设计的红外人脸测温仪是一种功能实用、易于使用的人脸测温设备,可以有效提高测温效率和识别准确率,广泛应用于各类人员密集场所的体温筛查和出入管理。

走向CTO的路上... 1720

View的绘制流程及源码分析

这篇文章主要讲解的是View的绘制流程以及源码分析,讲解这些的主要目的是为了能够在理解View的工作原理上更好的自定义View。 首先讲解一下布局文件是如何展现到屏幕上的。 一、布局文件是如何呈现在屏幕上的 我们从Activity的setContentView(R.layout.activity_main)入手了解UI绘制的起始过程。点进源码,我们会看到 Activity.java p...

很正经的程序员 1294

【学术情报智库】目录学的数字化蜕变:从古籍整理到AI知识图谱的跨越

我们既是浩瀚知识的守望者,也是数字智识的开拓者。目录学数字化不是技术替代传统,而是融合创新,让学术的系统性与多样性在新媒介下焕发新的生命。以AI为桥,既要智能链接,也要人文关照——这是对自我文明的深情回应,也是新知时代的庄严约定。

高校图书馆高级系统架构师/研究馆员,推动高校AI从概念演示到可评审、可交付、可运维:路线图与立项评审、PoC交付、评测回归与上线运维(Baseline/Eval/Ops)。 1422

《时序分析、时序约束和时序收敛》专栏的说明与导航

《时序分析、时序约束和时序收敛》专栏的说明与导航(建议阅读)。

孤独的单刀 14万+

View 体系详解:View 的工作流程

1、View 树的加载流程 当我们调用 startActivity() 方法的时候,会调用到 ActivityThread 中的 performLaunchActivity() 获取一个 Activity 实例, 并在 Instrumentation 的 callActivityOnCreate() 方法中调用 Activity 的 onCreate() 完成 DecorView 的创建。这样我...

weixin_34410662的博客 191

从0到1带大家搭建spring cloud alibaba 微服务大型应用框架(十一)spring-boot-admin 监控篇(2)springcloud 集成spring boot admin

从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(十一)spring-boot-admin 监控篇(2)springcloud 集成spring boot admin spring-boot-admin 服务端创建与集成入nacos spring-boot-admin 查看日志

峡谷电光马仔的博客 1386

【Android】View的工作流程——measure

看到这里你对Activity的构成有一定的了解,每个Activity都有一个与之关联的Window对象,而DecorView是这个Window的根视图。当DecorView被创建以及加载资源的时候,此时它的内容还无法进行显示,因为还没有加载到Window当中。接下来就看看是如何加载的。DecorViewWindow当我们调用Activity的方法的时候,最终调用的是的方法来创建上面的方法是用来创建Activity,里面会调用Activity的onCreate我们看到会调用方法最终调用到了Activity的。

2301_79828182的博客 1605

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

【Android】View的工作流程

measure流程分为View的measure流程和ViewGroup的measure流程。其中,ViewGroup的measure流程除完成自身测量外,还需遍历调用子元素的measure()方法。

2303_79296612的博客 1073

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

自定义View从实现到原理(四)- View的工作流程(一)

自定义View从实现到原理(四) 在之前几篇博客我i们已经探讨了View的事件分发机制,接下来就是这一步了,那么我们就来看一下View的工作流程: View的工作流程,指的就是measure,layout和draw。其中measure作用是测量View的宽和高,layout用来确定View的位置,都让我的作用就是用来绘制View了。 View的工作流程入口 在Activity组成那一部分,最后讲到了DecorView的创建以及它加载的资源,不过这个时候我们的DecorView并没有加载到Window上,因此

Loveyy 2447

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

一. View的工作流程

不忘初心 砥砺前行, Tomorrow Is Another Day ! 相关文章 一. View的工作流程 二. View的事件分发机制 三. View的常用API 四. View的动画效果 本文概要: 概述 Activity的UI层次及工作流程入口 理解MeasureSpe View的工作流程 自定义View的方式及注意事项 一. 概述 如无特别说明,本次源码版本均基于8....

weixin_33701251的博客 205

Android-View的工作流程

View的工作流程 measure: View ViewGroup layout: layout() onLayout() draw: draw()中调用的4个方法 PhoneWindow是窗口类,继承自抽象类Window,也是唯一子类。WindowManager是Window管理接口,继承自ViewManager,他的唯一实现类是WindowManagerImpl。WindowManagerImpl并没有真正实现windowManager接口逻辑,而是把逻辑转给了WindowManagerGl

qq_45424937的博客 1647

第四章:View的工作流程

View的工作流程,指的就是measure,layout,draw。measure用来测量View的宽和高,layout用来确定View的位置,draw用来绘制View。

friday1110的博客 640

Android View的工作流程

View的工作流程 本文主要介绍View的工作流程,也就是measure、layout、draw这三大流程,以及MeasureSpec,掌握这些知识就可以轻松的自定义View以及自定义ViewGroup。 measure Android的视图树中,根View肯定是一个ViewGroup(DecorView就是根View,实际上是一个FrameLayout),所以了解测量过程,应该从ViewGroup开始,沿着视图树去看,这样更容易理解。ViewGroup继承自View,View的的measure方法中会调用

行到水穷处 257

View的工作原理_View的工作流程

View的工作流程: 主要是指:measure,layout,draw这3大流程,measure确定View的测量宽高,layout确定View的最终宽高和4个顶点的位置,而draw则是把View绘制在屏幕上 1,measure过程 measure过程要分 情况来看,如果只是一个原始的View,通过measure方法就完成它的测量,如果是一个ViewGroup,除了完成自己的测量之外还要遍历去调用...

weixin_40877822的博客 217

自定义view之view的工作流程

自定义view是我们开发中经常遇到的问题,总结下来之后发现,对于自定义view来说,了解他的工作流程对我们自定义控件的时候有很大的帮助。 1、我们先来看看viewroot和decorview viewroot对应于viewrootimpl类,它是链接windowmanager和decorview的纽带,view的三大流程均是通过viewroot来完成的。view的绘制流程就是从viewroot

qq_28273051的博客 538

View工作流程

网上对于View的文章可谓是'一抓一大把',也足够说明了View在Android开发中的地位了。现在就准备梳理一下View的工作流程,顺便填一下以前理解上的坑些。 理解之前 在正式梳理(zhuangbi)之前,我们还是先说一下一些简单的概念,以便后面能够更好的理解。 View工作的主要流程: Measure-->Layout-->>Draw。然后我们是分别在 onMeasure,onLa

daweibalang717的专栏 569

安卓View工作流程

前言 提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。 提示:以下是本篇文章正文内容,下面案例可供参考 一、pandas是什么? 示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。 二、使用步骤 1.引入库 代码如下(示例): import numpy as np import pandas as pd import matplotli..

bugmaker的博客 564

安卓入门四十六 View工作流程

从上面的performTraversals()可知:View的绘制流程从顶级View(DecorView)的ViewGroup开始,一层一层从ViewGroup至子View遍历测绘。在Activity中通过 setContentView()所设置的布局文件其实是被加到内容栏之中的,成为其唯一子View = id为content的FrameLayout中。即:自上而下遍历、由父视图到子视图、每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。计算视图(View)的位置。

weixin_52735932的博客 618
上一篇: 从源码解析View的事件分发机制
下一篇: Android自定义view
「已注销」
博客等级 码龄9年 13粉丝 15原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值