Android View 的滑动方式

前言

自定义View作为Android进阶的基础,是我们开发者不得不学习的知识,而酷炫的自定义View效果,都离不开View的滑动,所以接下来我们来一起探究View的滑动方式,看看View是如何滑动的,为Android进阶的道路打下基础。

  • View 坐标系基本知识

    • 了解View的滑动方式,首先我们得了解View在什么位置,我们可以把手机屏幕区域看成是像数学中坐标系一样的区域,只不过是手机屏幕坐标系的Y轴和数学中的坐标系的Y轴正方向相反

    • 确定View的位置主要是根据View的left、top、right、bottom四个属性来决定,需要注意的是View的这四个属性是相对于它的父容器来说的,所以对应为left是View的左上角相对于父容器的横坐标,top为纵坐标,right为View右下角相对于父容器的横坐标,bottom为纵坐标。(具体可以看下方示意图A)

      //获取view位置的值
      left = View.getLeft();
      top = View.getTop();
      right = View.getRight();
      bottom = View.getBottom();
      
    • 除了上面确定View位置的参数,还有x,y,translationX,translationY这四个参数,x和y代表View的左上角的坐标值,而translationX,translationY是左上角坐标相对于View的父容器的偏移量,默认为零,也就是view不移动,则x和y等于left和top,他们换算关系可看下面示意图A,在View的滑动过程中,left和top表示的是View原始位置的值,这是不会改变的,所以改变的是滑动偏移量加上原始值得到新的左上角坐标。


      View坐标系和点击事件示意图.png
  • 当我们触摸屏幕,则可以通过点击事件来获取当前点击位置的值和相较于手机屏幕左上角的偏移量坐标(如上图B所示)。

  • Android View 的滑动方式

    • layout方法改变View位置滑动View

      • 首先我们看看layout()方法源码

        @SuppressWarnings({"unchecked"})
        public void layout(int l, int t, int r, int b) {
              .......
          if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
              onLayout(changed, l, t, r, b);
              
              ........
              }
          }
        
      • 了解过自定义View的各位应该都知道,onLayout()是View绘制过程中的一个方法,可以通过它确定View的位置,也就是说我们通过layout()方法可以改变View的位置,下面我们通过onLayout方法做一个可以随意滑动 view的例子

         @Override
         public boolean onTouchEvent(MotionEvent event) {
           //获取触屏时候的坐标
          Log.e("毛麒添","getLeft:"+getLeft()+"getTop:"+getTop()+"getRight:"+getRight()+"getBottom:"+getBottom());
           x = event.getRawX();
           y = event.getRawY();
          switch (event.getAction()){
              case MotionEvent.ACTION_DOWN:
                  break;
              case MotionEvent.ACTION_MOVE:
                  //手指移动偏移量
                  int offsetX = (int) (x-lastX);
                  int offsetY = (int) (y-lastY);
                  layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
                  break;
              case MotionEvent.ACTION_UP:
                  Log.e("毛麒添","getLeft:"+getLeft()+"getTop:"+getTop()+"getRight:"+getRight()+"getBottom:"+getBottom());
                  break;
          }
          lastX=x;
          lastY=y;
          return super.onTouchEvent(event);
         }
        
        • 通过打印left,top,right,bottom数值可以发现layout方法是真实改变了View的位置而不只是View的内容。
layout1.gif
  • offsetLeftAndRight()与offsetTopAndBottom() 方法改变View的位置让其滑动

    • 修改上面的方法,效果图和onLayout一样,同时offsetLeftAndRight()与offsetTopAndBottom()方法也是真实改变了View的位置而不只是View的内容。
     case MotionEvent.ACTION_MOVE:
                //手指移动偏移量
                int offsetX = (int) (x-lastX);
                int offsetY = (int) (y-lastY);
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);
                break;
    
  • 使用scrollTo()和scrollBy()滑动View

    • scrollTo()和scrollBy()是View提供的滑动方法,scrollTo()移动到某个某个点,scrollBy()表示根据传入的偏移量进行移动。先看源码实现
    /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
     public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
     }
    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }
    
    
    • 通过源码我们可以看到scrollBy()的实现其实是调用了scrollTo()方法。这里有个mScrollX和mScrollY的规则我们需要明白:scrollTo()中mScrollX的值等于view左边缘和view内容左边缘在水平方向的距离,并且当view的左边缘在view的内容左边缘右边时,mScrollX为正,反之为负;同理mScrollY等于view上边缘和view内容上边缘在竖直方向的距离,并且当view的上边缘在view的内容上边缘下边时,mScrollY为正,反之为负。当View没有使用scrollTo()和scrollBy()进行滑动的时候,mScrollX和mScrollY默认等于零,也就是view的左边缘与内容左边缘重合。
    • 根据上面的规则,我们假设将view内容右下滑动,得到下图
