自定义控件 - 流式布局(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 .
推荐阅读
-
ios的collection控件的自定义布局实现与设计
-
ios的collection控件的自定义布局实现与设计
-
在Winform界面使用自定义用户控件及TabelPanel和StackPanel布局控件
-
WPF的ListView控件自定义布局用法实例
-
Kotlin:FlowLayout横向流式自定义布局
-
[WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画
-
Android 深入探究自定义view之流式布局FlowLayout的使用
-
Android自定义View2--onMeasure,onLayout源码分析和自定义流式布局
-
自定义View制作简单的流式布局(搜索历史记录)
-
自定义View 流式布局(历史搜索,热门搜索)