Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義ViewGroup實戰-----流式布局

Android自定義ViewGroup實戰-----流式布局

編輯:關於Android編程

本文是參考了鴻神之後的文章之後做的一些修改與總結,添加了一些自己的筆記,增加對自定義ViewGroup的理解。文章後面會給出原文地址。

首先,什麼是流式布局(FlowLayout),我個人的理解就是各個寬高不完全相同的view控件之間按照一定的規律放置,當一行或者一列中放滿了控件,再放置下一個控件時,由於空間寬度或者高度不夠,會自動放置到下一行或者下一列。比較常見的應用是一些熱門標簽,精彩評論等。比如下圖中的尺寸大小標簽。\ \

其中上圖中的尺寸大小作為一個整體的ViewGroup,其中放置著各個子控件,由於他們寬度大小不一,不很很好的運用現有的布局來實現,而自定義的流式ViewGroup很好的可以實現。

相比於自定義View,最重要的是onDraw()方法,自定義的ViewGroup最重要的是onMeasure(),onLayout()。一個是幫助我們去測量整個ViewGroup的子控件,另一個是幫助我們去在我們的ViewGroup中去放置我們的子控件。

1.整體分析

1、由於每一個ViewGroup都對應著一個LayoutParams,都需要指定一個LayoutParams,對於FlowLayout,我們目前只需要能夠識別控件之間的margin即可,即使用MarginLayoutParams.

2、onMeasure中計算所有childView的寬和高,然後根據childView的寬和高,計算自己的寬和高。(當然,如果父控件的寬或者高不是wrap_content,直接使用父ViewGroup傳入的計算值即可)

3、onLayout中對所有的childView進行布局。

2.LayoutParams

因為我們只需要支持margin,所以直接使用系統的MarginLayoutParams,重寫其中的generateLayoutParams方法

 

@Override  
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)  
{  
    return new MarginLayoutParams(getContext(), attrs);  
}  
3.onMeasure()方法

 

onMeasure完成對所有子控件的測量

 

/** 
     * 負責設置子控件的測量模式和大小 根據所有子控件設置自己的寬和高 
     */  
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
    {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
        // 獲得它的父容器為它設置的測量模式和大小  
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);  
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);  
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);  
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);  
  
        Log.e(TAG, sizeWidth + "," + sizeHeight);  
  
        // 如果是warp_content情況下,記錄寬和高  
        int width = 0;  
        int height = 0;  
        /** 
         * 記錄每一行的寬度,width不斷取最大寬度 
         */  
        int lineWidth = 0;  
        /** 
         * 每一行的高度,累加至height 
         */  
        int lineHeight = 0;  
  
        int cCount = getChildCount();  
  
        // 遍歷每個子元素  
        for (int i = 0; i < cCount; i++)  
        {  
            View child = getChildAt(i);  
            // 測量每一個child的寬和高  
            measureChild(child, widthMeasureSpec, heightMeasureSpec);  
            // 得到child的lp  
            MarginLayoutParams lp = (MarginLayoutParams) child  
                    .getLayoutParams();  
            // 當前子空間實際占據的寬度  
            int childWidth = child.getMeasuredWidth() + lp.leftMargin  
                    + lp.rightMargin;  
            // 當前子空間實際占據的高度  
            int childHeight = child.getMeasuredHeight() + lp.topMargin  
                    + lp.bottomMargin;  
            /** 
             * 如果加入當前child,則超出最大寬度,則的到目前最大寬度給width,類加height 然後開啟新行 
             */  
            if (lineWidth + childWidth > sizeWidth-getPaddingLeft()-getPaddingRight())  
            {  
                width = Math.max(lineWidth, width);// 取最大的  
                lineWidth = childWidth; // 重新開啟新行,開始記錄  
                // 疊加當前高度,  
                height += lineHeight;  
                // 開啟記錄下一行的高度  
                lineHeight = childHeight;  
            } else  
            // 否則累加值lineWidth,lineHeight取最大高度  
            {  
                lineWidth += childWidth;  
                lineHeight = Math.max(lineHeight, childHeight);  
            }  
            // 如果是最後一個,則將當前記錄的最大寬度和當前lineWidth做比較  
            if (i == cCount - 1)  
            {  
                width = Math.max(width, lineWidth);  
                height += lineHeight;  
            }  
  
        }  
        setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth  
                : width, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight  
                : height);  
  
    }  
首先得到其父容器傳入的測量模式和寬高的計算值,然後遍歷所有的childView,使用measureChild方法對所有的childView進行測量。然後如果父ViewGroup的寬和高設置為wrap_content,我們通過測量計算得到所有childView的寬和高。最後根據測量模式,如果是MeasureSpec.EXACTLY則直接使用父ViewGroup傳入的寬和高,否則設置為上面自己計算的寬和高。

 

其中第48行是考慮到假如我們父ViewGroup使用了padding這一屬性的話,那麼我們就得減去這個值。

第50行,原文中寫的是width=Math.max(lineWidth,childWidth);//取最大的 這樣會有個問題,假如第一行最寬,後面逐漸變小,那麼我們取的width也就有問題了,故修改過來。
還有第63到第67行,因為在計算最後一個控件時,不管換行不換行,我們都沒有比較最後一個控件的寬(假如沒換行)和將最後一個控件的高度加上去(假如是換行了)

 

4.onLayout()方法

onLayout中完成對所有childView的位置以及大小的指定

 

/** 
     * 存儲所有的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();  
  
        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();  
  
            // 如果已經需要換行  
            if (childWidth + lp.leftMargin + lp.rightMargin + lineWidth > width-getPaddingLeft()-getPaddingRight())  
            {  
                // 記錄這一行所有的View以及最大高度  
                mLineHeight.add(lineHeight);  
                // 將當前行的childView保存,然後開啟新的ArrayList保存下一行的childView  
                mAllViews.add(lineViews);  
                lineWidth = 0;// 重置行寬  
                lineViews = new ArrayList();  
            }  
            /** 
             * 如果不需要換行,則累加 
             */  
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;  
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin  
                    + lp.bottomMargin);  
            lineViews.add(child);  
        }  
        // 記錄最後一行  
        mLineHeight.add(lineHeight);  
        mAllViews.add(lineViews);  
  
        int left = getPaddingLeft();
        int top = getPaddingTop();
        // 得到總行數  
        int lineNums = mAllViews.size();  
        for (int i = 0; i < lineNums; i++)  
        {  
            // 每一行的所有的views  
            lineViews = mAllViews.get(i);  
            // 當前行的最大高度  
            lineHeight = mLineHeight.get(i);  
  
            Log.e(TAG, "第" + i + "行 :" + lineViews.size() + " , " + lineViews);  
            Log.e(TAG, "第" + i + "行, :" + lineHeight);  
  
            // 遍歷當前行所有的View  
            for (int j = 0; j < lineViews.size(); j++)  
            {  
                View child = lineViews.get(j);  
                if (child.getVisibility() == View.GONE)  
                {  
                    continue;  
                }  
                MarginLayoutParams lp = (MarginLayoutParams) child  
                        .getLayoutParams();  
  
                //計算childView的left,top,right,bottom  
                int lc = left + lp.leftMargin;  
                int tc = top + lp.topMargin;  
                int rc =lc + child.getMeasuredWidth();  
                int bc = tc + child.getMeasuredHeight();  
  
                Log.e(TAG, child + " , l = " + lc + " , t = " + t + " , r ="  
                        + rc + " , b = " + bc);  
  
                child.layout(lc, tc, rc, bc);  
                  
                left += child.getMeasuredWidth() + lp.rightMargin  
                        + lp.leftMargin;  
            }  
            left = getPaddingLeft();  
            top += lineHeight;  
        }  
  
    }  
allViews的每個Item為每行所有View的List集合。

 

 

mLineHeight記錄的為每行的最大高度。

23-48行,遍歷所有的childView,用於設置allViews的值,以及mLineHeight的值。

57行,根據allViews的長度,遍歷所有的行數

67-91行,遍歷每一行的中所有的childView,對childView的left , top , right , bottom 進行計算,和定位。

92-93行,重置left和top,准備計算下一行的childView的位置。

好了,到此完成了所有的childView的繪制區域的確定,到此,我們的FlowLayout的代碼也結束了

 

由於考慮到了padding,第32行要減去padding的大小,53、54行初始的left和top值也要相應的變化,同理第92行

其中第28-29行,利用的是view.getMeasureWidth()方法,為什麼不利用view.getWidth()方法呢,其實在這個例子中,你用這個也沒什麼影響。但是最好是選擇前者,其中下圖是兩者的最直接的區別的顯示。
\

左邊是getHeight,右邊是getMeasureHeight

 

5.布局文件如下

 



    

        

        

        

        

        
        

        

        

        

        
        

        

        

        

        
    


其中用到了style文件,節省了我們重復定義的時間,要修改時,我們只用修改這一個就好

 

style.xml

 


drawable設備背景

 

 



    
      
      
      


最終的效果如上圖所示。

 

6.動態添加View

當然,最後你的控件多半也不是固定大小的,你也初始不知道的,那得要動態添加。

那我們把布局文件改下,改成只有一個父ViewGroup,其余控件我們動態添加。

其中在要添加的Activity的onCreate方法中添加initView()方法

 

private void initView() {
		myFlowLayout = (MyFlowLayout) findViewById(R.id.myFlowLayout);
		// ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
		// ViewGroup.LayoutParams.WRAP_CONTENT,
		// ViewGroup.LayoutParams.WRAP_CONTENT);
		// lp.leftMargin = 35;
		// lp.rightMargin = 35;
		// lp.topMargin = 10;
		// lp.bottomMargin = 10;
		// for (int i = 0; i < str.length; i++) {
		// Button btn=new Button(this);
		// btn.setText(str[i]);
		// myFlowLayout.addView(btn,lp);
		// }
		LayoutInflater mInflater = LayoutInflater.from(this);
		for (int i = 0; i < str.length; i++) {
			TextView tv = (TextView) mInflater.inflate(R.layout.myview,
					myFlowLayout, false);
			tv.setText(str[i]);
			tv.setTag(i);
			tv.setOnClickListener(this);
			myFlowLayout.addView(tv);

		}
	}

 

其中我們用到了LayoutInflater,它可以將我們的xml布局文件轉變為view,這是動態添加的基礎。

7.添加點擊事件

可能你會對各個textView有點擊事件的要求,我們可以使用setTag方法。後期根據tag來區分不同的textView。

 

@Override
	public void onClick(View v) {
		reSetTextBg();
		switch ((int) v.getTag()) {
		case 0:
			v.setBackgroundColor(0xffff0000);
			break;
		case 1:
			v.setBackgroundColor(0xffff0000);
			break;
		case 2:
			v.setBackgroundColor(0xffff0000);
			break;
		default:
			break;
		}

	}
        //重置標簽顏色
	private void reSetTextBg() {
		for (int i = 0; i < str.length; i++) {
			TextView tView = (TextView) myFlowLayout.getChildAt(i);
			tView.setBackground(getResources().getDrawable(
					R.drawable.my_textview_bg));
		}
	}
\

 

好了,全文結束,有什麼問題,好的想法,歡迎留言指出!

另:如果你覺得本篇博客對你有用,那麼就頂一個~~

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