滑动效果是如何产生的
滑动一个View,本质上来说就是移动一个View。改变其当前所处的位置,它的原理与动画效果的实现非常相似,都是通过不断地改变View的坐标来实现这一效果。所以,要实现View的滑动,就必须监听用户触摸的事件,并根据事件传入的坐标,动态且不断地改变View的坐标,从而实现View跟随用户触摸的滑动而滑动。
Android坐标系
在物理学中,要描述一个物体的运动,就必须选定一个参考系。所谓滑动,正是相对于参考系的运动。在Android中,将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向
系统提供了getLocationOnScreen(intlocation[])来获取Android坐标中的位置,即该视图左上角Android的坐标,另外,在触摸事件中使用getRawX(),getRawY()方法来获取坐标同样是Android坐标系中的坐标。
视图坐标系
Android中除了上面所说的这种坐标系之外,还有一个视图坐标系,它描述了子视图在父视图中的位置关系。这两种坐标系并不矛盾也不复杂,他们的作用是相辅相成的。与Android坐标系类似,视图坐标系同样是以原点向右为X轴正方向,以原点向下为Y轴正方向,只不过在视图坐标系中,原点不再是Android坐标系中的屏幕最左上角,而是以父视图左上角为坐标原点。
在触摸事件中,通过getX(),getY()所获得的坐标就是视图坐标系中的坐标。
触控事件---------MotionEvent
触控事件MotionEvent在用户交互中,占着举足轻重的地位,学好触控事件是掌握后续内容的基础。首先,我们来看看MotionEvent中封装的一些常用的事件常量,它定义了触控事件的不同类型。
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()方法来获取触控事件的类型,并通过switch-case方法来进行筛选,这个代码的模式基本固定。
@Override
public boolean onTouchEvent(MotionEvent ev) {
//获取当前输入点的X、Y坐标(视图坐标)
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//处理触摸的按下事件
break;
case MotionEvent.ACTION_MOVE:
//处理触摸的移动事件
break;
case MotionEvent.ACTION_UP:
//处理触摸的离开事件
break;
}
return true;
}
在不涉及多点操作的情况下,通常可以使用以上代码来完成触控事件的监听,不过这里只是一个代码模板,后面我们会在触控事件中完成具体的逻辑。
在Android中,系统提供了非常多的方法来获取坐标值、相对距离等。方法丰富固然好,但是也给初学者带来了很多困惑,不知道在什么情况下使用什么方法,下面总结了一些API,结合Android坐标系来看看该如何使用它们。
这些方法可以分为如下两个类型:
1.View提供的获取坐标的方法
getTop():获取到的是View自身的顶边到其父布局顶边的距离
getLeft(): 获取到的是View自身的左边到其父布局左边的距离
getRight():获取到的是View自身的右边到其父布局左边的距离
getBottom():获取到的是View自身的底边到其父布局顶边的距离
2.MotionEvent提供的方法
getX():获取点击事件距离控件左边的距离,即视图坐标。
getY():获取点击事件距离控件顶边的距离,即视图坐标。
getRawX();获取点击事件距离整个屏幕顶边的距离,即绝对坐标。
getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。
实现滑动的七钟方法
当了解了Android坐标系和触控坐标系后,我们再来看看如何使用系统提供的API来实现动态地修改一个View的坐标,即实现滑动效果。而不管采用哪一种方式,其实现的思想基本是一致的,当触摸View时,系统记下当前触摸点坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量来修改View的坐标,这样不断重复,从而实现滑动过程。
例:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.administrator.myapplication.MainActivity">
<com.example.administrator.myapplication.DragView
android:id="@+id/dragView"
android:layout_width="100dp"
android:layout_height="100dp" />
</LinearLayout>
我们的目的就是让这个自定义View跟随手指在屏幕上的滑动而滑动。
Layout方法
我们知道,在View进行绘制的时候,会调用onLayout()方法来设置显示的位置。同样,可以通过修改View的left、top、right、bottom四个属性来控制View的坐标。与前面提供的模板代码一样,在每次回调onTouchEvent的时候,我们都来获取一下触摸点的坐标。
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸点的坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
//重新设置开始坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_UP:
//处理输入的离开动作
break;
}
return true;
}
这里有点非常需要注意,就是如果使用视图坐标系,是不需要在每次移动后重置起始坐标的,因为它的参考坐标是不会随着View的移动而改变的,但是绝对布局,需要在每次移动后进行重置。
offsetLeftAndRight()与offsetTopAndBottom()
这个方法相当于系统提供的一个对左右,上下移动的API的封装。当计算出偏移量后,只需要使用如下代码代码就可以完成View的重新布局,效果与使用layout方法一样。
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getX();
int rawY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸点的坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int officeX = rawX - lastX;
int officeY = rawY - lastY;
offsetLeftAndRight(officeX);
offsetTopAndBottom(officeY);
break;
case MotionEvent.ACTION_UP:
//处理输入的离开动作
break;
}
return true;
}
LayoutParams
LayoutParams保存了一个View的布局参数。因此可以在程序中,通过改变LayoutParams来动态地修改一个布局的位置参数,从而达到改变View位置的效果。我们可以很方便地在程序中使用getLayoutParams()来获取一个View的layoutParams。当然计算偏移量的方法与在Layout方法中计算offset也是一样。当获取到偏移量之后,就可以通过setLayoutParams来改变其LayoutParams。
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getX();
int rawY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸点的坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int officeX = rawX - lastX;
int officeY = rawY - lastY;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + officeX;
layoutParams.topMargin = getTop() + officeY;
setLayoutParams(layoutParams);
break;
case MotionEvent.ACTION_UP:
//处理输入的离开动作
break;
}
return true;
}
不过这里需要注意的是,通过getLayoutParams()获取layoutParams是,需要根据View所在的父布局的类型来设置不同的类型,比如这里将View放置在LinearLayout中,那么就可以使用LinearLayout.LayoutParams。类似地,如果在RelativeLayout中,就要使用RelativeLayout.LayoutParams.当然,这一切的前提是你必须要有一个父布局,不然系统无法获取LayoutParams。
在通过改变LayoutParams来改变一个View的位置时,通常改变的是这个View的magin属性,所以除了使用布局的LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams:
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getX();
int rawY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸点的坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int officeX = rawX - lastX;
int officeY = rawY - lastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + officeX;
layoutParams.topMargin = getTop() + officeY;
setLayoutParams(layoutParams);
break;
case MotionEvent.ACTION_UP:
//处理输入的离开动作
break;
}
return true;
}
我们可以发现,使用ViewGroup.MarginLayoutParams更加方便,不需要考虑父布局的类型,当然它们的本质都是一样的。
scrollTo()与scrollBy()
在一个View中,系统提供了scrollTo、scrollBy()两种方式来改变一个View的位置。这两个方法的区别非常好理解,与英文To与By的区别类似,scrollTo(x,y)标识移动到一个具体的坐标点(x,y),而scrollBy(dx,dy)表示移动的增量为dx,dy.
与前面几种方式相同,在获取偏移量后使用scrollBy来移动View。
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getX();
int rawY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸点的坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int officeX = rawX - lastX;
int officeY = rawY - lastY;
scrollBy(officeX,officeY);
break;
case MotionEvent.ACTION_UP:
//处理输入的离开动作
break;
}
return true;
}
但是当我们拖动View的时候,发现View并咩有移动。因为它移动的并不是我们想要移动的东西。scrollTo、scrollBy方法移动的是View的 content,即让View的内容移动,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移动的将是所有子View,但如果在View中使用,那么移动的将是View的内容,例如TextView,content就是它的文本;imageview,content就是它的drawable对象。
下面改进:
((View)getParent()). scrollBy(officeX,officeY);
但是,当再次拖动View的时候,你会发现View虽然动了,但是却在乱动。
上图,中间的矩形相当于屏幕,即可是区域,后面的content相当于画布,代表视图,大家可以看到,只有视图的中间部分目前是可视的,其他部分都不可见,可见区域中设置一个button,他的坐标是(20.10),下面我们使用scrollBy方法来进行移动后如图
我们会发现,虽然设置scrollBy(20.10),偏移量均为XY的正方向,但是屏幕的可视区域,Button却向反方向移动了,这就是参考系选择的不同,而产生的不同效果。
通过上面的分析,可以发现,我们将scrollBy的参数dx,dy设置成正数,那么content将向坐标轴负方向移动,反之,则正方向
完善:
//触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) event.getX();
int rawY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录触摸点的坐标
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
//计算偏移量
int officeX = rawX - lastX;
int officeY = rawY - lastY;
((View)getParent()). scrollBy(-officeX,-officeY);
break;
case MotionEvent.ACTION_UP:
//处理输入的离开动作
break;
}
return true;
}
scroller
既然提到了scrollTo、scrollBy方法,就不得不再说一说Scroller类。Scroller类与scrollTo、scrollBy方法十分相似,有着千丝万缕的联系。
1.初始阿虎scroller
首先,通过它的构造方法来创建一个人Scroller对象
//初始化mScroller
mScroller = new Scroller(context);
重写computeScroll()方法,实现模拟滑动
下面我们需要重写computeScroll()方法,它是使用Scroller类的核心,系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际上就是使用scrollTo方法。再结合Scroller对象,帮助获取到当前的滚动值。我们可以通过不断地瞬间移动一个小距离来实现整体上的平滑效果。通常情况下,computeScroll的代码可以利用如下模板代码来实现。
/**
* 模拟滑动
*/
@Override
public void computeScroll() {
super.computeScroll();
//判断Scroller是否执行完毕
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
}
//通过重绘来不断调用computeScroll
invalidate()
(); }
Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了getCurrX()、getCurrY()方法来获得当前的滑动坐标。在上面的代码中,唯一需要注意的是invalidate()方法,因为只能在computeScroll()方法中获取模拟过程中的scrollX和scrollY坐标。但computeScroll()方法是不会自动调用的,只能通过invalidate()->draw()->computeScroll()来间接调用computeSroll()方法,所以需要在模拟代码中调用invalidate()方法,实现循环获取scrollX和scrollY的目的。而当模拟过程结束后,scroller。computeScrollOffset()方法会返回false,从而中断循环,完成整个平滑移动过程。
startScroll开启模拟过程
最后,万事具备只欠东风。我们在需要使用平滑移动的事件中,使用Scroller类的startScroll()方法来开启平滑移动过程。startScroll()方法具有两个重载方法。
public void startScroll(int startX,int startY,int dx,int dy,int duration)
public void startScroll(int startX,int startY,int dx,int dy)
可以看到它们的区别就是一个具有指定的持续时长,而另一个没有。这个非常好理解,与在动画中设置duration和使用默认的显示时长是一个道理。而其他四个坐标,则与它们的命名含义相同,就是起始坐标与偏移量。在获取坐标的时候,通常可以使用getScrollX()和getScrollY()方法来获取父视图中content所滑动到的点的坐标,不过要注意的是这个值得正负,它与在scrollBy、scrollTo中讲解的情况是一样的。
通过上面三个步骤,我们就可以使用Scroller类来实现平滑移动了,下面回到实例中,在构造方法中初始化Scroller对象,并重写View的computeScroll()方法。最后,需要监听手指离开屏幕的事件,并在该事件中通过调用startScroll()方法完成平滑移动。
case MotionEvent.ACTION_UP:
//处理输入的离开动作
View view = ((View)getParent());
mScroller.startScroll(view.getScrollX(),view.getScrollY(),view.getScrollX(),view.getScrollY());
invalidate();
break;