美文网首页
ListView源码分析-下篇

ListView源码分析-下篇

作者: leilifengxingmw | 来源:发表于2021-01-31 14:36 被阅读0次

源码版本:28

本篇要点:滑动的时候是怎么回收View和填充新的View的。

AbsListView的onTouchEvent方法。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    //...
    initVelocityTrackerIfNotExists();
    final MotionEvent vtev = MotionEvent.obtain(ev);

    final int actionMasked = ev.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        mNestedYOffset = 0;
    }
    vtev.offsetLocation(0, mNestedYOffset);

    switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {
            onTouchDown(ev);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            onTouchMove(ev, vtev);
            break;
        }
        case MotionEvent.ACTION_UP: {
            onTouchUp(ev);
            break;
        }
       //...
    }
    if (mVelocityTracker != null) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
}

AbsListView的onTouchDown方法。

private void onTouchDown(MotionEvent ev) {
    //...   
    if (mTouchMode == TOUCH_MODE_OVERFLING) {
        //MotionEvent.ACTION_DOWN的时候,如果正在OVERFLING,则停止
        mFlingRunnable.endFling();
        if (mPositionScroller != null) {
            mPositionScroller.stop();
        }
        //将触摸模式置为TOUCH_MODE_OVERSCROLL
        mTouchMode = TOUCH_MODE_OVERSCROLL;
        mMotionX = (int) ev.getX();
        mMotionY = (int) ev.getY();
        mLastY = mMotionY;
        mMotionCorrection = 0;
        mDirection = 0;
    } else {
        final int x = (int) ev.getX();
        final int y = (int) ev.getY();
        //将坐标转化为转化为ListView中子View的位置
        int motionPosition = pointToPosition(x, y);

        if (!mDataChanged) {
            if (mTouchMode == TOUCH_MODE_FLING) {
                // Stopped a fling. It is a scroll.
                createScrollingCache();
                mTouchMode = TOUCH_MODE_SCROLL;
                mMotionCorrection = 0;
                motionPosition = findMotionRow(y);
                //注释1处,根据当前速度检查是否要停止fling
                mFlingRunnable.flywheelTouch();
            } else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) {
                //用户正常的按下操作,不是停止一个fling的操作,将mTouchMode置为TOUCH_MODE_DOWN
                mTouchMode = TOUCH_MODE_DOWN;
                //...
            }
        }

        if (motionPosition >= 0) {
            // Remember where the motion event started
            final View v = getChildAt(motionPosition - mFirstPosition);
            mMotionViewOriginalTop = v.getTop();
        }
    
        //按下时候的水平坐标
        mMotionX = x;
        //按下时候的竖直坐标
        mMotionY = y;
        mMotionPosition = motionPosition;
        //按下的时候,将mLastY重置为Integer.MIN_VALUE。
        mLastY = Integer.MIN_VALUE;
    }
    //...
}

注释1处,用户按下时,如果ListView当前正在fling,则需要检查根据速度检查是否要停止fling。

AbsListView的onTouchMove方法。

private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
    int pointerIndex = ev.findPointerIndex(mActivePointerId);
    if (pointerIndex == -1) {
        pointerIndex = 0;
        mActivePointerId = ev.getPointerId(pointerIndex);
    }
  
    if (mDataChanged) {
        layoutChildren();
    }

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

    switch (mTouchMode) {
        case TOUCH_MODE_DOWN:
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            //注释1处,检查是否需要滑动,true的话,直接break。
            if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
                break;
            }
            //...
            break;
        case TOUCH_MODE_SCROLL:
        case TOUCH_MODE_OVERSCROLL:
            //注释2处,如果需要直接滑动
            scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
            break;
    }
}

注释1处,AbsListView的startScrollIfNeeded方法。

private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
    //检查是否移动了足够远的距离,足够远的话,则认为是滚动。
    //移动的距离,指从下向上滑动,rawDeltaY小于0
    final int deltaY = y - mMotionY;
    final int distance = Math.abs(deltaY);
    final boolean overscroll = mScrollY != 0;
    if ((overscroll || distance > mTouchSlop) &&
            (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
        //(处于overscroll状态或者移动的距离足够远)并且(不是嵌套滑动状态)

        createScrollingCache();
        if (overscroll) {
            mTouchMode = TOUCH_MODE_OVERSCROLL;
            mMotionCorrection = 0;
        } else {
            mTouchMode = TOUCH_MODE_SCROLL;
            //指从下向上滑动,rawDeltaY为小于0,mMotionCorrection小于0
            mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
        }
        removeCallbacks(mPendingCheckForLongPress);
        setPressed(false);
        final View motionView = getChildAt(mMotionPosition - mFirstPosition);
        if (motionView != null) {
            motionView.setPressed(false);
        }
        //报告滑动状态是SCROLL_STATE_TOUCH_SCROLL
        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);
        }
        //注释1处,重点方法
        scrollIfNeeded(x, y, vtev);
        return true;
    }

    return false;
}

