本章将介绍两大部分内容:
发生滑动效果的原因
如何处理、实现滑动效果
一、滑动效果是如何产生的
滑动一个View,本质上来说就是改变当前View的位置。所以,要实现View的滑动,就必须监听用户触摸的事件,并根据事件传入的坐标,动态且不断地改变View的坐标,从而实现View随用户触摸的滑动而滑动。
首先需要先了解一下Android中的窗口坐标体系和屏幕的触控事件——MotionEvent。
1、Android坐标系
滑动是相对于参考系的运动。在Android中,将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点乡下是Y轴正方向。
系统提供了getLocationOnScreen(intlocation[])这样的方法来获取Android坐标系中点的位置,即视图左上角相对于坐标原点的位置。在触控事件中使用getRawX()、getRawY()方法获得的坐标同样是Android坐标系中的坐标。
2、视图坐标系
视图坐标系描述了子视图在父视图中的位置关系。这种坐标系以父视图的左上角为坐标原点,向右为X轴正方形,向下为Y轴正方向。
在触控事件中,通过getX()、getY()所获得坐标就是视图坐标系中的坐标。
3、触控事件——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 motionEvent) {
//获取当前输入点的X、Y坐标(视图坐标)
int x = (int)motionEvent.getX();
int y = (int)motionEvent.getY();
switch (motionEvent.getAction()){
case MotionEvent.ACTION_DOWN:
//处理输入的按下事件
break;
case MotionEvent.ACTION_MOVE:
//处理输入的移动事件
break;
case MotionEvent.ACTION_UP:
//处理输入的离开事件
break;
}
return true;
}
上面代码不过是完成了触控事件的监听,后面还要在触控事件中完成具体的逻辑。
在Android中,系统提供了很多方法来获取坐标值、相对距离等。下面总结了一些 API:
这些方法可以分成如下两个类别:
二、实现滑动的七种方法
系统提供了API来供我们动态的修改一个View的坐标,即实现滑动效果。但是不管哪种方式,其实现的基本思想都是当触摸View时,系统记下当前触摸点坐标,当手指移动时,系统记下移动后的触摸点的坐标,从而获取到相对于前一次坐标点的偏移量,通过偏移量来修改View的坐标,这样不断重复,从而实现滑动效果。
下面我们就实现这样一个效果:让自定义的一个View随着手指在屏幕上的滑动而滑动。
我们先定义一个View,并置于LinearLayout中:
1)先新建一个DragView类继承View
public class DragView extends View {
public DragView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
2)然后在布局中加入这个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.example.administrator.dragviewtest.DragView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/colorPrimary"/>
</LinearLayout>
初始化显示的效果如图:
1、layout方法
在View进行绘制时,会调用onLayout()方法来设置显示的位置。同样,可以通过修改View的left、top、right、bottom四个属性来控制View的坐标。
public class DragView extends View {
private int lastX,lastY;
public DragView(Context context,AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取当前触摸点的X、Y坐标(视图坐标)
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:
//计算偏移量
//并将偏移量作用到Layout方法中
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
//在当前left、top、right、bottom的基础上加上偏移量
layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
//重新设置初始坐标
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
}
上面代码中使用的是getX()、getY()方法来获取坐标值,即通过视图坐标来获取偏移量。当然,也可以使用getRawX()、getRawY()来获取坐标,并使用绝对坐标来计算偏移量。但是使用绝对坐标系,一定要记得在每次执行完ACTION_MOVE的逻辑后,都要重新设置初始坐标,这样才能准确地获取偏移量。
2、offsetLeftAndRight()与offsetTopAndBottom()
这个方法相当于系统提供的一个对左右、上下移动的API的封装。只需要在layout的实现的基础上将下面这行代码替换:
layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
替换成如下代码就可以对View进行重新布局,与layout的使用效果一样。
//同时对left和right进行偏移
offsetLeftAndRight(offsetX);
//同时对top和bottom进行偏移
offsetTopAndBottom(offsetY);
3、LayoutParams
LayoutParams保存了一个View的布局参数,因此我们可以通过改变LayoutParams来动态的修改一个布局的位置参数,从而达到改变View位置的效果:
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.rightMargin = getRight() + offsetY;
setLayoutParams(layoutParams);
使用getLayoutParams()来获取一个View的LayoutParams,但这里要注意需要根据View所在父布局的类型来设置不同的类型,比如,如果父布局是RelativeLayout中,就要使用RelativeLayout.LayoutParams。
在通过改变LayoutParams来改变一个View的位置时,通常改变的是这个View的Margin属性,所以除了使用布局的LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams来实现这样一个功能。
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.rightMargin = getRight() + offsetY;
setLayoutParams(layoutParams);
使用ViewGroup.MarginLayoutParams更加方便,因为它不需考虑父布局的类型。
4、scrollTo与scrollBy
在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View的位置。scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(dx,dy)表示移动的增加量为dx,dy。
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
scrollBy(offsetX,offsetY);
使用上述方法,效果展示我们的View却没有移动。实际上View确实移动了,只是它移动的是View的content,即让View的内容移动,例如TextView,content就是它的文本;ImageView,content就是它的drawable对象。如果在ViewGroup中使用scrollTo、scrollBy方法,那么移动的就将是所有子View。
所以我们就在该View所在的ViewGroup中来使用scrollBy方法。
((View)getParent()).scrollBy(offsetX,offsetY);
再次拖动View时,你会发现View虽然移动了,但却在乱动,并不是随着触摸点 移动而移动。这是因为参考系选择的不同。如果将scrollBy的参数dx和dy设置为正数,那么content将向坐标轴负方向移动;如果将scrollBy中的参数dx和dy设置为负数,那么content将向坐标轴正方向移动。所以我们必须将前面例子中的偏移量改为负值:
((View)getParent()).scrollBy(-offsetX,-offsetY);
这样效果就与前面几种方式的效果相同了。
5、S croller
Scroller类可以实现平滑移动的效果。
下面的例子演示了如何使用Scroller类实现平滑移动,让子View跟随者手指的滑动二滑动,但是在手指离开屏幕时,让子View平滑的移动到初始位置,即屏幕左上角。
使用Scroller类需要如下三个步骤:
第一步:初始化Scroller
首先通过它的构造方法来创建一个Scroll对象:
//初始化Scroller
Scroller mScroller = new Scroller(context);
第二步:重写computeScroll()方法,实现模拟滑动
computeScroll()方法是使用Scroller类的核心,系统在绘制View的时候会在draw()方法中调用该方法。这个方法实际上就是使用的scrollTo()方法,再结合Scroller对象,帮助获取到当前的滚动值。我们可以通过不断地瞬间移动一个小的距离来实现整体上的平滑移动效果:
public void computeScroll(){
super.computeScroll();
//判断Scroller是否执行完毕
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
//通过重绘来不断调用computeScroll
invalidate();
}
}
Scroller类提供了computeScrollOffset()方法来判断是否完成了整个滑动,同时也提供了getCurrX()、getCurrY()方法来获得当前的滑动坐标。
由于只能在computeScroll()方法中获取模拟过程中的scrollX和scrollY坐标,但computeScroll()是不会自动调用的,所以只能通过invalidate()-draw()-computeScroll()来简介调用computeScroll(),所以需要在模板代码中调用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)
他们的区别就是一个具有指定的持续时长,另一个没有。其他四个坐标,与他们的命名含义相同,就是起始坐标和偏移量。通常可以使用getScrollX()和getScrollY()方法来获取父视图中content所滑动到的点的坐标,不过要注意它的正负。
所以在实例中,首先初始化Scroller对象,然后重写View的computeSroll()方法,最后,需要监听手机离开屏幕事件,并该事件中通过调用startScroll()方法完成平滑移动。
case MotionEvent.ACTION_UP:
//手指离开时,执行滑动过程
View viewGroup = ((View)getParent());
mScroller.startScroll(viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
-viewGroup.getScrollY(),);
invalidate();
break;