Android学习笔记---深入理解View#04

上次我们对View的测量过程有了了解,接着这次肯定就是要沿着View的3大流程往下走。我们本次的主角就是View的Layout过程。

performLayout()开始出发

我们的已经知道了View的布局过程layout pass就是从performLayout()开始的,那么我们就先来大概的浏览一下这个函数的代码。

TIP: 这里我们先简略的浏览整个函数的代码(不用认真看),后面我们会将代码分开几个部分,一个部分一个部分的进行分析。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        // 先将当前是否有布局请求的flag设置为false
        mLayoutRequested = false;
       
        mScrollMayChange = true;
        // 将当前正在布局的flag设置为true
        mInLayout = true;
        // 获取decorView
        final View host = mView;
        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
            Log.v(TAG, "Laying out " + host + " to (" +
                    host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
        }

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
        try {
            // 对decorView进行布局
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            // 将当前正在布局的flag设置为false
            mInLayout = false;
            // 获取当前请求布局的View的个数
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {
                // requestLayout() was called during layout.
                // If no layout-request flags are set on the requesting views, there is no problem.
                // If some requests are still pending, then we need to clear those flags and do
                // a full request/measure/layout pass to handle this situation.
                // 获取需要进行布局的View
                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {
                    // Set this flag to indicate that any further requests are happening during
                    // the second pass, which may result in posting those requests to the next
                    // frame instead
                    // 设置正在处理布局请求的flag为true
                    mHandlingLayoutInLayoutRequest = true;

                    // Process fresh layout requests, then measure and layout
                    // 获取需要进行布局的View的个数
                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

                    mHandlingLayoutInLayoutRequest = false;

                    // Check the valid requests again, this time without checking/clearing the
                    // layout flags, since requests happening during the second pass get noop'd
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        final ArrayList<View> finalRequesters = validLayoutRequesters;
                        // Post second-pass requests to the next frame
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    Log.w("View", "requestLayout() improperly called by " + view +
                                            " during second layout pass: posting in next frame");
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }

            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
        
    }

performLayout()的代码仔细看起来也并不难,我们一点一点的来进行分析。我们都知道这个函数是在ViewRootImplperformTravers()进行调用的。那我们来看看它的传入参数。

performLayout(lp, desiredWindowWidth, desiredWindowHeight);

分别是当前的窗口布局参数lp,窗口的宽度和高度desiredWindowWidth,desiredWindowHeight
既然知道了参数的意义,那我们就可以对performLayout()进行分析了。首先我们就来分析下面部分的代码(为了看起来更加的清晰,我将一些无关的代码去掉了,去掉的部分我用省略号代替):

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        .........
        mInLayout = true;

       final View host = mView;
       ........
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            mInLayout = false;
.....        

这里我们可以看到首先是设置了mInLayout = true,表示当前正在布局的过程当中。随后final View host = mView获取了我们的decorView,然后就调用了我们host.layout()对我们的decorView进行布局。布局完成后就将我们的mInLayout设置为false表示当前不在布局过程中。这里我们需要关心的就只有host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())这句代码,但这并不急于一时,我们先将performLayout()整个函数的逻辑分析清楚后再对里面的内容进行深入。
那么我们就接着看performLayout()的下面的代码(和上面一样的,我只保留了关键的代码):

Tip:下面的代码可以先简略的扫一眼,后面我会详细的分析

int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
    ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
    if (validLayoutRequesters != null) {
        mHandlingLayoutInLayoutRequest = true;
        int numValidRequests = validLayoutRequesters.size();
        for (int i = 0; i < numValidRequests; ++i) {
            final View view = validLayoutRequesters.get(i);
            view.requestLayout();
        }
        measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);            
        mInLayout = true;
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        mHandlingLayoutInLayoutRequest = false;
        validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
        if (validLayoutRequesters != null) {
            final ArrayList<View> finalRequesters = validLayoutRequesters;
            getRunQueue().post(new Runnable() {
                @Override
                public void run() {
                    int numValidRequests = finalRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = finalRequesters.get(i);
                        ......... 
                        view.requestLayout();
                    }
                 }
             });
          }
       }
     }
