前言

flow layout, 流式布局, 这个概念在移动端或者前端开发中很常见,特别是在多标签的展示中, 往往起到了关键的作用。然而Android 官方, 并没有为开发者提供这样一个布局, 于是有很多开发者自己做了这样的工作,github上也出现了很多自定义FlowLayout。 最近, 我也实现了这样一个FlowLayout,自己感觉可能是当前最好用的FlowLayout了(捂脸),在这里做一下分享。
项目地址:https://github.com/lankton/android-flowlayout

展示


第一张图, 展示向FlowLayout中不断添加子View
第二张图, 展示压缩子View, 使他们尽可能充分利用空间
第三张图, 展示调整子View之间间隔, 使各行左右对齐


这张图,截断flowlayout到指定行数。--20160520更新。

##基本的流式布局功能##
在布局文件中使用FlowLayout即可:

<cn.lankton.flowlayout.FlowLayout
        android:id="@+id/flowlayout"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="10dp"app:lineSpacing="10dp"android:background="#F0F0F0"></cn.lankton.flowlayout.FlowLayout>

可以看到, 提供了一个自定义参数lineSpacing, 来控制行与行之间的间距。

压缩

flowLayout.relayoutToCompress();

压缩的方式, 是通过对子View重新排序, 使得它们能够更合理的挤占空间, 后面会做详细说明。

对齐

flowLayout.relayoutToAlign();

对齐, 不会改变子View的顺序, 也不会起到压缩的作用。

截断

flowLayout.specifyLines(int)

实现

流式布局的实现

重写generateLayoutParams方法

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {return new MarginLayoutParams(p);
}@Override
public LayoutParams generateLayoutParams(AttributeSet attrs)
{return new MarginLayoutParams(getContext(), attrs);
}

重写该方法的2种重载是有必要的。这样子元素的LayoutParams就是MarginLayoutParam, 包含了margin 属性, 正是我们需要的。

重写onMeasure

主要有2个目的, 第一是测量每个子元素的宽高, 第二是根据子元素的测量值, 设置的FlowLayout的测量值。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int mPaddingLeft = getPaddingLeft();int mPaddingRight = getPaddingRight();int mPaddingTop = getPaddingTop();int mPaddingBottom = getPaddingBottom();int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);int lineUsed = mPaddingLeft + mPaddingRight;int lineY = mPaddingTop;int lineHeight = 0;for (int i = 0; i < this.getChildCount(); i++) {View child = this.getChildAt(i);if (child.getVisibility() == GONE) {continue;}measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, lineY);MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();int childWidth = child.getMeasuredWidth();int childHeight = child.getMeasuredHeight();int spaceWidth = mlp.leftMargin + childWidth + mlp.rightMargin;int spaceHeight = mlp.topMargin + childHeight + mlp.bottomMargin;if (lineUsed + spaceWidth > widthSize) {//approach the limit of width and move to next linelineY += lineHeight + lineSpacing;lineUsed = mPaddingLeft + mPaddingRight;lineHeight = 0;}if (spaceHeight > lineHeight) {lineHeight = spaceHeight;}lineUsed += spaceWidth;}setMeasuredDimension(widthSize,heightMode == MeasureSpec.EXACTLY ? heightSize : lineY + lineHeight + mPaddingBottom);
}

代码逻辑很简单, 就是遍历子元素, 计算累计长度, 超过一行可容纳宽度, 就将累计长度清0,同时假设继续向下一行放置子元素。为什么是假设呢, 因为真正在FlowLayout中放置子元素的过程, 是在onLayout方法中的。
重点在最后的setMeasuredDimension方法。在日常使用FlowLayout中, 我们的宽度往往是固定值, 或者match_parent, 不需要根据内容而改变, 所以宽度值直接用widthSize, 即从传进来的测量值获得的宽度。
高度则根据MeasureSpec的mode来判断, EXACTLY意味着和宽度一样, 直接用测量值的宽度即可, 否则,则是wrap_content, 需要用子元素排布出来的高度进行判断。

