Android學習筆記---深入理解View#04

上次我們對View的測量過程有了了解,接著這次肯定就是要沿著View的3大流程往下走。我們本次的主角就是View的Layout過程。

performLayout()開始出發(fā)

我們的已經(jīng)知道了View的布局過程layout pass就是從performLayout()開始的,那么我們就先來大概的瀏覽一下這個函數(shù)的代碼。

TIP: 這里我們先簡略的瀏覽整個函數(shù)的代碼(不用認真看),后面我們會將代碼分開幾個部分,一個部分一個部分的進行分析。

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的個數(shù)
            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的個數(shù)
                    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()的代碼仔細看起來也并不難,我們一點一點的來進行分析。我們都知道這個函數(shù)是在ViewRootImplperformTravers()進行調(diào)用的。那我們來看看它的傳入?yún)?shù)。

performLayout(lp, desiredWindowWidth, desiredWindowHeight);

分別是當前的窗口布局參數(shù)lp,窗口的寬度和高度desiredWindowWidth,desiredWindowHeight
既然知道了參數(shù)的意義,那我們就可以對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,然后就調(diào)用了我們host.layout()對我們的decorView進行布局。布局完成后就將我們的mInLayout設置為false表示當前不在布局過程中。這里我們需要關心的就只有host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight())這句代碼,但這并不急于一時,我們先將performLayout()整個函數(shù)的邏輯分析清楚后再對里面的內(nèi)容進行深入。
那么我們就接著看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類的代碼中搜索了這個變量出現(xiàn)的地方。發(fā)現(xiàn)了一個函數(shù)requestLayoutDuringLayout(),我們可以來看看這個函數(shù)的代碼:

    /**
     * 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;
        }
    }

這個函數(shù)的注釋,官方已經(jīng)給出了很明確的解析。但有的同學可能英語不太好,我還是先簡單的來講一下注釋的內(nèi)容吧。
首先這個函數(shù)是在view tree正在進行的布局傳遞過程layout pass的時候由requestLayout()調(diào)用的。但通常情況下requestLayout()不會在布局過程中調(diào)用,除非container hierarchy(就是整個View樹的層級)知道它自己當前的狀態(tài)(例如當container hierarchy中所有的子View都完成了測量和在布局過程layout pass結(jié)束時完成布局的狀態(tài)下是可以調(diào)用的)。如果requestLayout()在一個frame(這里的frame應該是View的一次繪制流程,即一次完整的measure,layout,draw過程)進行的過程中被調(diào)用了,我們需要將所有要求進行布局的View進行注冊(記錄起來)。在frame結(jié)束的時候,我們檢查注冊過的View,看看是否還有那些沒有被正確的處理View進行布局請求。如果有的話,我們將view tree的所有標記清除,得到一個空白的容器,然后在本次frame強制的執(zhí)行第二次request/measure/layout過程。如果在第二次布局時還收到requestLayout()的調(diào)用,我們把這次的布局請求延遲到下一個frame,以此來避免進入死循環(huán)。
上面就是注釋中所說的東西,簡單的來講,這個函數(shù)就是用來判斷當前frame是否接受在布局過程layout pass中View的重新布局請求。而且這個函數(shù)的代碼邏輯也很簡單,首先傳進來的view參數(shù)就是請求重新布局的View對象,第一個條件判斷(view.mParent == null || view.mAttachInfo == null)表示view沒有父布局或view沒有所屬的窗口,簡單來說就是該View不會觸發(fā)其他的布局。所以接受布局請求返回true。接著就將view不重復地添加到mLayoutRequesters集合中,然后根據(jù)mHandlingLayoutInLayoutRequest來判斷當前是否正在進行布局請求的處理。若正在進行布局請求的處理就返回false,將當前的view的布局請求放到下一次frame進行,否則就接受請求,并在第二次布局過程中處理。

理解了這個函數(shù)后在回過來看我們的performLayout()的代碼就簡單不少了。但我發(fā)現(xiàn)performLayout()的代碼里還有一個比較關鍵的函數(shù)getValidLayoutRequesters()。這個函數(shù)看它的名字就知道是為了得到前面所說的有布局請求的View的集合。但我們前面講了將在布局過程layout pass中發(fā)生的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
                // 只觸發(fā)`view tree`層級結(jié)構(gòu)中可見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;
                    }
                }
            }
        }
        // 清除參數(shù)集合中的所有view,給下一次`frame`提供一個空的容器
        layoutRequesters.clear();
        return validLayoutRequesters;
    }

這個函數(shù)的功能就是遍歷參數(shù)layoutRequesters里的每個View,根據(jù)每個View在view tree層級結(jié)構(gòu)中是否可見以及是否已被處理,來判斷哪些view仍然需要布局。其中第2個參數(shù)secondLayoutRequests表示獲取的view集合是否是在一次frame里的第2次獲取。因為我們需要將布局請求分為兩種,在第2次獲取view的集合時得到的是在下一次frame中需要進行處理的view,而代碼中是通過第2個參數(shù)secondLayoutRequests來區(qū)分。
簡單的來說,這個函數(shù)就是用來獲取當前frame中需要進行布局處理的view集合或者獲取下一次frame時需要進行布局處理的view集合。

知道了mLayoutRequesters這個成員屬性和getValidLayoutRequesters()函數(shù)的意義后我們可以來繼續(xù)分析我們的performLayout()的代碼了。

// 獲取當前請求布局的View的數(shù)量
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;
            // 將相應布局請求發(fā)生到下一次`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();
                    }
                 }
             });
          }
       }
     }
..........
// 表示布局結(jié)束
mInLayout = false;
}

我在代碼中已經(jīng)做了詳細的注釋,我相信通過對mLayoutRequestersgetValidLayoutRequesters()兩個關鍵點的講解后,大家都能輕易的理解performLayout()的基本邏輯。那么現(xiàn)在我們要進入到View真正進行布局的地方---layout().

揭開layout()的面目

layout(int l, int t, int r, int b)這是我們的函數(shù)原型,4個參數(shù)分別是由父布局傳進來的,代表view在父布局中所放置的位置信息,分別為距離父布局的上下左右的位置。下面給出示意圖。

知道了參數(shù)的意義后,話不多說,馬上讓我們的主角登場吧!

    /**
     * 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;
            // 執(zhí)行布局的監(jiān)聽接口,像onClickListener等監(jiān)聽接口一樣
            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;
    }

我先來解析一下文檔注釋的內(nèi)容吧。首先這個函數(shù)是給View和它的后代節(jié)點(在view tree上)分配一個大小和位置,將它們布局在窗口上。這是布局機制的第2個階段(第1階段是測量階段)。在這個階段view tree上的
每個父階段都調(diào)用孩子節(jié)點的layout()函數(shù)來放置它們,而且使用的是孩子節(jié)點在測量過程measure pass中保存的測量值。View的派生類不應該重寫這個函數(shù)來實現(xiàn)自定義布局,而應該重寫onLayout()函數(shù),并在onLayout()中對它的孩子進行布局。
好了,解析完文檔的內(nèi)容后我們來分析代碼吧。代碼的邏輯也并不復雜,首先通過View的flag判斷在布局前是否需要進行測量。然后根據(jù)window的模式來對View進行布局。
下面我們來看看onLayout()這個函數(shù)吧:

    /**
     * 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) {
    }

可以看到這個函數(shù)在View類下的實現(xiàn)是空的函數(shù)體。這就說明了我們需要在子類來重寫onLayout()函數(shù)來完成我們的自定義布局。通常情況下,ViewGroup是需要實現(xiàn)該函數(shù)的,因為它需要處理子View的布局。既然這樣,我們就來看一下Android提供的FrameLayout類所實現(xiàn)的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的數(shù)量
        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的布局參數(shù)
                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();
                
                // 根據(jù)子View的gravity屬性和FrameLayout的布局方向
                // 設置布局開始和結(jié)束的位置,是從左邊開始,到右邊結(jié)束;還是從右邊開始,到左邊結(jié)束。
                // 得到一個代表水平布局方向的值
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
                
                // 下面的兩個switch語句分別根據(jù)布局的垂直方向?qū)傩院退椒较驅(qū)傩?                // 計算子View的上左位置,即Top和Left(將邊距考慮在內(nèi))
                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()這個函數(shù),但卻不清楚它的內(nèi)容,既然這樣,我們就來看看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請求布局時
            // 才觸發(fā)布局時請求的邏輯代碼
            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()) {
            // 調(diào)用ViewRootImpl的`requestLayout()`
            mParent.requestLayout();
        }
        if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
            mAttachInfo.mViewRequestingLayout = null;
        }
    }

照樣的,我先講講文檔注釋中的內(nèi)容。當我們的View的內(nèi)容發(fā)生了改變導致View的布局變得無效(即我們想要的效果沒有正確的展現(xiàn)在布局上),這個時候我們可以調(diào)用requestLayout()來告訴系統(tǒng)讓系統(tǒng)在view tree上執(zhí)行一次layout pass來刷新布局。如果正在執(zhí)行布局layout pass的時候調(diào)用了requestLayout(),那么布局的請求可能會在當前布局結(jié)束的時候重新進行一次layout pass,或者是等到下一次frame的時候才進行布局。如果子類要重寫該方法,需要先調(diào)用super.requestLayout()以保證能正確的處理各種布局請求。
文檔所說的與我們在前面分析的requestLayoutDuringLayout()函數(shù)時是一樣的。如果前面的分析看懂了,那么結(jié)合前面的分析,requestLayout()的代碼也很容易就能明白。
這里有一點需要注意的,就是下面部分的代碼:

    // 判斷是否需要對父View進行布局請求
    if (mParent != null && !mParent.isLayoutRequested()) {
          // 調(diào)用ViewRootImpl的`requestLayout()`
          mParent.requestLayout();
    }

這里根據(jù)條件,調(diào)用了ViewRootImplrequestLayout()函數(shù),這個函數(shù)就是我在前面的文章中提到過的,調(diào)用了scheduleTraversals()的函數(shù)。

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

而且我們也知道了這個函數(shù)最后會調(diào)用ViewRootImplperformTraversal()函數(shù),會執(zhí)行一遍View的measure,layout,draw流程。
這就說明了,當View的requestLayout()被調(diào)用的時候,我們的整個view tree可能會進行一次measure,layout,draw的過程,這時我們的ViewRootImpl中的performMeasure()performLayout()會一定被調(diào)用,但performDraw()就有可能被調(diào)用也有可能不被調(diào)用。

既然講到了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中指定的區(qū)域變?yōu)闊o效
    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);
                // 調(diào)用`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();
            }
        }
    }

注釋說了,這個函數(shù)會可見的View變成無效的狀態(tài),即需要進行重新繪制,這就說明了onDraw()函數(shù)將會被調(diào)用。
上面的代碼我們關注的部分是,首先invalidate()函數(shù)最終調(diào)用的是invalidateInternal()函數(shù)。所以我們直接看這個函數(shù)。在invalidateInternal()里,通過mPrivateFlags &= ~PFLAG_DRAWN;這句代碼將flag相應的位設置為未被繪制。然后可以看到通過p.invalidateChild(this, damage);調(diào)用了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();即可。這里也調(diào)用了scheduleTraversals(),也就是說最終也會調(diào)用performTraversals()函數(shù),但是由于沒有添加measurelayout的標記到flag中,所以只會調(diào)用performDraw()函數(shù)。
總的來說:

  • invalidate()函數(shù)會觸發(fā)performDraw()過程。
  • requestLayout()就會觸發(fā)performMeasure()performLayout()過程,也有可肯觸發(fā)performDraw()

注意:invalidate()需要在UI線程里被調(diào)用,如果要在非UI線程里調(diào)用,就需要調(diào)用postInvalidate()

更多關于invalidate()requestLayout()
的信息可以參考這兩篇博文:

總結(jié)

最后還是用圖片進行總結(jié)吧,因為這樣最實際。


最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容