• 实现滑动的7种方式详解


    Android开发中,滑动对一个app来说,是非常重要的,流畅的滑动操作,能够给用户带来用好的体验,那么本次就来讲讲android中实现滑动有哪些方式。其实滑动一个View,本质上是移动一个View,改变其当前所属的位置,要实现View的滑动,就必须监听用户触摸的事件,且获取事件传入的坐标值,从而动画的改变位置而实现滑动。

    android坐标系

    首先要知道android的坐标系与我们平常学习的坐标系是不一样的,在android中是将左上方作为坐标原点,向右为x抽正方向,向下为y抽正方向,像在触摸事件中,getRawX(),getRawY()获取到的就是Android坐标中的坐标.

    视图坐标系

    android开发中除了上面的这种坐标以外,还有一种坐标,叫视图坐标系,他的原点不在是屏幕左上方,而是以父布局坐上角为坐标原点,像在触摸事件中,getX(),getY()获取到的就是视图坐标中的坐标.

    触摸事件–MotionEvent

    触摸事件MotionEvent在用户交互中,有非常重要的作用,因此必须要掌握他,我们先来看看Motievent中封装的一些常用的触摸事件常亮:

     //单点触摸按下动作
     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()方法来获取触摸事件的类型,其代码模式如下:

     @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        //获取当前输入点的坐标,(视图坐标)
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //处理输入按下事件
                break;
            case MotionEvent.ACTION_MOVE:
                //处理输入的移动事件
                break;
            case MotionEvent.ACTION_UP:
                //处理输入的离开事件
                break;
        }
        return true; //注意,这里必须返回true,否则只能响应按下事件
    }    
    

    以上只是一个空壳的架构,遇到的具体的场景,也有可能会新增多其他事件,或是用不到这么多事件等等,要根据实际情况来处理。在介绍如何实现滑动之前先来看看android中给我们提供了那些常用的获取坐标值,相对距离等的方法,主要是有以下两个类别:

    • View 提供的获取坐标方法

      getTop(): 获取到的是View自身的顶边到其父布局顶边的距离

      getBottom(): 获取到的是View自身的底边到其父布局顶边的距离

      getLeft(): 获取到的是View自身的左边到其父布局左边的距离

      getRight(): 获取到的是View自身的右边到其父布局左边的距离

    • MotionEvent提供的方法

      getX(): 获取点击事件距离控件左边的距离,即视图坐标

      getY(): 获取点击事件距离控件顶边的距离,即视图坐标

      getRawX(): 获取点击事件距离整个屏幕左边的距离,即绝对坐标

      getRawY(): 获取点击事件距离整个屏幕顶边的距离,即绝对坐标

    介绍上面一些基本的知识点后,下面我们就来进入正题了,android中实现滑动的其中方法:

    实现滑动的7种方法

    其实不管是哪种滑动,他们的基本思路是不变的,都是:当触摸View时,系统记下当前的触摸坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获得相对前一个点的偏移量,通过偏移量来修改View的坐标,并不断的更新,重复此动作,即可实现滑动的过程。 
    首先我们先来定义一个View,并置于LinearLayout中,我们的目的是要实现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.liaojh.scrolldemo.DragView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#88ffffff"/>
    
    </LinearLayout>     
    

    layout方法

    我们知道,在进行View绘制时,会调用layout()方法来设置View的显示位置,而layout方法是通过left,top,right,bottom这四个参数来确定View的位置的,所以我们可以通过修改这四个参数的值,从而修改View的位置。首先我们在onTouchEvent方法中获取触摸点的坐标:

    float x = event.getX();
    float y = event.getY();
    

    接着在ACTION_DOWN的时候记下触摸点的坐标值:

    case MotionEvent.ACTION_DOWN:
                //记录按下触摸点的位置
                mLastX = x;
                mLastY = y;
                break;
    

    最后在ACTION_MOVE的时候计算出偏移量,且将偏移量作用到layout方法中:

    case MotionEvent.ACTION_MOVE:
                //计算偏移量(此次坐标值-上次触摸点坐标值)
                int offSetX = (int) (x - mLastX);
                int offSetY = (int) (y - mLastY);
    
                //在当前left,right,top.bottom的基础上加上偏移量
                layout(getLeft() + offSetX,
                        getTop() + offSetY,
                        getRight() + offSetX,
                        getBottom() + offSetY
                );
                break;     
    

    这样每次在手指移动的时候,都会调用layout方法重新更新布局,从而达到移动的效果,完整代码如下:

    package com.liaojh.scrolldemo;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    
    /**
     * @author LiaoJH
     * @DATE 15/11/7
     * @VERSION 1.0
     * @DESC TODO
     */
    public class DragView extends View
    {
        private float mLastX;
        private float mLastY;
    
         public DragView(Context context)
        {
            this(context, null);
        }
    
    public DragView(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }
    
    public DragView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        //获取当前输入点的坐标,(视图坐标)
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                //记录按下触摸点的位置
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量(此次坐标值-上次触摸点坐标值)
                int offSetX = (int) (x - mLastX);
                int offSetY = (int) (y - mLastY);
    
                //在当前left,right,top.bottom的基础上加上偏移量
                layout(getLeft() + offSetX,
                        getTop() + offSetY,
                        getRight() + offSetX,
                        getBottom() + offSetY
                );
    
                break;
        }
        return true;
    }
    }     
    

    当然也可以使用getRawX(),getRawY()来获取绝对坐标,然后使用绝对坐标来更新View的位置,但要注意,在每次执行完ACTION_MOVE的逻辑之后,一定要重新设置初始坐标,这样才能准确获取偏移量,否则每次的偏移量都会加上View的父控件到屏幕顶边的距离,从而不是真正的偏移量了。

       @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        //获取当前输入点的坐标,(绝对坐标)
        float rawX = event.getRawX();
        float rawY = event.getRawY();
        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                //记录按下触摸点的位置
                mLastX = rawX;
                mLastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量(此次坐标值-上次触摸点坐标值)
                int offSetX = (int) (rawX - mLastX);
                int offSetY = (int) (rawY - mLastY);
    
                //在当前left,right,top.bottom的基础上加上偏移量
                layout(getLeft() + offSetX,
                        getTop() + offSetY,
                        getRight() + offSetX,
                        getBottom() + offSetY
                );
    
                //重新设置初始位置的值
                mLastX = rawX;
                mLastY = rawY;
                break;
        }
        return true;
    }
    

    offsetLeftAndRight()与offsetTopAndBottom()

    这个方法相当于系统提供了一个对左右,上下移动的API的封装,在计算出偏移量之后,只需使用如下代码设置即可:

     offsetLeftAndRight(offSetX);
     offsetTopAndBottom(offSetY);
    

    偏移量的计算与上面一致,只是换了layout方法而已。

    LayoutParams

    LayoutParams保存了一个View的布局参数,因此可以在程序中通过动态的改变布局的位置参数,也可以达到滑动的效果,代码如下:

     LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
     lp.leftMargin = getLeft() + offSetX;
     lp.topMargin = getTop() + offSetY;
     setLayoutParams(lp);
    

    使用此方式时需要特别注意:通过getLayoutParams()获取LayoutParams时,需要根据View所在的父布局的类型来设置不同的类型,比如这里,View所在的父布局是LinearLayout,所以可以强转成LinearLayout.LayoutParams。

    在通过改变LayoutParams来改变View的位置时,通常改变的是这个View的Margin属性,其实除了LayoutParams之外,我们有时候还可以使用ViewGroup.MarginLayoutParams来改变View的位置,代码如下:

    ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
    lp.leftMargin = getLeft() + offSetX;
    lp.topMargin = getTop() + offSetY;
    setLayoutParams(lp);
    //使用这种方式的好处就是不用考虑父布局类型
    

    scrollTo与scrollBy

    在一个View中,系统提供了scrollTo与scrollBy两种方式来改变一个View的位置,其中scrollTo(x,y)表示移动到一个具体的坐标点(x,y),而scrollBy(x,y)表示移动的增量。与前面几种计算偏移量相同,使用scrollBy来移动View,代码如下:

     scrollBy(offSetX,offSetY);
    

    然后我们拖动View,发现View并没有移动,这是为杂呢?其实,方法没有错,view也的确移动了,只是他移动的不是我们想要的东西。scrollTo,scrollBy方法移动的是view的content,即让view的内容移动,如果是在ViewGroup中使用scrollTo,scrollBy方法,那么移动的将是所有的子View,而如果在View中使用的话,就是view的内容,所以我们需要改一下我们之前的代码:

    ((View)getParent()).scrollBy(offSetX, offSetY);
    

    这次是可以滑动了,但是我们发现,滑动的效果跟我们想象的不一样,完全相反了,这又是为什么呢?其实这是因为android中对于移动参考系选择的不同从而实现这样的效果,而我们想要实现我们滑动的效果,只需将偏移量设置为负值即可,代码如下:

    ((View) getParent()).scrollBy(-offSetX, -offSetY);
    

    同样的在使用绝对坐标时,使用scrollTo也可以达到这样的效果。

    scroller

    如果让一个View向右移动200的距离,使用上面的方式,大家应该发现了一个问题,就是移动都是瞬间完成的,没有那种慢慢平滑的感觉,所以呢,android就给我们提供了一个类,叫scroller类,使用该类就可以实现像动画一样平滑的效果。

    其实它实现的原理跟前面的scrooTo,scrollBy方法实现view的滑动原理类似,它是将ACTION_MOVE移动的一段位移划分成N段小的偏移量,然后再每一个偏移量里面使用scrollBy方法来实现view的瞬间移动,这样在整体的效果上就实现了平滑的效果,说白了就是利用人眼的视觉暂留特性。

    下面我们就来实现这么一个例子,移动view到某个位置,松开手指,view都吸附到左边位置,一般来说,使用Scroller实现滑动,需经过以下几个步骤:

    • 初始化Scroller

      //初始化Scroller,使用默认的滑动时长与插值器
      mScroller = new Scroller(context);  
      
    • 重写computeScroll()方法

      该方法是Scroller类的核心,系统会在绘制View的时候调用draw()方法中调用该方法,这个方法本质上是使用scrollTo方法,通过Scroller类可以获取到当前的滚动值,这样我们就可以实现平滑一定的效果了,一般模板代码如下:

       @Override
      public void computeScroll()
      {
          super.computeScroll();
          //判断Scroller是否执行完成
          if (mScroller.computeScrollOffset()) {
              ((View)getParent()).scrollTo(
                  mScroller.getCurrX(),
                  mScroller.getCurrY()
              );
              //调用invalidate()computeScroll()方法
              invalidate();
          }
      }
      

    Scroller类提供中的方法:

    computeScrollOffset(): 判断是否完成了真个滑动
    
    getCurrX(): 获取在x抽方向上当前滑动的距离
    
    getCurrY(): 获取在y抽方向上当前滑动的距离
    
    • startScroll开启滑动

      最后在需要使用平滑移动的事件中,使用Scroller类的startScroll()方法来开启滑动过程,startScroller()方法有两个重载的方法:

    – public void startScroll(int startX, int startY, int dx, int dy)

    – public void startScroll(int startX, int startY, int dx, int dy, int duration)

    可以看到他们的区别只是多了duration这个参数,而这个是滑动的时长,如果没有使用默认时长,默认是250毫秒,而其他四个坐标则表示起始坐标与偏移量,可以通过getScrollX(),getScrollY()来获取父视图中content所滑动到的点的距离,不过要注意这个值的正负,它与scrollBy,scrollTo中说的是一样的。经过上面这三步,我们就可以实现Scroller的平滑一定了。

    继续上面的例子,我们可以在onTouchEvent方法中监听ACTION_UP事件动作,调用startScroll方法,其代码如下:

     case MotionEvent.ACTION_UP:
                //第三步
                //当手指离开时,执行滑动过程
                ViewGroup viewGroup = (ViewGroup) getParent();
                mScroller.startScroll(
                        viewGroup.getScrollX(),
                        viewGroup.getScrollY(),
                        -viewGroup.getScrollX(),
                        0,
                        800
                );
                //刷新布局,从而调用computeScroll方法
                invalidate();
                break;
    

    属相动画

    使用属性动画同样可以控制一个View的滑动,下面使用属相动画来实现上边的效果(关于属相动画,请关注其他的博文),代码如下:

     case MotionEvent.ACTION_UP:
                ViewGroup viewGroup = (ViewGroup) getParent();
                //属性动画执行滑动
                ObjectAnimator.ofFloat(this, "translationX", viewGroup.getScrollX()).setDuration(500)
                              .start();
                break;
    

    ViewDragHelper

    一看这个类的名字,我们就知道他是与拖拽有关的,猜的没错,通过这个类我们基本可以实现各种不同的滑动,拖放效果,他是非常强大的一个类,但是它也是最为复杂的,但是不要慌,只要你不断的练习,就可以数量的掌握它的使用技巧。下面我们使用这个类来时实现类似于QQ滑动侧边栏的效果,相信广大朋友们多与这个现象是很熟悉的吧。

    先来看看使用的步骤是如何的:

    • 初始化ViewDragHelper

      ViewDragHelper这个类通常是定义在一个ViewGroup的内部,并通过静态方法进行初始化,代码如下:

      //初始化ViewDragHelper 
      viewDragHelper = ViewDragHelper.create(this,callback);

      它的第一个参数是要监听的View,通常是一个ViewGroup,第二个参数是一个Callback回调,它是整个ViewDragHelper的逻辑核心,后面进行具体介绍。

    • 拦截事件

      重写拦截事件onInterceptTouchEvent与onTouchEvent方法,将事件传递交给ViewDragHelper进行处理,代码如下:

      @Override
      public boolean onInterceptTouchEvent(MotionEvent ev)
      {
          //2. 将事件交给ViewDragHelper
          return  viewDragHelper.shouldInterceptTouchEvent(ev);
      }
      
      @Override
      public boolean onTouchEvent(MotionEvent event)
      {
          //2. 将触摸事件传递给ViewDragHelper,不可少
          viewDragHelper.processTouchEvent(event);
          return true;
      }
      
    • 处理computeScroll()方法

      前面我们在使用Scroller类的时候,重写过该方法,在这里我们也需要重写该方法,因为ViewDragHelper内部也是使用Scroller类来实现的,代码如下:

      //3. 重写computeScroll
      @Override
      public void computeScroll()
      {
          //持续平滑动画 (高频率调用)
          if (viewDragHelper.continueSettling(true))
              //  如果返回true, 动画还需要继续执行
              ViewCompat.postInvalidateOnAnimation(this);
      }
      
    • 处理回调Callback

      通过如下代码创建一个Callback:

           private ViewDragHelper.Callback callback = new ViewDragHelper.Callback()
      {
          @Override
          //此方法中可以指定在创建ViewDragHelper时,参数ViewParent中的那些子View可以被移动
          //根据返回结果决定当前child是否可以拖拽
          //  child 当前被拖拽的View
          //  pointerId 区分多点触摸的id
          public boolean tryCaptureView(View child, int pointerId)
          {
              //如果当前触摸的view是mMainView时开始检测
              return mMainView == child;
          }
      
          @Override
          //水平方向的滑动
          // 根据建议值 修正将要移动到的(横向)位置   (重要)
          // 此时没有发生真正的移动
          public int clampViewPositionHorizontal(View child, int left, int dx)
          {
              //返回要滑动的距离,默认返回0,既不滑动
              //参数参考clampViewPositionVertical
              f (child == mMainView)
              {
                  if (left > 300)
                  {
                      left = 300;
                  }
                  if (left < 0)
                  {
                      left = 0;
                  }
               }
              return left;
          }
      
          @Override
          //垂直方向的滑动
          // 根据建议值 修正将要移动到的(纵向)位置   (重要)
          // 此时没有发生真正的移动
          public int clampViewPositionVertical(View child, int top, int dy)
          {
              //top : 垂直向上child滑动的距离,
              //dy: 表示比较前一次的增量,通常只需返回top即可,如果需要精确计算padding等属性的话,就需要对left进行处理
              return super.clampViewPositionVertical(child, top, dy); //0
          }
      };
      

      到这里就可以拖拽mMainView移动了。

    下面我们继续来优化这个代码,还记得之前我们使用Scroller时,当手指离开屏幕后,子view会吸附到左边位置,当时我们监听ACTION_UP,然后调用startScroll来实现的,这里我们使用ViewDragHelper来实现。

    在ViewDragHelper.Callback中,系统提供了这么一个方法—onViewReleased(),我们可以通过重写这个方法,来实现之前的操作,当然这个方法内部也是通过Scroller来实现的,这也是为什么我们要重写computeScroll方法的原因,实现代码如下:

        @Override
        //拖动结束时调用
        public void onViewReleased(View releasedChild, float xvel, float yvel)
        {
            if (mMainView.getLeft() < 150)
            {
                // 触发一个平滑动画,关闭菜单,相当于Scroll的startScroll方法
                if (viewDragHelper.smoothSlideViewTo(mMainView, 0, 0))
                {
                    // 返回true代表还没有移动到指定位置, 需要刷新界面.
                    // 参数传this(child所在的ViewGroup)
                    ViewCompat.postInvalidateOnAnimation(DragLayout.this);
                }
            }
            else
            {
                //打开菜单
                if (viewDragHelper.smoothSlideViewTo(mMainView, 300, 0)) ;
                {
                    ViewCompat.postInvalidateOnAnimation(DragLayout.this);
                }
            }
            super.onViewReleased(releasedChild, xvel, yvel);
        }
    

    当滑动的距离小于150时,mMainView回到原来的位置,当大于150时,滑动到300的位置,相当于打开了mMenuView,而且滑动的时候是很平滑的。此外还有一些方法:

        @Override
        public void onViewCaptured(View capturedChild, int activePointerId)
        {
            // 当capturedChild被捕获时,调用.
            super.onViewCaptured(capturedChild, activePointerId);
        }
    
        @Override
        public int getViewHorizontalDragRange(View child)
        {
            // 返回拖拽的范围, 不对拖拽进行真正的限制. 仅仅决定了动画执行速度
            return 300;
        }
    
        @Override
        //当View位置改变的时候, 处理要做的事情 (更新状态, 伴随动画, 重绘界面)
        // 此时,View已经发生了位置的改变
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
        {
            // changedView 改变位置的View
            // left 新的左边值
            // dx 水平方向变化量
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }
    

    说明:里面还有很多关于处理各种事件方法的定义,如:

    onViewCaptured():用户触摸到view后回调

    onViewDragStateChanged(state):这个事件在拖拽状态改变时回调,比如:idle,dragging等状态

    onViewPositionChanged():这个是在位置改变的时候回调,常用于滑动时伴随动画的实现效果等

    对于里面的方法,如果不知道什么意思,则可以打印log,看看参数的意思。

    总结

    这里介绍的就是android实现滑动的七种方法,至于使用哪一种好,就要结合具体的项目需求场景了,毕竟硬生生的实现这个效果,而不管用户的使用体验式不切实际的,这里面个人觉得比较重要的是Scroller类的使用。属性动画以及ViewDragHelper类,特别是最后一个,也是最难最复杂的,但也是甩的最多的。

    终于写完了,好累的赶脚~~~

  • 相关阅读:
    JVM学习-垃圾回收算法
    JVM学习-jvm判断对象已死的方法
    JVM学习-jvm内存区域
    python 多线程
    Python+unittest+requests+excel实现接口自动化测试框架
    linux 运行tensorflow文件缺少_bz2问题及解决
    获取url地址
    微信小程序的小问题(2)
    微信小程序的小问题(1)
    前端知识
  • 原文地址:https://www.cnblogs.com/vegetate/p/9997237.html
Copyright © 2020-2023  润新知