Android 开发之漫漫长途 XIV——ListView

 关注 code小生 ,每日一篇技术推送!

作者:忘了12138
地址:http://www.cnblogs.com/wangle12138/p/8441136.html
声明:本文是 忘了12138 原创投稿,转发等请联系原作者授权。

前言

列表展示控件(ListView或者RecyclerView)是我们在开发过程中经常要使用到的一种控件。而我们学习Android开发的时候,ListView也是必须掌握的。那么本篇我们来说一下ListView,虽然现在ListView逐渐的被RecyclerView取代,包括我自己的项目中也是使用的RecyclerView。那么为什么要分析一个“过时”的东西呢?因为RecyclerView的前辈,许多遗留项目是基于ListView的,可能因为种种原因不能更换或者更换代价太大,那么我们如何在ListView的基础上优化App就成了我们不得不面对的问题。同时对于ListView的学习也有助于RecyclerView的掌握。

注:关于ListView的各部分内容,网上存在着大量的博客以及教程,讲解的有浅有深。本篇博客呢立足于平常开发时所遇到的一些问题,也是本身对知识的掌握程度的检视。

ListView的使用

ListView的简单使用

关于ListView的简单使用我这里就不详细分析了,只贴上一个实例源码以及做一个小结,对应的源码目录已用红框标出

ListView的简单使用实例

布局文件activity_list_view.xml

    <?xml version="1.0" encoding="utf-8"?>
   <LinearLayout
       xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical"
       >


       <ListView
           android:id="@+id/list_view"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:cacheColorHint="#00000000"
           android:divider="#f4f4f4"
           android:dividerHeight="1dp"
           >

       </ListView>

   </LinearLayout>

对应的源码ListViewActivity

    public class ListViewActivity extends AppCompatActivity {
       @BindView(R.id.list_view)
       ListView mListView;

       private List<String> mArrayList= new ArrayList();
       @Override
       protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_list_view);
           ButterKnife.bind(this);
           //初始化数据
           init();

           //创建Adapater
           ListViewAdapter adapter = new ListViewAdapter(this,mArrayList);

           //设置Adapter
           mListView.setAdapter(adapter);

           //设置item点击监听事件
           mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
               @Override
               public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                   Toast.makeText(ListViewActivity.this,mArrayList.get(position),Toast.LENGTH_SHORT).show();
               }
           });
       }

       private void init() {
           for (int i=0;i<20;i++){
               mArrayList.add("这是第"+i+"个View");
           }
       }
   }

ListView对应的的Adapter

    public class ListViewAdapter extends BaseAdapter {
       private static final String TAG = ListViewAdapter.class.getSimpleName();

       private List<String> mList;
       private LayoutInflater inflater;
       private ViewHolder viewHolder;

       public ListViewAdapter(Context context, List<String> list) {
           mList = list;
           this.inflater  = LayoutInflater.from(context);
       }

       @Override
       public int getCount() {
           return mList == null ? 0 : mList.size();
       }

       @Override
       public Object getItem(int position) {
           return mList == null ? null : mList.get(position);
       }

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

       @Override
       public View getView(int position, View convertView, ViewGroup parent) {

           if (convertView == null){
               viewHolder = new ViewHolder();
               convertView = inflater.inflate(R.layout.list_view_item_simple, null);
               viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view);
               convertView.setTag(viewHolder);

               Log.d(TAG,"convertView == null");
           }else {
               Log.d(TAG,"convertView != null");
               viewHolder = (ViewHolder) convertView.getTag();
           }

           viewHolder.mTextView.setText(mList.get(position));
           return convertView;
       }

       static class ViewHolder {
           private TextView mTextView;
       }
   }

ListView的简单使用小结

关于ListView的使用及Adapter优化,这里给出了我们常用的优化方法,使用ViewHolder进行性能优化,这些内容也都是老生常谈的内容。不过想要深入理解使用ViewHolder是如何做到优化的,我们还得继续向下看,这里呢只是给出一个小例子让读者能够应对初中级开发工程师的面试提问。

在面试初中级Android开发工程师的时候,关于列表项展示这块基本上是必问的,你如果使用的ListView,那么ListView的性能优化,以及后面要讲到的下拉刷新上拉加载,基本也是必问的,因为这是你平常项目开发中也是肯定要考虑到的点。

对于初中级Android开发工程师来说,面试ListView的性能优化时你要回答的上来以下两点:①在ListView的Adapter中复用getView方法中的convertView ②使用静态内部类ViewHolder,用于对控件的实例存储进行缓存,减少findViewById的调用次数。

ListView的进阶使用

属性介绍

在这一小节中,介绍一些ListView 中的一些重要属性,有一些经常在项目开发中用到,而有一些不太常用,不过可以作为知识面的扩充

  1. 分割线

    android:divider="#00000000" //或者在javaCode中如下定义:listView.setDividerHeight(0);
    android:divider="@drawable/list_driver" //设置分割线的图片资源
    android:divider="@drawable/@null" //不想显示分割线
  2. 滚动条

    android:scrollbars="none"//隐藏listView的滚动条   在javaCode中如下定义setVerticalScrollBarEnabled(true);
    android:fadeScrollbars="true" //设置为true就可以实现滚动条的自动隐藏和显示
  3. 去掉上边和下边黑色的阴影

    android:fadingEdge="none" 去掉上边和下边黑色的阴影
  4. 快速滚动

    android:fastScrollEnabled="true" 
    或者在javaCode中如下定义:mListView.setFastScrollEnabled(true); 来控制启用,参数false为隐藏。

    需要注意的是当你的滚动内容较小,不到当前ListView的3个屏幕高度时则不会出现这个快速滚动滑块,同时该方法仍然是AbsListView的基础方法,可以在ListView或GridView等子类中使用快速滚动辅助。

  5. stackFromBottom属性,这只该属性之后你做好的列表就会显示你列表的最下面,值为true和false

    android:stackFromBottom="true"  //该属性默认为false,设置为true后,你的列表会从最后一项向前显示一屏
  6. cacheColorHint属性,很多人希望能够改变一下ListView的背景,使他能够符合整体的UI设计,改变背景背很简单只需要准    备一张图片然后指定属性 android:background="@drawable/bg",不过不要高兴地太早,当你这么做以后,发现背景是   变了,但是当你拖动,或者点击list空白位置的时候发现ListItem都变成黑色的了,破坏了整体效果。

    如果你只是换背景的颜色的话,可以直接指定android:cacheColorHint为你所要的颜色,如果你是用图片做背景的话,那也只要将android:cacheColorHint指定为透明(#00000000)就可以了

关于上面的属性,读者可以逐一测试,我这里就不贴测试结果了。

ListView的RecyclerBin机制

上面的内容止步于Android初中级开发工程师,那么对于中高级来说,面试官就不满足于你上面的回答了,可能会问你一些更深入的问题。例如ListView展示成千上万条数据为什么没有发生OOM呢?ListView在滑动的时候异步请求所导致的图片错位问题产生的原理及如何解决??等等

要比较完美的回答出这样的问题,那么我们就得向ListView的源码进发。

ListView类及Adapter

我们先来上一张图


作为我们第一个详细讲解的系统控件ListView,其归根到底是个View,关于View以及ViewGroup我们这里就不分析了,我们从AdapterView开始
AdapterView

AdapterView顾名思义是个有Adapter的View,其内定义了setAdapter、getAdapter等抽象方法供子类实现。View说到底是展示数据的控件,就像我们的TextView一样,Android提供的这些View系统控件也都是为了展示各种各样的数据,那么AdapterView也不例外。Android设计AdapterView呢就是为了那些数据源无法确定的场景,你如果想展示大量数据,那么你需要自定义数据源(数据源可能是数组,也可能是List,也可能是数据库)。然后你需要自定义适配器即Adapter,让AdapterView通过适配器与数据源联系在一起。

也就是说AdapterView提供了一种不需要关心数据源的通用的展示大量数据的方法。

Adapter

Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与之前不同的是,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。而Adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。

也就是说Adapter是统一的接口,定义通用的适配接口,我们实现该接口方法进行自定义适配操作即可。(Android已经预先定义了一些场景所需要的接口和基类如BaseAdapter,ArrayAdapter等)

AbsListView

作为ListView和GridView的父类,AbsListView承担了很多职责,下面我们要分析的关于ListView的View复用机制即是通过该类的内部类RecycleBin完成的。

也就是说ListView和GridView使用的是同一种View复用机制,该机制主要是由两者的父类AbsListView中的内部类RecycleBin完成。别万一被问到了GridView的View复用机制,GridView为什么展示成千上万条数据不发生OOM等问题时傻了眼。。。。

注:以下源码来自android-6.0.0_r5

AbsListView$RecycleBin

    /**
    *RecycleBin有助于在布局中重用视图。RecycleBin有两个级别的存储:ActiveViews和ScrapViews。
    *ActiveViews是在布局开始时出现在屏幕上的视图。通过构造,它们显示当前信息。
    *在布局的最后,ActiveViews中的所有视图都被降级为ScrapViews。
    *ScrapViews是可以被适配器使用的旧视图,以避免不必要地分配视图。
    *
    */

   class RecycleBin {
       private RecyclerListener mRecyclerListener;

       /**
        * 在mActiveViews中存储的第一个View的位置.
        */

       private int mFirstActivePosition;

       /**
        *在布局开始时在屏幕上的视图。这个数组在布局开始时填充,
        *在布局的末尾,mActiveViews中的所有视图都被移动到mScrapViews
        *mActiveViews表示一个连续的视图范围,第一个视图的位置存储在mFirstActivePosition。
        */

       private View[] mActiveViews = new View[0];

       /**
        *可将适配器用作转换视图的未排序视图。
        */

       private ArrayList<View>[] mScrapViews;

       private int mViewTypeCount;

       private ArrayList<View> mCurrentScrap;

       /**
        * RecycleBin当中使用mActiveViews这个数组来存储View,
        * 调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
        *
        * @param childCount
        *            第一个参数表示要存储的view的数量
        * @param firstActivePosition
        *            ListView中第一个可见元素的position值
        */

       void fillActiveViews(int childCount, int firstActivePosition) {
         
       }

       /**
        *该方法与上面的fillActiveViews对应,功能是获取对应于指定位置的视图。视图如果被发现,就会从mActiveViews删除
        *
        * @param position
        * 表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。
        * @return
        * 返回找到的View 下次获取同样位置的View将会返回null。
        */

       View getActiveView(int position) {
         
           return null;
       }

       /**
        * 根据mViewTypeCount把一个View放进 ScapViews list. 这些View是未经过排序的.
        *
        * @param scrap
        *            需要被加入的View
        */

       void addScrapView(View scrap, int position) {
         
       }

       /**
        * @return 根据mViewTypeCount从mScapViews中获取
        */

        View getScrapView(int position) {
         
           return null;
       }

           /**
        * @return 用于从ScapViews list中取出一个View,这些废弃缓存中的View是没有顺序可言的,
        * 因此retrieveFromScrap方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回.
        */

        private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
           
               return scrap;
           } else {
               return null;
           }
       }


       /**
       *Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,
       *setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。
       */


       public void setViewTypeCount(int viewTypeCount) {
           
       }

   }

