ViewGroup触摸反馈

680 阅读3分钟

ViewGroup 的触摸反馈

  • 除了重写 onTouchEvent(),还需要重写 onInterceptTouchEvent()
  • onInterceptTouchEvent() 中,ACTION_DOWN 事件做的事和 onTouchEvent()基本一致或完全一致
    • 原因:ACTION_DOWN 在多数手势中起到的是起始记录的作用(例如记录 手指落点),而 onInterceptTouchEvent() 调用后,onTouchEvent() 未必 会被调用,因此需要把这个记录责任转交给 onInterceptTouchEvent()。
    • 有时 ACTION_DOWN 事件也会在经过 onInterceptTouchEvent() 之后再转 交给自己的 onTouchEvent()(例如当没有触摸到子 View 或者触摸到的子 View 没有消费事件时)。因此需要确认在 onInterceptTouchEvent() 和 onTouchEvent() 都被调用时,程序状态不会出问题。
  • onInterceptTouchEvent() 中,ACTION_MOVE 一般的作用是确认滑动,即当用 户朝某一方向滑动一段距离(touch slop)后,ViewGroup 要向自己的子 View 和父 View 确认自己将消费事件。
    • 确认滑动的方式:Math.abs(event.getX() - downX) >ViewConfiguration.getXxxSlop()

    • 告知子 View 的方式:在 onInterceptTouchEvent() 中返回 true,子 View 会收到 ACTION_CANCEL 事件,并且后续事件不再发给子 View

    • 告知父 View 的方式:调用 getParent().requestDisallowInterceptTouchEvent(true) 。这个方法会递 归通知每一级父 View,让他们在后续事件中不要再尝试通过 onInterceptTouchEvent() 拦截事件。这个通知仅在当前事件序列有效,即 在这组事件结束后(即用户抬手后),父 View 会自动对后续的新事件序列 启用拦截机制

VelocityTracker

  • 如果 GestureDetector 不能满足需求,或者觉得 GestureDetector 过于复杂, 可以自己处理 onTouchEvent() 的事件。但需要使用 VelocityTracker 来计算手 指移动速度。
  • 使用方法:
    • 在每个事件序列开始是(即 ACTION_DOWN 事件到来时),通过 VelocityTracker.obtain() 创建一个实例,或者使用 velocityTracker.clear() 把之前的某个实例重置
    • 对于每个事件(包括 ACTION_DOWN 事件),使用 velocityTracker.addMovement(event) 把事件添加进 VelocityTracker
    • 在需要速度的时候(例如在 ACTION_UP 中计算是否达到 fling 速度),使 用 velocityTracker.computeCurrentVelocity(1000, maxVelocity) 来计算实 时速度,并通过 getXVelocity() / getYVelocity() 来获取计算出的速度
      • 方法参数中的 1000 是指的计算的时间⻓度,单位是 ms。例如这里填 入 1000,那么 getXVelocity() 返回的值就是每 1000ms (即一秒)时 间内手指移动的像素数
      • 第二个参数是速度上限,超过这个速度时,计算出的速度会回落到这个 速度。例如这里填了 200,而实时速度是 300,那么实际的返回速度将 是 200
        • maxVelocity 可以通过 viewConfiguration.getScaledMaximumFlingVelocity() 来获取

scrollTo / scrollBy 和 computeScroll()

  • scrollTo() / scrollBy() 会设置绘制时的偏移,通常用于滑动控件设置偏移
  • scroll 值表示绘制行为在控件内部内容的起始偏移(类似:我要从内容的第 300 个像素开始绘制),因此 scrollTo() 内的参数填正值时,绘制内容会向负向移动
  • scrollTo() 是瞬时方法,不会自动使用动画。如果要用动画,需要配合 View.computeScroll() 方法
    • computeScroll() 在 View 重绘时被自动调用
    • 使用方式:
    // onTouchEvent() 中: 
    overScroller.startScroll(startX, startY, dx, dy); postInvalidateOnAnimation();
    ......
    
    // onTouchEvent() 外:
    override fun computeScroll() {
        if (overScroller.computeScrollOffset()) { // 计算实时位置
            scrollTo(overScroller.currX.toFloat(), overScroller.currY.toFloat()); // 更新界面
            postInvalidateOnAnimation(); // 下一帧继续 
        }
    }
    

