在android开发中,滑动对一个app来说,是非常重要的,流畅的滑动操作,能够给用户带来用好的体验,那么本次就来讲讲android中实现滑动有哪些方式。其实滑动一个View,本质上是移动一个View,改变其当前所属的位置,要实现View的滑动,就必须监听用户触摸的事件,且获取事件传入的坐标值,从而动画的改变位置而实现滑动。
首先要知道android的坐标系与我们平常学习的坐标系是不一样的,在android中是将左上方作为坐标原点,向右为x抽正方向,向下为y抽正方向,像在触摸事件中,getRawX(),getRawY()获取到的就是Android坐标中的坐标.
android开发中除了上面的这种坐标以外,还有一种坐标,叫视图坐标系,他的原点不在是屏幕左上方,而是以父布局坐上角为坐标原点,像在触摸事件中,getX(),getY()获取到的就是视图坐标中的坐标.
触摸事件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中实现滑动的其中方法:
其实不管是哪种滑动,他们的基本思路是不变的,都是:当触摸View时,系统记下当前的触摸坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获得相对前一个点的偏移量,通过偏移量来修改View的坐标,并不断的更新,重复此动作,即可实现滑动的过程。
首先我们先来定义一个View,并置于LinearLayout中,我们的目的是要实现View随着我们手指的滑动而滑动,布局代码如下:
我们知道,在进行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;
}
这个方法相当于系统提供了一个对左右,上下移动的API的封装,在计算出偏移量之后,只需使用如下代码设置即可:
offsetLeftAndRight(offSetX);
offsetTopAndBottom(offSetY);
偏移量的计算与上面一致,只是换了layout方法而已。
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);
//使用这种方式的好处就是不用考虑父布局类型
在一个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也可以达到这样的效果。
如果让一个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;
一看这个类的名字,我们就知道他是与拖拽有关的,猜的没错,通过这个类我们基本可以实现各种不同的滑动,拖放效果,他是非常强大的一个类,但是它也是最为复杂的,但是不要慌,只要你不断的练习,就可以数量的掌握它的使用技巧。下面我们使用这个类来时实现类似于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类,特别是最后一个,也是最难最复杂的,但也是甩的最多的。
终于写完了,好累的赶脚~~~