..........
mInLayout = false;
}

上面的代码有个地方让我非常的在意,就是mLayoutRequesters这个属性变量,因为后面有几次都用到了这个变量。这是一个ArrayList<View>类型的一个集合,从变量名来看,这应该是一个存放着需要进行布局请求的View的集合。但这只是我的猜测,然后我就在ViewRootImpl类的代码中搜索了这个变量出现的地方。发现了一个函数requestLayoutDuringLayout(),我们可以来看看这个函数的代码:

    /**
     * Called by {@link android.view.View#requestLayout()} if the view hierarchy is currently
     * undergoing a layout pass. requestLayout() should not generally be called during layout,
     * unless the container hierarchy knows what it is doing (i.e., it is fine as long as
     * all children in that container hierarchy are measured and laid out at the end of the layout
     * pass for that container). If requestLayout() is called anyway, we handle it correctly
     * by registering all requesters during a frame as it proceeds. At the end of the frame,
     * we check all of those views to see if any still have pending layout requests, which
     * indicates that they were not correctly handled by their container hierarchy. If that is
     * the case, we clear all such flags in the tree, to remove the buggy flag state that leads
     * to blank containers, and force a second request/measure/layout pass in this frame. If
     * more requestLayout() calls are received during that second layout pass, we post those
     * requests to the next frame to avoid possible infinite loops.
     *
     * <p>The return value from this method indicates whether the request should proceed
     * (if it is a request during the first layout pass) or should be skipped and posted to the
     * next frame (if it is a request during the second layout pass).</p>
     *
     * @param view the view that requested the layout.
     *
     * @return true if request should proceed, false otherwise.
     */
    boolean requestLayoutDuringLayout(final View view) {
        if (view.mParent == null || view.mAttachInfo == null) {
            // Would not normally trigger another layout, so just let it pass through as usual
            return true;
        }
        if (!mLayoutRequesters.contains(view)) {
            mLayoutRequesters.add(view);
        }
        if (!mHandlingLayoutInLayoutRequest) {
            // Let the request proceed normally; it will be processed in a second layout pass
            // if necessary
            return true;
        } else {
            // Don't let the request proceed during the second layout pass.
            // It will post to the next frame instead.
            return false;
        }
    }

这个函数的注释,官方已经给出了很明确的解析。但有的同学可能英语不太好,我还是先简单的来讲一下注释的内容吧。
首先这个函数是在view tree正在进行的布局传递过程layout pass的时候由requestLayout()调用的。但通常情况下requestLayout()不会在布局过程中调用,除非container hierarchy(就是整个View树的层级)知道它自己当前的状态(例如当container hierarchy中所有的子View都完成了测量和在布局过程layout pass结束时完成布局的状态下是可以调用的)。如果requestLayout()在一个frame(这里的frame应该是View的一次绘制流程,即一次完整的measure,layout,draw过程)进行的过程中被调用了,我们需要将所有要求进行布局的View进行注册(记录起来)。在frame结束的时候,我们检查注册过的View,看看是否还有那些没有被正确的处理View进行布局请求。如果有的话,我们将view tree的所有标记清除,得到一个空白的容器,然后在本次frame强制的执行第二次request/measure/layout过程。如果在第二次布局时还收到requestLayout()的调用,我们把这次的布局请求延迟到下一个frame,以此来避免进入死循环。
上面就是注释中所说的东西,简单的来讲,这个函数就是用来判断当前frame是否接受在布局过程layout pass中View的重新布局请求。而且这个函数的代码逻辑也很简单,首先传进来的view参数就是请求重新布局的View对象,第一个条件判断(view.mParent == null || view.mAttachInfo == null)表示view没有父布局或view没有所属的窗口,简单来说就是该View不会触发其他的布局。所以接受布局请求返回true。接着就将view不重复地添加到mLayoutRequesters集合中,然后根据mHandlingLayoutInLayoutRequest来判断当前是否正在进行布局请求的处理。若正在进行布局请求的处理就返回false,将当前的view的布局请求放到下一次frame进行,否则就接受请求,并在第二次布局过程中处理。

理解了这个函数后在回过来看我们的performLayout()的代码就简单不少了。但我发现performLayout()的代码里还有一个比较关键的函数getValidLayoutRequesters()。这个函数看它的名字就知道是为了得到前面所说的有布局请求的View的集合。但我们前面讲了将在布局过程layout pass中发生的requestLayout()分成了两种情况,那它又是怎样进行处理的呢?我们可以先到getValidLayoutRequesters()的代码中先看看。

    /**
     * This method is called during layout when there have been calls to requestLayout() during
     * layout. It walks through the list of views that requested layout to determine which ones
     * still need it, based on visibility in the hierarchy and whether they have already been
     * handled (as is usually the case with ListView children).
     *
     * @param layoutRequesters The list of views that requested layout during layout
     * @param secondLayoutRequests Whether the requests were issued during the second layout pass.
     * If so, the FORCE_LAYOUT flag was not set on requesters.
     * @return A list of the actual views that still need to be laid out.
     */
    private ArrayList<View> getValidLayoutRequesters(ArrayList<View> layoutRequesters,
            boolean secondLayoutRequests) {
        
        int numViewsRequestingLayout = layoutRequesters.size();
        // 用于暂存有布局请求的view
        ArrayList<View> validLayoutRequesters = null;
        // 遍历每个需要请求布局的view
        for (int i = 0; i < numViewsRequestingLayout; ++i) {
            View view = layoutRequesters.get(i);
            // 判断是否需要检查和清除view的flag
            if (view != null && view.mAttachInfo != null && view.mParent != null &&
                    (secondLayoutRequests || (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) ==
                            View.PFLAG_FORCE_LAYOUT)) {
                boolean gone = false;
                View parent = view;
                // Only trigger new requests for views in a non-GONE hierarchy
                // 只触发`view tree`层级结构中可见view的布局请求
                while (parent != null) {
                    if ((parent.mViewFlags & View.VISIBILITY_MASK) == View.GONE) {
                        gone = true;
                        break;
                    }
                    if (parent.mParent instanceof View) {
                        parent = (View) parent.mParent;
                    } else {
                        parent = null;
                    }
                }
                if (!gone) {
                    if (validLayoutRequesters == null) {
                        validLayoutRequesters = new ArrayList<View>();
                    }
                    // 将需要处理的view加入到返回集合
                    validLayoutRequesters.add(view);
                }
            }
        }
        // 判断是否为第2次获取view集合
        if (!secondLayoutRequests) {
            // If we're checking the layout flags, then we need to clean them up also
            // 遍历集合里所有的view,并将其中的flag重置
            for (int i = 0; i < numViewsRequestingLayout; ++i) {
                View view = layoutRequesters.get(i);
                while (view != null &&
                        (view.mPrivateFlags & View.PFLAG_FORCE_LAYOUT) != 0) {
                    view.mPrivateFlags &= ~View.PFLAG_FORCE_LAYOUT;
                    if (view.mParent instanceof View) {
                        view = (View) view.mParent;
                    } else {
                        view = null;
                    }
                }
            }
        }
        // 清除参数集合中的所有view,给下一次`frame`提供一个空的容器
        layoutRequesters.clear();
        return validLayoutRequesters;
    }

这个函数的功能就是遍历参数layoutRequesters里的每个View,根据每个View在view tree层级结构中是否可见以及是否已被处理,来判断哪些view仍然需要布局。其中第2个参数secondLayoutRequests表示获取的view集合是否是在一次frame里的第2次获取。因为我们需要将布局请求分为两种,在第2次获取view的集合时得到的是在下一次frame中需要进行处理的view,而代码中是通过第2个参数secondLayoutRequests来区分。
简单的来说,这个函数就是用来获取当前frame中需要进行布局处理的view集合或者获取下一次frame时需要进行布局处理的view集合。

