02_View的滑动

481 阅读6分钟

View滑动的几种方式

1. 使用scrollTo/scrollBy

为了实现View的滑动,View提供了scrollTo和scrollBy这两个方法。它们的具体实现如下:

/**
 * Move the scrolled position of your view. This will cause a call to onScrollChanged(int, int, int, int) and the view will be invalidated.
 * Params:
 * x – the amount of pixels to scroll by horizontally
 * y – the amount of pixels to scroll by vertically
 */
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

/**
 * Set the scrolled position of your view. This will cause a call to
 * {@link #onScrollChanged(int, int, int, int)} and the view will be
 * invalidated.
 * @param x the x position to scroll to
 * @param y the y position to scroll to
 */
public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

从上面的源码可以看出,scrollBy实际上是调用了scrollTo方法。scrollTo方法中修改了mScrollXmScrollY的值,下面对其进行简单介绍:

  • mScrollX:通过getScrollX()获取。修改mScrollX的值会改变View内容的位置,但View本身的位置(left、top、right、bottom)不会改变。需要注意的是,mScrollX表示View内容的位置和View本身位置的距离,而translationX表示View本身相对于父容器的偏移量。

    在滑动过程中,mScrollX总是等于View左边缘和View内容左边缘在水平方向的距离。
    当View左边缘在View内容左边缘的右边时,mScrollX为正值,反之为负值。
    如果从左向右滑动,那么mScrollX为负值,反之为正值。
    
  • mScrollY:同mScrollX

  • scrollTo:实现基于绝对位置的滑动,通过修改mScrollXmScrollY来改变View内容的位置。注意,scrollTo只能改变View内容的位置,而不能改变View在布局中的位置,使用如下:

    // 将View内容滑动到View的原位置
    view.scrollTo(0, 0);
    // 将View内容滑动到View原位置右方100px的位置
    view.scrollTo(-100, 0);
    // 将View内容滑动到View原位置下方100px的位置
    view.scrollTo(0, -100);
    
  • scrollBy:实际也是调用了scrollTo方法,实现基于当前位置的相对滑动,使用如下:

    // 基于View内容的当前位置,向右方滑动100px
    view.scrollBy(-100, 0);
    // 基于View内容的当前位置,向下方滑动100px
    view.scrollBy(0, -100);
    

2. 使用动画

2.1 View动画

通过改变translationXtranslationY属性,使用View动画可以让View进行平移,示例如下:

// 将View向右平移100px,向下平移100px
TranslateAnimation animation = new TranslateAnimation(0f, 100f, 0f, 100f);
animation.setDuration(1000);
animation.setFillAfter(true);
animation.setInterpolator(new LinearInterpolator());
view.startAnimation(animation);

需要注意的是,View动画并没有真正改变View的位置参数,这会导致View显示位置改变了,但点击事件仍需在原位置触发。

2.2 属性动画

使用属性动画可以改变View的位置参数,解决View动画事件触发位置的问题,示例如下:

// 将View向右平移100px
ObjectAnimator.ofFloat(view, "translationX", 0f, 100f).start();

3. LayoutParams

通过改变LayoutParams的margin值也可以实现View滑动,示例如下:

// 通过设置leftMargin增加100px,让View向右滑动100px
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
params.leftMargin += 100;
view.setLayoutParams(params);
view.requestLayout();

通过以上几种方式,可以实现View的滑动效果,每种方式都有其适用场景和优缺点,根据具体需求选择合适的方法。

4. 使用layout方法

在自定义View中,可以通过调用View的layout(left, top, right, bottom)方法来改变View的位置,实现滑动,如下:

// 在自定义View中,计算滑动的距离,然后调用layout方法来重新设置View的位置
// 将View向右滑动100px
int offsetX = 100;
layout(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());

接下来,我们实现一个自定义View,可以跟随手指滑动。这个View实现起来很简单,只需要重写它的onTouchEvent方法并处理ACTION_MOVE事件,根据两次滑动之间的距离就可以实现滑动。我们选择用layout方法来实现,核心代码如下:

// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 获取手指当前的坐标
    int x = (int) event.getRawX();
    int y = (int) event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            // 计算手指滑动的距离
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);

            // 重新计算View四个顶点的位置
            int left = getLeft() + deltaX;
            int top = getTop() + deltaY;
            int right = getRight() + deltaX;
            int bottom = getBottom() + deltaY;
            // 调用layout方法来重新设置View的位置
            layout(left, top, right, bottom);
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    mLastX = x;
    mLastY = y;
    return true;
}

5. 使用动画实现View跟随手指滑动

除了使用layout方法,我们还可以通过动画来实现View跟随手指滑动。实现方式类似,需要重写View的onTouchEvent方法并处理ACTION_MOVE事件。核心代码如下:

// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;

@Override
public boolean onTouchEvent(MotionEvent event) {
    // 获取手指当前的坐标
    int x = (int) event.getRawX();
    int y = (int) event.getRawY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            // 计算手指滑动的距离
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);

            // 根据当前translationX和translationY计算滑动后的值
            float translationX = getTranslationX() + deltaX;
            float translationY = getTranslationY() + deltaY;
            // 使用动画完成View滑动
            setTranslationX(translationX);
            setTranslationY(translationY);
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    mLastX = x;
    mLastY = y;
    return true;
}

6. 使用Scroller

Scroller用于实现View的弹性滑动。Scroller本身无法直接让View滑动,它需要和View的computeScroll方法配合使用。Scroller的工作过程如下:

  1. 创建Scroller对象,并调用startScroll方法,传递滑动相关参数。Scroller内部会保存这些参数。
  2. 调用invalidate方法触发View的重绘,重绘过程中会调用computeScroll方法。
  3. 重写computeScroll方法,在其中实现View的弹性滑动。

使用方式如下:

/**
 * 在View内部或外部调用,让View缓慢滚动到指定位置
 */
public void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;

    // startScroll方法保存了滑动参数,具体滑动由invalidate方法发起
    mScroller.startScroll(scrollX, 0, delta, 0, 1000);
    // invalidate方法会导致View重绘,重绘过程中会调用computeScroll方法
    invalidate();
}

/**
 * 重写computeScroll方法,完成View的滑动
 */
@Override
public void computeScroll() {
    // computeScrollOffset根据时间流逝计算当前scrollX和scrollY
    if (mScroller.computeScrollOffset()) {
        // 调用scrollTo滑动View,滑动距离由computeScrollOffset计算
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        // postInvalidate继续调用computeScroll方法,逐步完成View的整个滑动过程
        postInvalidate();
    }
}

下面是ScrollerstartScroll方法的实现:

/**
 * 保存滑动相关参数
 * @param startX 滑动起点的x坐标
 * @param startY 滑动起点的y坐标
 * @param dx x方向要滑动的距离
 * @param dy y方向要滑动的距离
 * @param duration 滑动过程的时间
 */
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的不同滑动效果。每种方式都有其适用场景和优缺点,应根据具体需求选择合适的方法。


View系列文章

01_View基础知识

02_View的滑动

03_View的事件分发机制

04_View的工作流程

05_自定义View

05_自定义ViewGroup

06_View滑动冲突处理