滑动效果的产生


滑动一个 View ,其实就是移动一个 View,本质上是对 View 的坐标位置进行不停的改变。那么要实现这个效果,就必须要监听用户的触摸事件,根据传入的事件类型和坐标,动态且不断的改变 View 的坐标。

以下Demo 在 github 均能获取

首先,我们需要了解坐标系的概念。

Android 坐标系

在现实中,要描述一个物体的运动,就需要一个参考系。所谓的滑动,就是相对于参考系的运动。在 Android 中,将屏幕的左上角顶点作为 Android 坐标系的原点,从这个原点往右为 X 轴的正方向,从这个点往下是 Y 轴的正方向。

android坐标系.png

在触控事件中,使用 getRawX() 以及 getRawY() 可以获取到当前触摸点相对于 Android 坐标系的坐标。

视图坐标系

除了上面说的这种坐标系之外,还有一个视图坐标系。跟 Android 坐标系类似,也是从这个原点往右为 X 轴的正方向,从这个点往下是 Y 轴的正方向,但是原点的位置不再是屏幕的左上角顶点,而是父视图的左上角坐标原点,找触控事件中可以使用 getX() 以及 getY() 获取到当前触摸点相对于视图坐标系中的坐标。

视图坐标系.jpg

触控事件 -- MotionEvent

触控事件 MotionEvent 在用户交互中十分重要的,MotionEvent 中封装了一些事件常量:

触摸按下动作
ACTION_DOWN

触摸移动动作
ACTION_MOVE

触摸动作取消
ACTION_CANCEL

触摸动作离开
ACTION_UP

一般我们会在 onTouchEvent(MotionEvent event) 方法中通过传进来的 MotionEvent 引用的 getAction 方法来获取时间的类型,并用 switch-case 的方法来进行筛选,根据不同的时间进行不同的逻辑操作。

通常模板如下:

public boolean onTouchEvent(MotionEvent event) {  switch (event.getAction()) {    case MotionEvent.ACTION_DOWN:      break;    case MotionEvent.ACTION_MOVE:      break;    case MotionEvent.ACTION_UP:      break;  }  return true; }

在 Android 中提供了很多的方法来获取坐标值,相对距离等,下面总结了一些 api 来看看在不同的坐标系下面应该如何使用。

这些方法可以分成如下两个类型:

  • View 提供的获取坐标方法
    • getTop:获取到的是 View 自身的顶边到父布局顶边的距离
    • getLeft:获取到的是 View 自身的左边到父布局左边的距离
    • getRight:获取到的是 View 自身的右边到父布局右边的距离
    • getBottom:获取到的是 View 自身的底边到父布局底边的距离
  • MotionEvent 提供的获取坐标方法
    • getX : 获取触摸点距离当前控件左边的距离,也就是视图坐标
    • getY : 获取触摸点距离当前控件顶边的距离,也就是视图坐标
    • getRawX : 获取触摸点距离屏幕左边的距离,也就是绝对坐标
    • getRawY : 获取触摸点距离屏幕顶边的距离,也就是绝对坐标
各种方法获取到的位置.jpg

实现滑动的几种办法


现在已经了解关于坐标系和触控事件了,再来看看如何实现动态的修改一个 View 的坐标,即实现滑动效果。不管采用哪一种方式,实现的思路其实都是大致相同的。就是当触摸 View 的时候,系统记下当前触摸点的坐标;当手指一动的时候,系统记下移动后的触摸点坐标,两次的相差就是这次移动的偏移量,然后通过偏移量来修改 View 的坐标。这样不断重复,就实现了滑动的过程。

流程图.jpg

下面通过一个简单的实例来实现这个效果,就是 View 随着手指的滑动而滑动。这里我们需要自定义一个 View 并且重写他的 onTouchEvent 方法。

跟随触摸滑动的View

layout 方法

