实现滑动的思想基本是一致的,当触摸View时,系统记下当前触摸点坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量来修改View的坐标,这样不断重复,从而实现滑动过程。

下面我们来看看实现滑动的几种方法。首先须定义一个View,并将其置于一个LinearLayout中,实现一个简单的布局,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.huangfei.example.DragView1
        android:layout_width="100dp"
        android:layout_height="100dp" />
</LinearLayout>

我们的目的就是让这个自定义的View随着手指在屏幕上的滑动而滑动,最终的效果如下所示:

android 上滑后台卡死 android 上下滑动_Scroller

layout方法

在View进行绘制时,会调用onLayout()方法来设置显示的位置。同样,可以通过修改View的left、top、right、bottom四个属性来控制View的坐标。代码如下:

private int mLastX;
    private int mLastY;

    // 视图坐标方式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                // 在当前left、top、right、bottom的基础上加上偏移量
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                break;
        }
        return true;
    }

在上面的代码中,使用getX()、getY()方法来获取坐标值,即通过视图坐标来获取偏移量。当然,同样可以使用getRawX()、getRawY()来获取坐标,并使用绝对坐标来计算偏移量,代码如下:

private int mLastX;
    private int mLastY;

    //  绝对坐标方式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录触摸点坐标
                mLastX = rawX;
                mLastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                // 计算偏移量
                int offsetX = rawX - mLastX;
                int offsetY = rawY - mLastY;
                // 在当前left、top、right、bottom的基础上加上偏移量
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                /**
                 * 使用绝对坐标方式,需注意一点,就是在每次执行完ACTION_MOVE的逻辑后,
                 * 一定要重新设置初始坐标,这样才能准确地获取偏移量
                 */
                mLastX = rawX;
                mLastY = rawY;
                break;
        }
        return true;
    }

offsetLeftAndRight()与offsetTopAndBottom()

这个方法相当于系统提供的一个对左右、上下移动的API的封装。当计算出偏移量后,只需要使用如下代码就可以完成View的重新布局,效果和使用Layout方法一样,代码如下所示:

//同时对left和right进行偏移
                offsetLeftAndRight(offsetX);
                //同时对top和bottom进行偏移
                offsetTopAndBottom(offsetY);

LayoutParams

LayoutParams保存了一个View的布局参数。因此可以在程序中,通过改变LayoutParams来动态地修改一个布局的位置参数,从而达到改变View位置的效果,代码如下。

/**
                 * 这里通过getLayoutParams()获取LayoutParams时,需要根据View所在父布局的类型来设置不同的类型,
                 * 比如这里将View放在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。类似的,如果
                 * 在RelativeLayout中,就要使用RelativeLayout.LayoutParams。当然,这一切的前提是你必须要有
                 * 一个父布局,不然系统无法获取到LayoutParams
                 */
                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);

在通过改变LayoutParams来改变一个View的位置时,通常改变的是这个View的Margin属性,所以除了使用布局的LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams来实现这样一个功能,代码如下:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);

我们可以发现,使用ViewGroup.MarginLayoutParams更加方便,不需要考虑父布局的类型,当然它们的本质是一样的。

scrollTo与scrollBy

在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View的位置。scrollTo(x, y)表示移动到一个相对于View原始位置(0, 0)的具体坐标点,而scrollBy(dx, dy)表示相对于View当前位置移动的增量为dx、dy。

与前面几种方式相同,在获取偏移量后使用scrollBy来移动View,代码如下所示:

int offsetX = x - mLastX;
int offsetY = y - mLastY;
scrollBy(offsetX , offsetY );

但是,当我们拖动View的时候,你会发现View并没有移动,这是怎么回事呢?难道是方法写错了,其实我们方法没有写错,View也的确移动了,只是它移动的并不是我们想要移动的东西。scrollTo、scrollBy方法移动的是View的content,既让View的内容移动,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移动的将是所有子View,但如果在View中使用,那么移动的将是View的内容,例如TextView,content就是它的文本;ImageView,content就是它的drawable对象。

那么我们就该在View所在的ViewGroup中来使用scrollBy方法,移动它的子View,代码如下:

((View) getParent()).scrollBy(offsetX, offsetY);

但是,当再次拖动View的时候,你会发现View虽然移动了,但却在乱动,并不是我们想要的跟随触摸点的移动而移动。这里需要先了解一下视图移动的知识。在下图中,中间的矩形相当于屏幕,即可视区域。后面的content就相当于画布,代表视图。可以看到,只有视图的中间部分目前是可视的,其他部分都不可见。在可见区域中,我们设置了一个Button,坐标为(20, 10)。

android 上滑后台卡死 android 上下滑动_android 上滑后台卡死_02

下面使用scrollBy方法,将屏幕(可视区域)在水平方向上向X轴正方向(右方)平移20,在竖直方向上向Y轴正方向(下方)平移10,平移之后的可视区域如下图所示:

android 上滑后台卡死 android 上下滑动_android 上滑后台卡死_03

我们可以发现,虽然设置scrollBy(20, 10),偏移量均为X轴、Y轴正方向上的正数,但是在屏幕的可视区域内,Button却向X轴、Y轴负方向上移动了。这就是因为参考系选择的不同而产生的不同效果。

通过上面的分析可以发现,如果将scrollBy中的参数dx、dy设置为正数,那么content将向坐标轴负方向移动;反之,content将向坐标轴正方向移动。所以要实现跟随触摸点的移动而移动,就必须将偏移量改为负值,代码如下:

int offsetX = x - lastX;
                int offsetY = y - lastY;
                ((View) getParent()).scrollBy(-offsetX, -offsetY);

类似地,也可以使用scrollTo方法来实现这一效果,代码如下。

int offsetX = x - lastX;
                int offsetY = y - lastY;
                View viewGroup = (View) getParent();
                viewGroup.scrollTo(-offsetX + viewGroup.getScrollX(), -offsetY + viewGroup.getScrollY());

Scroller

使用scrollTo与scrollBy方法,子View的平移都是瞬间发生的,在事件执行的时候平移就已经完成了。而Scroller类可以实现平滑移动的效果,而不再是瞬间完成的移动。

在下面的示例中,同样让子View跟随手指的滑动而滑动,但是在手指离开屏幕时,让子View平滑的移动到初始位置,即屏幕左上角,效果如下:

android 上滑后台卡死 android 上下滑动_Scroller_04

一般情况下,使用Scroller类需要如下三个步骤。

  • 初始化Scroller
// 初始化Scroller
mScroller = new Scroller(context);
  • 重写computeScroll()方法,实现模拟滑动

computeScroll()方法是使用Scroller类的核心,系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际上就是使用的scrollTo方法。再结合Scroller对象,帮助获取到当前的滚动值。我们可以通过不断地瞬间移动一个小的距离来实现整体上的平滑移动效果。computeScroll的代码如下:

@Override
    public void computeScroll() {
        super.computeScroll();
        // 判断Scroller是否执行完毕
        if(mScroller.computeScrollOffset()){
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            // 通过重绘来不断调用computeScroll
            invalidate();
        }
    }

Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了getCurrX()、getCurrY()方法来获取当前的滑动坐标。之所以调用invalidate()方法,是因为只能在computeScroll()方法中获取模拟过程中的scrollX和scrollY坐标。但computeScroll()方法是不会自动调用的,只能通过invalidate()——>draw()——>computeScroll()来间接调用computeScroll()方法,所以需要在代码中调用invalidate()方法,实现循环获取scrollX和scrollY的目的。而当模拟过程结束后,mScroller.computeScrollOffset()方法会返回false,从而中断循环,完成整个平滑移动过程。

  • startScroll开启模拟过程

在需要使用平滑移动的事件中,使用Scroller类的startScroll()方法来开启平滑移动过程。startScroll()方法具有两个重载方法。

public void startScroll(int startX, int startY, int dx, int dy)

public void startScroll(int startX, int startY, int dx, int dy, int duration)

它们唯一的区别就是一个具有指定的持续时长,而另一个没有,这个与动画中设置duration相似。而其它四个坐标,就是起始坐标(相对于View原始位置)与偏移量。

startScroll的代码如下:

case MotionEvent.ACTION_UP:
                // 手指离开时,执行滑动过程
                View viewGroup = (View) getParent();
                mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(),
                        -viewGroup.getScrollX(), -viewGroup.getScrollY());
                invalidate();
                break;

在startScroll()方法中,获取子View移动的距离——getScrollX()、getScrollY(),并将偏移量设为其相反数,从而将子View滑动到原来的位置。