Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> [Android] 自定義ViewGroup最佳入門實踐

[Android] 自定義ViewGroup最佳入門實踐

編輯:關於Android編程

1.View 繪制流程

ViewGroup也是繼承於View,下面看看繪制過程中依次會調用哪些函數。

 這裡寫圖片描述

說明:

measure()和onMeasure()

在View.Java源碼中:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> public final void measure(int widthMeasureSpec,int heightMeasureSpec){ ... onMeasure ... } protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }

可以看出measure()是被final修飾的,這是不可被重寫。onMeasure在measure方法中調用的,當我們繼承View的時候通過重寫onMeasure方法來測量控件大小。

layout()和onLayout(),draw()和onDraw()類似。

dispatchDraw()

View 中這個函數是一個空函數,ViewGroup 復寫了dispatchDraw()來對其子視圖進行繪制。自定義的 ViewGroup 一般不對dispatchDraw()進行復寫。

requestLayout()

當布局變化的時候,比如方向變化,尺寸的變化,會調用該方法,在自定義的視圖中,如果某些情況下希望重新測量尺寸大小,應該手動去調用該方法,它會觸發measure()和layout()過程,但不會進行 draw。

自定義ViewGroup的時候一般復寫

onMeasure()方法:

計算childView的測量值以及模式,以及設置自己的寬和高 

onLayout()方法,

 對其所有childView的位置進行定位

View樹:

這裡寫圖片描述

 樹的遍歷是有序的,由父視圖到子視圖,每一個 ViewGroup 負責測繪它所有的子視圖,而最底層的 View 會負責測繪自身。

measure:

自上而下進行遍歷,根據父視圖對子視圖的MeasureSpe╧y"/kf/yidong/wp/" target="_blank" class="keylink">WPS1LywQ2hpbGRWaWV319TJ7bXEss7K/aOszai5/TwvcD4NCjxwcmUgY2xhc3M9"brush:java;"> getChildMeasureSpec(parentHeightMeasure,mPaddingTop+mPaddingBottom,lp.height)

獲取ChildView的MeasureSpec,回調ChildView.measure最終調用setMeasuredDimension得到ChildView的尺寸:

mMeasuredWidth 和 mMeasuredHeight

Layout :

 也是自上而下進行遍歷的,該方法計算每個ChildView的ChildLeft,ChildTop;與measure中得到的每個ChildView的mMeasuredWidth 和 mMeasuredHeight,來對ChildView進行布局。
 

child.layout(left,top,left+width,top+height)

2.onMeasure過程

measure過程會為一個View及所有子節點的mMeasuredWidth
和mMeasuredHeight變量賦值,該值可以通過getMeasuredWidth()和getMeasuredHeight()方法獲得。

onMeasure過程傳遞尺寸的兩個類:

ViewGroup.LayoutParams (ViewGroup 自身的布局參數)

用來指定視圖的高度和寬度等參數,使用 view.getLayoutParams() 方法獲取一個視圖LayoutParams,該方法得到的就是其所在父視圖類型的LayoutParams,比如View的父控件為RelativeLayout,那麼得到的 LayoutParams 類型就為RelativeLayoutParams。

①具體值  

②MATCH_PARENT 表示子視圖希望和父視圖一樣大(不包含 padding 值)  

③WRAP_CONTENT 表示視圖為正好能包裹其內容大小(包含 padding 值)

MeasureSpecs

測量規格,包含測量要求和尺寸的信息,有三種模式:

①UNSPECIFIED

父視圖不對子視圖有任何約束,它可以達到所期望的任意尺寸。比如 ListView、ScrollView,一般自定義 View 中用不到

②EXACTLY 

父視圖為子視圖指定一個確切的尺寸,而且無論子視圖期望多大,它都必須在該指定大小的邊界內,對應的屬性為 match_parent 或具體值,比如 100dp,父控件可以通過MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸。

③AT_MOST 

父視圖為子視圖指定一個最大尺寸。子視圖必須確保它自己所有子視圖可以適應在該尺寸范圍內,對應的屬性為 wrap_content,這種模式下,父控件無法確定子 View 的尺寸,只能由子控件自己根據需求去計算自己的尺寸,這種模式就是我們自定義視圖需要實現測量邏輯的情況。 

3.onLayout 過程

子視圖的具體位置都是相對於父視圖而言的。View 的 onLayout 方法為空實現,而 ViewGroup 的 onLayout 為 abstract 的,因此,如果自定義的自定義ViewGroup 時,必須實現 onLayout 函數。

在 layout 過程中,子視圖會調用getMeasuredWidth()和getMeasuredHeight()方法獲取到 measure 過程得到的 mMeasuredWidth 和 mMeasuredHeight,作為自己的 width 和 height。然後調用每一個子視圖的layout(l, t, r, b)函數,來確定每個子視圖在父視圖中的位置。

4.示例程序

先上效果圖:

這裡寫圖片描述

代碼中有詳細的注釋,結合上文中的說明,理解應該沒有問題。這裡主要貼出核心代碼。

FlowLayout.java中(參照陽神的慕課課程)

