使用Adapter设计模式打造一个流式布局FlowLayout

流式布局可以说是在各种软件中的出场率都很高的一个布局方式,被广泛使用,像一些关键字搜索,标签等等的场景,更是随处可见,今天我们就来手把手打造一个FlowLayout。
FlowLayout由于是以一个容器的身份存在的,所以其需要继承的是ViewGroup而不是View,也就是说,我们今天所要做的,就是去自定义一个ViewGroup。
自定义ViewGroup和自定义View的套路基本都是一致的,但也有部分差异,下面,我们先来说说自定义ViewGroup的套路的几个关键点:

  1. 自定义属性(这个就不说了,在之前的文章中已经说了很多次了)

  2. 实现onMeasure()方法,通过测量每一个子View的宽高,来计算自身的宽高,最后达到测量自身宽高的目的。

  3. 实现onLayout()方法,该方法用于按照自己的想法来摆放可见(不为GONE)的子View。

  4. 实现onDraw()方法,默然情况下继承ViewGroup是不会调用该方法的,如果需要绘制一些界面,可以实现dispatchDraw()方法

  5. 注意点:在确定要自定义ViewGroup的时候,可以先考虑下先继承自一些已经封装好的ViewGroup,如LinearLayout等进行开发。

好了,下面,我们开始自定义FlowLayout
首先,自定义属性这一块,我们就直接忽略了,直接从onMeasure方法开始做起(判断view.visibility != GONE,之前忘加了):

/**
     * 测量该控件的宽高
     * 思路:通过测量每一个子view的宽高   来得到该layout的整体宽高
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        /**
         * 解决onMeasure被调用多次,导致在摆放子View的时候出现多次摆放的情况
         */
        mChildViews.clear();

        //获取子View的数量
        int childCount = getChildCount();

        //获取该layout的宽度   以方便控制后面TAG的摆放 和计算该Layout的高度
        int width = MeasureSpec.getSize(widthMeasureSpec);

        //初始化layout的高度
        int height = getPaddingTop() + getPaddingBottom();

        int lineWidth = getPaddingLeft();

        //初始化一个每一行的高度  计算layout的总高度时,取每一行的最高
        int maxHeight = 0;

        ArrayList<View> views = new ArrayList<>();
        mChildViews.add(views);
        //for循环测量子View
        for(int i = 0;i < childCount; i++){
            View childView = getChildAt(i);

            measureChild(childView,widthMeasureSpec,heightMeasureSpec);

            ViewGroup.MarginLayoutParams params = (MarginLayoutParams) childView.getLayoutParams();

            //当叠加的宽度大于该Layout的总宽度时,则换行
            if((lineWidth + childView.getMeasuredWidth() + params.leftMargin + params.rightMargin) > width){
                maxHeight = Math.max(maxHeight,childView.getMeasuredHeight() + params.topMargin + params.bottomMargin);
                height += maxHeight;
                lineWidth = getPaddingLeft() + childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
                views = new ArrayList<>();
                mChildViews.add(views);
                views.add(childView);
            }else{
                maxHeight = Math.max(maxHeight,childView.getMeasuredHeight() + params.topMargin + params.bottomMargin);
                lineWidth += childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
                views.add(childView);
            }
        }

        height += maxHeight;

        setMeasuredDimension(width,height);
    }

在计算宽度时,我们需要获取到每个子View的margin值,但是由于ViewGroup本身是没有LayoutParams的,所以这里模仿了下LinearLayout的方法,来获取MarginLayoutParams:

/**
     * 重写ViewGroup的该方法   获取到margin值(可以模仿LinearLayout)
     * @param attrs
     * @returnop
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }

在onMeasure方法中,当几个子View的宽度总和大于FlowLayout的宽度时,这个时候就需要换行。其他的,注释应该都有了!

下一步,就是实现onLayout()方法,来摆放所有的子View(判断view.visibility != GONE,之前忘加了)

“`
/**
* 摆放子view
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int maxHeight,left,right,top,bottom;
top = getPaddingTop();

    //循环遍历mChildViews
    for(ArrayList<View> views : mChildViews){
        left = getPaddingLeft();
        maxHeight = 0;

        for(int i = 0;i<views.size();i++){
            View childView = views.get(i);
            ViewGroup.MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
            maxHeight = Math.max(maxHeight,childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin);
            left += marginLayoutParams.leftMargin;
            right = left + childView.getMeasuredWidth();
            int childTop = top + marginLayoutParams.topMargin;
            bottom = childTop + childView.getMeasuredHeight();

            childView.layout(left,childTop,right,bottom);
            left += marginLayoutParams.rightMargin + childView.getMeasuredWidth();
        }

        top += maxHeight;
    }
}

到这里呢,基本是这个流式布局就完成了一大半了,下面要做的,就是往这个布局里面扔数据,测试其是否会按照我们既定的规则摆放。

在设置数据这一步,我们使用了Adapter的设计模式。下面来看具体的步骤:
1:定义一个抽象的BaseAdapter

/**
 * Created by DELL on 2017/9/9.
 * Description :
 */

public abstract class BaseAdapter {

    //获取view的数量
    public abstract int getCount();

    //获取View
    public abstract View getView(int position, ViewGroup parent);

}

这边暂时只定义了两个方法,一个是获取子View数量的,另外一个是获取到每一个子View的,暂时没有定义notifySetDataChanged等方法,这个可以根据自己的需求,给其设置一个观察者来处理。

下一步,需要在FloawLayout中去设置Adapter达到添加布局的效果

public void setAdapter(BaseAdapter adapter){
        //当Adapter为空的时候,抛出异常
        if(adapter == null){
            throw new NullPointerException("adapter not null");
        }

        this.mAdapter = adapter;

        int count = mAdapter.getCount();
        for(int i = 0;i<count;i++){
            View view = mAdapter.getView(i,this);
            addView(view);
        }
    }

每一个子View的布局:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/item_textview"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:background="@drawable/bg_textview"
          android:padding="10dp"
          android:layout_margin="5dp"
    >
</TextView>

子view背景代码:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <stroke android:color="@color/colorAccent" android:width="1dp"/>
    <corners android:radius="8dp"/>

</shape>

下面,在Activity中设置Adapter,来测试下效果:

public class MainActivity extends AppCompatActivity {

    private FlowLayout mFlowLayout;
    private ArrayList<String> mStrList;
    private LayoutInflater mLayoutInflater;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        initView();
    }

    private void initData(){
        mStrList = new ArrayList<>();
        mStrList.add("JUSTH HELLO!");
        mStrList.add("JUSTH HEJUSTH HELLO!LLO!");
        mStrList.add("JUSTH LLO!");
        mStrList.add("JUSTH HEJUSTH HELLO!LLO!");
        mStrList.add("JUSTH HELLO!");
        mStrList.add("JUSTLO!");
        mStrList.add("JUSTH HELLO!");
        mStrList.add("JUSTH HEJUSTH HELLO!LLO!");
        mStrList.add("JUSTH HELO!");
        mStrList.add("JUSTH HELLO!");
    }

    private void initView() {
        mFlowLayout = (FlowLayout) findViewById(R.id.taglayout);

        mLayoutInflater = LayoutInflater.from(this);

        mFlowLayout.setAdapter(new BaseAdapter() {
            @Override
            public int getCount() {
                return mStrList.size();
            }

            @Override
            public View getView(int position, ViewGroup parent) {
                View view = mLayoutInflater.inflate(R.layout.layout_textview,parent,false);
                ((TextView)view.findViewById(R.id.item_textview)).setText(mStrList.get(position));
                return view;
            }
        });
    }
}

最后,来一波效果图吧:

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值