Android开发艺术探索第三章:View的事件体系

Android开发艺术探索第三章:View的事件体系

Android小彩虹2021-08-17 0:44:23240A+A-

View 基础

  • View的位置由它的四个顶点决定,top, left, right, bottom。左上角坐标为(left, top),右下角坐标为(right, bottom)。这些坐标都是相对于View的父容器来首的,是相对坐标。在Android中x轴和y轴的正方向分别为右和下。

MotionEvent 和 TouchSlop

MotionEvent

  • ACTION_DOWN ---> 手指刚接触屏幕。
  • ACTION_MOVE ---> 手指从屏幕上移动。
  • ACTION_UP ---> 手指从屏幕上松开的一瞬间。
  • 用户点击屏幕后离开松开,顺序为:DOWN-->UP
  • 用户点击屏幕后滑动一会再松开,顺序为:DOWN->MOVE->MOVE...->MOVE->UP
  • MotionEvent: getX/getY 方法返回的是相对于当前View左上角的x和y坐标。
  • MotionEvent: getRawX/getRawY 方法返回的是相对于手机屏幕左上角的x和y坐标。

TouchSlop

  • TouchSlop这是一个常量,表示系统所能识别出的被认为是滑动的最小距离。不同设备这个值可能是不同的。ViewConfiguration.get(context).getScaledDoubleTapSlop()。可以通过这个方法拿到。

VelocityTracker GestureDetectoe Scroller

VelocityTracker

view.setOnTouchListener(object : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        val velocityTracker = VelocityTracker.obtain()
        velocityTracker.addMovement(event)
        // 表示计算当前的速率 参数1000表示1秒内走过的像素点
        velocityTracker.computeCurrentVelocity(1000)
        // 下面两行代码得到的表示在上面设置的时间间隔内水平或者竖直方向上所滑动的速度。
        // 可以为负数,比如水平方向从右往左滑就是负数。竖直方向从下往上滑动就是负数。
        val xVelocity = velocityTracker.getXVelocity()
        val yVelocity = velocityTracker.getYVelocity()
        Log.d("joker", "x方向在1000ms走过的像素: $xVelocity")
        Log.d("joker", "yVelocity: $yVelocity")
        velocityTracker.clear()
        velocityTracker.recycle()
        return true
    }
})
  • 公式: 速度=(终点位置 - 起点位置) / 时间段
  • 获取速度前必须先计算速度,computeCurrentVelocity的参数表示一个时间间隔,单位是毫秒。计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素数。
  • 这个对象不用了记得释放资源(最后两行代码)

GestureDetectoe

  • 手势检测,用户复制检测用户的单击,滑动,长按,双击等行为。
override fun onTouchEvent(event: MotionEvent?): Boolean {
//使用期按需要接管view的事件
    return gestureDetector.onTouchEvent(event)
}

OnGestureListener

GestureDetector(context, object : GestureDetector.OnGestureListener {
            override fun onDown(e: MotionEvent?): Boolean {
                // 手指触摸屏幕的那一瞬间 ,由一个ACTION_DOWN 来触发
                // 返回值如果是false,表示这个事件交给其他View来处理,并且该View剩下的事件不会触发
                // 返回值为true, 表示当前View处理这个事件,其他方法会被调用,比如onScroll等会调用
                Log.d("joker", "onDown")
                return true
            }
            override fun onShowPress(e: MotionEvent?) {
                // 如果按下直接拖动,就不会触发这个事件,注意不是按住拖动,是按下直接拖动
                // 和 OnDown() 的区别,它强调的是 没有 松开或者拖动的状态。
                // 手指轻轻触摸屏幕,尚未松开或拖动,有一个ACTION_DOWN触发。
                Log.d("joker", "onShowPress")
            }
            override fun onSingleTapUp(e: MotionEvent?): Boolean {
                // 手指(轻轻触摸屏幕后)松开,伴随着一个ACTION_UP触发,这是单击行为。
                // 返回值表示是否消费掉此事件
                // 如果是双击当中的一次单击行为也会触发,下面的onSingleTapConfirmed就不会触发
                Log.d("joker", "onSingleTapUp")
                return false
            }
            override fun onScroll(e1: MotionEvent?,e2: MotionEvent?,distanceX: Float,distanceY: Float): Boolean {
                // 按下屏幕并且拖动,由一个ACTION_DOWN 和 多个ACTION_MOVE 触发,这是拖动行为。
                Log.d("joker", "onScroll")
                return true
            }
            override fun onLongPress(e: MotionEvent?) {
                // 用户长按屏幕,长按行为
                Log.d("joker", "onLongPress")
            }
            override fun onFling(e1: MotionEvent?,e2: MotionEvent?,velocityX: Float,velocityY: Float): Boolean {
                // 用户按下屏幕,快速滑动后松开,由一个ACTION_DOWN, 多个 ACTION_MOVE 一个ACTION_UP组成
                // 这是快速滑动行为
                // 也可以表示当前滑动结束,在onScroll后调用
                Log.d("joker", "onFling")
                return true
            }
        })

