Android进阶之_实现滑动的7种方式详解

在android开发中,滑动对一个app来说,是非常重要的,流畅的滑动操作,能够给用户带来用好的体验,那么本次就来讲讲android中实现滑动有哪些方式。其实滑动一个View,本质上是移动一个View,改变其当前所属的位置,要实现View的滑动,就必须监听用户触摸的事件,且获取事件传入的坐标值,从而动画的改变位置而实现滑动。

android坐标系

首先要知道android的坐标系与我们平常学习的坐标系是不一样的,在android中是将左上方作为坐标原点,向右为x抽正方向,向下为y抽正方向,像在触摸事件中,getRawX(),getRawY()获取到的就是Android坐标中的坐标.

视图坐标系

android开发中除了上面的这种坐标以外,还有一种坐标,叫视图坐标系,他的原点不在是屏幕左上方,而是以父布局坐上角为坐标原点,像在触摸事件中,getX(),getY()获取到的就是视图坐标中的坐标.

触摸事件–MotionEvent

触摸事件MotionEvent在用户交互中,有非常重要的作用,因此必须要掌握他,我们先来看看Motievent中封装的一些常用的触摸事件常亮:

 //单点触摸按下动作
 public static final int ACTION_DOWN             = 0;
 //单点触摸离开动作
 public static final int ACTION_UP               = 1;
 //触摸点移动动作
 public static final int ACTION_MOVE             = 2;
 //触摸动作取消
 public static final int ACTION_CANCEL           = 3;
 //触摸动作超出边界
 public static final int ACTION_OUTSIDE          = 4;
 //多点触摸按下动作
 public static final int ACTION_POINTER_DOWN     = 5;
 //多点触摸离开动作
 public static final int ACTION_POINTER_UP       = 6;

以上是比较常用的一些触摸事件,通常情况下,我们会在OnTouchEvent(MotionEvent event)方法中通过event.getAction()方法来获取触摸事件的类型,其代码模式如下:

 @Override
public boolean onTouchEvent(MotionEvent event)
{
    //获取当前输入点的坐标,(视图坐标)
    float x = event.getX();
    float y = event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //处理输入按下事件
            break;
        case MotionEvent.ACTION_MOVE:
            //处理输入的移动事件
            break;
        case MotionEvent.ACTION_UP:
            //处理输入的离开事件
            break;
    }
    return true; //注意,这里必须返回true,否则只能响应按下事件
}    

以上只是一个空壳的架构,遇到的具体的场景,也有可能会新增多其他事件,或是用不到这么多事件等等,要根据实际情况来处理。在介绍如何实现滑动之前先来看看android中给我们提供了那些常用的获取坐标值,相对距离等的方法,主要是有以下两个类别:

  • View 提供的获取坐标方法

    getTop(): 获取到的是View自身的顶边到其父布局顶边的距离

    getBottom(): 获取到的是View自身的底边到其父布局顶边的距离

    getLeft(): 获取到的是View自身的左边到其父布局左边的距离

    getRight(): 获取到的是View自身的右边到其父布局左边的距离

  • MotionEvent提供的方法

    getX(): 获取点击事件距离控件左边的距离,即视图坐标

    getY(): 获取点击事件距离控件顶边的距离,即视图坐标

    getRawX(): 获取点击事件距离整个屏幕左边的距离,即绝对坐标

    getRawY(): 获取点击事件距离整个屏幕顶边的距离,即绝对坐标

介绍上面一些基本的知识点后,下面我们就来进入正题了,android中实现滑动的其中方法:

实现滑动的7种方法

其实不管是哪种滑动,他们的基本思路是不变的,都是:当触摸View时,系统记下当前的触摸坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获得相对前一个点的偏移量,通过偏移量来修改View的坐标,并不断的更新,重复此动作,即可实现滑动的过程。
首先我们先来定义一个View,并置于LinearLayout中,我们的目的是要实现View随着我们手指的滑动而滑动,布局代码如下:






     

layout方法