知道了mLayoutRequesters这个成员属性和getValidLayoutRequesters()函数的意义后我们可以来继续分析我们的performLayout()的代码了。

// 获取当前请求布局的View的数量
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
    // 获取当前`frame`需要进行处理布局请求的View的集合
    ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
    if (validLayoutRequesters != null) {
        // 表示当前正在处理布局请求
        mHandlingLayoutInLayoutRequest = true;
        int numValidRequests = validLayoutRequesters.size();
        // 对集合中的每个view并对它进行新的布局请求,进行测量和布局
        for (int i = 0; i < numValidRequests; ++i) {
            final View view = validLayoutRequesters.get(i);
            view.requestLayout();
        }
        // 对整个View树进行重新测量
        measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
        // 表示当前正在布局                 
        mInLayout = true;
        // 进行第2次布局
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
        // 表示布局请求处理完毕
        mHandlingLayoutInLayoutRequest = false;
        // 获取下次`frame`需要进行布局处理的view集合
        validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
        if (validLayoutRequesters != null) {
            final ArrayList<View> finalRequesters = validLayoutRequesters;
            // 将相应布局请求发生到下一次`frame`中进行处理
            getRunQueue().post(new Runnable() {
                @Override
                public void run() {
                    int numValidRequests = finalRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = finalRequesters.get(i);
                        ......... 
                        view.requestLayout();
                    }
                 }
             });
          }
       }
     }
..........
// 表示布局结束
mInLayout = false;
}

我在代码中已经做了详细的注释,我相信通过对mLayoutRequestersgetValidLayoutRequesters()两个关键点的讲解后,大家都能轻易的理解performLayout()的基本逻辑。那么现在我们要进入到View真正进行布局的地方—layout().

揭开layout()的面目

layout(int l, int t, int r, int b)这是我们的函数原型,4个参数分别是由父布局传进来的,代表view在父布局中所放置的位置信息,分别为距离父布局的上下左右的位置。下面给出示意图。

《Android学习笔记---深入理解View#04》

知道了参数的意义后,话不多说,马上让我们的主角登场吧!

    /**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     *
     * @param l Left position, relative to parent
     * @param t Top position, relative to parent
     * @param r Right position, relative to parent
     * @param b Bottom position, relative to parent
     */
    @SuppressWarnings({"unchecked"})
    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;
        // 判断当前的View的显示模式的并进行边界的设置,并以此来判断View的布局大小和位置是否有所改变
        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;
            // 执行布局的监听接口,像onClickListener等监听接口一样
            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);
                }
            }
        }
        // 设置相应的flag
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