OnDoubleTapListener

        gestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
            override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
                // 表示严格的单击行为,和onSingleTapUp的区别是用户这个时候只可能是单击,而不可能是双击中的一次/
                log("onSingleTapConfirmed")
                return true
            }

            override fun onDoubleTap(e: MotionEvent?): Boolean {
                // 表示一次双击行为
                log("onDoubleTap")
                return true
            }

            override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
                // 在一次双击过程中,ACTION_DOWN ACTION_MOVE ACTION_UP都会触发这个事件
                // 比如用户双击后不松手,拖动
                log("onDoubleTapEvent")
                return true
            }
        })
  • 也可以不使用上面两个监听器,而使用View的onTouchEvent来实现所需的监听。
  • 建议:如果是监听滑动相关,在onTrouchEvent中就行,如果是双击这种行为,就是用gesturedector。

view 的滑动

scrollTo / scrollBy

  • mScroolX的值 总 等于View左边缘和View内容左边缘在水平方向的距离。
  • mScroolY的值 总 等于View上边缘和View内容上边缘在竖直方向的距离。
  • scrollTo / scrollBy 只能改变View内容的位置,而不能改变View在布局中的位置。
  • 因为不能改变View在布局中的位置,所以不管怎么滑,也不能将当前View滑到附近View所在的区域。

使用动画

  • 使用补间动画,移动的只是view的影相。如果不设置fillAfter,动画完成的一刹那,就会还原。比如动画后的位置不会响应事件,因为只是影相到那里去了。

改变布局参数

  • layoutParams 那一套

Scroller 典型代码 以及 说明

val scroller: Scroller = Scroller(context)

fun startScroll() {
    scroller.startScroll(0, 0, 100, 100)
    invalidate()
}

override fun computeScroll() {
    if (scroller.computeScrollOffset()) {
        scrollTo(scroller.currX, scroller.currY)
        postInvalidate()
    }
    super.computeScroll()
}
  • invalidate()方法会导致view重绘,在View的draw方法中会调用computeScroll方法,在这个方法中会去向Scroller获取当前scrollX和scrollerY然后通过scrollergTo实现滑动,然后在调用postInvalidate方法再次通知重绘。

view 的事件分发

public boolean dispatchTouchEvent(MotionEvent ev)

  • 此方法用来做进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

  • 在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

public boolean onTouchEvent(MotionEvent event)

  • 在dispatchTouchEvent方法内部调用,用来处理当前点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

表示上面三个方法关系的 伪代码

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

说明

  • 传递顺序: Activity -> Window -> View 。到了View这一层后,如果所有的子View都不处理这个事件,那么这个事件会再次传回到Activity中,即Activity的onTouchEvent会被调用。
  • 同一个事件序列,以down事件开始,中间有数量不定的move,以up事件结束。
  • 一个事件序列只能被一个view拦截并且消耗,一旦一个元素拦截了此事件,那么同一个事件序列的其他事件都会直接交给他来处理。
  • 某个view一旦决定拦截,那么这一个事件序列都只能由他来处理,并且他的onInterceptTouchEvent不会再被调用。
  • 某个View一旦开始处理事件,如果他不消耗ACTION_DOWN事件(那次的onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给他来处理,并且事件将重新交他的父元素来处理,即父元素的onTouchEvent会被调用。事件一旦交给一个View来处理,那么他就必须消耗掉,否则同一个事件序列剩下的事件就不会再交给他来处理。
  • 如果View不消耗除ACTION_DOWN的其他事件,这个View还是能接到后续的事件,并且这些未消耗的其他事件都会传递给Activity来处理。
  • ViewGroup默认不拦截任何事件,即ViewGroup的onInterceptTouchEvent方法默认返回false。
  • View没有onInterceptTouchEvent方法,一旦有点击事件传递给他,那么他的onTouchEvent方法就会被调用。
  • View的onTouchEvent默认都会返回true,即消耗事件,除非他是显示声明为不可点击的。(clickable 和 longClickable都会flase)
  • view的enable属性不影响onTouchEvent默认返回true。
  • 事件的传递是由外向内的,即事件总是先传递给父元素,再由父元素分发给子View。通过requestDisallowInteceptTouchEvent方法可以子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。

滑动冲突的解决方法

  • 原理:比如上面,处理方法为,当用户左右滑动的时候,需要让外部的View拦截点击事件,当用户上下滑动的时候,需要让内部的View拦截点击事件。即,根据滑动是水平滑动还是竖直滑动来判断到底是由谁来拦截点击事件。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们