在 View 绘制的过程中,会调用 onLayout 方法来定位,同样,我们也可以手动调用此方法来对 View 进行手动的坐标定位。根据前面提供的思路,在按下的时候先保存一次触摸按下时的坐标。

  @Override public boolean onTouchEvent(MotionEvent event) {    //检测到触摸事件后 第一时间得到相对于父控件的触摸点坐标 并赋值给x,y    int x = (int) event.getX();    int y = (int) event.getY();    switch (event.getAction()) {      //触摸事件中绕不开的第一步,必然执行,将按下时的触摸点坐标赋值给 lastX 和 last Y      case MotionEvent.ACTION_DOWN:        lastX = x;        lastY = y;        break;      //触摸事件的第二步,这时候的x,y已经随着滑动操作产生了变化,用变化后的坐标减去首次触摸时的坐标得到 相对的偏移量      case MotionEvent.ACTION_MOVE:        int offsetX = x - lastX;        int offsetY = y - lastY;        //使用 layout 进行重新定位        layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX,    getBottom() + offsetY);        break;    }    return true;  }

以上,通过代码中的注释,应该都能明白操作的步骤了。

offsetLeftAndRight 与 offsetTopAndBottom

这两个方法相当于系统提供的一个对左右上下移动的 API 封装,得到偏移量之后使用如下代码就可以完成移动。

//使用 offsetLeftAndRight 和 offsetLeftAndRight 进行偏移,从而移动viewoffsetLeftAndRight(offsetX);offsetTopAndBottom(offsetY);

LayoutParams

LayoutParams 保存了一个 View 的布局参数,通过改变 LayoutParams 来动态的修改一个布局的位置参数,从而达到改变 View 位置的效果。我们可以很方便的在程序中使用 getLayoutParams 来获取一个 View 的 LayoutParams。得到偏移量后,就可以通过 setLayoutParams 来改变。

RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();layoutParams.leftMargin =getLeft()+offsetX;layoutParams.topMargin = getTop()+ offsetY;setLayoutParams(layoutParams);

这里的 RelativeLayout.LayoutParams 是根据你的父布局而定的 如果是 LinearLayout 的话就用 LinearLayout 的 LayoutParams。 当然了 如果你连父布局都没有,当我没说,那样是不能用这个方法的。

scrollTo 与 scrollBy

这里其实 scrollTo 和 scrollBy 使用起来很简单,但是理解起来稍微复杂一点。scrollTo 是直接移动到指定的坐标,而 scrollBy 是根据偏移量进行相对移动。但是需要注意的是,这两个都不是直接移动 View ,而是移动 View 中的 content ,比如 textView 中移动的是文字,imagView 中移动的是图片,移动的是内容,而不是本体。所以,我们应该在想要移动的 View 的父布局中去使用它,用它来移动 ViewGroup 中的子 View。

上面说的只是其中一个难点,还有一个难点就是参考系不同。
这样理解吧,ViewGroup 是一个长方形的相框,在相框背后是一块巨大的幕布,那么我们看到的内容,就是相框中所能囊括下的内容,在使用 scrollBy 进行移动的时候,移动的是整个相框,而相框里的内容没动,但是因为相框移动了,所以内容的位置也发生了变化。我们按照 X 轴将相框左边移动的话,那相框中的内容是在往右移动,所以在使用 scrollBy 的时候,内容是往反方向运动的,这里如果需要改为符合我们预期的移动方式,那么只需要将 scrollBy 的参数设置为负数即可。

迷之坐标.jpg

看看图中星星的位置就能理解了,我们往正方向右下角移动了坐标,但是左图中星星的位置在可视区域中是在往左上角移动,这是相反的。

((View) getParent()).scrollBy(-offsetX, -offsetY);

以上,就是使用 scrollBy 来进行移动,注意参数使用负数即可。

Scroller

前面我们使用的不管是 scrollBy 还是 scrollTo ,移动其实都是在一瞬间完成的,只是因为我们的触摸动作不断触发,View 不断改变位置,造成了一个过度动画的假象。如果使用一个按钮,比如点击按钮就会把 View 往右移动 100 像素,那么你会发现此时的 View 是瞬间变换位置,并不是慢慢移动到指定位置。这时候我们就需要 Scroller , Scroller 的内部其实也是用 scrollTo 方法来实现的,但是它可以根据需要移动的总距离,以及设置的移动时间,计算出每一次需要移动的距离,然后不断的进行移动,这样就实现了一个动画的效果

流程图2.jpg

以下的流程图是从手指抬起后开始。

下面我们使用 Scroller 来实现松开手指后 View 回到原来的位置的效果

弹弹弹

我们来看看以下代码,基本所有的代码的注释我都已经写上,结合代码理解应该就能明白:

public class TestView1 extends View {  //定义两个变量用于存储按下view时所处的坐标  int lastX = 0;  int lastY = 0;  //滑动~  Scroller scroller;  public TestView1(Context context, AttributeSet attrs) {    super(context, attrs);    scroller = new Scroller(context);  }  @Override public boolean onTouchEvent(MotionEvent event) {    //检测到触摸事件后 第一时间得到相对于父控件的触摸点坐标 并赋值给x,y    int x = (int) event.getX();    int y = (int) event.getY();    switch (event.getAction()) {      //触摸事件中绕不开的第一步,必然执行,将按下时的触摸点坐标赋值给 lastX 和 last Y      case MotionEvent.ACTION_DOWN:        lastX = x;        lastY = y;        break;      //触摸事件的第二步,这时候的x,y已经随着滑动操作产生了变化,用变化后的坐标减去首次触摸时的坐标得到 相对的偏移量      case MotionEvent.ACTION_MOVE:        int offsetX = x - lastX;        int offsetY = y - lastY;        ((View) getParent()).scrollBy(-offsetX, -offsetY);        break;      //触摸事件的第三步,必然执行,手指抬起时候触发,这里会将移动过的view还原到原来的位置,并且有过度效果不是突然移动      case MotionEvent.ACTION_UP:        //因为下面要使用父视图的引用来得到偏移量 所以要获得一个父视图引用        View viewGroup = (View) getParent();        //调用 startScroll 方法,参数为 起始X坐标,起始Y坐标,目的X坐标,目的Y坐标,过度动画持续时间        //这里使用了 viewGroup.getScrollX() 和 viewGroup.getScrollY() 作为起始坐标,ScrollY 和 ScrollX 记录了使用 scrollBy 进行偏移的量        //所以使用他们就等于是使用了现在的坐标作为起始坐标,目的坐标为他们的负数,就是偏移量为0的位置,也是view在没有移动之前的位置        scroller.startScroll(viewGroup.getScrollX(),         viewGroup.getScrollY(),        -viewGroup.getScrollX(),         -viewGroup.getScrollY(),         800);        //刷新view,这里很重要,如果不执行,下面的 computeScroll 方法就不会执行 computeScroll 方法是由 onDraw 方法调用的,而刷新 View 会调用 onDraw。        invalidate();        break;    }    return true;  }  @Override public void computeScroll() {    //在上面尝试刷新视图之后被调用,并且执行了 computeScrollOffset 方法,    //此方法根据上面传进来的起始坐标和目的坐标还有动画时间,进行计算每次移动的偏移量    //如果到达目的坐标 false ,如果不为零 说明没有到达目的坐标    if (scroller.computeScrollOffset()) {      //使用 scrollTo 方法进行移动,参数是从 scroller 的 getCurrX 以及 getCurrY 方法得到的,      // 这两个参数每次在执行 computeScrollOffset 之后都会改变,会越来越接近目的坐标。      ((View) getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY());          // 再次刷新 view 也等于是在循环执行此方法 直到 computeScrollOffset 判断到达目的坐标为止,      // 循环次数和每次移动的坐标距离相关,每次移动的坐标距离又跟目的坐标的距离和动画时长有关      //通常距离越长,动画时间越长,循环次数越多      invalidate();    }  }}

更多相关文章

  1. android 2.0发布
  2. AChartEngine中大饼图
  3. 多点触摸测试
  4. Android手指绘图(一)
  5. 一个旋转layout布局文件
  6. Android自定义View--时钟
  7. 百度map api for Android~搜索服务
  8. Android(安卓)判断触摸点是否在某个view的区域,解决子view与paren
  9. 【Android】OpenGL_ES基本用法

随机推荐

  1. eclipse中安装android ADT插件及无法下载
  2. Android(安卓)经典小技巧总结
  3. Android获取当前网络状态和获取当前设备
  4. 【Android】Android6.0发送短信Demo
  5. android判断当前网络状态,eth wifi pppoe
  6. GMS Android(安卓)Q移除launcher3 google
  7. Android——4.2.2 源码目录结构分析
  8. MMS PDU
  9. Android自定义样式style.xml
  10. mac 下启动Android(安卓)Studio 时出现 A