注释1处,重点方法,AbsListView的scrollIfNeeded方法。

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
    //手指从下向上滑动,rawDeltaY小于0
    int rawDeltaY = y - mMotionY;
    int scrollOffsetCorrection = 0;
    int scrollConsumedCorrection = 0;
    //正常MotionEvent的down事件的时候会将mLastY置为Integer.MIN_VALUE
    if (mLastY == Integer.MIN_VALUE) {
        //减去一个mTouchSlop的量,不影响
        rawDeltaY -= mMotionCorrection;
    }
    //...
    final int deltaY = rawDeltaY;
    //滑动增加的距离
    int incrementalDeltaY =
            mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
    int lastYCorrection = 0;
    //我们只看TOUCH_MODE_SCROLL的情况,忽略TOUCH_MODE_OVERSCROLL的情况。哈哈,偷个懒。
    if (mTouchMode == TOUCH_MODE_SCROLL) {
        if (y != mLastY) {
            //...

            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;
            if (incrementalDeltaY != 0) {
                //注释1处
                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) {
                    // 应用 overscroll,忽略 ...
                }
                mMotionY = y + lastYCorrection + scrollOffsetCorrection;
            }
            mLastY = y + lastYCorrection + scrollOffsetCorrection;
        }
    } 
}

注释1处,AbsListView的trackMotionScroll方法。

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;

    int effectivePaddingTop = 0;
    int effectivePaddingBottom = 0;
    //...
    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);
       
    //注释0处,不能滑动,直接返回
    if (cannotScrollDown || cannotScrollUp) {
        return incrementalDeltaY != 0;
    }
    //注释1处,手指是从下向上滑动,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;

    if (down) {
        //注释2处,手指是从下向上滑动,incrementalDeltaY < 0,所以top是大于0的。
        int top = -incrementalDeltaY;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            top += listPadding.top;
        }
        //注释3处,for循环
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getBottom() >= top) {
                break;
            } else {
                //注释3.1处,
                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();
                    //注释3.2处
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    } else {
        //注释4处,手指是从上向下滑动,incrementalDeltaY > 0
        int bottom = getHeight() - incrementalDeltaY;
        //...
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= bottom) {
                break;
            } else {
                //注释4.1
                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();
                    //注释4.2
                    mRecycler.addScrapView(child, position);
                }
            }
        }
    }

    mMotionViewNewTop = mMotionViewOriginalTop + deltaY;

    mBlockLayoutRequests = true;
        
    //注释5处,滚动出去的子View和ListView解除关联
    if (count > 0) {
        detachViewsFromParent(start, count);
        mRecycler.removeSkippedScrap();
    }

    //...

    //注释6处,核心滚动代码,根据incrementalDeltaY同步偏移所有的子View
    offsetChildrenTopAndBottom(incrementalDeltaY);

    if (down) {
        mFirstPosition += count;
    }

    final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
    //手指从上向下滑动,down为false,spaceAbove < absIncrementalDeltaY,表示第一个子View已经完全滑进屏幕了。需要填充新的子View。
    //手指从下向上滑动,down为true,spaceBelow < absIncrementalDeltaY,表示最后一个子View已经完全滑进屏幕了。需要填充新的子View。
    if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
        //注释7处,填充新的子View
        fillGap(down);
    }

    mRecycler.fullyDetachScrapViews();
    //...
    invokeOnItemScrollListener();

    return false;
}

注释0处,不能滑动,直接返回。

注释1处,手指是从下向上滑动,incrementalDeltaY < 0。

注释2处,手指是从下向上滑动,incrementalDeltaY < 0,所以top是大于0的。

注释3处,for循环是什么意思呢?解释一下:

  1. 比如我们手指从下向上滑动,滑动了500像素,那么incrementalDeltaY = -500top = -incrementalDeltaY =500
  2. 那么ListView整体是要向上滑动500像素的。遍历ListView的所有的子View,View的bottom坐标小于500像素的都要滑出屏幕。

注释3.1处,记录要滑出的View的数量,注释3.2处将要滑出屏幕的View加入到 RecyclerBin的mScrapViews数组中等待以后复用。

同理,注释4处,手指是从上向下滑动,incrementalDeltaY > 0。

  1. 比如我们手指从上向下滑动,滑动了500像素,那么incrementalDeltaY = 500。比如ListView的getHeight()=2000,那么bottom = getHeight() - incrementalDeltaY = 1500

  2. 那么ListView整体是要向下滑动500像素的。遍历ListView的所有的子View,View的top坐标大于1500像素的都要从滑出屏幕。

注释4.1处,记录要滑出的View的数量,注释4.2处将要滑出屏幕的View加入到RecyclerBin的mScrapViews数组中等待以后复用。

注释5处,滚动出去的子View和ListView解除关联。

注释6处,核心滚动代码,根据incrementalDeltaY同步偏移所有的子View。

/**
 * 将所有子View的竖直方向上的位置偏移指定的像素数。
 *
 * @param offset 指定的像素偏移量。
 */
public void offsetChildrenTopAndBottom(int offset) {
    final int count = mChildrenCount;
    final View[] children = mChildren;
    boolean invalidate = false;

    for (int i = 0; i < count; i++) {
        final View v = children[i];
        v.mTop += offset;
        v.mBottom += offset;
        if (v.mRenderNode != null) {
            invalidate = true;
            v.mRenderNode.offsetTopAndBottom(offset);
        }
    }

    if (invalidate) {
        invalidateViewProperty(false, false);
    }
    notifySubtreeAccessibilityStateChangedIfNeeded();
}

ListView正是通过这种将所以子View偏移指定的像素数来实现滚动效果的,偏移出屏幕的子View会和ListView取消关联,并加入到RecyclerBin的mScrapViews数组中等待以后复用。

注意:这里顺便提一嘴,ScrollView是修改自身的scrollY来实现的滚动,从而显示所有的子View的,和ListView是有区别的,注意一下。

注释7处,当我们滑动的时候,会将一些子View滑出屏幕,也需要填充新的子View到屏幕中。

@Override
void fillGap(boolean down) {
    final int count = getChildCount();
    if (down) {
        int paddingTop = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            paddingTop = getListPaddingTop();
        }
        final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                paddingTop;
        //手指从下向上滑动,down为true,向下填充新的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;
        //手指从上向下滑动,down为false,向上填充新的View。
        fillUp(mFirstPosition - 1, startOffset);
        correctTooLow(getChildCount());
    }
}

AbsListView的onTouchUp方法

private void onTouchUp(MotionEvent ev) {
    switch (mTouchMode) {
        case TOUCH_MODE_SCROLL:
        final int childCount = getChildCount();
        if (childCount > 0) {
            final int firstChildTop = getChildAt(0).getTop();
            final int lastChildBottom = getChildAt(childCount - 1).getBottom();
            final int contentTop = mListPadding.top;
            final int contentBottom = getHeight() - mListPadding.bottom;
            if (mFirstPosition == 0 && firstChildTop >= contentTop &&
                    mFirstPosition + childCount < mItemCount &&
                    lastChildBottom <= getHeight() - contentBottom) {
                mTouchMode = TOUCH_MODE_REST;
                //注释1处
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            } else {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);

                final int initialVelocity = (int)
                        (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
                // Fling if we have enough velocity and we aren't at a boundary.
                // Since we can potentially overfling more than we can overscroll, don't
                // allow the weird behavior where you can scroll to a boundary then
                // fling further.
                boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
                if (flingVelocity &&
                        !((mFirstPosition == 0 &&
                                firstChildTop == contentTop - mOverscrollDistance) ||
                          (mFirstPosition + childCount == mItemCount &&
                                lastChildBottom == contentBottom + mOverscrollDistance))) {
                        if (!dispatchNestedPreFling(0, -initialVelocity)) {
                        if (mFlingRunnable == null) {
                            mFlingRunnable = new FlingRunnable();
                        }
                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                        //注释2处,可以fling    
                        mFlingRunnable.start(-initialVelocity);
                    } 
                } 
            }
        } 
        break;
        //...
    }
    //...
}

注释1处,手指抬起的时候没有触发fling,就将滑动状态置为SCROLL_STATE_IDLE。

注释2处,手指抬起的时候可以fling,直接起飞。

FlingRunnable类实现了Runnable接口。

FlingRunnable的start方法。

void start(int initialVelocity) {
    int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
    mLastFlingY = initialY;
    mScroller.setInterpolator(null);
    //注释1处,调用OverScroller的fling方法开始根据速度计算下一帧的坐标
    mScroller.fling(0, initialY, 0, initialVelocity,
            0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
    //注释2处,将mTouchMode置为TOUCH_MODE_FLING
    mTouchMode = TOUCH_MODE_FLING;
    mSuppressIdleStateChangeCall = false;
    //注释3处,post一个runnable对象,下一帧到来的时候会调用run方法。
    postOnAnimation(this);

    if (PROFILE_FLINGING) {
        if (!mFlingProfilingStarted) {
            Debug.startMethodTracing("AbsListViewFling");
            mFlingProfilingStarted = true;
        }
    }
}

注释1处,调用OverScroller的fling方法开始根据速度计算下一帧的坐标。

注释2处,将mTouchMode置为TOUCH_MODE_FLING。

注释3处,post一个runnable对象,下一帧到来的时候会调用FlingRunnable的run方法。

FlingRunnable的run方法。

@Override
public void run() {
    switch (mTouchMode) {
        default:
            //停止fling
            endFling();
        return;

    case TOUCH_MODE_SCROLL:
        //fling已经结束了,直接return
        if (mScroller.isFinished()) {
            return;
        }
        // Fall through
        case TOUCH_MODE_FLING: {
            if (mDataChanged) {
                layoutChildren();
            }

            if (mItemCount == 0 || getChildCount() == 0) {
                endFling();
                return;
            }

            final OverScroller scroller = mScroller;
            //注释1处,more为true表示fling未结束
            boolean more = scroller.computeScrollOffset();
            //要到达的坐标
            final int y = scroller.getCurrY();

            // Flip sign to convert finger direction to list items direction
            //注释2处,上一帧和本次的滚动距离差值    
            int delta = mLastFlingY - y;

            // Pretend that each frame of a fling scroll is a touch scroll
            if (delta > 0) {
                //向上滚动。 List is moving towards the top. Use first view as mMotionPosition
                mMotionPosition = mFirstPosition;
                final View firstView = getChildAt(0);
                mMotionViewOriginalTop = firstView.getTop();

                // Don't fling more than 1 screen
                delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
            } else {
                //向下滚动。 List is moving towards the bottom. Use last view as mMotionPosition
                int offsetToLast = getChildCount() - 1;
                mMotionPosition = mFirstPosition + offsetToLast;

                final View lastView = getChildAt(offsetToLast);
                mMotionViewOriginalTop = lastView.getTop();

                // Don't fling more than 1 screen
                delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
            }

            // Check to see if we have bumped into the scroll limit
            View motionView = getChildAt(mMotionPosition - mFirstPosition);
            int oldTop = 0;
            if (motionView != null) {
                oldTop = motionView.getTop();
            }

            // Don't stop just because delta is zero (it could have been rounded)
            //注释3处,调用trackMotionScroll回收View,填充新的View。
            final boolean atEdge = trackMotionScroll(delta, delta);
            final boolean atEnd = atEdge && (delta != 0);
            if (atEnd) {
                //处理overScroll的情况,忽略。。。
            }
            if (more && !atEnd) {
                if (atEdge) invalidate();
                //注释4处
                mLastFlingY = y;
                postOnAnimation(this);
            } else {
                //注释5处
                endFling();
            }
            break;
        }
           
    }
}

注释1处,more为true表示fling未结束。

注释2处,计算上一帧和本次的滚动距离差值 。delta > 0 表示向上滚动否则是向下滚动。

注释3处,调用trackMotionScroll缓存滑出屏幕View,填充新的View。

注释4处,fling还没有结束,重新为mLastFlingY赋值,并继续postFlingRunnable,再下一帧到来的时候继续处理。

注释5处,如果fling结束那就结束。

FlingRunnable的endFling方法。

void endFling() {
    //将mTouchMode置为TOUCH_MODE_REST
    mTouchMode = TOUCH_MODE_REST;
    //移除掉FlingRunnable,不再响应下一帧
    removeCallbacks(this);
    removeCallbacks(mCheckFlywheel);
   //...

}

总结:ListView的源码学习到此告一段落。源码阅读起来感觉不是太难,哈哈,主要是没有去抠每一个细节。接下来没有变化的话,要开始写RecyclerView相关的源码分析了。

参考链接:

相关文章

网友评论

      本文标题:ListView源码分析-下篇

      本文链接:https://www.haomeiwen.com/subject/kzcrtltx.html