[Android] 滑动操作的原理及处理

滑动效果的产生


滑动一个 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 进行偏移,从而移动view
offsetLeftAndRight(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();
    }
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 导语 滑动算是Android比较常用的效果了,滑动的操作具有很好的用户体验性。 主要内容 滑动效果是如何产生的 实...
    一个有故事的程序员阅读 6,432评论 3 11
  • 内容是博主照着书敲出来的,博主码字挺辛苦的,转载请注明出处,后序内容陆续会码出。 当了解了Android坐标系和触...
    Blankj阅读 6,626评论 3 61
  • 开发中,为了增加更多炫丽的效果,我们经常在应用中添加滑动效果,今天就来分析一下 View 中滑动效果的实现原理以及...
    任教主来也阅读 2,943评论 0 14
  • 什么是View View 是 Android 中所有控件的基类。 View的位置参数 View 的位置由它的四个顶...
    acc8226阅读 1,149评论 0 7
  • 锻炼,养花 经济,科研 -------- 钢笔,太极 英语,看书 音乐,禅 难过的时候打打太极,想想你音乐和钢笔字...
    XTJ阅读 129评论 0 0