android ListViev 详解

xiao_yuanjl · · 76 次点击 · · 开始浏览    

android ListView:
下面是本人没事从网上各中扒,参考各个大牛写的文章,最后总结了一下listView,链接在这里就不写了,希望此片文章能有所帮助;

1.什么是ListView………………………………………………………………………….
2.Adapter的作用……………………………………………………………………………
3.RecycleBin机制…………………………………………………………………………..
4.了解View………………………………………………………………………………….
5.Layout……………………………………………………………………………………..
6.滑动加载数据……………………………………………………………………………..
7.HeaderView和 FooterView ……………………………………………………………….
8.HeaderViewListAdapter …………………………………………………………………..

      这里写图片描述

什么是ListView?
  在开发的过程中,在Android所有常用的原生控件当中,用法最复杂的应该就是ListView了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。
  ListView可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了,而ListView的使用过程中,又往往与Adapter结下了不结之缘。
   Listview有那些功能呢?功能有很多,例如显示列表,自定义上拉刷新,异步任务加载图片等;
ListView主要是一个显示列表视图滚动功能,通过Adapter适配器把数据程放到到listview上;ListView是Android手机系统中广泛使用的一个组件,一般应用于一行显示一个的内容,以垂直的方式显示所有的列表项,在显示联系人名单.菜单列表等都用到了ListView;图1是简单的一个ListView的实现效果图;
Listview通常有两个职责:
1)将数据填充;
通过setAdapter()方法,把数据源添加进去;
2)处理用户的点击操作;
通过setOnItemClickListener()方法来处理用户点击事件;
这里写图片描述
      图1
首先我们来看一下ListView的继承结构 图2;

java.lang.Object 
      android.view.View 
         android.view.ViewGroup 
            android.widget.AdapterView 
                 android.widget.AbsListView 
                      android.wid.widget.ListView 

这里写图片描述
               图2
  另外ListView 还有一个非常神奇的功能,我相信大家应该都体验过,即使在ListView中加载非常非常多的数据,比如达到成百上千条甚至更多,ListView都不会发生OOM或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。
  可以看到 ,ListView的继承结构还是相当复杂的,它是直接继承自的AbsListView,而AbsListView有两个子实现类,一个是ListView,另一个就是GridView,因此我们从这一点就可以猜出来,ListView和GridView在工作原理和实现上都是有很多共同点的。然后AbsListView又继承自AdapterView,AdapterView继承自ViewGroup。
  ListView是AdapterView的子类,AdapterView.OnItemClickListener. AdapterView.OnItemSelectedLister 这些关乎Item的操作都是在AdapterView 中就有的抽象;ListView只是再此基础上加上了一些功能,比如addFooterVIew. addHeaderView等,其实就是加了特殊性的Item,只能在头和尾添加。

Adapter的作用是什么呢?
  Adapter是把数据和用户界面View 绑定到一起的桥梁类,Adapter是连接后端数据和前端显示的适配器接口,是数据和UI(View)之间一个重要的纽带。在常见的View(ListView,GridView)等地方都需要用到Adapter,通过下面的图3理解一下;
  Adapter相信大家都不会陌生,我们平时使用ListView的时候一定都会用到它。那么话说回来大家有没有仔细想过,为什么需要Adapter这个东西呢?总感觉正因为有了Adapter,ListView的使用变得要比其它控件复杂得多。
             这里写图片描述
                           图3
  那么这里我们就先来了解一下Adapter到底起到了什么样的一个作用。其实说到底,控件就是为了交互和展示数据用的,只不过ListView更加特殊,它是为了展示很多很多数据用的,但是ListView只承担交互和展示工作,至于这些数据来自哪里,ListView是不关心的。因此,我们能设想到的最基本的ListView工作模式就是要有一个ListView控件和一个数据源。
不过如果真的让 ListView和数据源直接打交道的话,那ListView所要做的适配工作就非常繁杂了。因为数据源这个概念太模糊了,我们只知道它包含了很多数据,至于这个数据源到底是什么样类型,并没有严格的定义,有可能是数组,也有可能是集合,甚至有可能是数据库表中查询出来的游标。所以说如果ListView真的去为每一种数据源都进行适配操作的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了它本身应该负责的工作范围,不再是仅仅承担交互和展示工作就可以了,这样ListView就会变得比较臃肿。
  于是就有了Adapter这样一个机制出现。顾名思义,Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与之前不同的是,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。而Adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。原理示意图如下所示:图4
  
             这里写图片描述
                       图4

   在Android中与Adapter有关的所有接口,在我们使用过程中可以给据自己的需求实现接口或者继承类进行一定的扩展,比较常用的有BaseAdapter,SimpleAdapter,Arrayadapter,SimpleCursorAdapter等.
   
              这里写图片描述
                        图5

1)BaseAdapter是一个抽象类,继承它需要实现较多的方法,所以也就具有较高的灵活性;
2)Arrayadapter 支持泛型操作,最为简单,只能展示一行字;
3)SimpleCursorAdapter可以适用于简单的纯文字型ListView,它需要Cursor的子段和UI的id对应起来.如果需要实现更复杂的UI也可以重写其他方法.可以认为是SimpleAdapter的数据库的简单的结合,可以方便的把数据库的内容以列表的形式展示出来.

ListView 显示:
   从总体上理解大致理解了ListView和adapter,下面我们来创建个listview;创建listView有三种方法,第一种是在xml文件里使用ListView组件;第二种是直接继承ListAtivity,第三中是在java文件中直接new 出一个对象,下面我用了第一中方式实现了一下.图6
  
MainActivity

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.apptest.list.appdemo.ListsMainActivity" >
     <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </ListView>
</RelativeLayout

package com.example.test;        
import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MainActivity extends Activity {
    private ListView listView
          @Override  
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView = (ListView) findViewById(R.id.listView);
        // 列表项数组
        String[] weeks = { "星期天", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" };
        // 数组适配器
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_1, weeks);
        // 为ListView设置适配器
        listView.setAdapter(adapter);
 }}

  当然Adapter的作用不仅仅只有数据源适配这一点,还有一个非常非常重要的方法也需要我
们在Adapter当中去重写,就是getView()方法,下面我们会了解到。我们先来理解ListView

  这里写图片描述
      图6
RecycleBin机制?
  在理解ListView的同时,我们还一个东西需要我们去了解,就是RecycleBin,这个机制也是ListView能够实现成百上千条数据不会OOM 最重要的一个原因.它是写在AbsListView类中的,是它的内部类,所以所有继承自AbsListView的子类,也就是ListVIew 和GridView,都可以使用这个机制 .在AbsListView类中看RecycleBin代码;主要提出几个方法:
1-1:RecycleBin

    class RecycleBIn {
        private RecylerListener mRecyclerListener;
        private int mFirstActivePosition;
         …
        void fillActiveViews(int childCount,int firstActivePosition){
         …
        }
        View getActiveView(int postion){
        ...
        }
        void addScraView(View scrap){
         ….
        }
        View getScrapview(int position){
          ...
        }
        public void setViewTypeCount(int viewTypeCount){
        …
        }

    }

   fillActiveViews()这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储mActiveViews数组当中。
   getActiveView()这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View ,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View 将会返回null,也就是说mActiveViews不能被重复利用。
  addScrapView()用于将一个废弃的View进行缓存,该方法接收一个View 参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。
  getScrapView用于从废弃缓存中取出一个View ,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
  setViewTypeCount()我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。
  要注意的是 HeaderView / FootView是不被回收的。如有需要可以将自己定义的viewType设置为-1,否则,将会浪费内存,导致OOM

View?
  了解了RecycleBin中的主要方法以及它们的用处之后,下面我们再看ListView;ListView即使再特殊最终还是继承自View的,因此他的执行流程还会将按照View 的规则来执行,View的执行流程可以说分为三部,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。
  onDraw()在ListView当中也没有什么意义,因为ListView 本身并不负责绘制,而是由ListView 当中的子元素来进行绘制的。那么ListView 大部分的神奇功能其实都是在onLayout()方法中进行的了,在这不太明白的可以参考View,在这就不细说了。

