自定義控件 - 流式佈局(CofferFlowLayout)

自定義控件 - 流式佈局(CofferFlowLayout)

先看效果圖:

在這裏插入圖片描述

簡介

爲了方便大家理解自定義View裏的一些細節點,我這裏把開發者模式裏的“顯示佈局邊界”打開了。這個Demo功能很基礎簡單,就是顯示標籤,然後給每一個標籤添加點擊事件,長按刪除事件。如果後續想加其他功能,可以不斷的完善,這種瀑布流佈局實現非常成熟,花樣也很多。寫這個主要就是練手,加深對Measure 和layout的理解。

佈局

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

    <coffer.widget.CofferFlowLayout
        android:id="@+id/flow"
        android:padding="3dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>

CofferFlowLayout 就是此次瀑布流的實現。這個類繼承自ViewGroup。接下來看看這個類裏最核心的兩個方法:先把onMeasure 完整代碼貼出,然後拆分講解

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

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

        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        // 0、初始化行寬、行高
        int lineWidth = 0,lineHeight = 0;
        // 0.1 初始化瀑布流佈局真正的寬、高
        int realWidth = 0,realHeight = 0;
        // 1、設置瀑布流的最大寬、高
        int maxWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : mMaxSize;
        int maxHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : mMaxSize;

        // 2、測量子View的大小
        int childCount = getChildCount();
        mPosHelper.clear();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child != null && child.getVisibility() != GONE){
                measureChild(child,widthMeasureSpec,heightMeasureSpec);
                LayoutParams layoutParams = child.getLayoutParams();
                int leftMargin = 0;
                int rightMargin = 0;
                int topMargin = 0;
                int bottomMargin = 0;
                if (layoutParams instanceof MarginLayoutParams){
                    MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
                    leftMargin = marginLayoutParams.leftMargin;
                    rightMargin = marginLayoutParams.rightMargin;
                    topMargin = marginLayoutParams.topMargin;
                    bottomMargin = marginLayoutParams.bottomMargin;
                }
                // 2.1 計算出子View 佔據的寬、高
                int childWidth = leftMargin + rightMargin + child.getMeasuredWidth();
                int childHeight = topMargin + bottomMargin + child.getMeasuredHeight();
                // 2.2.1換行
                if (childWidth + lineWidth + paddingLeft + paddingRight > maxWidth) {
                    // 2.2.2 設置當前的行寬、高
                    lineWidth = childWidth;
                    realHeight = lineHeight;
                    lineHeight += childHeight;
                    // 2.2.3 計算子View的位置
                    ViewPosData data = new ViewPosData();
                    data.left = paddingLeft + leftMargin;
                    data.top = paddingTop + realHeight + topMargin;
                    data.right = paddingLeft + childWidth - rightMargin;
                    data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
                    mPosHelper.add(data);
                }else {
                    // 2.3.1 計算子View的位置
                    ViewPosData data = new ViewPosData();
                    data.left = paddingLeft + leftMargin +lineWidth;
                    data.top = paddingTop + realHeight + topMargin;
                    data.right = paddingLeft + childWidth + lineWidth - rightMargin;
                    data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
                    mPosHelper.add(data);
                    // 2.3.2 不換行,計算當前的行寬、高
                    lineWidth += childWidth;
                    lineHeight = Math.max(lineHeight,childHeight);
                }
            }
        }
        // 設置最終的寬、高
        realWidth = maxWidth;
        realHeight = Math.min(lineHeight + paddingBottom + paddingTop,maxHeight);
        setMeasuredDimension(realWidth,realHeight);
    }

這個方法我加了部分註釋,這裏再補充些。測量的時候,一定要考慮View的寬高設置模式,例如:wrap_content、400dp、match_parent。相比這些大家瞭解自定義View的都知道,因此這裏的首先就是要知道ViewGroup當前的測量模式、大小。

int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();

這裏就是獲取ViewGroup的padding ,開頭我給大家放的gif圖,之所以打開“佈局邊界”模式,就是讓大家能對pading有更直觀的認識,有很多時候我們在自定義ViewGroup時忽略這個屬性,導致自己用的時候發現不生效。後面還有margin屬性也是一樣。大家在測量時一定要主要把這些值計算進去。

int maxWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : mMaxSize;
int maxHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : mMaxSize;

這一句的寫法,根據不同的策略模式設置ViewGroup的最大大小。

public class ViewPosData {
    /**
     * View 的位置
     */
    public int left;
    public int top;
    public int right;
    public int bottom;
}
。。。。。。。。
/**
 * 這個集合存放所有子View的位置信息,方便後面佈局用
 */
private ArrayList<ViewPosData> mPosHelper;

這個輔助容器相當有用,其作用就是記錄所有子View的座標位置,有了玩意,可以在onLayout方法裏省略一大堆在onMeasure裏重複的邏輯。由於onMeasure會執行多次,因此在使用前一定要先清除數據。

 measureChild(child,widthMeasureSpec,heightMeasureSpec);
                LayoutParams layoutParams = child.getLayoutParams();
                int leftMargin = 0;
                int rightMargin = 0;
                int topMargin = 0;
                int bottomMargin = 0;
                if (layoutParams instanceof MarginLayoutParams){
                    MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
                    leftMargin = marginLayoutParams.leftMargin;
                    rightMargin = marginLayoutParams.rightMargin;
                    topMargin = marginLayoutParams.topMargin;
                    bottomMargin = marginLayoutParams.bottomMargin;
                }
                // 2.1 計算出子View 佔據的寬、高
                int childWidth = leftMargin + rightMargin + child.getMeasuredWidth();
                int childHeight = topMargin + bottomMargin + child.getMeasuredHeight();

測量ViewGroup前,一定要先測量子View 的大小。而子View的大小是有父View的MeasureSpec和自身LayoutParam所決定的。上面的這些代碼就是要計算出單個子View的寬高,注意,我這裏把子View 的margin也計算進去了,這個不要漏了!上面的那些代碼只是鋪墊,接下來重點核心來了:

// 2.2.1換行
                if (childWidth + lineWidth + paddingLeft + paddingRight > maxWidth) {
                    // 2.2.2 設置當前的行寬、高
                    lineWidth = childWidth;
                    realHeight = lineHeight;
                    lineHeight += childHeight;
                    // 2.2.3 計算子View的位置
                    ViewPosData data = new ViewPosData();
                    data.left = paddingLeft + leftMargin;
                    data.top = paddingTop + realHeight + topMargin;
                    data.right = paddingLeft + childWidth - rightMargin;
                    data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
                    mPosHelper.add(data);
                }else {
                    // 2.3.1 計算子View的位置
                    ViewPosData data = new ViewPosData();
                    data.left = paddingLeft + leftMargin +lineWidth;
                    data.top = paddingTop + realHeight + topMargin;
                    data.right = paddingLeft + childWidth + lineWidth - rightMargin;
                    data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
                    mPosHelper.add(data);
                    // 2.3.2 不換行,計算當前的行寬、高
                    lineWidth += childWidth;
                    lineHeight = Math.max(lineHeight,childHeight);
                }

這裏要說幾點。1、注意將pading加進去,我再強調一次。2、就是View座標的計算,這裏和後年的onLayout有密切聯繫。

ViewPosData data = new ViewPosData();
data.left = paddingLeft + leftMargin +lineWidth;
data.top = paddingTop + realHeight + topMargin;
data.right = paddingLeft + childWidth + lineWidth - rightMargin;
data.bottom = paddingTop + realHeight + childHeight - paddingBottom;

View 的座標是左、上、右、下。當我們水平橫着擺放時,top和bottom是不變的,bottom的值幾乎等於View的高度,這裏的幾乎是沒有包括的pading、margin的。大家還記得View的寬度 = getRight() - getLeft(),既然是橫着擺放,View 的left、right的值也是不斷累積,這裏我用了一個lineWidth做計算累積值。同理在換行時,高度也是如此。

// 設置最終的寬、高
realWidth = maxWidth;
realHeight = Math.min(lineHeight + paddingBottom + paddingTop,maxHeight);
setMeasuredDimension(realWidth,realHeight);

最後就是給ViewGroup設置所有子View累積計算的大小。最後在看看onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child != null && child.getVisibility() != View.GONE){
                ViewPosData data = mPosHelper.get(i);
                child.layout(data.left,data.top,data.right,data.bottom);
            }
        }
    }

有了ViewPosData幫忙記錄所有子View的座標,就不需要在重複計算了。沒有他,前面在onMeasure裏寫的那堆換行邏輯還有在囉嗦一遍。

至於給View設置事件啥的,我就不囉嗦了,接下來直接分享完整的源碼僅供參考:

public class CofferFlowLayout extends ViewGroup {

    private static final String TAG = "CofferFlowLayout_tag";

    /**
     * 在wrap_content下 View的最大值
     */
    private int mMaxSize;
    private Context mContext;
    /**
     * 這個集合存放所有子View的位置信息,方便後面佈局用
     */
    private ArrayList<ViewPosData> mPosHelper;

    public CofferFlowLayout(Context context) {
        super(context);
        init(context);
    }

    public CofferFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    private void init(Context context){
        mContext = context;
        mPosHelper = new ArrayList<>();
        mMaxSize = Util.dipToPixel(context,300);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

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

        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        // 0、初始化行寬、行高
        int lineWidth = 0,lineHeight = 0;
        // 0.1 初始化瀑布流佈局真正的寬、高
        int realWidth = 0,realHeight = 0;
        // 1、設置瀑布流的最大寬、高
        int maxWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : mMaxSize;
        int maxHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : mMaxSize;

        // 2、測量子View的大小
        int childCount = getChildCount();
        mPosHelper.clear();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child != null && child.getVisibility() != GONE){
                measureChild(child,widthMeasureSpec,heightMeasureSpec);
                LayoutParams layoutParams = child.getLayoutParams();
                int leftMargin = 0;
                int rightMargin = 0;
                int topMargin = 0;
                int bottomMargin = 0;
                if (layoutParams instanceof MarginLayoutParams){
                    MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
                    leftMargin = marginLayoutParams.leftMargin;
                    rightMargin = marginLayoutParams.rightMargin;
                    topMargin = marginLayoutParams.topMargin;
                    bottomMargin = marginLayoutParams.bottomMargin;
                }
                // 2.1 計算出子View 佔據的寬、高
                int childWidth = leftMargin + rightMargin + child.getMeasuredWidth();
                int childHeight = topMargin + bottomMargin + child.getMeasuredHeight();
                // 2.2.1換行
                if (childWidth + lineWidth + paddingLeft + paddingRight > maxWidth) {
                    // 2.2.2 設置當前的行寬、高
                    lineWidth = childWidth;
                    realHeight = lineHeight;
                    lineHeight += childHeight;
                    // 2.2.3 計算子View的位置
                    ViewPosData data = new ViewPosData();
                    data.left = paddingLeft + leftMargin;
                    data.top = paddingTop + realHeight + topMargin;
                    data.right = paddingLeft + childWidth - rightMargin;
                    data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
                    mPosHelper.add(data);
                }else {
                    // 2.3.1 計算子View的位置
                    ViewPosData data = new ViewPosData();
                    data.left = paddingLeft + leftMargin +lineWidth;
                    data.top = paddingTop + realHeight + topMargin;
                    data.right = paddingLeft + childWidth + lineWidth - rightMargin;
                    data.bottom = paddingTop + realHeight + childHeight - paddingBottom;
                    mPosHelper.add(data);
                    // 2.3.2 不換行,計算當前的行寬、高
                    lineWidth += childWidth;
                    lineHeight = Math.max(lineHeight,childHeight);
                }
            }
        }
        // 設置最終的寬、高
        realWidth = maxWidth;
        realHeight = Math.min(lineHeight + paddingBottom + paddingTop,maxHeight);
        setMeasuredDimension(realWidth,realHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child != null && child.getVisibility() != View.GONE){
                ViewPosData data = mPosHelper.get(i);
                child.layout(data.left,data.top,data.right,data.bottom);
            }
        }
    }

    /***********  以下是在父容器內創建子View   ************/
    private View createTagView(String title){
        View view = LayoutInflater.from(mContext).inflate(R.layout.activity_arrage_item,
                this, false);
        TextView textView = view.findViewById(R.id.text);
        textView.setText(title);
        return view;
    }

    private ArrayList<String> mTitle;
    private ItemClickListener mListener;

    public void setTag(ArrayList<String> title, final ItemClickListener listener){
        mTitle = title;
        mListener = listener;
        int count = title.size();
        for (int i = 0; i < count; i++) {
            View chid = createTagView(title.get(i));
            final int finalI = i;
            chid.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    Log.i(TAG,"onClick : "+finalI);
                    mListener.onClick(finalI);
                }
            });
            chid.setOnLongClickListener(new OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    Log.i(TAG,"onLongClick : "+finalI);
                    mListener.onLongClick(finalI);
                    return true;
                }
            });
            addView(chid);
        }
    }

    public interface ItemClickListener{
        void onClick(int position);
        void onLongClick(int position);
    }

    public void removeView(int position){
        View child = getChildAt(position);
        removeView(child);
        updata();
    }

    private void updata(){
        removeAllViews();
        setTag(mTitle,mListener);
    }
}
    
