欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

自定义控件 - 流式布局(CofferFlowLayout)

程序员文章站 2022-05-30 22:22:22
...

自定义控件 - 流式布局(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 .