Layout?
   如果你到ListView 源码中去找,你会发现ListView中是没有onLayout()这个方法的,这是因为这个方法是在ListView的父类AbsListView 中实现的;
1-2:AbsListView.onLayout

@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 mFirstActivePosition;
        if(changed){
            for(...){
                getChildAt(i).forceLayout();
            }
        ...
        }
        layoutChildren();
        mInLayout=false;
        …
        if(...){
      ...
        }
    }

  onLayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。在onLayout中调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入ListView的layoutChildren()方法;
1-3:ListView.layoutChildre

@override
   protected void layoutChidren(){
     final boolean blockLayoutRequests=mBlockLayoutRequests;
  if(...){
    return;
}
   try{
   ...
}
  ...
     int childCount=getChildCount();
     ...
   if(dataChanged){
      for(...){
      ...  
      }
    }else{
      recycleBin.fillActiveViews(childCout,firstPosition);
}
     ...
     switch(mLayoutMode){
     ...
}
}

  首先可以确定的是,ListView当中目前还没有任何子View,数据都还是由Adapter管理的,并没有展示到界面上,因此第getChildCount()方法得到的值肯定是0。接着会根据dataChanged这个布尔型的值来判断执行逻辑,dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,因此这里会进入到recycleBin.fillActiveViews(childCount,firstPosition);执行逻辑,调用RecycleBin的fillActiveViews()方法。
  按理来说,调用 fillActiveViews()方法是为了将ListView 的子View进行缓存的,可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。 接下来在根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入到第default语句当中。而下面又会紧接着进行两次if判断,childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到fillFromTop()方法;
1-4:ListView.fillFrmTop

    protected View fillFromTop(int nextTop){
     mFirstPosition=
           Math.min(mFirstPosition,mSelectedPosition);
     mFirstPosition=
           Math.min(mFirstPosition,mItemCount-1);
  if(...){
     ...  
}
  return fillDown(mFirstPosition,nextTop);
}

  它所负责的主要任务就是从 mFirstPosition开始,自顶至底去填充ListView。而这个方法本身并没有什么逻辑,就是判断了一下mFirstPosition值的合法性,然后调用fillDown()方法,那么我们就有理由可以猜测,填充ListView的操作是在fillDown()方法中完成的。
  进入fillDown()方法,这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。
  因此一开始的情况下 nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出while循环。
1-5:ListView.makeAndAddView

    Private View makeAndAddView(int position,Int y,boolean
           flow, int childrenLeft,boolean selected){
       View child;
       if(!mDataChanged){
         child=mRecycler.getActiveView(position);
          if(...){
         setupChild(child,positin,flow,
                     childrenLeft,selected,true);
          return child;
       }
    }
      ...
}

  在这里尝试从RecycleBin当中快速获取一个Active view,不过很遗憾的是目前RecycleBin当中还没有缓存任何的View,所以这里得到的值肯定是null。那么取得了null之后就会继续向下运行,调用obtainView()方法来再次尝试获取一个View,这次的obtainView()方法是可以保证一定返回一个View的,于是下面立刻将获取到的View传入到了setupChild()方法当中.
1-6:AbsListView.obtanVIew

    View obtainView(int position,boolean[] isScrap){
       Trace,traceBegin(...);
       isScrap[0]=false;
      final View transientView=
             mRecycler.getTransientStateView(position);
       ...
      final View child=
              mAdapter.getView(position,scrapView,this);     
      if(scrapView!=null){
          ...
    }
          ...
}

  obtainView()方法中的代码并不多,但却包含了非常非常重要的逻辑,整个ListView中最重要的内容可能就在这个方法里了。那么我们还是按照执行流程来看,在代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,同样的道理,这里肯定是获取不到的,getScrapView()方法会返回一个null。这时调用mAdapter的getView()方法来去获取一个View 。
  
  那么mAdapter是什么呢?就是当前ListView关联的适配器了。而getView()方法又是什么呢?这个就是我们平时使用ListView时最最经常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this.简单举个例子;
