天天看點

android 自定義控件--用viewGroup實作流式布局

java布局中有一個流式布局,但是android布局中并沒有。手機上用到流式布局大概就是熱門标簽的添加吧。流式布局就是控件一個一個的自動往右添加,如果超出寬度,則自動到下一行。

步驟分析

1.對于本布局,我們需要能得到margin屬性的LayoutParams,即MarginLayoutParams.

2.在onMeasure()方法中計算所有子view的高度和寬度,以便得到FlowLayout 的寬高(流式布局為warp_content模式)。

3.在onLayout()方法中放置所有子view的位置。

解決問題

1.得到MarginLayoutParams

隻需要我們重寫generateLayoutParams()方法

public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
    {
        return new MarginLayoutParams(getContext(), attrs);
    }
           

2.onMeasure()計算寬高

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //如果為warp_content模式下測量的寬高
        int width = ;
        int height = ;

        //記錄每一行的寬高
        int lineW = ;
        int lineH = ;

        int childCount = getChildCount();
        for (int i = ; i < childCount; i++) {
           View child = getChildAt(i);//擷取每一個子view
            measureChild(child,widthMeasureSpec,heightMeasureSpec);//測量子view
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childW = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;//得到子view的寬高
            int childH = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

            //如果放入該view是超過父布局寬度,需要換行,那麼高度累加,寬度取目前行與該子view最大的為父布局寬度
            if (childW + lineW > widthSize - getPaddingLeft() - getPaddingRight()){
                width = Math.max(lineW,childW);
                height +=lineH;//高度累加

                //開啟新行
                lineW = childW;
                lineH = childH;
            }else {//如果不換行,則寬度累加,高度取最大值
                lineW += childW;
                lineH = Math.max(lineH,childH);
            }

            if (i == childCount -){//最後一個子view
                width = Math.max(width, lineW);
                height += lineH;
            }
        }

        Log.i("FLOW",width+"   "+height);
        setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width +getPaddingLeft()+getPaddingRight(),
                (heightMode == MeasureSpec.EXACTLY ? heightSize : height +getPaddingTop()+getPaddingBottom()));
    }
           

首先得到父布局的測量模式和寬高,然後周遊所有的子view,得到子view的寬高,計算父布局wrap_content模式下的寬高,最後根據模式設定父布局的寬高。但是在測量時應注意一點,在周遊到最後一個子view時,可能會換行,會走換行的if語句,但是并沒有将在view的高度進行累加,是以要單獨寫一個判斷進行累加。

3.onLayout()為子view布局

List<List<View>> allViews  = new ArrayList<>();
    List<Integer> lineH = new ArrayList<>();
 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        allViews.clear();
        lineH.clear();

        int width = getWidth();//父布局的寬度

        int lineWidth = ;
        int lineHeight = ;

        //存放每一行的子view
        List<View> lineViews = new ArrayList<>();

        int childCount = getChildCount();

        for (int i = ; i < childCount; i++) {

            View child = getChildAt(i);//得到view執行個體

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

            //得到子view的寬高
            int childWidth = child.getMeasuredWidth() + lp.leftMargin +lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.bottomMargin +lp.topMargin;

            if (lineWidth + childWidth > (width -getPaddingLeft() - getPaddingRight())){//如果需要換行
                lineH.add(lineHeight);//儲存這一行的view以及最大高度
                allViews.add(lineViews);
                //重置寬高
                lineWidth = ;
                lineHeight = ;
                lineViews = new ArrayList<>();
            }
            //如果不換行,則行高等于最高的,行寬累加
                lineWidth = lineWidth + childWidth;
                lineHeight = Math.max(lineHeight,childHeight);
                lineViews.add(child);


        }
        lineH.add(lineHeight);
        allViews.add(lineViews);

        int lineNums = allViews.size();
        int left = getPaddingLeft();
        int top = getPaddingTop();
        for (int i =; i < lineNums; i++) {
            lineViews = allViews.get(i);
            lineHeight = lineH.get(i);

            //周遊每一行的view
            for (int j = ; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                if (child.getVisibility() == View.GONE){
                    continue;
                }

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

                //計算子view的坐标
                int lc = left +lp.leftMargin;
                int tc = top +lp.topMargin;
                int rc = lc + child.getMeasuredWidth();
                int bc = tc + child.getMeasuredHeight();

                child.layout(lc,tc,rc,bc);
                left += child.getMeasuredWidth() + lp.rightMargin
                        + lp.leftMargin;
            }
            //重置left和top 為下一行的計算坐準備
            left = getPaddingLeft();
            top +=lineHeight;
        }
    }
           

