- 前言
- 一、GestureDetectorCompat是什么?
- 二、使用步骤
- 1.定义一个GestureDetectorCompat的实例
- 2.重写方法,实现自定义效果
- ①想让检测器消费一系列的触摸事件,那么就要在重写方法`onDown`中去返回true,那么后续的一系列触摸过程才能让手势检测器获取
- ②既然我们要去实现双击效果,那么就得再给手势检测器设置一个监听
- ③实现双击变大变小
- ④重写onDoubleTap
- ④实现惯性滑动
- 总结
- 完整代码
前言
在view中我们可以重写onTouchEvent来自定义点击事件,但是MotionEvent给我们的选择太少,无法满足一些个性化的需求,比如双击,惯性滑动等等,所以我们引入GestureDetectorCompat监听器来实现一些额外的功能
一、GestureDetectorCompat是什么?
GestureDetectorCompat,翻译系为手势检测器,类似于外挂,钩子,把你在屏幕上的触摸和点击截取到,去替代默认的super.onTouchEvent(event),而是走我们在手势检测器中自定义的触摸效果
二、使用步骤 1.定义一个GestureDetectorCompat的实例代码如下:
private val gestureDetectorCompat = GestureDetectorCompat(context, this)
第二个参数为listener,让view实现GestureDetector.OnGestureListener接口,我们就可以直接填入this,但是要去重写抽象方法。
override fun onDown(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
override fun onShowPress(e: MotionEvent?) {
TODO("Not yet implemented")
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
TODO("Not yet implemented")
}
override fun onLongPress(e: MotionEvent?) {
TODO("Not yet implemented")
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
TODO("Not yet implemented")
}
2.重写方法,实现自定义效果
①想让检测器消费一系列的触摸事件,那么就要在重写方法onDown中去返回true,那么后续的一系列触摸过程才能让手势检测器获取
代码如下:
override fun onDown(e: MotionEvent?): Boolean {
return true
}
②既然我们要去实现双击效果,那么就得再给手势检测器设置一个监听
private val gestureDetectorCompat = GestureDetectorCompat(context, this).apply {
setOnDoubleTapListener(this@ScalableImageView)
}
setOnDoubleTapListener的参数是传入一个listener,那么我们还是填入view,让view去重写接口GestureDetector.OnDoubleTapListener的方法即可
重写的方法:
override fun onSingleTap/confirm/ied(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
TODO("Not yet implemented")
}
简化:
追踪GestureDetectorCompat的构造函数的源码我们可以看到,内部帮我们判断了listener的类型,如果是GestureDetector.OnDoubleTapListene的实现类,那么就会帮我们去执行setOnDoubleTapListener方法,不用我们再去配置,所以代码可以简化为:
private val gestureDetectorCompat = GestureDetectorCompat(context, this)③实现双击变大变小
效果:
目的是双击实现图片的放大缩小,所以我们做一个动画,控制图片的大小
private var scaleFraction = 0f
set(value) {
field = value
invalidate()
}
private val animator by lazy { ObjectAnimator.ofFloat(this, "scaleFraction", 0f, 1f) }
在ondraw中,拿到要缩放的比例系数,实现从小图片到大图片
val scale = smallScale + (bigScale - smallScale) * scaleFraction
scale(scale, scale, width / 2f, height / 2f)
drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
所以在实现双击的方法中,从小到大就是正常播放动画,从大到小就是反向播放动画
override fun onDoubleTap(e: MotionEvent?): Boolean {
if (isBig) {
//本来是大图片,那么就从小到大
animator.reverse()
} else {
animator.start()
}
isBig = !isBig
return true
}
④实现惯性滑动
核心是重写onFling方法
创建一个OverScroller对象
private val overScroller = OverScroller(context)
OverScroller的作用:控制一个点在一定范围内的惯性滑动
如图所示
那么如何控制一张图片的惯性移动呢?
把图片在大框中的移动等价为触摸点在小框中的移动,触摸点在x,y轴上移动的位移,同步到图片在xy轴上的偏移,那么就可以实现图片的惯性
说白了就是把你的触摸点控制在一个小框的范围内,就可以把图片控制在大框内
实现步骤:
①:定义两个变量作为小圆在x轴上移动的偏移,y轴上移动的偏移
private var offsetX = 0f
private var offsetY = 0f
②:重写onFling方法
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
overScroller.fling(
offsetX.toInt(),
offsetY.toInt(),
velocityX.toInt(),
velocityY.toInt(),
(-(bitmap.width * bigScale - width) / 2f).toInt(),
((bitmap.width * bigScale - width) / 2f).toInt(),
(-(bitmap.height * bigScale - height) / 2f).toInt(),
((bitmap.height * bigScale - height) / 2f).toInt(), 40.dp.toInt(), 40.dp.toInt()
)
postOnAnimation(this)
return false
}
第一个参数和第二个参数:手指点击下去时的位置
第三第四个参数:手指用力滑动时,在两个方向上的速度
第五第六个参数:包围触摸点的小框的范围
最后两个参数指滑出小框边界时,超出又恢复的范围
如下图所示的效果
总结:把触摸点的位移同步给图片
③实现流畅的惯性滑动
让view实现Runnable接口,重写run方法,按帧去更新滑动
override fun run() {
//只要惯性还没结束,就递归去更新图片位置
if (overScroller.computeScrollOffset()) {
offsetX = overScroller.currX.toFloat()
offsetY = overScroller.currY.toFloat()
invalidate()
postOnAnimation(this)
}
}
④在onFling中使用run方法
postOnAnimation(this)
总结
使用了一个手势检测器去检测一些额外的手势,如双击,惯性滑动等等,再去重写手势检测器的抽象方法,实现这些手势该有的反馈
完整代码package com.lbj23.customview.customview
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.widget.OverScroller
import androidx.core.view.GestureDetectorCompat
import com.lbj23.customview.R
import com.lbj23.customview.dp
import com.lbj23.customview.getAvatar
import kotlin.math.max
import kotlin.math.min
private const val EXTRA_SCALE = 1.5f
class ScalableImageView(context: Context?, attrs: AttributeSet?) : View(context, attrs),
GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, Runnable {
private val gestureDetectorCompat = GestureDetectorCompat(context, this).apply {
setIsLongpressEnabled(false)
}
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val imageSize = 120.dp.toInt()
private val bitmap = getAvatar(resources, R.drawable.test, imageSize)
private var originalOffsetX = 0f
private var originalOffsetY = 0f
private var offsetX = 0f
private var offsetY = 0f
private var bigScale = 0f
private var smallScale = 0f
private var isBig = false
private var scaleFraction = 0f
set(value) {
field = value
invalidate()
}
private val animator by lazy { ObjectAnimator.ofFloat(this, "scaleFraction", 0f, 1f) }
private val overScroller = OverScroller(context)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.apply {
if (isBig) {
translate(offsetX, offsetY)
}
val scale = smallScale + (bigScale - smallScale) * scaleFraction
scale(scale, scale, width / 2f, height / 2f)
drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
originalOffsetX = (width - bitmap.width) / 2f
originalOffsetY = (height - bitmap.height) / 2f
if (width / height.toFloat() < bitmap.width / bitmap.height.toFloat()) {
smallScale = width / bitmap.width.toFloat()
bigScale = (height / bitmap.height.toFloat()) * EXTRA_SCALE
} else {
smallScale = height / bitmap.height.toFloat()
bigScale = (width / bitmap.width.toFloat()) * EXTRA_SCALE
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
return gestureDetectorCompat.onTouchEvent(event)
}
override fun onDown(e: MotionEvent?): Boolean {
return true
}
override fun onShowPress(e: MotionEvent?) {
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
return false
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
distanceX: Float,
distanceY: Float
): Boolean {
if (isBig) {
offsetX -= distanceX
offsetX = min((bitmap.width * bigScale - width) / 2f, offsetX)
offsetX = max(-(bitmap.width * bigScale - width) / 2f, offsetX)
offsetY -= distanceY
offsetY = min((bitmap.height * bigScale - height) / 2f, offsetY)
offsetY = max(-(bitmap.height * bigScale - height) / 2f, offsetY)
invalidate()
}
return true
}
override fun onLongPress(e: MotionEvent?) {
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
overScroller.fling(
offsetX.toInt(),
offsetY.toInt(),
velocityX.toInt(),
velocityY.toInt(),
(-(bitmap.width * bigScale - width) / 2f).toInt(),
((bitmap.width * bigScale - width) / 2f).toInt(),
(-(bitmap.height * bigScale - height) / 2f).toInt(),
((bitmap.height * bigScale - height) / 2f).toInt(), 40.dp.toInt(), 40.dp.toInt()
)
postOnAnimation(this)
return false
}
override fun run() {
//只要惯性还没结束,就递归去更新图片位置
if (overScroller.computeScrollOffset()) {
offsetX = overScroller.currX.toFloat()
offsetY = overScroller.currY.toFloat()
invalidate()
postOnAnimation(this)
}
}
override fun onSingleTap/confirm/ied(e: MotionEvent?): Boolean {
return false
}
override fun onDoubleTap(e: MotionEvent?): Boolean {
if (isBig) {
//本来是大图片,那么就从小到大
animator.reverse()
} else {
animator.start()
}
isBig = !isBig
return true
}
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
return false
}
}



