ListView源码阅读:TouchEvent滑动事件

之所以上一篇文章没有讲这些,主要因为这些代码我看起来也是晕,不得不说google的工程师写的代码真的是让人佩服,该片主要记录我在研究ListView的滑动事件时的一些看法。接下来直接看代码吧。

Note:我们需要了解以下几个概念
 //表示我们不在触摸手势的中间
    static final int TOUCH_MODE_REST = -1;

 //提示我们收到一个down事件,判断他是轻拍还是滚动事件。
    static final int TOUCH_MODE_DOWN = 0;

 //表示触摸已被识别为轻拍,我们现在正在等待是否触摸事件是一个longperss
    static final int TOUCH_MODE_TAP = 1;

  //表示我们已经等待了所有可以等待的东西,但用户的手指仍在向下
    static final int TOUCH_MODE_DONE_WAITING = 2;

 //表示触摸手势为滚动
    static final int TOUCH_MODE_SCROLL = 3;

 //表示视图正在被抛出
    static final int TOUCH_MODE_FLING = 4;

 //指示触摸手势是一个覆盖滚动-一个滚动超过开始或结束
    static final int TOUCH_MODE_OVERSCROLL = 5;

 //指示视图被抛出到正常内容范围之外会弹回来
    static final int TOUCH_MODE_OVERFLING = 6;
接下来就进入onTouchEvent方法中进一步查看:
 @Override
    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;
        }

        //初始化VelocityTracker函数,用于计算手指滑动速度
        initVelocityTrackerIfNotExists();

        final MotionEvent vtev = MotionEvent.obtain(ev);

        //ev.getActionMasked():多点触控时必须使用该方法来获取Action,与单点触碰时的ev.getAction效果一样
        final int actionMasked = ev.getActionMasked();

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mNestedYOffset = 0;
        }

        vtev.offsetLocation(0, mNestedYOffset);

        /**
         *
         * ACTION_POINTER_UP 有非主要的手指按下(表示还有手指在屏幕上)
         *
         * ACTION_POINTER_DOWN 有非主要的手指抬起(表示还有手指在屏幕上)
         *
         */
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN: {
                onTouchDown(ev);
                break;
            }

            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: {//新的手指按下接管拖曳任务

                //获取当前手指的下标index,和id
                final int index = ev.getActionIndex();
                final int id = ev.getPointerId(index);

                //根据index获取相对于父类的x,y坐标
                final int x = (int) ev.getX(index);
                final int y = (int) ev.getY(index);
                mMotionCorrection = 0;
                mActivePointerId = id;
                mMotionX = x;
                mMotionY = y;

                //判断该触摸点位置是否位于父View的某一个子元素内
                final int motionPosition = pointToPosition(x, y);
                if (motionPosition >= 0) {
                    // 记录实际位置,根据Adapter中元素位置
                    final View child = getChildAt(motionPosition - mFirstPosition);
                    mMotionViewOriginalTop = child.getTop();
                    mMotionPosition = motionPosition;
                }
                mLastY = y;
                break;
            }
        }

        //追踪当前事件的速度
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();
        return true;
    }

在这里我主要关注的时Action=MOVE时的方法,也就是onTouchMove(ev, vtev);其余的一些方法我也做了一些注解,不在多讲,进入onTouchMove看一看。

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

        //根据手指ID获取当前手指Index
        int pointerIndex = ev.findPointerIndex(mActivePointerId);
        //若当前手指不存在,则默认第一个手指的id和Index
        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();
        }

        //根据Index获取当前手指的Y轴坐标相对于当前Parent
        final int y = (int) ev.getY(pointerIndex);

        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:
                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
                break;
        }
    }

通过以上方法可以看出ListView处理了多点触摸事件,当滑动过程中给数据发生改变时,调用layoutChildren方法重新设置数据。在这里根据Action,ListView会调用TOUCH_MODE_OVERSCROLL中的scrollIfNeeded方法进入该方法查看。

/**
     * x:当前手指X轴坐标
     *
     * y:当前手指Y轴坐标
     *
     * vtev:Event事件
     */
    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;

        //调用该方法TOUCH_MODE_SCROLL
        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");
            }

            //前一个Event事件结束后的Y值,若不相等,代表手指滑动了
            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();

                    //若存在Parent则请求不拦截触摸滑动事件
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }

                //根据mMotionPosition获取当前手指所在View位置,若不存在从中间开始
                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
                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) {
                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
                }
                ...
    }

这里的核心代码就是trackMotionScroll该方法,通过我的注释可以看到,当手指在ListView内部滑动,且大于最小滑动距离,有Parent则请求父类不拦截该触摸事件,调用trackMotionScroll实现滑动,咱们进入trackMotionScroll方法看一看。

 // deltaY:手指按下时到手指当前的距离,这是一个总距离
 // incrementalDeltaY:距离上次触发event事件,手指在Y轴上移动的距离 incrementalDeltaY < 0页面向下滑动
 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?
        /**
         * spaceAbove=listPadding.top-getChildAt(0).getTop()
         *
         * 若不设置child的setPadding,则effectivePaddingTop=firstTop=0,则spaceAbove>=0,默认为0
         *
         * 最底部可用bottom
         */
        final int spaceAbove = effectivePaddingTop - firstTop;
        final int end = getHeight() - effectivePaddingBottom;
        final int spaceBelow = lastBottom - end;

        //实际高度去除padding
        final int height = getHeight() - mPaddingBottom - mPaddingTop;

        //确保deltaY在一个合理的范围内
        if (deltaY < 0) {
            deltaY = Math.max(-(height - 1), deltaY);
        } else {
            deltaY = Math.min(height - 1, deltaY);
        }

        //确保incrementalDeltaY在一个合理的范围内,比如最小不能小于-(height - 1),最大不能大于height - 1
        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;
        }

        //<0表示手指上滑y坐标减小,页面下滑
        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;

        //firstPosition为第一个可见的View的position,依据Adapter的实际数量
        //down=true:向下滑动(手指向上滑动) incrementalDeltaY<0
        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++) {

                final View child = getChildAt(i);
                //child.getBottom() >= top,说明当前child说明当前child依旧在屏幕内
                if (child.getBottom() >= top) {
                    break;
                } else {

                    //child.getBottom() < top说明当前child已经移出屏幕。调用RectcleBin机制将该child添加进入废弃缓存中

                    //count计数器用于记录有多少个child被移除屏幕
                    count++;
                    int position = firstPosition + i;
                    if (position >= headerViewsCount && position < footerViewsStart) {
                        // The view will be rebound to new data, clear any
                        // system-managed transient state.
                        child.clearAccessibilityFocus();

                        //将移出屏幕的view添加到废弃缓存中,并记录位置.依据Adapter中数量
                        mRecycler.addScrapView(child, position);
                    }
                }
            }
        } else {

            //向上滑动(手指向下移动)

            //这里使用ListView的可用高度-y轴偏移,是为了计算判断各子View是否会超出屏幕
            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);

                //child.getTop() <= bottom 当前child位于屏幕内
                if (child.getTop() <= bottom) {
                    break;
                } else {

                    //当前child已经移出屏幕,调用RectcleBin机制将该child添加进入废弃缓存中
                    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;

        //count>0表示有child被移出屏幕,调用detachViewsFromParent将所有被移出的child detach调
        if (count > 0) {
            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();
        }

        //调用该法,让所有的child进行相应的偏移,实现滑动效果
        offsetChildrenTopAndBottom(incrementalDeltaY);

        if (down) {
            mFirstPosition += count;
        }

        //滑动距离在ListView内部,则加载屏幕外数据
        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);

        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {

            //加载屏幕外数据
            fillGap(down);
        }

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

        mBlockLayoutRequests = false;

        invokeOnItemScrollListener();

        return false;
    }

这里的代码还是比较长的,我没有做删减,从上到下过一遍吧,可以看到首先计算出了一些变量参数,来看一一看什么意思

 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;

首先获取ListView的子元素,获取第一个子元素的Top,和最后一个元素的Bottom值。这里的mListPadding属于ListView的Padding值,画个图加深一下理解这里写图片描述

 int effectivePaddingTop = 0;
        int effectivePaddingBottom = 0;
        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
            effectivePaddingTop = listPadding.top;
            effectivePaddingBottom = listPadding.bottom;
        }

然后计算出可用空间的Top值和Bottom值,后面通过incrementalDeltaY的正负值来判断页面上滑还是下滑。注意这里页面的上下滑动与手指的移动正好是相反的,比如手指上移,页面下移,incrementalDeltaY<0.代码中注释写的比较清楚,可以看到当View被移除屏幕时,会调用mRecycler.addScrapView方法将移除View添加到废弃缓存中。ListView会调用detachViewsFromParent方法将移除的Viewdetach掉。然后调用fillGap方法加载屏幕内数据,fillGap方法具体实现位于ListView中

/**
     * {@inheritDoc}
     *
     * down=true  向下滑动(手指向上)
     */
    @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();
            }

            //获取需要加载的View的getTop,count>0,则getTop=最后一个View的Bottom+分割线宽度,反之取paddingTop
            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
                    paddingTop;
            fillDown(mFirstPosition + count, startOffset);
            correctTooHigh(getChildCount());
        } else {
            //顶部加载
            int paddingBottom = 0;
            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
                paddingBottom = getListPaddingBottom();
            }
            //获取需要加载的View的getBottom,count>0,则getBottom第一个View的Top+分割线宽度,反之取paddingBottom
            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
                    getHeight() - paddingBottom;
            fillUp(mFirstPosition - 1, startOffset);
            correctTooLow(getChildCount());
        }
    }

这里down==true表示当前手指向上滑,屏幕向下移动,获取当前需要加载的View的Top,然后调用fillDown方法。down==false表示手指向下滑动,屏幕向上移动,获取需要加载的View的Bottom,然后调用fillUp方法,这两个方法之前的博客已经分析过这里只看一些不一样的点。

//ListView 初始化时,mRecycler中缓存的View为null
        if (!mDataChanged) {


            //第二次获取View是从缓存中读取
            final View activeView = mRecycler.getActiveView(position);

            /**
             *   在拉youtchild中第二次调用了recycleBin.fillActiveViews(childCount, firstPosition)缓存了
             *   屏幕显示的所有View,因此直接读取缓存复用,调用setupChild方法填充ListView
             */

            if (activeView != null) {
                // Found it. We're reusing an existing child, so it just needs
                // to be positioned like a scrap view.
                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
                return activeView;
            }
        }

可以看到首先还是会调用mRecycler.getActiveView(position)来获取缓存View,但此时并不会获取到,因为我们之前分析过getActiveView中的View一旦被获取就会被置null,而之前第二次测量已经获取过,所以此时调用以下方法obtainView方法

//从废弃的缓存View列表中获取一个View,mRecycler会根据position获取Type来获取View
        final View scrapView = mRecycler.getScrapView(position);

        //调用Adapter的getView方法获取一个子View
        /**
         * 因为第一次调用时mRecycler钟缓存的废弃view==null,所以直接调用mAdapter.getView方法返回一个View
         *
         * 这里可以看出在我们自定义适配器时要判断convertView是否为null,
         *
         *
         */
        final View child = mAdapter.getView(position, scrapView, this);

        //列表滑出屏幕时,判断是否将scrapView添加进废弃View集合
        if (scrapView != null) {
            if (child != scrapView) {

                //view不匹配,重新缓存到废弃View集合中
                mRecycler.addScrapView(scrapView, position);
            } else if (child.isTemporarilyDetached()) {
                outMetadata[0] = true;

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

这里还是只看主要代码,调用getScrapView获取指定Type的View,此时肯定能获取到,因为移除的View会被自动添加到该废弃缓存中,此时调用mAdapter.getView(position, scrapView, this)获取View,这个方法就比较熟悉了,就是我在适配器中重写的方法,此时若scrapView为null则直接inflate一个View,否则就复用缓存的废弃View,直接替换数据就可以了,这也就是ListView为什么能够加载成百上千数据的原因。

到现在为止,ListView的源码分析也就告了一个段落,写下这两篇文章只是为了加深自己的印象,为了以后复习比较容易,若有写错的地方,就当权当没看见吧!^_^

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值