Android Scroll分析(二)实现滑动的多种方法

当了解了Android坐标系和触控事件后, 我们再来看看如何使用系统的API来实现动态修改一个View的坐标, 但实现思路基本是一致的,在触摸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.gavinandre.androidherosamples.chapterfour.DragView1
        android:layout_width="100dp"
        android:layout_height="100dp"/>
</LinearLayout>

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

这里写图片描述

layout方法

View进行绘制时,会调用onLayout()方法来设置显示的位置。因此我们就可以在layout()方法里修改View的left、top、right、bottom四个属性来控制View的坐标, 代码如下所示:

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

在上面的代码中,每次回调onTouchEvent时获取触摸点坐标, 然后在ACTION_DOWN事件中记录触摸点坐标, 最后在ACTION_MOVE事件中计算偏移量, 将偏移量作用到Layout方法中, 这样每次移动后, View都会调用Layout方法来对自己重新布局, 从而达到移动View的效果.
上面代码我们使用getX()、getY()方法来获取坐标值,即通过视图坐标来获取偏移量。当然,同样可以使用getRawX()、getRawY()来获取坐标,并使用绝对坐标来计算偏移量,代码如下:

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

使用绝对坐标系需要注意一点, 就是每次执行完ACTION_MOVE一定要重新设置初始坐标, 这样才能准确地获取偏移量.

offsetLeftAndRight()与offsetTopAndBottom()

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

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

offsetX,offsetY的计算方法与前面Layout方法中的一样

LayoutParams

LayoutParams保存了一个View的布局参数。因此可以在程序中,通过改变LayoutParams来动态地修改一个布局的位置参数,从而达到改变View位置的效果. 我们可以很方便的在程序中使用getLayoutParams()来获取一个View的LayoutParams, 计算偏移量的方法与Layout方法中一样. 获取到偏移量之后就可以通过setLayoutParams来改变其LayoutParams,代码如下。

    //这里要注意如果父布局是LinearLayout, 那么就要使用LinearLayout.LayoutParams
    //如果是Relativelayout,则要使用Relativelayout.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并没有移动,这是因为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)。

这里写图片描述

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

这里写图片描述

我们可以发现,虽然设置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

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

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

这里写图片描述

一般情况下,使用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, int duration)
public void startScroll(int startX, int startY, int dx, int dy)

区别就是一个可以指定持续时长,另一个没有,与动画中设置duration类似。而其它四个坐标,就是起始坐标(相对于View原始位置)与偏移量,在获取坐标时通常可以使用getScrollX()与getScrollY()方法来获取父视图中content所滑动到的点的坐标,不过这个值的正负与scrollBy、scrollTo中讲解的情况一样。

最后我们只要在onTouchEvent中增加一个ACTION_UP监听就可以使用Scroller类来实现平滑移动了,代码如下:

    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滑动到原来的位置,这里要注意的还是invalidate()方法,需要使用这个方法来通知View进行重绘,从而来调用computeScroll()的模拟过程。

代码下载

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值