代碼分析:

allViews 存放所有的子view,lineH 存放每一行的最大高度,lineView 存放每一行的view。

然後周遊所有子view,設定每一行的高度,和每一行的子view,最後周遊每一行的子view。設定每一個view的left,top,right,bottom.

測試

我用幾個textView來測試,看一看效果

在res/values/styles.xml中:

<style name="text_flag_01">  
       <item name="android:layout_width">wrap_content</item>  
       <item name="android:layout_height">wrap_content</item>  
       <item name="android:layout_margin">dp</item>  
       <item name="android:background">@drawable/flag_01</item>  
       <item name="android:textColor">#ffffff</item>  
   </style>
           

frag_01.xml

<?xml version="1.0" encoding="utf-8"?>  
<shape xmlns:android="http://schemas.android.com/apk/res/android" >  

    <solid android:color="#7690A5" >  
    </solid>  

    <corners android:radius="5dp"/>  
    <padding  
        android:bottom="2dp"  
        android:left="10dp"  
        android:right="10dp"  
        android:top="2dp" />  

</shape>  
           

item_flow.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"

    style="@style/text_flag_01"
    android:layout_margin="5dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >

</TextView>
           

代碼動态添加textview

FlowLayout flow;
String[] str = new String[]{"hallo world1","text","FlowLayout Image3","hallo world1",
            "textView2","FlowLayout Image3","hallo world1",
            "textView2","FlowLayout Image3"};

 LayoutInflater inflater = LayoutInflater.from(this);
        for (int i = ; i < str.length; i++) {
            TextView tv = (TextView) inflater.inflate(R.layout.item_flow,flow,false);
            tv.setText(str[i]);
            flow.addView(tv);
        }
           

最後效果如圖

android 自定義控件--用viewGroup實作流式布局

到這裡,流式布局基本上就實作了,如果想動态添加,可以自己定義一個接口實作單個添加标簽。

優化

上面的方法實作了流式布局,但是我們可以看到,在onMeasure()和onLayout()方法中都計算了子view的寬高。如此,我們可不可以隻計算一次呢,在onMeasure()中就将view的坐标計算好呢?

要解決這個問題,就需要有一個數組或清單來儲存每一個view的坐标。

比如定義一個類,記錄坐标點

public class ViewPosition{
        int left;
        int top;
        int right;
        int bottom;

        public ViewPosition(int left,int top,int right,int bottom){
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }
    }
           

在onMeasure()中實作坐标計算

List<ViewPos> vPos = new ArrayList<>();

if (childW + lineW > widthSize - getPaddingLeft() - getPaddingRight()){//如果放入該view是超過父布局寬度,換行
                width = Math.max(lineW,childW);//取最大行寬為父布局行寬
                height +=lineH;//高度累加

                //開啟新行
                lineW = childW;
                lineH = childH;
                vPos.add(new ViewPos(getPaddingLeft()+lp.leftMargin,
                        getPaddingTop()+lp.topMargin+height,
                        getPaddingLeft() + childW - lp.rightMargin,
                        getPaddingTop() + height + childH - lp.bottomMargin));
            }else {//如果不換行,則寬度累加,高度取最大值
                vPos.add(new ViewPos(getPaddingLeft() + lineW + lp.leftMargin,
                        getPaddingTop() + height + lp.topMargin,
                        getPaddingLeft() + lineW + childW - lp.rightMargin,
                        getPaddingTop() + height + childH - lp.bottomMargin));
                lineW += childW;
                lineH = Math.max(lineH,childH);

            }
           

最後在onLayout()中就簡單了

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        for (int i = ; i < count; i++) {
            View child = getChildAt(i);
            ViewPos pos = vPos.get(i);
            //設定View的左邊、上邊、右邊底邊位置
            child.layout(pos.left, pos.top, pos.right, pos.bottom);
        }
    }
           

參考部落格:Android 自定義ViewGroup 實戰篇 -> 實作FlowLayout