1-7 :getView

  public View getView(int position,
                View convertView,ViewGroup parent){
         Fruit fruit = getItem(position);  
         View view;  
      if (convertView == null) {  
        view = LayoutInflater.from(getContext()).inflate(resourceId, null);  
       } else {  
        view = convertView;  
       }  
       ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);  
       TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);  
       fruitImage.setImageResource(fruit.getImageId());  
       fruitName.setText(fruit.getName());  
       return view; 
}

  getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法来去加载一个布局。会对这个view进行一些属性和值的设定,最后将view返回。
  那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时;
1-8:ListView.SetupChild

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

        final boolean isSelected = selected && shouldShowSelector(); 
        final boolean updateChildSelected = isSelected != child.isSelected(); 
        final int mode = mTouchMode; 
        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && 
                mMotionPosition == position; 
        final boolean updateChildPressed = isPressed != child.isPressed(); 
        final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); 
         ... 

        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter 
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { 
            attachViewToParent(child, flowDown ? -1 : 0, p); 
        } else { 
            p.forceAdd = false; 
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 
                p.recycledHeaderFooter = true; 
            } 
            addViewInLayout(child, flowDown ? -1 : 0, p, true); 
        } 

        }

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

第二次Layout
  虽然我在源码中并没有找出具体的原因,但如果你自己做一下实验的话就会发现,即使是一个再简单的 View,在展示到界面上之前都会经历至少两次onMeasure()和两次onLayout()的过程。其实这只是一个很小的细节,平时对我们影响并不大,因为不管是onMeasure()或者onLayout()几次,反正都是执行的相同的逻辑,我们并不需要进行过多关心。但是在ListView中情况就不一样了,因为这就意味着layoutChildren()过程会执行两次,而这个过程当中涉及到向ListView中添加子元素,如果相同的逻辑执行两遍的话,那么ListView中就会存在一份重复的数据了。因此ListView在layoutChildren()过程当中做了第二次Layout的逻辑处理,非常巧妙地解决了这个问题,
  下面我们就来分析一下第二次Layout的过程。其实第二次Layout和第一次Layout的基本流程是差不多的,那么我们还是从layoutChildren()方法开始看起:
1-9:ListView.layoutChildren

  @Override 
    protected void layoutChildren() { 
        final boolean blockLayoutRequests = mBlockLayoutRequests; 
        ... 
        try { 
            ... 
            final int childrenTop = mListPadding.top; 
            final int childrenBottom = mBottom - mTop - mListPadding.bottom; 
            final int childCount = getChildCount(); 
            ... 
            final int firstPosition = mFirstPosition; 
            final RecycleBin recycleBin = mRecycler; 
            if (dataChanged) { 
                for (int i = 0; i < childCount; i++) { 
                    recycleBin.addScrapView(getChildAt(i), firstPosition+i); 
                } 
            } else { 
                recycleBin.fillActiveViews(childCount, firstPosition); 
            } 
            // Clear out old views 
            detachAllViewsFromParent(); 
            recycleBin.removeSkippedScrap(); 

            switch (mLayoutMode) { 
               ... 
            default: 
             if (childCount == 0) {
                            }else{
                    }
        } finally { 
            if (!blockLayoutRequests) { 
                mBlockLayoutRequests = false; 
            } 
        } 
    }

  在调用getChildCount()方法来获取子View的数量,只不过现在得到的值不会再是0了,而是ListView中一屏可以显示的子View数量,因为我们刚刚在第一次Layout过程当中向ListView添加了这么多的子View。在调用了RecycleBin的fillActiveViews()方法,这次效果可就不一样了,因为目前ListView中已经有子View了,这样所有的子View都会被缓存到RecycleBin的mActiveViews数组当中,后面将会用到它们。
  接下来将会是非常非常重要的一个操作,在调用了detachAllViewsFromParent()方法。这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。
  那么我们接着看,在if(childCount==0){}else{}判断中,会进入到else语句当中。而else语句中又有三个逻辑判断,第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断通常是成立的,因为mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中;
1-10:ListView.fillSpecific

    private View fillSpecific(int position, int top) { 
        boolean tempIsSelected = position == mSelectedPosition; 
       ... 
        final int dividerHeight = mDividerHeight; 
        if (!mStackFromBottom) { 
        ... 
             correctTooHigh(childCount); 
            } 
        } else { 
           ... 
            if (childCount > 0) { 
                 correctTooLow(childCount); 
            } 
        } 
        if (tempIsSelected) { 
            return temp; 
        } else if (above != null) { 
            return above; 
        } else { 
            return below; 
        } 
    } 
}

  fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码如下所示:
1-11:ListView.fillSpecific

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


        if (!mDataChanged) { 
            // Try to use an existing view for this position 
            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; 
            } 
        } 

        // Make a new view for this position, or convert an unused view if possible 
        child = obtainView(position, mIsScrap); 

        // This needs to be positioned and measured 
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); 

        return child; 
    }

  仍然还是在尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View.就不会再进入到obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。注意在setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的,那么我们再次回到setupChild()方法当中:
1-12:ListView.setupChild

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

        final boolean isSelected = selected && shouldShowSelector(); 
        final boolean updateChildSelected = isSelected != child.isSelected(); 
        final int mode = mTouchMode; 
        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL && 
                mMotionPosition == position; 
        final boolean updateChildPressed = isPressed != child.isPressed(); 
        final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); 
         ... 

        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter 
                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { 
            attachViewToParent(child, flowDown ? -1 : 0, p); 
        } else { 
            p.forceAdd = false; 
            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 
                p.recycledHeaderFooter = true; 
            } 
            addViewInLayout(child, flowDown ? -1 : 0, p, true); 
        } 

        }

  可以看到,setupChild()方法的最后一个参数是recycled,然后在第if((recycled && !p.forceAdd)||(p.recycledHeaderFooter&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER))else{}
行会对这个变量进行判断,由于recycled现在是true,所以会执行attachViewToParent()方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()方法。这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。那么由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()方法是正确的选择。

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

滑动加载更多数据
  经历了Layout过程,虽说我们已经可以在ListView中看到内容了,然而关于ListView最神奇的部分我们却还没有接触到,因为目前ListView中只是加载并显示了第一屏的数据而已。比如说我们的Adapter当中有1000条数据,但是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工作并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。由于滑动部分的机制是属于通用型的,即ListView和GridView都会使用同样的机制,因此这部分代码就肯定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,我们就来看一下AbsListView中的这个方法:
1-13:AbsListView.onTouchEvent

   @Override 
    public boolean onTouchEvent(MotionEvent ev) { 
        if (!isEnabled()) { 
            // A disabled view that is clickable still consumes the touch 
            // events, it just doesn't respond to them. 
            return isClickable() || isLongClickable(); 
        } 
       ... 
        initVelocityTrackerIfNotExists(); 
        final MotionEvent vtev = MotionEvent.obtain(ev); 
        final int actionMasked = ev.getActionMasked(); 
        if (actionMasked == MotionEvent.ACTION_DOWN) { 
            mNestedYOffset = 0; 
        } 
        vtev.offsetLocation(0, mNestedYOffset); 
        switch (actionMasked) { 
            case MotionEvent.ACTION_DOWN: { 
                onTouchDown(ev); 
                break; 
            } 
            case MotionEvent.ACTION_MOVE: { 
                onTouchMove(ev, vtev); 
                break; 
            } 
           ... 
            } 
            ... 
        return true; 
    }

  这个方法中的代码比较多,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作,我们就只看这部分代码就可以了。可以看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的,至于为什么那又要牵扯到另外的好几个方法,这里限于篇幅原因就不再展开讲解,喜欢寻根究底的同学们可以自己去源码里找一找原因,我们主要来分析listview。
  这样的话,代码就应该会走到scrollIfNeeded()方法,此方法当中并没有什么太多需要注意的东西,唯一一点重要的就是调用的trackMotionScroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次,代码如下所示:
