使用ListView多Type的错误姿势 Posted 2021-03-25 wangziqiang123
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用ListView多Type的错误姿势相关的知识,希望对你有一定的参考价值。
项目中,有这样的一个需求:
有三种打印机类型,每种类型可以添加、删除对应类型的打印机。 按照以往,我写的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 220 public class 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 (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); } } }); } @Override 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; } } @Override public int getCount () { if (mPrinters != null ) { return mPrinters.size() + 3 ; } return 0 ; } @Override public Object getItem (int position) { return null ; } @Override public long getItemId (int position) { return position; } private class PrinterManagerViewHolder { private TextView headerTitle; private ImageView headerIcon; private ImageView headerAdd; private View headerDivider; 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 @Override 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 大专栏 使用ListView多Type的错误姿势 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 253 public class 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 (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; } @Override 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() { @Override 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() { @Override public void onClick (View v) { } }); } 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() { @Override public void onClick (View v) { } }); 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() { @Override public void onClick (View v) { PrinterSettingActivity.navigateTo(mContext, printerInfo); } }); final boolean finalConnected = connected; holder.connect.setOnClickListener(new View.OnClickListener() { @Override 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); } @Override 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; } } @Override public int getViewTypeCount () { return 2 ; } @Override public int getCount () { if (mPrinters != null ) { return mPrinters.size() + 3 ; } return 3 ; } } @Override 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 2 private 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 51 FATAL EXCEPTION: main Process: com.maimairen.app.jinchuhuo.dev, PID: 15967 java.lang.ArrayIndexOutOfBoundsException: length=2 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开始的 。 因为自己长期以来一直是之前的那种做法,错了太多次了,却没有及时发现错误,经过这次同事的指正,总算是纠正过来了。写篇博客备忘,忘性太大了~
以上是关于使用ListView多Type的错误姿势的主要内容,如果未能解决你的问题,请参考以下文章
片段活动错误中的ListView Adapter上下文,我该怎么办?
片段活动中的 ListView 适配器上下文错误,我该怎么办?
如何使用 viewpager 和片段为我的 ListView 设置适配器
从mysql的片段中加载ListView
片段中ListView的android自定义适配器
Listview 项目未在 Fragment Android 中显示