Androd自定義控件(四)自定義類繼承viewgroup

在前面已經跟大家分享了,自定義view概述,自定義view需要知道的方法,自定義類繼承view,自定義組合控件。今天跟大家分享一下自定義類繼承viewgroup,當初挖的坑也就快填完了(四種自定義view,今天是第三種),希望大家能有所收穫。

1.自定義viewgroup和組合控件的區別

從目的來看:大部分情況下組合控件是用創建一個囊括邏輯和佈局的視圖的方式,達到重複使用而不用在不同的場合中寫重複的代碼目的,而自定義viewgroup是更傾向於自定義屬性來定製 ViewGroup 中子視圖的位置。

從實現方式來看:組合控件的一般需要加載一個已經寫好的佈局,聲明方法來控制佈局中寫好的控件,不需要自己處理viewgroup的測量和佈局的過程,比如自定義的App頂欄等。而自定義viewgroup一般是通過measure和layout來控制子view的位置,比如流式佈局等。

2.ViewGroup概述

A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.

一個ViewGroup是一個可以包含其他view的特殊View,ViewGroup是各個Layout和View容器組件的基類。這個類還定義了ViewGroup.LayoutParams類來作爲佈局參數的基類。

LayoutParams are used by views to tell their parents how they want to be laid out.
The base LayoutParams class just describes how big the view wants to be for both width and height.

LayoutParams 通常是子view用來告訴父容器他們的位置。基類LayoutParams 僅僅描述了子view的寬和高。

3.ViewGroup和LayoutParams之間的關係

不知道大家有沒有注意到,當我們在LinearLayout中寫子View的時候,可以用layout_gravity,layout_weight屬性;而zaiRelativeLayout中的子View有layout_centerInParent屬性,卻沒有layout_gravity,layout_weight,這是爲什麼呢?這是因爲每個ViewGroup需要指定一個LayoutParams,用於確定支持子View哪些屬性。

4.Android繪製視圖的方式

Layout is a two pass process: a measure pass and a layout pass. The measuring pass is implemented in measure(int, int) and is a top-down traversal of the view tree. Each view pushes dimension specifications down the tree during the recursion. At the end of the measure pass, every view has stored its measurements. The second pass happens in layout(int, int, int, int) and is also top-down. During this pass each parent is responsible for positioning all of its children using the sizes computed in the measure pass.

繪製佈局由兩個遍歷過程組成: 測量過程和佈局過程。 測量過程由 measure(int, int) 方法完成, 該方法從上到下遍歷視圖樹。 在遞歸遍歷過程中, 每個視圖都會向下層傳遞尺寸和規格。 當measure 方法遍歷結束, 每個視圖都保存了各自的尺寸信息。 第二個過程由 layout(int, int, int,int) 方法完成, 該方法也是由上而下遍歷視圖樹, 在遍歷過程中, 每個父視圖通過測量過程的結果定位所有子視圖的位置信息。

5.自定義viewgroup的步驟

我們以官方文檔中的demo爲例。這是一個相對綜合的例子,處理的幾乎所有的佈局情況。通過這個demo我們可以舉一反三,把特殊情況簡單。先看下效果圖:

這裏寫圖片描述

實現效果:自定義類繼承viewgroup實現linearlayout橫向擺放的效果,並且自定義屬性“layout_position”來控制子view在水平方向的位置。

自定義類繼承ViewGroup,並初始化

@RemoteViews.RemoteView
public class CustomLayout extends ViewGroup {
    /** The amount of space used by children in the left gutter. */
    private int mLeftWidth;

    /** The amount of space used by children in the right gutter.
     * 計算右側的子view需要的空間。
     */
    private int mRightWidth;

    /** These are used for computing child frames based on their gravity.
     * 計算子view基於他們gravity的畫面
     */
    private final Rect mTmpContainerRect = new Rect();
    private final Rect mTmpChildRect = new Rect();

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

    public CustomLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
}

這裏需要注意的一點是,如果我們自定義的viewgroup不需要滾動的話,儘量重寫shouldDelayChildPressedState方法,並返回false。

Return true if the pressed state should be delayed for children or descendants of this ViewGroup. Generally, this should be done for containers that can scroll, such as a List. This prevents the pressed state from appearing when the user is actually trying to scroll the content. The default implementation returns true for compatibility reasons. Subclasses that do not scroll should generally override this method and return false.

官方文檔中的說明是,當返回false的時候,如果用戶試圖滾動內容,會阻止這個viewgroup出現按壓狀態。出於兼容的目的,這個方法默認是返回true的。

/**
 * Any layout manager that doesn't scroll will want this.
 */
@Override
public boolean shouldDelayChildPressedState() {
    return false;
}

爲子視圖添加自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="CustomLayoutLP">
        <attr name="android:layout_gravity"/>
        <attr name="layout_position">
            <enum name="middle" value="0"/>
            <enum name="left" value="1"/>
            <enum name="right" value="2"/>
        </attr>
    </declare-styleable>

</resources>

因爲屬性名的前綴是layout_,沒有包含一個視圖屬性,因此該屬性會被添加到LayoutParams的屬性表中。

同時,在onMeasure方法之前,要先創建一個自定義的LayoutParams,該類用於存儲每個子視圖的gravity和position。

 /**
     * Custom per-child layout information.
     * 創建自定義 LayoutParams類, 該類用於保存每個子視圖的信息(gravity,position)
     */
    public static class LayoutParams extends MarginLayoutParams {
        /**
         * The gravity to apply with the View to which these layout parameters
         * are associated.
         */
        public int gravity = Gravity.TOP | Gravity.START;

        public static int POSITION_MIDDLE = 0;
        public static int POSITION_LEFT = 1;
        public static int POSITION_RIGHT = 2;

        public int position = POSITION_MIDDLE;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            // Pull the layout param values from the layout XML during
            // inflation.  This is not needed if you don't care about
            // changing the layout behavior in XML.
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
            gravity = a.getInt(R.styleable.CustomLayoutLP_android_layout_gravity, gravity);
            position = a.getInt(R.styleable.CustomLayoutLP_layout_position, position);
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }

要使用新定義的layoutparams,我們可能需要重寫的方法爲:

// The rest of the implementation is for custom per-child layout parameters.
    // If you do not need these (for example you are writing a layout manager
    // that does fixed positioning of its children), you can drop all of this.
 @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new CustomLayout.LayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

但是從註釋中我們瞭解到:如果我們用不到這些方法,比如我們的viewgroup中的子view是固定的,那麼我們可以丟掉他們。

重寫onMeasure()方法

/**
     * Ask all children to measure themselves and compute the measurement of this
     * layout based on the children.
     * 令每個子視圖測量自身,計算該viewgroup基於子view的尺寸
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        // These keep track of the space we are using on the left and right for
        // views positioned there; we need member variables so we can also use
        // these for layout later.
        mLeftWidth = 0;
        mRightWidth = 0;

        // Measurement will ultimately be computing these values.
        //使用寬和高計算佈局的最終大小
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        // Iterate through all children, measuring them and computing our dimensions
        // from their size.
        //遍歷所有的孩子,從他們的大小測量和計算我們的大小
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // Measure the child.
                //令每個子視圖測量自身
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

                // Update our size information based on the layout params.  Children
                // that asked to be positioned on the left or right go in those gutters.
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (lp.position == LayoutParams.POSITION_LEFT) {
                    mLeftWidth += Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                } else if (lp.position == LayoutParams.POSITION_RIGHT) {
                    mRightWidth += Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                } else {
                    maxWidth = Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                }
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
            }
        }

        // Total width is the maximum width of all inner children plus the gutters.
        maxWidth += mLeftWidth + mRightWidth;

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Report our final dimensions.
        //使用計算的到的寬和高設置整個佈局的測量尺寸
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }

最後一步爲onLayout()方法

/**
     * Position all children within this layout.
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();

        // These are the far left and right edges in which we are performing layout.
        int leftPos = getPaddingLeft();
        int rightPos = right - left - getPaddingRight();

        // This is the middle region inside of the gutter.
        final int middleLeft = leftPos + mLeftWidth;
        final int middleRight = rightPos - mRightWidth;

        // These are the top and bottom edges in which we are performing layout.
        final int parentTop = getPaddingTop();
        final int parentBottom = bottom - top - getPaddingBottom();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                // Compute the frame in which we are placing this child.
                if (lp.position == LayoutParams.POSITION_LEFT) {
                    mTmpContainerRect.left = leftPos + lp.leftMargin;
                    mTmpContainerRect.right = leftPos + width + lp.rightMargin;
                    leftPos = mTmpContainerRect.right;
                } else if (lp.position == LayoutParams.POSITION_RIGHT) {
                    mTmpContainerRect.right = rightPos - lp.rightMargin;
                    mTmpContainerRect.left = rightPos - width - lp.leftMargin;
                    rightPos = mTmpContainerRect.left;
                } else {
                    mTmpContainerRect.left = middleLeft + lp.leftMargin;
                    mTmpContainerRect.right = middleRight - lp.rightMargin;
                }
                mTmpContainerRect.top = parentTop + lp.topMargin;
                mTmpContainerRect.bottom = parentBottom - lp.bottomMargin;

                // Use the child's gravity and size to determine its final
                // frame within its container.
                Gravity.apply(lp.gravity, width, height, mTmpContainerRect, mTmpChildRect);

                // Place the child.
                //放置子view
                child.layout(mTmpChildRect.left, mTmpChildRect.top,
                        mTmpChildRect.right, mTmpChildRect.bottom);
            }
        }
    }

上述邏輯並不複雜,循環調用子view的onlayout方法,根據onMeasure的到的參數對子view進行佈局。

在佈局文件中使用

<?xml version="1.0" encoding="utf-8"?>
<oracleen.customlayout.CustomLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- put first view to left. -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="fill_vertical|center_horizontal"
        android:background="@color/test1"
        android:text="l1"
        app:layout_position="left"/>

    <!-- stack second view to left. -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="fill_vertical|center_horizontal"
        android:background="@color/test1"
        android:text="l2"
        app:layout_position="left"/>

    <!-- also put a view on the right. -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="fill_vertical|center_horizontal"
        android:background="@color/test1"
        android:text="r1"
        app:layout_position="right"/>

    <!-- by default views go in the middle; use fill vertical gravity -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="fill_vertical|center_horizontal"
        android:background="@color/test2"
        android:text="fill-vert"/>

    <!-- by default views go in the middle; use fill horizontal gravity -->
    <TextView
        android:layout_width="30dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|fill_horizontal"
        android:background="@color/test2"
        android:text="fill-horiz"/>

    <!-- by default views go in the middle; use top-left gravity -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top|left"
        android:background="@color/test3"
        android:text="top-left"/>

    <!-- by default views go in the middle; use center gravity -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:background="@color/test3"
        android:text="center"/>

    <!-- by default views go in the middle; use bottom-right -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:background="@color/test3"
        android:text="bottom-right"/>

</oracleen.customlayout.CustomLayout>

好,到這裏基本就結束了。源碼我也上傳了,大部分方法都加了註釋,點擊這裏下載。

發佈了40 篇原創文章 · 獲贊 143 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章