ScrollView 触摸事件

ScrollView 触摸事件一览

ScrollView 继承于 FrameLayout,属于 ViewGroup 控件。View 树的触摸事件是从 ViewGroup 的 dispatchTouchEvent 开始分发的。先判断 ViewGroup 的 onInterceptTouchEvent 是否拦截,同时这里也可以调用 ViewGroup 的 requestDisallowInterceptTouchEvent 让 ViewGroup 不调用 onInterceptTouchEvent,如果事件被拦截,则调用 ViewGroup 的超类即 View 的 dispatchTouchEvent,反之,则调用子视图的 dispatchTouchEvent 。

上图针对的是 ACTION_DOWN 事件。

  • Activity 接收事件后,由 dispatchTouchEvent进行分发。Activity 的 dispatchTouchEvent 如果不调用 super (无论返回 true or false)则事件不会向下分发。所以一般 activity 的 dispatchTouchEvent 需要调用 super 才能向下分发。
  • ViewGroup 的 dispatchTouchEvent,用来向下分发事件。如果此方法内不调用super,直接返回 true 则代表直接消费终止。返回 false 代表不在分发直接交给复层处理。调用 super 则会执行 onInterceptTouchEvent 方法。
  • onInterceptTouchEvent 方法用来判断当前 ViewGroup 是否需要拦截此事件。如果拦截返回 true,则直接调用当前 ViewGroup 的 onTouchEvent 自己处理。不需要拦截返回 false 或者直接调用 super 即可。
  • 最下次 View 的 dispatchTouchEvent 接受到事件后,true 代表消费终止,false 则直接调用自己的 onTouchEvent处理事件。注意 View 没有 onInterceptTouchEvent,此方法只有 ViewGroup 有。
  • down 事件在哪个View 消费了,那么 move 和 up 事件就只会从上向下传递到这个 view,不会继续向下传递。

View 之 dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    ...
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    ...
    return result;
}
复制代码

根据源码可知:

  • 首先执行 dispatchTouchEvent 方法。
  • 在 dispatchTouchEvent 方法中先执行 onTouch 方法,后执行 onClick 方法(onClick方法在onTouchEvent中执行)。
  • 如果 onTouch 返回false或者 mOnTouchListener 为null(控件没有设置 setOnTouchListener 方法)或者控件不是enable的情况下会调运 onTouchEvent , dispatchTouchEvent 返回值与 onTouchEvent 返回一样。
  • 如果不是enable的设置了 onTouch 方法也不会执行,只能通过重写控件的 onTouchEvent 方法处理, dispatchTouchEvent 返回值与 onTouchEvent 返回一样。
  • 如果是enable且 onTouch 返回true情况下, dispatchTouchEvent 直接返回true,不会调用 onTouchEvent 方法。

View 之 onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    ...
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    ...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                ...
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
                ...
                break;
                
            case MotionEvent.ACTION_DOWN:
                ...
                break;

            case MotionEvent.ACTION_CANCEL:
                ...
                break;

            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        return true;
    }
    return false;
}

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    return result;
}
复制代码

根据源码可知:

  • onTouchEvent 方法中会在ACTION_UP分支中触发 onClick 的监听。
  • 当 dispatchTouchEvent 在进行事件分发的时候,只有前一个action返回true,才会触发下一个action(也就是说dispatchTouchEvent返回true才会进行下一次action派发)。

ViewGroup 之 dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action);
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }
    ...
    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        ...
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        resetTouchState();
    } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
        final int actionIndex = ev.getActionIndex();
        final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
        removePointersFromTouchTargets(idBitsToRemove);
    }
}
复制代码

根据源码可知:

  • 使用变量intercepted来标记ViewGroup是否拦截Touch事件的传递,mGroupFlags可以根据 requestDisallowInterceptTouchEvent 方法来设置是否拦截的标志 FLAG_DISALLOW_INTERCEPT 。
  • FLAG_DISALLOW_INTERCEPT一旦设置之后,ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作。在resetTouchState方法中重置FLAG_DISALLOW_INTERCEPT标记位。因此,子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。当ViewGroup决定拦截事件后,那么后续的点击事件将默认交给它处理并且不再调用它的onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT标记位的作用是让ViewGroup不再拦截事件,前提是ViewGroup不拦截ACTION_DOWN事件。
  • dispatchTransformedTouchEvent 将Touch事件传递给特定的子View。在该方法中为一个递归调用,会递归调用 dispatchTouchEvent 方法。在 dispatchTouchEvent 中如果子View为ViewGroup并且Touch没有被拦截那么递归调用 dispatchTouchEvent ,如果子View为View那么就会调用其 onTouchEvent 。 dispatchTransformedTouchEvent 方法如果返回true则表示子View消费掉该事件,同时进入该if判断。
  • dispatchTransformedTouchEvent 方法返回false,即子View的 onTouchEvent 返回false(即Touch事件未被消费)。那么该子View就无法继续处理ACTION_MOVE事件和ACTION_UP事件。
  • dispatchTransformedTouchEvent 会调用递归调用 dispatchTouchEvent 和 onTouchEvent ,所以 dispatchTransformedTouchEvent 的返回值实际上是由 onTouchEvent 决定的。

ViewGroup 之 dispatchTransformedTouchEvent

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    ...
    if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
    }
    ...
}
复制代码

根据源码可知:

  • 当child == null时会将Touch事件传递给该ViewGroup自身的dispatchTouchEvent()处理,即super.dispatchTouchEvent(event)(也就是View的这个方法,因为ViewGroup的父类是View);当child != null时会调用该子view(当然该view可能是一个View也可能是一个ViewGroup)的dispatchTouchEvent(event)处理,即child.dispatchTouchEvent(event)。
  • Android事件派发是先传递到最顶级的ViewGroup,再由ViewGroup递归传递到View的。
  • 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
  • 子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件。

常见滑动冲突场景

外部滑动和内部滑动方向不一致

  1. ViewPager和Fragment配合使用组成的页面滑动效果。这种冲突的解决方式,一般都是根据水平滑动还是竖直滑动(滑动的距离差)来判断到底是由谁来拦截事件。
  2. 外部滑动和内部滑动方向一致。内外两层同时能上下滑动或者能同时左右滑动。这种一般都是根据业务来进行区分。
  3. 以上两种场景的嵌套

滑动冲突的解决方式

外部拦截法

外部拦截法,就是所有事件都先经过父容器的拦截处理,由父容器来决定是否拦截。这种方式需要重写父容器的onInterceptTouchEvent方法,伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器需要当前点击事件) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted=false;
            break;
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}
复制代码
  • 不拦截ACTION_DOWN事件。一旦父容器拦截ACTION_DOWN,则后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,无法传递给子元素。
  • ACTION_MOVE事件根据具体需求来决定是否拦截。
  • ACTION_UP事件必须返回false,ACTION_UP事件本身没什么意义,但如果父容器在ACTION_UP返回true会导致子元素无法接收ACTION_UP事件,无法响应onClick事件。
内部拦截法

内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素。内部拦截法需要配合requestDisallowInterceptTouchEvent方法才能正常工作。这种方式需要重写子元素的dispatchTouchEvent方法,伪代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要当前点击事件) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(ev);
}
复制代码

父元素需要默认拦截除ACTION_DOWN事件以外的其他事件,父元素修改如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction()==MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
复制代码

ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制。一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去。

ScrollView 触摸事件流程

onInterceptTouchEvent

onInterceptTouchEvent 所进行的处理,即在 ACTION_DOWN 资源初始化,ACTION_MOVE 判断是否开始拖动手势,ACTION_CANCEL && ACTION_UP 中进行资源释放。这里涉及了多指触摸的处理。

预处理
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
    return true;
}
复制代码

用 mIsBeingDragged 变量来保存当前是否已经开始进行拖动手势,这个后面会讲到,同时当前分发事件类型为 ACTION_MOVE,那么直接返回 true,即拦截事件向子视图进行分发。

ACTION_MOVE
if (!inChild((int) ev.getX(), (int) y)) {
    mIsBeingDragged = false;
    recycleVelocityTracker();
    break;
}
复制代码

如果触摸事件没有作用于子视图范围内,那么就不处理,同时释放速度跟踪器(一般用于 fling 手势的判定)。

mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);

mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();

startNestedScroll(SCROLL_AXIS_VERTICAL);
复制代码

