美文网首页
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