Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)

时间:2021-09-09 17:18:58

Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)


本文由 Luzhuo 编写,转发请保留该信息.
原文: http://blog.csdn.net/Rozol/article/details/78161840


  • 把数据用列表的形式,动态滚动的方式,展示给用户.
  • ListView 作为界面展示的容器控件必然会直接或者间接的继承ViewGroup, 现在看看源代码的继承结构

    public class ListView extends AbsListView { }

    public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, ViewTreeObserver.OnTouchModeChangeListener, RemoteViewsAdapter.RemoteAdapterConnectionCallback { }

    public abstract class AdapterView<T extends Adapter> extends ViewGroup { }
  • 现在知道ListView确实是继承ViewGroup的,那么就会重写 onMeasure() onLayout() onDraw() 这三个基本的方法, 大家是否注意到继承ViewGroup的后,onMeasure() onLayout()会多次执行的问题(执行了4次onMeasure(), 2次onLayout()),以下是log.

    • Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)

基本使用

public class MainActivity extends AppCompatActivity {
private ListView listView;
private String[] listImage = Resource.grilImage;
private BitmapUtils bitmapUtils;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initView();
initData();
}

private void initView() {
bitmapUtils = new BitmapUtils(this);
listView = (ListView) findViewById(R.id.listview);
}

private void initData() {
ImageAdapter IAdapter = new ImageAdapter(MainActivity.this, listImage, bitmapUtils);
listView.setAdapter(IAdapter);

listView.setOnScrollListener(new PauseOnScrollListener(bitmapUtils, false, true));
}
}

public class ImageAdapter extends BaseAdapter {
private Context context;
private String[] listImage;
private LayoutInflater inflater;
private BitmapUtils bitmapUtils;

public ImageAdapter(Context context, String[] listImage, BitmapUtils bitmapUtils) {
this.context = context;
this.listImage = listImage;
inflater = LayoutInflater.from(context);
this.bitmapUtils = bitmapUtils;
}

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

@Override
public Object getItem(int position) {
return listImage[position];
}

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

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(convertView == null){
convertView = inflater.inflate(R.layout.item_list, null);
viewHolder = new ViewHolder();

viewHolder.imageview = (ImageView)convertView.findViewById(R.id.imageview);
viewHolder.textview = (TextView)convertView.findViewById(R.id.textview);

convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}

if(listImage.length != 0){
bitmapUtils.display(viewHolder.imageview, listImage[position]);
viewHolder.textview.setText("第"+position+"张图片");
}
return convertView;
}

class ViewHolder{
ImageView imageview;
TextView textview;
}
}

Adapter适配器模式

  • 从上面的使用代码可以看出,要让ListView正常工作,就要设置Adapter,Adapter就是适配器

    public class MyAdapter extends BaseAdapter {
    @Override
    public int getCount() {
    return 0;
    }

    @Override
    public Object getItem(int position) {
    return null;
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
    return null;
    }
    }
  • 继承Adapter会被要求必须重写上述4个方法. 数据是不尽相同的, ListView只关心交互和展示的工作,不关心你数据是什么样的,从哪来的. 而Adapter的统一接口就解决的数据适配的问题.

  • 示范图:
    Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)

recycleBin机制

  • 为了简洁说明ListView是怎么工作的,先讲下 AbsListView 类里的内部类 RecycleBin

    public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, ViewTreeObserver.OnTouchModeChangeListener, RemoteViewsAdapter.RemoteAdapterConnectionCallback {

    final RecycleBin mRecycler = new RecycleBin();

    class RecycleBin {
    private RecyclerListener mRecyclerListener;

    /**
    * The position of the first view stored in mActiveViews.
    */

    private int mFirstActivePosition;

    /**
    * Views that were on screen at the start of layout. This array is populated at the start of
    * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
    * Views in mActiveViews represent a contiguous range of Views, with position of the first
    * view store in mFirstActivePosition.
    */

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

    /**
    * Unsorted views that can be used by the adapter as a convert view.
    */

    // ↓↓↓
    private ArrayList<View>[] mScrapViews;

    private int mViewTypeCount;
    // ↓↓↓
    private ArrayList<View> mCurrentScrap;

    private ArrayList<View> mSkippedScrap;

    private SparseArray<View> mTransientStateViews;
    private LongSparseArray<View> mTransientStateViewsById;

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

    /**
    * Fill ActiveViews with all of the children of the AbsListView.
    *
    * @param childCount The minimum number of views mActiveViews should hold
    * @param firstActivePosition The position of the first view that will be stored in
    * mActiveViews
    */

    // ↓↓↓
    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;
    // Remember the position so that setupChild() doesn't reset state.
    lp.scrappedFromPosition = firstActivePosition + i;
    }
    }
    }

    /**
    * Get the view corresponding to the specified position. The view will be removed from
    * mActiveViews if it is found.
    *
    * @param position The position to look up in mActiveViews
    * @return The view if it is found, null otherwise
    */

    // ↓↓↓
    View getActiveView(int position) {
    int index = position - mFirstActivePosition;
    final View[] activeViews = mActiveViews;
    if (index >=0 && index < activeViews.length) {
    final View match = activeViews[index];
    activeViews[index] = null;
    return match;
    }
    return null;
    }

    /**
    * @return A view from the ScrapViews collection. These are unordered.
    */

    // ↓↓↓
    View getScrapView(int position) {
    final int whichScrap = mAdapter.getItemViewType(position);
    if (whichScrap < 0) {
    return null;
    }
    if (mViewTypeCount == 1) {
    return retrieveFromScrap(mCurrentScrap, position);
    } else if (whichScrap < mScrapViews.length) {
    return retrieveFromScrap(mScrapViews[whichScrap], position);
    }
    return null;
    }

    /**
    * Puts a view into the list of scrap views.
    * <p>
    * If the list data hasn't changed or the adapter has stable IDs, views
    * with transient state will be preserved for later retrieval.
    *
    * @param scrap The view to add
    * @param position The view's position within its parent
    */

    // ↓↓↓
    void addScrapView(View scrap, int position) {
    final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
    if (lp == null) {
    // Can't recycle, but we don't know anything about the view.
    // Ignore it completely.
    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)) {
    // Can't recycle. If it's not a header or footer, which have
    // special handling and should be ignored, then skip the scrap
    // heap and we'll fully detach the view later.
    if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
    getSkippedScrap().add(scrap);
    }
    return;
    }

    scrap.dispatchStartTemporaryDetach();

    // The the accessibility state of the view may change while temporary
    // detached and we do not allow detached views to fire accessibility
    // events. So we are announcing that the subtree changed giving a chance
    // to clients holding on to a view in this subtree to refresh it.
    notifyViewAccessibilityStateChangedIfNeeded(
    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);

    // 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.
    if (mTransientStateViewsById == null) {
    mTransientStateViewsById = new LongSparseArray<>();
    }
    mTransientStateViewsById.put(lp.itemId, scrap);
    } else if (!mDataChanged) {
    // If the data hasn't changed, we can reuse the views at
    // their old positions.
    if (mTransientStateViews == null) {
    mTransientStateViews = new SparseArray<>();
    }
    mTransientStateViews.put(position, scrap);
    } else {
    // Otherwise, we'll have to remove the view and start over.
    getSkippedScrap().add(scrap);
    }
    } else {
    if (mViewTypeCount == 1) {
    mCurrentScrap.add(scrap);
    } else {
    mScrapViews[viewType].add(scrap);
    }

    if (mRecyclerListener != null) {
    mRecyclerListener.onMovedToScrapHeap(scrap);
    }
    }
    }
    }
    }
    • 以上代码是从源码中拷贝出来,并删掉了一些不重要的方法
    • 成员变量:

      • View[] mActiveViews: 用于存放活动View(也就是在屏幕上展示的View)
      • int mFirstActivePosition: 存放mActiveViews中第一个View的Position(也就是第几个Item)
      • ArrayList<View>[] mScrapViews: 废弃的View,可通过Adapter转为convertView继续使用(看看基本使用Adapter代码,我们就是将这些废弃的view重复使用的)
      • ArrayList<View> mCurrentScrap: ViewTypeCount == 1 的废弃View会被存在这个集合里
      • 方法:
      • public void setViewTypeCount(int viewTypeCount) {}: 该方法会根据传入的类型数量初始化 mScrapViews 和 mCurrentScrap; mScrapViews 存了不同类型View的集合, mCurrentScrap是mScrapViews的第一个集合; 看来ListView是可以传入多个类型的View的
      • void fillActiveViews(int childCount, int firstActivePosition) {}: 主要是将mActiveViews填满子View (保存屏幕上展示的View)
      • View getActiveView(int position) {}: 根据position获取mActiveViews里的View
      • View getScrapView(int position) {}: 源码主要调用了retrieveFromScrap(mCurrentScrap, position)方法,现在看看这个方法是干吗的?

        private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
        final int size = scrapViews.size();
        if (size > 0) {
        // See if we still have a view for this position or ID.
        for (int i = 0; i < size; i++) {
        final View view = scrapViews.get(i);
        final AbsListView.LayoutParams params =
        (AbsListView.LayoutParams) view.getLayoutParams();

        if (mAdapterHasStableIds) {
        final long id = mAdapter.getItemId(position);
        if (id == params.itemId) {
        return scrapViews.remove(i);
        }
        } else if (params.scrappedFromPosition == position) {
        final View scrap = scrapViews.remove(i);
        clearAccessibilityFromScrap(scrap);
        return scrap;
        }
        }
        final View scrap = scrapViews.remove(size - 1);
        clearAccessibilityFromScrap(scrap);
        return scrap;
        } else {
        return null;
        }
        }
        • 看来getScrapView(int position)是获取废弃的View, 如果能获取到就返回View,获取不到就返回null
    • void addScrapView(View scrap, int position) {}: 把废弃的View添加到mCurrentScrap里, 把具有过渡效果的废弃View添加到mTransientStateViews里(带有过渡效果的View这里不做讲解)
    • 可见recycleBin主要工作就是填满和获取展示View,添加和获取缓存View.
    • Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)

ListView的执行逻辑源码

ListView的初始化逻辑

  • ListView作为View容器控件,那么我们就从 onMeasure() onLayout() 这2个基本的被重写方法开始研究
  • onMeasure() 主要是测量ListView的大小; onLayout()用于确定子View的布局, 这才是核心, 该方法并没有在ListView中实现, 而是在抽象父类AbsListView中实现.

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

    mInLayout = true;

    final int childCount = getChildCount();
    // ↓↓↓ 1. 如果change == true, ListView的大小和位置发生变化
    if (changed) {
    for (int i = 0; i < childCount; i++) {
    // ↓↓↓ 2. 那么就把所有子布局强制重绘
    getChildAt(i).forceLayout();
    }
    mRecycler.markChildrenDirty();
    }

    // ↓↓↓ 3. 调用子类ListView的layoutChildren()方法
    layoutChildren();
    mInLayout = false;

    mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

    // TODO: Move somewhere sane. This doesn't belong in onLayout().
    if (mFastScroll != null) {
    mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
    }
    }
    • 接着看看layoutChildren()做了什么

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

      mBlockLayoutRequests = true;

      try {
      super.layoutChildren();

      invalidate();

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

      final int childrenTop = mListPadding.top;
      final int childrenBottom = mBottom - mTop - mListPadding.bottom;
      // ↓↓↓ 1. ListView中还未填充任务子View, 得到结果为0
      final int childCount = getChildCount();

      int index = 0;
      int delta = 0;

      View sel;
      View oldSel = null;
      View oldFirst = null;
      View newSel = null;

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

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

      // Handle the empty set by removing all views that are visible
      // and calling it a day
      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() + ")]");
      }

      setSelectedPositionInt(mNextSelectedPosition);

      AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
      View accessibilityFocusLayoutRestoreView = null;
      int accessibilityFocusPosition = INVALID_POSITION;

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

      View focusLayoutRestoreDirectChild = null;
      View focusLayoutRestoreView = null;

      // Take focus back to us temporarily to avoid the eventual call to
      // clear focus when removing the focused child below from messing
      // things up when ViewAncestor assigns focus back to someone else.
      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();
      }

      // Pull all children into the RecycleBin.
      // These views will be reused if possible
      final int firstPosition = mFirstPosition;
      final RecycleBin recycleBin = mRecycler;
      // ↓↓↓ 2. 一旦数据源有变化, dataChanged == true
      if (dataChanged) {
      for (int i = 0; i < childCount; i++) {
      recycleBin.addScrapView(getChildAt(i), firstPosition+i);
      }
      } else {
      // ↓↓↓ 2.2 如果数据源, 执行该方法, 调用RecycleBin的fillActiveViews()缓存展示的所有view, 但是现在并没有展示的View
      recycleBin.fillActiveViews(childCount, firstPosition);
      }

      // Clear out old views
      // ↓↓↓ 2.3 从父容器中清除所有view, 现在没有可清理的view
      detachAllViewsFromParent();
      recycleBin.removeSkippedScrap();

      // ↓↓↓ 3. 判断布局模式, 默认布局模式为LAYOUT_NORMAL
      switch (mLayoutMode) {
      case LAYOUT_SET_SELECTION:
      if (newSel != null) {
      sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
      } else {
      sel = fillFromMiddle(childrenTop, childrenBottom);
      }
      break;
      case LAYOUT_SYNC:
      sel = fillSpecific(mSyncPosition, mSpecificTop);
      break;
      case LAYOUT_FORCE_BOTTOM:
      sel = fillUp(mItemCount - 1, childrenBottom);
      adjustViewsUpOrDown();
      break;
      case LAYOUT_FORCE_TOP:
      mFirstPosition = 0;
      sel = fillFromTop(childrenTop);
      adjustViewsUpOrDown();
      break;
      case LAYOUT_SPECIFIC:
      final int selectedPosition = reconcileSelectedPosition();
      sel = fillSpecific(selectedPosition, mSpecificTop);
      /**
      * When ListView is resized, FocusSelector requests an async selection for the
      * previously focused item to make sure it is still visible. If the item is not
      * selectable, it won't regain focus so instead we call FocusSelector
      * to directly request focus on the view after it is visible.
      */

      if (sel == null && mFocusSelector != null) {
      final Runnable focusRunnable = mFocusSelector
      .setupFocusIfValid(selectedPosition);
      if (focusRunnable != null) {
      post(focusRunnable);
      }
      }
      break;
      case LAYOUT_MOVE_SELECTION:
      sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
      break;
      default:
      // ↓↓↓ 3.1 于是会执行该部分代码
      if (childCount == 0) {
      if (!mStackFromBottom) {
      final int position = lookForSelectablePosition(0, true);
      setSelectedPositionInt(position);
      // ↓↓↓ 3.2 执行方法
      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;
      }

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

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

      if (sel != null) {
      // The current selected item should get focus if items are
      // focusable.
      if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
      final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
      focusLayoutRestoreView != null &&
      focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
      if (!focusWasTaken) {
      // Selected item didn't take focus, but we still want to
      // make sure something else outside of the selected view
      // has focus.
      final View focused = getFocusedChild();
      if (focused != null) {
      focused.clearFocus();
      }
      positionSelector(INVALID_POSITION, sel);
      } else {
      sel.setSelected(false);
      mSelectorRect.setEmpty();
      }
      } else {
      positionSelector(INVALID_POSITION, sel);
      }
      mSelectedTop = sel.getTop();
      } else {
      final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP
      || mTouchMode == TOUCH_MODE_DONE_WAITING;
      if (inTouchMode) {
      // If the user's finger is down, select the motion position.
      final View child = getChildAt(mMotionPosition - mFirstPosition);
      if (child != null) {
      positionSelector(mMotionPosition, child);
      }
      } else if (mSelectorPosition != INVALID_POSITION) {
      // If we had previously positioned the selector somewhere,
      // put it back there. It might not match up with the data,
      // but it's transitioning out so it's not a big deal.
      final View child = getChildAt(mSelectorPosition - mFirstPosition);
      if (child != null) {
      positionSelector(mSelectorPosition, child);
      }
      } else {
      // Otherwise, clear selection.
      mSelectedTop = 0;
      mSelectorRect.setEmpty();
      }

      // Even if there is not selected position, we may need to
      // restore focus (i.e. something focusable in touch mode).
      if (hasFocus() && focusLayoutRestoreView != null) {
      focusLayoutRestoreView.requestFocus();
      }
      }

      // Attempt to restore accessibility focus, if necessary.
      if (viewRootImpl != null) {
      final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
      if (newAccessibilityFocusedView == null) {
      if (accessibilityFocusLayoutRestoreView != null
      && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
      final AccessibilityNodeProvider provider =
      accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
      if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
      final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
      accessibilityFocusLayoutRestoreNode.getSourceNodeId());
      provider.performAction(virtualViewId,
      AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
      } else {
      accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
      }
      } else if (accessibilityFocusPosition != INVALID_POSITION) {
      // Bound the position within the visible children.
      final int position = MathUtils.constrain(
      accessibilityFocusPosition - mFirstPosition, 0,
      getChildCount() - 1);
      final View restoreView = getChildAt(position);
      if (restoreView != null) {
      restoreView.requestAccessibilityFocus();
      }
      }
      }
      }

      // Tell focus view we are done mucking with it, if it is still in
      // our view hierarchy.
      if (focusLayoutRestoreView != null
      && focusLayoutRestoreView.getWindowToken() != null) {
      focusLayoutRestoreView.dispatchFinishTemporaryDetach();
      }

      mLayoutMode = LAYOUT_NORMAL;
      mDataChanged = false;
      if (mPositionScrollAfterLayout != null) {
      post(mPositionScrollAfterLayout);
      mPositionScrollAfterLayout = null;
      }
      mNeedSync = false;
      setNextSelectedPositionInt(mSelectedPosition);

      updateScrollIndicators();

      if (mItemCount > 0) {
      checkSelectionChanged();
      }

      invokeOnItemScrollListener();
      } finally {
      if (mFocusSelector != null) {
      mFocusSelector.onLayoutComplete();
      }
      if (!blockLayoutRequests) {
      mBlockLayoutRequests = false;
      }
      }
      }

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

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

      // ↓↓↓ 3.2 循环计算子布局位置, 然后调用makeAndAddView()
      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;
      }

      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
      // ↓↓↓ 3.3 从RecycleBin里获取一个缓存的View, 但是缓存为空, 返回null
      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
      // ↓↓↓ 3.4 然而从RecycleBin里获取缓存的View失败, 则试图通过obtainView()方法获取一个View
      child = obtainView(position, mIsScrap);

      // This needs to be positioned and measured
      // ↓↓↓ 3.7 将适配器获得的View, 交给该方法
      setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

      return child;
      }

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

      isScrap[0] = false;

      // ...

      // ↓↓↓ 3.5 从RecycleBin里获取一个缓存的废弃的View, 现在RecycleBin里没有缓存任何View, 所以返回null
      final View scrapView = mRecycler.getScrapView(position);
      // ↓↓↓ 3.6 并且从适配器里获取一个View, 这个只要我们给了数据, 一定能获取到
      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()) {
      isScrap[0] = true;

      // Finish the temporary detach started in addScrapView().
      child.dispatchFinishTemporaryDetach();
      } else {
      // we set isScrap to "true" only if the view is temporarily detached.
      // if the view is fully detached, it is as good as a view created by the
      // adapter
      isScrap[0] = false;
      }

      }
      }

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

      if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
      child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
      }

      setItemViewLayoutParams(child, position);

      if (AccessibilityManager.getInstance(mContext).isEnabled()) {
      if (mAccessibilityDelegate == null) {
      mAccessibilityDelegate = new ListItemAccessibilityDelegate();
      }
      if (child.getAccessibilityDelegate() == null) {
      child.setAccessibilityDelegate(mAccessibilityDelegate);
      }
      }

      Trace.traceEnd(Trace.TRACE_TAG_VIEW);

      return child;
      }

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

      // 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);
      p.isEnabled = mAdapter.isEnabled(position);

      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;
      }
      // ↓↓↓ 3.8 把获得的View添加到ListView中, 这样就完成了一次onLayout的加载
      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) {
      final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
      mListPadding.left + mListPadding.right, p.width);
      final int lpHeight = p.height;
      final int childHeightSpec;
      if (lpHeight > 0) {
      childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
      } else {
      childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
      MeasureSpec.UNSPECIFIED);
      }
      child.measure(childWidthSpec, childHeightSpec);
      } else {
      cleanupLayoutState(child);
      }

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

      if (mCachingStarted && !child.isDrawingCacheEnabled()) {
      child.setDrawingCacheEnabled(true);
      }

      if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition)
      != position) {
      child.jumpDrawablesToCurrentState();
      }

      Trace.traceEnd(Trace.TRACE_TAG_VIEW);
      }
    • 第一次onLayout, 由于第一次, 所以屏幕上和RecycleBin的缓存里并没有View, 只有通过适配器获取View, 直至将屏幕填满.

    • 接下来我们来看看第二次onLayout, 与第一次的差别是, 这一次屏幕上已经有View了

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

      mInLayout = true;

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

      // ↓↓↓ 1.第二次的onLayout执行还是从这里开始
      layoutChildren();
      mInLayout = false;

      mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;

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

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

      mBlockLayoutRequests = true;

      try {
      super.layoutChildren();

      invalidate();

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

      final int childrenTop = mListPadding.top;
      final int childrenBottom = mBottom - mTop - mListPadding.bottom;
      // 2. 获取子View的数量, 由于屏幕已经填充了View, 所以这里返回的数字 > 0
      final int childCount = getChildCount();

      int index = 0;
      int delta = 0;

      View sel;
      View oldSel = null;
      View oldFirst = null;
      View newSel = null;

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


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

      // Handle the empty set by removing all views that are visible
      // and calling it a day
      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() + ")]");
      }

      setSelectedPositionInt(mNextSelectedPosition);

      AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
      View accessibilityFocusLayoutRestoreView = null;
      int accessibilityFocusPosition = INVALID_POSITION;

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

      View focusLayoutRestoreDirectChild = null;
      View focusLayoutRestoreView = null;

      // Take focus back to us temporarily to avoid the eventual call to
      // clear focus when removing the focused child below from messing
      // things up when ViewAncestor assigns focus back to someone else.
      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();
      }

      // Pull all children into the RecycleBin.
      // These views will be reused if possible
      final int firstPosition = mFirstPosition;
      final RecycleBin recycleBin = mRecycler;
      if (dataChanged) {
      for (int i = 0; i < childCount; i++) {
      recycleBin.addScrapView(getChildAt(i), firstPosition+i);
      }
      } else {
      // ↓↓↓ 3. 将屏幕上的View全部添加到RecycleBin里
      recycleBin.fillActiveViews(childCount, firstPosition);
      }

      // Clear out old views
      // ↓↓↓ 4. 从父容器中清除所有的view
      detachAllViewsFromParent();
      recycleBin.removeSkippedScrap();

      switch (mLayoutMode) {
      case LAYOUT_SET_SELECTION:
      if (newSel != null) {
      sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
      } else {
      sel = fillFromMiddle(childrenTop, childrenBottom);
      }
      break;
      case LAYOUT_SYNC:
      sel = fillSpecific(mSyncPosition, mSpecificTop);
      break;
      case LAYOUT_FORCE_BOTTOM:
      sel = fillUp(mItemCount - 1, childrenBottom);
      adjustViewsUpOrDown();
      break;
      case LAYOUT_FORCE_TOP:
      mFirstPosition = 0;
      sel = fillFromTop(childrenTop);
      adjustViewsUpOrDown();
      break;
      case LAYOUT_SPECIFIC:
      final int selectedPosition = reconcileSelectedPosition();
      sel = fillSpecific(selectedPosition, mSpecificTop);
      /**
      * When ListView is resized, FocusSelector requests an async selection for the
      * previously focused item to make sure it is still visible. If the item is not
      * selectable, it won't regain focus so instead we call FocusSelector
      * to directly request focus on the view after it is visible.
      */

      if (sel == null && mFocusSelector != null) {
      final Runnable focusRunnable = mFocusSelector
      .setupFocusIfValid(selectedPosition);
      if (focusRunnable != null) {
      post(focusRunnable);
      }
      }
      break;
      case LAYOUT_MOVE_SELECTION:
      sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
      break;
      default:
      // ↓↓↓ 5. 这里的childCount > 0
      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 {
      // ↓↓↓ 6. 所以执行以下判断代码
      // ↓↓↓ 6.1 由于是新加载的, 所以不会有被选择的条目, 所以 mSelectedPosition = -1, 改判断不成立
      if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
      sel = fillSpecific(mSelectedPosition,
      oldSel == null ? childrenTop : oldSel.getTop());
      // ↓↓↓ 6.2 所以这 mFirstPosition = 0, mItemCount >= 0, 判断成立
      } else if (mFirstPosition < mItemCount) {
      // ↓↓↓ 7. 可见该方法会去获取缓存View,然后优先填充指定位置的View, 然后填满其他位置的View, 第二次onLayout结束
      sel = fillSpecific(mFirstPosition,
      oldFirst == null ? childrenTop : oldFirst.getTop());
      } else {
      sel = fillSpecific(0, childrenTop);
      }
      }
      break;
      }

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

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

      if (sel != null) {
      // The current selected item should get focus if items are
      // focusable.
      if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
      final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
      focusLayoutRestoreView != null &&
      focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
      if (!focusWasTaken) {
      // Selected item didn't take focus, but we still want to
      // make sure something else outside of the selected view
      // has focus.
      final View focused = getFocusedChild();
      if (focused != null) {
      focused.clearFocus();
      }
      positionSelector(INVALID_POSITION, sel);
      } else {
      sel.setSelected(false);
      mSelectorRect.setEmpty();
      }
      } else {
      positionSelector(INVALID_POSITION, sel);
      }
      mSelectedTop = sel.getTop();
      } else {
      final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP
      || mTouchMode == TOUCH_MODE_DONE_WAITING;
      if (inTouchMode) {
      // If the user's finger is down, select the motion position.
      final View child = getChildAt(mMotionPosition - mFirstPosition);
      if (child != null) {
      positionSelector(mMotionPosition, child);
      }
      } else if (mSelectorPosition != INVALID_POSITION) {
      // If we had previously positioned the selector somewhere,
      // put it back there. It might not match up with the data,
      // but it's transitioning out so it's not a big deal.
      final View child = getChildAt(mSelectorPosition - mFirstPosition);
      if (child != null) {
      positionSelector(mSelectorPosition, child);
      }
      } else {
      // Otherwise, clear selection.
      mSelectedTop = 0;
      mSelectorRect.setEmpty();
      }

      // Even if there is not selected position, we may need to
      // restore focus (i.e. something focusable in touch mode).
      if (hasFocus() && focusLayoutRestoreView != null) {
      focusLayoutRestoreView.requestFocus();
      }
      }

      // Attempt to restore accessibility focus, if necessary.
      if (viewRootImpl != null) {
      final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
      if (newAccessibilityFocusedView == null) {
      if (accessibilityFocusLayoutRestoreView != null
      && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
      final AccessibilityNodeProvider provider =
      accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
      if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
      final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
      accessibilityFocusLayoutRestoreNode.getSourceNodeId());
      provider.performAction(virtualViewId,
      AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
      } else {
      accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
      }
      } else if (accessibilityFocusPosition != INVALID_POSITION) {
      // Bound the position within the visible children.
      final int position = MathUtils.constrain(
      accessibilityFocusPosition - mFirstPosition, 0,
      getChildCount() - 1);
      final View restoreView = getChildAt(position);
      if (restoreView != null) {
      restoreView.requestAccessibilityFocus();
      }
      }
      }
      }

      // Tell focus view we are done mucking with it, if it is still in
      // our view hierarchy.
      if (focusLayoutRestoreView != null
      && focusLayoutRestoreView.getWindowToken() != null) {
      focusLayoutRestoreView.dispatchFinishTemporaryDetach();
      }

      mLayoutMode = LAYOUT_NORMAL;
      mDataChanged = false;
      if (mPositionScrollAfterLayout != null) {
      post(mPositionScrollAfterLayout);
      mPositionScrollAfterLayout = null;
      }
      mNeedSync = false;
      setNextSelectedPositionInt(mSelectedPosition);

      updateScrollIndicators();

      if (mItemCount > 0) {
      checkSelectionChanged();
      }

      invokeOnItemScrollListener();
      } finally {
      if (mFocusSelector != null) {
      mFocusSelector.onLayoutComplete();
      }
      if (!blockLayoutRequests) {
      mBlockLayoutRequests = false;
      }
      }
      }

      private View fillSpecific(int position, int top) {
      boolean tempIsSelected = position == mSelectedPosition;
      // ↓↓↓ 6.3 获取缓存View并显示
      View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
      // Possibly changed again in fillUp if we add rows above this one.
      mFirstPosition = position;

      View above;
      View below;

      final int dividerHeight = mDividerHeight;
      if (!mStackFromBottom) {
      // ↓↓↓ 6.8 向上填满view
      above = fillUp(position - 1, temp.getTop() - dividerHeight);
      // This will correct for the top of the first view not touching the top of the list
      adjustViewsUpOrDown();
      // ↓↓↓ 6.9 向下填满view
      below = fillDown(position + 1, temp.getBottom() + dividerHeight);
      int childCount = getChildCount();
      if (childCount > 0) {
      correctTooHigh(childCount);
      }
      } else {
      below = fillDown(position + 1, temp.getBottom() + dividerHeight);
      // This will correct for the bottom of the last view not touching the bottom of the list
      adjustViewsUpOrDown();
      above = fillUp(position - 1, temp.getTop() - dividerHeight);
      int childCount = getChildCount();
      if (childCount > 0) {
      correctTooLow(childCount);
      }
      }

      if (tempIsSelected) {
      return temp;
      } else if (above != null) {
      return above;
      } else {
      return below;
      }
      }

      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
      // ↓↓↓ 6.4 从RecycleBin里获取缓存的View
      child = mRecycler.getActiveView(position);
      if (child != null) {
      // Found it -- we're using an existing child
      // This just needs to be positioned
      // ↓↓↓ 6.5 获取到view后执行该方法
      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;
      }

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

      // 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);
      p.isEnabled = mAdapter.isEnabled(position);

      // ↓↓↓ 6.6 recycled = true 这是参数传进来的, 第一次onLayout时p.forceAdd被标记为false. (p.forceAdd = false;)
      if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
      && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
      // ↓↓↓ 6.7 因此执行这句, 让所有View都处于attach状态, 即显示
      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);
      }

      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) {
      final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
      mListPadding.left + mListPadding.right, p.width);
      final int lpHeight = p.height;
      final int childHeightSpec;
      if (lpHeight > 0) {
      childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
      } else {
      childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
      MeasureSpec.UNSPECIFIED);
      }
      child.measure(childWidthSpec, childHeightSpec);
      } else {
      cleanupLayoutState(child);
      }

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

      if (mCachingStarted && !child.isDrawingCacheEnabled()) {
      child.setDrawingCacheEnabled(true);
      }

      if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition)
      != position) {
      child.jumpDrawablesToCurrentState();
      }

      Trace.traceEnd(Trace.TRACE_TAG_VIEW);
      }
      • 第二次onLayout主要做的是: 将屏幕上的View全部缓存到RecycleBin里, 然后将他们从父容器中清除(detach);
      • 然后从RecycleBin里获取缓存的View, 并将他们添加到父容器(attach), 然后依次向上, 向下填充满屏幕
      • 另外 attachViewToParent 和 detachAllViewsFromParent 是组合使用的
  • ListView初始化过程图
    Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)

ListView滑动加载逻辑

  • 两次的onLayout只是初始化了第一屏的view, 当我们手指上下滑的时候, ListView会重复利用滑出屏幕的条目, 这才是ListView的神奇之处. 想要了解滑动事件是如何处理的, 那么就要从 onTouchEvent() 入手.

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

    if (mPositionScroller != null) {
    mPositionScroller.stop();
    }

    if (mIsDetaching || !isAttachedToWindow()) {
    // Something isn't right.
    // Since we rely on being attached to get data set change notifications,
    // don't risk doing anything where we might try to resync and find things
    // in a bogus state.
    return false;
    }

    startNestedScroll(SCROLL_AXIS_VERTICAL);

    if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
    return true;
    }

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

    // ↓↓↓ 1. 各种的触摸事件, 我们只需关心移动事件Move即可
    case MotionEvent.ACTION_MOVE: {
    onTouchMove(ev, vtev);
    break;
    }

    case MotionEvent.ACTION_UP: {
    onTouchUp(ev);
    break;
    }

    case MotionEvent.ACTION_CANCEL: {
    onTouchCancel();
    break;
    }

    case MotionEvent.ACTION_POINTER_UP: {
    onSecondaryPointerUp(ev);
    final int x = mMotionX;
    final int y = mMotionY;
    final int motionPosition = pointToPosition(x, y);
    if (motionPosition >= 0) {
    // Remember where the motion event started
    final View child = getChildAt(motionPosition - mFirstPosition);
    mMotionViewOriginalTop = child.getTop();
    mMotionPosition = motionPosition;
    }
    mLastY = y;
    break;
    }

    case MotionEvent.ACTION_POINTER_DOWN: {
    // New pointers take over dragging duties
    final int index = ev.getActionIndex();
    final int id = ev.getPointerId(index);
    final int x = (int) ev.getX(index);
    final int y = (int) ev.getY(index);
    mMotionCorrection = 0;
    mActivePointerId = id;
    mMotionX = x;
    mMotionY = y;
    final int motionPosition = pointToPosition(x, y);
    if (motionPosition >= 0) {
    // Remember where the motion event started
    final View child = getChildAt(motionPosition - mFirstPosition);
    mMotionViewOriginalTop = child.getTop();
    mMotionPosition = motionPosition;
    }
    mLastY = y;
    break;
    }
    }

    if (mVelocityTracker != null) {
    mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
    }

    private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    if (mHasPerformedLongPress) {
    // Consume all move events following a successful long press.
    return;
    }

    int pointerIndex = ev.findPointerIndex(mActivePointerId);
    if (pointerIndex == -1) {
    pointerIndex = 0;
    mActivePointerId = ev.getPointerId(pointerIndex);
    }

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

    final int y = (int) ev.getY(pointerIndex);

    // ↓↓↓ 2. 这里的 mTouchMode = TOUCH_MODE_SCROLL
    switch (mTouchMode) {
    case TOUCH_MODE_DOWN:
    case TOUCH_MODE_TAP:
    case TOUCH_MODE_DONE_WAITING:
    // Check if we have moved far enough that it looks more like a
    // scroll than a tap. If so, we'll enter scrolling mode.
    if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
    break;
    }
    // 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]);
    }
    break;
    case TOUCH_MODE_SCROLL:
    case TOUCH_MODE_OVERSCROLL:
    // ↓↓↓ 3. 于是也就执行该分支的代码
    scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
    break;
    }
    }

    private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
    int rawDeltaY = y - mMotionY;
    int scrollOffsetCorrection = 0;
    int scrollConsumedCorrection = 0;
    if (mLastY == Integer.MIN_VALUE) {
    rawDeltaY -= mMotionCorrection;
    }
    if (dispatchNestedPreScroll(0, mLastY != Integer.MIN_VALUE ? mLastY - y : -rawDeltaY, mScrollConsumed, mScrollOffset)) {
    rawDeltaY += mScrollConsumed[1];
    scrollOffsetCorrection = -mScrollOffset[1];
    scrollConsumedCorrection = mScrollConsumed[1];
    if (vtev != null) {
    vtev.offsetLocation(0, mScrollOffset[1]);
    mNestedYOffset += mScrollOffset[1];
    }
    }
    final int deltaY = rawDeltaY;
    int incrementalDeltaY =
    mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
    int lastYCorrection = 0;

    if (mTouchMode == TOUCH_MODE_SCROLL) {
    if (PROFILE_SCROLLING) {
    if (!mScrollProfilingStarted) {
    Debug.startMethodTracing("AbsListViewScroll");
    mScrollProfilingStarted = true;
    }
    }

    if (mScrollStrictSpan == null) {
    // If it's non-null, we're already in a scroll.
    mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll");
    }

    if (y != mLastY) {
    // We may be here after stopping a fling and continuing to scroll.
    // If so, we haven't disallowed intercepting touch events yet.
    // Make sure that we do so in case we're in a parent that can intercept.
    if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 && Math.abs(rawDeltaY) > mTouchSlop) {
    final ViewParent parent = getParent();
    if (parent != null) {
    parent.requestDisallowInterceptTouchEvent(true);
    }
    }

    final int motionIndex;
    if (mMotionPosition >= 0) {
    motionIndex = mMotionPosition - mFirstPosition;
    } else {
    // If we don't have a motion position that we can reliably track,
    // pick something in the middle to make a best guess at things below.
    motionIndex = getChildCount() / 2;
    }

    int motionViewPrevTop = 0;
    View motionView = this.getChildAt(motionIndex);
    if (motionView != null) {
    motionViewPrevTop = motionView.getTop();
    }

    // No need to do all this work if we're not going to move anyway
    boolean atEdge = false;
    // ↓↓↓ 4. 如果移动增量不为0, 说明用户进行了移动操作, 执行该方法
    if (incrementalDeltaY != 0) {
    // ↓↓↓ 4.1 执行该方法, 传递按下的Y坐标, 和Y轴移动增量
    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
    }

    // Check to see if we have bumped into the scroll limit
    motionView = this.getChildAt(motionIndex);
    if (motionView != null) {
    // Check if the top of the motion view is where it is
    // supposed to be
    final int motionViewRealTop = motionView.getTop();
    if (atEdge) {
    // Apply overscroll

    int overscroll = -incrementalDeltaY - (motionViewRealTop - motionViewPrevTop);
    if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll, mScrollOffset)) {
    lastYCorrection -= mScrollOffset[1];
    if (vtev != null) {
    vtev.offsetLocation(0, mScrollOffset[1]);
    mNestedYOffset += mScrollOffset[1];
    }
    } else {
    final boolean atOverscrollEdge = overScrollBy(0, overscroll, 0, mScrollY, 0, 0, 0, mOverscrollDistance, true);

    if (atOverscrollEdge && mVelocityTracker != null) {
    // Don't allow overfling if we're at the edge
    mVelocityTracker.clear();
    }

    final int overscrollMode = getOverScrollMode();
    if (overscrollMode == OVER_SCROLL_ALWAYS ||
    (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
    if (!atOverscrollEdge) {
    mDirection = 0; // Reset when entering overscroll.
    mTouchMode = TOUCH_MODE_OVERSCROLL;
    }
    if (incrementalDeltaY > 0) {
    mEdgeGlowTop.onPull((float) -overscroll / getHeight(), (float) x / getWidth());
    if (!mEdgeGlowBottom.isFinished()) {
    mEdgeGlowBottom.onRelease();
    }
    invalidateTopGlow();
    } else if (incrementalDeltaY < 0) {
    mEdgeGlowBottom.onPull((float) overscroll / getHeight(), 1.f - (float) x / getWidth());
    if (!mEdgeGlowTop.isFinished()) {
    mEdgeGlowTop.onRelease();
    }
    invalidateBottomGlow();
    }
    }
    }
    }
    mMotionY = y + lastYCorrection + scrollOffsetCorrection;
    }
    mLastY = y + lastYCorrection + scrollOffsetCorrection;
    }
    } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
    // ...
    }
    }

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

    // ↓↓↓ 5. 当 incrementalDeltaY < 0 时, 表示向下滑动, 否则为向上滑动
    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;

    // ↓↓↓ 6. 当向下滑动时
    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++) {
    // ↓↓↓ 6.1 获取子View
    final View child = getChildAt(i);
    if (child.getBottom() >= top) {
    break;
    } else {
    // ↓↓↓ 6.2 如果子View的bottom < top 时, 则说明子View已经滑出屏幕了
    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();
    // ↓↓↓ 6.3 将其添加到RecycleBin的废弃View里, 并count++
    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) {
    // ↓↓↓ 7. 如果有需要被回收的View, 则将这些View从父容器中清除 (detach)
    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();
    }

    // ↓↓↓ 8. 然后根据手指滑动的增量incrementalDeltaY, 移动这些View, 也就产生了listview里的itemView的移动效果
    offsetChildrenTopAndBottom(incrementalDeltaY);

    if (down) {
    mFirstPosition += count;
    }

    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    // ↓↓↓ 9. 如果一个View的底部移入了屏幕, 或者 一个View的顶部移入了屏幕, 则执行该方法
    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;
    }

    abstract void fillGap(boolean down);

    void fillGap(boolean down) {
    final int count = getChildCount();
    // ↓↓↓ 9.1 如果向下滑
    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;
    // ↓↓↓ 9.2 则执行该方法, 来填充View
    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());
    }
    }

    private View fillDown(int pos, int nextTop) {
    // ...

    while (nextTop < end && pos < mItemCount) {
    // is this the selected item?
    boolean selected = pos == mSelectedPosition;
    // ↓↓↓ 9.3 就需要去创建或者获取需要的View
    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;
    }

    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
    // ↓↓↓ 9.4 获取缓存的View, 既然View都已经展示在屏幕上了, 那么就没有可用的缓存View 了
    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
    // ↓↓↓ 9.5 于是就会调用该方法去获取
    child = obtainView(position, mIsScrap);

    // This needs to be positioned and measured
    // ↓↓↓ 9.8 获取到View后, 执行该方法
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

    return child;
    }

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

    isScrap[0] = false;

    // ...

    // ↓↓↓ 9.6 获取一个废弃的View, 可能获取到, 可能获取不到
    final View scrapView = mRecycler.getScrapView(position);
    // ↓↓↓ 9.7 从适配器里获取一个View, 这是必然能获取到的, 因为getView参数收了scrapView, 如果该scrapView的不为null, 我们将赋值, 为null, 我们将创建
    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()) {
    isScrap[0] = true;

    // Finish the temporary detach started in addScrapView().
    child.dispatchFinishTemporaryDetach();
    } else {
    // we set isScrap to "true" only if the view is temporarily detached.
    // if the view is fully detached, it is as good as a view created by the
    // adapter
    isScrap[0] = false;
    }

    }
    }

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

    if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    }

    setItemViewLayoutParams(child, position);

    if (AccessibilityManager.getInstance(mContext).isEnabled()) {
    if (mAccessibilityDelegate == null) {
    mAccessibilityDelegate = new ListItemAccessibilityDelegate();
    }
    if (child.getAccessibilityDelegate() == null) {
    child.setAccessibilityDelegate(mAccessibilityDelegate);
    }
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);

    return child;
    }

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

    // 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);
    p.isEnabled = mAdapter.isEnabled(position);

    if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
    // 9.9 然后就将View添加到父容器中 (attach), 基本流程就到这里结束了
    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);
    }

    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) {
    final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
    mListPadding.left + mListPadding.right, p.width);
    final int lpHeight = p.height;
    final int childHeightSpec;
    if (lpHeight > 0) {
    childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
    } else {
    childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
    MeasureSpec.UNSPECIFIED);
    }
    child.measure(childWidthSpec, childHeightSpec);
    } else {
    cleanupLayoutState(child);
    }

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

    if (mCachingStarted && !child.isDrawingCacheEnabled()) {
    child.setDrawingCacheEnabled(true);
    }

    if (recycled && (((AbsListView.LayoutParams)child.getLayoutParams()).scrappedFromPosition)
    != position) {
    child.jumpDrawablesToCurrentState();
    }

    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    • 可见ListView在向下滑动时, 会将移出屏幕的View添加到RecycleBin的废弃View里, 然后将其标记为detach状态, 然后移动屏幕上的View, 让他们产生对应的滑动
    • 同时, 屏幕上方的View会进入屏幕, 首先从RecycleBin里获取缓存View, 获取不到就到Adapter里去获取
  • ListView滑动过程图
    Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)

异步加载图片错位解决方案

  • 通过设置Tag的方式解决:
    • 方式一:
      • ImageView.setTag(imageUrl); // 对ImageView设置Tag
      • ImageView imageView = (ImageView) ListView.findViewWithTag(imageUrl); // 从ListVIew中寻找Tag,返回不为null说明该ImageView还在展示中,为其设置图片
      • 原理: 由于ImageView是被不断的回收利用的, 每次对同一个ImageView对象设置Tag都会被覆盖, 所以以前的ImageView的Tag是找不着的.
  • 案例:

    • 方式一实现代码:

      • Adapter代码, 跟往常一样

        public class Case1Adapter extends BaseAdapter {
        private LayoutInflater inflater;
        private List<String> mDatas;

        public Case1Adapter(Context context, List<String> datas) {
        this.inflater = LayoutInflater.from(context);
        this.mDatas = datas;
        }

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

        @Override
        public Object getItem(int i) {
        return mDatas.get(i);
        }

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

        @Override
        public View getView(int i, View convertView, ViewGroup viewGroup) {
        ViewHolder viewHolder;
        if(convertView == null){
        convertView = inflater.inflate(R.layout.item, null);
        viewHolder = new ViewHolder();

        viewHolder.sync = (TextView)convertView.findViewById(R.id.sync);
        viewHolder.async = (TextView)convertView.findViewById(R.id.async);

        convertView.setTag(viewHolder);
        }else{
        viewHolder = (ViewHolder) convertView.getTag();
        }

        viewHolder.sync.setText("当前位置为: " + i);

        if (mDatas.size() != 0) {
        AsyncUtil.ayncLoaderData((ListView) viewGroup, viewHolder.async, mDatas.get(i));
        }

        return convertView;
        }

        class ViewHolder{
        TextView sync;
        TextView async;
        }
        }
      • 这里主要看DataTask线程池里的操作

        public class AsyncUtil {

        public static void ayncLoaderData(ListView listView, TextView textView, String s) {
        if (textView == null || s == null) return;

        textView.setEnabled(false);

        new DataTask(listView, textView, s).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }

        /**
        * 线程池
        * @author Luzhuo
        */

        private static class DataTask extends AsyncTask<Object, Void, Void>{
        private ListView mListView;
        private TextView mTextView;
        private String mData;

        public DataTask(ListView listView, TextView textView, String s) {
        this.mListView = listView;
        this.mTextView = textView;
        this.mData = s;
        }

        @Override
        protected void onPreExecute() {
        // 设置Tag
        mTextView.setTag(mData);
        }

        /**
        * 设置延迟, 模拟图片的网络请求耗时
        */

        @Override
        protected Void doInBackground(Object... params) {
        SystemClock.sleep(500);
        return null;
        }

        /**
        * 设置文字, 模拟图片设置
        * @param result
        */

        @Override
        protected void onPostExecute(Void result) {
        // 获取有该Tag的View
        TextView tv = (TextView) mListView.findViewWithTag(mData);
        if (tv == null) return;

        mTextView.setText("异步数据: " + mData);
        mTextView.setEnabled(true);
        }
        }
        }
      • 效果图:

        • 未做处理的效果
          Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)
        • 使用方式一处理后的效果
          Android_ListView (基本使用 / RecycleBin机制 / 源码解析 / 异步图片错位解决方案)