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

2019-04-08  本文已影响0人  怡红快绿

比较生硬地滑动一个View,用户体验实在是太差了,所以我们需要实现优雅的弹性滑动效果。那么如何实现弹性滑动呢?

主要思想:把一次滑动分成若干次小距离滑动,并在一定时间内完成以达到弹性滑动的效果。

具体实现方法有很多,例如使用Scroller、Handler#postDelay和Thread#sleep。

一、使用Scroller

Scroller典型用法

//创建实例
Scroller scroller = new Scroller(context);

//设置滑动参数
scroller.startScroll(scroller.getStartX(), scroller.getStartY(), 1000, 1000, 1000);
//重绘View
invalidate();

//重写computeScroll方法
@Override
public void computeScroll() {
    if (mScroller != null) {
        if (mScroller.computeScrollOffset()) {
            mScrollX = mScroller.getCurrX();
            mScrollY = mScroller.getCurrY();
            scrollTo(mScrollX, mScrollY );
            postInvalidate();  // So we draw again
        }
    }
}

我们先描述上述代码主要逻辑:当我们构造Scroller对象并调用它的startScroll方法时,Scroller内部其实什么也没做,它只是保存我们传递的几个参数,我们根据源码看看这些参数的含义

    /**
     * Start scrolling by providing a starting point, the distance to travel,
     * and the duration of the scroll.
     * 
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     * @param duration Duration of the scroll in milliseconds.
     */
    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;
    }

注意这里的滑动指的是View内容的滑动,而不是它本身位置的改变。

既然startScroll方法没有做与滑动相关的动作,那么Scroller是怎么让View滑动的呢?答案就是invalidate方法,invalidate方法会导致View重绘,View重绘时draw方法中又会去调用computeScroll方法,这是一个空方法,需要我们自己去实现,这个方法才是View能够实现弹性滑动的关键所在。具体过程是这样的:

  1. View重绘时在draw方法中调用computeScroll方法,computeScroll方法会向当前的Scroller获取新的mScrollX、mScrollY;
  2. 通过scrollTo(mScrollX, mScrollY )实现滑动;
  3. 调用postInvalidate方法再次重绘,绘制过程与第一次一样;
  4. 如此反复直到滑动到指定位置。

开始重复绘制当然是有条件的,难道要任由View一直重绘下去?我们显然不能这样做。阅读TextView源码我们发现computeScroll方法开始执行之前有这么一个判断语句"if (mScroller.computeScrollOffset()) ",我们来看看computeScrollOffset方法的实现。

/**
 * Call this when you want to know the new location.  If it returns true,
 * the animation is not yet finished.
 */
public boolean computeScrollOffset() {
    if (mFinished) {
        return false;
    }
    int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - 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;
}

我们可以看到,方法内部是根据时间流逝的百分比来计算出mStartX 、mStartY变化的百分比,从而最终计算出mCurrX、mCurrY的值,从而确定新位置。方法computeScrollOffset返回true时表示滑动未结束,否则表示滑动结束。

二、通过动画

动画本身就是一个渐变的过程,所以我们可以用它来实现弹性效果。我们尝试着使用动画让一个View的内容在1秒内向右移动100像素:

ObjectAnimator.ofFloat(hello, "translationX", 0, 100)
        .setDuration(1000).start();

像上面这样通过动画来实现View的内容滑动还不是我想介绍的重点,我们还可以利用动画的特性,配合scrollTo()方法来实现一些动画不能实现的效果,例如模仿Scroller实现View的弹性滑动。用法如下:

ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float animatedFraction = animation.getAnimatedFraction();
        Log.d(TAG, "onAnimationUpdate: " + animatedFraction);
        hello.scrollTo((int) (-100 * animatedFraction), 0);
    }
});
valueAnimator.start();

上面定义的动画valueAnimator 本质上并没有作用在任何对象上,它只是在1000ms内完成了整个动画的过程。
这个方法的思想其实和Scroller非常类似,都是将一次滑动分割为多次小距离滑动:通过一个渐变的百分比animatedFraction 计算出每次需要滑动的距离,然后配合scrollTo完成整个滑动过程。
AnimatorUpdateListener监听事件作用非常大,我们可以在它的onAnimationUpdate方法内实现各种自定义滑动效果。

三、使用延时策略

核心思想:发送一系列延时消息,然后在这些消息中进行View的内容滑动,从而达到弹性滑动的效果。

我们可以使用Handler、View的postDelayed方法或者使用线程的sleep方法。下面以Handler为例来实现弹性滑动,其他方法思想都是类似的。

private static final int SCROLL_TO = 1;
private static final int SCROLL_COUNT = 50;
private int count;
private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case SCROLL_TO:
                count++;
                if (count <= SCROLL_COUNT) {
                    //计算百分比
                    float fraction = count / (float)SCROLL_COUNT;
                    Log.d(TAG, "handleMessage: "+fraction);
                    //根据百分比计算滑动的距离
                    int scrollX = (int) (fraction * -500);
                    hello.scrollTo(scrollX, 0);
                    //发送第count+1次消息
                    sendMessageDelay();
                }
        }
    }
};

注意:这里主要目的是为了介绍延迟策略,所以没有考虑Handler内存泄漏的问题,如何正确使用Handler请参考Android中常见的内存泄漏 & 解决方案

四、总结

上面几种实现弹性滑动的方法,都只是简单介绍了它们的实现思想,在实际使用中肯定比这要复杂得多,我们需要对它们进行灵活地扩展,这样它们才能在开发工作中发挥更大的作用。


参考

《Android开发艺术探索》

上一篇下一篇

猜你喜欢

热点阅读