使用ListView多Type的错误姿势

◇◆丶佛笑我妖孽 提交于 2019-12-01 21:58:00

项目中,有这样的一个需求:

有三种打印机类型,每种类型可以添加、删除对应类型的打印机。
按照以往,我写的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开始的
因为自己长期以来一直是之前的那种做法,错了太多次了,却没有及时发现错误,经过这次同事的指正,总算是纠正过来了。写篇博客备忘,忘性太大了~

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!