欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

View的事件体系一(触摸、手势、滑动)

程序员文章站 2024-03-24 15:05:04
...

一. View基础触摸、手势、滑动事件

View是所有界面层的空间的一种抽象。
Andorid屏幕坐标系原点在左上角, 而View的left,right,top,bottom属性,都是相对于其父View的坐标。


1. MotionEvent 和 TouchSlop

  1. MotionEvent

    手指触摸屏幕后,典型的事件类型:

    • ACTION_DOWN:手指刚接触屏幕;
    • ACTION_MOVE:手指在屏幕上移动;
    • Aciton_UP:手指在屏幕上松开的瞬间

    一般来说,正常触摸屏幕的行为,会触发一系列事件,比如

    • 点击屏幕后立即松开,事件序列: DOWN -> UP
    • 点击屏幕,滑动一会再松开,事件序列:DOWN -> MOVE -> …. -> MOVE -> UP

    MotionEvent提供两组方法,分别用于获取相对于当前View的坐标,和相对于屏幕原点的坐标:

    • getX() / getY() :获取相对于当前View左上角的 x 和 y 坐标。
    • getRawX() / getRawY:获取相对于屏幕原点的 x 和 y 坐标。
  2. TouchSlop

    • TouchSlop是系统所能识别出的被认为是滑动的最小距离,通过如下代码获取:
      ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();

    • 在处理滑动时,可以以此来过滤掉距离太小的滑动,提升用户体验。

2. VelocityTracker、GestureDetector 和 Scroller

  1. VelocityTracker 速度追踪器,可以追踪当前事件的速度:

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (velocityTracker1 == null) {
            velocityTracker1 = VelocityTracker.obtain();
        }
        velocityTracker1.addMovement(event);
        //参数单位:毫秒
        velocityTracker1.computeCurrentVelocity(1000);
        int xVelocity = (int) velocityTracker1.getXVelocity();
        int yVelocity = (int) velocityTracker1.getYVelocity();
        Log.d(TAG, "velocity:" + xVelocity + ", " + yVelocity);
        return true;
    }

    在合适的时候,需要回收它:

    protected void onDestroy() {
        velocityTracker.clear();
        velocityTracker.recycle();
        super.onDestroy();
    }
  2. GestureDetector

    • 创建GestureDetector对象,并实现接口.

      GestureDetector gestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() {
          @Override
          public boolean onDown(MotionEvent e) {
              Log.d(TAG, "由ACTION_DOWN 触发 "+e.getAction());
              return false;
          }
      
          @Override
          public void onShowPress(MotionEvent e) {
              Log.d(TAG, "由ACTION_DOWN 触发, 触摸屏幕后,尚未松开或者拖动 "+ e.getAction());
          }
      
          @Override
          public boolean onSingleTapUp(MotionEvent e) {
              Log.d(TAG, "触摸屏幕后松开,单击行为 "+ e.getAction());
              return false;
          }
      
          @Override
          public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
              Log.d(TAG, "触摸屏幕并拖动 "+ e1.getAction()+"  ,  "+e2.getAction());
              return false;
          }
      
          @Override
          public void onLongPress(MotionEvent e) {
              Log.d(TAG, "长按行为 "+ e.getAction());
          }
      
          @Override
          public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
              Log.d(TAG, "快速滑动行为 "+e1.getAction()+"  ,  "+e2.getAction());
              return false;
          }
      });

      然后接管onTouchListener的返回:

      View.OnTouchListener touchListener = new View.OnTouchListener() {
          @Override
          public boolean onTouch(View v, MotionEvent event) {
              // ...
              gestureDetector.setIsLongpressEnabled(false);
              // return true;
              return gestureDetector.onTouchEvent(event);
          }
      };

      这样就可以监控手势行为了。

    • 双击回调接口:OnDoubleTapListener

      GestureDetector.OnDoubleTapListener doubleTapListener = new GestureDetector.OnDoubleTapListener() {
          @Override
          public boolean onSingleTapConfirmed(MotionEvent e) {
              //严格单击行为。不可能是双击中的一次单击。
              return false;
          }
      
          @Override
          public boolean onDoubleTap(MotionEvent e) {
              //双击,由2次连续的单击组成。它不可能和onSingleTapConfirmed共存。
              return false;
          }
      
          @Override
          public boolean onDoubleTapEvent(MotionEvent e) {
              //表示发生了双击行为。
              // 在双击的期间,ACTION_DOWN, ACTION_MOVE 和 ACTION_UP 都会触发此回调
              return false;
              }
          };

      你可以为GestureDetector对象注入OnDoubleTapListener:

      gestureDetector.setOnDoubleTapListener(doubleTapListener);
  3. Scroller

    • 弹性滑动对象,使View的滑动有过渡效果。如果我们使用View的 scrollTo / scrollBy ,滑动过程是瞬间完成的,用户体验较差。
    • 使用方法:

      在自定义View或ViewGroup类中:

      Scroller scroller ;
      public void smoothScrollBy(int dx, int dy) {
          Log.d(TAG, "startX: " + scroller.getFinalX() + " ,startY: " + scroller.getFinalY());
          scroller.startScroll(scroller.getFinalX(), scroller.getFinalY(), dx, dy);
          invalidate();
      }
      
      @Override
      public void computeScroll() {
          if (scroller.computeScrollOffset()) {
              scrollTo(scroller.getCurrX(), scroller.getCurrY());
              postInvalidate();
          }
      }

      当需要滑动时,调用smoothScrollBy 即可。

    • scroller.startScroll 方法的参数说明:

       /**
       * Start scrolling by providing a starting point and the distance to travel.
       * The scroll will use the default value of 250 milliseconds for the
       * duration.
       * 
       * @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.
       */
      public void startScroll(int startX, int startY, int dx, int dy) {
          startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
      }
      • 当 startX > 0,起始坐标相对于View向左偏移
      • 当 startY > 0,起始坐标相对于View向上偏移
      • 当 dx > 0,View的内容移动方向为左
      • 当 dy > 0,View的内容移动方向为上

