3

Android之实现滑动的七种方法总结

 3 years ago
source link: http://www.androidchina.net/6748.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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类,特别是最后一个,也是最难最复杂的,但也是甩的最多的。

转载请注明:Android开发中文站 » Android之实现滑动的七种方法总结


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK