自定義ViewGroup--CascadeLayout

現在我們需要做一個撲克牌排列的佈局,如下圖:
這裏寫圖片描述
可能最容易想到的佈局方式就是使用relativelayout來完成,然後對其margin進行調整。但是這樣一來,佈局將顯得非常繁瑣。想想如果是一套撲克牌,54張呢?那得計算多少次啊!

這裏就引出了本篇文章的主題,自定義ViewGroup,其實是有自定義的ViewGroup完全可以實現上面的功能,且可以對各個子View(即每張撲克牌)進行統一管理。

在實現自定義ViewGroup之前,我們先要了解一下其原理:
繪製佈局由兩個遍歷組成,測量過程和佈局過程,測量過程由measure函數完成,該方法會從上而下的遍歷視圖樹,在遞歸遍歷的過程中,每個視圖都會向下傳遞尺寸和規格,當遍歷完成,每個視圖都保存了各自的尺寸;佈局過程則由layout函數完成,該方法也會至上而下遍歷,在遍歷過程中,每個父視圖通過測量過程的結果定位所有子視圖的位置信息。

在自定義ViewGroup過程中,這兩個過程分別在onMeasure和onLayout中完成。

下面來看代碼,代碼是最好的老師:
首先是佈局文件

//定義命名空間,後面是程序的包名
    xmlns:daven="http://schemas.android.com/apk/res/com.example.hello" 

     <com.example.hello.CascadeLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:layout_marginTop="20dp"
          daven:horizontal_spacing="20dp"
          daven:vertical_spacing="30dp">

         <View
             android:layout_width="100dp"
             android:layout_height="130dp"
             daven:layout_vertical_spacing="50dp"
             android:background="@drawable/poker_39"/>

         <View
             android:layout_width="100dp"
             android:layout_height="130dp"
             android:background="@drawable/poker_40"/>

         <View
             android:layout_width="100dp"
             android:layout_height="130dp"
             android:background="@drawable/poker_48"/>

     </com.example.hello.CascadeLayout>     

自定義屬性,首先需要在attr.xml中什麼屬性:

    <declare-styleable name="CascadeLayout">
        <attr name="horizontal_spacing" format="dimension"/>
        <attr name="vertical_spacing" format="dimension"/>    
    </declare-styleable>

    <declare-styleable name="CascadeLayout_LayoutParams">
        <attr name="layout_vertical_spacing" format="dimension"/> 
    </declare-styleable>  

這些自定義屬性可以在自定義ViewGroup的構造函數中通過context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout)來獲取。

然後下面就是完整的自定義ViewGroup的過程,這裏我們當然是要繼承ViewGroup來完成,我們將其命名爲CascadeLayout,實際上我們常用的佈局如relativelayout, linearlayout等都是繼承ViewGroup完成的。

package com.example.hello;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class CascadeLayout extends ViewGroup {

    private int mHorizontalSpacing;
    private int mVerticalSpacing;

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

    public CascadeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);

        mHorizontalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_horizontal_spacing, 30);
        mVerticalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_vertical_spacing, 30);

        a.recycle();
    }

    public static class LayoutParams extends ViewGroup.LayoutParams{
        int top;
        int left;
        public int verticalSpacing;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.CascadeLayout_LayoutParams);
            verticalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,-1);
            a.recycle();
        }

        public LayoutParams(int w, int h) {
            super(w, h);
        }   
    }

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

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

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs){
        return new LayoutParams(getContext(), attrs);

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getPaddingLeft();
        int height = getPaddingTop();
        int verticalSpacing;

        final int count = getChildCount();
        for( int i=0; i<count; i++){
            verticalSpacing = mVerticalSpacing;
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            width = getPaddingLeft() + mHorizontalSpacing * i;

            lp.top = width;
            lp.left = height;

            if( lp.verticalSpacing >= 0){
                verticalSpacing = lp.verticalSpacing;
            }

            width += child.getMeasuredWidth();
            height += verticalSpacing;
        }

        width += getPaddingRight();
        height += getChildAt(getChildCount() - 1).getMeasuredHeight()+ getPaddingBottom();

        setMeasuredDimension(resolveSize(width, widthMeasureSpec), 
                resolveSize(height, heightMeasureSpec));
    }

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

        final int count = getChildCount();

        for ( int i = 0; i<count; i++){
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();

            child.layout(lp.top, lp.left, 
                    lp.top + child.getMeasuredWidth(),
                    lp.left + child.getMeasuredHeight());
        }
    }
}

需要注意的是,如果要使得自定義的LayoutParams,需要重寫方法checkLayoutParams、generateDefaultLayoutParams以及generateLayoutParams,不過基本上寫法都一樣。

爲什麼要自定義ViewGroup?
1. 在不同的Activity中複用該視圖,更容易維護
2. 開發者可以使用自定義屬性來定製ViewGroup中子視圖的位置
3. 佈局文件更加簡明,更容易理解

其實在應用“雅虎每日新聞News Digest”中完全有使用到類似的控件,只不過人家把名字改了!
這裏寫圖片描述
該應用真的效果很不錯,這個桌面wiget也是非常不錯的。大家看看佈局層次圖,這裏他取名字爲StackView,實際上還是ViewGroup,不過他的功能比上面的CascadeLayout更加強大。
這裏寫圖片描述

該博文參考了50 Android Hacks!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章