最近使用APP的时候经常看到有

这种流式布局 ,今天我就跟大家一起来动手撸一个这种自定义控件.

首先说一下自定义控件的流程:

自定义控件一般要么继承View要么继承ViewGroup

View的自定义流程:

继承一个View-->重写onMeasure方法-->重写onDraw方法-->定义自定义属性-->处理手势操作

ViewGroup的自定义流程:

继承一个ViewGroup-->重写onMeasure方法-->重写onLayout-->重写onDraw方法->定义自定义属性-->处理手势操作

我们可以看到自定义View和自定义ViewGroup略微有些不同,自定义ViewGroup多了个onlayout方法,那么这些方法都有什么作用呢?这里由于篇幅的问题不做过多的描述,简单的说

onMeasure:用来计算,计算自身显示在页面上的大小

onLayout:用来计算子View摆放的位置,因为View已经是最小单元了,所以没有字View,所以没有onLayout方法

onDraw:用来绘制你想展示的东西

定义自定义属性就是暴露一些属性给外部调用

好了,了解了自定义View的基本自定义流程,我们可以知道我们应该需要自定义一个ViewGroup就可以满足该需求.

首先自定义一个View命名为FlowLayout继承ViewGroup

public class FlowLayout extends ViewGroup {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);}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int count = getChildCount();for (int i = 0; i < count; i++) {getChildAt(i).layout(l,t,r,b);}}
}

可以看到onLayout是必须重写的,不然系统不知道你这个ViewGroup的子View摆放的位置.

然后XML中引用

<test.hxy.com.testflowlayout.FlowLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/mFlowLayout"android:layout_margin="10dp"android:layout_width="match_parent"android:layout_height="match_parent" />

Activity中设置数据

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);FlowLayout mFlowLayout = (FlowLayout) findViewById(R.id.mFlowLayout);List<String> list = new ArrayList<>();list.add("java");list.add("javaEE");list.add("javaME");list.add("c");list.add("php");list.add("ios");list.add("c++");list.add("c#");list.add("Android");for (int i = 0; i < list.size(); i++) {View inflate = LayoutInflater.from(this).inflate(R.layout.item_personal_flow_labels, null);TextView label = (TextView) inflate.findViewById(R.id.tv_label_name);label.setText(list.get(i));mFlowLayout.addView(inflate);}}

运行一下:

咦!!!这时候发现我们添加的子View竟然没添加进去?这是为什么呢?

这时候就不得不说一下onMeasure方法了,我们重写一下onMeasure然后在看一下:

   @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int totalWidth = MeasureSpec.getSize(widthMeasureSpec);int totalHeight = MeasureSpec.getSize(heightMeasureSpec);int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft();int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();int modeWidth = MeasureSpec.getMode(widthMeasureSpec);int modeHeight = MeasureSpec.getMode(heightMeasureSpec);final int count = getChildCount();for (int i = 0; i < count; i++) {final View child = getChildAt(i);int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth);int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight);// 测量childchild.measure(childWidthMeasureSpec, childHeightMeasureSpec);}setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec));}

在运行一下:

我们可以看到确实是有View显示出来了,可是为什么只有一个呢?

其实这里显示的不是只有一个,而是所有的子View都盖在一起了,所以看起来就像只有一个View,这是因为我们的onLayout里面getChildAt(i).layout(l,t,r,b);所有的子View摆放的位置都是一样的,所以这边要注意一下,自定义ViewGroup的时候一般onLayout和onMeasure都必须重写,因为这两个方法一个是计算子View的大小,一个是计算子View摆放的位置,缺少一个子View都会显示不出来.

接下来我们在改写一下onLayout方法让子View都显示出来

@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int count = getChildCount();for (int i = 0; i < count; i++) {getChildAt(i).layout(l,t,r,b);l+=getChildAt(i).getMeasuredWidth();}}

这样子View就不会重叠在一起了,可是又发现一个问题,就是子View都在排在同一行了,我们怎么才能让子View计算排满一行就自动换行呢?

接下来我们定义一个行的类Line来保存一行的子View:

class Line{int mWidth = 0;// 该行中所有的子View累加的宽度int mHeight = 0;// 该行中所有的子View中高度的那个子View的高度List<View> views = new ArrayList<View>();public void addView(View view) {// 往该行中添加一个views.add(view);mWidth += view.getMeasuredWidth();int childHeight = view.getMeasuredHeight();mHeight = mHeight < childHeight ? childHeight : mHeight;//高度等于一行中最高的View}//摆放行中子View的位置public void Layout(int l, int t){}}

这样我们就可以让FlowLayout专门对Line进行摆放,然后Line专门对本行的View进行摆放

接下来针对Line我们重新写一下onMeasure和onLayout方法

    @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int totalWidth = MeasureSpec.getSize(widthMeasureSpec);int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft();int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();restoreLine();// 还原数据,以便重新记录int modeWidth = MeasureSpec.getMode(widthMeasureSpec);int modeHeight = MeasureSpec.getMode(heightMeasureSpec);final int count = getChildCount();for (int i = 0; i < count; i++) {final View child = getChildAt(i);if (child.getVisibility() == View.GONE) {break;}int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeWidth);int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : modeHeight);// 测量childchild.measure(childWidthMeasureSpec, childHeightMeasureSpec);if (mLine == null) {mLine = new Line();}int measuredWidth = child.getMeasuredWidth();mUsedWidth += measuredWidth;// 增加使用的宽度if (mUsedWidth < sizeWidth) { //当本行的使用宽度小于行总宽度的时候直接加进line里面mLine.addView(child);mUsedWidth += mHorizontalSpacing;// 加上间隔if (mUsedWidth >= sizeWidth){if (!newLine()){break;}}}else {// 使用宽度大于总宽度。需要换行if (mLine.getViewCount() == 0){//如果这行一个View也没有超过也得加进去,保证一行最少有一个ViewmLine.addView(child);if (!newLine()) {// 换行break;}}else {if (!newLine()) {// 换行break;}mLine.addView(child);mUsedWidth += measuredWidth + mHorizontalSpacing;}}}if (mLine !=null && mLine.getViewCount() > 0 && !mLines.contains(mLine)){mLines.add(mLine);}int totalHeight = 0;final int linesCount = mLines.size();for (int i = 0; i < linesCount; i++) {// 加上所有行的高度totalHeight += mLines.get(i).mHeight;}totalHeight += mVerticalSpacing * (linesCount - 1);// 加上所有间隔的高度totalHeight += getPaddingTop() + getPaddingBottom();// 加上padding// 设置布局的宽高,宽度直接采用父view传递过来的最大宽度,而不用考虑子view是否填满宽度,因为该布局的特性就是填满一行后,再换行// 高度根据设置的模式来决定采用所有子View的高度之和还是采用父view传递过来的高度setMeasuredDimension(totalWidth, resolveSize(totalHeight, heightMeasureSpec));}

可能有点长,不过注释都写得比较清楚了,简单的说就是遍历计算子View的宽高,动态加入行中,如果View的宽大于剩余的行宽就在取一行放下,接下来我们在重写一些onLayout:

    @Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {if (!mNeedLayout && changed){mNeedLayout = false;int left = getPaddingLeft();//获取最初的左上点int top = getPaddingTop();int count = mLines.size();for (int i = 0; i < count; i++) {Line line = mLines.get(i);line.LayoutView(left,top);//摆放每一行中子View的位置top +=line.mHeight+ mVerticalSpacing;//为下一行的top赋值}}}

由于我们把子View的摆放都放在Line中了,所以onLayout比较简单,接下来我们看一下Line的LayoutView方法:

public void LayoutView(int l, int t) {int left = l;int top = t;int count = getViewCount();int layoutWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//行的总宽度//剩余的宽度,是除了View和间隙的剩余空间int surplusWidth = layoutWidth - mWidth - mHorizontalSpacing * (count - 1);if (surplusWidth >= 0) {for (int i = 0; i < count; i++) {final View view = views.get(i);int childWidth = view.getMeasuredWidth();int childHeight = view.getMeasuredHeight();//计算出每个View的顶点,是由最高的View和该View高度的差值除以2int topOffset = (int) ((mHeight - childHeight) / 2.0 + 0.5);if (topOffset < 0) {topOffset = 0;}view.layout(left,top+topOffset,left+childWidth,top + topOffset + childHeight);left += childWidth + mVerticalSpacing;//为下一个View的left赋值}}}

也是比较简单,其实就是根据宽度动态计算而已,我们看看效果吧

可以了吧,看起来是大功告成了,可是我们发现左边和右边的间距好像不相等,能不能让子View居中显示呢?答案当然是可以的,接下来我们提供个方法,让外部可以设置里面子View的对齐方式:

public interface AlienState {int RIGHT = 0;int LEFT = 1;int CENTER = 2;@IntDef(value = {RIGHT, LEFT, CENTER})@interface Val {}}public void setAlignByCenter(@AlienState.Val int isAlignByCenter) {this.isAlignByCenter = isAlignByCenter;requestLayoutInner();}private void requestLayoutInner() {new Handler(Looper.getMainLooper()).post(new Runnable() {@Overridepublic void run() {requestLayout();}});}

提供一个setAlignByCenter的方法,分别有左对齐右对齐和居中对齐,然后我们在Line的layoutView中改写一下:

                    //布局Viewif (i == 0) {switch (isAlignByCenter) {case AlienState.CENTER:left += surplusWidth / 2;break;case AlienState.RIGHT:left += surplusWidth;break;default:left = 0;break;}}

在layoutView中把剩余的宽度按照对齐的类型平分一下就得到我们要的效果了

好了,这样就达到我们要的效果了.可是在回头来看一下我们MainActivity里面的写法会不会感觉很撮呢?对于习惯了ListView,RecyclerView的Adapter写法的我们有没有办法改一下,像写adapter一样来写布局呢?聪明的程序猿是没有什么办不到的,接下来我们就来改写一下:

    public void setAdapter(List<?> list, int res, ItemView mItemView) {if (list == null) {return;}removeAllViews();int layoutPadding = dipToPx(getContext(), 8);setHorizontalSpacing(layoutPadding);setVerticalSpacing(layoutPadding);int size = list.size();for (int i = 0; i < size; i++) {Object item = list.get(i);View inflate = LayoutInflater.from(getContext()).inflate(res, null);mItemView.getCover(item, new ViewHolder(inflate), inflate, i);addView(inflate);}}public abstract static class ItemView<T> {abstract void getCover(T item, ViewHolder holder, View inflate, int position);}class ViewHolder {View mConvertView;public ViewHolder(View mConvertView) {this.mConvertView = mConvertView;mViews = new SparseArray<>();}public <T extends View> T getView(int viewId) {View view = mViews.get(viewId);if (view == null) {view = mConvertView.findViewById(viewId);mViews.put(viewId, view);}try {return (T) view;} catch (ClassCastException e) {e.printStackTrace();}return null;}public void setText(int viewId, String text) {TextView view = getView(viewId);view.setText(text);}}

然后我们在MainActivity中在使用一下:

protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);FlowLayout mFlowLayout = (FlowLayout) findViewById(R.id.mFlowLayout);List<String> list = new ArrayList<>();list.add("java");list.add("javaEE");list.add("javaME");list.add("c");list.add("php");list.add("ios");list.add("c++");list.add("c#");list.add("Android");mFlowLayout.setAlignByCenter(FlowLayout.AlienState.CENTER);mFlowLayout.setAdapter(list, R.layout.item, new FlowLayout.ItemView<String>() {@Overridevoid getCover(String item, FlowLayout.ViewHolder holder, View inflate, int position) {holder.setText(R.id.tv_label_name,item);}});}

怎么样,是不是就根绝在跟使用adapter一样了呢.

Demo已放到github,欢迎大家指点:https://github.com/huang7855196/FlowLayout.git

Android FlowLayout流式布局相关推荐

  1. Android FlowLayout 流式布局

    FlowLayout 流式布局 Android 流式布局控件,实现自动换行,操出范围可以滑动功能,未使用控件复用功能,所以不应该有太多的子控件. 主要包含功能: 流式布局,自动换行 使用Adapter ...

  2. android 热搜词 布局,Android FlowLayout流式布局打造热门标签(高仿抖音热搜)

    需要先学习下面2个内容 1.已经基本给大家介绍了如何自定义ViewGroup,如果你还不了解 2.宽高的计算 一.XML布局 从布局图中可以看到,FlowLayout中包含了很多TextView.难度 ...

  3. 自定义 FlowLayout流式布局搜索框 加 GreenDao存取搜索记录,使用RecyclerView展示

    输入框布局的shape <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android ...

  4. android recyclerview流式布局,Android FlexboxLayout流式布局

    Android FlexboxLayout流式布局 FlexBoxLayout是为Android带来了与 CSS Flexible Box Layout(CSS 弹性盒子)相似功能的库. 一:添加依赖 ...

  5. Android自定义流式布局-FlowLayout

    很多App在搜索页时都有对热门标签的一个排列,而Android官方没有对应的布局控件,所以自己写了一个流式布局(也称标签布局)--FlowLayout. 为了大家使用的方便,没有添加自定义属性,所以需 ...

  6. Android第三方流式布局FlowLayout简单实用(搜索历史记录)

    效果图: 导入大Model下: maven { url 'https://jitpack.io' } builde.gradle依赖: implementation 'com.github.LRH19 ...

  7. android 搜索历史流布局,FlowLayout流式布局实现搜索清空历史记录

    本文实例为大家分享了FlowLayout实现搜索清空历史记录的具体代码,供大家参考,具体内容如下 效果图:点击搜索框将搜索的历史在流式布局中展示出来,清空历史记录就会将历史清空,每次搜索后都存入sp中 ...

  8. Flowlayout流式布局使用(轻量级)

    Flowlayout属于自定义流式布局,意思就是说从左上角开始添加原件,依次往后排,第一行挤满了就换一行接着排. 本文所使用的FlowLayout来自于鸿洋大神的框架. 只取了一个自定义控件,没有鸿洋 ...

  9. android分组流式布局,Android 流式布局实现

    概述 本文主要分享Android流式布局实现,实现效果如下: 在实现之前先来看一下View的生命周期,如下图: 流式布局属于自定义ViewGroup,重点关注onMeasure与onLayout方法 ...

最新文章

  1. UE5真实环境设计入门学习教程
  2. Java中实现cd命令:运行中更改当前工作目录
  3. 天下会 - 搜索实战系列之视频
  4. 碎片化学习前端资料分享~
  5. 南邮 计算机网络,南邮计算机网络_期末复习纲要-精简版教材.pdf
  6. 删除用户和用户下的所有表
  7. 某热门单击手游lua解密.md
  8. Ubuntu20.04安装ZOOM
  9. 我与刘强东的故事:他的1000亿和我的5000万(转超级表格)
  10. 工具使用,PS隐藏技能—对称绘画
  11. OsgEarth添加圆锥体
  12. 微信页面隐藏分享按钮
  13. 如何利用计算机隐藏文件,如何查找隐藏的计算机文件夹
  14. Buffon投针实验 · 数学的直观理解 · 基础知识很重要
  15. 53. 验证外星语词典
  16. 学习用 JS/CSS 画一个时钟
  17. java 图像锐化_Java实现图像的模糊与锐化实例
  18. SemanticKitti数据集的使用
  19. 打印时总跟出一页计算机主的纸,打印机一直出纸怎么解决
  20. 离散数学 笔记 zucc

热门文章

  1. User root is not allowed to impersonate anonymous
  2. 匹兹堡大学申请条件计算机科学,匹兹堡大学计算机科学理学硕士研究生申请要求及申请材料要求清单...
  3. 树莓派4B启动失败之原因排查及解决方案
  4. 英语发音规则---I字母
  5. 【halcon机器视觉教程】黑洞是什么洞?我来给你拍个照,黑洞成像系统
  6. 华为无线wifi设备连接到服务器,华为wifi路由器安装上网的方法
  7. “影响力”就是你存在的价值。文/江湖一剑客
  8. 普通IT中年自救指南(一)
  9. pathlib库使用手册
  10. 前端基础第二天——HTML5基础