返回介绍

2. 源码分析

发布于 2024-12-23 21:34:54 字数 25385 浏览 0 评论 0 收藏

2.1 继承关系

2.2 主要辅助类

//用来计算滑动位置
private OverScroller mScroller;
//用来绘制边缘阴影
private EdgeEffect mEdgeGlowTop;
private EdgeEffect mEdgeGlowBottom;
//用于计算滑动时的加速度
private VelocityTracker mVelocityTracker;

2.3 构造方法

ScrollView 的构造方法如下:

public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
  super(context, attrs, defStyleAttr, defStyleRes);
  initScrollView();

  final TypedArray a = context.obtainStyledAttributes(
      attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
  setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
  a.recycle();
}

在构造方法中分别调用了 initScrollView()setFillViewport() 方法,代码如下:

private void initScrollView() {
  //初始化 OverScroller
  mScroller = new OverScroller(getContext());
  setFocusable(true);
  setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
  setWillNotDraw(false);
  final ViewConfiguration configuration = ViewConfiguration.get(mContext);
  //被认为是滑动操作的最小距离
  mTouchSlop = configuration.getScaledTouchSlop();
  //最小加速度
  mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
  //最大加速度
  mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
  //用手指拖动超过边缘的最大距离
  mOverscrollDistance = configuration.getScaledOverscrollDistance();
  //滑动超过边缘的最大距离
  mOverflingDistance = configuration.getScaledOverflingDistance();
}

可以看到是初始化了一些类与参数,继续看看 setFillViewport()

public void setFillViewport(boolean fillViewport) {
  if (fillViewport != mFillViewport) {
    mFillViewport = fillViewport;
    requestLayout();
  }
}

只是根据布局文件中的 fillViewport 属性来给 mFillViewport 赋值并调用 requestLayout() 方法。 mFillViewport 如果为 true 则表示:将子 View 的高度延伸到和视图高度一致,即充满整个视图。初始化结束之后,会进入到绘制流程。下面我们按照 Measure -> Layout -> Draw 的绘制流程来分析 ScrollView 中的实现。

2.4 Measure、Layout 与 Draw

2.4.1 onMeasure 方法的实现

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);

  if (!mFillViewport) {
    return;
  }

  final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  if (heightMode == MeasureSpec.UNSPECIFIED) {
    return;
  }

  if (getChildCount() > 0) {
    // 获取子 View
    final View child = getChildAt(0);
    // 获取 ScrollView 的高度
    final int height = getMeasuredHeight();
    if (child.getMeasuredHeight() < height) {
      final int widthPadding;
      final int heightPadding;
      final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
      final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
      // 获取 ScrollView 的 padding
      if (targetSdkVersion >= VERSION_CODES.M) {
        widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
        heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
      } else {
        widthPadding = mPaddingLeft + mPaddingRight;
        heightPadding = mPaddingTop + mPaddingBottom;
      }

      final int childWidthMeasureSpec = getChildMeasureSpec(
          widthMeasureSpec, widthPadding, lp.width);
      final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
          height - heightPadding, MeasureSpec.EXACTLY);
      //根据新的高度重新 measure 子 View
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
  }
}

从代码中可以看到首先调用了 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 即父类 FrameLayoutonMeasure() 方法。如果我们将 mFillViewport 设置为 false 的话将会直接 return 。当为 true 时才会继续执行,会根据子 View 的高度和 ScrollView 本身的高度决定是否重新 measureView 使其充满 ScrollViewScrollViewonMeasure() 其实就是处理了 mFillViewport

2.4.1 onLayout 方法的实现

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  super.onLayout(changed, l, t, r, b);
  mIsLayoutDirty = false;
  // Give a child focus if it needs it
  if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
    scrollToChild(mChildToScrollTo);
  }
  mChildToScrollTo = null;
  //是否还未添加过 window 中去
  if (!isLaidOut()) {
    if (mSavedState != null) {
      mScrollY = mSavedState.scrollPosition;
      mSavedState = null;
    } // mScrollY default value is "0"

    final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
    final int scrollRange = Math.max(0,
        childHeight - (b - t - mPaddingBottom - mPaddingTop));

    // Don't forget to clamp
    if (mScrollY > scrollRange) {
      mScrollY = scrollRange;
    } else if (mScrollY < 0) {
      mScrollY = 0;
    }
  }

  // Calling this with the present values causes it to re-claim them
  scrollTo(mScrollX, mScrollY);
}

首先也是调用了父类的 onLayout 方法。接下来处理了是否有需要滚动到的 View ,以及根据保存的滚动状态来决定是否需要滚动。如果需要则调用 scrollTo() 方法。

2.4.1 draw 方法的实现

@Override
public void draw(Canvas canvas) {
  super.draw(canvas);
  if (mEdgeGlowTop != null) {
    final int scrollY = mScrollY;
    final boolean clipToPadding = getClipToPadding();
    if (!mEdgeGlowTop.isFinished()) {
      ......
      if (mEdgeGlowTop.draw(canvas)) {
        postInvalidateOnAnimation();
      }
      canvas.restoreToCount(restoreCount);
    }
    if (!mEdgeGlowBottom.isFinished()) {
      ......
      if (mEdgeGlowBottom.draw(canvas)) {
        postInvalidateOnAnimation();
      }
      canvas.restoreToCount(restoreCount);
    }
  }
}

依然是调用了父类的 draw 方法。之后则是根据是否需要绘制边缘阴影来绘制阴影。 ScrollView 的边缘阴影就是在这里绘制的。值得一提的是包括 ListView 以及 RecycleView 的边缘阴影都是用这种方法来绘制的。以上就是 ScrollView 的整个绘制流程。可以看出都是调用了父类的对应方法。自身只处理了一些与 ScrollView 相关的属性。分析完绘制流程我们就来看看 ScrollView 中的触摸事件处理机制,来看看 ScrollView 中的滑动滚动到底是如何做到的:

2.5 触摸事件处理

说到触摸事件的分发与消费机制这算是一个比较基础的知识。但是要是完全掌握也并不是那么容易的,这里推荐一篇文章 Android:View 的事件分发与消费机制 。对事件处理机制还不了解的同学可以先看看这边文章。 ScrollView 因为是继承自 ViewGroup 的,所以触摸事件会依次调用 dispatchTouchEvent() -> onInterceptTouchEvent() 若返回 true -> onTouchEvent() 处理触摸事件。 ScrollView 并没有重写 dispatchTouchEvent() 方法,所以我们从 onInterceptTouchEvent() 方法来看。

2.5.1 onInterceptTouchEvent 方法的实现

//这个方法只决定我们是否拦截这个手势,如果返回 true,则 onMotionEvent 会被调用,并处理滑动事件。
//此方法并不处理事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

  //如果是移动手势并在处于拖拽阶段,直接返回 true
  final int action = ev.getAction();
  if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
    return true;
  }

  //如果并不能滑动则返回 false
  if (getScrollY() == 0 && !canScrollVertically(1)) {
    return false;
  }

  switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_MOVE: {
      //检测用户是否移动了足够远的距离。

      final int activePointerId = mActivePointerId;
      if (activePointerId == INVALID_POINTER) {
        // If we don't have a valid id, the touch down wasn't on content.
        break;
      }

      final int pointerIndex = ev.findPointerIndex(activePointerId);
      if (pointerIndex == -1) {
        Log.e(TAG, "Invalid pointerId=" + activePointerId
            + " in onInterceptTouchEvent");
        break;
      }
      //得到当前触摸的 y 左边
      final int y = (int) ev.getY(pointerIndex);
      //计算移动的插值
      final int yDiff = Math.abs(y - mLastMotionY);
      //如果 yDiff 大于最小滑动距离,并且是垂直滑动则认为触发了滑动手势。
      if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
        //标记拖动状态为 true
        mIsBeingDragged = true;
        //赋值 mLastMotionY
        mLastMotionY = y;
        //初始化 mVelocityTracker 并添加
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);
        mNestedYOffset = 0;
        if (mScrollStrictSpan == null) {
          mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
        }
        final ViewParent parent = getParent();
        if (parent != null) {
          //通知父布局不再拦截触摸事件
          parent.requestDisallowInterceptTouchEvent(true);
        }
      }
      break;
    }

    case MotionEvent.ACTION_DOWN: {
      final int y = (int) ev.getY();
      //触摸点不在子 View 内
      if (!inChild((int) ev.getX(), (int) y)) {
        mIsBeingDragged = false;
        recycleVelocityTracker();
        break;
      }

      //记录当前位置
      mLastMotionY = y;
      //记录 pointer 的 ID,ACTION_DOWN 总会在 index 0
      mActivePointerId = ev.getPointerId(0);
      //初始化 mVelocityTracker
      initOrResetVelocityTracker();
      mVelocityTracker.addMovement(ev);
      //如果在滑动过程中则 mIsBeingDragged = true
      mIsBeingDragged = !mScroller.isFinished();
      if (mIsBeingDragged && mScrollStrictSpan == null) {
        mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
      }
      //回调 NestedScroll 相关接口
      startNestedScroll(SCROLL_AXIS_VERTICAL);
      break;
    }

    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
      //清除 Drag 状态
      mIsBeingDragged = false;
      mActivePointerId = INVALID_POINTER;
      recycleVelocityTracker();
      if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
        postInvalidateOnAnimation();
      }
      //回调 NestedScroll 相关接口
      stopNestedScroll();
      break;
    case MotionEvent.ACTION_POINTER_UP:
      //当多个手指触摸中有一个手指抬起时,判断是不是当前 active 的点,如果是则寻找新的
      //mActivePointerId
      onSecondaryPointerUp(ev);
      break;
  }
  //最终根据是否开始拖拽的状态返回
  return mIsBeingDragged;
}

以上就是 onInterceptTouchEvent() 的整体实现。 onInterceptTouchEvent() 只决定是否拦截触摸事件并交给 onTouchEvent() 处理。内部并不处理触摸逻辑。 ScrollView 中根据 mIsBeingDragged 来决定是否拦截事件。当手指按下发生 MotionEvent.ACTION_DOWN 时,会记录当前位置并检测是否在快速滚动过程中如果是则返回 true

当手指移动发生 MotionEvent.ACTION_MOVE 时,会判断是否是垂直方向上的滑动事件,如果是则返回 true 。当手指抬起发生 MotionEvent.ACTION_UP 时,则清除状态并返回 false 。在返回 true 的情况中, onTouchEvent() 方法就会被调用来处理触摸事件。我们继续来看 onTouchEvent() 方法的实现。

2.5.2 onTouchEvent 方法的实现

在看 onTouchEvent() 的实现之前,我们知道在 ScrollView 中手指无论怎么移动,只会有垂直方向上的滑动发生。而触摸事件的大致流程是:

  ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

我们根据事件的类型分别来分析:

  • ACTION_DOWN: ACTION_DOWN 代表手指按下时第一个发生的事件,在 onTouchEvent() 中实现如下:
  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    //初始化 VelocityTracker
    initVelocityTrackerIfNotExists();
    //复制当前的 MotionEvent 赋值给 vtev
    MotionEvent vtev = MotionEvent.obtain(ev);

    final int actionMasked = ev.getActionMasked();

    if (actionMasked == MotionEvent.ACTION_DOWN) {
      mNestedYOffset = 0;
    }
    //调整 vtev 的偏移量
    vtev.offsetLocation(0, mNestedYOffset);

    switch (actionMasked) {
      case MotionEvent.ACTION_DOWN: {
        if (getChildCount() == 0) {
          return false;
        }
        // 将!mScroller.isFinished() 赋值给 mIsBeingDragged
        if ((mIsBeingDragged = !mScroller.isFinished())) {
          final ViewParent parent = getParent();
          if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(true);
          }
        }

        //如果正在 fling 状态并且用户触摸。则停止 fling。
        //当处于 fling 过程中 isFinished 为 false。
        //fling :即快速滑动。
        if (!mScroller.isFinished()) {
          //停止
          mScroller.abortAnimation();
          if (mFlingStrictSpan != null) {
            mFlingStrictSpan.finish();
            mFlingStrictSpan = null;
          }
        }

        //记录触摸事件的初始值
        mLastMotionY = (int) ev.getY();
        mActivePointerId = ev.getPointerId(0);
        startNestedScroll(SCROLL_AXIS_VERTICAL);
        break;
      }
      ......
    }

    if (mVelocityTracker != null) {
      mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
  }

在处理各种事件之前,首先初始化了 VelocityTracker 。并且复制一个新的 MotionEvent 对象用于计算加速度。接着开始处理 ACTION_DOWN :首先是给 mIsBeingDragged 赋值,接着检查是否在 fling 动画执行过程中,如果正在执行则停止,这也是为什么我们在 ScrollView 滑动过程中手指触摸时会终止 ScrollView 的滑动。最后记录了 mLastMotionYmActivePointerId

  • ACTION_MOVE: 当手指移动时,会产生 ACTION_MOVE 事件:
 @Override
  public boolean onTouchEvent(MotionEvent ev) {
    ......
    switch (actionMasked) {
      ......
      //如果为 ACTION_MOVE 事件时
      case MotionEvent.ACTION_MOVE:
        final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
        if (activePointerIndex == -1) {
          Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
          break;
        }
        //得到当前 y 值
        final int y = (int) ev.getY(activePointerIndex);
        //计算偏移量 deltaY
        int deltaY = mLastMotionY - y;
        //如果 dispatchNestedPreScroll 返回 true,即有 NestedScroll 存在
        if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
          deltaY -= mScrollConsumed[1];
          vtev.offsetLocation(0, mScrollOffset[1]);
          mNestedYOffset += mScrollOffset[1];
        }
        //如果还未处于 drag 状态,并且 deltaY 大于最小滑动距离,
        //则赋值 mIsBeingDragged 为 true
        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) {
          //记录当前的 y 值
          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);

          // 调用 overScrollBy() 方法处理滑动事件。
          if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
              && !hasNestedScrollingParent()) {
            // Break our velocity if we hit a scroll barrier.
            mVelocityTracker.clear();
          }

          final int scrolledDeltaY = mScrollY - oldY;
          final int unconsumedY = deltaY - scrolledDeltaY;
          //处理 NestedScroll
          if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
            mLastMotionY -= mScrollOffset[1];
            vtev.offsetLocation(0, mScrollOffset[1]);
            mNestedYOffset += mScrollOffset[1];
          } else if (canOverscroll) {
            //如果 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();
            }
          }
        }
        break;
    }
    ......
  }

ACTION_MOVE 事件中,首先计算当前的垂直偏移量 deltaY 。然后判断是否大于最小滑动距离,并且给 mIsBeingDragged 赋值。接着如果 mIsBeingDraggedtrue 。就取得处理滑动需要的各种参数,并调用 overScrollBy() 方法来处理触摸事件, overScrollBy() 是在 View 里实现的方法,大致实现如下:

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

    int newScrollX = scrollX + deltaX;
    if (!overScrollHorizontal) {
      maxOverScrollX = 0;
    }

    int newScrollY = scrollY + deltaY;
    if (!overScrollVertical) {
      maxOverScrollY = 0;
    }

    // Clamp values if at the limits and record
    final int left = -maxOverScrollX;
    final int right = maxOverScrollX + scrollRangeX;
    final int top = -maxOverScrollY;
    final int bottom = maxOverScrollY + scrollRangeY;

    boolean clampedX = false;
    if (newScrollX > right) {
      newScrollX = right;
      clampedX = true;
    } else if (newScrollX < left) {
      newScrollX = left;
      clampedX = true;
    }

    boolean clampedY = false;
    if (newScrollY > bottom) {
      newScrollY = bottom;
      clampedY = true;
    } else if (newScrollY < top) {
      newScrollY = top;
      clampedY = true;
    }

    onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);

    return clampedX || clampedY;
  }

方法的参数很清楚了应该不难理解,可以看到在 overScrollBy() 方法中根据我们传入的参数以及 View 本身是否可以滑动的设定,等等来最终决定了新的 newScrollXnewScrollY 。接着调用了 onOverScrolled() 方法来处理滑动, onOverScrolled() 方法在 View 中是空实现,所以再回到 ScrollView 中可以看到重写了 onOverScrolled() 方法:

  @Override
  protected void onOverScrolled(int scrollX, int scrollY,
      boolean clampedX, boolean clampedY) {
    // Treat animating scrolls differently; see #computeScroll() for why.
    if (!mScroller.isFinished()) {
      final int oldX = mScrollX;
      final int oldY = mScrollY;
      mScrollX = scrollX;
      mScrollY = scrollY;
      invalidateParentIfNeeded();
      onScrollChanged(mScrollX, mScrollY, oldX, oldY);
      if (clampedY) {
        mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
      }
    } else {
      super.scrollTo(scrollX, scrollY);
    }

    awakenScrollBars();
  }

首先是有一句注释是说:不同的对待滑动动画,查看 computeScroll() 方法看原因。说到滑动动画一定是和 Scroller 相关了,目前我们还没涉及到,下面我们再谈。回到这里看到根据 !mScroller.isFinished() 来判断,根据前面的判断得知,要么是滑动动画并不存在,要么就已经被终止,所以在这里 !mScroller.isFinished()false 。所以会调用 super.scrollTo(scrollX, scrollY); 最终产生滑动。到这里手指触摸产生的滑动就分析完了。

  • ACTION_UP: ACTION_UP 是当我们手指离开时产生的事件,在 ScrollView 中当我们手指离开时,会根据当前的加速度再滑动一段距离。具体的实现我们来看看是如何实现的:
 @Override
  public boolean onTouchEvent(MotionEvent ev) {
    ......
    switch (actionMasked) {
      ......
      case MotionEvent.ACTION_UP:
        //如果实在 drag 状态中
        if (mIsBeingDragged) {
          //计算加速度
          final VelocityTracker velocityTracker = mVelocityTracker;
          velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
          int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
          //如果有有效的加速度
          if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
            //处理带有加速度的滑动事件
            flingWithNestedDispatch(-initialVelocity);
          } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
              getScrollRange())) {
            postInvalidateOnAnimation();
          }

          mActivePointerId = INVALID_POINTER;
          //清除 drag 状态
          endDrag();
        }
        break;
    }
    ......
  }

可以看到代码并不复杂,在计算了加速度后,调用了 flingWithNestedDispatch(-initialVelocity);

  private void flingWithNestedDispatch(int velocityY) {
    final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
        (mScrollY < getScrollRange() || velocityY < 0);
    if (!dispatchNestedPreFling(0, velocityY)) {
      dispatchNestedFling(0, velocityY, canFling);
      if (canFling) {
        fling(velocityY);
      }
    }
  }

代码如上,我们这里不考虑 NestedFling 的方式,所以 dispatchNestedPreFling(0, velocityY) 默认会返回 false ,所以最终会执行 fling(velocityY);

  public void fling(int velocityY) {
    if (getChildCount() > 0) {
      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);

      if (mFlingStrictSpan == null) {
        mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
      }

      postInvalidateOnAnimation();
    }
  }

可以看到是调用了 mScrollerfling 方法,在上一篇 Scroller 源码分析 中,我们已经详细解释了 Scroller 的原理, ScrollView 中虽然使用的是 OverScroller 但是使用方法也是类似的。所以在调用了 mScrollerfling 方法后。我们需要在 computeScroll() 处理 mScroller 计算出的值。 ScrollView 中的 computeScroll() 方法实现如下:

  @Override
  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());
          }
        }
      }

      if (!awakenScrollBars()) {
        // Keep on drawing until the animation has finished.
        postInvalidateOnAnimation();
      }
    } else {
      if (mFlingStrictSpan != null) {
        mFlingStrictSpan.finish();
        mFlingStrictSpan = null;
      }
    }
  }

我省略了一些注释,意思是说: computeScroll() 会在绘制的过程中调用,为了不重复的显示滚动条。这里重复做了 scrollTo() 方法中的代码。但并没有调用 scrollTo() ,因为 scrollTo() 中也有滚动条相关的处理。所以 computeScroll() 中也调用了 overScrollBy() 方法处理滑动。所以最终仍然会调用 onOverScrolled() 方法:

  @Override
  protected void onOverScrolled(int scrollX, int scrollY,
      boolean clampedX, boolean clampedY) {
    // Treat animating scrolls differently; see #computeScroll() for why.
    if (!mScroller.isFinished()) {
      final int oldX = mScrollX;
      final int oldY = mScrollY;
      mScrollX = scrollX;
      mScrollY = scrollY;
      invalidateParentIfNeeded();
      onScrollChanged(mScrollX, mScrollY, oldX, oldY);
      if (clampedY) {
        mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
      }
    } else {
      super.scrollTo(scrollX, scrollY);
    }

    awakenScrollBars();
  }

这次就会进入到第一个 if 语句里,可以看到是给 mScrollXmScrollY 赋值后调用了 invalidateParentIfNeeded(); 方法来完成最终的滑动处理。

  • ACTION_POINTER_DOWN
 @Override
  public boolean onTouchEvent(MotionEvent ev) {
    ......
    switch (actionMasked) {
      ......
      case MotionEvent.ACTION_POINTER_DOWN: {
        //更新状态,即新的触摸手势决定是否滑动。
        final int index = ev.getActionIndex();
        mLastMotionY = (int) ev.getY(index);
        mActivePointerId = ev.getPointerId(index);
        break;
      }
    }
    ......
  }

ACTION_POINTER_DOWN 是指有另外一个手指发生了触摸。这里的处理是将 mActivePointerId 赋值给新的点了。所以在 ScrollView 中当有一个手指按下,我们再按下另一个手指时,第二个按下的手指能决定 ScrollView 的滑动。

  • ACTION_POINTER_UP
 @Override
  public boolean onTouchEvent(MotionEvent ev) {
    ......
    switch (actionMasked) {
      ......
      case MotionEvent.ACTION_POINTER_UP:
        onSecondaryPointerUp(ev);
        mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
        break;
    }
    ......
  }

ACTION_POINTER_UP 是指多个手指中的一个手指离开屏幕。所以这里会检测是否是当前 active 的手指离开了,并做相应的处理,具体逻辑在 onSecondaryPointerUp(ev); 方法中,我们就不多解释了。至此整个 ScrollView 我们应该有了一个清晰完整的理解了。最后再分享一个小 trick 。一行代码实现仿 ios 的弹性 ScrollView

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。