【进阶android】ListView源码分析——ListView的重用视图机制

来源:互联网 发布:mac虚拟机关闭创建快照 编辑:程序博客网 时间:2024/05/12 03:40

        在上一篇文章【进阶android】ListView源码分析——子视图的七种填充方式之中,我们重点分析了ListView众多填充方式;而在这些众多填充方式之中,我们频繁的使用了makeAndAddView方法。

        makeAndAddView方法,根据其方法名,我们便可知道该方法拥有两部分功能,其中一个功能是make View,而另一个功能是add View;当然这个View是指ListView中的子视图。

       ListView如何make子视图呢?其实现机制算是ListView最经典的一个特点了,即ListView子视图重用机制。ListView之中的重用机制主要是通过其父类AbsListView中的一个内部类RecycleBin来封装实现的。至于add子视图这一方面,则需要结合上一篇文章进阶android】ListView源码分析——子视图的七种填充方式来结合分析:如果说ListView之中众多填充方式定义了以何种方式来填充子视图(是从上而下填充?还是根据某一指定的子视图从上从下同时填充?),那么makeAndView方法中的add View功能则是真正的将子视图附加到父视图之中。

      根据上面所述,我们打算按照以下两点来分析ListView的视图重用机制:

      1、着重分析RecycleBin类;梳理清除ListView是如何定义重用视图机制;

      2、结合RecycleBing类,分析makeAndAddView方法具体的执行流程;

      3、根据上面所述,可知makeAndAddView拥有两个功能,其中生成子视图是由方法obtainView负责;添加子视图的功能是由setupChild方法来负责;因此第三步则分析obtainView方法的执行流程;

      4、分析setupChild方法的执行流程。

一、RecycleBin类

      RecycleBin类是AbsListView类定义的一个内部类。其作用是推进AbsListView对象的视图重机制,总体而言RecycleBin就像一个视图池的集合,在这个集合里一共有4种所示图池。

  • 活动视图池:mActiveViews,View[ ],该数组中的元素表示布局之前位于可见屏幕之上无需重新绑定数据的视图;展示布局前屏幕的信息;
  • 废弃视图池:mScrapViews,ArrayList<View>[ ],二维数组,其中每一个元素表示某一中视图类型对应的废弃视图池;而废弃视图则表示可被适配器重用而无需重新新建视图的视图(但需要重新绑定数据)。废弃视图池中的视图并没有真正被废弃。
  • 临时视图池:mTransientStateViews,SparseArray<View>;mTransientStateViewsById,LongSparseArray<View>;通过两个数据结构来保存具有临时状态的视图;一般而言,处于动画状态之中的View具有临时状态。之所以要使用两种数据结构,是因为前者主要针对非稳定的适配器,而后者主要针对稳定适配器;BaseAdapter是一个非稳定适配器;一个适配器是稳定的,表示这个适配器中的数据无论如何变化,一个行ID终究映射一个相同的item;
  • 删除视图池:mSkippedScrap,ArrayList<String>,该池中保存的视图都是需要真正的从RecycleBin视图池集合之中删除的视图。

       RecycleBin视图池集合中,活动视图池和废弃视图池是主要实现ListView视图重用的两个视图池;在开始布局的时候,如果ListView的数据未改变则直接将屏幕上显示的所有子视图移动进活动视图池中;如果ListView的数据已经改变,则直接将屏幕之中所有显示的子视图全部移进废弃视图池之中;随即ListView则开始布局操作,如果数据未改变则冲活动视图池中重用视图,反之则从废弃视图池中重用视图。而布局结束后,则检测活动废弃池中是否还存在未重用的子视图,如果存在,这些未重用的子视图全部降级为废弃视图丢进废弃视图池之中。

       活动视图池与废弃视图池相比较,两者都是实现视图重用的主要视图池;然而两者也有一个极为明显的不同:活动视图池表示视图对应的item并未发生改变,无需调用适配器的getView方法来重新将视图和item绑定在一起;而废弃视图池则刚好相反,它其中的视图表示视图对应的item已经改变,如果要重用,则必须调用适配器的getView来重新绑定视图与item。

       结合上述的分析,我们可用将ListView之中所有的子视图按照其生命周期分为五类:显示、活跃、废弃、临时以及丢弃;这5类不同状态的子视图的转换规则如下:

       1、ListView正在展示于屏幕之上的视图;这是所有ListView所有子视图最开始的状态;

       2、当ListView需要重新布局时,如果列表的数据未改变,则子视图由显示状态转换为活跃状态;

       3、当ListView需要重新布局时,并且列表的数据发生了改变,则子视图则由显示状态转换为废弃状态;

       4、布局过程之中,ListView又会根据数据是否发生改变,分别将活跃状态与废弃状态的视图,转换为显示状态;

       5、布局后,如果RecycleBin视图池集合之中还存在活跃状态的视图,则将这些视图全部转换为废弃状态;

       6、任何视图(包括)在转化为废弃状态的视图的过程中,如果活跃视图存在临时状态,且当前数据未改变,则最终转化为临时状态的视图;如果存在临时状态,且数据已经改变则最终转化为丢弃状态的视图。

       由此,我们可以看出ListView中的测量、布局过程中对子视图的生命周期有着强烈的影响,同时这些状态之间的转换也是由RecycleBin类封装成一个个内部的方法,如此我们将以ListView的测量布局过程作为分析ReCycleBin视图池集合中相应方法顺序。

1.1测量过程中调用的方法

       根据【进阶android】ListView源码分析——布局三大方法一文,当ListView在进行测量时,会获取一个子视图来获取子视图的宽度和高度,而此处子视图的获取则需要RecycleBin类进行相应的配合,从视图池中集合获取一个子视图,当这个子视图使用完毕,还需将此子视图还给视图池集合中相应的位置。

       从RecycleBin之中获取子视图的过程中,如果视图池之中没有相应的子视图,则会返回nulll;此时,则会通过适配器的getView方法来生成一个子视图;在使用完毕此子视图后,则将该子视图放入RecycleBin之中。

       获取与还予两个动作皆由Recycle类中的getScrapView方法与addScrapView方法来处理;我们先分析从废弃视图池之中获取废弃视图。

获取废弃视图——getScrapView方法

       在测量过错之中获取子视图,会通过getScrapView获取子视图,如果废弃视图池中物相应的子视图,则返回null。

       同时根据上文的分析,Recycle类中的废弃视图池是一个二维数组,该数组按照不同的视图类型对所有的废弃视图进行分类,每一类的视图类型对应的废弃视图池就是这个二维数组的一个元素。

      而getScrapView则根据需要哪个位置的子视图,来判断该位置的视图的类型,根据视图类型找到对应的废弃视图池,从此废弃视图池中找到一个废弃视图作为一个子视图返回。该方法的源码如下:

View getScrapView(int position) {            if (mViewTypeCount == 1) {                return retrieveFromScrap(mCurrentScrap, position);            } else {            //通过适配器getItemViewType的方法找到指定位置对应的视图类型                int whichScrap = mAdapter.getItemViewType(position);                if (whichScrap >= 0 && whichScrap < mScrapViews.length) {                    return retrieveFromScrap(mScrapViews[whichScrap], position);                }            }            return null;//如果废弃视图池中无对应的视图,直接返回true        }
       从源码中可以看出,getScrapView的主要目的还是寻出指定位置对应的视图对应的视图类型。而retrieveFromScrap方法则是通过指定废弃视图池找寻到合适的废弃视图用以重用。

      我们继续分析retrieveFromScrap方法;retrieveFromScrap方法是AbsListView类的静态方法,源代码如下:

static View retrieveFromScrap(ArrayList<View> scrapViews, int position) {        int size = scrapViews.size();        if (size > 0) {            // See if we still have a view for this position.            for (int i=0; i<size; i++) {                View view = scrapViews.get(i);                if (((AbsListView.LayoutParams)view.getLayoutParams())                        .scrappedFromPosition == position) {//根据废弃位置来匹配视图                    scrapViews.remove(i);                    return view;                }            }            return scrapViews.remove(size - 1);//返回最后一个视图        } else {            return null;        }    }
       AbsListView.LayoutParams继承与ViewGroup.LayoutParams,与父类相比,AbsListView.LayoutParams多了一些特属于ListView的属性,这些属性用以标识ListView子视图的一些公有属性;其中一个比较重要的属性为scrappedFromPosition,根据属性名,可用先推测出此属性是表示该视图是从那一位置被废弃,即上一次使用该视图的位置。

      如果指定的位置(第二个入参)不存在视图,则返回废弃视图池最后一个视图。

还予废弃视图——addScrapView方法

       addScrapView方法是将指定视图放入其对应类型的废弃视图池之中;需要放入废弃视图池中的视图,可以是显示状态的视图,临时状态的视图以及废弃状态的视图三种类型的视图。同时,该方法不仅仅只是会将视图放入废弃视图池中,如果视图的状态为临时状态,则会直接放入对应的临时视图池或删除视图池中。

      该方法的源码如下:

 void addScrapView(View scrap, int position) {            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();            if (lp == null) {//如果需要废弃的子视图没有布局参数则直接返回                return;            }            //保存废弃前使用该需要废弃的子视图的位置            lp.scrappedFromPosition = position;            // Remove but don't scrap header or footer views, or views that            // should otherwise not be recycled.            final int viewType = lp.viewType;            if (!shouldRecycleViewType(viewType)) {                return;            }             ......            // Don't scrap views that have transient state.            //不要将具有临时状态的视图放入废弃视图池之中            final boolean scrapHasTransientState = scrap.hasTransientState();            if (scrapHasTransientState) {//处于临时状态中的视图                if (mAdapter != null && mAdapterHasStableIds) {                    // If the adapter has stable IDs, we can reuse the view for                    // the same data.                //如果当前适配器具有稳定行ID,则将具有临时状态                //的视图放入mTransientStateViewsById之中                    if (mTransientStateViewsById == null) {                        mTransientStateViewsById = new LongSparseArray<View>();                    }                    mTransientStateViewsById.put(lp.itemId, scrap);                } else if (!mDataChanged) {                    // If the data hasn't changed, we can reuse the views at                    // their old positions.                //如果当前适配器为空或者不是一个稳定行ID的适配器,                //但是数据未改变,则将具有临时状态的视图放入                //mTransientStateViews之中                    if (mTransientStateViews == null) {                        mTransientStateViews = new SparseArray<View>();                    }                    mTransientStateViews.put(position, scrap);                } else {                    //具有临时状态的视图,且数据已经改变的情况下,需要将该视图添加到mIsScrap之中                    // Otherwise, we'll have to remove the view and start over.                    if (mSkippedScrap == null) {                        mSkippedScrap = new ArrayList<View>();                    }                    mSkippedScrap.add(scrap);                }            } else {//非临时状态的视图                if (mViewTypeCount == 1) {                    mCurrentScrap.add(scrap);                } else {                    mScrapViews[viewType].add(scrap);                }                .....            }        }
        addScrapView方法有两个参数:

  • scrap  需要废弃(或者丢弃)的视图;
  • position 第一个入参废弃前,在ListView之中所处的位置。

       addScrapView方法将scrap分为两类,这两类按照不同的处理方式进行处理;这两类分别为:具有临时状态的scrap和非具有临时状态的scrap。

       具有临时状态的视图,无论数据是否改变,只要存在一个稳定行ID的适配器,则统统将临时状态的视图放入mTransientStateViewsById对应的临时视图池中;如果不存在一个稳定行ID的适配器(或者适配器为空),则需要考虑数据是否改变,如果未改变则将此临时状态放入mTransientStateViews对应的临时视图池中,反之则直接丢入删除视图池中(一般而言,ListView在布局过程中会统一删除删除视图池中所有的内容)。

      不具有临时状态的视图,则直接添加进相应视图类型的废弃视图池中。

1.2布局前调用的方法

      布局前调用的方法是指ListView类中layoutChildren方法中进行子视图填充之前,所处理的流程。

     此流程中,主要涉及到RecycleBin类的局部流程是:布局时,会先判断ListView的数据是否改变,如果改变,则将当前所有显示在屏幕上的视图添加到废弃视图池中(调用addScrapView方法),而这一步也体现了显示状态的视图转化为废弃视图(甚至转换为临时、丢弃状态的匪徒)。

     如果数据未改变,则将当前所有显示则屏幕上的视图添加到活跃视图池中,而这一步也体现了显示状态的视图转化为活跃状态的视图,这一步调用的是RecycleBin类中的fillActiveViews方法。

    从以上分析,也可与侧面推出活跃状态的视图与废弃状态的视图的区别:活跃视图无需重新绑定数据,因为数据未改变,废弃视图需要重新绑定数据,因为数据已经改变。

    执行完显示状态的视图应该转换为何种状态的视图后,会调用RecycleBin类的removeSkippedScrap方法,统一删除删除视图池中的所有内容

    对于addScrapView方法,在1.1.2节已有详细的分析,对于removeSkippedScrap方法,其实现逻辑极为简单,就不特意分析;下面我们具体看看fillActivityView方法。

    fillActiveView方法主要的目的将当前屏幕上所有的视图转换为活跃状态的视图;ListView中layoutChildren方法中调用fillActiveView方法的代码如下:

recycleBin.fillActiveViews(childCount, firstPosition);
     childCount表示当前屏幕上的子视图,firstPosition表示当前屏幕上第一个子视图对应的item在适配器中的位置。

     下面fillActive源码如下:

void fillActiveViews(int childCount, int firstActivePosition) {            if (mActiveViews.length < childCount) {            //如果当前屏幕的子视图数量大于活跃视图池的数量,            //扩充活跃视图池                mActiveViews = new View[childCount];            }            mFirstActivePosition = firstActivePosition;            //noinspection MismatchedReadAndWriteOfArray            final View[] activeViews = mActiveViews;            for (int i = 0; i < childCount; i++) {                View child = getChildAt(i);//获取当前子视图                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();                // Don't put header or footer views into the scrap heap                //不要将页眉页脚视图放入活跃视图池之中                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {                    // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.                    //        However, we will NOT place them into scrap views.                    activeViews[i] = child;                }            }        }
       该方法的逻辑并不复杂,它将当前所有的子视图,按照该子视图在ListView之中的位置从小到大放入活跃视图池中,即子视图在ListView之中的下标,与其缓存在活跃视图池中的下标一致。

1.3布局中调用的方法,即makeAndAddView方法之中调用的方法

      根据前文可知,RecycleBin类中活跃状态的视图和废弃状态的视图有一个最明显的区别:前者表示的视图一般不需要重新绑定数据,而后者表示的视图一般需要重新绑定数据,而触发是否绑定数据的机制则是根据适配器判断是否发生了数据改变。

      而则makeAndAddView方法之中,首选判断ListView的数据是否发生改变;如果未发生改变,则将活跃状态的视图转换为显示状态的视图。

      而将活跃视图转换为显示状态的视图,是调用RecycleBin类的getActiveView(int position)方法;该方法的逻辑十分简单,直接根据入参获取RecycleBin类中的活跃视图池中对应下标的活跃状态视图。

      如果ListView的数据发生改变,则又会根据指定的位置是否处于临时状态(即该位置在发生动画等),如果处于临时状态则从临时视图池中获取相应的临时视图;如果不出与临时状态,则直接从废弃视图池中获取废弃状态的视图。这两种情况下,无论是那种状态的视图都需要重新调用适配器的getView方法来进行重新数据绑定。从临时视图池中获取视图是调用RecycleBin.getTransientStateView(int position)方法来获取;后者则是通过getScrapView方法来获取。这两个方法的实现逻辑相似,且在前面已经结合getScrapView方法的原来来梳理了逻辑,所以此处就不在累述。

1.4布局后调用的方法

       布局之前,需要将所有显示状态的视图丢入重用视图池之中(无论是放入活跃视图池中还是废弃视图池集合之中);布局之中,从重用池中获取相应的视图,将其转换为显示状态视图;而布局之后,则主要处理活跃状态池中未在布局之中被重用的视图,因为这一次布局后,这些余下的活跃视图(如果存在)没有被重用,那么在下一次布局时,这些活跃视图肯定需要重新绑定数据,所以将这些余下的活跃视图全部转换为废弃状态的视图。

      将活跃状态的视图转换为废弃状态的视图主要是通过RecycleBin类中的scrapActiveViews()方法。

      该方法的源代码如下:

void scrapActiveViews() {            final View[] activeViews = mActiveViews;//活跃视图池            final boolean hasListener = mRecyclerListener != null;            final boolean multipleScraps = mViewTypeCount > 1;            ArrayList<View> scrapViews = mCurrentScrap;            final int count = activeViews.length;            for (int i = count - 1; i >= 0; i--) {                final View victim = activeViews[i];                if (victim != null) {//如果该位置的活跃视图未被重用                    final AbsListView.LayoutParams lp                            = (AbsListView.LayoutParams) victim.getLayoutParams();                    final int whichScrap = lp.viewType;                    activeViews[i] = null;//从活跃视图池中删除该视图                    if (victim.hasTransientState()) {//具有临时状态                        // Store views with transient state for later use.                        victim.dispatchStartTemporaryDetach();                        if (mAdapter != null && mAdapterHasStableIds) {//适配器具有稳定行ID                            if (mTransientStateViewsById == null) {                                mTransientStateViewsById = new LongSparseArray<View>();                            }                            long id = mAdapter.getItemId(mFirstActivePosition + i);                            mTransientStateViewsById.put(id, victim);                        } else if (!mDataChanged) {//不具有稳定行ID,但是数据未改变                            if (mTransientStateViews == null) {                                mTransientStateViews = new SparseArray<View>();                            }                            mTransientStateViews.put(mFirstActivePosition + i, victim);                        } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {//不具有稳定行ID,且数据已改变,且视图不为页眉、页脚视图                            // The data has changed, we can't keep this view.                            removeDetachedView(victim, false);//从ViewGroup中删除该子视图                        }                    } else if (!shouldRecycleViewType(whichScrap)) {//不具有临时状态,且该视图类型不应被回收                        // Discard non-recyclable views except headers/footers.                        if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {                            removeDetachedView(victim, false);                        }                    } else {//不具有临时状态,且该视图类型应该被回收                        // Store everything else on the appropriate scrap heap.                        if (multipleScraps) {                            scrapViews = mScrapViews[whichScrap];//视图类型对应的废弃视图池                        }                        ......                        lp.scrappedFromPosition = mFirstActivePosition + i;//记录是从哪个位置丢弃的                        scrapViews.add(victim);                        ......                    }                }            }            ......        }
           该方法通过遍历活跃视图池,来收寻未被重用的活跃状态的视图,对于未被使用的活跃状态的视图,判断其是否具有临时状态,如果具有临时状态,则根据是否具有稳定的行ID以及数据是否改变,来分别将活跃状态视图放入临时视图池中或者直接从父视图之中删除;如果不具有临时状态,则判断该类型的视图是否应该回收,来将该视图放入废弃视图池中,或者直接从父视图之中删除该子视图。

      该逻辑与addScrapView方法相似;只是addScrapView在处理临时状态视图且数据已经改变这种情况的时候,会将视图放入丢弃视图池中缓存一下,待下次布局时在一次性清空丢弃视图池;而scrapActiveViews则直接清空该情况下对应的视图(从父视图之中删除对应子视图)。

      至此,RecycleBin类的主要逻辑就梳理清楚了!该重用视图池集合中,主要封装了四种优先级的视图池;而所提供的方法也主要是用于这四种状态的视图之间的相互转换!而这种转换与ListView的布局又有着极为紧密的关系。

     在清楚ListView的视图来源之后(如何获取子视图),我们下面接着分析ListView是如何使用这种重用机制以及每一个子视图具体的定位于ListView之中。

二、makeAndAddView方法的流程

        makeAndAddView方法的逻辑十分简单,根据函数名就可以知道,该方法获取一个子视图,再将子视图添加到ListView之中。其中添加ListView主要是通过setupChild方法来设置,而获取子视图则是根据数据有无改变来判断获取子视图的方法。如果数据没有改变,则不需重新绑定子视图和对应的item数据,直接根据RecycleBin类中的活跃视图池,来进行视图重用,如果数据改变,则从RecycleBin类中的废弃视图池数组或临时视图池之中获取。

       该方法代码如下:

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;    }

      该方法的5个入参含义如下:

  • position:获取的子视图对应的item在适配器数据集中的位置;
  • y:获取的子视图的顶部Y值或者底部Y值,是顶部Y值还算底部Y值是由第三个入参决定;
  • flow:true表示顶部Y值,false表示底部Y值;
  • childrenLeft:获取的子视图左边的X值;
  • selected:获取的子视图是否是一个被选择的视图。

       一般而言,ListView主要则进行子视图填充时,使用此方法较多!尤其是,上一篇文章【进阶android】ListView源码分析——子视图的七种填充方式介绍的方法,这些方法与makeAndAddView相比,前者主要侧重与填充的方式,后者侧重与具体如何获取一个子视图,并将子视图如何添加到ListView之中。

      根据makeAndAddView方法中的源码可知,获取子视图主要通过两个方式:一个是调用RecycleBin对象的getActiveView方法,则布局之前,会将所有显示的子视图全部添加到活跃视图池之中,而在此处则从此活跃视图池中重用视图,由于本文前面有介绍过getActiveView方法的实现,所以此处便不再累述;另一种方式则调用obtainView方法。

三、obtainView方法的流程

        如果ListView的数据已经改变,或者RecycleBin对象的活跃视图池无当前位置的重用视图,则通过obtainView方法来获取子视图。该方法生成一个子视图也是两种情况:要么是重新初始化一个子视图,要么从视图池中重用一个子视图(此方法所涉及的视图池不包含活跃视图池和丢弃视图池)。

       该方法的源码如下:

View obtainView(int position, boolean[] isScrap) {        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");        isScrap[0] = false;//mIsScrap数组的第一个元素被设置为false        // Check whether we have a transient state view. Attempt to re-bind the        // data and discard the view if we fail.        //检查我们是否有一个临时的视图,用来重新绑定视图;如果绑定失败,我们将丢弃该视图        final View transientView = mRecycler.getTransientStateView(position);        if (transientView != null) {            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();            // If the view type hasn't changed, attempt to re-bind the data.            //如果类型一样则重新绑定数据            if (params.viewType == mAdapter.getItemViewType(position)) {                final View updatedView = mAdapter.getView(position, transientView, this);                // If we failed to re-bind the data, scrap the obtained view.                //如果绑定失败,则将获取的视图重新丢弃到废弃视图堆之中                if (updatedView != transientView) {                    mRecycler.addScrapView(updatedView, position);                }            }            // Scrap view implies temporary detachment.            isScrap[0] = true;            return transientView;        }        //调用getView方法        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.                            //绑定失败从新废弃堆之中,例如没有使用ViewHolder机制               mRecycler.addScrapView(scrapView, position);            } else {                isScrap[0] = true;//子视图来至于重用池,所以数组的第一个元素设置为true                // Clear any system-managed transient state so that we can                // recycle this view and bind it to different data.                //删除一切临时状态,从而我们能够回收这个视图,并能将其绑定到不同的数据                ......                child.dispatchFinishTemporaryDetach();            }        }        ......        return child;    }
        在具体分析方法的源码之前,我们先看看第二个入参:isScrap,该参数是一个boolean数组,在ListView之中,一般这个数组只有一个元素;如果obtainView方法中是从重用视图池中获取的视图,则该数组的第一个元素则被设置为true,否则第一个元素则为false。

        在obtainView方法源码之中,首选查询指定位置(第一个入参)是否存在临时视图,即临时视图池中是否在指定位置上存在视图,如果临时视图存在,则将此子视图提取出来,并且调用适配器的getView方法,将该子视图与数据重新绑定起来,最后再将获取的具有临时状态的子视图返回。

       如果没有临时视图,则调用RecycleBin类的getScrapView方法,从废弃视图池中获取一个子视图。注意此处的视图池,只表示废弃视图池数组,即该废弃视图池数组中每一个子视图都是废弃状态(需要重新绑定数据的非临时状态视图),而获取视图之后,紧跟着调用适配器的getView方法重新绑定数据。这一步骤隐含了一种情况,即从废弃视图池中没有成功获取到子视图(即getScrapView的返回值为空),这种情况就包含了第一次加载ListView时,调用适配器的getView方法这种情景,可以结合ViewHolder机制来分析这种情况。

       总体而言,obtainView之中生成子视图一共有三种方式:重用临时视图池中相应位置的视图,重用废弃视图池数组中相应视图类型及位置的视图以及通过适配器getView方法直接初始化一个子视图(这一方式可以结合平时如何实现getView方法来理解)。

      而结合makeAndAddView方法,则可知ListView的子视图有四种来源:如果ListView的数据未改变,则从RecycleBin类中的活跃视图池中直接获取;如果指定位置在临时视图池中存在对应的临时视图,则获取临时视图并重新绑定数据;如果指定位置在对应的视图类型中的废弃视图池中存在废弃视图,则获取废弃视图并重新绑定数据;如果以上三种类型的视图池都无可用子视图,则根据适配器getView的方法来初始化一个新的子视图。

         以上我们便大致分析了ListView子视图如何生成,也就是makeAndAddView方法的第一个功能,下面我们接着分析makeAndView方法的第二个功能:将生成的子视图添加到ListView之中。即setupChild方法。

四、setupChild方法的流程

         setupChild方法主要是将指定的子视图添加到ListView之中,并且确保该子视图能够正确的在ListView之中定位,当然如果需要,还要重新测量子视图的宽度与高度。

         setupChild方法的方法声明如下:

 private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,boolean selected, boolean recycled)

        该方法一共有7个方法:其中第2、3、4、5、6个入参的含义与makeAndAddView方法的五个参数的含义一样;而第一个参数则表示需要添加到ListView之中的子视图,而最后一个参数则与obtainView方法的第二个参数(也就是那个只有一个元素的boolean数组)。一般而言,ListView之中,obtainView方法与setupChild方法是成对出现的,obtainView方法确定该boolean数组的值(获取的视图来至于重用池则为true,反之为false),setupChild方法则根据该boolean数组的值来确定指定的子视图是否来至于重用池。

        方法的源码如下:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,            boolean selected, boolean recycled) {        ......        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();        // Respect layout params that are already in the view. Otherwise make some up...        // noinspection unchecked        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();        if (p == null) {            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();        }        p.viewType = mAdapter.getItemViewType(position);        //如果满足一下两种情况之一,则只是粘贴视图,而不立即重绘屏幕:        //1:视图来至于重用池且不算强制添加的;        //2:或者当视图为页脚(页眉)类型且页眉页脚视图需要回收的        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&                p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {            attachViewToParent(child, flowDown ? -1 : 0, p);//-1表示粘贴到视图尾部,0则表示粘贴到视图头部        } else {            p.forceAdd = false;            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {                p.recycledHeaderFooter = true;            }            //添加到ListView并重新布局            addViewInLayout(child, flowDown ? -1 : 0, p, true);        }        if (updateChildSelected) {            child.setSelected(isSelected);//更新选择状态        }        if (updateChildPressed) {            child.setPressed(isPressed);//更新按下状态        }        //确定当前子视图是否处于被选择的状态        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {            if (child instanceof Checkable) {                ((Checkable) child).setChecked(mCheckStates.get(position));            } else if (getContext().getApplicationInfo().targetSdkVersion                    >= android.os.Build.VERSION_CODES.HONEYCOMB) {                child.setActivated(mCheckStates.get(position));            }        }        if (needToMeasure) {//需要重新测量,则测量子视图的长宽            int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,                    mListPadding.left + mListPadding.right, p.width);            int lpHeight = p.height;            int childHeightSpec;            if (lpHeight > 0) {//如果子视图指定了高度,则完全由父视图决定                childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);            } else {//否则(无论是未制定高度,或者高度为wrap_content)都完全是由子视图自己来决定                childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);            }            child.measure(childWidthSpec, childHeightSpec);        } else {            cleanupLayoutState(child);//将子视图的强制布局flag清除        }        final int w = child.getMeasuredWidth();        final int h = child.getMeasuredHeight();        final int childTop = flowDown ? y : y - h;        if (needToMeasure) {            final int childRight = childrenLeft + w;            final int childBottom = childTop + h;            child.layout(childrenLeft, childTop, childRight, childBottom);//布局子视图        } else {//调整布局            child.offsetLeftAndRight(childrenLeft - child.getLeft());            child.offsetTopAndBottom(childTop - child.getTop());        }        ......    }
         总体而言,setupChild方法的源码主要有三个大的步骤:添加子视图、测量子视图、布局子视图;其中测量子视图和布局子视图不一定会执行。

         首先,确定四个方法变量:子视图是否被选择、子视图是否更新选择状态、子视图是否被按下、子视图是否更新按下状态;是否更新子视图状态的标准便是当前状态和子视图之前的对应的状态是否一致。

         接着,根据上一步的四个boolean变量,确定是否需要重新测量该子视图;如果子视图不是来自于重用池,或者需要更新选择状态,或者需要更新按下状态,这三种情况至少一种,则该子视图需要重新测量;

        判断该子视图是否需要重新测量后,就将该子视图添加到ListView之中(主要添加到ListView中的子视图数组之中) ;一般而言子视图如果来至于重用池(即最后一个入参的值为true),则只需调用attachViewToParent方法将子视图添加,否则则需要调用addViewInLayout方法将子视图添加。对于attachViewToParent方法和attachViewToParent方法,两者都是ViewGroup类中添加子视图到子视图数组之中的方法;两者不同的是,前者是一个轻量级的方法,一般调用者能够post一个Runnable,这个Runnable能够执行requestLayout方法,在所有的粘贴/拆开调用接收后, 再执行requestLayout方法;

        接着,根据需要判断是否需要更新子视图的选择、按下状态;然后检测当前子视图是否需要被check;ListView通过mCheckStates变量来判断当前位置是否是一个被选择的item,mCheckStates变量是一个SparseBooleanArray类型,主要存储每一个item与是否被选择的映射关系。

        最后便是调用子视图的measure和layout方法来分别对子视图进行测量和布局;当然,并不是所有的子视图都需要重新测量或者布局,不过一旦子视图一旦重新测量,那么就一定会重新布局。

      至此,我们便将setupChild方法分析完毕。

     ListView通过AbsListView.RecycleBIn类来实现视图池;视图池中有四种状态的视图,这四种状态从高到低分别为:活跃视图池、废弃视图池、临时视图池以及废弃视图池,这四种视图与当前正显示在ListView上面的视图组成了整个ListView的子视图系统。这五种状态能够彼此转换,并且大部分的转换都是在layoutChildren方法中实现。

      makeAndAddView方法,总体而言有两个功能:生成一个子视图,添加一个子视图;对于生存子视图这个功能而言,主要是根据两个准则来判断子视图的来源,数据是否改变和视图是否具有临时状态,前者主要决定子视图是来自与活跃视图池还算来自与废弃视图池,后者主要决定子视图来至于临时视图池还算来至于废弃视图池。当然,子视图如果在视图池中并未存在,那只有通过适配器的getView方法来生成一个子视图。至于添加子视图,无非便是将指定的子视图添加并布局到ListView之中;ListView中的layoutChildren方法主要针对所有的子视图这一个整体来实现的,对应所有的子视图而言,更侧重与这些子视图是从何处开始布局?以那种方向开始布局?对于makeAndAddView而言,则侧重与具体某一个子视图的布局,因为更需要子视图自身来进行布局,即调用子视图的layout方法。

       至此,我们就分析完毕ListView的重用机制;在下一篇文章中我们将分析ListView的滑动机制【进阶android】ListView源码分析——ListView的滚动机制。

     


0 0
原创粉丝点击