手写viewPager

class TwoPager(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {
  private var downX = 0f
  private var downY = 0f
  private var downScrollX = 0f
  private var scrolling = false
  private val overScroller: OverScroller = OverScroller(context)
  private val viewConfiguration: ViewConfiguration = ViewConfiguration.get(context)
  private val velocityTracker = VelocityTracker.obtain()
  private var minVelocity = viewConfiguration.scaledMinimumFlingVelocity
  private var maxVelocity = viewConfiguration.scaledMaximumFlingVelocity
  private var pagingSlop = viewConfiguration.scaledPagingTouchSlop

  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    measureChildren(widthMeasureSpec, heightMeasureSpec)
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
  }

  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var childLeft = 0
    val childTop = 0
    var childRight = width
    val childBottom = height
    val count: Int = getChildCount()
    for (index in 0 until count) {
      val child = getChildAt(index)
      child.layout(childLeft, childTop, childRight, childBottom)
      childLeft += width
      childRight += width
    }
  }

  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    if (event.actionMasked == MotionEvent.ACTION_DOWN) {
      velocityTracker.clear()
    }
    velocityTracker.addMovement(event)
    var result = false
    when (event.actionMasked) {
      MotionEvent.ACTION_DOWN -> {
        scrolling = false
        downX = event.x
        downY = event.y
        downScrollX = scrollX.toFloat()
      }
      MotionEvent.ACTION_MOVE -> if (!scrolling) {
        val dx = downX - event.x
        if (abs(dx) > pagingSlop) {
          scrolling = true
          parent.requestDisallowInterceptTouchEvent(true)
          result = true
        }
      }
    }
    return result
  }

  override fun onTouchEvent(event: MotionEvent): Boolean {
    if (event.actionMasked == MotionEvent.ACTION_DOWN) {
      velocityTracker.clear()
    }
    velocityTracker.addMovement(event)
    when (event.actionMasked) {
      MotionEvent.ACTION_DOWN -> {
        downX = event.x
        downY = event.y
        downScrollX = scrollX.toFloat()
      }
      MotionEvent.ACTION_MOVE -> {
        val dx = (downX - event.x + downScrollX).toInt()
          .coerceAtLeast(0)
          .coerceAtMost(width)
        scrollTo(dx, 0)
      }
      MotionEvent.ACTION_UP -> {
        velocityTracker.computeCurrentVelocity(1000, maxVelocity.toFloat()) // 5m/s 5km/h
        val vx = velocityTracker.xVelocity
        val scrollX = scrollX
        val targetPage = if (abs(vx) < minVelocity) {
          if (scrollX > width / 2) 1 else 0
        } else {
          if (vx < 0) 1 else 0
        }
        val scrollDistance = if (targetPage == 1) width - scrollX else -scrollX
        overScroller.startScroll(getScrollX(), 0, scrollDistance, 0)
        postInvalidateOnAnimation()
      }
    }
    return true
  }

  override fun computeScroll() {
    if (overScroller.computeScrollOffset()) {
      scrollTo(overScroller.currX, overScroller.currY)
      postInvalidateOnAnimation()
    }
  }
}
<?xml version="1.0" encoding="utf-8"?>
<com.dsh.txlessons.viewgrouptouch.TwoPager xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.dsh.txlessons.viewgrouptouch.ViewGroupTouchActivity"
    >

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#795548" />

    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#388E3C" />

</com.dsh.txlessons.viewgrouptouch.TwoPager>