mScrollX和mScrollY值的判断.png
- 结合上面的知识,我们将上面滑动的例子改写一下,如果使用scrollTo()则只是滑动到我们手指滑动偏移量的距离的点,达不到要求,而scrollBy()是在scrollTo()的基础上偏移滑动的位置,正好符合我们自由滑动的要求,并且根据上面的分析mScrollX和mScrollY为负值,则滑动偏移也应该为负值才能达到我们想要的自由滑动效果(这个大家需要自己好好想明白可能才会更加清楚理解)  
 ```
   case MotionEvent.ACTION_MOVE:
            //手指移动偏移量
            int offsetX = (int) (x-lastX);
            int offsetY = (int) (y-lastY);
            //滑动方式1
            ((View)getParent()).scrollBy(-offsetX,-offsetY);
            break;
 ```  
  • 根据滑动打印的日志我们可以看出,scrollBy()和scrollTo()在滑动的过程中只是改变了View内容的位置,而没有改变初始的left,right,top,bottom的值
view的位置没有发生改变.png
  • 使用动画让View滑动

    • xml补间动画的方式让View滑动

      • 定义一个xml文件,500ms移动到500,500的位置并保持位置
      <?xml version="1.0" encoding="utf-8"?>
      <set xmlns:android="http://schemas.android.com/apk/res/android"
      android:fillAfter="true"
      android:duration="500"
       >
      <translate android:fromXDelta="0"
             android:fromYDelta="0"
             android:toXDelta="500"
             android:toYDelta="500"
      />
      </set>
      
      • 代码调用
      startAnimation(AnimationUtils.loadAnimation(mContext, R.anim.testscroll));
      
      • 补间对View的滑动也只是改变了View的显示效果,不会对View的属性做真正的改变,也就是说补间动画也没有真正改变View的位置
    • 属性动画让View滑动

      • 自从Android3.0开始加入了属性动画(了解属性动画可以查看郭霖大佬博客),属性动画不仅能作用于View产生动画效果,也能作用于其他属性来产生动画效果,可以说属性动画相较于补间动画是非常灵活的,并且属性动画是真正改变View的位置属性。
      • 属性动画一般我们使用ObjectAnimator,让View2秒时间水平平移到300位置,并且移动完后我们点击View看还能响应点击事件(如下图所示)
      ObjectAnimator.ofFloat(testScroll,"translationX",0,300).setDuration(2000).start();
      
      
objectAnimator.gif
- 改变布局参数 LayoutParams 滑动View

  - 平常我们开发设置View的位置可以在xml中设定,也可以在代码中设置。LayoutParams有一个View的所有布局参数信息,所有我们可以通过设置View的LayoutParams参数的leftMargin和topMargin达到上面自由滑动View的效果。
  
   ```
   ......
   case MotionEvent.ACTION_MOVE:
            //手指移动偏移量
            int offsetX = (int) (x-lastX);
            int offsetY = (int) (y-lastY);
            //滑动方式5
            moveView(offsetX,offsetY);
            break;
    
   ......
   
   private void moveView(int offsetX, int offsetY) {
    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
    layoutParams.leftMargin = getLeft() + offsetX;
    layoutParams.topMargin = getTop() + offsetY;
    setLayoutParams(layoutParams);
    }
   ```
  • 既然布局参数已经改变,则View的实际位置肯定也已经改变。

  • Scroller弹性滑动

    • 首先我们要明白什么是Scroller?

      • Scroller是弹性滑动的帮助类,它本身并不能实现View的弹性滑动,它必须要配合scrollTo()或scrollBy()和实现View的computeScroll的方法才能实现View的弹性滑动
      • Scroller实现弹性滑动的典型例子
        Scroller mScroller=new Scroller(context);
        
        public void smoothScrollTo(int desx,int desy){
        int scaleX = (int) getScaleX();
        int scaleY = (int) getScaleY();
        int deltaX = desx-scaleX;
        int deltaY = desy-scaleY;
        //3秒内弹性滑到desx desy 位置
        mScroller.startScroll(scaleX,scaleY,deltaX,deltaY,3000);
        //重新绘制界面 会调用computeScroll方法
        invalidate();
        }
        
        @Override
        public void computeScroll() {
        super.computeScroll();
        if(mScroller.computeScrollOffset()){//还没滑动到指定位置
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
         }
        }
        
        //拿到自定View的实例对象调用smoothScrollTo实现右下方向3秒
        //内到指定位置的弹性滑动
        //为什么是-300 请看scrollTo()或scrollBy()滑动解析
        testScroll.smoothScrollTo(-300,-300);
        
    • 下面我们从源码角度来分析一下Scroller是如何实现弹性滑动的

      • 先看startScroll()方法
      /**
       * Start scrolling by providing a starting point, the distance to travel,
       * and the duration of the scroll.
       * 
       * @param startX Starting horizontal scroll offset in pixels. Positive
       *        numbers will scroll the content to the left.
       * @param startY Starting vertical scroll offset in pixels. Positive numbers
       *        will scroll the content up.
       * @param dx Horizontal distance to travel. Positive numbers will scroll the
       *        content to the left.
       * @param dy Vertical distance to travel. Positive numbers will scroll the
       *        content up.
       * @param duration Duration of the scroll in milliseconds.
       */
       public void startScroll(int startX, int startY, int dx, int dy, int duration) {
       mMode = SCROLL_MODE;
       mFinished = false;
       mDuration = duration;
       mStartTime = AnimationUtils.currentAnimationTimeMillis();
       mStartX = startX;
       mStartY = startY;
       mFinalX = startX + dx;
       mFinalY = startY + dy;
       mDeltaX = dx;
       mDeltaY = dy;
       mDurationReciprocal = 1.0f / (float) mDuration;
       }
       
       //View 中 computeScroll()方法没有实现内容,需要子View 自行实现
         /**
          * Called by a parent to request that a child update its values for mScrollX
          * and mScrollY if necessary. This will typically be done if the child is
          * animating a scroll using a {@link android.widget.Scroller Scroller}
          * object.
          */
          public void computeScroll() {
          }
      
      • 通过源码我们看到startScroll()方法只是传递了我们传入的参数,滑动的起点startX、startY,滑动的距离dx、dy,和弹性滑动的时间,没看到有滑动的操作,那Scroller是如何让View滑动呢?而答案就是我们再调用startScroll()方法之后又调用了invalidate()方法,该方法会引起view的重绘,而View的重绘会调用computeScroll()方法,通过上面的源码,我们知道computeScroll()方法在view中是空实现,所以我们自己实现该放法的时候则调用scrollTo方法获取scrollX和scrollY当前让view进行滑动,但是这只是滑动一段距离,好像还没有弹性滑动,别急,我们看看Scroller的computeScrollOffset()方法
       /**
        * Call this when you want to know the new location.  If it returns true,
        * the animation is not yet finished.
        */ 
        public boolean computeScrollOffset() {
         if (mFinished) {
           return false;
          }
         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
      
         if (timePassed < mDuration) {
           switch (mMode) {
           case SCROLL_MODE:
               final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
               mCurrX = mStartX + Math.round(x * mDeltaX);
               mCurrY = mStartY + Math.round(x * mDeltaY);
               break;
               .......
             }
         }
         else {
           .......
         }
        return true;
       }
      
      • 通过computeScrollOffset()的源码,我们已经可以一目了然,根据时间流逝的百分比算出scrollX和scrollY改变的百分比并计算出他们的值,类似动画中的插值器的概念。每次重绘缓慢滑动一段距离,在一段时间内缓慢滑动就成了弹性滑动,就比scrollTo方法的一下滑动完舒服多了,我们还需要注意computeScrollOffset()的返回值,如果返回false表示滑动完了,true则表示没有滑动完。
      • 这里我们梳理一下Scroller实现弹性滑动的工作原理:Scroller必须要配合scrollTo()或scrollBy()和实现View的computeScroll的方法才能实现View的弹性滑动,invalidate()引发第一次重绘,重绘距离滑动开始时间有一个时间间隔,在这个时间间隔中获取View滑动的位置,通过scrollTo()进行滑动,滑动完postInvalidate()再次进行重绘,没有滑动完则继续上面的操作,最终组成弹性滑动。

到此,View的滑动方式就已经了解完了。如果文章中有写得不对的地方,请给我留言指出,大家一起学习进步。如果觉得我的文章给予你帮助,也请给我一个喜欢和关注。

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

推荐阅读更多精彩内容

  • 预备知识 Android屏幕区域划分我们先看一副图来了解一下Android屏幕的区域划分,如下:Android屏幕...
    小芸论阅读 8,947评论 6 44
  • 开发中,为了增加更多炫丽的效果,我们经常在应用中添加滑动效果,今天就来分析一下 View 中滑动效果的实现原理以及...
    任教主来也阅读 2,966评论 0 14
  • 什么是View View 是 Android 中所有控件的基类。 View的位置参数 View 的位置由它的四个顶...
    acc8226阅读 1,156评论 0 7
  • 如果你是正确的,不要太多争辩,做人留一线,日后好相见。 如果你是出色的,不要到处显摆,别人会在你的得意忘形中远离。...
    双面人阿清阅读 153评论 0 1
  • 《窗边的小豆豆》是日本女作家黑柳彻子的作品。小豆豆是一个被一所小学退了学的怪孩子。她的妈妈为了不让她在心里留下自...
    闪亮的星zx阅读 327评论 0 1