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
正式讲解之前先看下我们实现后的效果图:
目前网上有很多的教程来写流式布局实现,我看到的版本大体上有两种,一种是继承ViewGroup,然后重写其onMeasure和onLayout方法,另一种则是继承自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
- Android-教你自作一个简单而又实用的流式Tag标签布局
- Android-教你自作一个简单而又实用的流式Tag标签布局
- Android 自定义一个简单而又实用的流式Tag标签布局
- 流式布局TAG标签
- 简单而又实用的足球分析方法
- Android流式布局FlowLayout,一款针对Tag的布局
- 一些常用的简单而又实用的命令
- 【Android】一个简单又实用的toolbar
- 自作标签
- android 在布局中合理的使用tag标签的好处
- CoordinatorLayout布局的简单实用
- 分享一个帮助你自定义标签并且兼容现代浏览器的javascript类库 : X-tag
- 一个小小的tag标签输入插件
- 带你实现一个简单实用的时间线
- 自作的简单的DOS
- 用来设置标签的流式布局简单设计
- 一个简单而又灵活的数据库操作类
- 一个简单而又不影响运行的日志函数
- 初涉java(事件处理与事件监听)
- 4w5:第四周程序填空题3(二维数组的重载[][])
- win10安装sublime text 2
- <hdoj1285>确定比赛名次
- Asp.net 控件用法汇总-RadioButtonList、DropDownList、button、Checkbox...(续)
- Android-教你自作一个简单而又实用的流式Tag标签布局
- poj 1375
- 在数据库中插入数据测试java后台接口数据传输
- 【Codeforces 703B - Mishka and trip】
- POJ:2585 Window Pains(简单拓扑排序+有难度打map表)
- linux学习第二篇:初识linux简单命令
- Lonlife-ACM 1010 Alarm
- SDUT数据结构实验之串三:KMP应用
- POJ 1724 ROADS