自定義View onLayout篇

RoyGit發表於2017-12-13

OK,先提一下完結的onMeasure 篇,沒看完的小夥伴先看一下onMeasure 寫給新人看的自定義View-onMeasure篇(1) 寫給新人看的自定義View-onMeasure篇(2) 先說一下View的layout 和 onLayout。 這裡為了方便理解,以寫出自定義View為目的,不做太深入,其一是因為,我們知道這麼多,就已經可以寫出自定義ViewGroup了,另一方面,深入了我也不知道。總之,大家在看完文章,如果想知道更多的細節的話,就去研究一下View的layout原始碼。

OK,話不多說,先分析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);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            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;
    }
複製程式碼

按慣例先說一下每個引數 l:View左邊界距離父容器的左邊界的距離 t:View上邊界距離父容器上邊界的距離 r:View右邊界距離父容器左邊界的距離 b:View下邊界距離父容器上邊界的距離

具體如下圖所示如下圖所示:(圖片是老師[GcsSloop]Github上面的 我拷貝來用一下)

自定義View.jpeg

好的,下面可以直接看幾組關鍵程式碼

   boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

複製程式碼

可以看到,isLayoutModeOptical這個方法,是判斷是否有光學邊界的(光學邊界這裡暫時用不到,請自行谷歌)我們來仔細看setOpticalFrame,setFrame這兩個方法

    private boolean setOpticalFrame(int left, int top, int right, int bottom) {
        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;
        Insets childInsets = getOpticalInsets();
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
複製程式碼

可以看到,這個setOpticalFrame方法,最終也是呼叫了setFrame,那好我們可以直接繼續看setFrame方法了。

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (DBG) {
            Log.d("View", this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }

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

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;


            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                // If we are visible, force the DRAWN bit to on so that
                // this invalidate will go through (at least to our parent).
                // This is because someone may have invalidated this view
                // before this call to setFrame came in, thereby clearing
                // the DRAWN bit.
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                // parent display list may need to be recreated based on a change in the bounds
                // of any child
                invalidateParentCaches();
            }

            // Reset drawn bit to original value (invalidate turns it off)
            mPrivateFlags |= drawn;

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

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }
複製程式碼

這裡面程式碼就不用一句一句的分析,看大概,我們便可以看出步驟,先是比較了新位置和老位置是否有差異,如果有差異會呼叫sizechanged來更新我們View的位置。

OK 這個方法大概分析完畢了,我們先回到layout方法繼續 onLayout(changed, l, t, r, b); OK ,找到今天的主角了。我們點進去這個方法,看裡面做了什麼。

    /**
     * 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) {
    }
複製程式碼

有點奇怪,什麼也沒做。這個其實是android留給我們自己去實現的一個方法,也就是大家都知道的,去佈局子View的位置,只有含有子View的容器,才需要重寫這個方法,也就是ViewGroup。

OK ,通過上面的分析,可以得到兩個結論 1、View通過layout方法來確認自己在父容器中的位置 2、 ViewGroup通過onLayout 方法來確定View在容器中的位置

OK,光有理論沒什麼卵用,來實現一個簡單的流式佈局,來驗證一下

public class MyViewGroup extends ViewGroup {

    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       //遍歷子View,測量每個View的大小
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
複製程式碼

自定義ViewGroup,通過onLayout()方法給子View佈局,前提,我們必須得知道每個子View的寬度和高度,對吧。所以我們先要在onMeasure的時候,測量一下每個子View的具體大小,前面已經把View和ViewGroup的onMeasure都分析過了,這邊不在贅述。直接遍歷子View,然後measureChild即可得到所有子View的measureSize(注意這裡說的是measureSize,為什麼是measureSize,之後再談)。

OK 已經測量出子View的具體大小了,那麼下面,我們就來安排他們的位置。

    private int horizontalSpace = 10;//水平間距
    private int verticalSpace = 10;//垂直間距
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int hadUsedHorizontal = 0;//水平已經使用的距離
        int hadUsedVertical = 0;//垂直已經使用的距離
        int width = getMeasuredWidth();
//        int height = getMeasuredHeight();
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            //判斷是否已經超出寬度
            if (view.getMeasuredWidth() + hadUsedHorizontal > width) {
                //已經超出了寬度
                hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace;
                hadUsedHorizontal = 0;
            }
            view.layout(hadUsedHorizontal, hadUsedVertical, hadUsedHorizontal + view.getMeasuredWidth(), hadUsedVertical + view.getMeasuredHeight());
            hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth();
        }
    }
複製程式碼

我們先是定義了水平已經使用的距離,和垂直已經使用的距離,而且,如果有需要 我們還需要水平和垂直的間距,都定義出來。OK,可以看到,邏輯很簡單,每次layout子View的時候,我們都要判斷,子View寬度,已經超出了父View的寬度,如果超出了,就換行。最後呼叫子View的layout來確定子view的位置。 OK,記得剛才說我們原始碼裡面獲取子View大小的時候,寬度為例子使用getMeasuredWidth,為什麼用這個而不用getWidth呢?也就是我前面說的measureSize

OK我們看一下。我們在ViewGroup onMeasure的時候,呼叫了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.measure,也就是view的measure方法。我們繼續看裡面做了什麼,由於程式碼很多,我就不貼上了。view的.measure 方法呼叫了自己的onMeasure方法,也就像我們在onMeasure說的那樣,之後子View會呼叫setMeasuredDimension來提交自己的寬高。我們看看這個setMeasuredDimension

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
複製程式碼

OK ,這就清楚了,最終,我們呼叫measureChild方法 最終會把子View的大小傳給mMeasuredSize。那可能會有朋友問,那getWidth,和getHeight會得到什麼呢?在onMeasure 方法的時候,getwidth 和getheight都是0;為什麼呢? View的getWidth原始碼:

    public final int getWidth() {
        return mRight - mLeft;
    }
複製程式碼

而我們剛才分析layout的原始碼時候就知道,mRight和mleft是在layout方法之後才賦值的,所以在測量子View的時候,是無法拿到getWidth 和 getHeight的。 OK 最後貼出原始碼和佈局檔案

public class MyViewGroup extends ViewGroup {

    private int horizontalSpace = 10;
    private int verticalSpace = 10;

    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int hadUsedHorizontal = 0;//水平已經使用的距離
        int hadUsedVertical = 0;//垂直已經使用的距離
        int width = getMeasuredWidth();
//        int height = getMeasuredHeight();
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            //判斷是否已經超出寬度
            if (view.getMeasuredWidth() + hadUsedHorizontal > width) {
                //已經超出了寬度
                hadUsedVertical = hadUsedVertical + view.getMeasuredHeight() + verticalSpace;
                hadUsedHorizontal = 0;
            }
            view.layout(hadUsedHorizontal, hadUsedVertical, hadUsedHorizontal + view.getMeasuredWidth(), hadUsedVertical + view.getMeasuredHeight());
            hadUsedHorizontal = hadUsedHorizontal + horizontalSpace + view.getMeasuredWidth();
        }
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            measureChild(view, widthMeasureSpec, heightMeasureSpec);
        }
    }

    /**
     * @param child                   子View
     * @param parentWidthMeasureSpec  寬度測量規格
     * @param widthUsed               父view在寬度上已經使用的距離
     * @param parentHeightMeasureSpec 高度測量規格
     * @param heightUsed              父view在高度上已經使用的距離
     */
    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
    }
}

複製程式碼

佈局檔案

<?xml version="1.0" encoding="utf-8"?>
<picture.yisi.com.viewconfigrationtest.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="80dp"
        android:layout_height="40dp"
        android:background="@color/colorAccent"/>

    <TextView
        android:layout_width="80dp"
        android:layout_height="40dp"
        android:background="@color/colorAccent"/>

    <TextView
        android:layout_width="80dp"
        android:layout_height="40dp"
        android:background="@color/colorAccent"/>
    <TextView
        android:layout_width="80dp"
        android:layout_height="40dp"
        android:background="@color/colorAccent"/>
    <TextView
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:background="@color/colorAccent"/>
    <TextView
        android:layout_width="80dp"
        android:layout_height="40dp"
        android:background="@color/colorAccent"/>

</picture.yisi.com.viewconfigrationtest.MyViewGroup>

複製程式碼

最終效果

Paste_Image.png

之後我們還會繼續來完善這個ViewGroup,讓他變成一個強大的ViewGroup。OK,onLayout可能就寫到這裡了,如果有補充的,之後會在補充。這個分析的比較簡單,為了是讓新人能快速的學會如何使用onLayout,如果大家想深入瞭解,建議去谷歌一下onLayout,有很多講的比較詳細的。 那麼我們明天見嘍。 當然,如果你喜歡,別忘了贊贊贊讚我。

相關文章