ListView的layout(布局过程)

ListView虽然很复杂,但是其继承自View,终究逃不过View的那5大过程,关于这部分内容读者如果不清楚,可参看之前的博文,[Android开发之漫漫长途 Ⅴ——Activity的显示之ViewRootImpl的预测量、窗口布局、最终测量、布局、绘制]()

从之前的文章我们就知道,View经过预测量、窗口布局(根据条件进入)、最终测量、布局、绘制阶段,那么对于ListView也不例外,

在第一次“心跳”performTraversals()函数中,我们会对ListView进行预测量、最终测量 2次测量,onMeasure()方法被调用两次,1次布局 onLayout()方法调用1次,

到最后调用draw方法我们来看下面这段代码

      if (!cancelDraw && !newSurface) {
           if (!skipDraw || mReportNextDraw) {
               if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                   for (int i = 0; i < mPendingTransitions.size(); ++i) {
                       mPendingTransitions.get(i).startChangingAnimations();
                   }
                   mPendingTransitions.clear();
               }

               //调用 performDraw();
               performDraw();
           }
   } else {
       if (viewVisibility == View.VISIBLE) {
           //调用 scheduleTraversals();
           scheduleTraversals();
       } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
           for (int i = 0; i < mPendingTransitions.size(); ++i) {
               mPendingTransitions.get(i).endChangingAnimations();
           }
           mPendingTransitions.clear();
       }
   }

从上面的代码我们可以看出,,如果cancelDraw为true或者newSurface为true时,,会调用 scheduleTraversals();而这个函数会导致“心跳”performTraversals()函数的调用,再重新走一遍上面的过程

这也导致了

第1次“心跳”onMeasure() onMeasure() onLayout()

第2次“心跳”onMeasure() onLayout() onDraw()

有些读者可能会问了,为什么第1次心跳是2次onMeasure() onMeasure(),第二次心跳是1次onMeasure呢,这跟View的measure机制有关

[View.java]

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
     ......
     // 当FLAG_FORCE_LAYOUT位为1时,就是当前视图请求一次布局操作
     //或者当前当前widthSpec和heightSpec不等于上次调用时传入的参数的时候
     //才进行从新测量。
       if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
               widthMeasureSpec != mOldWidthMeasureSpec ||
               heightMeasureSpec != mOldHeightMeasureSpec) {
               ......
               onMeasure(widthMeasureSpec, heightMeasureSpec);
               ......
       }
       ......
   }

也就是说对于任意一个View而言,想显示到界面,至少经过两次onMeasur及onLayout的调用

对于测量和绘制不是我们这个ListView所关心的,我们只关心它的布局

上面说了半天,其实就是让读者对ListView的测量、布局、绘制流程有个更深入的了解,对于其他View,我们并不关心它进行了几次Measure,几次layout,但是对于ListView而言这个却比较重要,因为ListView是在布局过程中向其中添加数据的,如果多次布局,那么不就添加重复数据了吗?这个我们可以看到ListView巧妙的设计来避免了重复添加数据的问题。

第1次layout

谈到layout,相信读者也都了然一笑,肯定看onLayout方法,结果发现ListView中没有此方法,

不着急,我们去它爸爸,果然在它爸爸那里找到了

[AbsListView.java]

    @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
       super.onLayout(changed, l, t, r, b);

       mInLayout = true;

       final int childCount = getChildCount();
       if (changed) {
           for (int i = 0; i < childCount; i++) {
               getChildAt(i).forceLayout();
           }
           mRecycler.markChildrenDirty();
       }

       layoutChildren();
       mInLayout = false;

       mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

       // TODO: Move somewhere sane. This doesn't belong in onLayout().
       if (mFastScroll != null) {
           mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
       }
   }

上面的代码很短,也不复杂,复杂的操作被封装在layoutChildren();方法中了,跟进

     /**
    * Subclasses must override this method to layout their children.
    */

   protected void layoutChildren() {
   }

空方法

看其说明应该是子类重写了该方法了,,我们又回到ListView

[ListView.java]

     @Override
   protected void layoutChildren() {

       try {
       
           final int childrenTop = mListPadding.top;
           final int childrenBottom = mBottom - mTop - mListPadding.bottom;
           /**
           *第1次Layout时,ListView内还没有数据,数据还在Adapter那,这里childCount=0
           */


           final int childCount = getChildCount();

           int index = 0;
           int delta = 0;

              ......
           /**
           *dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,
           */

           boolean dataChanged = mDataChanged;
           if (dataChanged) {
               handleDataChanged();
           }
           ......
       
           } else {
                   /**
                   *RecycleBin的fillActiveViews()方法缓存View
                   *可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。
                   */

               recycleBin.fillActiveViews(childCount, firstPosition);
           }

           /**
           *目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。
           */

           detachAllViewsFromParent();
           recycleBin.removeSkippedScrap();

           /**
           *mLayoutMode默认为LAYOUT_NORMAL
           */

           switch (mLayoutMode) {
         
           default:
               if (childCount == 0) {
                   if (!mStackFromBottom) {
                       /**
                       *mStackFromBottom我们在上面的属性中讲过,该属性默认为false
                       */

                       final int position = lookForSelectablePosition(0, true);
                       setSelectedPositionInt(position);
                       //调用fillFromTop方法
                       sel = fillFromTop(childrenTop);
                   } else {
                       final int position = lookForSelectablePosition(mItemCount - 1, false);
                       setSelectedPositionInt(position);
                       sel = fillUp(mItemCount - 1, childrenBottom);
                   }
             
               }
               break;
           }

          /**
           *RecycleBin的scrapActiveViews从mActivieViews缓存到mScrapActiveViews
           *可是目前RecycleBin的mActivieViews也没什么数据,因此这一行暂时还起不了任何作用。
           */

           recycleBin.scrapActiveViews();

           ......
       }
   }

第1次layout时就是做一些初始化ListView的操作,调用fillFromTop方法去填充ListView,跟进fillFromTop

[ListView.java]

    /**
   *参数nextTop表示下一个子View应该放置的位置,
   *这里传入的nextTop=mListPadding.top;明显第一个子View是放在ListView的最上方,
   *注意padding不属于子View,属于父View的一部分
   */

    private View fillFromTop(int nextTop) {

       //调用fillDown
       return fillDown(mFirstPosition, nextTop);
   }

跟进fillDown

[ListView.java]

    /**
    * 从pos开始从上向下填充ListViwe
    *
    * @param pos
    *        list中的位置
    *        
    * @param nextTop
    *        下一个Item应该放置的位置
    *
    * @return
    *       返回选择位置的View,这个位置在我们的放置范围之内
    */

   private View fillDown(int pos, int nextTop) {
       View selectedView = null;

       int end = (mBottom - mTop);
       if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
           end -= mListPadding.bottom;
       }

       /**
       *这里循环判断,终止的条件是下一个item放置的位置>listview的底部或者当前的位置>总数
       */

       while (nextTop < end && pos < mItemCount) {

           boolean selected = pos == mSelectedPosition;
           /**
           *这里调用makeAndAddView来获得一个View
           */

           View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

           //更新nextTop的值
           nextTop = child.getBottom() + mDividerHeight;
           if (selected) {
               selectedView = child;
           }
           //自增pos
           pos++;
       }

       setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
       return selectedView;
   }

继续跟进makeAndAddView

[ListView.java]

    /**
    * 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews
    * @param position
    *        列表中的逻辑位置
    * @param y
    *       被添加View的上 边位置或者下 边位置
    * @param flow
    *        如果flow是true,那么y是View的上 边位置,否则那么y是View的下 边位置
    * @param childrenLeft Left edge where children should be positioned
    * @param selected Is this position selected?
    * @return
    *       返回被添加的View
    */

   private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
           boolean selected)
{
       View child;


       if (!mDataChanged) {
           // 尝试从mActiveViews中获取,第1次肯定是没有获取到
           child = mRecycler.getActiveView(position);
           if (child != null) {
               // Found it -- we're using an existing child
               // This just needs to be positioned
               setupChild(child, position, y, flow, childrenLeft, selected, true);

               return child;
           }
       }

       // 为这个position创建View
       child = obtainView(position, mIsScrap);

       // 调用setupChild
       setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

       return child;
   }

我们第1次layout时,尝试从mActiveViews中获取View,这里child为null,那么我们跟进obtainView
我们没有在ListView中找到该方法,那么应该在其父类中,,跟进

[AbsListView.java]

    /**
    * 获取一个视图,并让它显示与指定的数据相关联的数据的位置。
    *当我们已经发现视图无法在RecycleBin重复使用。剩下的唯一选择就是转换旧视图或创建新视图。
    *
    * @param position The position to display
    * @param isScrap Array of at least 1 boolean, the first entry will become true if
    *                the returned view was taken from the scrap heap, false if otherwise.
    *
    * @return A view displaying the data associated with the specified position
    */

   View obtainView(int position, boolean[] isScrap) {
     
       return child;
   }

使用obtainView一定会得到一个View,这个View要么是新创建的,要么是从mScrapViews中复用的。在第1次layout中,肯定无法得到复用的View,那么新创建的,

 final View child = mAdapter.getView(position, scrapView, this);

这里的mAdapter毫无疑问是跟ListView关联的Adapter,那么getView方法,读者应该想到了,就是我们需要重写的那个方法

    public class ListViewAdapter extends BaseAdapter {

       /**
       *convertView = scrapView在第1次layout中,scrapView为null
       *parent = this 即ListView本身
       */

       @Override
       public View getView(int position, View convertView, ViewGroup parent) {

           if (convertView == null){
               viewHolder = new ViewHolder();
               convertView = inflater.inflate(R.layout.list_view_item_simple, null);
               viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view);
               convertView.setTag(viewHolder);

               Log.d(TAG,"convertView == null");
           }else {
               Log.d(TAG,"convertView != null");
               viewHolder = (ViewHolder) convertView.getTag();
           }

           viewHolder.mTextView.setText(mList.get(position));
           return convertView;
       }


       static class ViewHolder {
           private TextView mTextView;
       }
   }

现在我们对这个方法有了更深入的认识,convertView就是我们得到的在mScrapViews中的View,所以我们在getView中判断convertView是否为null,如果为null,通过LayoutInfalter加载。

那么到这里我们就需要回到

[ListView.java]

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
           boolean selected)
{
       View child;

       ......
       // view我们这里已经得到了
       child = obtainView(position, mIsScrap);

       // 调用setupChild
       setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

       return child;
   }

接着跟进setupChild
[ListView.java]

    /**
    * 添加一个子View然后确定该子View的测量(如果必要的话)和合理的位置
    *
    * @param child
    *        被添加的子View
    * @param position
    *        子View的位置
    * @param y
    *        子View被放置的坐标
    * @param flowDown
    *        如果是true的话,那么上面参数中的y表示子View的上 边的位置,否则为下 边的位置
    * @param childrenLeft Left edge where children should be positioned
    * @param selected Is this position selected?
    * @param recycled 布尔值,意为child是否是从RecycleBin中得到的,如果是的话,不需要重新Measure
    * 第1次layout时,该值为false
    */

   private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
           boolean selected, boolean recycled)
{
       Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");

     
   }

这里调用了addViewInLayout()方法将它添加到了ListView当中。那么根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满然后就跳出,也就是说即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上。

那么到此为止,第一次Layout过程结束。

也就是说,ListView的第1次layout中,只是填充ListView的子View,即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,并不涉及RecycleBin的运作

第2次layout

[ListView.java]

     @Override
   protected void layoutChildren() {      

           invalidate();
           
           /**
           *第2次Layout时,ListView中已经有了一屏的子View,
           *调用detachAllViewsFromParent();把ListView中的所有子View detach了
           *这也是多次调用layout不会重复添加数据的原因
           */

           detachAllViewsFromParent();
           recycleBin.removeSkippedScrap();

           /**
           *mLayoutMode默认为LAYOUT_NORMAL
           */

           switch (mLayoutMode) {
       
           default:
               if (childCount == 0) {
                 
               } else {
                   /**
                   *第2次Layout时childCount不为0
                   */

                   if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                       sel = fillSpecific(mSelectedPosition,
                               oldSel == null ? childrenTop : oldSel.getTop());
                   } else if (mFirstPosition < mItemCount) {
                       sel = fillSpecific(mFirstPosition,
                               oldFirst == null ? childrenTop : oldFirst.getTop());
                   } else {
                       sel = fillSpecific(0, childrenTop);
                   }
               }
               break;
           }

          /**
           *RecycleBin的scrapActiveViews方法把mActivieViews中的View再缓存到mScrapActiveViews
           */

           recycleBin.scrapActiveViews();

           ......
       }
   }

其实第二次Layout和第一次Layout的基本流程是差不多的,
第2次Layout时childCount不为0,我们进入了

    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
       sel = fillSpecific(mSelectedPosition,
               oldSel == null ? childrenTop : oldSel.getTop());
   } else if (mFirstPosition < mItemCount) {
       sel = fillSpecific(mFirstPosition,
               oldFirst == null ? childrenTop : oldFirst.getTop());
   } else {
       sel = fillSpecific(0, childrenTop);
   }

第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断通常是成立的,因为mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中,代码如下所示:

    /**
    *它和fillUp()、fillDown()方法功能也是差不多的,
    *主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,
    *然后再加载该子View往上以及往下的其它子View。
    *那么由于这里我们传入的position就是第一个子View的位置,
    *于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了
    *
    * @param position The reference view to use as the starting point
    * @param top Pixel offset from the top of this view to the top of the
    *        reference view.
    *
    * @return The selected view, or null if the selected view is outside the
    *         visible area.
    */

   private View fillSpecific(int position, int top) {
       boolean tempIsSelected = position == mSelectedPosition;
       View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
       // Possibly changed again in fillUp if we add rows above this one.
       mFirstPosition = position;

     
   }