mLastMotionY 记录按下时的坐标信息,mActivePointerId 记录当前分发触摸事件的手指 id,这个一般用于多指的处理,initOrResetVelocityTracker 初始化速度跟踪器,同时使用 addMovement 记录当前触摸事件信息,mScroller 是一般用于 fling 手势处理,这里的作用是处理上一次的 fling,startNestedScroll 则是嵌套滚动机制的知识了。

ACTION_MOVE
final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
    break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
    break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
    mIsBeingDragged = true;
    mLastMotionY = y;
    initVelocityTrackerIfNotExists();
    mVelocityTracker.addMovement(ev);
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
}
复制代码

对 mActivePointerId 进行是否为有效的判断,如果有效,则通过 findPointerIndex 获取作用手指 id 的下标,记录为 pointerIndex ,为什么要获取这个值,我们知道现在的手机屏幕都是支持多指触摸的,所以我们需要根据某个按下的手指的触摸信息来进行处理。yDiff 是滑动的距离,mTouchSlop 则是 SDK 定义的可作为判定是否开始进行拖动的距离常量,可以通过 ViewConfiguration 的 getScaledTouchSlop 获取,如果大于这个值,我们可以认为开始了拖动的手势。 getNestedScrollAxes 这个同样是用于嵌套滚动机制的。如果开始了拖动手势,mIsBeingDragged 标记为 true,同样使用速度跟踪器记录信息,这里还会调用 ViewParent 的 requestDisallowInterceptTouchEvent,防止父视图拦截了事件,即 onInterceptTouchEvent。

ACTION_CANCEL && ACTION_UP
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
stopNestedScroll();
复制代码

进行一些释放资源的操作,比如 mIsBeingDragged 设置为 false,释放速度跟踪器等等。

ACTION_UP 是所有的手指(多指触摸)抬起时分发的事件,而 ACTION_CANCEL 则是触摸取消事件类型,一般什么时候会分发这个事件呢?举个例子,如果某个子视图已经消费了 ACTION_DOWN,即在这个事件分发时,向父视图传递了 true 的返回值,那么一般情况下,父视图不会再拦截接下来的事件,比如 ACTION_MOVE 等,但是如果父视图在这种情况下,还拦截了事件传递,即在 onInterceptTouch 中返回了 true,那么在 ViewGroup 的 dispatchTouchEvent 中会给已经确认消费事件的子视图分发一个 TOUCH_CANCEL 的事件。
复制代码
ACTION_POINTER_UP

多指触摸时,某个手指抬起时分发的事件。

final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
    mLastMotionY = (int) ev.getY(newPointerIndex);
    mActivePointerId = ev.getPointerId(newPointerIndex);
    if (mVelocityTracker != null) {
        mVelocityTracker.clear();
    }
}
复制代码

当某个手指抬起时,而这个手指刚好是我们当前使用的,则重新初始化资源。

onTouchEvent

onTouchEvent 和 onInterceptTouchEvent 处理有些相似,主要是在 TOUCH_MOVE 中在判定为拖动手势后进行真正的业务逻辑处理,同时在 ACTION_UP 中根据速度跟踪器的获取的速度,判定是否符合 fling 手势,如果符合,则使用 Scroller 进行计算。

ACTION_DOWN
if (getChildCount() == 0) {
    return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
}

if (!mScroller.isFinished()) {
    mScroller.abortAnimation();
}

mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(SCROLL_AXIS_VERTICAL);
复制代码

onTouchEvent 在 ACTION_DOWN 事件分发中,主要是进行资源初始化,同时也处理上一次的 fling 任务,比如调用 Scroller 的 abortAnimation,如果 Scroller 还没结束 fling 计算,则中止处理。

ACTION_MOVE
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
    break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
    // 嵌套滚动处理
    deltaY -= mScrollConsumed[1];
    vtev.offsetLocation(0, mScrollOffset[1]);
    mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
    mIsBeingDragged = true;
    if (deltaY > 0) {
        deltaY -= mTouchSlop;
    } else {
        deltaY += mTouchSlop;
    }
}

if (mIsBeingDragged) {
    /// 业务逻辑
}
复制代码

进行多指处理,获取指定手指的触摸事件信息。mIsBeingDragged 为 false,同时会再进行一次拖动手势的判定,判定逻辑和 onInterceptTouchEvent 中类似,如果 mIsBeingDragged 为 true,则开始进行真正的逻辑处理。

if (mIsBeingDragged) {
    mLastMotionY = y - mScrollOffset[1];

    final int oldY = mScrollY;
    final int range = getScrollRange();
    final int overscrollMode = getOverScrollMode();
    boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
            && !hasNestedScrollingParent()) {
        mVelocityTracker.clear();
    }

    final int scrolledDeltaY = mScrollY - oldY;
    final int unconsumedY = deltaY - scrolledDeltaY;
    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
        mLastMotionY -= mScrollOffset[1];
        vtev.offsetLocation(0, mScrollOffset[1]);
        mNestedYOffset += mScrollOffset[1];
    } else if (canOverscroll) {
        final int pulledToY = oldY + deltaY;
        if (pulledToY < 0) {
            mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                    ev.getX(activePointerIndex) / getWidth());
            if (!mEdgeGlowBottom.isFinished()) {
                mEdgeGlowBottom.onRelease();
            }
        } else if (pulledToY > range) {
            mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                    1.f - ev.getX(activePointerIndex) / getWidth());
            if (!mEdgeGlowTop.isFinished()) {
                mEdgeGlowTop.onRelease();
            }
        }
        if (mEdgeGlowTop != null
                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
            postInvalidateOnAnimation();
        }
    }
}
复制代码

EdgeEffect 是用于拖动时,边缘的阴影效果。

ACTION_UP
if (mIsBeingDragged) {
    final VelocityTracker velocityTracker = mVelocityTracker;
	velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
    
    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
    	flingWithNestedDispatch(-initialVelocity);
    }
    
    mActivePointerId = INVALID_POINTER;
    endDrag();
}
复制代码

当手指全部抬起时,可以使用速度跟踪器进行 fling 手势的判定,同时释放资源。通过 getYVelocity 获取速度,在判断是否可以作为 fling 手势处理,mMaximumVelocity 是处理的最大速度,mMinimumVelocity 是处理的最小速度,这两个值同样可以通过 ViewConfiguration 的 getScaledMaximumFlingVelocity 和 getScaledMinimumFlingVelocity 获取。一般情况对 fling 的处理是通过 Scroller 进行处理的,因为这里涉及复杂的数学知识,而 Scroller 可以帮我们简化这里的操作,使用如下:

int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();

mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
                    Math.max(0, bottom - height), 0, height/2);

postInvalidateOnAnimation();
复制代码

通过传递当前拖动手势速度值来调用 fling 进行处理,然后在 computeScrollOffset 方法中,进行真正的滚动处理:

public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      	int oldX = mScrollX;
        int oldY = mScrollY;
        int x = mScroller.getCurrX();
        int y = mScroller.getCurrY();

        if (oldX != x || oldY != y) {
            final int range = getScrollRange();
            final int overscrollMode = getOverScrollMode();
            final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                    (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

            overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
                    0, mOverflingDistance, false);
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);

            if (canOverscroll) {
                if (y < 0 && oldY >= 0) {
                    mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
                } else if (y > range && oldY <= range) {
                    mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
                }
            }
        }
        postInvalidateOnAnimation();
    }
}
复制代码

Scroller 并不会为我们进行滚动处理,它只是提供了计算的模型,通过调用 computeScrollOffset 进行计算,如果返回 true,表示计算还没结束,然后通过 getCurrX 或 getCurrY 获取计算后的值,最后进行真正的滚动处理,比如调用 scrollTo 等等,这里需要注意的是,需要调用 invalidate 来确保进行下一次的 computeScroll 调用,这里使用的 postInvalidateOnAnimation 其作用是类似的。

ACTION_CANCEL
if (mIsBeingDragged && getChildCount() > 0) {
    if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
        postInvalidateOnAnimation();
    }
    mActivePointerId = INVALID_POINTER;
    endDrag();
}
复制代码

释放资源。

ACTION_POINTER_DOWN

当有新的手指按下时分发的事件。

final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
复制代码

以新按下的手指的信息重新计算。

ACTION_POINTER_UP

处理和 onInterceptTouch 一致。

SDK 工具类

系统已经提供 GestureDetector 来进行手势的判定,我们只需要在相应的手势回调方法中进行我们的业务逻辑即可。还有更强大的 ViewDragHelper。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值