重写onLayout

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {int mPaddingLeft = getPaddingLeft();int mPaddingRight = getPaddingRight();int mPaddingTop = getPaddingTop();int lineX = mPaddingLeft;int lineY = mPaddingTop;int lineWidth = r - l;usefulWidth = lineWidth - mPaddingLeft - mPaddingRight;int lineUsed = mPaddingLeft + mPaddingRight;int lineHeight = 0;for (int i = 0; i < this.getChildCount(); i++) {View child = this.getChildAt(i);if (child.getVisibility() == GONE) {continue;}MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();int childWidth = child.getMeasuredWidth();int childHeight = child.getMeasuredHeight();int spaceWidth = mlp.leftMargin + childWidth + mlp.rightMargin;int spaceHeight = mlp.topMargin + childHeight + mlp.bottomMargin;if (lineUsed + spaceWidth > lineWidth) {//approach the limit of width and move to next linelineY += lineHeight + lineSpacing;lineUsed = mPaddingLeft + mPaddingRight;lineX = mPaddingLeft;lineHeight = 0;}child.layout(lineX + mlp.leftMargin, lineY + mlp.topMargin, lineX + mlp.leftMargin + childWidth, lineY + mlp.topMargin + childHeight);if (spaceHeight > lineHeight) {lineHeight = spaceHeight;}lineUsed += spaceWidth;lineX += spaceWidth;}
}

这段代码也很好理解, 逐个判断子元素,是继续在本行放置, 还是需要换行放置。这一步和onMeasure一样, 基本上所有的FlowLayout都会进行重写, 我的自然也没什么特别的新意, 这两块就不重点介绍了。下面重点介绍一下2种布局优化的实现。

压缩的实现

关于如何实现压缩, 这个问题开始的确很让我头疼。因为我的脑子里只有大致的概念,那就是压缩应该是一个什么样的效果, 而这个模糊的概念很难转换成具体的数学模型。没有数学模型, 就无法用代码解决这个问题,简直恨不得回到大学重学算法。。但有一个想法是明确的, 那就是解决这个问题, 实际上就是对子元素的重新排序。
后来决定简化思路, 用类似贪心算法的思维解决问题,那就是:逐行解决, 每一行都争取最大程度的占满。
1. 从第一行开始, 从子元素集合中,选出一部分, 使得这一部分子元素可以最大程度的占据这一行;
2. 将这部分已经选出的从集合中拿出, 继续对下一行执行第一步操作。
这个思路确立了, 那我们如何从集合中选出子集, 对某一行进行最大程度的占据呢?
我们已知的条件:
1. 子元素集合
2. 每行可容纳宽度
3. 每个子元素的宽度
这个时候, 脑子里就想到了01背包问题:
已知
1. 物品集合
2. 背包总容量
3. 每个物品的价值
4. 每个物品的体积
求背包包含物品的最大价值(及其方案
有朋友可能有疑问, 二者确实很像, 但不是还差着一个条件吗?嗯 ,是的。。但是在当前状况下,因为我们要尽可能的占满某一行, 那么每个子元素的宽度就不仅仅是限制了, 也是价值所在。
这样, 该状况就完全和01背包问题一致了。之后就可以用动态规划解决问题了。 关于如何用动态规划解决01背包问题, 其实我也忘的差不多了, 也是在网上查着资料, 一边回顾,一边实现的。所以这里我自己就不展开介绍了, 也不贴自己的代码了(感兴趣的可以去github查看), 放一个链接。我感觉这个链接里的讲解对我回顾相关知识点帮助很大,有兴趣的也可以看看~
背包问题——“01背包”详解及实现(包含背包中具体物品的求解)

对齐的实现

这个功能,我最早是在bilibili的ipad客户端上看到的,如下。

当时觉得挺好看的,还想过一阵怎么做, 但一时没想出来。。。这次实现FlowLayout, 就顺手将这种对齐样式用自己的想法实现了一下。

public void relayoutToAlign() {int childCount = this.getChildCount();if (0 == childCount) {//no need to sort if flowlayout has no child viewreturn;}int count = 0;for (int i = 0; i < childCount; i++) {View v = getChildAt(i);if (v instanceof BlankView) {//BlankView is just to make childs look in alignment, we should ignore them when we relayoutcontinue;}count++;}View[] childs = new View[count];int[] spaces = new int[count];int n = 0;for (int i = 0; i < childCount; i++) {View v = getChildAt(i);if (v instanceof BlankView) {//BlankView is just to make childs look in alignment, we should ignore them when we relayoutcontinue;}childs[n] = v;MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();int childWidth = v.getMeasuredWidth();spaces[n] = mlp.leftMargin + childWidth + mlp.rightMargin;n++;}int lineTotal = 0;int start = 0;this.removeAllViews();for (int i = 0; i < count; i++) {if (lineTotal + spaces[i] > usefulWidth) {int blankWidth = usefulWidth - lineTotal;int end = i - 1;int blankCount = end - start;if (blankCount > 0) {int eachBlankWidth = blankWidth / blankCount;MarginLayoutParams lp = new MarginLayoutParams(eachBlankWidth, 0);for (int j = start; j < end; j++) {this.addView(childs[j]);BlankView blank = new BlankView(mContext);this.addView(blank, lp);}this.addView(childs[end]);start = i;i --;lineTotal = 0;}} else {lineTotal += spaces[i];}}for (int i = start; i < count; i++) {this.addView(childs[i]);}
}

代码很长, 但说起来很简单。获得子元素列表,从头开始, 逐一判断哪些子元素在同一行。即每一次的start 到 end。 然后计算这些子元素装满一行的话, 还差多少, 设为d。则每两个子元素之间需要补上的间距为 d / (end - start)。 如果设置间距呢, 首先我们肯定不能去更改子元素本身的性质。那么, 就只能在两个子元素中间补上一个宽度为d / (end - start) 的BlankView了。
至于这个BlankView是个什么鬼, 定义如下:

class BlankView extends View {public BlankView(Context context) {super(context);}
}

你看, 根本什么也没做。 那我新写一个类继承View的意义是什么呢? 其实从上边对齐的代码里也能看到,这样我们在遍历FlowLayout的子元素时, 就可以通过 instance of BlankView 来判断是真正需要处理、计算的子元素,还是我们后来加上的补位View了

截断的实现

假设要截断为N行, 则取子元素列表中,前N行的,重新布局。详见github代码。

总结

代码没有全部贴出, 因为所有的代码都在github上了~这里再贴一下项目地址:
https://github.com/lankton/android-flowlayout

这个项目, 肯定还是有很多需要优化的地方, 欢迎各位提出各种意见或者建议,也期待能够被大家使用。
可以的话,也顺求star~ 谢谢。

更新

发布到JCenter-20160519

为方便使用,已将library发布到JCenter,开发者可以使用gradle或者maven进行依赖的配置。

gradle

compile 'cn.lankton:flowlayout:1.0.1'

maven

<dependency><groupId>cn.lankton</groupId><artifactId>flowlayout</artifactId><version>1.0.1</version><type>pom</type>
</dependency>

【Android】自定义FlowLayout,支持多种布局优化--android-flowlayout相关推荐

  1. android自动标齐,自定义FlowLayout,支持多种布局优化--android-flowlayout

    前言 flow layout, 流式布局, 这个概念在移动端或者前端开发中很常见,特别是在多标签的展示中, 往往起到了关键的作用.然而Android 官方, 并没有为开发者提供这样一个布局, 于是有很 ...

  2. android自定义radiogroup,Android 自定义View实现任意布局的RadioGroup效果

    前言 RadioGroup是继承LinearLayout,只支持横向或者竖向两种布局.所以在某些情况,比如多行多列布局,RadioGroup就并不适用 . 本篇文章通过继承RelativeLayout ...

  3. Android 自定义View 手写瀑布流组件FlowLayout

    目录 FlowLayout实现关键步骤: 1.创建一个view继承自ViewGroup 2.重写并实现onMeasure方法 3.重写并实现onLayout方法 纸上得来终觉浅,绝知此事要躬行. 动手 ...

  4. android textview 文字居中_Android布局优化,看这3点就够了

    码个蛋(codeegg)第 712 次推文 作者:Android技术 博客:https://www.jianshu.com/p/2ee61b88175e 前言 在编写Android布局时总会遇到这样或 ...

  5. android自定义数字键盘和字母键盘,Android自定义键盘的实现(数字键盘和字母键盘)...

    Android自定义键盘的实现(数字键盘和字母键盘) 发布时间:2020-09-04 03:18:48 来源:脚本之家 阅读:100 作者:浪淘沙xud 在项目中,产品对于输入方式会有特殊的要求,需要 ...

  6. android自定义进度条百分比跟着走,Android自定义View实现水平带数字百分比进度条...

    这个进度条可以反映真实进度,并且完成百分比的文字时随着进度增加而移动的,所在位置也恰好是真实完成的百分比位置,效果如下: 思路如下:第一部分是左侧的蓝色直线,代表已经完成的进度:第二部分是右侧灰色的直 ...

  7. android 自定义皮肤,仿墨迹天气在Android App中实现自定义zip皮肤更换

    在这里谈一下墨迹天气的换肤实现方式,不过首先声明我只是通过反编译以及参考了一些网上其他资料的方式推测出的换肤原理, 在这里只供参考. 若大家有更好的方式, 欢迎交流. 墨迹天气下载的皮肤就是一个zip ...

  8. Android自定义View 开发流程综合简述 Android自定义View(三)

    本文简述一下自定义View中常用方法 1 简述 自定义View可以认为是继承自View或者ViewGroup Android中的任何一个布局.任何一个控件其实都是直接或间接继承自View的,如Text ...

  9. android自定义进度条百分比跟着走,Android studio圆形进度条 百分数跟随变化

    本文实例为大家分享了Android studio圆形进度条展示的具体代码,供大家参考,具体内容如下 MainActivity import android.support.v7.app.AppComp ...

最新文章

  1. 学习Kotlin(六)扩展与委托
  2. 积跬步,聚小流------html知识大纲归纳总结
  3. 八大排序算法交换排序算法
  4. java message_Java Message System简介
  5. php获取访问者ip地址汇总,php获取访问者IP地址汇总_PHP
  6. 两种设置安卓背景图片的方法
  7. python的编码规范【摘】
  8. 爬虫使用代理socks
  9. 信创云:打造自主可控云基础设施 | 厂商征集
  10. 昨晚 win7 盗版 黑屏了
  11. 2017NOIp模拟赛08.20
  12. unity 神笔画画
  13. Springboot 使用JPA Repository 注入失败问题
  14. 没别的,就聊聊数据通信网络
  15. 【机器学习笔记】——决策树(Decision Tree)
  16. arm开发板源码编译mysql
  17. LeetCode-Day1
  18. 手把手教你怎么把爱奇艺QSV格式转换成MP4格式
  19. 二、工厂模式思维导图
  20. BZOJ2264 : Free Goodies

热门文章

  1. 常用API部分测试题
  2. 写两个函数 分别求两个整数的最大公约数和最小公倍数 用主函数调用这两个函数 并输出结果 两个整数由键盘输入
  3. Oracle COALESCE函数
  4. 全志T3开发板(4核ARM Cortex-A7)测评合集——从开发板到PLC
  5. 通过adb安装apk到android手机
  6. Facebook创始人原型电影《社交网络》票房夺冠
  7. 碳足迹分析软件市场现状研究分析报告-
  8. seo优化需要c语言吗,SEO优化人员需要优化哪些代码?
  9. 骂人的到底是些什么人
  10. FLAC3D模拟:复杂模型的建立与导入