那么我们第3次回到makeAndAddView

[ListView.java]

    /**
    * 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews
    * @param position
    *        列表中的逻辑位置
    * @param y
    *       被添加View的上 边位置或者下 边位置
    * @param flow
    *        如果flow是true,那么y是View的上 边位置,否则那么y是View的下 边位置
    * @param childrenLeft Left edge where children should be positioned
    * @param selected Is this position selected?
    * @return
    *       返回被添加的View
    */

   private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
           boolean selected)
{
       View child;


       if (!mDataChanged) {
           // 尝试从mActiveViews中获取,第2次layout中child可以从mActiveViews中获取到
           child = mRecycler.getActiveView(position);
           if (child != null) {
               //这里child不为空,调用setupChild,注意最后一个参数为true
               setupChild(child, position, y, flow, childrenLeft, selected, true);

               return child;
           }
       }

       // 为这个position创建View
       child = obtainView(position, mIsScrap);

       // 调用setupChild
       setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

       return child;
   }

我们这里又调用了setupChild,跟上面不同的是最后一个参数

[ListView.java]

    /**
    * 添加一个子View然后确定该子View的测量(如果必要的话)和合理的位置
    *
    * @param child
    *        被添加的子View
    * @param position
    *        子View的位置
    * @param y
    *        子View被放置的坐标
    * @param flowDown
    *        如果是true的话,那么上面参数中的y表示子View的上 边的位置,否则为下 边的位置
    * @param childrenLeft Left edge where children should be positioned
    * @param selected Is this position selected?
    * @param recycled 布尔值,意为child是否是从RecycleBin中得到的,如果是的话,不需要重新Measure
    * 第2次layout时,该值为true
    */

   private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
           boolean selected, boolean recycled)
{
       Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");

       
   }

由于recycled现在是true,所以会执行attachViewToParent()方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()方法。这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。那么由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()方法是正确的选择。

经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。

也就是说,ListView的第2次layout中,把ListView中的所有子View缓存到RecycleBin中的mActiveViews,然后再detach掉ListView中所有子View,接着attach回来(这时使用的是mActiveViews中的缓存,没有重新inflate),然后再把mActiveViews缓存到mScrapViews中(还记得RecycleBin中的getActiveView方法吗,我们是怎么描述这个方法的,功能是获取对应于指定位置的视图。视图如果被发现,就会从mActiveViews删除,也就是说不能从同一个位置的View不能从mActiveViews中获得第二次)

滑动加载

经历了两次Layout过程,虽说我们已经可以在ListView中看到内容了,然而关于ListView最神奇的部分我们却还没有接触到,因为目前ListView中只是加载并显示了第一屏的数据而已。关于触摸事件的分发机制,读者不太清楚的可参看前面的博文Android开发之漫漫长途 Ⅵ——图解Android事件分发机制(深入底层源码)

我们这里直接来看onTouchEvent

[AbsListView.java]

    @Override
   public boolean onTouchEvent(MotionEvent ev) {
          ......
       switch (actionMasked) {
           ......

           case MotionEvent.ACTION_MOVE: {
               onTouchMove(ev, vtev);
               break;
           }
           ......
       }

      ......
      return true;
   }

其他的我们一概不关心,径直找到 MotionEvent.ACTION_MOVE当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,跟进scrollIfNeeded

总体而言,这一步也算是一个外壳,真正跟踪滑动运行的是trackMotionScroll方法。trackMotionScroll方法的逻辑较为复杂;

这部分过程如下图

fillGap在AbsListView.java是个抽象方法,那么显然在子类中找其重写的具体实现方法

abstract void fillGap(boolean down);

[ListView.java]

    //这里参数down应该是true,我们是从上向下滑动
   @Override
   void fillGap(boolean down) {
     
   }

我们再次来看fillDown方法

[ListView.java]

    /**
    * 从pos开始从上向下填充ListViwe
    *
    * @param pos
    *        list中的位置
    *        
    * @param nextTop
    *        下一个Item应该放置的位置
    *
    * @return
    *       返回选择位置的View,这个位置在我们的放置范围之内
    */

   private View fillDown(int pos, int nextTop) {
       View selectedView = null;

       int end = (mBottom - mTop);
       if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
           end -= mListPadding.bottom;
       }

       /**
       *这里循环判断,终止的条件是下一个item放置的位置>listview的底部或者当前的位置>总数
       */

       while (nextTop < end && pos < mItemCount) {

           boolean selected = pos == mSelectedPosition;
           /**
           *这里调用makeAndAddView来获得一个View
           */

           View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

           //更新nextTop的值
           nextTop = child.getBottom() + mDividerHeight;
           if (selected) {
               selectedView = child;
           }
           //自增pos
           pos++;
       }

       setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
       return selectedView;
   }

fillDown逻辑并没有什么变化,再次makeAndAddView

[ListView.java]

    /**
    * 获取一个View并添加进ListView。这个View呢可能是新创建的,也有可能是来自mActiveViews,或者是来自mScrapViews
    * @param position
    *        列表中的逻辑位置
    * @param y
    *       被添加View的上 边位置或者下 边位置
    * @param flow
    *        如果flow是true,那么y是View的上 边位置,否则那么y是View的下 边位置
    * @param childrenLeft Left edge where children should be positioned
    * @param selected Is this position selected?
    * @return
    *       返回被添加的View
    */

   private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
           boolean selected)
{
       View child;


       if (!mDataChanged) {
           // 尝试从mActiveViews中获取,因为我们第2次layout中已经从mActiveViews中获取到View了,,所以这次获取的为null
           child = mRecycler.getActiveView(position);
           if (child != null) {
               //这里child不为空,调用setupChild,注意最后一个参数为true
               setupChild(child, position, y, flow, childrenLeft, selected, true);

               return child;
           }
       }

       // 为这个position创建View
       child = obtainView(position, mIsScrap);

       // 调用setupChild
       setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

       return child;
   }

接着我们又来到了obtainView,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了

[ListView.java]

    /**
    * 获取一个视图,并让它显示与指定的数据相关联的数据的位置。
    *当我们已经发现视图无法在RecycleBin重复使用。剩下的唯一选择就是转换旧视图或创建新视图。
    *
    * @param position The position to display
    * @param isScrap Array of at least 1 boolean, the first entry will become true if
    *                the returned view was taken from the scrap heap, false if otherwise.
    *
    * @return A view displaying the data associated with the specified position
    */

   View obtainView(int position, boolean[] isScrap) {
       Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

       isScrap[0] = false;

       Trace.traceEnd(Trace.TRACE_TAG_VIEW);
       return child;
   }

我们可以看到这句

    final View scrapView = mRecycler.getScrapView(position);
   final View child = mAdapter.getView(position, scrapView, this);

RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

那么另外还有一点是需要大家留意的,这里获取到了一个scrapView,然后我们在第上述代码中第2行将它作为第二个参数传入到了Adapter的getView()方法当中。我们再次来看我们重写的getView方法

    public class ListViewAdapter extends BaseAdapter {

       @Override
       public View getView(int position, View convertView, ViewGroup parent) {

           if (convertView == null){
               viewHolder = new ViewHolder();
               convertView = inflater.inflate(R.layout.list_view_item_simple, null);
               viewHolder.mTextView=(TextView) convertView.findViewById(R.id.text_view);
               convertView.setTag(viewHolder);

               Log.d(TAG,"convertView == null");
           }else {
               Log.d(TAG,"convertView != null");
               viewHolder = (ViewHolder) convertView.getTag();
           }

           viewHolder.mTextView.setText(mList.get(position));
           return convertView;
       }


       static class ViewHolder {
           private TextView mTextView;
       }
   }

这次convertView不为null了,最后返回了convertView

这下你彻底明白了吗??

最后再上张图

源码地址:源码传送门

本篇总结

本篇呢,分析详细介绍了ListView及其View复用机制,文中若有不正确或者不恰当的地方,欢迎各位读者前来拍砖。

下篇预告

下篇呢我们把ListView换成RecyclerView

参考博文

http://blog.csdn.net/guolin_blog/article/details/44996879

此致,敬礼


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值