1-14:AbsListView.TrackMotionScroll

 boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { 
        final int childCount = getChildCount(); 
        if (childCount == 0) { 
            return true; 
        } 
        ... 
        final boolean down = incrementalDeltaY < 0; 

        final boolean inTouchMode = isInTouchMode(); 
        if (inTouchMode) { 
            hideSelector(); 
        } 
        final int headerViewsCount = getHeaderViewsCount(); 
        final int footerViewsStart = mItemCount - getFooterViewsCount(); 
        int start = 0; 
        int count = 0; 
        if (down) { 
            int top = -incrementalDeltaY; 
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 
                top += listPadding.top; 
            } 
            for (int i = 0; i < childCount; i++) { 
              ... 
        } else { 
            ... 
        } 
        if (count > 0) { 
            detachViewsFromParent(start, count); 
            mRecycler.removeSkippedScrap(); 
        } 
        ... 
        offsetChildrenTopAndBottom(incrementalDeltaY); 
         ... 
        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); 
        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { 
            fillGap(down); 
        } 
        ... 
        return false; 
    }

  这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实我们就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了。
  如果incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动。下面将会进行一个边界值检测的过程,可以看到,从if(down){}开始,当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,如果该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,所以会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。那么如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。
  如果if(count>0){detachViewsFromParent(start, count);},会根据当前计数器的值来进行一个detach操作,它的作用就是把所有移出屏幕的子View全部detach掉,在ListView的概念当中,所有看不到的View就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证ListView的高性能和高效率。接着在调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY作为参数传入,这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此我们就可以猜出fillGap()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧:
1-14:abstract-AbsListView.fillGap

  /** 
     * Fills the gap left open by a touch-scroll. During a touch scroll, children that 
     * remain on screen are shifted and the other ones are discarded. The role of this 
     * method is to fill the gap thus created by performing a partial layout in the 
     * empty space. 
     * 
     * @param down true if the scroll is going down, false if it is going up 
     */ 
    abstract void fillGap(boolean down);

  OK,AbsListView中的fillGap()是一个抽象方法,那么我们到ListView查看,是否是在listview中实现呢?回到ListView当中,fillGap()方法的代码如下所示:

 @Override 
    void fillGap(boolean down) { 
        final int count = getChildCount(); 
        if (down) { 
            int paddingTop = 0; 
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 
                paddingTop = getListPaddingTop(); 
            } 
            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight : 
                    paddingTop; 
            fillDown(mFirstPosition + count, startOffset); 
            correctTooHigh(getChildCount()); 
        } else { 
            int paddingBottom = 0; 
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 
                paddingBottom = getListPaddingBottom(); 
            } 
            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight : 
                    getHeight() - paddingBottom; 
            fillUp(mFirstPosition - 1, startOffset); 
            correctTooLow(getChildCount()); 
        } 
    }

  down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:
1-16:ListView.makeAndView

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


        if (!mDataChanged) { 
            // Try to use an existing view for this position 
            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; 
            } 
        } 

        // Make a new view for this position, or convert an unused view if possible 
        child = obtainView(position, mIsScrap); 

        // This needs to be positioned and measured 
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); 

        return child; 
    }

  这里首先仍然是会尝试调用 RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值是null。
既然getActiveView()方法返回的值是null,那么就还是会走到obtainView()方法当中,代码如下所示:
1-17:AbsListView.obtainView

 View obtainView(int position, boolean[] isScrap) { 
       ... 
            isScrap[0] = true; 
        ... 
        final View scrapView = mRecycler.getScrapView(position); 
        final View child = mAdapter.getView(position, scrapView, this); 
        if (scrapView != null) { 
            if (child != scrapView) { 
                // Failed to re-bind the data, return scrap to the heap. 
                mRecycler.addScrapView(scrapView, position); 
            } else { 
                isScrap[0] = true; 

                // Finish the temporary detach started in addScrapView(). 
                child.dispatchFinishTemporaryDetach(); 
            } 
        } 

        if (mCacheColorHint != 0) { 
            child.setDrawingCacheBackgroundColor(mCacheColorHint); 
        } 
       ... 
        return child; 
    }

  这里会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。那么另外还有一点是需要大家留意的,这里获取到了一个scrapView,getView(position, scrapView, this)是将它作为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?我们再次看一下一个简单的getView()方法示例:
1-18:getView

    public View getView(int position,
                View convertView,ViewGroup parent){
         Fruit fruit = getItem(position);  
         View view;  
      if (convertView == null) {  
        view = LayoutInflater.from(getContext()).inflate(resourceId, null);  
       } else {  
        view = convertView;  
       }  
       ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);  
       TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);  
       fruitImage.setImageResource(fruit.getImageId());  
       fruitName.setText(fruit.getName());  
       return view; 
}

  第二个参数就是我们最熟悉的convertView呀,平时我们在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView,因为convertView就是我们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这些道理你是不是已经完全搞明白了?之后的代码又都是我们熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中,因为缓存中的View也是之前从ListView中detach掉的,这部分代码就不再重复进行分析了。
为了方便大家理解,图7:       
        这里写图片描述
                      图7
HeaderView和 FooterView
  到这里我们在说一说addHeaderView和addFooterView这两个方法,这两个方法也是在listview中掉用,它的主要作用是在listview的头部和尾部可以用来添加 ,在android对列表分组显示的需求等进行使用;
1-19:ListView.addHeaderView

    public void addHeaderView(View v, Object data, boolean isSelectable) { 
        final FixedViewInfo info = new FixedViewInfo(); 
        info.view = v; 
        info.data = data; 
        info.isSelectable = isSelectable; 
        mHeaderViewInfos.add(info); 
        mAreAllItemsSelectable &= isSelectable; 
        // Wrap the adapter if it wasn't already wrapped. 
        if (mAdapter != null) { 
            if (!(mAdapter instanceof HeaderViewListAdapter)) { 
                mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter); 
            } 
            // In the case of re-adding a header view, or adding one later on, 
            // we need to notify the observer. 
            if (mDataSetObserver != null) { 
                mDataSetObserver.onChanged(); 
            } 
        } 
    }
public void addfooterView(View v, Object data, boolean isSelectable) {
      ...
}

  在代码中使用 listView .addHeaderView() 方法可以在ListView组件上方添加上其他组件,并且连结在一起像是一个新组件。如果多次使用 .addHeaderView() ,则最先添加的组件在最上方,按添加的先后顺序由上到下;图8

listView.addHeaderView(view1)
listView.addHeaderView(view2)                                                                            listView.addHeaderView(view3)

这里写图片描述
   图8
当listview需要添加headerview时,可以通过调用listview的addHeaderView(headView, null, false) 方法,该方法还有一个重载方法 addHeaderView(headView);这两个方法的区别是前一个方法可以控制header是否可以被selected,如果不想被selected则将第三个参数设置成false;
  addHeaderView方法必须放在listview.setadapter前面,给listview添加头部必须在绑定adapter前添加,否则会报错。原因是当我们在调用setAdapter方法时Android会判断当前listview是否已经添加header,如果已经添加则会生成一个新的tempadapter,这个新的tempadapter包含我们设置的adapter所有内容以及listview的header和footer。所以当我们在给listview添加了header后在程序中调用listview.getadapter时返回的是tempadapter而不是我们通过setadapter传进去的adapter。如果没有设置adapter则tempadapter与我们自己的adapter是一样的。
  listview.getadapter().getcount()方法返回值会比我们预期的要大,原因是添加了header。我们自定义adapter里面的getitem方法里面返回的position是不包括header的,是我们自定义adapter中数据position编号从0开始,也就是说与我们传进去的list的位置是一样的。

  而Activity中listview的onitemclick方法:public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,long arg3)arg2是当前click的位置,这个位置是指在tempadapter中的位置,从0开始如果listview中添加了header则0代表header。

  listView 还没有setAdapter时,headerView是没有没显示的:我之前自己做了一个关于addheaderview的需求的时候遇到过类似的问题: 我要根据业务来显示或者移除headerview,如果 你已经setadapter了,那么你是无法再去添加headview的它会报错:adapter本来就是用来绑定数据的,你都还没有setadapter,你加一个headerView就没有任何意义了; 
  
 这里写图片描述

