##功能需求
最近项目里有个需求,要实现一个标签多选的功能,标签用 GridView 展示,点击 GridView 的条目,选中标签,可以多选,就像下面图片里展示的效果。
点击左侧的井号,控制下方的标签列表显示和隐藏,点击条目选中,如果选中一条,则直接在井号右边列出来条目内容,如果选中两条以上,则显示选中了多少条。
##再造轮子的缘由
本来以为是一个很简单的功能,调用 API 里的方法就完了,就像下面这样:1
2mGridView.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL);// 设置为多选模式
mGridView.setMultiChoiceModeListener(this);// 设置多选模式监听器
真正开始做的时候才发现,原来在 Android 3.0 之前是没有 MultiChoiceMode 的方法的,而且 3.0 之后的多选也是需要长按才能进入多选模式,与项目需求也不符合。这时候才想起来,之前做图片选择上传的时候,也纠结过这个问题,当时就是自己又手动实现了多选的功能。既然是第二次碰到,干脆记录下来。也就是说,在这个例子里实现了文字的多选,如果想要实现图片的多选,方法过程都是一样的,只需要将其中的对应参数替换即可。
##方案制定
其实具体怎么去实现,方案有很多种,实现起来也都比较简单,每个人喜好都不一样,从我个人角度来讲,我更倾向于用 Fragment 去实现下面的标签列表,这个页面本身是一个 Activity ,Fragment 和 Activity 之间数据传递也比较方便,更重要的是,通过控制 Fragment 的显示和隐藏,操作起来非常流畅。类似的界面,我也试过用普通的 View 和 PopupWindow 去实现,但是用 View 去实现,每次显示都需要初始化 View ,会很卡;而用 PopupWindow ,则其图层视图时在页面之上的,不符合这样的使用场景。
##标签列表的实现
###如何设计点击多选
我们知道 GridView 跟 ListView 一样,又条目点击事件,调用 setOnItemClickListener 并实现对应借口即可。但是,这只是单选事件,点击了一个条目,如果再点击另一个条目,焦点就跳去另一个条目了,如果要实现多选,我们需要保持前面被点击的条目的状态不变。这个时候,就需要一个开关了, CheckBox 就闪亮登场了。通过 CheckBox 去设置 Item 的状态,然后跟 OnItemClick 的事件一样,通过接口回调,设置 setOnItemClickListener 事件,相当于单击事件方法的重载,当然要传入其他的一些参数。
###条目的布局设置
通过上面的分析,我们发现需要一个 TextView 去展示标签,并显式地展现条目的被点击和取消点击,同时还需要一个 CheckBox ,去记录条目的状态并设置和改变条目状态。当然, CheckBox 是要 gone 掉的,下面就是条目的布局:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/tv_tags_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/tags_bg_color_seletor"
android:gravity="center"
android:paddingBottom="3dip"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:paddingTop="3dip"
android:textColor="@drawable/tags_text_color_seletor"
android:textSize="15sp" >
</TextView>
<CheckBox
android:id="@+id/tags_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@id/tv_tags_item"
android:layout_alignRight="@id/tv_tags_item"
android:layout_alignTop="@id/tv_tags_item"
android:checked="false"
android:clickable="false"
android:focusable="false"
android:visibility="gone" />
</RelativeLayout>
###GridView 适配器的创建
在适配器里,getCount() ,getItem() ,getItemId() ,getView() ,这些方法自不必说,既然是要写 OnItemClick 的重载方法,就必须要重新设计接口,将多出来的 CheckBox 作为参数传进去。同时,还需要设置条目的点击事件,因此要在 getView() 里,设置 convertView.setOnClickListener() ,至于参数,当然就是多选点击接口的实现类对象。因此,在 adapter 里需要一个内部类去实现多选的接口。
1 | import java.util.List; |
当然,要展示条目选中前后的状态变化,我做了一个选择器,设置一个标识,下面是我的选择器:
背景的选择器:1
2
3
4
5
6
7
8
9/** tags_bg_color_seletor */
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/tags_select_shape"></item>
<item android:state_pressed="true" android:drawable="@drawable/tags_select_shape"></item>
<item android:state_selected="false" android:drawable="@drawable/tags_not_select_shape"></item>
</selector>
被选中的条目背景1
2
3
4
5
6/** tags_select_shape */
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<gradient android:angle="90" />
<solid android:color="@color/orange"/>
</shape>
未被选中的条目的背景1
2
3
4
5
6/** tags_not_select_shape */
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<gradient android:angle="90" />
<solid android:color="@color/white"/>
</shape>
文字颜色的选择器1
2
3
4
5
6
7
8<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="@color/white"></item>
<item android:state_pressed="true" android:color="@color/white"></item>
<item android:state_selected="false" android:color="@color/black"></item>
</selector>
###标签列表的展示
接下来就比较简单了,主要就是在 Fragment 里实现 Adapter 里的接口,实现多选即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117import java.util.ArrayList;
import java.util.List;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.GridView;
import android.widget.TextView;
import com.ihuiben.ihuiben.R;
import com.ihuiben.ihuiben.adapter.TagsAdapter;
import com.ihuiben.ihuiben.entity.ColumnsInfo;
import com.ihuiben.ihuiben.manager.BaseApplication;
import com.lidroid.xutils.DbUtils;
import com.lidroid.xutils.db.sqlite.Selector;
import com.lidroid.xutils.exception.DbException;
/**
* 选择标签的 Fragment
*/
public class TagsFragment extends Fragment {
private View view;
private TextView tvComplete;
private GridView gvTags;
private TagsAdapter mAdapter;
List<ColumnsInfo> tvSelectedList = new ArrayList<ColumnsInfo>();
private TagsFragmentCallBack mTagsFragmentCallBack;
private DbUtils db = BaseApplication.db;
private static class TagsFragmentHolder {
private static final TagsFragment INSTANCE = new TagsFragment();
}
private TagsFragment() {
}
public static final TagsFragment getInstance() {
return TagsFragmentHolder.INSTANCE;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
view = inflater.inflate(R.layout.choose_tags, container, false);
tvComplete = (TextView) view.findViewById(R.id.tv_complete_choose_tags);
gvTags = (GridView) view.findViewById(R.id.gv_tags);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
tvComplete.setVisibility(View.GONE);
//此处时我们项目的需求,从缓存里获取数据并展示,使用了 xUtils 里的数据库框架,实际应用里,将数据替换为自己需要的即可。
try {
List<ColumnsInfo> columnsInfos = db.findAll(Selector.from(ColumnsInfo.class).where("_columnid", "=", "3").orderBy("orders"));
mAdapter = new TagsAdapter(columnsInfos, getActivity(), onItemClickClass);
gvTags.setAdapter(mAdapter);
} catch (DbException e) {
e.printStackTrace();
}
}
@Override
public void onAttach(android.app.Activity activity) {
super.onAttach(activity);
if (!(activity instanceof TagsFragmentCallBack)) {
throw new IllegalStateException("TagsFragmentCallBack所在的Activity必须实现TagsFragmentCallBack接口");
}
mTagsFragmentCallBack = (TagsFragmentCallBack) activity;
};
@Override
public void onDetach() {
super.onDetach();
mTagsFragmentCallBack = null;
};
/** 实现接口,点击选中或者取消选中,并获取其被选中的集合 */
TagsAdapter.OnItemClickClass onItemClickClass = new TagsAdapter.OnItemClickClass() {
@Override
public void OnItemClick(View v, int position, CheckBox checkBox, TextView textView) {
try {
if (checkBox.isChecked()) {
checkBox.setChecked(false);
textView.setBackgroundColor(getActivity().getResources().getColor(R.color.white));
textView.setTextColor(getActivity().getResources().getColor(R.color.black));
for (int i = 0; i < tvSelectedList.size(); i++) {
if (textView.getText().toString().equals(tvSelectedList.get(i).getCatename())) {
tvSelectedList.remove(i);
}
}
} else {
ColumnsInfo mColumnsInfo = new ColumnsInfo();
mColumnsInfo = db.findFirst(Selector.from(ColumnsInfo.class).where("catename", "=", textView.getText().toString()));
checkBox.setChecked(true);
textView.setBackgroundColor(getActivity().getResources().getColor(R.color.orange));
textView.setTextColor(getActivity().getResources().getColor(R.color.white));
tvSelectedList.add(mColumnsInfo);
}
//调用接口回传数据给 Acitivty
mTagsFragmentCallBack.onItemSelected(position, tvSelectedList);
} catch (DbException e) {
e.printStackTrace();
}
}
};
/** 在此处设计接口给 Fragment 所在的 Activity 传递数据 */
public interface TagsFragmentCallBack {
public void onItemSelected(int index, List<ColumnsInfo> tvSelectedList);
};
}
##将 Fragment 选择的标签数据回传给 Activity
Fragment 和 Activity 之间的数据传递是一个非常常用的概念,一般从 Acitivty 往 Fragment 传递数据有两种方式,一种是通过构造参数,另一种是设置 Bundle ,而官方推荐的是使用 Bundle ;从 Fragment 往 Activity 里传递数据,一般就通过接口。在这里,我使用了接口,将 Fragment 里选择的标签数据回传给 Acitivty ,如上代码最后几行,即是给 Acitivty 回传数据的接口,当然,必须要在实现点击事件里的方法里去调用这个接口的方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/** 回传数据接口方法的实现 */
private List<ColumnsInfo> columnsInfoList = new ArrayList<ColumnsInfo>();
@Override
public void onItemSelected(int index, List<ColumnsInfo> tvSelectedList) {
this.columnsInfoList = tvSelectedList;
Log.e("onItemSelected", columnsInfoList.size() + "");
if (tvSelectedList.size() == 0) {
chooseTagsCount.setVisibility(View.INVISIBLE);
} else if (tvSelectedList.size() == 1) {
chooseTagsCount.setVisibility(View.VISIBLE);
chooseTagsCount.setText(tvSelectedList.get(0).getCatename());
} else {
chooseTagsCount.setVisibility(View.VISIBLE);
chooseTagsCount.setText("已选择" + tvSelectedList.size() + "个标签");
}
}
这样,上面的方法里成员变量 columnsInfoList 里就存储了 Fragment 里回传过来的数据。通过点击标签条目,即可实现多选,并且是实时的展示:当不选择时,没有提示;当选择一个条目时,展示选择的内容;当选择两个以上条目时,就展示选择条目的数量。