本文转自http://www.jb51.net/article/78961.htm
讲讲Checkbox的基本使用.在XML中定义
1
2
3
4
5
6
|
xml
version
=
"1.0"
encoding
=
"utf-8"
?>
<
CheckBox
xmlns:android
=
"http://schemas.android.com/apk/res/android"
android:id
=
"@+id/cbx"
android:layout_width
=
"wrap_content"
android:layout_height
=
"wrap_content"
android:checked
=
"false"
/>
|
在Activity中使用
1
2
3
4
5
6
7
|
CheckBox cbx = (CheckBox) findViewById(R.id.cbx);
cbx.setOnCheckedChangeListener(
new
CompoundButton.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
//do something
}
});
|
很简单.要注意的是,CheckBox本身是一个视图,是展示给用户看的,因此我们要用数据来控制它的展示.所以,我们的CheckBox在Activity中要这么写
1
2
3
4
5
6
7
8
9
10
11
12
13
|
boolean
isChecked=
false
;
CheckBox cbx = (CheckBox) findViewById(R.id.cbx);
cbx.setOnCheckedChangeListener(
new
CompoundButton.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
if
(isChecked){
//do something
}
else
{
//do something else
}
}
});
cbx.setChecked(isChecked);
|
这样,我们改变数据的时候,视图的状态就会跟着数据来做改变了.注意,监听器一定要这setChecked之前设置,这样才能体现出来数据来控制视图的展示.
单独用CheckBox很easy,接下来,复杂的情况来啦,CheckBox如何跟ListView/RecyclerView(以下简称LV/RV)配合使用.这就不能简单的考虑问题啦,要知道LV/RV中的视图个数跟数据集的里面的数据并不一致,真正的视图个数远小于数据集中数据项的个数.因为屏幕上在列表中的视图是可以复用的.由于LV/RV的复用机制,如果我们没有用数据来控制CheckBox状态的话,将会导致CheckBox的显示在列表中错乱.比方说你只对第一个Item中的CheckBox做了选中操作,当列表向上滚动的时候,你会发现,下面的Item中居然也会有被选中的.当然,问题的关键就在于要用数据来控制视图的显示.因此在getView/onBindViewHolder中,我们应该这么写.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
holder.cbx.setTag(item);
holder.cbx.setOnCheckedChangeListener(
new
CompoundButton.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
Item item =(Item) buttonView.getTag();
if
(isChecked){
item.setCheckState(
true
);
//do something
}
else
{
item.setCheckState(
false
);
//do something else
}
}
});
cbx.setChecked(item.getCheckState());
|
这种方法基本正确,但是我们要额外的给每个数据项里面添加一个字段来记录状态,这代价就有点大了.一是不必这么做,二是这会导致本地数据结构跟服务端结构不一致.通常,列表中使用CheckBox的话,很明显是把选中的item给记录下来,可以这么理解,选中的状态是列表给的,而item本身应该是无状态的.那么,如果重构我们的代码呢,Android为我们提供了一种完美的数据结构来解决这个问题.你可以用SparseArray,也可以用SparseBooleanArray,我现在习惯使用SparseBooleanArray,ok,请看代码
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
|
private
class
Adapter
extends
RecyclerView.Adapter
SparseBooleanArray mCheckStates=
new
SparseBooleanArray();
@Override
public
RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int
viewType) {
//...
}
@Override
public
void
onBindViewHolder(RecyclerView.ViewHolder holder,
int
position) {
holder.cbx.setTag(position);
holder.cbx.setOnCheckedChangeListener(
new
CompoundButton.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
int
pos =(
int
)buttonView.getTag();
if
(isChecked){
mCheckStates.put(pos,
true
);
//do something
}
else
{
mCheckStates.delete(pos);
//do something else
}
}
});
cbx.setChecked(mCheckStates.get(position,
false
));
}
@Override
public
int
getItemCount() {
//...
}
}
|
这样列表就能正常显示了,而且在你选中CheckBox的时候,会自动触发onCheckedChanged来对mCheckStates来进行更新.此时,如果你想用程序来选中某个item的时候,那么直接这样就行了.
1
2
|
mCheckStates.put(pos,
true
);
adapter.notifyDatasetChanged();
|
如果我们想要取出列表列中所有的数据项,有了SparseBooleanArray,
1
2
3
4
5
6
|
ArrayList
new
ArrayList<>();
for
(
int
i=
0
;i < mCheckStates.size();i++){
if
(mCheckStates.valueAt(i)){
selItems.add(allItems.get(mCheckStates.keyAt(i)));
}
}
|
由于CheckBox这个控件太容易变了,为什么这么说呢,因为就算你把它设成disabled的话,它依然是可以点选的,它的onCheckedChanged依然会触发.那么我们该怎么办呢.程序员考虑问题呢,一般都是先想最笨的方法啦,既然onCheckedChanged依然会触发,那我就在里面把buttonView再设置成!isCheck的不就行了嘛.
1
2
3
4
5
6
7
|
holder.cbx.setOnCheckedChangeListener(
new
CompoundButton.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
buttonView.setChecked(!isChecked);
//...
}
});
|
但是这么写的话,就会调用buttonView的onCheckedChanged,其实buttonView就是外面的holder.cbx,这就会造成死循环.因此我们如果用cbx本身去改变状态的话,那么一定要加锁.
1
2
3
4
5
6
7
8
9
10
11
12
|
boolean
lockState=
false
;
holder.cbx.setOnCheckedChangeListener(
new
CompoundButton.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
if
(lockState)
return
;
//不然cbx改变状态.
lockState=
true
;
buttonView.setChecked(!isChecked);
lockState=
false
;
//...
}
});
|
对cbx加锁其实还是挺常用的,比方说在onCheckedChanged中,你要发一个请求,而请求的结果反过来会更新这个cbx的选中状态,你就必须要用lockState来直接改变cbx的状态了,以便于cbx的状态跟mCheckStates里面的是一致的.
mada mada,还有一种情况,如果在onCheckedChanged的时候,isChecked跟mCheckStates.get(pos)一致的话,这会导致什么情况呢.
1
2
3
4
5
6
7
8
9
10
11
|
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
int
pos =(
int
)buttonView.getTag();
if
(isChecked){
mCheckStates.put(pos,
true
);
//do something
}
else
{
mCheckStates.delete(pos);
//do something else
}
}
|
这就会让你的//do something做两次,这么做就是没有必要的啦,而且如果你的//do something是网络请求的话,这样就会导致更大问题.所以,我们有必要对这种情况做过滤.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
if
(lockState)
return
;
int
pos =(
int
)buttonView.getTag();
if
(mCheckStates.get(pos,
false
) == isChecked)
return
;
if
(isChecked){
mCheckStates.put(pos,
true
);
//do something
}
else
{
mCheckStates.delete(pos);
//do something else
}
}
|
好啦,如果你能将CheckBox跟SparseBooleanArray联用,并且能考虑到加锁和过滤重选的话,那么说明你使用CheckBox的姿势摆正了.一个列表仅仅能让用户上滚下滑,那是最简单的使用,通常,由于列表项过多,产品会给列表项添加筛选的功能,而通常我们做筛选,会考虑到使用Spinner来做,但是,有用android自身提供的Spinner扩展性太差,而且长得丑,往往导致大家一怒之下,弃而不用.PopupWindow(介绍),我先介绍一下原理,首先给CheckBox设置setOnCheckedChangeListener,然后在onCheckedChanged里面,isChecked分支中弹出PopupWindow,!isChecked中,读取Popupwindow中的结果,用新的筛选条件来更新列表.ok,上代码:
MainActivity:
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
|
public
class
MainActivity
extends
AppCompatActivity {
String[] filter_type_strs = {
"音乐"
,
"书籍"
,
"电影"
};
CheckBox cbx;
private
boolean
lockState=
false
;
int
current_filter_type=
0
;
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
cbx = (CheckBox) findViewById(R.id.cbx);
cbx.setText(filter_type_strs[current_filter_type]);
cbx.setOnCheckedChangeListener(
new
CompoundButton.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(CompoundButton buttonView,
boolean
isChecked) {
if
(lockState)
return
;
try
{
if
(isChecked) {
//此处传入了cbx做参数
PopupWindow pw =
new
FilterLinePw(buttonView.getContext(), cbx, filter_type_strs);
pw.showAsDropDown(cbx);
}
else
{
//此处的buttonView就是cbx
Integer pos = (Integer) buttonView.getTag();
if
(pos ==
null
|| pos == -
1
)
return
;
current_filter_type = pos;
Toast.makeText(MainActivity.
this
,
"搜索"
+filter_type_strs[current_filter_type], Toast.LENGTH_SHORT).show();
}
}
catch
(NullPointerException e) {
//以防万一
lockState =
true
;
buttonView.setChecked(!isChecked);
lockState =
false
;
}
}
});
}
}
|
FilterLinePw
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
|
public
class
FilterLinePw
extends
PopupWindow {
RadioGroup radioGroup;
CheckBox outCbx;
//为动态生成radioButton生成id
int
[] rbtIds = {
0
,
1
,
2
};
public
FilterLinePw(Context context, CheckBox outCbx, String[] items) {
super
(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
View contentview = LayoutInflater.from(context).inflate(R.layout.filter_line_popupwindow,
null
);
setContentView(contentview);
setFocusable(
true
);
setOutsideTouchable(
true
);
this
.outCbx = outCbx;
contentview.setOnKeyListener(
new
View.OnKeyListener() {
@Override
public
boolean
onKey(View v,
int
keyCode, KeyEvent event) {
if
(keyCode == KeyEvent.KEYCODE_BACK) {
dismiss();
return
true
;
}
return
false
;
}
});
contentview.setFocusable(
true
);
// 这个很重要
contentview.setFocusableInTouchMode(
true
);
contentview.setOnClickListener(
new
View.OnClickListener() {
@Override
public
void
onClick(View v) {
dismiss();
}
});
init(context, contentview,items);
}
private
void
init(Context context, View contentview, String[] items) {
/**
* 用传入的筛选条件初始化UI
*/
radioGroup = (RadioGroup) contentview.findViewById(R.id.filter_layout);
radioGroup.clearCheck();
if
(items ==
null
)
return
;
for
(
int
i =
0
; i < items.length; i++) {
RadioButton radioButton = (RadioButton) LayoutInflater.from(context).inflate(R.layout.line_popupwindow_rbt,
null
);
radioButton.setId(rbtIds[i]);
radioButton.setText(items[i]);
radioGroup.addView(radioButton, -
1
, radioGroup.getLayoutParams());
if
(items[i].equals(outCbx.getText())) {
outCbx.setTag(i);
radioButton.setChecked(
true
);
}
}
radioGroup.setOnCheckedChangeListener(
new
RadioGroup.OnCheckedChangeListener() {
@Override
public
void
onCheckedChanged(RadioGroup group,
int
checkedId) {
dismiss();
}
});
}
//重点内容,重写dismiss();
@Override
public
void
dismiss() {
if
(outCbx !=
null
&& outCbx.isChecked()) {
int
id = radioGroup.getCheckedRadioButtonId();
RadioButton rbt = (RadioButton) radioGroup.findViewById(id);
Integer old_tag = (Integer) outCbx.getTag();
if
(old_tag ==
null
) {
super
.dismiss();
return
;
}
if
(old_tag != id) {
outCbx.setTag(id);
outCbx.setText(rbt.getText());
}
else
{
outCbx.setTag(-
1
);
}
//下面执行之后,会执行MainActivity中的onCheckedChanged里的否定分支
outCbx.setChecked(
false
);
}
super
.dismiss();
}
}
|
效果图:
其实重点在PopupWindow里面,MainActivity的CheckBox作为参数传递到了 PopupWindow里.首先,用户点击MainActivity的CheckBox,接着会执行isChecked分支,这样PopupWindow就展示给了用户,这样用户操作的环境就到了PopupWindow里面,等用户选择好筛选条件后,PopupWindow就把筛选条件设给outCbx,然后改变outCbx状态,从而触发MainActivity中onCheckedChanged中的否定分支,此时展示的是一个Toast,实际应用中可以是一个网络请求.同时,由于PopupWindow的代码并没有阻塞操作,所以会接着执行下一句 super.dismiss(),这样你在MainActivity就不用担心PopupWindow的关闭问题啦.最后,在MainActivity中还加入了try-catch来以防万一,这种机制真是太神奇啦.这种机制把筛选操作从Activity中分离了出来,以后我们写筛选可以完全独立于Activity啦,随后我会把其他筛选的情况开源,
CheckBox是继承自TextView,很多时候,我们的CheckBox的button属性设置的图片都不大,这就导致点击CheckBox的区域也小,因此,我们需要用到TouchDelegate来扩大CheckBox的可点击区域上代码:
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
|
public
class
FrameLayoutCheckBox
extends
FrameLayout {
CompoundButton cbx;
public
FrameLayoutCheckBox(Context context) {
super
(context);
}
public
FrameLayoutCheckBox(Context context, AttributeSet attrs) {
super
(context, attrs);
}
public
FrameLayoutCheckBox(Context context, AttributeSet attrs,
int
defStyleAttr) {
super
(context, attrs, defStyleAttr);
}
private
CheckBox findCheckBox(View view) {
//无递归广度优先遍历寻找CheckBox - -!我只是想重温一下C
ArrayList
new
ArrayList<>();
views.add(view);
while
(!views.isEmpty()) {
View c = views.remove(
0
);
if
(c
instanceof
CheckBox) {
return
(CheckBox) c;
}
else
if
(c
instanceof
ViewGroup) {
ViewGroup fa = (ViewGroup) c;
for
(
int
i =
0
; i < fa.getChildCount(); i++) {
views.add(fa.getChildAt(i));
}
}
}
return
null
;
}
@Override
protected
void
onFinishInflate() {
super
.onFinishInflate();
if
(getChildCount() >
0
) {
View child = findCheckBox(
this
);
if
(child
instanceof
CompoundButton) cbx = (CompoundButton) child;
}
}
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
if
(cbx !=
null
) {
Rect bounds =
new
Rect(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + getMeasuredWidth() + getPaddingRight(), getPaddingTop() + getMeasuredHeight() + getPaddingBottom());
TouchDelegate delegate =
new
TouchDelegate(bounds, cbx);
setTouchDelegate(delegate);
}
}
}
|
这个类可以当成FrameLayout,我们可以把CheckBox放里面,然后CheckBox的点击区域就是整个FrameLayout的区域啦.当然这个类也适用于RadioButton,但是你不能放多个CompoundButton在里面。
Android性能优化之谈谈SparseArray,SparseBooleanArray和SparseIntArray
相关内容转自http://www.lxway.com/42920216.htm
相信大家都明白,手机软件的开发不同于PC软件的开发,因为手机性能相对有限,内存也有限,所谓“寸土寸金”,可能稍有不慎,就会导致性能的明显降低。Android为了方便开发者,特意在android.util这个包中提供了几个提高效率的工具类,比如之前用过的LruCache类,这次我们来谈谈其他工具类,SparseArray,SparseBooleanArray和 SparseIntArray。
总体说,它们都是类似map这样key-value的存储方式,但是由于查找的算法不一样。因此效率也各不同。但要明白,没有说哪个一定是最好的。只有根据不同需求在不同场景去应用,才能获取较优的结果。
package android.util; import com.android.internal.util.ArrayUtils; / * SparseArrays 利用integer去管理object对象。不像一个正常的object对象数组,它能在索引数中快速的查找到所需的结果。(这 * 句话是音译,原意是能在众多索引数中“撕开一个缺口”,为什么原文这么表达?下面会慢慢说清楚。)它比HashMap去通过Integer索引 * 查找object对象时在内存上更具效率,不仅因为它避免了用来查找的自动“装箱”的keys,并且它的数据结构不依赖额外的对象去 * 各个映射中查找匹配。 * * SparseArrays map integers to Objects. Unlike a normal array of Objects, * there can be gaps in the indices. It is intended to be more memory efficient * than using a HashMap to map Integers to Objects, both because it avoids * auto-boxing keys and its data structure doesn't rely on an extra entry object * for each mapping. * * 请注意,这个容器会保持它的映射关系在一个数组的数据结构中,通过二分检索法驱查找key。(这里我们终于知道,为何这个工具类中, * 提供的添加映射关系的操作中,key的类型必须是integer。因为二分检索法,将从中间“切开”,integer的数据类型是实现这种检索过程的保证。) * * 如果保存大量的数据,这种数据结构是不适合的,换言之,SparseArray这个工具类并不应该用于存储大量的数据。这种情况下,它的效率 * 通常比传统的HashMap更低,因为它的查找方法并且增加和移除操作(任意一个操作)都需要在数组中插入和删除(两个步骤才能实现)。 * * 如果存储的数据在几百个以内,它们的性能差异并不明显,低于50%。 * * (OK,那么光看Android官方的介绍我们就有初步结论了,大量的数据我们相对SparseArray会优先选择HashMap,如果数据在几百个这个数目, * 那么选择它们任意一个去实现区别不大,如果数量较少,就选择SparseArray去实现。 其实如果我们理解了二分法,就很容易了SparseArray的 * 实现原理,以及SparseArray和HashMap它们之间的区别了。) * *Note that this container keeps its mappings in an array data structure, * using a binary search to find keys. The implementation is not intended to be appropriate for * data structures * that may contain large numbers of items. It is generally slower than a traditional * HashMap, since lookups require a binary search and adds and removes require inserting * and deleting entries in the array. For containers holding up to hundreds of items, * the performance difference is not significant, less than 50%.
* * * 为了提高性能,这个容器包含了一个实现最优的方法:当移除keys后为了立刻使它的数组紧密,它会“遗留”已经被移除(标记了要删除)的条目(entry) 。 * 所被标记的条目(entry)(还未被当作垃圾回收掉前)可以被相同的key复用,也会在垃圾回收机制当作所有要回收的条目的一员被回收,从而使存储的数组更紧密。 * * (我们下面看源码就会发现remove()方法其实是调用delete()方法的。印证了上面这句话所说的这种优化方法。 * 因为这样,能在每次移除元素后一直保持数组的数据结构是紧密不松散的。) * * 垃圾回收的机制会在这些情况执行:数组需要扩充,或者映射表的大小被恢复,或者条目值被重新检索后恢复的时候。 * *To help with performance, the container includes an optimization when removing * keys: instead of compacting its array immediately, it leaves the removed entry marked * as deleted. The entry can then be re-used for the same key, or compacted later in * a single garbage collection step of all removed entries. This garbage collection will * need to be performed at any time the array needs to be grown or the the map size or * entry values are retrieved.
* * 当调用keyAt(int)去获取某个位置的key的键的值,或者调用valueAt(int)去获取某个位置的值时,可能是通过迭代容器中的元素 * 去实现的。 * *It is possible to iterate over the items in this container using * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using *
*/ public class SparseArraykeyAt(int)
with ascending values of the index will return the * keys in ascending order, or the values corresponding to the keys in ascending * order in the case ofvalueAt(int)
.
implements Cloneable { //... }
1,SparseArray的原理是二分检索法,也因此key的类型都是整型。
2,(HashMap和SparseArray比较)当存储大量数据(起码上千个)的时候,优先选择HashMap。如果只有几百个,用哪个区别不大。如果数量不多,优先选择SparseArray。
3,SparseArray有自己的垃圾回收机制。(当数量不是很多的时候,这个不必关心。)
接着将里面的主要方法列出来:
private int index = 1; private String value = "value"; public void testSparseArray() { //创建一个SparseArray对象 SparseArraysparseArray = new SparseArray (); //向sparseArray存入元素value,key为index sparseArray.put(index, value); //这个方法本质也是利用put(key, value)去存入数据 sparseArray.append(index, value); sparseArray.indexOfKey(index); //查找value所在的位置,如果不存在,则返回-1 sparseArray.indexOfValue(value); //更新某个key的值 sparseArray.setValueAt(index, value); //获取index所对应的值,没有则返回null sparseArray.get(index); //获取index所对应的值,没有则返回自定义的默认值"default-value" sparseArray.get(index,"default-value"); //删除index对应的元素 sparseArray.delete(index); //移除,本质也是调用delete(int)方法 sparseArray.remove(index); //清空所有数据 sparseArray.clear(); }
SparseBooleanArray和SparseIntArray,其实看名字也知道,它们跟SparseArray极其类似,只是存储类型加以限制了。SparseBooleanArray只能存储boolean值,而SparseIntArray只能存储integer类型的值。它们也同样实现了Cloneable接口,可以直接调用clone方法,也同样是以二分法为依据。而其他的主要方法也是一样的。下面以SparseBooleanArray为简单例子写出主要的方法,从方法看出,两者和SparseArray的确是灰常类似的。SparseIntArray的代码就不再贴出来了,因为都一样的。我们在使用的过程中举一反三,会用一个,其他2个也就会用了呢。
public void testSparseBooleanArray() { // SparseBooleanArray sparseBooleanArray = new SparseBooleanArray(); //这种创建方式可以设置容器的大小 SparseBooleanArray sparseBooleanArray = new SparseBooleanArray(5); //存入数据,同样有两种方法 sparseBooleanArray.put(int, boolean); sparseBooleanArray.append(int, boolean); //根据key获取对应的boolean值,没有则返回false sparseBooleanArray.get(key); //跟上面类似,valueIfKeyNotFound是自定义的假设不存在则返回的默认值 sparseBooleanArray.get(key, valueIfKeyNotFound); //获取第5个位置的键值 sparseBooleanArray.keyAt(5); //获取第5个元素的值 sparseBooleanArray.valueAt(5); //删除某个key的元素 sparseBooleanArray.delete(key); //清除所有 sparseBooleanArray.clear(); }