我们知道,在进行View绘制时,会调用layout()方法来设置View的显示位置,而layout方法是通过left,top,right,bottom这四个参数来确定View的位置的,所以我们可以通过修改这四个参数的值,从而修改View的位置。首先我们在onTouchEvent方法中获取触摸点的坐标:

float x = event.getX();
float y = event.getY();

接着在ACTION_DOWN的时候记下触摸点的坐标值:

case MotionEvent.ACTION_DOWN:
            //记录按下触摸点的位置
            mLastX = x;
            mLastY = y;
            break;

最后在ACTION_MOVE的时候计算出偏移量,且将偏移量作用到layout方法中:

case MotionEvent.ACTION_MOVE:
            //计算偏移量(此次坐标值-上次触摸点坐标值)
            int offSetX = (int) (x - mLastX);
            int offSetY = (int) (y - mLastY);

            //在当前left,right,top.bottom的基础上加上偏移量
            layout(getLeft() + offSetX,
                    getTop() + offSetY,
                    getRight() + offSetX,
                    getBottom() + offSetY
            );
            break;     

这样每次在手指移动的时候,都会调用layout方法重新更新布局,从而达到移动的效果,完整代码如下:

package com.liaojh.scrolldemo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * @author LiaoJH
 * @DATE 15/11/7
 * @VERSION 1.0
 * @DESC TODO
 */
public class DragView extends View
{
    private float mLastX;
    private float mLastY;

     public DragView(Context context)
    {
        this(context, null);
    }

public DragView(Context context, AttributeSet attrs)
{
    this(context, attrs, 0);
}

public DragView(Context context, AttributeSet attrs, int defStyleAttr)
{
    super(context, attrs, defStyleAttr);
}

@Override
public boolean onTouchEvent(MotionEvent event)
{
    //获取当前输入点的坐标,(视图坐标)
    float x = event.getX();
    float y = event.getY();
    switch (event.getAction())
    {
        case MotionEvent.ACTION_DOWN:
            //记录按下触摸点的位置
            mLastX = x;
            mLastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            //计算偏移量(此次坐标值-上次触摸点坐标值)
            int offSetX = (int) (x - mLastX);
            int offSetY = (int) (y - mLastY);

            //在当前left,right,top.bottom的基础上加上偏移量
            layout(getLeft() + offSetX,
                    getTop() + offSetY,
                    getRight() + offSetX,
                    getBottom() + offSetY
            );

            break;
    }
    return true;
}
}     

当然也可以使用getRawX(),getRawY()来获取绝对坐标,然后使用绝对坐标来更新View的位置,但要注意,在每次执行完ACTION_MOVE的逻辑之后,一定要重新设置初始坐标,这样才能准确获取偏移量,否则每次的偏移量都会加上View的父控件到屏幕顶边的距离,从而不是真正的偏移量了。

   @Override
public boolean onTouchEvent(MotionEvent event)
{
    //获取当前输入点的坐标,(绝对坐标)
    float rawX = event.getRawX();
    float rawY = event.getRawY();
    switch (event.getAction())
    {
        case MotionEvent.ACTION_DOWN:
            //记录按下触摸点的位置
            mLastX = rawX;
            mLastY = rawY;
            break;
        case MotionEvent.ACTION_MOVE:
            //计算偏移量(此次坐标值-上次触摸点坐标值)
            int offSetX = (int) (rawX - mLastX);
            int offSetY = (int) (rawY - mLastY);

            //在当前left,right,top.bottom的基础上加上偏移量
            layout(getLeft() + offSetX,
                    getTop() + offSetY,
                    getRight() + offSetX,
                    getBottom() + offSetY
            );

            //重新设置初始位置的值
            mLastX = rawX;
            mLastY = rawY;
            break;
    }
    return true;
}

offsetLeftAndRight()与offsetTopAndBottom()

这个方法相当于系统提供了一个对左右,上下移动的API的封装,在计算出偏移量之后,只需使用如下代码设置即可:

 offsetLeftAndRight(offSetX);
 offsetTopAndBottom(offSetY);

偏移量的计算与上面一致,只是换了layout方法而已。

LayoutParams

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

 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
 lp.leftMargin = getLeft() + offSetX;
 lp.topMargin = getTop() + offSetY;
 setLayoutParams(lp);

使用此方式时需要特别注意:通过getLayoutParams()获取LayoutParams时,需要根据View所在的父布局的类型来设置不同的类型,比如这里,View所在的父布局是LinearLayout,所以可以强转成LinearLayout.LayoutParams。

在通过改变LayoutParams来改变View的位置时,通常改变的是这个View的Margin属性,其实除了LayoutParams之外,我们有时候还可以使用ViewGroup.MarginLayoutParams来改变View的位置,代码如下:

ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
lp.leftMargin = getLeft() + offSetX;
lp.topMargin = getTop() + offSetY;
setLayoutParams(lp);
//使用这种方式的好处就是不用考虑父布局类型

scrollTo与scrollBy

在一个View中,系统提供了scrollTo与scrollBy两种方式来改变一个View的位置,其中scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(x,y)表示移动的增量。与前面几种计算偏移量相同,使用scrollBy来移动View,代码如下:

 scrollBy(offSetX,offSetY);

然后我们拖动View,发现View并没有移动,这是为杂呢?其实,方法没有错,view也的确移动了,只是他移动的不是我们想要的东西。scrollTo,scrollBy方法移动的是view的content,即让view的内容移动,如果是在ViewGroup中使用scrollTo,scrollBy方法,那么移动的将是所有的子View,而如果在View中使用的话,就是view的内容,所以我们需要改一下我们之前的代码:

((View)getParent()).scrollBy(offSetX, offSetY);

这次是可以滑动了,但是我们发现,滑动的效果跟我们想象的不一样,完全相反了,这又是为什么呢?其实这是因为android中对于移动参考系选择的不同从而实现这样的效果,而我们想要实现我们滑动的效果,只需将偏移量设置为负值即可,代码如下:

((View) getParent()).scrollBy(-offSetX, -offSetY);

同样的在使用绝对坐标时,使用scrollTo也可以达到这样的效果。

scroller

如果让一个View向右移动200的距离,使用上面的方式,大家应该发现了一个问题,就是移动都是瞬间完成的,没有那种慢慢平滑的感觉,所以呢,android就给我们提供了一个类,叫scroller类,使用该类就可以实现像动画一样平滑的效果。

其实它实现的原理跟前面的scrooTo,scrollBy方法实现view的滑动原理类似,它是将ACTION_MOVE移动的一段位移划分成N段小的偏移量,然后再每一个偏移量里面使用scrollBy方法来实现view的瞬间移动,这样在整体的效果上就实现了平滑的效果,说白了就是利用人眼的视觉暂留特性。

下面我们就来实现这么一个例子,移动view到某个位置,松开手指,view都吸附到左边位置,一般来说,使用Scroller实现滑动,需经过以下几个步骤:

  • 初始化Scroller

    //初始化Scroller,使用默认的滑动时长与插值器
    mScroller = new Scroller(context);  
    
  • 重写computeScroll()方法

    该方法是Scroller类的核心,系统会在绘制View的时候调用draw()方法中调用该方法,这个方法本质上是使用scrollTo方法,通过Scroller类可以获取到当前的滚动值,这样我们就可以实现平滑一定的效果了,一般模板代码如下:

     @Override
    public void computeScroll()
    {
        super.computeScroll();
        //判断Scroller是否执行完成
        if (mScroller.computeScrollOffset()) {
            ((View)getParent()).scrollTo(
                mScroller.getCurrX(),
                mScroller.getCurrY()
            );
            //调用invalidate()computeScroll()方法
            invalidate();
        }
    }
    

Scroller类提供中的方法:

computeScrollOffset(): 判断是否完成了真个滑动

getCurrX(): 获取在x抽方向上当前滑动的距离

getCurrY(): 获取在y抽方向上当前滑动的距离
  • startScroll开启滑动

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

– 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这个参数,而这个是滑动的时长,如果没有使用默认时长,默认是250毫秒,而其他四个坐标则表示起始坐标与偏移量,可以通过getScrollX(),getScrollY()来获取父视图中content所滑动到的点的距离,不过要注意这个值的正负,它与scrollBy,scrollTo中说的是一样的。经过上面这三步,我们就可以实现Scroller的平滑一定了。

继续上面的例子,我们可以在onTouchEvent方法中监听ACTION_UP事件动作,调用startScroll方法,其代码如下:

 case MotionEvent.ACTION_UP:
            //第三步
            //当手指离开时,执行滑动过程
            ViewGroup viewGroup = (ViewGroup) getParent();
            mScroller.startScroll(
                    viewGroup.getScrollX(),
                    viewGroup.getScrollY(),
                    -viewGroup.getScrollX(),
                    0,
                    800
            );
            //刷新布局,从而调用computeScroll方法
            invalidate();
            break;

属相动画

使用属性动画同样可以控制一个View的滑动,下面使用属相动画来实现上边的效果(关于属相动画,请关注其他的博文),代码如下:

 case MotionEvent.ACTION_UP:
            ViewGroup viewGroup = (ViewGroup) getParent();
            //属性动画执行滑动
            ObjectAnimator.ofFloat(this, "translationX", viewGroup.getScrollX()).setDuration(500)
                          .start();
            break;

ViewDragHelper

一看这个类的名字,我们就知道他是与拖拽有关的,猜的没错,通过这个类我们基本可以实现各种不同的滑动,拖放效果,他是非常强大的一个类,但是它也是最为复杂的,但是不要慌,只要你不断的练习,就可以数量的掌握它的使用技巧。下面我们使用这个类来时实现类似于QQ滑动侧边栏的效果,相信广大朋友们多与这个现象是很熟悉的吧。

先来看看使用的步骤是如何的:

  • 初始化ViewDragHelper

    ViewDragHelper这个类通常是定义在一个ViewGroup的内部,并通过静态方法进行初始化,代码如下:

    //初始化ViewDragHelper
    viewDragHelper = ViewDragHelper.create(this,callback);

    它的第一个参数是要监听的View,通常是一个ViewGroup,第二个参数是一个Callback回调,它是整个ViewDragHelper的逻辑核心,后面进行具体介绍。

  • 拦截事件

    重写拦截事件onInterceptTouchEvent与onTouchEvent方法,将事件传递交给ViewDragHelper进行处理,代码如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {
        //2. 将事件交给ViewDragHelper
        return  viewDragHelper.shouldInterceptTouchEvent(ev);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        //2. 将触摸事件传递给ViewDragHelper,不可少
        viewDragHelper.processTouchEvent(event);
        return true;
    }
    
  • 处理computeScroll()方法

    前面我们在使用Scroller类的时候,重写过该方法,在这里我们也需要重写该方法,因为ViewDragHelper内部也是使用Scroller类来实现的,代码如下:

    //3. 重写computeScroll
    @Override
    public void computeScroll()
    {
        //持续平滑动画 (高频率调用)
        if (viewDragHelper.continueSettling(true))
            //  如果返回true, 动画还需要继续执行
            ViewCompat.postInvalidateOnAnimation(this);
    }
    
  • 处理回调Callback

    通过如下代码创建一个Callback:

         private ViewDragHelper.Callback callback = new ViewDragHelper.Callback()
    {
        @Override
        //此方法中可以指定在创建ViewDragHelper时,参数ViewParent中的那些子View可以被移动
        //根据返回结果决定当前child是否可以拖拽
        //  child 当前被拖拽的View
        //  pointerId 区分多点触摸的id
        public boolean tryCaptureView(View child, int pointerId)
        {
            //如果当前触摸的view是mMainView时开始检测
            return mMainView == child;
        }
    
        @Override
        //水平方向的滑动
        // 根据建议值 修正将要移动到的(横向)位置   (重要)
        // 此时没有发生真正的移动
        public int clampViewPositionHorizontal(View child, int left, int dx)
        {
            //返回要滑动的距离,默认返回0,既不滑动
            //参数参考clampViewPositionVertical
            f (child == mMainView)
            {
                if (left > 300)
                {
                    left = 300;
                }
                if (left < 0)
                {
                    left = 0;
                }
             }
            return left;
        }
    
        @Override
        //垂直方向的滑动
        // 根据建议值 修正将要移动到的(纵向)位置   (重要)
        // 此时没有发生真正的移动
        public int clampViewPositionVertical(View child, int top, int dy)
        {
            //top : 垂直向上child滑动的距离,
            //dy: 表示比较前一次的增量,通常只需返回top即可,如果需要精确计算padding等属性的话,就需要对left进行处理
            return super.clampViewPositionVertical(child, top, dy); //0
        }
    };
    

    到这里就可以拖拽mMainView移动了。

下面我们继续来优化这个代码,还记得之前我们使用Scroller时,当手指离开屏幕后,子view会吸附到左边位置,当时我们监听ACTION_UP,然后调用startScroll来实现的,这里我们使用ViewDragHelper来实现。

在ViewDragHelper.Callback中,系统提供了这么一个方法—onViewReleased(),我们可以通过重写这个方法,来实现之前的操作,当然这个方法内部也是通过Scroller来实现的,这也是为什么我们要重写computeScroll方法的原因,实现代码如下:

    @Override
    //拖动结束时调用
    public void onViewReleased(View releasedChild, float xvel, float yvel)
    {
        if (mMainView.getLeft() < 150)
        {
            // 触发一个平滑动画,关闭菜单,相当于Scroll的startScroll方法
            if (viewDragHelper.smoothSlideViewTo(mMainView, 0, 0))
            {
                // 返回true代表还没有移动到指定位置, 需要刷新界面.
                // 参数传this(child所在的ViewGroup)
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);
            }
        }
        else
        {
            //打开菜单
            if (viewDragHelper.smoothSlideViewTo(mMainView, 300, 0)) ;
            {
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);
            }
        }
        super.onViewReleased(releasedChild, xvel, yvel);
    }

当滑动的距离小于150时,mMainView回到原来的位置,当大于150时,滑动到300的位置,相当于打开了mMenuView,而且滑动的时候是很平滑的。此外还有一些方法:

    @Override
    public void onViewCaptured(View capturedChild, int activePointerId)
    {
        // 当capturedChild被捕获时,调用.
        super.onViewCaptured(capturedChild, activePointerId);
    }

    @Override
    public int getViewHorizontalDragRange(View child)
    {
        // 返回拖拽的范围, 不对拖拽进行真正的限制. 仅仅决定了动画执行速度
        return 300;
    }

    @Override
    //当View位置改变的时候, 处理要做的事情 (更新状态, 伴随动画, 重绘界面)
    // 此时,View已经发生了位置的改变
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
    {
        // changedView 改变位置的View
        // left 新的左边值
        // dx 水平方向变化量
        super.onViewPositionChanged(changedView, left, top, dx, dy);
    }

说明:里面还有很多关于处理各种事件方法的定义,如:

onViewCaptured():用户触摸到view后回调

onViewDragStateChanged(state):这个事件在拖拽状态改变时回调,比如:idle,dragging等状态

onViewPositionChanged():这个是在位置改变的时候回调,常用于滑动时伴随动画的实现效果等

对于里面的方法,如果不知道什么意思,则可以打印log,看看参数的意思。

总结

这里介绍的就是android实现滑动的七种方法,至于使用哪一种好,就要结合具体的项目需求场景了,毕竟硬生生的实现这个效果,而不管用户的使用体验式不切实际的,这里面个人觉得比较重要的是Scroller类的使用。属性动画以及ViewDragHelper类,特别是最后一个,也是最难最复杂的,但也是甩的最多的。

终于写完了,好累的赶脚~~~

你可能感兴趣的:(android)