Android-教你自作一个简单而又实用的流式Tag标签布局

来源:互联网 发布:域名升级访问中sdashao 编辑:程序博客网 时间:2024/05/17 02:48

在这一章节,我们继续学习Android自定义控件。这里要自定义的是Android里面的一个常用控件-Android流式Tag布局,这里我们命名为:FlowTagLayout,我们要实现的流式布局,有如下特色:

  • 填充数据和ListView、GridView用法一样使用Adapter,更新数据直接通过adapter.notifyDataChanged来更新
  • 支持点击、单选、多选三种模式:FLOW_TAG_CHECKED_NONE、FLOW_TAG_CHECKED_SINGLE、FLOW_TAG_CHECKED_MULTI

正式讲解之前先看下我们实现后的效果图:

image

目前网上有很多的教程来写流式布局实现,我看到的版本大体上有两种,一种是继承ViewGroup,然后重写其onMeasureonLayout方法,另一种则是继承自RelativeLayout,例如这个TagView

而我们这里采用的是第一种方法,因为我感觉第一种方法简单、清晰、明了!!

因为我们直接继承的ViewGroup,所以要指定它的LayoutParams,这里因为只需要margin,所以我们直接返回MarginLayoutParams就可以了,代码如下:

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

onMeasure测量

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        //获取Padding        // 获得它的父容器为它设置的测量模式和大小        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);        //FlowLayout最终的宽度和高度值        int resultWidth = 0;        int resultHeight = 0;        //测量时每一行的宽度        int lineWidth = 0;        //测量时每一行的高度,加起来就是FlowLayout的高度        int lineHeight = 0;        //遍历每个子元素        for (int i = 0, childCount = getChildCount(); i < childCount; i++) {            View childView = getChildAt(i);            //测量每一个子view的宽和高            measureChild(childView, widthMeasureSpec, heightMeasureSpec);            //获取到测量的宽和高            int childWidth = childView.getMeasuredWidth();            int childHeight = childView.getMeasuredHeight();            //因为子View可能设置margin,这里要加上margin的距离            MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();            int realChildWidth = childWidth + mlp.leftMargin + mlp.rightMargin;            int realChildHeight = childHeight + mlp.topMargin + mlp.bottomMargin;            //如果当前一行的宽度加上要加入的子view的宽度大于父容器给的宽度,就换行            if ((lineWidth + realChildWidth) > sizeWidth) {                //换行                resultWidth = Math.max(lineWidth, realChildWidth);                resultHeight += realChildHeight;                //换行了,lineWidth和lineHeight重新算                lineWidth = realChildWidth;                lineHeight = realChildHeight;            } else {                //不换行,直接相加                lineWidth += realChildWidth;                //每一行的高度取二者最大值                lineHeight = Math.max(lineHeight, realChildHeight);            }            //遍历到最后一个的时候,肯定走的是不换行            if (i == childCount - 1) {                resultWidth = Math.max(lineWidth, resultWidth);                resultHeight += lineHeight;            }            setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : resultWidth,                    modeHeight == MeasureSpec.EXACTLY ? sizeHeight : resultHeight);        }    }

代码注释的很详细,首先得到其父容器传入的测量模式和宽高的计算值,然后遍历所有的childView,使用measureChild方法对所有的childView进行测量。然后根据所有childView的测量得出的宽和高得到该ViewGroup如果设置为wrap_content时的宽和高

onLayout

    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        int flowWidth = getWidth();        int childLeft = 0;        int childTop = 0;        //遍历子控件,记录每个子view的位置        for (int i = 0, childCount = getChildCount(); i < childCount; i++) {            View childView = getChildAt(i);            //跳过View.GONE的子View            if (childView.getVisibility() == View.GONE) {                continue;            }            //获取到测量的宽和高            int childWidth = childView.getMeasuredWidth();            int childHeight = childView.getMeasuredHeight();            //因为子View可能设置margin,这里要加上margin的距离            MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();            if (childLeft + mlp.leftMargin + childWidth + mlp.rightMargin > flowWidth) {                //换行处理                childTop += (mlp.topMargin + childHeight + mlp.bottomMargin);                childLeft = 0;            }            //布局            int left = childLeft + mlp.leftMargin;            int top = childTop + mlp.topMargin;            int right = childLeft + mlp.leftMargin + childWidth;            int bottom = childTop + mlp.topMargin + childHeight;            childView.layout(left, top, right, bottom);            childLeft += (mlp.leftMargin + childWidth + mlp.rightMargin);        }    }

onLayout方法就是将子View摆放到FlowTagLayout中,核心就是childView.layout(l,t,r,b)方法。

测量完了,布局也完了,下面就是填充数据了,我们这里采用的是Adapter模式,用法基本上和我们常用的ListView、GridView一样,用户只要写一个适配器Adapter,然后调用xxx.setAdapter方法,就把数据源绑到控件上了,而且这种做法还有个好处:子View可以是任意类型的控件

填充数据和ListView、GridView用法一样使用Adapter,更新数据直接通过adapter.notifyDataChanged来更新

我研究了下ListView和GridView的adapter.notifyDataChanged实现,一句话:观察者模式!首先,我们要在FlowTagLayout里面注册一个观察者,当我们调用adapter.notifyDataChanged的时候能通知这个观察者来刷新页面。

/**     * 像ListView、GridView一样使用FlowLayout     *     * @param adapter     */    public void setAdapter(ListAdapter adapter) {        if (mAdapter != null && mDataSetObserver != null) {            mAdapter.unregisterDataSetObserver(mDataSetObserver);        }        //清除现有的数据        removeAllViews();        mAdapter = adapter;        if (mAdapter != null) {            mDataSetObserver = new AdapterDataSetObserver();            mAdapter.registerDataSetObserver(mDataSetObserver);        }    }

方法 mAdapter.registerDataSetObserver(mDataSetObserver); 
就注册了观察者,我们继续看:

class AdapterDataSetObserver extends DataSetObserver {        @Override        public void onChanged() {            super.onChanged();            reloadData();        }        @Override        public void onInvalidated() {            super.onInvalidated();        }    }

当我们调用adapter.notifyDataChanged方法的时候,就会执行onChanged这个方法,我加了一个reloadData方法:

/**     * 重新加载刷新数据     */    private void reloadData() {        removeAllViews();        for (int i = 0; i < mAdapter.getCount(); i++) {            final int j = i;            mCheckedTagArray.put(i, false);            final View childView = mAdapter.getView(i, null, this);            addView(childView, new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));            final int finalI = i;            childView.setOnClickListener(new OnClickListener() {                @Override                public void onClick(View v) {                    if (mTagCheckMode == FLOW_TAG_CHECKED_NONE) {                        if (mOnTagClickListener != null) {                            mOnTagClickListener.onItemClick(FlowTagLayout.this, childView, j);                        }                    } else if (mTagCheckMode == FLOW_TAG_CHECKED_SINGLE) {                        //判断状态                        if (mCheckedTagArray.get(j)) {                            mCheckedTagArray.put(j, false);                            childView.setSelected(false);                            if (mOnTagSelectListener != null) {                                mOnTagSelectListener.onItemSelect(FlowTagLayout.this, new ArrayList<Integer>());                            }                            return;                        }                        for (int k = 0; k < mAdapter.getCount(); k++) {                            mCheckedTagArray.put(k, false);                            getChildAt(k).setSelected(false);                        }                        mCheckedTagArray.put(j, true);                        childView.setSelected(true);                        if (mOnTagSelectListener != null) {                            mOnTagSelectListener.onItemSelect(FlowTagLayout.this, Arrays.asList(j));                        }                    } else if (mTagCheckMode == FLOW_TAG_CHECKED_MULTI) {                        if (mCheckedTagArray.get(j)) {                            mCheckedTagArray.put(j, false);                            childView.setSelected(false);                        } else {                            mCheckedTagArray.put(j, true);                            childView.setSelected(true);                        }                        //回调                        if (mOnTagSelectListener != null) {                            List<Integer> list = new ArrayList<Integer>();                            for (int k = 0; k < mAdapter.getCount(); k++) {                                if (mCheckedTagArray.get(k)) {                                    list.add(k);                                }                            }                            mOnTagSelectListener.onItemSelect(FlowTagLayout.this, list);                        }                    }                }            });        }    }

这个方法的作用就是重新加载子View,先是移除所有的子View,然后从Adapter中获取子View,addView到FlowTagLayout中,在这个过程中,我们给每个子View添加了点击事件,点击事件里面的逻辑很简单,就是根据FlowTagLayout的三种模式分别处理单击、单选、多选逻辑,三种模式分别为:

/**     * FlowLayout not support checked     */    public static final int FLOW_TAG_CHECKED_NONE = 0;    /**     * FlowLayout support single-select     */    public static final int FLOW_TAG_CHECKED_SINGLE = 1;    /**     * FlowLayout support multi-select     */    public static final int FLOW_TAG_CHECKED_MULTI = 2;

为了使单击、单选、多选事件通知到Activity、Fragment,我们加入了两个监听方法:

/** * Created by HanHailong on 15/10/20. */public interface OnTagClickListener {    void onItemClick(FlowTagLayout parent, View view, int position);}

/** * Created by HanHailong on 15/10/20. */public interface OnTagSelectListener {    void onItemSelect(FlowTagLayout parent, List<Integer> selectedList);}

改写的都写了,我们怎么使用呢?请继续往下看

用法

首先,我们先写一个适配器TagAdapter,写法完全和写ListView的适配器一样:

/** * Created by HanHailong on 15/10/19. */public class TagAdapter<T> extends BaseAdapter {    private final Context mContext;    private final List<T> mDataList;    public TagAdapter(Context context) {        this.mContext = context;        mDataList = new ArrayList<>();    }    @Override    public int getCount() {        return mDataList.size();    }    @Override    public Object getItem(int position) {        return mDataList.get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        View view = LayoutInflater.from(mContext).inflate(R.layout.tag_item, null);        TextView textView = (TextView) view.findViewById(R.id.tv_tag);        T t = mDataList.get(position);        if (t instanceof String) {            textView.setText((String) t);        }        return view;    }    public void onlyAddAll(List<T> datas) {        mDataList.addAll(datas);        notifyDataSetChanged();    }    public void clearAndAddAll(List<T> datas) {        mDataList.clear();        onlyAddAll(datas);    }}

布局tag_item.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="wrap_content"    android:layout_height="wrap_content">    <TextView        android:id="@+id/tv_tag"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginLeft="10dp"        android:layout_marginTop="10dp"        android:background="@drawable/round_rectangle_bg"        android:paddingBottom="5dp"        android:paddingLeft="10dp"        android:paddingRight="10dp"        android:paddingTop="5dp"        android:text="TAG标签"        android:textColor="@color/normal_text_color" /></LinearLayout>

再看我们引用FlowTagLayout的主布局代码:

<?xml version="1.0" encoding="utf-8"?><ScrollView 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:layout_width="match_parent"    android:layout_height="match_parent"    android:fillViewport="true"    app:layout_behavior="@string/appbar_scrolling_view_behavior"    tools:context=".MainActivity"    tools:showIn="@layout/activity_main">    <LinearLayout        android:layout_width="match_parent"        android:layout_height="match_parent"        android:orientation="vertical"        android:paddingBottom="30dp">        <LinearLayout            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:orientation="horizontal">            <TextView                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_margin="10dp"                android:text="颜色\n(点击)" />            <com.hhl.library.FlowTagLayout                android:id="@+id/color_flow_layout"                android:layout_width="wrap_content"                android:layout_height="wrap_content" />        </LinearLayout>        <LinearLayout            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:layout_marginTop="10dp"            android:orientation="horizontal">            <TextView                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_margin="10dp"                android:text="尺寸\n(单选)" />            <com.hhl.library.FlowTagLayout                android:id="@+id/size_flow_layout"                android:layout_width="wrap_content"                android:layout_height="wrap_content" />        </LinearLayout>        <LinearLayout            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:layout_marginTop="10dp"            android:orientation="horizontal">            <TextView                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_margin="10dp"                android:text="移动\n(多选)" />            <com.hhl.library.FlowTagLayout                android:id="@+id/mobile_flow_layout"                android:layout_width="wrap_content"                android:layout_height="wrap_content" />        </LinearLayout>    </LinearLayout></ScrollView>

最后,我们看Activity里面是怎么使用的:

package com.hhl.flowlayoutdemo;import android.os.Bundle;import android.support.design.widget.FloatingActionButton;import android.support.design.widget.Snackbar;import android.support.v7.app.AppCompatActivity;import android.support.v7.widget.Toolbar;import android.view.Menu;import android.view.MenuItem;import android.view.View;import com.hhl.library.FlowTagLayout;import com.hhl.library.OnTagClickListener;import com.hhl.library.OnTagSelectListener;import java.util.ArrayList;import java.util.List;public class MainActivity extends AppCompatActivity {    private FlowTagLayout mColorFlowTagLayout;    private FlowTagLayout mSizeFlowTagLayout;    private FlowTagLayout mMobileFlowTagLayout;    private TagAdapter<String> mSizeTagAdapter;    private TagAdapter<String> mColorTagAdapter;    private TagAdapter<String> mMobileTagAdapter;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);        setSupportActionBar(toolbar);        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);        fab.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)                        .setAction("Action", null).show();            }        });        mColorFlowTagLayout = (FlowTagLayout) findViewById(R.id.color_flow_layout);        mSizeFlowTagLayout = (FlowTagLayout) findViewById(R.id.size_flow_layout);        mMobileFlowTagLayout = (FlowTagLayout) findViewById(R.id.mobile_flow_layout);        //颜色        mColorTagAdapter = new TagAdapter<>(this);        mColorFlowTagLayout.setAdapter(mColorTagAdapter);        mColorFlowTagLayout.setOnTagClickListener(new OnTagClickListener() {            @Override            public void onItemClick(FlowTagLayout parent, View view, int position) {                Snackbar.make(view, "颜色:" + parent.getAdapter().getItem(position), Snackbar.LENGTH_LONG)                        .setAction("Action", null).show();            }        });        //尺寸        mSizeTagAdapter = new TagAdapter<>(this);        mSizeFlowTagLayout.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);        mSizeFlowTagLayout.setAdapter(mSizeTagAdapter);        mSizeFlowTagLayout.setOnTagSelectListener(new OnTagSelectListener() {            @Override            public void onItemSelect(FlowTagLayout parent, List<Integer> selectedList) {                if (selectedList != null && selectedList.size() > 0) {                    StringBuilder sb = new StringBuilder();                    for (int i : selectedList) {                        sb.append(parent.getAdapter().getItem(i));                        sb.append(":");                    }                    Snackbar.make(parent, "移动研发:" + sb.toString(), Snackbar.LENGTH_LONG)                            .setAction("Action", null).show();                }else{                    Snackbar.make(parent, "没有选择标签", Snackbar.LENGTH_LONG)                            .setAction("Action", null).show();                }            }        });        //移动研发标签        mMobileTagAdapter = new TagAdapter<>(this);        mMobileFlowTagLayout.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI);        mMobileFlowTagLayout.setAdapter(mMobileTagAdapter);        mMobileFlowTagLayout.setOnTagSelectListener(new OnTagSelectListener() {            @Override            public void onItemSelect(FlowTagLayout parent, List<Integer> selectedList) {                if (selectedList != null && selectedList.size() > 0) {                    StringBuilder sb = new StringBuilder();                    for (int i : selectedList) {                        sb.append(parent.getAdapter().getItem(i));                        sb.append(":");                    }                    Snackbar.make(parent, "移动研发:" + sb.toString(), Snackbar.LENGTH_LONG)                            .setAction("Action", null).show();                }else{                    Snackbar.make(parent, "没有选择标签", Snackbar.LENGTH_LONG)                            .setAction("Action", null).show();                }            }        });        initColorData();        initSizeData();        initMobileData();    }    private void initMobileData() {        List<String> dataSource = new ArrayList<>();        dataSource.add("android");        dataSource.add("安卓");        dataSource.add("SDK源码");        dataSource.add("IOS");        dataSource.add("iPhone");        dataSource.add("游戏");        dataSource.add("fragment");        dataSource.add("viewcontroller");        dataSource.add("cocoachina");        dataSource.add("移动研发工程师");        dataSource.add("移动互联网");        dataSource.add("高薪+期权");        mMobileTagAdapter.onlyAddAll(dataSource);    }    private void initColorData() {        List<String> dataSource = new ArrayList<>();        dataSource.add("红色");        dataSource.add("黑色");        dataSource.add("花边色");        dataSource.add("深蓝色");        dataSource.add("白色");        dataSource.add("玫瑰红色");        dataSource.add("紫黑紫兰色");        dataSource.add("葡萄红色");        dataSource.add("屎黄色");        dataSource.add("绿色");        dataSource.add("彩虹色");        dataSource.add("牡丹色");        mColorTagAdapter.onlyAddAll(dataSource);    }    /**     * 初始化数据     */    private void initSizeData() {        List<String> dataSource = new ArrayList<>();        dataSource.add("28 (2.1尺)");        dataSource.add("29 (2.2尺)");        dataSource.add("30 (2.3尺)");        dataSource.add("31 (2.4尺)");        dataSource.add("32 (2.5尺)........");        dataSource.add("33 (2.6尺)");        dataSource.add("34 (2.7尺)");        dataSource.add("35 (2.8尺)");        dataSource.add("36 (2.9尺)");        dataSource.add("37 (3.0尺)");        dataSource.add("38 (3.1尺)");        dataSource.add("39 (3.2尺)........");        mSizeTagAdapter.onlyAddAll(dataSource);    }    @Override    public boolean onCreateOptionsMenu(Menu menu) {        // Inflate the menu; this adds items to the action bar if it is present.        getMenuInflater().inflate(R.menu.menu_main, menu);        return true;    }    @Override    public boolean onOptionsItemSelected(MenuItem item) {        // Handle action bar item clicks here. The action bar will        // automatically handle clicks on the Home/Up button, so long        // as you specify a parent activity in AndroidManifest.xml.        int id = item.getItemId();        //noinspection SimplifiableIfStatement        if (id == R.id.action_settings) {            return true;        }        return super.onOptionsItemSelected(item);    }}

好了,一个简单而实用的流式标签就轻松搞定了!!

TODO

  • 添加初始化选中标签
  • 添加tag样式支持(颜色、图标等等)
  • 像ListView、GridView一样复用子View
  • 其他…

如果你觉得本篇博客对你有用,那么就留个言或者顶一个~~

最后,附上github源码FlowTagLayout

0 0
原创粉丝点击