onMeasure方法

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        // 獲得它的父容器為它設置的測量模式和大小
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // 用於warp_content情況下,來記錄父view寬和高
        int width = 0;
        int height = 0;

        // 取每一行寬度的最大值
        int lineWidth = 0;
        // 每一行的高度累加
        int lineHeight = 0;

        // 獲得子view的個數
        int cCount = getChildCount();

        for (int i = 0; i < cCount; i++)
        {
            View child = getChildAt(i);
            // 測量子View的寬和高(子view在布局文件中是wrap_content)
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 得到LayoutParams
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            // 根據測量寬度加上Margin值算出子view的實際寬度(上文中有說明)
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            // 根據測量高度加上Margin值算出子view的實際高度
            int childHeight = child.getMeasuredHeight() + lp.topMargin+ lp.bottomMargin;

            // 這裡的父view是有padding值的,如果再添加一個元素就超出最大寬度就換行
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight())
            {
                // 父view寬度=以前父view寬度、當前行寬的最大值
                width = Math.max(width, lineWidth);
                // 換行了,當前行寬=第一個view的寬度
                lineWidth = childWidth;
                // 父view的高度=各行高度之和
                height += lineHeight;
                //換行了,當前行高=第一個view的高度
                lineHeight = childHeight;
            } else{
                // 疊加行寬
                lineWidth += childWidth;
                // 得到當前行最大的高度
                lineHeight = Math.max(lineHeight, childHeight);
            }
            // 最後一個控件
            if (i == cCount - 1)
            {
                width = Math.max(lineWidth, width);
                height += lineHeight;
            }
        }
        /**
         * EXACTLY對應match_parent 或具體值
         * AT_MOST對應wrap_content
         * 在FlowLayout布局文件中
         * android:layout_width="fill_parent"
         * android:layout_height="wrap_content"
         *
         * 如果是MeasureSpec.EXACTLY則直接使用父ViewGroup傳入的寬和高,否則設置為自己計算的寬和高。
         */
        setMeasuredDimension(
                modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop()+ getPaddingBottom()
        );

    }

onLayout方法

 //存儲所有的View
    private List> mAllViews = new ArrayList>();
    //存儲每一行的高度
    private List mLineHeight = new ArrayList();

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
        mAllViews.clear();
        mLineHeight.clear();

        // 當前ViewGroup的寬度
        int width = getWidth();

        int lineWidth = 0;
        int lineHeight = 0;
        // 存儲每一行所有的childView
        List lineViews = new ArrayList();

        int cCount = getChildCount();

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

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin+ lp.bottomMargin);
            lineViews.add(child);

            // 換行,在onMeasure中childWidth是加上Margin值的
            if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight())
            {
                // 記錄行高
                mLineHeight.add(lineHeight);
                // 記錄當前行的Views
                mAllViews.add(lineViews);

                // 新行的行寬和行高
                lineWidth = 0;
                lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
                // 新行的View集合
                lineViews = new ArrayList();
            }

        }
        // 處理最後一行
        mLineHeight.add(lineHeight);
        mAllViews.add(lineViews);

        // 設置子View的位置

        int left = getPaddingLeft();
        int top = getPaddingTop();

        // 行數
        int lineNum = mAllViews.size();

        for (int i = 0; i < lineNum; i++)
        {
            // 當前行的所有的View
            lineViews = mAllViews.get(i);
            lineHeight = mLineHeight.get(i);

            for (int j = 0; j < lineViews.size(); j++)
            {
                View child = lineViews.get(j);
                // 判斷child的狀態
                if (child.getVisibility() == View.GONE)
                {
                    continue;
                }

                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                int lc = left + lp.leftMargin;
                int tc = top + lp.topMargin;
                int rc = lc + child.getMeasuredWidth();
                int bc = tc + child.getMeasuredHeight();

                // 為子View進行布局
                child.layout(lc, tc, rc, bc);

                left += child.getMeasuredWidth() + lp.leftMargin+ lp.rightMargin;
            }
            left = getPaddingLeft() ;
            top += lineHeight ;
        }

    }

    /**
     * 因為我們只需要支持margin,所以直接使用系統的MarginLayoutParams
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs)
    {
        return new MarginLayoutParams(getContext(), attrs);
    }

以及MainActivity.java

public class MainActivity extends Activity {

    LayoutInflater mInflater;
    @InjectView(R.id.id_flowlayout1)
    FlowLayout idFlowlayout1;
    @InjectView(R.id.id_flowlayout2)
    FlowLayout idFlowlayout2;
    private String[] mVals = new String[]
            {"Do", "one thing", "at a time", "and do well.", "Never", "forget",
                    "to say", "thanks.", "Keep on", "going ", "never give up."};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.inject(this);
        mInflater = LayoutInflater.from(this);
        initFlowlayout2();
    }

    public void initFlowlayout2() {
        for (int i = 0; i < mVals.length; i++) {
            final RelativeLayout rl2 = (RelativeLayout) mInflater.inflate(R.layout.flow_layout, idFlowlayout2, false);
            TextView tv2 = (TextView) rl2.findViewById(R.id.tv);
            tv2.setText(mVals[i]);
            rl2.setTag(i);
            idFlowlayout2.addView(rl2);
            rl2.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int i = (int) v.getTag();
                    addViewToFlowlayout1(i);
                    rl2.setBackgroundResource(R.drawable.flow_layout_disable_bg);
                }
            });

        }
    }
    public void addViewToFlowlayout1(int i){
        RelativeLayout rl1 = (RelativeLayout) mInflater.inflate(R.layout.flow_layout, idFlowlayout1, false);
        ImageView iv = (ImageView) rl1.findViewById(R.id.iv);
        iv.setVisibility(View.VISIBLE);
        TextView tv1 = (TextView) rl1.findViewById(R.id.tv);
        tv1.setText(mVals[i]);
        rl1.setTag(i);
        idFlowlayout1.addView(rl1);
        rl1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int i = (int) v.getTag();
                idFlowlayout1.removeView(v);
                View view = idFlowlayout2.getChildAt(i);
                view.setBackgroundResource(R.drawable.flow_layout_bg);
            }
        });
    }

這個項目源碼已近上傳,想要看源碼的朋友可以

點擊 FlowLayout

如果有什麼疑問可以給我留言,不足之處歡迎在github上指出,謝謝!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved