项目中,有这样的一个需求:
有三种打印机类型,每种类型可以添加、删除对应类型的打印机。
按照以往,我写的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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220public class PrinterManagerAdapter extends BaseAdapter {
public final static int BLUETOOTH_HEADER = 0;
public final static int BLUETOOTH_PRINTER = 1;
public final static int NET_HEADER = 2;
public final static int NET_PRINTER = 3;
public final static int CLOUD_HEADER = 4;
public final static int CLOUD_PRINTER = 5;
private Context mContext;
private LayoutInflater mInflater;
private OnItemFunctionClickListener listener;
private List<PrinterInfo> mPrinters = new ArrayList<>();
private int blueToothCount = 0;
private int netCount = 0;
private int cloudCount = 0;
public PrinterManagerAdapter(Context context, List<PrinterInfo> printers, SwipePartMenuListView listView) {
mContext = context;
mPrinters = printers;
mInflater = LayoutInflater.from(context);
getNotSwipeItem(listView);
}
private void getNotSwipeItem(SwipePartMenuListView listView) {
blueToothCount = 0;
netCount = 0;
cloudCount = 0;
for (PrinterInfo info : mPrinters) {
if (info.printerType == PrinterInfo.TYPE_BLUETOOTH) {
blueToothCount++;
} else if (info.printerType == PrinterInfo.TYPE_NETWORK) {
netCount++;
} else if (info.printerType == PrinterInfo.TYPE_CLOUD) {
cloudCount++;
}
}
List<Integer> titleList = new LinkedList<>();
titleList.add(0);
titleList.add(1 + blueToothCount);
titleList.add(2 + blueToothCount + netCount);
listView.setCannotSwipePositionList(titleList);
}
public void setPrinters(List<PrinterInfo> printers, SwipePartMenuListView listView) {
this.mPrinters = printers;
getNotSwipeItem(listView);
}
public void setListener(OnItemFunctionClickListener listener) {
this.listener = listener;
}
public View getView(int position, View convertView, ViewGroup parent) {
PrinterManagerViewHolder holder;
final int viewType = getItemViewType(position);
holder = new PrinterManagerViewHolder();
if (viewType == BLUETOOTH_HEADER || viewType == NET_HEADER || viewType == CLOUD_HEADER) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item_printer_title, parent, false);
holder.headerTitle = (TextView) convertView.findViewById(R.id.printer_category_tv);
holder.headerIcon = (ImageView) convertView.findViewById(R.id.printer_icon_iv);
holder.headerAdd = (ImageView) convertView.findViewById(R.id.printer_add_device_iv);
holder.headerDivider = convertView.findViewById(R.id.printer_title_divider);
convertView.setTag(holder);
} else {
holder = (PrinterManagerViewHolder) convertView.getTag();
}
} else {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item_printer_devices, parent, false);
holder.connect = (TextView) convertView.findViewById(R.id.printer_connect_tv);
holder.divider = convertView.findViewById(R.id.printer_last_divider);
holder.setting = (TextView) convertView.findViewById(R.id.printer_setting_tv);
holder.title = (TextView) convertView.findViewById(R.id.printer_title_tv);
holder.connectIcon = (ImageView) convertView.findViewById(R.id.printer_connect_iv);
convertView.setTag(holder);
} else {
holder = (PrinterManagerViewHolder) convertView.getTag();
}
}
switch (viewType) {
case BLUETOOTH_HEADER:
holder.headerTitle.setText("蓝牙打印机");
holder.headerAdd.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.btn_setting_goods_add));
holder.headerAdd.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
BlueToothPrinterActivity.navigateTo(mContext);
}
});
break;
case NET_HEADER:
holder.headerTitle.setText("网络打印机");
holder.headerAdd.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.btn_setting_goods_add));
holder.headerAdd.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
NetPrinterActivity.navigateTo(mContext);
}
});
break;
case CLOUD_HEADER:
holder.headerTitle.setText("云打印机");
holder.headerAdd.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.btn_setting_goods_scan));
if (position == getCount() - 1) {
holder.headerDivider.setVisibility(View.VISIBLE);
}
break;
case BLUETOOTH_PRINTER:
PrinterInfo printer = mPrinters.get(position - 1);
initPrinter(holder, printer);
break;
case NET_PRINTER:
PrinterInfo netPrinter = mPrinters.get(position - 2);
initPrinter(holder, netPrinter);
break;
case CLOUD_PRINTER:
if (position == getCount() - 1) {
holder.divider.setVisibility(View.VISIBLE);
}
PrinterInfo cloudPrinter = mPrinters.get(position - 3);
initPrinter(holder, cloudPrinter);
break;
}
return convertView;
}
private void initPrinter(PrinterManagerViewHolder holder, final PrinterInfo printerInfo) {
boolean connected = false;
holder.title.setText(printerInfo.name);
HashMap<String, IPrinter> printers = PrinterManager.getInstance().getPrinterList();
if (printers.keySet().contains(printerInfo.mac)) {
connected = true;
holder.connect.setText("断开");
holder.connectIcon.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.ic_connect));
} else {
holder.connect.setText("连接");
holder.connectIcon.setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.ic_break));
}
holder.setting.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
PrinterSettingActivity.navigateTo(mContext, printerInfo);
}
});
final boolean finalConnected = connected;
holder.connect.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (listener != null) {
listener.onConnectClicked(printerInfo, finalConnected);
}
}
});
}
public int getItemViewType(int position) {
if (position == 0) {
return BLUETOOTH_HEADER;
} else if (position > 0 && position <= blueToothCount) {
return BLUETOOTH_PRINTER;
} else if (position == blueToothCount + 1) {
return NET_HEADER;
} else if (position > blueToothCount + 1 && position <= netCount + blueToothCount + 1) {
return NET_PRINTER;
} else if (position == netCount + blueToothCount + 2) {
return CLOUD_HEADER;
} else {
return CLOUD_PRINTER;
}
}
public int getCount() {
if (mPrinters != null) {
return mPrinters.size() + 3;
}
return 0;
}
public Object getItem(int position) {
return null;
}
public long getItemId(int position) {
return position;
}
private class PrinterManagerViewHolder {
// Header
private TextView headerTitle;
private ImageView headerIcon;
private ImageView headerAdd;
private View headerDivider;
// Printer
private TextView connect;
private TextView title;
private View divider;
private TextView setting;
private ImageView connectIcon;
}
public interface OnItemFunctionClickListener {
void onConnectClicked(PrinterInfo printer, boolean connected);
}
}
我将 view 分为了6个 Type ,三种头部 Type 使用一种布局,三种打印机 Type 使用一种布局,然后总共用了一个 ViewHoloder 。
然后发现一个问题:
当我删除一个打印机之后,刷新界面的时候崩溃了
问题其实很简单:就是删除的打印机(BLUETOOTH_PRINTER Type)convertView 进入到缓存里面,然后下个 Item 的 Type 是 NET_HEADER Type,由于重用机制,这个 Item 会重用 convertView,此时这个 convertView 绑定的 ViewHoloder 是 Printer 部分,而自己要使用的是 Header 部分,其 view 都为 null了,导致空指针崩溃。
解决办法:添加代码1
2
3
4
public int getViewTypeCount() {
return 6;
}
ListView 的缓存机制是可以针对不同 Type 来进行缓存的,当不复写这个方法的时候,其默认的实现是 返回1 ,所以导致getItemViewType
返回的 Type 实际上是没有用的,不管是什么 Type, ListView 填充的 convertView 永远是一样的。所以,当改成 返回6 的时候, ListView 便会填充 6 种 convertView 了,所绑定的 ViewHoloder 具有的属性也会一样,就避免了空指针崩溃了。
当和同事讨论这点的时候,同事指出: 有几种布局,就用几种 Type,几种 ViewHoloder,一一对应才是官方推荐的行为。
自己想了下,确实是的。当网络打印机这个 Item 要显示的时候,如果缓存中有蓝牙打印机的 convertView,我是用不了的,因为他们的 Type 不一样。这样的一个做法,就是自己把 ListView 的缓存机制整乱了。
修改后的代码如下: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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253public class PrinterManagerAdapter extends BaseAdapter {
private final static int TYPE_HEADER = 0;
private final static int TYPE_PRINTER = 1;
private Context mContext;
private LayoutInflater mInflater;
private OnItemFunctionClickListener listener;
private ArrayList<PrinterInfo> mPrinters;
private int blueToothCount = 0;
private int netCount = 0;
private int cloudCount = 0;
public PrinterManagerAdapter(Context context, ArrayList<PrinterInfo> printers, SwipePartMenuListView listView) {
mContext = context;
mPrinters = printers;
mInflater = LayoutInflater.from(context);
setNotSwipeItems(listView);
}
private void setNotSwipeItems(SwipePartMenuListView listView) {
blueToothCount = 0;
netCount = 0;
cloudCount = 0;
for (PrinterInfo info : mPrinters) {
if (info.printerType == PrinterInfo.TYPE_BLUETOOTH) {
blueToothCount++;
} else if (info.printerType == PrinterInfo.TYPE_NETWORK) {
netCount++;
} else if (info.printerType == PrinterInfo.TYPE_CLOUD) {
cloudCount++;
}
}
List<Integer> titleList = new ArrayList<>();
titleList.add(0);
titleList.add(1 + blueToothCount);
titleList.add(2 + blueToothCount + netCount);
listView.setCannotSwipePositionList(titleList);
}
public void setPrinters(ArrayList<PrinterInfo> printers, SwipePartMenuListView listView) {
this.mPrinters = printers;
setNotSwipeItems(listView);
}
public void setListener(OnItemFunctionClickListener listener) {
this.listener = listener;
}
public View getView(int position, View convertView, ViewGroup parent) {
HeaderViewHolder headerHolder = null;
PrinterViewHolder printerHolder = null;
int viewType = getItemViewType(position);
if (viewType == TYPE_HEADER) {
if (convertView == null) {
headerHolder = new HeaderViewHolder();
convertView = mInflater.inflate(R.layout.item_printer_title, parent, false);
headerHolder.headerTitle = (TextView) convertView.findViewById(R.id.printer_category_tv);
headerHolder.headerIcon = (ImageView) convertView.findViewById(R.id.printer_icon_iv);
headerHolder.headerAdd = (ImageView) convertView.findViewById(R.id.printer_add_device_iv);
headerHolder.headerDivider = convertView.findViewById(R.id.printer_title_divider);
convertView.setTag(headerHolder);
} else {
headerHolder = (HeaderViewHolder) convertView.getTag();
}
} else {
if (convertView == null) {
printerHolder = new PrinterViewHolder();
convertView = mInflater.inflate(R.layout.item_printer_devices, parent, false);
printerHolder.connect = (TextView) convertView.findViewById(R.id.printer_connect_tv);
printerHolder.divider = convertView.findViewById(R.id.printer_last_divider);
printerHolder.setting = (TextView) convertView.findViewById(R.id.printer_setting_tv);
printerHolder.title = (TextView) convertView.findViewById(R.id.printer_title_tv);
printerHolder.connectIcon = (ImageView) convertView.findViewById(R.id.printer_connect_iv);
convertView.setTag(printerHolder);
} else {
printerHolder = (PrinterViewHolder) convertView.getTag();
}
}
switch (viewType) {
case TYPE_HEADER:
if (headerHolder != null) {
if (position == 0) {
headerHolder.headerTitle.setText("蓝牙打印机");
headerHolder.headerIcon.setImageResource(R.drawable.ic_printer_bluetooth);
headerHolder.headerAdd.setImageResource(R.drawable.btn_setting_goods_add);
headerHolder.headerAdd.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
BlueToothPrinterActivity.navigateTo(mContext, mPrinters);
}
});
} else if (position == blueToothCount + 1) {
headerHolder.headerTitle.setText("网络打印机");
headerHolder.headerIcon.setImageResource(R.drawable.ic_printer_wifi);
headerHolder.headerAdd.setImageResource(R.drawable.btn_setting_goods_add);
headerHolder.headerAdd.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// NetPrinterActivity.navigateTo(mContext);
}
});
} else if (position == netCount + blueToothCount + 2) {
headerHolder.headerTitle.setText("云打印机");
headerHolder.headerIcon.setImageResource(R.drawable.ic_printer_cloud);
headerHolder.headerAdd.setImageResource(R.drawable.btn_setting_goods_scan);
headerHolder.headerAdd.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// NetPrinterActivity.navigateTo(mContext);
}
});
if (position == getCount() - 1) {
headerHolder.headerDivider.setVisibility(View.VISIBLE);
} else {
headerHolder.headerDivider.setVisibility(View.GONE);
}
}
}
break;
case TYPE_PRINTER:
if (printerHolder != null) {
int index;
if (position > 0 && position <= blueToothCount) {
index = position - 1;
} else if (position > blueToothCount + 1 && position <= netCount + blueToothCount + 1) {
index = position - 2;
} else {
index = position - 3;
if (position == getCount() - 1) {
printerHolder.divider.setVisibility(View.VISIBLE);
} else {
printerHolder.divider.setVisibility(View.GONE);
}
}
PrinterInfo printer = mPrinters.get(index);
initPrinter(printerHolder, printer);
}
break;
default:
break;
}
return convertView;
}
private void initPrinter(PrinterViewHolder holder, final PrinterInfo printerInfo) {
if (holder != null) {
boolean connected = false;
holder.title.setText(printerInfo.name);
HashMap<String, IPrinter> printers = PrinterManager.getInstance().getPrinterList();
if (printers.keySet().contains(printerInfo.mac)) {
connected = true;
holder.connect.setText("断开");
holder.connectIcon.setImageResource(R.drawable.ic_connect);
} else {
holder.connect.setText("连接");
holder.connectIcon.setImageResource(R.drawable.ic_break);
}
holder.setting.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
PrinterSettingActivity.navigateTo(mContext, printerInfo);
}
});
final boolean finalConnected = connected;
holder.connect.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (listener != null) {
listener.onConnectClicked(printerInfo, finalConnected);
}
}
});
}
}
public PrinterInfo getPrinterInfo(int position) {
int index;
if (position > 0 && position <= blueToothCount) {
index = position - 1;
} else if (position > blueToothCount + 1 && position <= netCount + blueToothCount + 1) {
index = position - 2;
} else {
index = position - 3;
}
return mPrinters.get(index);
}
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
} else if (position > 0 && position <= blueToothCount) {
return TYPE_PRINTER;
} else if (position == blueToothCount + 1) {
return TYPE_HEADER;
} else if (position > blueToothCount + 1 && position <= netCount + blueToothCount + 1) {
return TYPE_PRINTER;
} else if (position == netCount + blueToothCount + 2) {
return TYPE_HEADER;
} else {
return TYPE_PRINTER;
}
}
public int getViewTypeCount() {
return 2;
}
public int getCount() {
if (mPrinters != null) {
return mPrinters.size() + 3;
}
return 3;
}
}
public long getItemId(int position) {
return position;
}
private class HeaderViewHolder {
private TextView headerTitle;
private ImageView headerIcon;
private ImageView headerAdd;
private View headerDivider;
}
private class PrinterViewHolder {
private TextView connect;
private TextView title;
private View divider;
private TextView setting;
private ImageView connectIcon;
}
public interface OnItemFunctionClickListener {
void onConnectClicked(PrinterInfo printer, boolean connected);
}
}
这样的话,结构其实会更加清晰,拆分得更具体。
另外,注意一点: 代码中的 Type 类型 TYPE_HEADER 是从0开始的。这是因为不从 0 开始当 Adapter notifyDataSetChanged 时就会报错。举个栗子:1
2private final static int TYPE_HEADER = 5;
private final static int TYPE_PRINTER = 6;
报错信息: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
51FATAL EXCEPTION: main
Process: com.maimairen.app.jinchuhuo.dev, PID: 15967
java.lang.ArrayIndexOutOfBoundsException: length=2; index=5
at android.widget.AbsListView$RecycleBin.addScrapView(AbsListView.java:6726)
at android.widget.ListView.layoutChildren(ListView.java:1644)
at android.widget.AbsListView.onLayout(AbsListView.java:2148)
at android.view.View.layout(View.java:16653)
at android.view.ViewGroup.layout(ViewGroup.java:5438)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1743)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1586)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1495)
at android.view.View.layout(View.java:16653)
at android.view.ViewGroup.layout(ViewGroup.java:5438)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336)
at android.widget.FrameLayout.onLayout(FrameLayout.java:273)
at android.view.View.layout(View.java:16653)
at android.view.ViewGroup.layout(ViewGroup.java:5438)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1743)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1586)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1495)
at android.view.View.layout(View.java:16653)
at android.view.ViewGroup.layout(ViewGroup.java:5438)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336)
at android.widget.FrameLayout.onLayout(FrameLayout.java:273)
at android.view.View.layout(View.java:16653)
at android.view.ViewGroup.layout(ViewGroup.java:5438)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1743)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1586)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1495)
at android.view.View.layout(View.java:16653)
at android.view.ViewGroup.layout(ViewGroup.java:5438)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:336)
at android.widget.FrameLayout.onLayout(FrameLayout.java:273)
at com.android.internal.policy.PhoneWindow$DecorView.onLayout(PhoneWindow.java:2678)
at android.view.View.layout(View.java:16653)
at android.view.ViewGroup.layout(ViewGroup.java:5438)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2198)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1958)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1134)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6050)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:860)
at android.view.Choreographer.doCallbacks(Choreographer.java:672)
at android.view.Choreographer.doFrame(Choreographer.java:608)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:846)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5438)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:739)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)
可以看到,ListView 从缓存中去取 view 的时候,是用的 Type 的值来作为 index 的,所以 Type 类型一定是从0开始的。
因为自己长期以来一直是之前的那种做法,错了太多次了,却没有及时发现错误,经过这次同事的指正,总算是纠正过来了。写篇博客备忘,忘性太大了~