ios 自动换行FlowLayout
最近产品需要实现自动换行功能,在gitHub看了一下,虽然有不少,但都有那么一点不满足需求的,或者感觉用着不方便的。所以干脆自己写了一份,顺便有时间写了一份ios的版本,有兴趣的可点击上面链接。
在这里先提供下载地址:https://github.com/lanqi-x/flowLayout
然后来个图先:
实现概要思路为1、继承ViewGroup,实现对子view进行布局,不进行其他处理,不跟业务挂钩,以保证其灵活性。2、支持adapter方式,仿recycleview的adapter,使用观察者模式。(需要注意的是,1该控件并未实现子view的复用,2不建议同时使用adapter和自己在代码中直接调用addView)
实现该控件,我写了三个类,分别为FlowLayout、FlowAdapter和FlowDataSetObserver,这里按照简易度,简单的介绍下。(如想只看FlowLayout的实现可点击这里)
1、FlowDataSetObserver
先贴下代码:
public void onChanged() {
// Do nothing
}
public void onItemRangeChanged(int positionStart, int itemCount) {
// do nothing
}
public void onItemRangeInserted(int positionStart, int itemCount) {
// do nothing
}
public void onItemRangeRemoved(int positionStart, int itemCount) {
// do nothing
}
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
// do nothing
}
这是一个观察者,很简单都是空实现,相信看过recycleView中Adapter源码的同学都很熟悉了,就是对应几个数据改变的事件,没什么好说的。
2、FlowAdapter
这个代码有点多,就不贴太多代码了,具体思路跟recycleView的Adapter差不多。
常用的几个方法有以下几个和数据改变的notifyItemRangeChanged等,意义也跟recycleView的一样就不解释了
public abstract T onCreateViewHolder(FlowLayout flowLayout, int viewType);
public abstract void onBindViewHolder(T view, int position);
public long getItemId(int position) {
return position;
}
public abstract Object getItem(int position);
public int getItemViewType(int position) {
return 0;
}
public abstract int getItemCount();
然后内部类AdapterDataObservable继承java.util.ArrayList包下的Observable实现被观察者,看过源码的同学应该都知道Observable里有一个ArrayList数组,registerObserve()方法既是将Observer对象存在该数组中,所以被观察者即实现跟观察者对应的几个方法,对观察者们进行一个个的回调(个人觉得观察者模式就是被观察者对观察者的接口回调),贴个方法做例子:
public void notifyItemRangeChanged(int positionStart, int itemCount) {
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
}
}
准备都做好了,终于来到自定义控件了。(其实实际做的时候,是先简单的实现的FlowLayout,后来才想要来个Adapter模式再一点点加上去的)
首先继承ViewGroup,然后对子View进行摆放,大家都知道自定义ViewGroup主要的处理onMeasure和onLayout方法,所以这里也主要讲这两个方法。首先是测量方法onMeasure。这里主要是计算每个子View的宽度,确定每一行有几个子View,为了不重复计算和方便服用,跟其他同学的实现方式一样,封装了一个内部类Line,来负责装每一行的子View计算每一行的高度和对子View进行摆放。
所以onMeasure方法就会变得简单一点,就对子View进行for循环,回去每个子View的宽度和内边距之和,当其大于FlowLayout的宽度时,则过行,及new一个新的Line对象。关键代码为:
int childWidth = child.getMeasuredWidth();
mUsedWidth += childWidth;// 增加使用的宽度
if (mUsedWidth <= sizeWidth) {// 使用宽度小于总宽度,该child属于这一行。
mLine.addView(child);// 添加child
mUsedWidth += mHorizontalSpacing;// 加上间隔
if (mUsedWidth >= sizeWidth) {// 加上间隔后如果大于等于总宽度,需要换行
if (!newLine()) {
break;
}
}
} else {// 使用宽度大于总宽度。需要换行
if (mLine.getViewCount() == 0) {// 如果这行一个child都没有,那么就加上去,以保证每行都有至少有一个child
mLine.addView(child);// 添加child
if (!newLine()) {// 换行
break;
}
} else {// 如果该行有数据了,就直接换行
if (!newLine()) {// 换行
break;
}
// 在新的一行,因为这一行一个child都没有,先加上去,以保证每行都有至少有一个child
mLine.addView(child);
mUsedWidth += childWidth + mHorizontalSpacing;
}
}
最后FlowLayout的高度根据配置,如是MATCH_PARENT和WRAP_CONTENT,则其高度为每行高度和每行的行距之和,否则则为用户设置的高度
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));
而layoutView方法传进来的起始坐标,和子View的高宽度对子View进行layout即可,例如:
for (int i = 0; i < count; i++) {
final View view = views.get(i);
int childWidth = view.getMeasuredWidth();
int childHeight = view.getMeasuredHeight();
// 计算出每个View的顶点,是由最高的View和该View高度的差值除以2,目的是为了每个子View垂直居中
int topOffset = (int) ((mHeight - childHeight) / 2.0 + 0.5);
if (topOffset < 0) {
topOffset = 0;
}
// 布局View
view.layout(left, top + topOffset, left + childWidth, top
+ topOffset + childHeight);
left += childWidth + mHorizontalSpacing; // 为下一个View的left赋值
}
不知道怎么自定义控件的xml属性的自己百度一下,这个有很多人介绍过,就不多说了,但在这里补充一点,就是如果你想visibility属性一样,xml代码提示出现的是gone、invisible等,而不是自己填的,可这样实现
即该属性为枚举类型,在xml赋值的时候也只能选择这几个值的其中一个,代码中获取值,比如这里的value是数字,那么代码中就可以直接typedArray.getInt()获取了,从而避免传入你不支持的值。如果你想用户可以从这几个值中选择,或自己输入数字,那么可以将format="enum"改为format="integer"就行了,不过在xml代码提示中你定义的这几个值会排到最后,value中的integer值会排在前面。
对了,最后提醒一句如要自定义的属性在xml中有代码提示,那么
好,本文结束,如有说的不好的地方请多多包涵,也可在评论中指点一下。