Android中View滑动方式的选择

View的滑动是我们开发中需要的一项基本技能,当然,Android在这方面做的还是比较出色, 提供了多种实现方式。

  1. 重写View的onTouchEvent或设置View的setOnTouchListener(),在MotionEvent.MotionEvent.ACTION_MOVE中做相应的滑动处理;
  2. 采用动画的方式(View动画或属性动画)实现;

不论哪种方式,其实质都是可以调用scrollTo/scrollBy,或者setLayoutParams()或layout()来实现的。

下面我们对这几种情况进行详细说明:

由于onTouchEvent和setOnTouchListener()可实现相同的功能,但setOnTouchListener()使用起来更简单,因为不需要自定义View, 同时setOnTouchListener中的onTouch比onTouchEvent优先执行,从View的dispatchTouchEvent()就可看出:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        // 没有设置mOnTouchListener的时候再执行onToucheEvent()
        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

那么下面就以setOnToucheListener()为例,来实现第一种滑动方案:

view.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                    startX = motionEvent.getX();
                startY = motionEvent.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                    float distanceX = motionEvent.getX() - startX;
                float distanceY = motionEvent.getY() - startY;
                // 处理滑动事件,其滑动距离为distance
                startX = motionEvent.getX();
                startY = moitonEvent.getY();
                break;
            default:
                break;
        }

        return true;
    }
});

上面的代码是实现滑动的一个基本框架,其滑动方式的差异就存在于处理滑动事件的部分。下面我们通过具体的方式来实现:

scrollTo/scrollBy方式

在使用这种方式的时候需要注意以下问题:

  1. 它引起的移动仅仅是内容的移动,View本身是不移动的。例如,对于TextView,使用这种方式滑动时,移动的只是文字,而TextView本身不会移动;而对于ViewGroup, 移动的当然也就是childView了。

  2. scrollTo表示移动至指定位置,scrollBy表示移动指定偏移量。这两个函数中的参数需要特别注意一下,参数>0表示沿着坐标轴反方向移动,反之向坐标轴正方向移动,与我们理解的坐标总是反的。

  3. 由于移动的是View的内容而不是View本身,所以移动之后View的事件触发区域还停留在移动之前的位置,也就是说,移动之后,View的内容所在的位置可能会出现无法响应事件的情况。

了解了上面的基本知识,下面我们来通过这种方式来完善我们的代码,下面我们只展示处理滑动事件的代码:

scrollBy(distanceX, distanceY);
/* 当然也可以通过scrollTo来实现
int scrollX = view.getScrollX();
int scrollY = view.getScrollY();
scrollTo(scrollX + distanceX, scrollY + distanceY);
*/

毫无疑问,scrollBy比scrollTo实现起来要简单,毕竟scrollBy也是通过scrollTo来实现的,源码如下:

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

这种方式实现滑动很简单,但局限性也很大,因为它只能移动View的内容,而不能移动View本身,所以经常使用在ViewGroup的滑动处理中。

LayoutParams方式

在开发过程中,我们经常遇到需要动态修改View位置或大小的情况,一般我们都是通过获取其LayoutParams,然后设置相应的属性,最后调用setLayoutParams()来完成。那在滑动处理过程中,也可以使用这种方式,动态地修改其LayoutParams的属性来实现滑动。具体代码如下:

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams();
params.leftMargin += distanceX;
params.topMargin += distanceY;
view.setLayoutParams(params);

与scrollBy/scrollTo不同的是,这种方式移动的是View本身,而不是其内容。同时,其中的distanceX/distanceY大于0表示向坐标轴正方向移动,反之表示向坐标轴反方向移动。

调用View的layout()方法

View的layout()方法用来动态设置View的边距,通过重复调用这个方法,也可以达到滑动的方式。具体代码如下:

view.layout((int)(view.getLeft() + distanceX), (int)(view.getTop() + distanceY, 0, 0));
view.requestLayout();    // 注意调用layout后需要调用requestLayout刷新布局

与LayoutParams方式一样,这种方式移动的也是View本身。从本质上来讲,这两种方式没什么区别,都是通过影响Layout过程来实现的。

从onLayout()的源码中我们可以看出, 影响View位置的几个因素:

  1. 对齐方式;
  2. 边距,如left, top, right, bottom;
  3. params参数的margin属性;
  4. parentView的padding属性;

理论上来将,修改这几种因素都可引起View位置的变化,但是对齐方式局限性太大,而且各ViewGroup还存在差异,显然不可取。修改边距其实就是layout()方式,params参数当然也就是LayoutParams方式。而parentView的padding属性影响所有的childView, 所以不能通过这种方式来修改某个View的位置。

以上方式都是在涉及滑动手势的情况下的解决方案,其实在日常开发中,我们还会遇到其他需要View移动的场景,如点击引起View的移动。在这种情况下,我们就可以考虑使用动画方式来解决了。

View动画方式

Android提供了缩放,渐变,平移和旋转四种View动画,这种动画的特点是可重复播放,但动画结束后View将恢复位置。

属性动画方式

属性动画可认为是View动画的扩充,因为它可以通过渐变的方式来修改View的属性。这样就可解决scrollBy/scrollTo/LayoutParmas/layout()这几种方式单次移动引起的动画的生硬感。例如:将一个view向右移动200px,这里我们选择layoutParams的方式,那么伪代码应该如下所示:

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams();
params.leftMargin += 200;
view.setLayoutParams(leftMargin);

如果这种改变是在点击事件下触发的,那么就会发现view瞬间向右移动了200px, 这种感觉很生硬,那如何解决了,这里属性动画就派上用场了。修改代码如下:

ValueAnimator animator = ValueAnimator.ofInt(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        int value = (int) valueAnimator.getAnimatedValue();
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams();
        params.leftMargin += (value - lastValue);
        view.setLayoutParams(leftMargin);
        lastValue = value;
    }
});

属性动画将根据动画的执行时间和插值器将200px分成若干份,这样通过多次setLayoutParams的方式就实现了平滑移动的效果。

注意:本文的主线是分析View的滑动方案的选择,所以不对View动画和属性动画做更详细的介绍,对于这部分,更详细的内容可自行了解。

总结:

通过对Android提供的几种移动方案的分析,我们可以总结出以下结论:

  1. 如果需要跟随手势移动,移动的如果是View的内容(针对ViewGroup是childView), 那么选择scrollBy/scrollTo来处理;如果移动的是View本身,那么选择LayoutParams或者layout()的方式来实现;
  2. 如果是单次移动,且移动完成后恢复位置或重复性的移动效果,使用View动画或动画集合实现;
  3. 如果是点击,长按,双击等事件引起的View移动,使用属性动画是一个不错的选择;

以上仅是个人使用过程中的一些简单总结,如有不恰当之处,欢迎指正~

,