HeaderViewListAdapter
  在这里我也给大家描述一下HeaderViewListAdapter:类名有点不确切,是支持Header和Footter的,HeaderViewListAdapter的主要作用就是在ListAdapter基础上封装和升级,为其提供了添加列表头和列表尾的功能。在自己的代码中是不会直接使用该类的。
1-20:HeaderViewListAdapter(1)

public class HeaderViewListAdapter implements WrapperListAdapter, Filterable { 
    ... 
    public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos, 
                                 ArrayList<ListView.FixedViewInfo> footerViewInfos, 
                                 ListAdapter adapter) {
   …
mAdapter = adapter; 
        mIsFilterable = adapter instanceof Filterable; 

        if (headerViewInfos == null) { 
            mHeaderViewInfos = EMPTY_INFO_LIST; 
        } else { 
            mHeaderViewInfos = headerViewInfos; 
        } 

        if (footerViewInfos == null) { 
            mFooterViewInfos = EMPTY_INFO_LIST; 
        } else { 
            mFooterViewInfos = footerViewInfos; 
        } 

        mAreAllFixedViewsSelectable = 
                areAllListInfosSelectable(mHeaderViewInfos) 
                && areAllListInfosSelectable(mFooterViewInfos)

    }

HeaderViewListAdapter(ArrayList

public class HeaderViewListAdapter implements WrapperListAdapter, Filterable { 
  ... 
  public boolean removeHeader(View v) { 
        for (int i = 0; i < mHeaderViewInfos.size(); i++) { 
           ...
        } 

        return false; 
    } 

    public boolean removeFooter(View v) { 
        for (int i = 0; i < mFooterViewInfos.size(); i++) { 
           ...
        } 

        return false; 
    }
    }

  我们来看一下removeHeader()和removeFooter()应该注意的地方;removeHeader()用来删除标题头;removeFooter()用来删我们通常在加载数据时,为了省流量不会一次性把数据全部下完,一般是分段下载。分段下载一般会在listview最后面放一个进度条表示正在加载数据,当数据加载完时,我们又要清除它。
mLoadingLayout = (FrameLayout) View.inflate(this, R.layout.load, null);
listView.addFooterView(mLoadingLayout);
listView.requestFocus();
  这是listview尾部添加一个进度条。listView.removeFooterView(mLoadingLayout);这是移除尾部的进度条。有时候在移除时回报空指针但listview不为null ,mLoadingLayout也不为null,但还是报空指针,原因是因为listview要分为三部分。一是头部,二是中间部,三是尾部。在设置了头部或尾部时,必须要有中间部才能真正意义上的生效。没生效就去移除就会报空指针错误所以在listView.removeFooterView(mLoadingLayout);时必须先调用 listView.setAdapter(adapter);(设置中间部)adapter可以数据可以为0但不可为null;

public class HeaderViewListAdapter implements WrapperListAdapter, Filterable { 
    ... 
   public int getCount() { 
        if (mAdapter != null) { 
            return getFootersCount() + getHeadersCount() + mAdapter.getCount(); 
        } else { 
            return getFootersCount() + getHeadersCount(); 
        } 
    }
    ...
    public int getHeadersCount() { 
        return mHeaderViewInfos.size(); 
    } 
    public int getFootersCount() { 
        return mFooterViewInfos.size(); 
    }
   ...

    }

  看到这里就算差不多了,对于getHeadersCount()这个方法可以支持多个标题栏;getFootersCount()这个方法支持多个位注栏,getCount获取adaper数量等一下方法就不一一详细说了;这些比较容易理解,有兴趣的同学可以自己进HeaderViewListAdapter去查看;

             勤奋思学习的枝叶,当然很苦,智慧是学习的花朵,当然香郁;

终于写完了…

本文来自:CSDN博客

感谢作者:xiao_yuanjl

查看原文:android ListViev 详解

76 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet