Android View 事件体系

一、View的位置参数

top、bottom、left、right都是相对于ViewGroup的,易知:

width = right - left
height = bottom - top

另外还有x、y、translationX、translationY参数,x、y表示View左上角的坐标,translationX和translationY表示View的偏移量。在View平移的过程中,top、bottom、left、right不会改变,只有translationX、translationY、x、y会改变。

x = left + translationX
y = top + translationY

二、触摸View时,MotionEvent事件的位置信息

注:rawY是包含状态栏和ActionBar高度的。

三、最小滑动距离touchSlop

距离高于touchSlop的滑动才算有效滑动,此参数与设备有关,获取代码如下:

ViewConfiguration.get(context).scaledTouchSlop

四、速度追踪器VelocityTracker

使用方式如下:

customView.setOnTouchListener { v, event ->
    val velocityTracker = VelocityTracker.obtain()
    // 将event事件添加到VelocityTracker中追踪速度
    velocityTracker.addMovement(event)
    // 获取速度之前必须先计算速度,传入的参数表示速度的时间单位,这里表示:n像素/1000毫秒。注意它不是指多长时间计算一次速度,只是表示速度单位的不同
    velocityTracker.computeCurrentVelocity(1000)
    // 向下滑动时xVelocity为正,向上滑动时xVelocity为负。向右滑动时yVelocity为正,向左滑动时yVelocity为负
    Log.d("~~~","xVelocity = ${velocityTracker.xVelocity}, yVelocity = ${velocityTracker.yVelocity}")
    // 使用完后需要手动回收
    velocityTracker.clear()
    velocityTracker.recycle()
    true
}

注:向下滑动时xVelocity为正,向上滑动时xVelocity为负。向右滑动时yVelocity为正,向左滑动时yVelocity为负。

速度 = (终点位置 - 起点位置) / 时间段

五、手势检测GestureDetector

手势检测回调接口如下:

1.OnGestureListener

  • onDown(MotionEvent e):手指按下屏幕,由ACTION_DOWN触发

  • onShowPress(MotionEvent e):手指按下屏幕,尚未松开或拖动,强调的是没有松开或者拖动的状态。快速拖动时此回调不一定触发

  • onLongPress(MotionEvent e):用户长按后触发,触发之后不会触发其他OnGestureListener回调,直至松开(UP事件)。

  • onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY):手指按下屏幕并拖动,由一个ACTION_DOWN,多个ACTION_MOVE触发,表示拖动行为

  • onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY):用户执行快速滑动操作之后的回调,MOVE事件之后手松开(UP事件)那一瞬间的x或者y方向速度,如果达到一定数值,就是快速滑动操作,由一个ACTION_DOWN、多个ACTION_MOVE和一个ACTION_UP触发。

  • onSingleTapUp(MotionEvent e):用户手指松开(UP事件)的时候如果没有执行onScroll()和onLongPress()这两个回调的话,就会回调这个,表示一个点击抬起事件。

2.OnDoubleTapListener,这个Listener监听双击和单击事件。

  • onSingleTapConfirmed(MotionEvent e):可以确认这是一个单击事件的时候会回调,注意和onSingleTapUp的区别,即这只可能是单击,而不可能是双击中的一次单击。

  • onDoubleTap(MotionEvent e):双击,它不可能和onSingleTapConfirmed共存。

  • onDoubleTapEvent(MotionEvent e):onDoubleTap()回调之后的输入事件(DOWN、MOVE、UP)都会回调这个方法(这个方法可以实现一些双击后的控制,如让View双击后变得可拖动等)。

3.OnContextClickListener,用于检测外部设备上的按钮是否按下

  • onContextClick(MotionEvent e):当外部设备点击时候的回调,如外接键盘、外接蓝牙触控笔等等

4.SimpleOnGestureListener

实现了上面三个接口的类,拥有上面三个的所有回调方法。

使用SimpleOnGestureListener只需要选取我们所需要的回调方法来重写就可以了,减少了代码量。

例如:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val gestureDetector =
            GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
                override fun onShowPress(e: MotionEvent?) {
                    Log.d("~~~", getCurrentMethod())
                }

                override fun onSingleTapUp(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onDown(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return true
                }

                override fun onFling(
                    e1: MotionEvent?,
                    e2: MotionEvent?,
                    velocityX: Float,
                    velocityY: Float
                ): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onScroll(
                    e1: MotionEvent?,
                    e2: MotionEvent?,
                    distanceX: Float,
                    distanceY: Float
                ): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onLongPress(e: MotionEvent?) {
                    Log.d("~~~", getCurrentMethod())
                }

                override fun onDoubleTap(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return false
                }

                override fun onContextClick(e: MotionEvent?): Boolean {
                    Log.d("~~~", getCurrentMethod())
                    return super.onContextClick(e)
                }
            })
        view.setOnTouchListener { v, event ->
            // down 0, up 1, move 2
            Log.d("~~~", getCurrentMethod() + ", ${event.action}")
            gestureDetector.onTouchEvent(event)
        }
    }

    fun getCurrentMethod(): String {
        return Thread.currentThread().stackTrace[3].methodName
    }
}

