首页 > 代码库 > Android之实现滑动的七种方法总结
Android之实现滑动的七种方法总结
在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类,特别是最后一个,也是最难最复杂的,但也是甩的最多的。
Android之实现滑动的七种方法总结