Android之ListView的getItemViewType和getViewTypeCount

mpuz8057 7年前
   <p>PS:感觉这两个方法其实还是很容易理解的,也算是给我其他两个朋友写的吧,帮他们搞清楚这两个方法的用法和概念。同时还有一些小细节问题需要注意。</p>    <p>学习内容:</p>    <p><strong>1.getItemViewType和getViewTypeCount</strong></p>    <p>getItemViewType和getViewTypeCount是ListView中实现复杂列表的两个相关的方法,普通的ListView中Item是相同的,那么我们只需要实现Adapter中四个抽象方法即可,但是如果页面中Item长得比较的复杂呢?比如说这个。</p>    <p style="text-align:center"><strong><img src="https://simg.open-open.com/show/23b5a75c46ddd76b35069bd4ceb53297.png"> </strong></p>    <p>比如说这个列表项,其实也不是很复杂,这种类型的Item也有其他的实现方式,比如说在Adapter中实现SectionIndexer也是可以实现的,但是我们就拿这个来说明一下问题,如果一个Item第一种类型是TextView,第二种类型是ImageView+Button+TextView呢,那么这样复杂的列表我们就需要使用getItemViewType()和getTypeViewCount()两个方法去实现了。这两个方法理解起来还是比较容易的,获取Item中Type的类型以及Item中Type的相关数量。废话就不多说了,直接说实现方式。</p>    <pre>  <code class="language-java">public class ListAdapter extends BaseAdapter {        /**       * Item类型,int值.必须从0开始依次递增.       * */      private static final int TYPE_TITLE = 0;      private static final int TYPE_CONTENT = 1;        /**       * Item Type 的数量       * */      private static final int TYPE_ITEM_COUNT = 2;        /**       * 数据       * */      private List<Company> mData = new ArrayList<>();      private Context context;            public ListAdapter(Context context,List<Company>mData){          this.context = context;          this.mData = mData;      }          @Override      public int getCount() {          return mData.size();      }        @Override      public Object getItem(int position) {          return mData.get(position);      }        @Override      public long getItemId(int position) {          return position;      }        @Override      public View getView(int position, View convertView, ViewGroup viewGroup) {            /**           * 不同类型的ViewHolder           * */          TitleViewHolder titleViewHolder = null;          CompanyViewHolder contentViewHolder = null;          /**           * 对类型进行判断,分别inflate不同的布局.           * */          switch (getItemViewType(position)){              case TYPE_TITLE:                  titleViewHolder = new TitleViewHolder();                  if(convertView == null){                      convertView = View.inflate(context, R.layout.view_holder_company_index,null);                      titleViewHolder.title = (TextView) convertView.findViewById(R.id.tv_title);                      //setTag()                      convertView.setTag(titleViewHolder);                  }else{                      //getTag();                      titleViewHolder = (TitleViewHolder) convertView.getTag();                  }                  titleViewHolder.title.setText(mData.get(position).getName());                  break;              case TYPE_CONTENT:                  contentViewHolder = new CompanyViewHolder();                  if(convertView == null){                      convertView = View.inflate(context,R.layout.view_holder_company,null);                      contentViewHolder.content = (TextView) convertView.findViewById(R.id.tv_content);                      convertView.setTag(contentViewHolder);                  }else{                      contentViewHolder = (CompanyViewHolder) convertView.getTag();                  }                  contentViewHolder.content.setText(mData.get(position).getCode());                  break;          }          return convertView;      }        /**       * 根据position获取Item的类型       * */      @Override      public int getItemViewType(int position) {          if(TextUtils.isEmpty(mData.get(position).getCode())){              return TYPE_TITLE;          }else{              return TYPE_CONTENT;          }      }        /**       * 返回Item Type的总数量       * */      @Override      public int getViewTypeCount() {          return TYPE_ITEM_COUNT;      }        static class TitleViewHolder{          TextView title;      }        static class CompanyViewHolder{          TextView content;      }  }  </code></pre>    <ul>     <li>首先我们需要为不同的Item设置不同的数值,int值,因为getItemViewType返回的是int值,所以需定义成int,必须从0开始,依次递增。原因我后续会做出解释。</li>     <li>重写getItemViewType和getViewTypeCount方法,getViewTypeCount返回Item的类型总数,getViewTypeCount则需要进行判断,判断方式一般都是通过JavaBean中的相关字段来判断的,因此这块不需要过于纠结。只需要根据position获取Item的具体类型进行判断然后就返回就可以了。</li>     <li>定义ViewHolder,根据类型的不同需要定义多个ViewHolder,减少findViewById()的次数。</li>     <li> <p>重写getView()中的相关方法,在getView中首先根据position获取Item的类型去加载不用的布局,这里同时会setViewType为不同类型的Item设置RecycleBin,解决ListView由于多个类型Item的复用问题。不清楚RecycleBin机制的读者可以去看下ListView的复用机制这里说到了RecycleBin,如果不懂这个机制是看不明白下面的解释的。</p> </li>     <li>最后根据传递过来的数据setAdapter然后为Item进行赋值就完成了。</li>    </ul>    <p>大体的一个思路就是这样实现的,这里需要说一下为什么定义Item的类型的时候必须要从0开始,依次递增,那么原因是什么呢?如果我们有三种类型,我们将Item定义成1,2,4,那么势必会出现ArrayIndexOutOfBoundsException,也就是所谓的数组越界,我上网查了很多资料都说会出现异常,并且Google也确实标明了,Note: Integers must be in the range 0 to  getViewTypeCount() - 1.  IGNORE_ITEM_VIEW_TYPE can also be returned.但是看到这里就没有后续了,没人会去说明这个问题是怎样发生的,为什么要这样去定义。可能我就是闲的蛋疼的那种人,不弄明白确实感到不舒服。我在解释一下具体的原因:</p>    <p>其实发生这种情况一般都是我们在下拉的时候出现的问题,在第一次加载第一页的时候是不会直接出现崩溃现象的,那么心细的读者可能会明白这有可能是ListView在复用时出现的问题,其实却是就是ListView复用机制导致的。我们来看一下这个方法:</p>    <pre>  <code class="language-java">/**   *ListView在针对不同Item复用时会调用这个方法   *为每一种不同的Item设置一个RecycleBin,用于复用.   */  public void setViewTypeCount(int viewTypeCount) {           if (viewTypeCount < 1) {          throw new IllegalArgumentException("Can't have a viewTypeCount < 1");      }      // noinspection unchecked        /**       * 根据viewTypeCount的数量设置一个ArrayList.       * 同时为每一个Item再设置一个ArrayList,用来存储ScrapView.       * 相当于一个二维数组来维护每一个Item的ScrapView数组.       * 这里也就相当于为不同的Item设置单独的RecycleBin.       */      ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];      for (int i = 0; i < viewTypeCount; i++) {         scrapViews[i] = new ArrayList<View>();      }      //保存ViewTypeCount,也就 = 2      mViewTypeCount = viewTypeCount;      //当前的Scrap是第一个Item的ScrapView数组.      mCurrentScrap = scrapViews[0];      //mScrapViews就保存了一个二维数组维护的RecycleBin.      mScrapViews = scrapViews;  }  </code></pre>    <p><img src="https://simg.open-open.com/show/dc5adc93b710fd0e6176d6f86af2264d.png"></p>    <p>这是具体的数据结构,简单理解就是每一个Item都有对应的ScrapView数组。这里其实并不是出问题的地方,我们都知道Item一旦被移出了屏幕,首先会Detach掉,然后被加入到mScrapView数组中(废弃View池),那么在addScrapView的时候就会出现异常.</p>    <pre>  <code class="language-java">  void addScrapView(View scrap) {            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();            if (lp == null) {                return;            }            // Don't put header or footer views or views that should be ignored            // into the scrap heap            int viewType = lp.viewType;            if (!shouldRecycleViewType(viewType)) {                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {                    removeDetachedView(scrap, false);                }                return;            }            /**           *核心代码就是这块,由于我们mViewTypeCount != 1 的,因此if条件不成立.           *因此会执行else代码.           */          if (mViewTypeCount == 1) {                dispatchFinishTemporaryDetach(scrap);                mCurrentScrap.add(scrap);            } else {                dispatchFinishTemporaryDetach(scrap);                mScrapViews[viewType].add(scrap);            }                if (mRecyclerListener != null) {                mRecyclerListener.onMovedToScrapHeap(scrap);            }        }    </code></pre>    <p>问题就在于这个else代码当中,我们可以看到mScrapViews[viewType].add(scrap)代码执行了,我们前面图片上显示的,mScrapViews[]是很据Item的种类数量new出来的,由于我们Item总数是两种类型,那么mScrapViews[].length = 2,但是这里是mScrapView[viewtype],viewtype是什么,其实就是我们getItemViewType的返回值,如果我们将类型定义成2和3,那么他会访问mScrapView[2]和mScrapView[3],可想而知,一定会出现ArrayIndexOutOffBoundsException.这就是数组越界的真正原因。</p>    <p> </p>    <p>来自:http://www.cnblogs.com/RGogoing/p/5872217.html</p>    <p> </p>