3. View的滑动

滑动方法有三种:
1. scrollToscrollBy:

```
/**
 * 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();
        }
    }
}
```

请注意,移动的是View中的内容,而View在布局中的位置不变。
参数说明:
- x > 0 : View中的内容向左移动
- y > 0 :View中的内容向上移动
多调式几次机会明白参数与移动的规律。

  1. 使用动画

    • 可以使用XML定义动画,也可以在代码直接设置:

      ObjectAnimator.ofFloat(tvTest1,"translationX",0,300).setDuration(1000).start();
    • 在”Android 开发艺术探索” 一书第132页中,作者提到移动动画结束之后的View,其位置并没有发生改变; 尽管View移动到了新位置,但只能点击其移动之前的区域,View才会响应点击事件。

      不过在测试中发现,这个问题已经被修复,android版本是比较新的API 27: Android 8.1 (Oreo)

  2. 改变布局参数:

    直接改变View的坐标和尺寸,达到移动View或改变大小的目的:

    ```
    private void moveView(View view) {
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        params.leftMargin += 100;
        view.requestLayout();
        //或者重新注入布局参数:setLayoutParams
    //  view.setLayoutParams(params);
    }
    ```
    
  3. 弹性滑动:在上一小节已经介绍了Scroller实现弹性滑动的方案。此外,还可以使用动画ValueAnimator

    final int deltaX = -100;
    final ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(1000);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float fraction = animation.getAnimatedFraction();
            llTest1.scrollTo((int) (deltaX * fraction),0);
        }
    });
    //....启动动画,放在需要的地方。
    valueAnimator.start();

    此外,还有一种方法:启动一个线程,然后不断地给Handler发送消息,在handleMessage方法中,对View调用scrollToscrollBy,就可以实现View的弹性滑动效果。不过这已经属于奇技淫巧了,用力过度,浪费不必要的开销。