Android -- 自定義ViewGroup實現FlowLayout效果


1,在開發的時候,常在我們的需求中會有這種效果,添加一個商品的一些熱門標簽,效果圖如下:

2,從上面效果可以看得出來,這是一個自定義的ViewGroup,然后實現換行效果,讓我們一起來實現一下

  • 自定義屬性

  從上面的效果來看,我們需要動態的設置每個lable的寬度和高度,所以我們編寫如下的自定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FlowLayout">
        <!--    標簽之間的間距-->
        <attr name="lineSpace" format="dimension"/>
        <!--    每一行之間的間距-->
        <attr name="rowSpace" format="dimension"/>
    </declare-styleable>
</resources>

  在布局文件中使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              xmlns:flowlayout="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
<com.qianmo.flowlayout.FlowLayout
    android:id="@+id/flowLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="20dip"
    flowlayout:lineSpace="20dip"
    flowlayout:rowSpace="10dip"/>
</LinearLayout>

  在類中獲取自定義屬性

public class FlowLayout extends ViewGroup {
    private static String TAG = "FlowLayout";

    //自定義屬性
    private int LINE_SPACE;
    private int ROW_SPACE;

    //放置標簽的集合
    private List<String> lables;
    private List<String> lableSelects;

    public FlowLayout(Context context) {
        this(context, null);
    }

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

    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //獲取自定義屬性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        LINE_SPACE = a.getDimensionPixelSize(R.styleable.FlowLayout_lineSpace, 10);
        ROW_SPACE = a.getDimensionPixelSize(R.styleable.FlowLayout_rowSpace, 10);
        a.recycle();

    }
}
  • 初始化數據數據源

  向FlowLayout類中添加數據

 /**
     * 添加標簽
     *
     * @param lables 標簽集合
     * @param isAdd  是否添加
     */
    public void setLables(List<String> lables, boolean isAdd) {
        if (this.lables == null) {
            this.lables = new ArrayList<>();
        }
        if (this.lableSelects == null) {
            this.lableSelects = new ArrayList<>();
        }
        if (isAdd) {
            this.lables.addAll(lables);
        } else {
            this.lables.clear();
            this.lables = lables;
        }
        if (lables != null && lables.size() > 0) {
            for (final String lable : lables) {
                final TextView tv = new TextView(getContext());
                tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
                        LayoutParams.WRAP_CONTENT));
                tv.setText(lable);
                tv.setTextSize(20);
                tv.setBackgroundResource(R.drawable.shape_item_lable_bg);
                tv.setTextColor(Color.BLACK);
                tv.setGravity(Gravity.CENTER);
                tv.setPadding(12, 5, 12, 5);

                //判斷是否選中
                if (lableSelects.contains(lable)) {
                    tv.setSelected(true);
                    tv.setTextColor(getResources().getColor(R.color.tv_blue));
                } else {
                    tv.setSelected(false);
                    tv.setTextColor(getResources().getColor(R.color.tv_gray));
                }

                //點擊之后選中標簽
                tv.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        tv.setSelected(tv.isSelected() ? false : true);
                        if (tv.isSelected()) {
                            tv.setTextColor(getResources().getColor(R.color.tv_blue));
                            lableSelects.add(lable);
                        } else {
                            tv.setTextColor(getResources().getColor(R.color.tv_gray));
                            lableSelects.remove(lable);
                        }
                    }
                });

                //添加到容器中
                addView(tv);
            }
        }
    }

  下面的代碼是textview的背景選擇器

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <!--選中效果-->
    <item android:state_selected="true">
        <shape >
            <solid android:color="#ffffff" />
            <stroke android:color="@color/tv_blue"
                    android:width="2px"/>
            <corners android:radius="10000dip"/>
        </shape>
    </item>
    <!--默認效果-->
    <item>
        <shape >
            <solid android:color="#ffffff" />
            <stroke android:color="@color/divider_gray"
                    android:width="2px"/>
            <corners android:radius="10000dip"/>
        </shape>
    </item>
</selector>
  • 重寫onMeasure方法

  本布局在寬度上是使用的建議的寬度(填充父窗體或者具體的size),如果需要wrap_content的效果,還需要重新計算,當然這種需求是非常少見的,所以直接用建議寬度即可;布局的高度就得看其中的標簽需要占據多少行(row ),那么高度就為row * 單個標簽的高度+(row -1) * 行距,代碼如下:

 /**
     * 通過測量子控件高度,來設置自身控件的高度
     * 主要是計算
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //測量所有子view的寬高
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        //獲取view的寬高測量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

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

        //這里的寬度建議使用match_parent或者具體值,當然當使用wrap_content的時候沒有重寫的話也是match_parent所以這里的寬度就直接使用測量的寬度
        int width = widthSize;

        int height;
        //判斷寬度
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            int row = 1;
            int widthSpace = width; //寬度剩余空間
            for (int i = 0; i < getChildCount(); i++) {
                View view = getChildAt(i);
                //獲取標簽寬度
                int childW = view.getMeasuredWidth();
                //判斷剩余寬度是否大於此標簽寬度
                if (widthSpace >= childW) {
                    widthSpace -= childW;
                } else {
                    row++;
                    widthSpace = width - childW;
                }
                //減去兩邊間距
                widthSpace -= LINE_SPACE;
            }
            //獲取子控件的高度
            int childH = getChildAt(0).getMeasuredHeight();
            //測算最終所需要的高度
            height = (childH * row) + (row - 1) * ROW_SPACE;
        }

        //保存測量高度
        setMeasuredDimension(width, height);
    }
  • 重寫OnLayout方法

  onLayout(boolean changed, int l, int t, int r, int b)方法是一個抽象方法,自定義ViewGroup時必須實現它,用於給布局中的子控件分配位置,其中的參數l,t,r,b分別代表本ViewGroup的可用空間(除去margin和padding后的剩余空間)的左、上、右、下的坐標(相對於自身),相當於一個約束,如果子控件擺放的位置超過這個范圍,超出的部分將不可見。

 /**
     * 擺放子view
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int row = 0;
        int right = 0;
        int bottom = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View chileView = getChildAt(i);
            int childW = chileView.getMeasuredWidth();
            int childH = chileView.getMeasuredHeight();
            right += childW;
            bottom = (childH + ROW_SPACE) * row + childH;
            if (right > (r - LINE_SPACE)) {
                row++;
                right = childW;
                bottom = (childH + ROW_SPACE) * row + childH;
            }
            chileView.layout(right - childW, bottom - childH, right, bottom);
            right += LINE_SPACE;
        }
    }

  看一下實現的效果圖

  

  ok,這樣我們就全部實現了,需要源碼的同學可以在這里去下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM