0%

View 的事件体系(三):View 弹性滑动

1. 使用 Scroller

  • Scroller 的典型用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Scroller scroller = new Scroller(mContext);

    // 缓慢滚动到指定位置
    private void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    // 1000 ms 内滑向 destX,效果就是慢慢滑动
    mScroller.startScroll(scrollX, 0, deltaX, 0, 1000);
    invalidate();
    }

    @Override
    public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
    scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    postInvalidate();
    }
    }
  • Scroller 的工作原理

    • startScroll() 方法实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public void startScroll(int startX, int startY, int dx, int dy, int duration) {
      mMode = SCROLL_MODE;
      mFinished = false;
      mDuration = duration;
      mStartTime = AnimationUtils.currentAnimationTimeMillis();
      mStartX = startX;
      mStartY = startY;
      mFinalX = startX + dx;
      mFinalY = startY + dy;;
      mDeltaX = dx;
      mDeltaY = dy;
      mDurationReciprocal = 1.0f / (float) mDuration;
      }
      • startX 和 startY 表示的是滑动的起点,dx 和 dy 表示的是要滑动的距离,而 duration 表示的是滑动时间,即整个滑动过程完成所需要的时间,这里的滑动是指 View 内容的滑动而非 View 本身位置的改变
      • invalidate() 方法会导致 View 重绘,在 View 的 draw() 方法中又会去调用 computeScroll() 方法,正是因为这个自己实现的在 View 中是空实现的 computeScroll() 方法,View 才能实现弹性滑动
      • 当 View 重绘后会在 draw() 方法中调用 computeScroll(),而 computeScroll() 又会去向 Scroller 获取当前的 scrollX 和 scrollY
        • 然后通过 scrollTo() 方法实现滑动
        • 接着又调用 postInvalidate() 方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致 computeScroll() 方法被调用
        • 然后继续向 Scroller 获取当前的 scrollX 和 scrollY,并通过 scrollTo() 方法滑动到新的位置,如此反复,直到整个滑动过程结束
    • computeScrollOffset() 方法实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      /**
      * Call this when you want to know the new location. If it returns true,
      * the animation is not yet finished.
      */
      public booean computeScrollOffset() {
      ...
      int timePassed = (int) (AnimationUtils.currentAnimationTimeMilis() - mStartTime);

      if (timePassed < mDuration) {
      switch (mMode) {
      case SCROLL_MODE:
      final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
      mCurrX = mStartX + Math.round(x * mDeltaX);
      mCurrY = mStartY + Math.round(x * mDeltaY);
      break;
      ...
      }
      }
      return true;
      }
      • 根据时间的流逝来计算出当前的 scrollX 和 scrollY 的值
        • 根据时间流逝的百分比算出 scrollX 和 scrollY 改变的百分比并计算出当前的值
        • 方法返回值为 true 表示滑动还未结束,false 则表示滑动已经结束
    • 原理总结

      • Scoller 本身并不能实现 View 的滑动,它需要配合 View 的 computeScroll() 方法才能完成弹性滑动的效果
      • 它不断地让 View 重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔 Scroller 就可以得出 View 当前的滑动位置,知道了滑动位置就可以通过 srollTo() 方法来完成 View 的滑动
      • View 的每一次重绘都会导致 View 进行小幅度的滑动,多次的小幅度滑动就组成了弹性滑动,类似电影的帧放映

2. 通过动画

  • 动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果

    1
    2
    // Demo: 让一个 View 的内容在 100ms 内向左移动 100 像素
    ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
  • 利用动画的特性来实现一些动画不能实现的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Demo: 模仿 Scroller 来实现 View 的弹性滑动
    final int startX = 0;
    final int deltaX = 100;
    ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
    animator.addUpdateListener(new AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animator) {
    float fraction = animator.getAnimatedFraction();
    mButton.scrollTo(startX + (int) (deltaX * fraction), 0);
    }
    });
    animator.start();
    • 上面的动画本质上没有作用于任何对象上,它只是在 1000ms 内完成了整个动画过程
    • 利用这个特性,就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前 View 所要滑动的距离,这里的滑动针对的是 View 的内容而非 View 本身
    • 这个方法的思想和 Scroller 类似,都是通过改变一个百分比配合 scrollTo() 方法来完成 View 的滑动
    • 采用这种方法除了能完成弹性滑动以外,还可以实现其他动画效果,完全可以在 onAnimationUpdate() 方法中加上想要的其他操作

3. 使用延时策略

  • 核心思想:通过发送一系列的延时消息从而达到一种渐进式的效果

  • 实现方式:

    • 使用 Handler
    • 使用 View 的 postDelayed() 方法:通过它来发送一个消息,在消息中进行 View 的滑动,如果接连不断地发送这种延时消息,就可以实现弹性滑动的效果
    • 使用线程的 sleep() 方法:通过在 while 循环中不断滑动 View 且 sleep(),就可以实现弹性滑动的效果
  • Demo:

    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
    // 使用 Handler,在大约 1000ms 内将 View 的内容向左移动 100 像素
    // 之所以说大约,是因为采用这种方法无法精确定时,因为系统的消息调度也需要时间且所需时间不定
    private static final int MESSAGE_SCROLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 33;

    private int mCount = 0;

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case MESSAGE_SCROLL_TO:
    mCount++;
    if (mCount <= FRAME_COUNT) {
    float fraction = mCount / (float) FRAME_COUNT;
    int scrollX = (int) (fraction * 100);
    mButton.scrollTo(scrollX, 0);
    mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
    }
    break;
    default:
    break;
    }
    }
    }
-------------------- 本文结束感谢您的阅读 --------------------