Android Scroll详解(二):OverScroller实战 + ListView 的 OverScrollBy 两个参数的解析

作者: ztelur 
联系方式:segmentfaultcsdngithub

本文仅供个人学习,不用于任何形式商业目的,转载请注明原作者、文章来源,链接,版权归原文作者所有。

 本文是Android滚动相关的系列文章的第二篇,主要总结一下使用手势相关的代码逻辑。主要是单点拖动,多点拖动,fling和OveScroll的实现。每个手势都会有代码片段。 
 对android滚动相关的知识还不太了解的同学可以先阅读一下文章:

 为了节约你的时间,我特地将文章大致内容总结如下:

  • 手势Drag的实现和原理
  • 手势Fling的实现和原理
  • OverScroll效果和EdgeEffect效果的实现和原理。

 详细代码请查看我的github

Drag

 Drag是最为基本的手势:用户可以使用手指在屏幕上滑动,以拖动屏幕相应内容移动。实现Drag手势其实很简单,步骤如下:

  • ACTION_DOWN事件发生时,调用getXgetY函数获得事件发生的x,y坐标值,并记录在mLastXmLastY变量中。
  • ACTION_MOVE事件发生时,调用getXgetY函数获得事件发生的x,y坐标值,将其与mLastXmLastY比较,如果二者差值大于一定限制(ScaledTouchSlop),就执行scrollBy函数,进行滚动,最后更新mLastXmLastY的值。
  • ACTION_UPACTION_CANCEL事件发生时,清空mLastXmLastY
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int actionId = MotionEventCompat.getActionMasked(event);
        switch (actionId) {
            case MotionEvent.ACTION_DOWN:
                mLastX = event.getX();
                mLastY = event.getY();
                mIsBeingDragged = true;
                if (getParent() != null) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                float curX = event.getX();
                float curY = event.getY();
                int deltaX = (int) (mLastX - curX);
                int deltaY = (int) (mLastY - curY);
                if (!mIsBeingDragged && (Math.abs(deltaX)> mTouchSlop ||
                                                        Math.abs(deltaY)> mTouchSlop)) {
                    mIsBeingDragged = true;
                    // 让第一次滑动的距离和之后的距离不至于差距太大
                    // 因为第一次必须>TouchSlop,之后则是直接滑动
                    if (deltaX > 0) {
                        deltaX -= mTouchSlop;
                    } else {
                        deltaX += mTouchSlop;
                    }
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                // 当mIsBeingDragged为true时,就不用判断> touchSlopg啦,不然会导致滚动是一段一段的
                // 不是很连续
                if (mIsBeingDragged) {
                        scrollBy(deltaX, deltaY);
                        mLastX = curX;
                        mLastY = curY;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                mLastY = 0;
                mLastX = 0;
                break;
            default:
        }
        return mIsBeingDragged;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

多触点Drag

 上边的代码只适用于单点触控的手势,如果你是两个手指触摸屏幕,那么它只会根据你第一个手指滑动的情况来进行屏幕滚动。更为致命的是,当你先松开第一个手指时,由于我们少监听了ACTION_POINTER_UP事件,将会导致屏幕突然滚动一大段距离,因为第二个手指移动事件的x,y值会和第一个手指移动时留下的mLastXmLastY比较,导致屏幕滚动。

 如果我们要监听并处理多触点的事件,我们还需要对ACTION_POINTER_DOWNACTION_POINTER_UP事件进行监听,并且在ACTION_MOVE事件时,要记录所有触摸点事件发生的x,y值。

  • ACTION_POINTER_DOWN事件发生时,我们要记录第二触摸点事件发生的x,y值为mSecondaryLastXmSecondaryLastY,和第二触摸点pointer的id为mSecondaryPointerId
  • ACTION_MOVE事件发生时,我们除了根据第一触摸点pointer的x,y值进行滚动外,也要更新mSecondayLastXmSecondaryLastY
  • ACTION_POINTER_UP事件发生时,我们要先判断是哪个触摸点手指被抬起来啦,如果是第一触摸点,那么我们就将坐标值和pointer的id都更换为第二触摸点的数据;如果是第二触摸点,就只要重置一下数据即可。
        switch (actionId) {
            .....
            case MotionEvent.ACTION_POINTER_DOWN:
                activePointerIndex = MotionEventCompat.getActionIndex(event);
                mSecondaryPointerId = MotionEventCompat.findPointerIndex(event,activePointerIndex);
                mSecondaryLastX = MotionEventCompat.getX(event,activePointerIndex);
                mSecondaryLastY = MotionEventCompat.getY(event,mActivePointerId);
                break;
            case MotionEvent.ACTION_MOVE:
                ......
                // handle secondary pointer move
                if (mSecondaryPointerId != INVALID_ID) {
                    int mSecondaryPointerIndex = MotionEventCompat.findPointerIndex(event, mSecondaryPointerId);
                    mSecondaryLastX = MotionEventCompat.getX(event, mSecondaryPointerIndex);
                    mSecondaryLastY = MotionEventCompat.getY(event, mSecondaryPointerIndex);
                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                //判断是否是activePointer up了
                activePointerIndex = MotionEventCompat.getActionIndex(event);
                int curPointerId  = MotionEventCompat.getPointerId(event,activePointerIndex);
                Log.e(TAG, "onTouchEvent: "+activePointerIndex +" "+curPointerId +" activeId"+mActivePointerId+
                                        "secondaryId"+mSecondaryPointerId);
                if (curPointerId == mActivePointerId) { // active pointer up
                    mActivePointerId = mSecondaryPointerId;
                    mLastX = mSecondaryLastX;
                    mLastY = mSecondaryLastY;
                    mSecondaryPointerId = INVALID_ID;
                    mSecondaryLastY = 0;
                    mSecondaryLastX = 0;
                    //重复代码,为了让逻辑看起来更加清晰
                } else{ //如果是secondary pointer up
                    mSecondaryPointerId = INVALID_ID;
                    mSecondaryLastY = 0;
                    mSecondaryLastX = 0;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_ID;
                mLastY = 0;
                mLastX = 0;
                break;
            default:
        }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

Fling

 当用户手指快速划过屏幕,然后快速立刻屏幕时,系统会判定用户执行了一个Fling手势。视图会快速滚动,并且在手指立刻屏幕之后也会滚动一段时间。Drag表示手指滑动多少距离,界面跟着显示多少距离,而fling是根据你的滑动方向与轻重,还会自动滑动一段距离。Filing手势在android交互设计中应用非常广泛:电子书的滑动翻页、ListView滑动删除item、滑动解锁等。所以如何检测用户的fling手势是非常重要的。 
 在检测Fling时,你需要检测手指在屏幕上滑动的速度,这是你就需要VelocityTrackerScroller这两个类啦。

  • 我们首先使用VelocityTracker.obtain()这个方法获得其实例
  • 然后每次处理触摸时间时,我们将触摸事件通过addMovement方法传递给它
  • 最后在处理ACTION_UP事件时,我们通过computeCurrentVelocity方法获得滑动速度;
  • 我们判断滑动速度是否大于一定数值(MinFlingSpeed),如果大于,那么我们调用Scrollerfling方法。然后调用invalidate()函数。
  • 我们需要重载computeScroll方法,在这个方法内,我们调用ScrollercomputeScrollOffset()方法啦计算当前的偏移量,然后获得偏移量,并调用scrollTo函数,最后调用postInvalidate()函数。
  • 除了上述的操作外,我们需要在处理ACTION_DOWN事件时,对屏幕当前状态进行判断,如果屏幕现在正在滚动(用户刚进行了Fling手势),我们需要停止屏幕滚动。

 具体这一套流程是如何运转的,我会在下一篇文章中详细解释,大家也可以自己查阅代码或者google来搞懂其中的原理。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        .....
        if (mVelocityTracker == null) {
            //检查速度测量器,如果为null,获得一个
            mVelocityTracker = VelocityTracker.obtain();
        }
        int action = MotionEventCompat.getActionMasked(event);
        int index = -1;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                ......
                                if (!mScroller.isFinished()) { //fling
                    mScroller.abortAnimation();
                }
                .....
                break;
            case MotionEvent.ACTION_MOVE:
                ......
                break;
            case MotionEvent.ACTION_CANCEL:
                endDrag();
                break;
            case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                //当手指立刻屏幕时,获得速度,作为fling的初始速度     mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed);
                    int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
                    if (Math.abs(initialVelocity) > mMinFlingSpeed) {
                        // 由于坐标轴正方向问题,要加负号。
                        doFling(-initialVelocity);
                    }
                    endDrag();
                }
                break;
            default:
        }
        //每次onTouchEvent处理Event时,都将event交给时间
        //测量器
        if (mVelocityTracker != null) {
            mVelocityTracker.addMovement(event);
        }
        return true;
    }
    private void doFling(int speed) {
        if (mScroller == null) {
            return;
        }
        mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000);
        invalidate();
    }
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
OverScroll

 在Android手机上,当我们滚动屏幕内容到达内容边界时,如果再滚动就会有一个发光效果。而且界面会进行滚动一小段距离之后再回复原位,这些效果是如何实现的呢?我们需要使用ScrollerscrollTo的升级版OverScrolleroverScrollBy了,还有发光的EdgeEffect类。 
 我们先来了解一下相关的API,理解了这些接口参数的含义,你就可以轻松使用这些接口来实现上述的效果啦。

protected boolean overScrollBy(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent)
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
  • int deltaX,int deltaY : 偏移量,也就是当前要滚动的x,y值。
  • int scrollX,int scrollY : 当前的mScrollX和mScrollY的值。
  • int scrollRangeX,int scrollRangeY: 标示可以滚动的最大的x,y值,也就是你视图真实的长和宽。也就是说,你的视图可视大小可能是100,100,但是视图中的内容的大小为200,200,所以,上述两个值就为200,200
  • int maxOverScrollX,int maxOverScrollY:允许超过滚动范围的最大值,x方向的滚动范围就是0~maxOverScrollX,y方向的滚动范围就是0~maxOverScrollY。
  • boolean isTouchEvent:是否在onTouchEvent中调用的这个函数。所以,当你在computeScroll中调用这个函数时,就可以传入false。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
 
 
  • 1
  • 1
  • int scrollX,int scrollY:就是x,y方向的滚动距离,就相当于mScrollXmScrollY。你既可以直接把二者赋值给相应的成员变量,也可以使用scrollTo函数。
  • boolean clampedX,boolean clampY:表示是否到达超出滚动范围的最大值。如果为true,就需要调用OverScrollspringBack函数来让视图回复原来位置。
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
 
 
  • 1
  • 1
  • int startX,int startY:标示当前的滚动值,也就是mScrollXmScrollY的值。
  • int minX,int maxX:标示x方向的合理滚动值
  • int minY,int maxY:标示y方向的合理滚动值。

 相信看完上述的API之后,大家会有很多的疑惑,所以这里我来举个例子。 
 假设视图大小为100*100。当你一直下拉到视图上边缘,然后在下拉,这时,mScrollY已经达到或者超过正常的滚动范围的最小值了,也就是0,但是你的maxOverScrollY传入的是10,所以,mScrollY最小可以到达-10,最大可以为110。所以,你可以继续下拉。等到mScrollY到达或者超过-10时,clampedY就为true,标示视图已经达到可以OverScroll的边界,需要回滚到正常滚动范围,所以你调用springBack(0,0,0,100)。

 然后我们再来看一下发光效果是如何实现的。 
 使用EdgeEffect类。一般来说,当你只上下滚动时,你只需要两个EdgeEffect实例,分别代表上边界和下边界的发光效果。你需要在下面两个情景下改变EdgeEffect的状态,然后在draw()方法中绘制EdgeEffect

  • 处理ACTION_MOVE时,如果发现y方向的滚动值超过了正常范围的最小值时,你需要调用上边界实例的onPull方法。如果是超过最大值,那么就是调用下边界的onPull方法。
  • computeScroll函数中,也就是说Fling手势执行过程中,如果发现y方向的滚动值超过正常范围时的最小值时,调用onAbsorb函数。

 然后就是重载draw方法,让EdgeEffect实例在画布上绘制自己。你会发现,你必须对画布进行移动或者旋转来让EdgeEffect绘制出上边界或者下边界的发光的效果,因为EdgeEffect对象自己是没有上下左右的概念的。

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (mEdgeEffectTop != null) {
            final int scrollY = getScrollY();
            if (!mEdgeEffectTop.isFinished()) {
                final int count = canvas.save();
                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
                canvas.translate(getPaddingLeft(),Math.min(0,scrollY));
                mEdgeEffectTop.setSize(width,getHeight());
                if (mEdgeEffectTop.draw(canvas)) {
                    postInvalidate();
                }
                canvas.restoreToCount(count);
            }

        }
        if (mEdgeEffectBottom != null) {
            final int scrollY = getScrollY();
            if (!mEdgeEffectBottom.isFinished()) {
                final int count = canvas.save();
                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
                canvas.translate(-width+getPaddingLeft(),Math.max(getScrollRange(),scrollY)+getHeight());
                canvas.rotate(180,width,0);
                mEdgeEffectBottom.setSize(width,getHeight());
                if (mEdgeEffectBottom.draw(canvas)) {
                    postInvalidate();
                }
                canvas.restoreToCount(count);
            }

        }
    }

 @Override
    public boolean onTouchEvent(MotionEvent event) {
            ......
            case MotionEvent.ACTION_MOVE:
                .....
                if (mIsBeingDragged) {
                    overScrollBy(0,(int)deltaY,0,getScrollY(),0,getScrollRange(),0,mOverScrollDistance,true);
                    final int pulledToY = (int)(getScrollY()+deltaY);
                    mLastY = y;
                    if (pulledToY<0) {
                        mEdgeEffectTop.onPull(deltaY/getHeight(),event.getX(mActivePointerId)/getWidth());
                        if (!mEdgeEffectBottom.isFinished()) {
                            mEdgeEffectBottom.onRelease();
                        }
                    } else if(pulledToY> getScrollRange()) {
                        mEdgeEffectBottom.onPull(deltaY/getHeight(),1.0f-event.getX(mActivePointerId)/getWidth());
                        if (!mEdgeEffectTop.isFinished()) {
                            mEdgeEffectTop.onRelease();
                        }
                    }
                    if (mEdgeEffectTop != null && mEdgeEffectBottom != null &&(!mEdgeEffectTop.isFinished()
                                        || !mEdgeEffectBottom.isFinished())) {
                        postInvalidate();
                    }
                }
                .....
        }
        ....
    }
    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (!mScroller.isFinished()) {  
            int oldX = getScrollX();
            int oldY = getScrollY();
            scrollTo(scrollX,scrollY);
            onScrollChanged(scrollX,scrollY,oldX,oldY);
            if (clampedY) {
                Log.e("TEST1","springBack");
                mScroller.springBack(getScrollX(),getScrollY(),0,0,0,getScrollRange());
            }
        } else {
            // TouchEvent中的overScroll调用
            super.scrollTo(scrollX,scrollY);
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();

            int range = getScrollRange();
            if (oldX != x || oldY != y) {
                overScrollBy(x-oldX,y-oldY,oldX,oldY,0,range,0,mOverFlingDistance,false);
            }
            final int overScrollMode = getOverScrollMode();
            final boolean canOverScroll = overScrollMode == OVER_SCROLL_ALWAYS ||
                    (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
            if (canOverScroll) {
                if (y<0 && oldY >= 0) {
                    mEdgeEffectTop.onAbsorb((int)mScroller.getCurrVelocity());
                } else if (y> range && oldY < range) {
                    mEdgeEffectBottom.onAbsorb((int)mScroller.getCurrVelocity());
                }
            }
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105

后记

 本篇文章是系列文章的第二篇,大家可能已经知道如何实现各类手势,但是对其中的机制和原理还不是很了解,之后的第三篇会讲解从本篇代码的视角讲解一下android视图绘制的原理和Scroller的机制,希望大家多多关注。

文章转自:http://blog.csdn.net/u012422440/article/details/51090459


====================================================================================

ListView 的 OverScrollBy 方法 detalY 与 scrollY 参数解析

​​1. 为什么要分析这两个参数

overScrollBy 翻译过来是“滚动越界后的变化”。这是一个在 View 里定义的方法 ,当 View 已经滚动到顶部或者底部之后,如果继续滑动,这个方法就会响应,方法参数传递了后续滚动的变化量。

但是在使用中发现在使用,当达到顶部后,由手指保持滑动和由惯性导致滑动,两种行为产生了有趣的数值变化,因此对其真实的数据含义进行了分析如下。

2. 基本数据

2.1自定义 MyListView 继承 ListView,覆写方法的如下。

/** 
 * 
@param deltaY Change in Y in pixels。在Y方向变化的像素大小
 * 
@param scrollY Current Y scroll value in pixels before applying deltaY。在应用deltaY之前的Y位置
 */

protected boolean overScrollBy(int deltaX, int deltaY, 
                               int scrollX,int scrollY, 
                               int scrollRangeX, int scrollRangeY,
                               int maxOverScrollX, int maxOverScrollY, 
                               boolean isTouchEvent) {

   System.out.println("deltaY="+deltaY+";scrollY="+scrollY+";isTouchEvent="+isTouchEvent);

returnsuper.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}

2.2 列表到达顶部的截图,出现闪烁的波纹即表示达到了列表顶部

3.从 Log 猜测数据的含义

3.1 当到达顶部之后,由惯性导致滑动时的 Log 数据

I/System.out: deltaY=-197; scrollY=0; isTouchEvent=false
I/System.out: deltaY=1; scrollY=-9; isTouchEvent=false
I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-2; isTouchEvent=false
I/System.out: deltaY=0; scrollY=0; isTouchEvent=false

3.2 当到达顶端后,由手指继续滑动时的 Log 数据

I/System.out: deltaY=-4; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-5; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-5; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-9; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-14; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-8; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-29; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-26; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-27; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-15; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-12; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-6; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-5; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-2; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-1; scrollY=0; isTouchEvent=true

3.4 参数分析

首先,当惯性滑动时打印的 isTouchEvent 为 false,当手指继续滑动时 isTouchEvent 为 true。由此可以方便的区分滚动数据的来源。

其次,当惯性滑动时 scrollY 从 -9 变化为 ,可以理解是到达顶部之后列表弹动时Y轴的滚动变化。由手指滑动时始终为 ,因为此时列表并没有出现滚动。

最后,deltaY 的值根据注释可以知道是两次滚动之间的Y变化量,我们截取一段惯性滚动的 Log 来分析。

I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false

第一行和第二行的 scrollY 分别为 -8、-7,而第一行的 deltaY 为 1,确实是他们的差值。

第二行和第三行的 scrollY 分别为 -7、-4,而第二行的 deltaY 为 3,也确实是他们的差值。

也就是说deltaY 是前后两次移动的差值,并且应该是“新的Y位置 - 旧的Y位置”

3.5 奇怪的底层运算

从惯性滑动的 deltaY 算式,推断由手指拖拽时的 deltaY 值。虽然 Log 里的 scrollY 始终为 ,但是手指是由上向下滑动,可以假设开始时手指在 ,向下移动后达到 100,那么根据算式可以得到 “100 - 0 = 100”,但是从 Log 里可以看到,由手指造成的滚动 scrollY 始终为负值。

也就是他的表达式应该为“旧的Y位置 - 新的Y位置”,这是第一个奇怪的地方,也就是滚动到列表顶部的 deltaY 算法似乎有多个。

第二个不合理在于,细看惯性滑动的 Log

I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false

第一行的 scrollY 为 -8,变化了 1 个像素,所以到第二行的时候 scrollY 变成了 -7。但是,这里的含义似乎是说“我根据当前的位置,计算出来一个变化量,然后滚动列表”。正常来说,应该是“我先移动列表,再根据上一次的位置来计算变化量”,从 Log 来看,是违背计算习惯的。

所以,研究这里的两个参数的运算方式也就比较有意思了。

4.overSceollBy 方法被谁调用,传递了什么值

既然需要研究系统代码执行,那就只能是使用 Debug 调试来查看运行过程了,首先要确定到哪里打断点。

在文章开始说过,overScrollBy 是一个在 View 里定义的方法 ,但是 View 并没有处理具体的调用逻辑。真正调用 overSrollBy 方法的是那些可以滚动的控件,比如AbsListView、HorizontalScrollView、ScrollView。

经过查看,在 ListView 里没有调用 overScrollBy 方法,而在 AbsListView 里由 4 处调用,全部打上断点,运行等待执行。

4.1当手指下拉时执行的代码

private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
  int rawDeltaY = y - mMotionY;
// .......

  final int deltaY = rawDeltaY;
  int incrementalDeltaY =
    mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
// .....

  int overscroll = -incrementalDeltaY -
    (motionViewRealTop - motionViewPrevTop);
// ....

  final boolean atOverscrollEdge = overScrollBy(0, overscroll,
                                                0, mScrollY, 0, 0, 0, mOverscrollDistance, true);

//....

4.2当手指下拉时执行的 Debug 截图

4.3当手指下拉时执行的数据分析

可以看到最终传递的 overscroll 变量,在一般情况下,就是等于 incrementalDeltaY 的值,而incrementalDeltaY 最终数据是来自下面的代码

int incrementalDeltaY =
                mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;

先看简单的变量 deltaY,它是来自 rawDeltaY,在 scrollIfNeeded 方法第一句可以看到,

int rawDeltaY = y - mMotionY;

这里的 scrollIfNeeded 方法最终是在 onTouchEvent 方法的 ACTION_MOVE 事件被调用,y 就是事件发生时的手指位置。而 mMotionY 的值根据查看文档说明,

/**
 * The Y value associated with the the down motion event
 * 在down事件时的手指Y位置
 */

int mMotionY;

也就是手指按下时的位置。那么 rawDeltaY 也就是 "当前手指位置 - 手指按下的位置"

但是这只是 mLastY != Integer.MIN_VALUE 不成立时的值,这个条件什么时候会成立呢,经过查找,mLastY 是在 onTouchEvent 的 ACTION_DOWN 事件时被赋值为 Integer.MIN_VALUE。

也就是只有在手指按下的时候, incrementalDeltaY 使用 rawDetalY 的值,其他时候都是使用 y - mLastY + scrollConsumedCorrection,经过调试, scrollConsumedCorrection 一般都是保持在 ,而mLastY 记录的就是上一次手指的位置,那么的表达式就是"当前手指位置 - 手指按下的位置",计算出来的 incrementalDeltaY 在赋值给 overscroll 变量前,又被转换为了 -incrementalDeltaY ,那么表达式最终就是 "手指按下的位置 - 当前手指位置"

转换一下语义的话就是我们之前猜测的"旧的Y位置 - 新的Y位置",符合预期。

4.4当惯性滑动到顶部时的代码

privateclass FlingRunnable implements Runnable {
// .....

    @Override
public void run() {
switch (mTouchMode) {
//....

case TOUCH_MODE_OVERFLING: {
            final OverScroller scroller = mScroller;
if (scroller.computeScrollOffset()) {
                final int scrollY = mScrollY;
                final int currY = scroller.getCurrY();
                final int deltaY = currY - scrollY;
if (overScrollBy(0, deltaY, 0, scrollY, 0, 0,
                        0, mOverflingDistance, false)) {
// ....
        }
        }
    }
}

4.5 当惯性滑动到顶部时的 Debug 截图

4.6当惯性滑动到顶部时的数据分析

可以看到,由手指滑动和惯性滑动确实是执行了不同的代码块。

这里的代码是在屏幕自动滑动时执行,而 deltaY 是来自:

final int deltaY = currY - scrollY;

其中,currY 是根据波纹动画时间计算的下一次移动位置;scrollY 是来自 View 类,在记录了上一次滚动后的位置。

由此证实了之前猜测的算法,当惯性滑动时,deltaY 的值是 “新的Y位置 - 旧的Y位置”

打印出下面的 Log 也可以理解了,

I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false

这里是先有了下一步的移动位置,然后才计算出来 deltaY。只是新的位置还没有真的使用到 View上,所以返回的是上一次的位置。

5.总结

• deltaY 的值确实是根据不同的操作,使用了不同的算法。当手势拖拽的时候是使用 "旧的Y位置 - 新的Y位置",当惯性滑动的时候是使用"新的Y位置 - 旧的Y位置"

• scrollY 的值是上一次移动的值,deltaY 是''列表移动之后"根据新位置计算出来的偏移量。之所以打印时先出现 deltaY,后出现移动的新位置,是因为新位置还没有应用到列表上。​​​​

文章转自:http://weibo.com/ttarticle/p/show?id=2309404041662291690476


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值