再探ListView数据展示

本文探讨了ListView在Android中的常见问题,包括无数据时的显示效果,如何正确更新数据,以及ViewHolder的原理和优化。通过实例分析了ListView更新数据时的常见误区,并提供了正确的数据设置方法。同时,详细解释了ViewHolder如何帮助提高ListView的效率,以及其与ListView复用机制的关系。
摘要由CSDN通过智能技术生成

前记: 最近刚换了新工作,从天津来到了大帝都来工作 ,双十一的时候也入手了两本新书 《神兵利器》《Android群英传》 ,因此也可以正式开始我的Android进阶之路。前半个月一直在忙于适应新公司的工作,一些遇到的零碎的知识点都记录在自己的笔记中。在今后的日子里,自己会尽量把这些知识点整理记录下来。用来提高和巩固自己的Android知识。

  今天要写的是有关于ListView的一些知识点。ListView虽说用的很多了,但是对于有些知识点,我个人而言还是处于半懂不懂不懂得阶段,虽然它已经逐渐在RecyclerView取代,但是对于公司项目而言,ListView现在还处于举足轻重的地位。所以今天就对其常规用法做一个总结:文章大体包含下面几个方面:
  
1. ListView的数据更新
2. ListView的没有条目的时候显示效果
3. ViewHolder的原理是如何优化ListView的


ListView设置没有条目时的显示效果

问题引入: 我们在使用ListView展示来自服务器或者网络的数据的时候,经常会由于网络不好或者服务器宕(dang)机,ListVIew将没有数据可以展示这是我们一般会显示一张友好的图片提示,提示用户下拉刷新,或者检查网络。

我们一般的做法是可能是使用帧布局(FrameLayout)中覆盖到ListView上,然后通过一定的条件隐藏或者显示该图片,其实像这样的功能ListView本身就有:那就是 listView.setEmptyView(View view) ,具体使用方式如下:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>


    <ImageView
        android:id="@+id/image_empty"
        android:src="@mipmap/ic_launcher"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

 </RelativeLayout>

然后在代码将imageView 设置给listView

private ListView mListView;
private ImageView mImageView;

......

mListView = (ListView) findViewById(R.id.listView);
mImageView = (ImageView) findViewById(R.id.image);
mListView.setEmptyView(mImageView);

  上述代码当Listview没有数据的时候将会显示ImageView的图片,当有数据的时候ListView将会自动隐藏该ImageView。一个小小的方法我们并不常用,但是知道了以后会省去我们好多设置error图片的步骤

ListView到底该如何更新数据?

问题引入: 回想一下我们是怎么给ListView设置数据的? 大致需要这几步

  1. 定义BaseAdapter的子类,复写getView getCount getItem getItemId 这四个方法
  2. 定义一个构造方法,将需要加载数据的集合通过构造方法传递进来,并指向内部的集合
  3. 生成定义好的Adapter的对象,通过setAdapter方法设置给ListView

这样对于我们展示一个普通的不会变动的ListView当然没有问题。so 当我们是一个初学者的时候我们也认为,这是一个方程式,这样写结果就是对的。那么简单看下下面这段代码

class Adapter extends BaseAdapter {
        private LayoutInflater mInflater;
        private Context mContext;
        private ArrayList<String> mArrayList = null;

        public Adapter(Context context, ArrayList<String> arrayList) {
            mContext = context;
            mArrayList = arrayList;
            mInflater = LayoutInflater.from(mContext);
        }

        @Override
        public int getCount() {
            return mArrayList.size();
        }

        @Override
        public Object getItem(int position) {
            return mArrayList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.item, parent, false);
                holder = new ViewHolder();
                holder.mTextView = (TextView) convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.mTextView.setText(mArrayList.get(position));
            return convertView;
        }

        static class ViewHolder {
            TextView mTextView;
        }
    }

------
Activity中的代码:

    mDatas = new ArrayList<>();
    mAdapter = new Adapter(this, mDatas);
    mListView.setAdapter(mAdapter);

相信大家已经对上述代码足够熟悉了,如果我们此刻运行代码,listView将没有任何数据显示。因为我们通过构造参数传递给 Adapter 的 mDatas 是没有任何数据的。

此时我们假设我们通过网络请求从网络请求来了一条数据,然后我们要设置给ListView进行展示,我们通常的做法是:

     case R.id.btn_add:
                i++;
                mDatas.add("第" + i + "条目");//操作的是添加进去的ArrayList
                mAdapter.notifyDataSetChanged();
                break;

