项目中,有这样的一个需求:
有三种打印机类型,每种类型可以添加、删除对应类型的打印机。
按照以往,我写的Adapter是这样的:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
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; // 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了,导致空指针崩溃。
解决办法:添加代码
1234 |
@Overridepublic int getViewTypeCount() { return 6;} |
ListView 的缓存机制是可以针对不同 Type 来进行缓存的,当不复写这个方法的时候,其默认的实现是 返回1 ,所以导致getItemViewType
返回的 Type 实际上是没有用的,不管是什么 Type, ListView 填充的 convertView 永远是一样的。所以,当改成 返回6 的时候, ListView 便会填充 6 种 convertView 了,所绑定的 ViewHoloder 具有的属性也会一样,就避免了空指针崩溃了。
当和同事讨论这点的时候,同事指出: 有几种布局,就用几种 Type,几种 ViewHoloder,一一对应才是官方推荐的行为。
自己想了下,确实是的。当网络打印机这个 Item 要显示的时候,如果缓存中有蓝牙打印机的 convertView,我是用不了的,因为他们的 Type 不一样。这样的一个做法,就是自己把 ListView 的缓存机制整乱了。
修改后的代码如下:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 大专栏 使用ListView多Type的错误姿势214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
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) {// 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() { @Override 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() { @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 时就会报错。举个栗子:
12 |
private final static int TYPE_HEADER = 5;private final static int TYPE_PRINTER = 6; |
报错信息:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 |
FATAL EXCEPTION: mainProcess: com.maimairen.app.jinchuhuo.dev, PID: 15967java.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开始的。
因为自己长期以来一直是之前的那种做法,错了太多次了,却没有及时发现错误,经过这次同事的指正,总算是纠正过来了。写篇博客备忘,忘性太大了~
原文地址:https://www.cnblogs.com/wangziqiang123/p/11718109.html