Android自定义ViewGroup实现标签流容器FlowLayout

本篇文章讲的是Android 自定义ViewGroup之实现标签流式布局-FlowLayout,开发中我们会经常需要实现类似于热门标签等自动换行的流式布局的功能,网上也有很多这样的FlowLayout,但不影响我对其的学习。和往常一样,主要还是想总结一下自定义ViewGroup的开发过程以及一些需要注意的地方。

按照惯例,我们先来看看效果图

一、写代码之前,有几个是问题是我们先要弄清楚的:

1、什么是ViewGroup:从名字上来看,它可以被翻译为控件组,言外之意是ViewGroup内部包含了许多个控件,是一组View。在Android的设计中,ViewGroup也继承了View,这就意味着View本身就可以是单个控件也可以是由多个控件组成的一组控件;

2、ViewGroup的种类:常见的有LinearLayout、RelativeLayout、FrameLayout、AbsoluteLayout、GirdLayout、TableLayout。其中LinearLayout和RelativeLayout使用的最多的两种;

3、ViewGroup的职责:给childView计算出建议的宽和高和测量模式 ,然后决定childView的位置;

4、话说何为流式布局(FlowLayout):就是控件根据ViewGroup的宽,自动的从左往右添加。如果当前行还能放得这个子View,就放到当前行,如果当前行剩余的空间不足于容纳这个子View,则自动添加到下一行的最左边;

二、先总结下自定义ViewGroup的步骤:

1、自定义ViewGroup的属性
2、在ViewGroup的构造方法中获得我们自定义的属性
3、重写onMesure
4、重写onLayout

三、ViewGroup的几个构造函数:
1、public FlowLayout(Context context)
—>Java代码直接new一个FlowLayout实例的时候,会调用这个只有一个参数的构造函数;
2、public FlowLayout(Context context, AttributeSet attrs)
—>在默认的XML布局文件中创建的时候调用这个有两个参数的构造函数。AttributeSet类型的参数负责把XML布局文件中所自定义的属性通过AttributeSet带入到View内;
3、public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr)
—>构造函数中第三个参数是默认的Style,这里的默认的Style是指它在当前Application或者Activity所用的Theme中的默认Style,且只有在明确调用的时候才会调用
4、public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
—>该构造函数是在API21的时候才添加上的

四、下面我们就开始来看看自定义ViewGroup的主要代码啦

1、自定义ViewGroup的属性,首先在res/values/ 下建立一个attr.xml , 在里面定义我们的需要用到的属性以及声明相对应属性的取值类型

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <!--每个item纵向间距-->
 <attr name="verticalSpacing" format="dimension" />
 <!-- 每个item横向间距-->
 <attr name="horizontalSpacing" format="dimension" />

 <declare-styleable name="FlowLayout">
 <attr name="verticalSpacing" />
 <attr name="horizontalSpacing" />
 </declare-styleable>

</resources>

我们定义了verticalSpacing以及horizontalSpacing2个属性,分别表示每个标签之间纵向间距和横向间距,其中format是值该属性的取值类型,format取值类型总共有10种,包括:string,color,demension,integer,enum,reference,float,boolean,fraction和flag。

2、然后在XML布局中声明我们的自定义View

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:custom="http://schemas.android.com/apk/res-auto"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical">

 <TextView
 android:layout_width="match_parent"
 android:layout_height="48dp"
 android:background="#38353D"
 android:gravity="center"
 android:text="标签"
 android:textColor="@android:color/white"
 android:textSize="16dp" />

 <ScrollView
 android:layout_width="match_parent"
 android:layout_height="wrap_content">

 <LinearLayout
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="vertical">

 <TextView
 android:id="@+id/tv_remind"
 android:layout_width="match_parent"
 android:layout_height="46dp"
 android:background="@android:color/white"
 android:gravity="center_vertical"
 android:paddingLeft="15dp"
 android:text="我的标签(最多5个) "
 android:textSize="16dp" />

 <com.per.flowlayoutdome.FlowLayout
 android:id="@+id/tcy_my_label"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@android:color/white"
 android:padding="5dp"
 android:visibility="gone"
 custom:horizontalSpacing="6dp"
 custom:verticalSpacing="12dp" />

 <View
 android:layout_width="match_parent"
 android:layout_height="10dp"
 android:background="#f6f6f6" />

 <RelativeLayout
 android:layout_width="match_parent"
 android:layout_height="46dp"
 android:background="@android:color/white">

 <TextView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_centerVertical="true"
  android:paddingLeft="15dp"
  android:text="推荐标签 "
  android:textSize="16dp" />
 </RelativeLayout>

 <View
 android:layout_width="match_parent"
 android:layout_height="1dp"
 android:background="#f6f6f6" />

 <com.per.flowlayoutdome.FlowLayout
 android:id="@+id/tcy_hot_label"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:background="@android:color/white"
 android:padding="5dp"
 custom:horizontalSpacing="6dp"
 custom:verticalSpacing="12dp" />
 </LinearLayout>
 </ScrollView>
</LinearLayout>

一定要引入xmlns:custom=”http://schemas.android.com/apk/res-auto”,Android Studio中我们可以使用res-atuo命名空间,就不用在添加自定义View全类名。

3、在View的构造方法中,获得我们的自定义的样式

/**
 * 每个item纵向间距
 */
 private int mVerticalSpacing;
 /**
 * 每个item横向间距
 */
 private int mHorizontalSpacing;

 public FlowLayout(Context context) {
 this(context, null);
 }

 public FlowLayout(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
 }

 public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
 super(context, attrs, defStyle);
 /**
 * 获得我们所定义的自定义样式属性
 */
 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FlowLayout, defStyle, 0);
 for (int i = 0; i < a.getIndexCount(); i++) {
 int attr = a.getIndex(i);
 switch (attr) {
 case R.styleable.FlowLayout_verticalSpacing:
  mVerticalSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_verticalSpacing, 5);
  break;
 case R.styleable.FlowLayout_horizontalSpacing:
  mHorizontalSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_horizontalSpacing, 10);
  break;
 }
 }
 a.recycle();
 }

我们重写了3个构造方法,在上面的构造方法中说过默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造方法调用三个参数的构造方法,然后在三个参数的构造方法中获得自定义属性。

一开始一个参数的构造方法和两个参数的构造方法是这样的:

 public FlowLayout(Context context) {
 super(context);
 }

 public FlowLayout(Context context, AttributeSet attrs) {
 super(context, attrs);
 }

有一点要注意的是super应该改成this,然后让一个参数的构造方法引用两个参数的构造方法,两个参数的构造方法引用三个参数的构造方法,代码如下:

 public FlowLayout(Context context) {
 this(context, null);
 }

 public FlowLayout(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
 }

4、重写onMesure方法

/**
 * 负责设置子控件的测量模式和大小 根据所有子控件设置自己的宽和高
 */
 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 /**
 * 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
 */
 int heighMode = MeasureSpec.getMode(heightMeasureSpec);
 int heighSize = MeasureSpec.getSize(heightMeasureSpec);
 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
 /**
 * 高
 */
 int height = 0;
 /**
 * 每一行的高度,累加至height
 */
 int lineHeight = 0;
 /**
 * 在warp_content情况下,记录当前childView的左边的一个位置
 */
 int childLeft = getPaddingLeft();
 /**
 * 在warp_content情况下,记录当前childView的上边的一个位置
 */
 int childTop = getPaddingTop();
 // getChildCount得到子view的数目,遍历循环出每个子View
 for (int i = 0; i < getChildCount(); i++) {
 //拿到index上的子view
 View childView = getChildAt(i);
 // 测量每一个child的宽和高
 measureChild(childView, widthMeasureSpec, heightMeasureSpec);
 //当前子空间实际占据的高度
 int childHeight = childView.getMeasuredHeight();
 //当前子空间实际占据的宽度
 int childWidth = childView.getMeasuredWidth();
 lineHeight = Math.max(childHeight, lineHeight);// 取最大值
 //如果加入当前childView,超出最大宽度,则将目前最大宽度给width,类加height 然后开启新行
 if (childWidth + childLeft + getPaddingRight() > widthSize) {
 childLeft = getPaddingLeft();// 重新开启新行,开始记录childLeft
 childTop += mVerticalSpacing + childHeight;// 叠加当前的高度
 lineHeight = childHeight;// 开启记录下一行的高度
 }else{
 //否则累加当前childView的宽度
 childLeft += childWidth + mHorizontalSpacing;
 }
 }
 height += childTop + lineHeight + getPaddingBottom();
 setMeasuredDimension(widthSize, heighMode == MeasureSpec.EXACTLY ? heighSize : height);
 }

首先首先得到其父容器传入的测量模式和宽高的计算值,然后遍历所有的childView,使用measureChild方法对所有的childView进行测量。然后根据所有childView的测量得出的高得到该ViewGroup如果设置为wrap_content时的高。最后根据模式,如果是MeasureSpec.EXACTLY则直接使用父ViewGroup传入的高,否则设置为自己计算的高,细心的朋友会问,那儿宽呢,在这里我们默认宽为MeasureSpec.EXACTLY模式。

5、重写onLayout方法

@Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
 int width = r - l;
 int childLeft = getPaddingLeft();
 int childTop = getPaddingTop();
 int lineHeight = 0;
 //遍历所有childView根据其宽和高,计算子控件应该出现的位置
 for (int i = 0; i < getChildCount(); i++) {
 final View childView = getChildAt(i);
 if (childView.getVisibility() == View.GONE) {
 continue;
 }
 int childWidth = childView.getMeasuredWidth();
 int childHeight = childView.getMeasuredHeight();
 lineHeight = Math.max(childHeight, lineHeight);
 // 如果已经需要换行
 if (childLeft + childWidth + getPaddingRight() > width) {
 childLeft = getPaddingLeft();
 childTop += mVerticalSpacing + lineHeight;
 lineHeight = childHeight;
 }
 childView.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
 childLeft += childWidth + mHorizontalSpacing;
 }
 }

onLayout中完成对所有childView的位置以及大小的指定

6、到此,我们对自定义ViewGroup的代码已经写完了,有几点要注意的:
(1)getChildAt(int index):获得index上的子view;
(2)getChildCount():得到所有子view的数目;
(3)measureChild(childView, widthMeasureSpec, heightMeasureSpec):使用子view自身的测量方法,测量每一个child的宽和高;

回归到主题,现在我们把自定义ViewGroup,实现FlowLayout的部分完成了,接下来的就是一些逻辑代码了

五、下面就是一些逻辑代码啦

1、我把FlowLayout里面完整的代码贴出来:

package com.per.flowlayoutdome;

import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

/**
 * @author: xiaolijuan
 * @description: 流式布局-标签流容器
 * @projectName: FlowLayoutDome
 * @date: 2016-06-16
 * @time: 16:21
 */
public class FlowLayout extends ViewGroup{
 /**
 * 每个item纵向间距
 */
 private int mVerticalSpacing;
 /**
 * 每个item横向间距
 */
 private int mHorizontalSpacing;
 private BaseAdapter mAdapter;
 private TagItemClickListener mListener;
 private DataChangeObserver mObserver;

 public FlowLayout(Context context) {
 this(context, null);
 }

 public FlowLayout(Context context, AttributeSet attrs) {
 this(context, attrs, 0);
 }

 public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
 super(context, attrs, defStyle);
 /**
 * 获得我们所定义的自定义样式属性
 */
 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.FlowLayout, defStyle, 0);
 for (int i = 0; i < a.getIndexCount(); i++) {
 int attr = a.getIndex(i);
 switch (attr) {
 case R.styleable.FlowLayout_verticalSpacing:
  mVerticalSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_verticalSpacing, 5);
  break;
 case R.styleable.FlowLayout_horizontalSpacing:
  mHorizontalSpacing = a.getDimensionPixelSize(R.styleable.FlowLayout_horizontalSpacing, 10);
  break;
 }
 }
 a.recycle();
 }

 /**
 * 负责设置子控件的测量模式和大小 根据所有子控件设置自己的宽和高
 */
 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 /**
 * 获得此ViewGroup上级容器为其推荐的宽和高,以及计算模式
 */
 int heighMode = MeasureSpec.getMode(heightMeasureSpec);
 int heighSize = MeasureSpec.getSize(heightMeasureSpec);
 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
 /**
 * 高
 */
 int height = 0;
 /**
 * 每一行的高度,累加至height
 */
 int lineHeight = 0;
 /**
 * 在warp_content情况下,记录当前childView的左边的一个位置
 */
 int childLeft = getPaddingLeft();
 /**
 * 在warp_content情况下,记录当前childView的上边的一个位置
 */
 int childTop = getPaddingTop();
 // getChildCount得到子view的数目,遍历循环出每个子View
 for (int i = 0; i < getChildCount(); i++) {
 //拿到index上的子view
 View childView = getChildAt(i);
 // 测量每一个child的宽和高
 measureChild(childView, widthMeasureSpec, heightMeasureSpec);
 //当前子空间实际占据的高度
 int childHeight = childView.getMeasuredHeight();
 //当前子空间实际占据的宽度
 int childWidth = childView.getMeasuredWidth();
 lineHeight = Math.max(childHeight, lineHeight);// 取最大值
 //如果加入当前childView,超出最大宽度,则将目前最大宽度给width,类加height 然后开启新行
 if (childWidth + childLeft + getPaddingRight() > widthSize) {
 childLeft = getPaddingLeft();// 重新开启新行,开始记录childLeft
 childTop += mVerticalSpacing + childHeight;// 叠加当前的高度
 lineHeight = childHeight;// 开启记录下一行的高度
 }else{
 //否则累加当前childView的宽度
 childLeft += childWidth + mHorizontalSpacing;
 }
 }
 height += childTop + lineHeight + getPaddingBottom();
 setMeasuredDimension(widthSize, heighMode == MeasureSpec.EXACTLY ? heighSize : height);
 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
 int width = r - l;
 int childLeft = getPaddingLeft();
 int childTop = getPaddingTop();
 int lineHeight = 0;
 //遍历所有childView根据其宽和高,计算子控件应该出现的位置
 for (int i = 0; i < getChildCount(); i++) {
 final View childView = getChildAt(i);
 if (childView.getVisibility() == View.GONE) {
 continue;
 }
 int childWidth = childView.getMeasuredWidth();
 int childHeight = childView.getMeasuredHeight();
 lineHeight = Math.max(childHeight, lineHeight);
 // 如果已经需要换行
 if (childLeft + childWidth + getPaddingRight() > width) {
 childLeft = getPaddingLeft();
 childTop += mVerticalSpacing + lineHeight;
 lineHeight = childHeight;
 }
 childView.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
 childLeft += childWidth + mHorizontalSpacing;
 }
 }

 private void drawLayout() {
 if (mAdapter == null || mAdapter.getCount() == 0) {
 return;
 }
 removeAllViews();
 for (int i = 0; i < mAdapter.getCount(); i++) {
 View view = mAdapter.getView(i, null, null);
 final int position = i;
 view.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {
  if (mListener != null) {
  mListener.itemClick(position);
  }
 }
 });
 addView(view);
 }
 }

 public void setAdapter(BaseAdapter adapter) {
 if (mAdapter == null) {
 mAdapter = adapter;
 if (mObserver == null) {
 mObserver = new DataChangeObserver();
 mAdapter.registerDataSetObserver(mObserver);
 }
 drawLayout();
 }
 }

 public void setItemClickListener(TagItemClickListener mListener) {
 this.mListener = mListener;
 }

 public interface TagItemClickListener {
 void itemClick(int position);
 }

 class DataChangeObserver extends DataSetObserver {
 @Override
 public void onChanged() {
 drawLayout();
 }

 @Override
 public void onInvalidated() {
 super.onInvalidated();
 }
 }
}

2、FlowLayoutAdapter

package com.per.flowlayoutdome;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;

import java.util.List;

/**
 * @author: adan
 * @description: 流式布局适配器
 * @projectName: FlowLayoutDome
 * @date: 2016-06-16
 * @time: 16:22
 */
public class FlowLayoutAdapter extends BaseAdapter {

 private Context mContext;
 private List<String> mList;

 public FlowLayoutAdapter(Context context, List<String> list) {
 mContext = context;
 mList = list;
 }

 @Override
 public int getCount() {
 return mList.size();
 }

 @Override
 public String getItem(int position) {
 return mList.get(position);
 }

 @Override
 public long getItemId(int position) {
 return position;
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
 ViewHolder holder;
 if (convertView == null) {
 convertView = LayoutInflater.from(mContext).inflate(
  R.layout.item_tag, null);
 holder = new ViewHolder();
 holder.mBtnTag = (Button) convertView.findViewById(R.id.btn_tag);
 convertView.setTag(holder);
 } else {
 holder = (ViewHolder) convertView.getTag();
 }
 holder.mBtnTag.setText(getItem(position));
 return convertView;
 }

 static class ViewHolder {
 Button mBtnTag;
 }
}

3、MainActivity

package com.per.flowlayoutdome;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends Activity {
 private TextView tv_remind;

 private FlowLayout tcy_my_label, tcy_hot_label;
 private FlowLayoutAdapter mMyLabelAdapter, mHotLabelAdapter;
 private List<String> MyLabelLists, HotLabelLists;

 private static int TAG_REQUESTCODE = 0x101;

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

 private void initView() {
 tv_remind = (TextView) findViewById(R.id.tv_remind);
 tcy_my_label = (FlowLayout) findViewById(R.id.tcy_my_label);
 tcy_hot_label = (FlowLayout) findViewById(R.id.tcy_hot_label);
 }

 private void initData() {
 String[] date = getResources().getStringArray(R.array.tags);
 HotLabelLists = new ArrayList<>();
 for (int i = 0; i < date.length; i++) {
 HotLabelLists.add(date[i]);
 }
 mHotLabelAdapter = new FlowLayoutAdapter(this, HotLabelLists);
 tcy_hot_label.setAdapter(mHotLabelAdapter);
 tcy_hot_label.setItemClickListener(new TagCloudLayoutItemOnClick(1));

 MyLabelLists = new ArrayList<>();
 mMyLabelAdapter = new FlowLayoutAdapter(this, MyLabelLists);
 tcy_my_label.setAdapter(mMyLabelAdapter);
 tcy_my_label.setItemClickListener(new TagCloudLayoutItemOnClick(0));

 String labels = String.valueOf(getIntent().getStringExtra("labels"));
 if (!TextUtils.isEmpty(labels) && labels.length() > 0
 && !labels.equals("null")) {
 String[] temp = labels.split(",");
 for (int i = 0; i < temp.length; i++) {
 MyLabelLists.add(temp[i]);
 }
 ChangeMyLabels();
 }

 }

 /**
 * 刷新我的标签数据
 */
 private void ChangeMyLabels() {
 tv_remind.setVisibility(MyLabelLists.size() > 0 ? View.GONE
 : View.VISIBLE);
 tcy_my_label.setVisibility(MyLabelLists.size() > 0 ? View.VISIBLE
 : View.GONE);
 mMyLabelAdapter.notifyDataSetChanged();
 }

 /**
 * 标签的点击事件
 *
 * @author lijuan
 */
 class TagCloudLayoutItemOnClick implements FlowLayout.TagItemClickListener {
 int index;

 public TagCloudLayoutItemOnClick(int index) {
 this.index = index;
 }

 @Override
 public void itemClick(int position) {
 switch (index) {
 case 0:
  MyLabelLists.remove(MyLabelLists.get(position));
  ChangeMyLabels();
  break;
 case 1:
  if (MyLabelLists.size() < 5) {
  if (HotLabelLists.get(position).equals("自定义")) {
  startActivityForResult(
   new Intent(MainActivity.this,
   AddTagActivity.class),
   TAG_REQUESTCODE);
  } else {
  Boolean isExits = isExist(MyLabelLists,
   HotLabelLists.get(position));
  if (isExits) {
  Toast.makeText(MainActivity.this, "此标签已经添加啦", Toast.LENGTH_LONG).show();
  return;
  }
  MyLabelLists.add(HotLabelLists.get(position));
  ChangeMyLabels();
  }
  } else {
  Toast.makeText(MainActivity.this, "最多只能添加5个标签", Toast.LENGTH_LONG).show();
  }
  break;
 default:
  break;
 }
 }
 }

 /**
 * 将数组里面的字符串遍历一遍,看是否存在相同标签
 *
 * @param str
 * @param compareStr
 * @return
 */
 public static Boolean isExist(List<String> str, String compareStr) {
 Boolean isExist = false;//默认沒有相同标签
 for (int i = 0; i < str.size(); i++) {
 if (compareStr.equals(str.get(i))) {
 isExist = true;
 }
 }
 return isExist;
 }

 /**
 * 回传数据
 */
 @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 if (TAG_REQUESTCODE == requestCode) {
 if (resultCode == AddTagActivity.TAG_RESULTCODE) {
 String label = data.getStringExtra("tags");
 MyLabelLists.add(label);
 ChangeMyLabels();
 }
 }
 }
}

4、AddTagActivity

package com.per.flowlayoutdome;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

/**
 * @author: xiaolijuan
 * @description: 添加自定义标签
 * @date: 2016-06-10
 * @time: 14:37
 */
public class AddTagActivity extends Activity implements View.OnClickListener{

 private EditText mEtLabel;
 private Button mBtnSure;
 public final static int TAG_RESULTCODE = 0x102;

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

 private void initData() {
 // 根据输入框输入值的改变提示最大允许输入的个数
 mEtLabel.addTextChangedListener(new TextWatcher_Enum());
 }

 private void initView() {
 mEtLabel = (EditText) findViewById(R.id.et_label);
 mBtnSure = (Button) findViewById(R.id.btn_sure);

 mBtnSure.setOnClickListener(this);
 }

 @Override
 public void onClick(View v) {
 switch (v.getId()) {
 case R.id.btn_sure:
 String label = mEtLabel.getText().toString();
 if (TextUtils.isEmpty(label)) {
  Toast.makeText(AddTagActivity.this,"自定义标签不应为空",Toast.LENGTH_LONG).show();
  return;
 }
 Intent intent = getIntent();
 intent.putExtra("tags", label);
 setResult(TAG_RESULTCODE, intent);
 finish();
 break;
 }
 }

 /**
 * 根据输入框输入值的长度超过8个字符的时候,弹出输入的标签应控制在8个字
 *
 * @author lijuan
 *
 */
 class TextWatcher_Enum implements TextWatcher {
 @Override
 public void beforeTextChanged(CharSequence s, int start, int count,
   int after) {
 }

 @Override
 public void onTextChanged(CharSequence s, int start, int before,
   int count) {
 int lenght = mEtLabel.getText().toString().trim().length();
 if (lenght > 8) {
 Toast.makeText(AddTagActivity.this,"输入的标签应控制在8个字",Toast.LENGTH_LONG).show();
 }
 }

 @Override
 public void afterTextChanged(Editable s) {

 }
 }
}

5、activity_main.xml在上面已经贴出来了,在这里就不重复了,我们创建了arrays.xml,在这里定义了一写热门的标签:

<?xml version="1.0" encoding="UTF-8"?>
<resources>

 <string-array name="tags">
 <item>美妆</item>
 <item>画板</item>
 <item>漫画</item>
 <item>高科技</item>
 <item>韩国电影</item>
 <item>股票</item>
 <item>美术</item>
 <item>高富帅</item>
 <item>鸿泰安</item>
 <item>运动</item>
 <item>外语</item>
 <item>财经</item>
 <item>大叔</item>
 <item>非主流</item>
 <item>暴走漫画</item>
 <item>心理学</item>
 <item>汉语</item>
 <item>白富美</item>
 <item>自定义</item>
 </string-array>

</resources>

6、item_tag.xml

<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/btn_tag"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:background="@drawable/selector_btn_item"
 android:gravity="center"
 android:minHeight="30dp"
 android:minWidth="45dp"
 android:paddingLeft="16dp"
 android:paddingRight="16dp"
 android:textSize="12sp" />

6、activity_add_tag.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="250dp"
 android:layout_height="wrap_content"
 android:background="@android:color/white"
 android:gravity="center_horizontal"
 android:orientation="vertical"
 android:padding="5dp" >

 <LinearLayout
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="vertical" >

 <TextView
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_marginLeft="5dp"
 android:text="请输入想要添加的标签"
 android:textColor="@android:color/black"
 android:textSize="16dp" />

 <EditText
 android:id="@+id/et_label"
 android:layout_width="match_parent"
 android:layout_height="80dp"
 android:layout_margin="5dp"
 android:background="@drawable/selector_btn_item"
 android:gravity="center_vertical|start"
 android:maxLength="8"
 android:paddingLeft="10dp"
 android:textColor="@android:color/black"
 android:textSize="16dp" />
 </LinearLayout>

 <Button
 android:id="@+id/btn_sure"
 android:layout_width="50dp"
 android:layout_height="32dp"
 android:layout_marginLeft="16dp"
 android:layout_marginRight="16dp"
 android:layout_marginTop="5dp"
 android:background="#38353D"
 android:gravity="center"
 android:text="确定"
 android:textColor="@android:color/white"
 android:textSize="14dp" />

</LinearLayout>

7、selector_btn_item.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:state_pressed="true">
 <shape>
 <solid android:color="#ff76787b" />
 <corners android:bottomLeftRadius="5dp" android:bottomRightRadius="5dp" android:topLeftRadius="5dp" android:topRightRadius="5dp" />
 <stroke android:width="1px" android:color="#ffd1d1d1" />
 </shape>
 </item>
 <item>
 <shape>
 <solid android:color="#ffffff" />
 <corners android:bottomLeftRadius="2.5dp" android:bottomRightRadius="2.5dp" android:topLeftRadius="2.5dp" android:topRightRadius="2.5dp" />
 <stroke android:width="0.5px" android:color="#ffd1d1d1" />
 </shape>
 </item>
</selector>

最后一点了吧,我们在AndroidManifest.xml中需要添加

<activity
 android:name=".AddTagActivity"
 android:theme="@style/dialogstyle" />

用于我们自定义标签,弹出的一个类似于对话框的一个Activity,这里我们引用了自定义一个样式

<style name="dialogstyle">
 <!--设置dialog的背景-->
 <item name="android:windowBackground">@android:color/transparent</item>
 <!--设置Dialog的windowFrame框为无-->
 <item name="android:windowFrame">@null</item>
 <!--设置无标题-->
 <item name="android:windowNoTitle">true</item>
 <!--是否浮现在activity之上-->
 <item name="android:windowIsFloating">true</item>
 <!--是否半透明-->
 <item name="android:windowIsTranslucent">true</item>
 <!--设置窗口内容不覆盖-->
 <item name="android:windowContentOverlay">@null</item>
 <!--设置动画,在这里使用让它继承系统的Animation.Dialog-->
 <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
 <!--背景是否模糊显示-->
 <item name="android:backgroundDimEnabled">true</item>
 </style>

对于这个类似于对话框的一个Activity,有不明白的可以上我之前的一篇文章: Android中使用Dialog风格弹出框的Activity

源码下载:http://xiazai.jb51.net/201609/yuanma/AndroidFlowLayout(jb51.net).rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 从源码解析Android中View的容器ViewGroup

    这回我们是深入到ViewGroup内部\,了解ViewGroup的工作,同时会阐述更多有关于View的相关知识.以便为以后能灵活的使用自定义空间打更近一步的基础.希望有志同道合的朋友一起来探讨,深入Android内部,深入理解Android. 一.ViewGroup是什么?        一个ViewGroup是一个可以包含子View的容器,是布局文件和View容器的基类.在这个类里定义了ViewGroup.LayoutParams类,这个类是布局参数的子类. 其实ViewGroup也就是Vie

  • Android中实现多行、水平滚动的分页的Gridview实例源码

    功能要求: (1)比如每页显示2X2,总共2XN,每个item显示图片+文字(点击有链接). 如果单行水平滚动,可以用Horizontalscrollview实现. 如果是多行水平滚动,则结合Gridview(一般是垂直滚动的)和Horizontalscrollview实现. (2)水平滚动翻页,下面有显示当前页的icon. 1.实现自定义的HorizontalScrollView(HorizontalScrollView.java): 因为要翻页时需要传当前页给调用者,所以fling函数中自己

  • 详解Android使GridView横向水平滚动的实现方式

    Android为我们提供了竖直方向的滚动控件GridView,但如果我们想让它水平滚动起来,就需要自己实现了. 以下使用的测试数据datas集合都为List<ResolveInfo>类型,用来存储手机中的所有App public static List<ResolveInfo> getAppData(Context context) { PackageManager packageManager = context.getPackageManager(); Intent mainI

  • Android应用开发中自定义ViewGroup视图容器的教程

    一.概述 在写代码之前,我必须得问几个问题: 1.ViewGroup的职责是啥? ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width).高度(layout_height).对齐方式(layout_gravity)等:当然还有margin等:于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 :决定childView的位置:为什么只是

  • Android使用RecyclerView实现水平滚动控件

    前言 相信大家都知道Android滚动控件的实现方式有很多, 使用RecyclerView也比较简单. 做了一个简单的年龄滚动控件, 让我们来看看RecyclerView的使用方式, 主要有以下几点: (1) 对齐控件中心位置. (2) 计算滚动距离. (3) 高亮中心视图. (4) 实时显示中心数据. (5) 停止时自动对齐. (6) 滚动时, 设置按钮状态开关. 效果 1. 框架 主要关注RecyclerView部分逻辑. /** * 初始化年龄滑动条 */ private void ini

  • Android实现Activity水平和垂直滚动条的方法

    本文实例讲述了Android实现Activity水平和垂直滚动条的方法.分享给大家供大家参考,具体如下: <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:scrollbars="

  • android listview 水平滚动和垂直滚动的小例子

    网上有很多解决 android listview 水平和垂直滚动的代码,我没有按照他们说的做(以前没搜到 O(∩_∩)O~) 我采用的是添加HorizontalScrollViewJava代码 复制代码 代码如下: < ScrollView android:id="@+id/ScrollView01" android:layout_height="300px" android:layout_x="16px" android:layout_y

  • Android使用Recyclerview实现图片水平自动循环滚动效果

    简介: 本篇博客主要介绍的是如何使用RecyclerView实现图片水平方向自动循环(跑马灯效果) 效果图:  思路: 1.准备m张图片 1.使用Recyclerview实现,返回无数个(实际Interge.MAXVALUE)item,第n个item显示第n%m张图片 3.使用recyclerview.scrollBy  每个一段时间水平滚动一段距离 4.通过layoutManager.findFirstVisibleItemPosition()获取当前显示的第一个View是第几个item,上面

  • Android中标签容器控件的实例详解

    前言 在一些APP中我们可以看到一些存放标签的容器控件,和我们平时使用的一些布局方式有些不同,它们一般都可以自动适应屏幕的宽度进行布局,根据对自定义控件的一些理解,今天写一个简单的标签容器控件,给大家参考学习. 下面这个是我在手机上截取的一个实例,是在MIUI8系统上截取的 这个是我实现的效果图 原理介绍 根据对整个控件的效果分析,大致可以将控件分别从以下这几个角度进行分析: 1.首先涉及到自定义的ViewGroup,因为现有的控件没法满足我们的布局效果,就涉及到要重写onMeasure和onL

  • Android开发实现自定义水平滚动的容器示例

    本文实例讲述了Android开发实现自定义水平滚动的容器.分享给大家供大家参考,具体如下: public class HorizontalScrollView extends ViewGroup { //手势 private GestureDetector mGestureDetector; private HorizontalScroller mScroller; private int curID; //快速滑动 private boolean isFlying; //--回调函数-----

  • Android自定义控件之继承ViewGroup创建新容器

    欢迎大家来学习本节内容,前几节我们已经学习了其他几种自定义控件,分别是Andriod 自定义控件之音频条及 Andriod 自定义控件之创建可以复用的组合控件还没有学习的同学请先去学习下,因为本节将使用到上几节所讲述的内容. 在学习新内容之前,我们先来弄清楚两个问题: 1 . 什么是ViewGroup? ViewGroup是一种容器.它包含零个或以上的View及子View. 2 . ViewGroup有什么作用? ViewGroup内部可以用来存放多个View控件,并且根据自身的测量模式,来测量

随机推荐