单击一次,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 1
~~~: onSingleTapUp
~~~: onSingleTapConfirmed

双击一次,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 1
~~~: onSingleTapUp
~~~: onTouch, 0
~~~: onDoubleTap
~~~: onDoubleTapEvent
~~~: onDown
~~~: onTouch, 1
~~~: onDoubleTapEvent

长按,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onShowPress
~~~: onLongPress
~~~: onTouch, 1

快速滑动,Log如下:

~~~: onTouch, 0
~~~: onDown
~~~: onShowPress
~~~: onTouch, 2
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 1
~~~: onFling

或者:

~~~: onTouch, 0
~~~: onDown
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 2
~~~: onScroll
~~~: onTouch, 1
~~~: onFling

实际开发中,可以不使用GestureDetector,完全可以在View的onTouchEvent方法中实现所需的监听。《Android开发艺术探索》中建议:如果只是监听滑动相关的,建议在onTouchEvent中实现,如果监听双击行为的话,那就使用GestureDetector。

六、改变View位置的三种方式

1.使用Scroller的scrollTo和scrollBy

Scroller用来实现View的弹性滑动,Scroller的典型使用如下:

private val scroller by lazy { Scroller(context) }
private fun smoothScrollTo(destX: Int, destY: Int) {
    scroller.startScroll(scrollX, scrollY, destX - scrollX, destY - scrollY)
    invalidate()
}
override fun computeScroll() {
    super.computeScroll()
    if (scroller.computeScrollOffset()) {
        scrollTo(scroller.currX, scroller.currY)
        postInvalidate()
    }
}

Scroller的工作原理是根据起始位置和目标位置生成一系列变化的值,通过scrollTo方法一次滑动一小段直至滑动完成。

startScroll方法仅仅用来做初始化操作,然后我们调用了View的invalidate方法使View重绘,重绘时会调用computeScroll方法。我们重写了这个方法,在这个方法中调用computeScrollOffsetcomputeScrollOffset方法会根据插值器和时间间隔获取当前变化的值。如果computeScrollOffset返回true,表示尚未到达目标值,这时我们使用scrollTo(scroller.currX, scroller.currY)滑动一小段距离,然后调用postInvalidate使View再次重绘,重绘时又调用computeScroll方法,…,直至滑动完成。

例如,使用Scroller实现一个简易的ViewPager:

class ScrollLayout @JvmOverloads constructor(
    context: Context,
    attr: AttributeSet,
    defStyleAttr: Int = 0
) : ViewGroup(context, attr, defStyleAttr) {
    private val scroller by lazy { Scroller(context) }
    private val touchSlop by lazy { ViewConfiguration.get(context).scaledTouchSlop }
    private var xLastTouch = 0f
    private var leftBorder = 0
    private var rightBorder = 0

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        if (!changed || childCount == 0) return
        children.withIndex().forEach {
            val index = it.index
            val view = it.value
            view.layout(
                index * view.measuredWidth,
                0,
                (index + 1) * view.measuredWidth,
                view.measuredHeight
            )
        }
        leftBorder = children.first().left
        rightBorder = children.last().right
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        children.forEach {
            measureChild(it, widthMeasureSpec, heightMeasureSpec)
        }
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                xLastTouch = ev.rawX
            }
            MotionEvent.ACTION_MOVE -> {
                val distance = Math.abs(ev.rawX - xLastTouch)
                xLastTouch = ev.rawX
                if (distance > touchSlop) return true
            }
        }
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_MOVE -> {
                val scrolledX = (xLastTouch - event.rawX).toInt()
                if (slideToBorder(scrolledX)) return true
                scrollBy(scrolledX, 0)
                xLastTouch = event.rawX
            }
            MotionEvent.ACTION_UP -> {
                val targetIndex = (scrollX + width / 2) / width
                val distanceX = targetIndex * width - scrollX
                scroller.startScroll(scrollX, 0, distanceX, 0)
                invalidate()
            }
        }
        return super.onTouchEvent(event)
    }

    /**
     * 是否滑到了边界
     */
    private fun slideToBorder(scrolledX: Int): Boolean {
        if (scrolledX + scrollX < leftBorder) {
            scrollTo(leftBorder, 0)
            return true
        } else if (scrollX + width + scrolledX > rightBorder) {
            scrollTo(rightBorder - width, 0)
            return true
        }
        return false
    }

    override fun computeScroll() {
        super.computeScroll()
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.currX, scroller.currY)
            invalidate()
        }
    }
}

具体参见郭神的文章:Android Scroller完全解析,关于Scroller你所需知道的一切

2.View属性动画

早期版本的Android系统只支持补间动画,造成的问题是View执行完动画后,只有视图移到了新位置,“真身”还在老位置,所以点击新位置不能触发点击事件。Android3.0之后加入了属性动画,实现了View“真身”随着视图一起移动。早期的应用为了让属性动画兼容Android3.0之前的版本,需要使用JakeWharton大神的nineOldAndroids库。而现在的Android应用基本都设置为支持Android4.4以上,这个库也已经被JakeWharton标记为过时。所以我们只需学习属性动画即可。

属性动画使用很简单,例如:

ObjectAnimator.ofFloat(button, "translationX", 0f, 100f).start()

内部原理是使用ValueAnimator生成了一系列变化的值,这一点和Scroller是类似的。例如上面的代码和下面的代码是等价的:

val valueAnimator = ValueAnimator.ofFloat(0f, 100f)
valueAnimator.addUpdateListener {
    button.translationX = it.animatedValue as Float
}
valueAnimator.start()

由此可知,属性动画不受属性限制,任何属性都可以使用属性动画。常见的属性有translationXrotationalpha,分别对应平移、旋转、透明度动画。

ValueAnimator可以设置动画时长、插值器、监听器等,例如:

// 单位是毫秒,从源码中可以看到默认值是300ms
valueAnimator.duration = 1000
valueAnimator.interpolator = BounceInterpolator()
valueAnimator.addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationRepeat(animation: Animator?) {
        super.onAnimationRepeat(animation)
    }
    override fun onAnimationEnd(animation: Animator?) {
        super.onAnimationEnd(animation)
    }
    override fun onAnimationCancel(animation: Animator?) {
        super.onAnimationCancel(animation)
    }
    override fun onAnimationPause(animation: Animator?) {
        super.onAnimationPause(animation)
    }
    override fun onAnimationStart(animation: Animator?) {
        super.onAnimationStart(animation)
    }
    override fun onAnimationResume(animation: Animator?) {
        super.onAnimationResume(animation)
    }
})

这里使用的BounceInterpolator是Android自带的插值器,效果是反复弹起。Android自带以下插值器:

  • 反复弹起的插值器BounceInterpolator
  • 不断加速的插值器AccelerateInterpolator
  • 不断减速的插值器DecelerateInterpolator
  • 先加速再减速的插值器AccelerateDecelerateInterpolator
  • 先后退再前冲的插值器AnticipateInterpolator
  • 正弦曲线插值器CycleInterpolator(1f)
  • 先超过目标位置再后退插值器OvershootInterpolator
  • 匀速插值器LinearInterpolator

默认插值器是匀速插值器LinearInterpolator。如果想要自定义插值器可以查看郭神的这篇文章:Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法

AnimatorListenerAdapter和手势监听器的SimpleOnGestureListener类似,是一个实现了Animator.AnimatorListener,Animator.AnimatorPauseListener接口的类,使用AnimatorListenerAdapter可以仅重写自己需要的回调,减少代码量。

使用AnimatorSet实现组合动画,例如:

val moveIn = ObjectAnimator.ofFloat(button, "translationX", -500f, 0f)
val rotate = ObjectAnimator.ofFloat(button, "rotation", 0f, 360f)
val fadeInOut = ObjectAnimator.ofFloat(button, "alpha", 1f, 0f, 1f)
val animSet = AnimatorSet()
animSet.play(rotate).with(fadeInOut).after(moveIn)
animSet.duration = 5000
animSet.start()

AnimatorSet这个类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象,将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下四个方法:

  • after(Animator anim) 将现有动画插入到传入的动画之后执行
  • after(long delay) 将现有动画延迟指定毫秒后执行
  • before(Animator anim) 将现有动画插入到传入的动画之前执行
  • with(Animator anim) 将现有动画和传入的动画同时执行

3.改变布局参数

以ConstraintLayout为例:

val set= ConstraintSet().apply { clone(button.parent as ConstraintLayout) }
set.constrainWidth(R.id.button, 500)
set.constrainHeight(R.id.button, 500)
set.applyTo(button.parent as ConstraintLayout)

七、事件分发机制

View的事件分发机制使用的是典型的责任链模式,可以使用以下伪代码表示:

fun dispatchTouchEvent(event: MotionEvent): Boolean {
    var consume: Boolean
    if (onInterceptTouchEvent(event)) {
        consume = onTouchEvent(event)
    } else {
        consume = child.dispatchTouchEvent(event)
    }
    return consume
}

具体可参考笔者的另一篇文章:通俗讲解 Android 事件分发机制 —— 责任链模式的典型应用

参考文章

《Android开发艺术探索》
Android手势检测——GestureDetector全面分析
Android Scroller完全解析,关于Scroller你所需知道的一切
Android属性动画完全解析

原创文章 67 获赞 68 访问量 6万+

猜你喜欢

转载自blog.csdn.net/AlpinistWang/article/details/90212431