美文网首页
How ScrollView works ?

How ScrollView works ?

作者: ivolianer | 来源:发表于2016-04-16 09:09 被阅读64次

    ScrollView 继承自 FrameLayout,是什么让 FrameLayout 变成了可以滚动,可 fling 的呢?
    是因为 onInterceptTouchEvent 和 onTouchEvent 的重写。

    ScrollView 只能包含一个子视图,常常是一个线性布局。ScrollView 所关心的是,子视图是否消费了 Down 事件。不关心的是,子视图内部又经历了多少次事件分发。


    当子视图不消费 DOWN 事件

    onInterceptTouchEvent 函数会被调用。
    (先无视掉无关紧要的代码:诸如简单判断,确保滚动结束,确保父视图不拦截事件,边缘效果,多点触控,内部滚动,越界滚动,回收工作等等。)

    case MotionEvent.ACTION_DOWN: {
    final int y = (int) ev.getY();
    mLastMotionY = y;
    mActivePointerId = ev.getPointerId(0);
    }
    // DOWN 事件产生时,一般 mIsBeingDragged 已经被重置为 false 了
    return mIsBeingDragged;
    

    DOWN 事件不会被拦截,在子视图中寻找能够接收到事件的是视图,让它处理。
    根据假设,子视图不消费 DOWN 事件,最后由 ScrollView 的 onTouchEvent 处理。

    case MotionEvent.ACTION_DOWN: {
    mLastMotionY = (int) ev.getY();
    mActivePointerId = ev.getPointerId(0);
    break;
    }
    return true;
    

    ScrollView 会消费掉 Down 事件(否则将收不到后续事件),并记录位置。

    MOVE 事件

    由于子视图没有消费 DOWN 事件,即 touchTarget 为 null,后续事件将被拦截并由 ScrollView 的 onTouchEvent 处理。

    case MotionEvent.ACTION_MOVE: {
    final int y = (int) ev.getY(activePointerIndex);
    // 相对坐标,绝对距离
    int deltaY = mLastMotionY - y;
    // 如果移动距离大于最小距离,则标识为正在拖拽,并消耗掉这段距离
    if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {  
    mIsBeingDragged = true;
    if (deltaY > 0) {
    deltaY -= mTouchSlop;
    } else {
    deltaY += mTouchSlop;
    }}
    // 如果正在拖拽
    if (mIsBeingDragged) {
    final int range = getScrollRange();
    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)) {}
    break;
    // !!! ---- MOVE 和 UP 的返回值有何意义? ---- !!!
    return true;
    
    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {     
    int newScrollY = scrollY + deltaY;
    onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
    }
    
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
    super.scrollTo(scrollX, scrollY);
    }
    

    如果移动距离小于 TouchSlop,则不做任何响应。
    如果移动距离大于 TouchSlop,则确认正在拖拽,并消耗这段距离。
    如果正在拖拽,则更新 mScrollY 并重画。

    UP 事件
    case MotionEvent.ACTION_UP:
    if (mIsBeingDragged) {
    // 计算滑动速度,并 fling
    // 下拉向上滚,所以速度是取负值
    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();
    }
    
    private void flingWithNestedDispatch(int velocityY) {
    if (canFling) {  fling(velocityY); }
    }
    
    public void fling(int velocityY) {
    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);
    }
    
    滑动日志

    4-16 13:10:15.497 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 0
    04-16 13:10:15.497 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 0
    04-16 13:10:15.497 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 0
    04-16 13:10:15.532 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
    04-16 13:10:15.532 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 2
    04-16 13:10:15.549 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
    04-16 13:10:15.549 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 2
    04-16 13:10:15.560 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
    04-16 13:10:15.561 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 2
    04-16 13:10:15.561 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 1
    04-16 13:10:15.561 5038-5038/ivolianer.viewgroup E/result: ScrollView onTouch dispatch event 1


    当子视图消费 DOWN 事件(比上面的情况复杂)

    interceptTouchEvent 被调用

    case MotionEvent.ACTION_DOWN: {   
    final int y = (int) ev.getY(); 
    mLastMotionY = y;
    mActivePointerId = ev.getPointerId(0);
    }
    

    不拦截 DOWN 事件,寻找能接收到事件的子视图,让它处理。
    根据假设,子视图消费 DOWN 事件,ScrollView 本身的 onTouchEvent 不会被调用。

    MOVE 事件

    因为 target 不为 null,intereceptTouchEvent 被调用。

    case MotionEvent.ACTION_MOVE: {
      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;
      }
      break;
    }
    return mIsBeingDragged;
    
    如果滑动距离始终小于 TouchSlop

    事件不会被拦截。
    所有 MOVE 事件都会 TouchTarget 处理。
    最后的 UP 事件也会传递给 TouchTarget。

    日志

    04-16 13:12:03.104 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 0
    04-16 13:12:03.104 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 0
    04-16 13:12:03.126 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
    04-16 13:12:03.126 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 2
    04-16 13:12:03.176 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 2
    04-16 13:12:03.176 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 2
    04-16 13:12:04.101 5038-5038/ivolianer.viewgroup E/result: ScrollView dispatch event 1
    04-16 13:12:04.101 5038-5038/ivolianer.viewgroup E/result: LinearLayout dispatch event 1

    如果滑动距离大于 TouchSlop

    mIsBeingDragged = true,并拦截事件。

    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
      return true;
    }
    

    后续的 MOVE 事件也会被拦截。

    拦截事件的实质
    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
    if (dispatchTransformedTouchEvent(ev, cancelChild,  target.child,target.pointerIdBits)) {
    handled = true;
    }
    

    把事件传递给 TouchTarget 时。
    如果不拦截,正常分发事件。
    如果拦截了,事件会被替换成 CANCEL 事件。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,  View child, int desiredPointerIdBits) {
      final boolean handled; 
      final int oldAction = event.getAction();
      if (cancel || oldAction == MotionEvent.ACTION_CANCEL){
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
          handled = super.dispatchTouchEvent(event);
        } else {
          handled = child.dispatchTouchEvent(event);
        }
      return handled;    
    }
    

    并且会重置 TouchTarget ,这会导致后续 UP 事件也无法收到。

    if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked ==MotionEvent.ACTION_HOVER_MOVE) {
      resetTouchState();
    } 
    

    日志

    04-16 13:01:47.191 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 0
    04-16 13:01:47.195 17678-17678/ivolianer.viewgroup E/result: LinearLayout receive event 0
    04-16 13:01:47.231 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
    04-16 13:01:47.231 17678-17678/ivolianer.viewgroup E/result: LinearLayout receive event 2
    04-16 13:01:47.247 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
    04-16 13:01:47.247 17678-17678/ivolianer.viewgroup E/result: LinearLayout receive event 3
    04-16 13:01:47.264 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
    04-16 13:01:47.264 17678-17678/ivolianer.viewgroup E/result: ScrollView onTouch receive event 2
    04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 2
    04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView onTouch receive event 2
    04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView receive event 1
    04-16 13:01:47.328 17678-17678/ivolianer.viewgroup E/result: ScrollView onTouch receive event 1


    BB了这多,总结下:

    ScrollView 给了你传递 DOWN 和 UP 事件的机会,为了让子视图有机会实现点击或长按。但当你企图传递 MOVE 事件时(滑动距离大于 TouchSlop),他会无情拦截下来,并重置 TouchTarget。

    相关文章

      网友评论

          本文标题:How ScrollView works ?

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