关闭

[置顶] Android之实现滑动的七种方法总结

1107人阅读 评论(1) 收藏 举报
分类:



            

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

*layout方法

*offsetLetfAndRight()与offsetTopAndBottom()

*LayoutParams

*scrollTo与scrollBy

*Scroller

*属性动画

*ViewDragHelper

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()方法来获取触摸事件的类型,其代码模式如下:

 @Overridepublic 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随着我们手指的滑动而滑动,布局代码如下:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"          android:layout_width="match_parent"          android:layout_height="match_parent"          android:orientation="vertical"><com.liaojh.scrolldemo.DragView    android:layout_width="100dp"    android:layout_height="100dp"    android:background="#88ffffff"/></LinearLayout>     

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);}@Overridepublic 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的父控件到屏幕顶边的距离,从而不是真正的偏移量了。

   @Overridepublic 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类可以获取到当前的滚动值,这样我们就可以实现平滑一定的效果了,一般模板代码如下:

     @Overridepublic 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进行处理,代码如下:

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

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

    //3. 重写computeScroll@Overridepublic 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类,特别是最后一个,也是最难最复杂的,但也是甩的最多的。

2
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:156297次
    • 积分:2828
    • 等级:
    • 排名:第12637名
    • 原创:125篇
    • 转载:56篇
    • 译文:0篇
    • 评论:54条
    博客专栏
    文章分类