阅读ListView源码

郭林大神的博客 http://blog.csdn.net/guolin_blog/article/details/44996879
下面这篇博客对listView的触摸事件讲的比较细
http://www.jianshu.com/p/7f95297b6271
注:本文是以API 25的代码为准

使用ListView的时候,还有一个东西不得不提就是BaseAdapter,这有这两个东西同时使用的时候,才能显示出列表.下面我们就一起来看看ListView和BaseAdapter

BaseAdapter及其父亲和爷爷:

从图中我们可以看出BaseAdapter实现了ListAdapter和SpinnerAdapter,而ListAdapter和SpinnerAdapter又同时继承了Adapter接口,下面我们就一起看看这个几个类里面都干了些什么.

Adapter:官方文档描述->Adapter是AdapterView和数据之间的一个桥梁,Adapter提供对数据项的访问,也负责填充view.

Adapter中提供了getCount(),getView(),registerDataSetObserver()等最基本的方法.

ListAdapter:官方文档描述->ListAdapter是Adapter 的扩展,ListAdapter和ListView配合可以显示任何的数据

ListAdapter中相较于Adapter只扩展了两个方法areAllItemsEnabled()和isEnabled(position),areAllItemsEnabled()是设置所有的items都可用(点击或者选中)或者不可用(不可点击或者不可选中)(由返回值来判断),isEnabled(position)是设置指定位置的item是否可以点击或者选中(由返回值来判断)

SpinnerAdapter:官方文档描述->SpinnerAdapter是Adapter的扩展,Spinner一般显示两种视图,一种是Spinner本身的视图,一种是在他按下以后显示的列表视图.

SpinnerAdapter中相较于Adapter中只扩展了一个方法getDropDownView(int position,View convertView,ViewGroup parent)该方法和Adapter中的getView()方法效果基本是一致的,是用于创建显示在Spinner中的ui的,一般是供AbsSpinner调用. BaseAdapter中对该方法的实现就是直接返回getView()方法

BaseAdapter:官方文档描述->实现了Adapter的普通基类,可用于listView(因为实现了listAdapter)和Spanner(因为实现了SpannerAdapter)

BaseAdapter实现了Adapter ,ListAdapter,Spanner中大部分的方法比如notifyDataSetChanged(),registerDataSetObserver(),unregisterDataSetObserver().BaseAdpter中只有一个成员变量就是mDataSetObservable
它是DataSetObservable的实例
private final DataSetObservable mDataSetObservable = new DataSetObservable();

其中registerDataSetObserver()(设置数据变化监听)就是将传过来的DataSetObserver设置给了DataSetObserveable.

 public void registerDataSetObserver(DataSetObserver observer) {
         mDataSetObservable.registerObserver(observer);
     }

registerDataSetObserver()这个方法回到我们给listView设置adapter的时候调用

 @Override
public void setAdapter(ListAdapter adapter) {
    if (mAdapter != null && mDataSetObserver != null) {
        mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }
********
     mDataSetObserver = new AdapterDataSetObserver();
     mAdapter.registerDataSetObserver(mDataSetObserver);   
     mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
 }

unregisterDataSetObserver()(取消数据变化监听)也是调用DataSetObserveable的取消监听的方法

public void unregisterDataSetObserver(DataSetObserver observer) {
    mDataSetObservable.unregisterObserver(observer);
}

notifyDataSetChanged()这是我们经常使用的方法,是用来进行刷新列表的他其实也是调用DataSetObserveable中的notifyChanged()方法来出发数据变化的

public void notifyDataSetChanged() {
    mDataSetObservable.notifyChanged();
}

从上面这三个方法分析得出,其实DataSetObservable才是真正监听数据变化的类


再说ListView之前我们先说说ListView中非常重要的缓存机制RecycleBin

RecycleBin

这里的RecycleBin代码并不全,我只是把最主要的几个方法提了出来。那么我们先来对这几个方法进行简单解读,这对后面分析ListView的工作原理将会有很大的帮助。

  • private View[] mActiveViews = new View[0];-->mActiveViews 中保存的是从第一个可见的item开始的连续的视图范围,当布局结束时mActiveViews中的view将会转移到mScrapViews中

  • private ArrayList<View>[] mScrapViews;-->mScrapViews中保存的是废弃的view,比如说是移出屏幕的view

  • private ArrayList<View> mCurrentScrap;-->和mScrapViews一样也是用来保存废弃view的

  • 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当中有这样一个功能就行了。

ListView

listView继承体系.png

从上图可以清晰的看出ListView的继承体系下面我们就对这些类做一些大致的了解

AbsListView官方文档描述->它是可用于实现虚拟化列表的基类,这个列表没有特殊的空间定义.例如这个类的子类可以以网格,轮播,堆叠等方式显示列表内容.-->所以我们如果要自定义列表就可以继承该类
AdapterView官方文档描述->AdapterView就是其子View由Adapter决定的View.

下面是对ListView源码的解读

setAdapter():这个方法的作用是绑定listView所需的数据
  1. 调用这个方法中首先是将之前给adapter设置的监听解除掉然后也将listView进行重置也将之前缓存的view进行清除
    //解除数据监听
    if (mAdapter != null && mDataSetObserver != null) {
    mAdapter.unregisterDataSetObserver(mDataSetObserver);
    }
    //listView进行重置
    resetList();
    //缓冲池进行清除
    mRecycler.clear();

     //重置成员变量
     mOldSelectedPosition = INVALID_POSITION;
     mOldSelectedRowId = INVALID_ROW_ID;
    
  2. 如果该listView中包含头布局或者是脚布局那么就会把该adapter包装成HeaderViewListAdapter否则不会包装

     if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {//如果有头布局或者脚布局则进行包装
         mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
     } else {不包装
         mAdapter = adapter;
     }
    
  3. 调用父类的setAdapter方法,也是做了一些数据还原,和初始化

     // AbsListView#setAdapter will update choice mode states.
     super.setAdapter(adapter);
    
  4. 当mAdapter不为null的时候对listView中的变量进行初始化

       if (mAdapter != null) {
         mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
         mOldItemCount = mItemCount;
         mItemCount = mAdapter.getCount();
         checkFocus();
    
         //设置数据监听
         mDataSetObserver = new AdapterDataSetObserver();
         mAdapter.registerDataSetObserver(mDataSetObserver);
    
          //这里给缓存给负责缓存的对象设置一共要生成几种缓存的view  
          mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
    
         int position;
         if (mStackFromBottom) {
             position = lookForSelectablePosition(mItemCount - 1, false);
         } else {
             position = lookForSelectablePosition(0, true);
         }
         setSelectedPositionInt(position);
         setNextSelectedPositionInt(position);
    
         if (mItemCount == 0) {
             // Nothing selected
             checkSelectionChanged();
        }
    
  5. mRecycler.setViewTypeCount(mAdapter.getViewTypeCount())这里给缓存给负责缓存的对象设置一共要生成几种缓存的view
    public void setViewTypeCount(int viewTypeCount) {
    if (viewTypeCount < 1) {
    throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
    }
    //noinspection unchecked
    ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
    for (int i = 0; i < viewTypeCount; i++) {
    scrapViews[i] = new ArrayList<View>();
    }
    mViewTypeCount = viewTypeCount;
    mCurrentScrap = scrapViews[0];
    mScrapViews = scrapViews;
    }

  6. mAdapter为null的情况下也做了一下操作

     else {
         mAreAllItemsSelectable = true;
         checkFocus();
         // Nothing selected
         checkSelectionChanged();
     }
    
  7. 最后请求重绘

    requestLayout();
    

所以setAdapter()中主要是做数据的重置和初始化工作,在这里添加了数据监听,也初始化了item的缓冲池

在setAdapter()方法的最后它调用了requestLayout()方法,那么我们就来看看onLayout()中是怎么摆放item的,但是在这之前我们还是很有必要看一下onMeasure()方法,看看listView是如何测量自己的子View的

onMeasure()

listView自己实现了onMeasure()方法

1.在onMeasure()中的第一句代码就是调用了父类的onMeasure()方法也就是AbsListView的onMeasure()方法,父类的onMeasure()主要是设置了listView的Padding

    // Sets up mListPadding
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

2.当listView中有item而且他的宽或者高德MeasureSpec mode 是 UNSPECIFIED的时候

    mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
    //当listView中有item而且他的宽或者高德MeasureSpec mode 是 UNSPECIFIED的时候
    if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
            || heightMode == MeasureSpec.UNSPECIFIED)) {
        //去获取第一个childView
         final View child = obtainView(0, mIsScrap);

3.其中调用了obtainView(int position, boolean[] outMetadata)这个方法去获取childView,这个方法中的第一个if判断是对 "transient state view"的判断一般一般不会进到这个if语句中 --->现在还不知道这个transient state view具体是什么东西0.0

    // 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) {
                setItemViewLayoutParams(updatedView, position);
                mRecycler.addScrapView(updatedView, position);
            }
        }

        outMetadata[0] = true;

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

4.下面这段代码就是一般真正的从复用view的代码.首先他先从recycleBin中取出scrapView然后传给adapter的getView(),getView()也会返回一个child,如果scrapView不为null则说明缓冲池中有view,然后再进行判断scrapViwe和child是否相同,如果不相同就调用 mRecycler.addScrapView(scrapView, position);将scrapView再添加到缓冲池中

    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 if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

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

5.到这里obtainView()就完成了产出一个child的任务,下面的代码就是给child进行设置一些东西,比如"DrawingCacheBackgroundColor","LayoutParams"等

6.到最后就是直接返回这个来之不易的child

      .....
      return child;

7.obtainView()方法走完以后他返回了一个child,下面会调用measureScrapChild()这个方法测量child

        // Lay out child directly against the parent measure spec so that
        // we can obtain exected minimum width and height.
        //上面注释的意思就是 按照父亲的MeasureSpec来测量可以获取到child的最小宽度和高度
        measureScrapChild(child, 0, widthMeasureSpec, heightSize);

8.child测量完了以后然后将child添加到缓存池中

       if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                ((LayoutParams) child.getLayoutParams()).viewType)) {
            mRecycler.addScrapView(child, 0);
        }

9.下来的代码就是调用setMeasureDimension(widthSize, heightSize)将listView的宽高进行保存

ListView的onMeasure()总结:

**_看完代码以后我们知道在onMeasure()方法的时候已经把position为0的child造出来了,也进行了测量并加入到了缓冲池RecycleBin中.但是值得注意的是在ListView的父类的宽或者搞的mode是MeasureSpec.UNSPECIFIED这里ListView的高只会是一个item的高 (这也就是scallView嵌套listView出现问题的原因)_**

    if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
            || heightMode == MeasureSpec.UNSPECIFIED)) {
        final View child = obtainView(0, mIsScrap);
    ******
    if (heightMode == MeasureSpec.UNSPECIFIED) {
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }
    ******

解决的方法也就很简单将重写listView的onMeasure()方法就行了,就是将父类的 mode从 MeasureSpec.UNSPECIFIED改为MeasureSpec.AT_MOST因为当父类的mode的是MeasureSpec.AT_MOST的时候listView会循环获取child的高并想加

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
    MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, expandSpec);
}

onMeasure()说完了下面我们来看看onLayout()

onLayout()

ListView中没有实现onLayout()方法,但是复写了父类onLayout()中调用的一个方法layoutChildren()方法.后面我们就一起看看ListView中的layoutChildren()是怎么实现的,但是在看layoutChildren()之前我们先看看AbsListView中的onLayout()都干了什么.

onLayout()>AbsListView-->其实在这个方法中也没有做什么处理就是判断了一下当change改变的时候的情况,主要还是在layoutChildren()方法中,下面我们就看看这个方法

layoutChildren()>ListView这个方法也是挺长的,大概有300多行.

1.layoutChildren中首先做的一个操作就是避免在短时间内重复调用layoutChildren()该方法

    final boolean blockLayoutRequests = mBlockLayoutRequests;
    if (blockLayoutRequests) {
        return;
    }

    mBlockLayoutRequests = true;
    *****
    //最后又将mBlockLayoutRequests置为false
    finally {
     *****  
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = false;
        }
    }

2.调用了父类的layoutChildren()方法,其实是一个空方法

 try {
        super.layoutChildren();
      ****

3.调用了invalidate()方法来出发draw()方法,这里不太明白为什么这里要调用invalidate(),难道是要一遍摆放一边?

 invalidate();

4.是在对adapter为null的情况进行了判断,当adapter为null的时候,会清空列表然后直接返回

 if (mAdapter == null) {
    resetList();
    invokeOnItemScrollListener();
    return;
 }

5.在不同的layoutMode情况下对第一个item,选中的item进行缓存

        // Remember stuff we will need down below
        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            index = mNextSelectedPosition - mFirstPosition;
            if (index >= 0 && index < childCount) {
                newSel = getChildAt(index);
            }
            break;
        case LAYOUT_FORCE_TOP:
        case LAYOUT_FORCE_BOTTOM:
        case LAYOUT_SPECIFIC:
        case LAYOUT_SYNC:
            break;
        case LAYOUT_MOVE_SELECTION:
        default:
            // Remember the previously selected view
            index = mSelectedPosition - mFirstPosition;
            if (index >= 0 && index < childCount) {
                oldSel = getChildAt(index);
            }

            // Remember the previous first child
            oldFirst = getChildAt(0);

            if (mNextSelectedPosition >= 0) {
                delta = mNextSelectedPosition - mSelectedPosition;
            }

            // Caution: newSel might be null
            newSel = getChildAt(index + delta);
        }

6.当数据变化时 ,则进行同步数据

        boolean dataChanged = mDataChanged;
        if (dataChanged) {
            handleDataChanged();
        }

7.对空item或者错误的item数进行判断和操作,当空item的时候也会清空列表,当错误的item数使会抛出异常

        if (mItemCount == 0) {
            resetList();
            invokeOnItemScrollListener();
            return;
        } else if (mItemCount != mAdapter.getCount()) {
            throw new IllegalStateException("The content of the adapter has changed but "
                    + "ListView did not receive a notification. Make sure the content of "
                    + "your adapter is not modified from a background thread, but only from "
                    + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
                    + "when its content changes. [in ListView(" + getId() + ", " + getClass()
                    + ") with Adapter(" + mAdapter.getClass() + ")]");
        }

8.同步选中的数据

       setSelectedPositionInt(mNextSelectedPosition);

9.对child是否具有辅助功能进行判断,如果有则进入辅助功能的逻辑

        // Remember which child, if any, had accessibility focus. This must
        // occur before recycling any views, since that will clear
        // accessibility focus.
        final ViewRootImpl viewRootImpl = getViewRootImpl();
        if (viewRootImpl != null) {
            final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
            if (focusHost != null) {
                final View focusChild = getAccessibilityFocusedChild(focusHost);
                if (focusChild != null) {
                    if (!dataChanged || isDirectChildHeaderOrFooter(focusChild)
                            || focusChild.hasTransientState() || mAdapterHasStableIds) {
                        // The views won't be changing, so try to maintain
                        // focus on the current host and virtual view.
                        accessibilityFocusLayoutRestoreView = focusHost;
                        accessibilityFocusLayoutRestoreNode = viewRootImpl
                                .getAccessibilityFocusedVirtualView();
                    }

                    // If all else fails, maintain focus at the same
                    // position.
                    accessibilityFocusPosition = getPositionForView(focusChild);
                }
            }
        }

10.对焦点进行处理

        final View focusedChild = getFocusedChild();
        if (focusedChild != null) {
            // TODO: in some cases focusedChild.getParent() == null

            // We can remember the focused view to restore after re-layout
            // if the data hasn't changed, or if the focused position is a
            // header or footer.
            if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)
                    || focusedChild.hasTransientState() || mAdapterHasStableIds) {
                focusLayoutRestoreDirectChild = focusedChild;
                // Remember the specific view that had focus.
                focusLayoutRestoreView = findFocus();
                if (focusLayoutRestoreView != null) {
                    // Tell it we are going to mess with it.
                    focusLayoutRestoreView.dispatchStartTemporaryDetach();
                }
            }
            requestFocus();
        }

11.将所有的item都加入到缓存池中

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

12.清除缓存池中旧的view

        detachAllViewsFromParent();
        recycleBin.removeSkippedScrap();

13.对mLayoutMode进行判断然后选择填充listView的方法,一般的话都会执行default.

        switch (mLayoutMode) {
        case LAYOUT_SET_SELECTION:
            if (newSel != null) {
                sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
            } else {
                sel = fillFromMiddle(childrenTop, childrenBottom);
            }
            break;
          ***********
          default:
            if (childCount == 0) {
                if (!mStackFromBottom) {
                    final int position = lookForSelectablePosition(0, true);
                    setSelectedPositionInt(position);
                    sel = fillFromTop(childrenTop);
                } else {
                    final int position = lookForSelectablePosition(mItemCount - 1, false);
                    setSelectedPositionInt(position);
                    sel = fillUp(mItemCount - 1, childrenBottom);
                }
            } else {
                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;
        }

14.在default中当childCount为0的时候会执行fillFromTop()方法,去从上倒下的填充listView

 //Fills the list from top to bottom, starting with mFirstPosition
private View fillFromTop(int nextTop) {
    mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
    mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
    if (mFirstPosition < 0) {
        mFirstPosition = 0;
    }
    return fillDown(mFirstPosition, nextTop);
}

15.在fillFromTop()方法中其实也就调用了一个方法fillDown(),这个方法中就调用了makeAndAddView()方法去填充 一屏 的listView

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

    while (nextTop < end && pos < mItemCount) {
        // is this the selected item?
        boolean selected = pos == mSelectedPosition;
        View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);

        nextTop = child.getBottom() + mDividerHeight;
        if (selected) {
            selectedView = child;
        }
        pos++;
    }

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

16.fillFromTop()中调用了makeAndAddView()方法去创建一个View并将这个View添加到listView中.在makeAndAddView()方法中首先回去RecycleBin中拿缓存的View如果这个view不为null,则进行使用并返回,如果是null就会调用obtainView()去创建一个新的view,使用并返回

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    if (!mDataChanged) {
        // Try to use an existing view for this position.
        final View activeView = mRecycler.getActiveView(position);
        if (activeView != null) {
            // Found it. We're reusing an existing child, so it just needs
            // to be positioned like a scrap view.
            setupChild(activeView, position, y, flow, childrenLeft, selected, true);
            return activeView;
        }
    }

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

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

    return child;
}

17.obtainView()view就将adapter和listView联系了起来,在obtainView()中首先回去RecycleBin中获取一个废弃的View 然后传到adapter中的getView()方法中,这里这个废弃的View很有可能的null.

    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 if (child.isTemporarilyDetached()) {
            outMetadata[0] = true;

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

18.调用mRecycler.getActiveView()或者obtainView()产生了view以后调用setupChild()方法,其中setupChild()方法中会调用addViewInLayout()或者attachViewToParent()将这个view添加到listView中attachViewToParent()比addViewInLayout()的效率高很多

19.刷新RecycleBin中mActiveViews中的缓存-->其实是将mActiveViews中的缓存挪到了mScrapViews

       // Flush any cached views that did not get reused above
        recycleBin.scrapActiveViews();

20.移除没有用的header和footer

        // remove any header/footer that has been temp detached and not re-attached
        removeUnusedFixedViews(mHeaderViewInfos);
        removeUnusedFixedViews(mFooterViewInfos);

21.找到选中的位置然后对选中的位置进行一系列到操作


onInterceptTouchEvent()

下面我们一起来看看listView滑动过程中的操作,那么就要从onInterceptTouchEvent()这个方法开始了.onInterceptTouchEvent()->listView中没有重写这个方法,那么我们就在AbsListView中看看吧.

1.MotionEvent.ACTION_DOWN中当listView在惯性滑动或者滑动状态是就要拦截

        if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
            mMotionCorrection = 0;
            return true;
        }

2.获取触摸位置对应的item的position,这个方法在listView中实现了

 int motionPosition = findMotionRow(y);

3.当listView处于fling状态而且触摸的位置在listView上则进行记录,触摸的X坐标,触摸的Y坐标,触摸位置对应的item的position,和TouchMode;

        if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
            // User clicked on an actual view (and was not stopping a fling).
            // Remember where the motion event started
            v = getChildAt(motionPosition - mFirstPosition);
            mMotionViewOriginalTop = v.getTop();//触摸位置对应的item的top值
            mMotionX = x;//触摸的X坐标
            mMotionY = y;//触摸的Y坐标
            mMotionPosition = motionPosition;//触摸位置对应的item的position
            mTouchMode = TOUCH_MODE_DOWN;//将TouchMode设置为TOUCH_MODE_DOWN
            clearScrollingCache();
        }

4.当TouchMode为TOUCH_MODE_FLING的时候拦截

        if (touchMode == TOUCH_MODE_FLING) {
            return true;
        }

5.MotionEvent.ACTION_MOVE中判断是否拦截,是onInterceptTouchEvent()的核心

            if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) {
                return true;
            }

6.所以我们来看一下startScrollIfNeeded()这个方法

 // 这个方法在onInterceptTouchEvent的move事件中调用,在onTouchEvent()的onTouchMove()方法
  // 中开始时候也会调用
   private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
// Check if we have moved far enough that it looks more like a
// scroll than a tap
// 得到当前事件的y值与down事件时候设置的值的差值
final int deltaY = y - mMotionY;
final int distance = Math.abs(deltaY);
// mScrollY!=0即overscroll为true ,核心为distance > mTouchSlop即拦截事件自己处理
// mTouchSlop在构造函数中初始化并赋值了
final boolean overscroll = mScrollY != 0;
if ((overscroll || distance > mTouchSlop) &&
        (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
    createScrollingCache();
    if (overscroll) {
        mTouchMode = TOUCH_MODE_OVERSCROLL;
        mMotionCorrection = 0;
    } else {
        // 设置触摸模式为TOUCH_MODE_SCROLL,在onTouchEvent()用到
        mTouchMode = TOUCH_MODE_SCROLL;
        mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
    }
    // 取消子view的长按监听触发
    removeCallbacks(mPendingCheckForLongPress);
    setPressed(false);
    final View motionView = getChildAt(mMotionPosition - mFirstPosition);
    // listview拦截了事件本身处理,所以恢复可能设置子view的press状态
    if (motionView != null) {
        motionView.setPressed(false);
    }
    // 通知ScrollState状态变化回调
    reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
    // Time to start stealing events! Once we've stolen them, don't let anyone
    // steal from us
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
    // 作用如名,如果满足条件,滚动listview
    scrollIfNeeded(x, y, vtev);
    return true;
}

return false;
}

7.MotionEvent.ACTION_CANCEL或者MotionEvent.ACTION_UP中则是将TouchMode恢复成默认状态,然后回收VelocityTracker相关资源

    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP: {
        mTouchMode = TOUCH_MODE_REST;//将TouchMode恢复成默认状态
        mActivePointerId = INVALID_POINTER;
        recycleVelocityTracker();//回收VelocityTracker相关资源
        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        stopNestedScroll();
        break;
    }

onTouchEvent()

onTouchEvent()->listView中没有重写onTouchEvent方法,那么我们就在AbsListView中看看吧 ,因为onTouchEvent()中的逻辑很多 所以我们就主要
看看Down事件(onTouchDown())和Move事件(onTouchMove())

onTouchDown()中首先会判断TouchMode如果是fling状态,就让他停下来,然后记录手指点击的位置和TouchMode等

    if (mTouchMode == TOUCH_MODE_OVERFLING) {
        // Stopped the fling. It is a scroll.
        mFlingRunnable.endFling();
        if (mPositionScroller != null) {
            mPositionScroller.stop();
        }
        mTouchMode = TOUCH_MODE_OVERSCROLL;
        mMotionX = (int) ev.getX();
        mMotionY = (int) ev.getY();
        mLastY = mMotionY;
        mMotionCorrection = 0;
        mDirection = 0;
    }

当TouchMode不是fling状态的话,首先会记录手指的落点,然后计算到落点对应的item的position

        final int x = (int) ev.getX();
        final int y = (int) ev.getY();
        int motionPosition = pointToPosition(x, y);

当数据没有变化,我们就要重新判断TouchMode,如果是TOUCH_MODE_FLING的话就要将TouchMode改为TOUCH_MODE_SCROLL并且马上中断fling,然后通过findMotionRow()这个方法重新找一下手指落点对应的item的position

          if (mTouchMode == TOUCH_MODE_FLING) {
                // Stopped a fling. It is a scroll.
                createScrollingCache();
                mTouchMode = TOUCH_MODE_SCROLL;
                mMotionCorrection = 0;
                motionPosition = findMotionRow(y);
                mFlingRunnable.flywheelTouch();//中断fling
            }

当点击的item是可用的那么又要改变TouchMode

            else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) {
                // User clicked on an actual view (and was not stopping a
                // fling). It might be a click or a scroll. Assume it is a
                // click until proven otherwise.
                mTouchMode = TOUCH_MODE_DOWN;

                // FIXME Debounce
                if (mPendingCheckForTap == null) {
                    mPendingCheckForTap = new CheckForTap();
                }

                mPendingCheckForTap.x = ev.getX();
                mPendingCheckForTap.y = ev.getY();
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
            }

各种情况下获取到了手指落点对应的position也就是motionPosition,这里对motionPosition进行判断如果大于等于零,那么就获取到这个item,并记录他的top值

       if (motionPosition >= 0) {
            // Remember where the motion event started
            final View v = getChildAt(motionPosition - mFirstPosition);
            mMotionViewOriginalTop = v.getTop();
        }

下来在对落点的坐标和motionPostion进行保存

        mMotionX = x;
        mMotionY = y;
        mMotionPosition = motionPosition;
        mLastY = Integer.MIN_VALUE;

最后判断TouchMode是TOUCH_MODE_DOWN,而且motionPosition不是默认值并且ListView接受这个down时间那么就取消其他的操作(后面看看mPendingCheckForTap这个runnable是干啥的)

    if (mTouchMode == TOUCH_MODE_DOWN && mMotionPosition != INVALID_POSITION
            && performButtonActionOnTouchDown(ev)) {
            removeCallbacks(mPendingCheckForTap);
    }

onTouchMove()中如果数据改变了就要重新排列item

    if (mDataChanged) {
        // Re-sync everything if data has been changed
        // since the scroll operation can query the adapter.
        layoutChildren();
    }

在mode时间中还有一个switch语句,当TouchMode为TOUCH_MODE_DOWN,TOUCH_MODE_TAP,TOUCH_MODE_DONE_WAITING等没有动的状态是会去判断是否可以滚动
,核心判断调条件为down事件y与down事件mMotionY值的差值绝对值是否大于mTouchSlop

            if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
                break;
            }

下面会判断点击的区域是否在ListView内 如果不是则取消操作

            // Otherwise, check containment within list bounds. If we're
            // outside bounds, cancel any active presses.
            final View motionView = getChildAt(mMotionPosition - mFirstPosition);
            final float x = ev.getX(pointerIndex);
            if (!pointInView(x, y, mTouchSlop)) {
                setPressed(false);
                if (motionView != null) {
                    motionView.setPressed(false);
                }
                removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                        mPendingCheckForTap : mPendingCheckForLongPress);
                mTouchMode = TOUCH_MODE_DONE_WAITING;
                updateSelectorState();
            } else if (motionView != null) {
                // Still within bounds, update the hotspot.
                final float[] point = mTmpPoint;
                point[0] = x;
                point[1] = y;
                transformPointToViewLocal(point, motionView);
                motionView.drawableHotspotChanged(point[0], point[1]);
            }

如果已经是TOUCH_MODE_SCROLL和TOUCH_MODE_OVERSCROLL这种滚动状态,则执行scrollIfNeeded()方法判断是否要进行滚动.

        case TOUCH_MODE_SCROLL:
        case TOUCH_MODE_OVERSCROLL:
            scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
            break;

scrollifNeeded()中主要负责滚动的方法就是trackMotionScroll()

trackMotionScroll()这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实我们就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了.在滚动的过程中使用fillGap()方法进行item的填充

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

    final int firstTop = getChildAt(0).getTop();
    final int lastBottom = getChildAt(childCount - 1).getBottom();

    final Rect listPadding = mListPadding;

    // "effective padding" In this case is the amount of padding that affects
    // how much space should not be filled by items. If we don't clip to padding
    // there is no effective padding.
    int effectivePaddingTop = 0;
    int effectivePaddingBottom = 0;
    if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
        effectivePaddingTop = listPadding.top;
        effectivePaddingBottom = listPadding.bottom;
    }

     // FIXME account for grid vertical spacing too?
    final int spaceAbove = effectivePaddingTop - firstTop;
    final int end = getHeight() - effectivePaddingBottom;
    final int spaceBelow = lastBottom - end;

    final int height = getHeight() - mPaddingBottom - mPaddingTop;
    if (deltaY < 0) {
        deltaY = Math.max(-(height - 1), deltaY);
    } else {
        deltaY = Math.min(height - 1, deltaY);
    }

    if (incrementalDeltaY < 0) {
        incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
    } else {
        incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
    }

    final int firstPosition = mFirstPosition;

    // Update our guesses for where the first and last views are
    if (firstPosition == 0) {
        mFirstPositionDistanceGuess = firstTop - listPadding.top;
    } else {
        mFirstPositionDistanceGuess += incrementalDeltaY;
    }
    if (firstPosition + childCount == mItemCount) {
        mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
    } else {
        mLastPositionDistanceGuess += incrementalDeltaY;
    }

    final boolean cannotScrollDown = (firstPosition == 0 &&
            firstTop >= listPadding.top && incrementalDeltaY >= 0);
    final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
            lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);

    if (cannotScrollDown || cannotScrollUp) {
        return incrementalDeltaY != 0;
    }

    //incrementalDeltaY表示据上次触发event事件手指在Y方向上位置的改变量
    final boolean down = incrementalDeltaY < 0;//用来判断是向上滑动还是向下滑动,true的话就是向下滑动,false就是向上滑动

    final boolean inTouchMode = isInTouchMode();
    if (inTouchMode) {
        hideSelector();
    }

    final int headerViewsCount = getHeaderViewsCount();
    final int footerViewsStart = mItemCount - getFooterViewsCount();

    int start = 0;
    int count = 0;

    if (down) {//down为true则向下滑动,false则为向上滑动
        int top = -incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            top += listPadding.top;
        }
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {//说明child还在屏幕内
                break;
            } else {//说明child已经挪到了屏幕外
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    //将item重置
                    child.clearAccessibilityFocus();
                    //将child放进RecycleBin的回收池内
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    } else {//逻辑和向下移动一样只不过是向上移动
        int bottom = getHeight() - incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            bottom -= listPadding.bottom;
        }
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                start = i;
                count++;
                int position = firstPosition + i;
                if (position >= headerViewsCount && position < footerViewsStart) {
                    // The view will be rebound to new data, clear any
                    // system-managed transient state.
                    child.clearAccessibilityFocus();
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }

    mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

    mBlockLayoutRequests = true;

    if (count > 0) {
        //将回收掉的item和listView切断关系
        detachViewsFromParent(start, count);
        mRecycler.removeSkippedScrap();
    }

    // invalidate before moving the children to avoid unnecessary invalidate
    // calls to bubble up from the children all the way to the top
    if (!awakenScrollBars()) {
       invalidate();
    }
    //这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,
    //这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。
    offsetChildrenTopAndBottom(incrementalDeltaY);

    if (down) {
        mFirstPosition += count;
    }

    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        fillGap(down);
    }

    mRecycler.fullyDetachScrapViews();
    if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
        final int childIndex = mSelectedPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(mSelectedPosition, getChildAt(childIndex));
        }
    } else if (mSelectorPosition != INVALID_POSITION) {
        final int childIndex = mSelectorPosition - mFirstPosition;
        if (childIndex >= 0 && childIndex < getChildCount()) {
            positionSelector(INVALID_POSITION, getChildAt(childIndex));
        }
    } else {
        mSelectorRect.setEmpty();
    }

    mBlockLayoutRequests = false;

    invokeOnItemScrollListener();

    return false;
}

onTouchUp()中主要处理了filing状态,和状态重置

而filing状态的主要代码是在FlingRunnable中我们来看看FlingRunnable的start()方法.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容