这里我通过一个按钮的点击事件进行模拟数据的添加。每点击一次为集合添加一条数据并通知Adapter刷新。我们发现这也没什么问题数据可以正确显示。ok,我们暂且记一下mDatas与Adapter中的mDatas是同一个集合。

java中通过构造方法传递的参数,是引用传递,而不是值传递。我们在生成Adapter的时候讲外部的mDatas = new ArrayList<>();传递给Adapter中的mArrayList如果现在还闹不清楚什么是引用传递和值传递,那么就理解成。mDatasmArrayList这两个引用,指向的都是内存中的new ArrayLIst<>();生成的对象。

下面我们将要模拟一下通常我们下拉刷新的操作:
1. 从网络获取一个封装了条目展示内容对象的信息字符串
2. 通过Json解析成对应的Entity对象,封装成集合
3. 将这个集合设置给之前的mDatas;

 case R.id.btn_add_new:
    initNewData();
    mDatas = mNewDatas;
    mAdapter.notifyDataSetChanged();
  break;
    ... ...

  private ArrayList<String> mNewDatas 
  private void initNewDatas() {
     mNewDatas = new ArrayList<>()
     mNewDatas.add("新集合1");
     mNewDatas.add("新集合1");
     mNewDatas.add("新集合1");
  }

如果我们通过上述的方式,试图为Adapter设置新的数据,那么你就大错特错了。这也就大家通常说的 notifyDataSetChanged() 方法不起作用了情况。大部分是处在下拉刷新的时候设置数据的情况。

解惑时间:

首先我们需要看到错误的情况和起初没有错误的情况的区别是什么,对!就是我们对mDatas赋值的操作。

正常情况:

mDatas.add("第" + i + "条目");

错误情况:

mDatas = mNewDatas;

如果我把这两句话单独列出来,然后问 这两个mDatas 还是同一个对象么? 相信大家都会异口同声的回答不是。 原因也很简单,表面解释如下:

mDatas 本身只是一个引用,而起初的 new ArraList<>()生成的对象和 mNewDatas已经不是同一个对象了。

往虚拟机深层解释就是:

mDatas为一个引用,位于内存中的栈内存,而new ArrayList(),和mNewDatas是对象,位于内存中的堆内存,一开始mDatas引用指向的是new ArrayList()的内存区域,这与Adapter中的mArrayList指向的是同一个对象。 而当我点击了下拉刷新之后,我们将mDatas指向了新的对象 mNewDatas,所以之后无论我们在怎么操作mDatas都跟Adapter没有关系了。

  • notifyDataSetChanged解惑

ok,解释到这种程度大家还可能有一个疑问就是notifyDataSetChanged是怎么工作的了,其实即使不了解这个方法的工作原理,通过上边的梳理,也能知道为什么数据不能正常刷新了。其实notifyDataSetChanged是ListView有名的设计方式之一 ——观察者模式。我们来简单看下它是怎么通知ListView更新数据的。

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
    private final DataSetObservable mDataSetObservable = new DataSetObservable();
   public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }
   public void notifyDataSetChanged() {
       mDataSetObservable.notifyChanged();
   }

我们可以看到当我们BaseAdapter初始化的时候都会初始化一个数据被观察者:mDataSetObservable。当被观察者数据发生改变时,通知观察者。这个观察者是我们在ListVIew中系统为我们注册的:

 mDataSetObserver = new AdapterDataSetObserver();
 mAdapter.registerDataSetObserver(mDataSetObserver);

当我们调用notifyDataSetChanged方法就是通知AdapterDataSetObserver,数据有所改变,要你更新数据。在这个方法中,又调用了DataSetObserveable的notifyChanged方法:

    /**
     * Invokes {@link DataSetObserver#onChanged} on each observer.
     * Called when the contents of the data set have changed.  The recipient
     * will obtain the new contents the next time it queries the data set.
     */
    public void notifyChanged() {
        synchronized(mObservers) {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

通过上述方法描述我们大概就可以了解,我们调用notifyDataSetChanged方法实质是通知每个Observer去调用onChange方法,当然之后的源代码就不深入研究了( - - ! ),但是我们应该清楚一点,不是我们每次调用notifyDataSetChanged所有数据条目都会重新进行赋值,而是有改变的数据才会重新赋值。

小结:通过上述的探索我们应该明白之后我们在使用ListVIew的时候如何为起设置新的数据

  1. 方法一:
 mDatas.clear();
 mDatas.addAll(mNewDatas);
 mAdapter.notifyDataSetChanged();
  1. 方法二:

Adapter中的添加方法:

 public void refresh(ArrayList<String> list) { 
        mList = list; 
        notifyDataSetChanged(); 
    } 

外部调用:

mAdapter.refresh(mNewDatas);

所以我们只需要保证我们一直操作的是Adapter中的mArrayList即可。


ViewHolder是如何帮助我们提高效率的

在2103年Google开发者大会上,Google提出了ListView的正确使用姿势,即使用convertView和ViewHolder来提高效率。

这里关于ListView的复用机制就不再赘述,网上有好多讲解很详细的文章,随便一搜就一大堆。现在看下我们是如何使用VIewHolder的:

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.item, parent, false);
                holder = new ViewHolder();
                holder.mTextView = (TextView) convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.mTextView.setText(mArrayList.get(position));
            return convertView;
        }

        static class ViewHolder {
            TextView mTextView;
        }

相信学过Android的人现在应该对这种写法都可以达到默写的程度了。我们现在的问题主要有

  1. 在ListView中展示数据的时候我们共生成了多少个ViewHolder对象?
  2. setTag方法究竟做了什么?

我认为闹清楚这两点对于我们清楚VIewHolder工作原理有很大作用。

问题1: 在ListView中展示数据的时候我们共生成了多少个ViewHolder对象?

我们可以通过一个集合,来看下在ListVIew使用过程中共建立的多少个ViewHolder

private ArrayList<ViewHolder> mViewHolders = new ArrayList<>();


@Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.item, parent, false);
                holder = new ViewHolder();
                mViewHolders.add(holder);//每创建一个对象都存入集合中

                holder.mTextView = (TextView) convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.mTextView.setText(mArrayList.get(position));

            Log.e("ViewHolder","ViewHolder的个数" + mViewHolders.size());//打印ViewHolder的个数
            return convertView;
        }

通过打印发现,ViewHolder最大的个数为7个,那么这个数字跟什么有关,其实是跟ListView什么时候开始复用convertView有关系。也就是等于一个屏幕能放下多少个条目+1的数量。

这里写图片描述

问题2: setTag方法究竟做了什么?

    /**
     * Sets the tag associated with this view. A tag can be used to mark
     * a view in its hierarchy and does not have to be unique within the
     * hierarchy. Tags can also be used to store data within a view without
     * resorting to another data structure.
     *
     * @param tag an Object to tag the view with
     *
     * @see #getTag()
     * @see #setTag(int, Object)
     */
    public void setTag(final Object tag) {
        mTag = tag;
    }

通过setTag方法的解释我们可以理解,View本身可以存储一个标志位,这个标志位可以是Object类型。其实就是当我们生成一个新的item view的时候我们将一个viewholder与该view进行绑定,当ListView开始服用convertView的时候我们通过getTag就可取出对应的View中的tag,从而获取之前findViewById的View,来进行新的数据设置。这一切都与convertView的复用机制相关联。如果你还不理解那么看下StrackOverFlow中的解释:

Android takes your convertView and puts it in its pool to recycle it and passes it again to you. but its pool may not have enough convertViews so it again passes a new convertView thats null. so again the story is repeated till the pool of android is filled up. after that android takes a convertView from its pool and passes it to you. you will find that its not null so you ask it where are my object references that I gave to you for the first time? (getTag) so you will get those and do whatever you like.
More elaboration on below line

but its pool may not have enough convertViews so it again passes a new convertView thats null

android pool is empty when your listView is going to create. so for the first item of your listView it sends you a convertView that must be displayed. after that android saves it in its pool, so its pool now contains just one convertView. for your second item of your listView that is going to create android can not use its pool because it is actually has one element and that element is your first item and it is being shown right now so it has to pass another convertView. this process repeates until android found a convertView in its pool thats not being displayed now and passes it to you.

Android inflates each row till the screen filled up after that when you scroll the list it uses holder.

上述解释与我描述的大同小异,但是英文描述可能更能表示出重点。

通过上述的探究我们明白了,ViewHolder的复用机制其实与ListVIew的Item的复用机制息息相关。我们还用改了解,ListVIew是可以展示布局条目类型的数据的。那么此时ViewHolder绑定的就是不同布局种的View,但是这并不影响,如果我们将ViewHolder当成一个条目中VIew的集合的话可能更好理解一些。

写在最后

其实这篇文章现在看起来有些过时了,毕竟ListView都快要淘汰了,但是对于刚步入Android大门的开发者来说,这些知识有必要掌握。这样才能有助于了解Android的工作模式。作为一个迈入进阶之门的小白来说,这些总结对我的提升也是蛮大的。欢迎大家指出我的理解错误。小弟将会及时更正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值