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

猜你喜欢

转载自blog.csdn.net/qq_26439323/article/details/105915766