public class ArrangeViewActivity extends AppCompatActivity {

    private CofferFlowLayout mCofferFlowLayout;
    private int marginSize;
    private int mViewSize;
    private ArrayList<String> mTitle;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_arrage_main);
        mCofferFlowLayout = findViewById(R.id.flow);

        marginSize = Util.dipToPixel(this,3);
        mViewSize = Util.dipToPixel(this,10);

//        setView();
        setView2();
    }

    /**
     * 方式二
     */
    private void setView2(){
        mTitle = new ArrayList<>();
        mTitle.add("涼宮春日的憂鬱");
        mTitle.add("嘆息");
        mTitle.add("煩悶");
        mTitle.add("消失");
        mTitle.add("動搖");
        mTitle.add("暴走");
        mTitle.add("陰謀");
        mTitle.add("憤慨");
        mTitle.add("分裂");
        mTitle.add("驚愕");
        mCofferFlowLayout.setTag(mTitle, new CofferFlowLayout.ItemClickListener() {
            @Override
            public void onClick(int position) {
                Toast.makeText(ArrangeViewActivity.this,mTitle.get(position),
                        Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onLongClick(int position) {
                mTitle.remove(position);
                mCofferFlowLayout.removeView(position);
            }
        });
    }

    /**
     * 方式一: 將標籤View 在這裏創建
     */
    private void setView(){
        mCofferFlowLayout.addView(createTagView("涼宮春日的憂鬱"));
        mCofferFlowLayout.addView(createTagView("嘆息"));
        mCofferFlowLayout.addView(createTagView("煩悶"));
        mCofferFlowLayout.addView(createTagView("消失"));
        mCofferFlowLayout.addView(createTagView("動搖"));
        mCofferFlowLayout.addView(createTagView("暴走"));
        mCofferFlowLayout.addView(createTagView("陰謀"));
        mCofferFlowLayout.addView(createTagView("憤慨"));
        mCofferFlowLayout.addView(createTagView("分裂"));
        mCofferFlowLayout.addView(createTagView("驚愕"));
    }

    private View createTagView(String content){
        TextView textView = new TextView(this);
        textView.setText(content);
        textView.setTextColor(Color.WHITE);
        textView.setBackground(getResources().getDrawable(R.drawable.bg_gradient));
        ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        layoutParams.leftMargin = marginSize;
        layoutParams.bottomMargin = marginSize;
        layoutParams.topMargin = marginSize;
        layoutParams.rightMargin = marginSize;
        textView.setLayoutParams(layoutParams);
        return textView;
    }

}
    
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/text"
        android:text="憂鬱"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="5dp"
        android:gravity="center"
        android:layout_marginBottom="5dp"
        android:layout_marginRight="5dp"
        android:textColor="@color/white"
        android:background="@drawable/bg_gradient"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</FrameLayout>    

這個是activity_arrage_item.xml .

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