我先来解析一下文档注释的内容吧。首先这个函数是给View和它的后代节点(在view tree上)分配一个大小和位置,将它们布局在窗口上。这是布局机制的第2个阶段(第1阶段是测量阶段)。在这个阶段view tree上的
每个父阶段都调用孩子节点的layout()函数来放置它们,而且使用的是孩子节点在测量过程measure pass中保存的测量值。View的派生类不应该重写这个函数来实现自定义布局,而应该重写onLayout()函数,并在onLayout()中对它的孩子进行布局。
好了,解析完文档的内容后我们来分析代码吧。代码的逻辑也并不复杂,首先通过View的flag判断在布局前是否需要进行测量。然后根据window的模式来对View进行布局。
下面我们来看看onLayout()这个函数吧:

    /**
     * Called from layout when this view should
     * assign a size and position to each of its children.
     *
     * Derived classes with children should override
     * this method and call layout on each of
     * their children.
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

可以看到这个函数在View类下的实现是空的函数体。这就说明了我们需要在子类来重写onLayout()函数来完成我们的自定义布局。通常情况下,ViewGroup是需要实现该函数的,因为它需要处理子View的布局。既然这样,我们就来看一下Android提供的FrameLayout类所实现的onLayout()的代码:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // 对子View进行布局
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom,
                                  boolean forceLeftGravity) {
        // 得到子View的数量
        final int count = getChildCount();
        
        // 计算得到FrameLayout的上下左右
        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();
        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
        
        // 遍历每一个子View,按照FrameLayout的属性对子View进行布局
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            
            // 只对属性不为GONE的view进行布局
            if (child.getVisibility() != GONE) {
            
                // 得到子View的布局参数
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                
                // 得到子View测量后的宽高
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;
                
                // 获取View的gravity属性
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }
                
                // 得到FrameLayout的布局方向,RTL或LTR
                final int layoutDirection = getLayoutDirection();
                
                // 根据子View的gravity属性和FrameLayout的布局方向
                // 设置布局开始和结束的位置,是从左边开始,到右边结束;还是从右边开始,到左边结束。
                // 得到一个代表水平布局方向的值
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                
                // 下面的两个switch语句分别根据布局的垂直方向属性和水平方向属性
                // 计算子View的上左位置,即Top和Left(将边距考虑在内)
                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;
                }
                // 对子View进行布局
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

虽然代码有点长,但逻辑都非常的清晰,而且我在代码中也做了相应的注释,相信大家能很容易的就看得明白。

requestLayout()invalidate()

到这里我们的布局过程layout pass也分析完毕了。但这里还有一个疑问,就是我们在分析performLayout()的时候多次提到了requestLayout()这个函数,但却不清楚它的内容,既然这样,我们就来看看requestLayout()的真面目吧。

    /**
     * Call this when something has changed which has invalidated the
     * layout of this view. This will schedule a layout pass of the view
     * tree. This should not be called while the view hierarchy is currently in a layout
     * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
     * end of the current layout pass (and then layout will run again) or after the current
     * frame is drawn and the next layout occurs.
     *
     * <p>Subclasses which override this method should call the superclass method to
     * handle possible request-during-layout errors correctly.</p>
     */
    @CallSuper
    public void requestLayout() {
        if (mMeasureCache != null) mMeasureCache.clear();
        
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
            // 只有在当前的View请求布局,而不是父级层级的View请求布局时
            // 才触发布局时请求的逻辑代码
            ViewRootImpl viewRoot = getViewRootImpl();
            if (viewRoot != null && viewRoot.isInLayout()) {
                if (!viewRoot.requestLayoutDuringLayout(this)) {
                    return;
                }
            }
            mAttachInfo.mViewRequestingLayout = this;
        }
        
        // 设置相应的布局flag,或操作表示在flag中添加该标志
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
        mPrivateFlags |= PFLAG_INVALIDATED;
        
        // 判断是否需要对父View进行布局请求
        if (mParent != null && !mParent.isLayoutRequested()) {
            // 调用ViewRootImpl的`requestLayout()`
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

照样的,我先讲讲文档注释中的内容。当我们的View的内容发生了改变导致View的布局变得无效(即我们想要的效果没有正确的展现在布局上),这个时候我们可以调用requestLayout()来告诉系统让系统在view tree上执行一次layout pass来刷新布局。如果正在执行布局layout pass的时候调用了requestLayout(),那么布局的请求可能会在当前布局结束的时候重新进行一次layout pass,或者是等到下一次frame的时候才进行布局。如果子类要重写该方法,需要先调用super.requestLayout()以保证能正确的处理各种布局请求。
文档所说的与我们在前面分析的requestLayoutDuringLayout()函数时是一样的。如果前面的分析看懂了,那么结合前面的分析,requestLayout()的代码也很容易就能明白。
这里有一点需要注意的,就是下面部分的代码:

    // 判断是否需要对父View进行布局请求
    if (mParent != null && !mParent.isLayoutRequested()) {
          // 调用ViewRootImpl的`requestLayout()`
          mParent.requestLayout();
    }

这里根据条件,调用了ViewRootImplrequestLayout()函数,这个函数就是我在前面的文章中提到过的,调用了scheduleTraversals()的函数。

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

而且我们也知道了这个函数最后会调用ViewRootImplperformTraversal()函数,会执行一遍View的measure,layout,draw流程。
这就说明了,当View的requestLayout()被调用的时候,我们的整个view tree可能会进行一次measure,layout,draw的过程,这时我们的ViewRootImpl中的performMeasure()performLayout()会一定被调用,但performDraw()就有可能被调用也有可能不被调用。

既然讲到了requestLayout(),那就不得不提与它非常密切的invalidate()了。

    /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     */
    public void invalidate() {
        invalidate(true);
    }
     /**
     * This is where the invalidate() work actually happens. A full invalidate()
     * causes the drawing cache to be invalidated, but this function can be
     * called with invalidateCache set to false to skip that invalidation step
     * for cases that do not need it (for example, a component that remains at
     * the same dimensions with the same content).
     *
     * @param invalidateCache Whether the drawing cache for this view should be
     *            invalidated as well. This is usually true for a full
     *            invalidate, but may be set to false if the View's contents or
     *            dimensions have not changed.
     */
    void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }
    
    // 将View中指定的区域变为无效
    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }

        if (skipInvalidate()) {
            return;
        }

        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                // 去掉flag中的draw标记,以此来表示view未被绘制
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            mPrivateFlags |= PFLAG_DIRTY;

            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            // Propagate the damage rectangle to the parent view.
            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                // 调用`ViewRootImpl`的`invalidateChild()`
                p.invalidateChild(this, damage);
            }

            // Damage the entire projection receiver, if necessary.
            if (mBackground != null && mBackground.isProjected()) {
                final View receiver = getProjectionReceiver();
                if (receiver != null) {
                    receiver.damageInParent();
                }
            }

            // Damage the entire IsolatedZVolume receiving this view's shadow.
            if (isHardwareAccelerated() && getZ() != 0) {
                damageShadowReceiver();
            }
        }
    }

注释说了,这个函数会可见的View变成无效的状态,即需要进行重新绘制,这就说明了onDraw()函数将会被调用。
上面的代码我们关注的部分是,首先invalidate()函数最终调用的是invalidateInternal()函数。所以我们直接看这个函数。在invalidateInternal()里,通过mPrivateFlags &= ~PFLAG_DRAWN;这句代码将flag相应的位设置为未被绘制。然后可以看到通过p.invalidateChild(this, damage);调用了ViewRootImplinvalidateChild()。下面就是ViewRootImpl中相关的代码。

    @Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }

    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);

        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }

        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }

        invalidateRectOnScreen(dirty);

        return null;
    }

    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
            mAttachInfo.mSetIgnoreDirtyState = true;
            mAttachInfo.mIgnoreDirtyState = true;
        }

        // Add the new dirty rect to the current one
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        // Intersect with the bounds of the window to skip
        // updates that lie outside of the visible region
        final float appScale = mAttachInfo.mApplicationScale;
        final boolean intersected = localDirty.intersect(0, 0,
                (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        if (!intersected) {
            localDirty.setEmpty();
        }
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            // 关注这里
            scheduleTraversals();
        }
    }

我们只需关注上面最后的那句代码scheduleTraversals();即可。这里也调用了scheduleTraversals(),也就是说最终也会调用performTraversals()函数,但是由于没有添加measurelayout的标记到flag中,所以只会调用performDraw()函数。
总的来说:

  • invalidate()函数会触发performDraw()过程。
  • requestLayout()就会触发performMeasure()performLayout()过程,也有可肯触发performDraw()

注意:invalidate()需要在UI线程里被调用,如果要在非UI线程里调用,就需要调用postInvalidate()

更多关于invalidate()requestLayout()
的信息可以参考这两篇博文:

总结

最后还是用图片进行总结吧,因为这样最实际。

《Android学习笔记---深入理解View#04》

    原文作者:Kenny_Ho
    原文地址: https://www.jianshu.com/p/6d66ea4998de
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