android 自动换行FlowLayout

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);
    }
}

3、FlowLayout

准备都做好了,终于来到自定义控件了。(其实实际做的时候,是先简单的实现的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));

对于onLayout方法,只要负责获取每行的起始座标点传给Line的layoutView方法即可。

而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赋值
                }

个人觉得关键的代码就这些了,实际还实现了每行剩余出来空间的分配方式、最多能显示几行和定义了FlowLayout的xml属性等,如想了解,请自己看代码吧!

不知道怎么自定义控件的xml属性的自己百度一下,这个有很多人介绍过,就不多说了,但在这里补充一点,就是如果你想visibility属性一样,xml代码提示出现的是gone、invisible等,而不是自己填的,可这样实现

       <attr name="surplusSpacingMode" format="enum">
            <enum name="SURPLUSSPACINGMODE_AUTO" value="0" />
            <enum name="SURPLUSSPACINGMODE_SHARE" value="1" />
            <enum name="SURPLUSSPACINGMODE_SPACE" value="2"/>
        </attr>
即该属性为枚举类型,在xml赋值的时候也只能选择这几个值的其中一个,代码中获取值,比如这里的value是数字,那么代码中就可以直接typedArray.getInt()获取了,从而避免传入你不支持的值。如果你想用户可以从这几个值中选择,或自己输入数字,那么可以将format="enum"改为format="integer"就行了,不过在xml代码提示中你定义的这几个值会排到最后,value中的integer值会排在前面。

对了,最后提醒一句如要自定义的属性在xml中有代码提示,那么<declare-styleable name="FlowLayout">这里的name要跟你自定义的控件名一样。

好,本文结束,如有说的不好的地方请多多包涵,也可在评论中指点一下。









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章