在这一章节,我们继续学习Android自定义控件。这里要自定义的是Android里面的一个常用控件-Android流式Tag布局,这里我们命名为:FlowTagLayout,我们要实现的流式布局,有如下特色:
目前网上有很多的教程来写流式布局实现,我看到的版本大体上有两种,一种是继承ViewGroup,然后重写其onMeasure和onLayout方法,另一种则是继承自RelativeLayout,例如这个TagView
而我们这里采用的是第一种方法,因为我感觉第一种方法简单、清晰、明了!!
因为我们直接继承的ViewGroup,所以要指定它的LayoutParams,这里因为只需要margin,所以我们直接返回MarginLayoutParams就可以了,代码如下:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@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时的宽和高
@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.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());
}
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 list = new ArrayList();
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 selectedList);
}
改写的都写了,我们怎么使用呢?请继续往下看
首先,我们先写一个适配器TagAdapter,写法完全和写ListView的适配器一样:
/**
* Created by HanHailong on 15/10/19.
*/
public class TagAdapter extends BaseAdapter {
private final Context mContext;
private final List 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 datas) {
mDataList.addAll(datas);
notifyDataSetChanged();
}
public void clearAndAddAll(List datas) {
mDataList.clear();
onlyAddAll(datas);
}
}
再看我们引用FlowTagLayout的主布局代码:
最后,我们看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 mSizeTagAdapter;
private TagAdapter mColorTagAdapter;
private TagAdapter 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 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 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 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 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 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);
}
}
好了,一个简单而实用的流式标签就轻松搞定了!!
如果你觉得本篇博客对你有用,那么就留个言或者顶一个~~
最后,附上github源码FlowTagLayout