介绍
最近在学习事件分发的知识点,刚好做这个Demo也是为了巩固知识点 参照了手机系统相册的浏览交互交过,主要包含几个方面
- 双击放大缩小图片
- 图片放大状态时左右滑动图片
- 左右滑动切换图片
- 下滑缩小关闭图片,改变背景透明度
效果展示
话不多说,先来看实现效果
实现过程
整体实现思路
使用到了ViewPager2,在viewpagerAdapter中图片的装载,后面主要需要去处理viewpager和图片的左右滑动冲突
自定义View中使用了SDK提供的GestureDetector类接管onTouchEvnet,主要是自己写起来不太方便,双击事件还得判断两次ACTION_UP的时间间隔,抛掷事件还得判断滑动的速度,使用ScaleGestureDetector类判断双指缩放事件
图片是通过canvas.drawBitmap()方法进行绘制到屏幕上,通过canvas.scale()控制缩放
- 重写
onTouchEventonTouchEvent里面进行事件的接管处理,在ACTION_UP的时候会判断当前Y的偏移距离
/** 接管Touch事件*/
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
var consume: Boolean
// 有新的事件来了,惯性滑动还没停止,强制停止
if (!overScroller.isFinished) {
overScroller.forceFinished(true)
}
if (event.action == MotionEvent.ACTION_UP) {
logD("平移距离",translationY.toString())
if (translationY / height > SCROLL_INTERVAL) {
// 超过了滑动阈值,退出View
onImageExit.invoke()
}
if (translationY != 0f) {
// 重置
currentScale = smallScale
translationY = 0f
translationX = 0f
}
}
// 双指操作优先
consume = scaleGestureDetector.onTouchEvent(event)
if (!scaleGestureDetector.isInProgress) {
consume = gestureDetector.onTouchEvent(event)
}
return consume
}
- 重写
dispatchTouchEvent,用了内部拦截的方式处理事件冲突
/** 解决与viewPager的事件冲突 */
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val x = event.rawX // 屏幕坐标系
val y = event.rawY
when (event.action) {
MotionEvent.ACTION_DOWN -> {
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
// 定义滑动规则
val deltaX = x - lastX
val deltaY = y - lastY
if (translationY != 0f) {
// picture正在平移中,不允许拦截事件
parent.requestDisallowInterceptTouchEvent(true)
return super.dispatchTouchEvent(event)
}
if (!isEnlarge) {
parent.requestDisallowInterceptTouchEvent(false)
} else {
if (abs(deltaX) > abs(deltaY)) {
// 左右滑动
if (deltaX < 0) {
// 左
Log.d("事件冲突", "左滑了,x=${x},y=${y},lastX=${lastX},lastY=${lastY}")
if (offsetX == -(bitmap.width * bigScale - width) / 2) {
parent.requestDisallowInterceptTouchEvent(false)
Log.d("事件冲突", "左滑让事件给父亲")
}
} else {
// 右
Log.d("事件冲突", "右滑了,x=${x},y=${y},lastX=${lastX},lastY=${lastY}")
if (offsetX == (bitmap.width * bigScale - width) / 2) {
parent.requestDisallowInterceptTouchEvent(false)
Log.d("事件冲突", "右滑让事件给父亲")
}
}
}
}
}
}
lastX = x
lastY = y
return super.dispatchTouchEvent(event)
}
- 双击放大缩小图片 这个需求的关键点在于,如果获取到放大和缩小的倍数,只需要获取到了,每次在双击事件触发的时候进行切换就好了
if (bitmap.width.toFloat() / bitmap.height > width.toFloat() / height) {
// 比较宽高比,判断目标bitmap是横向还是纵向,如果比屏幕宽高比大,说明相比于屏幕来说,这张图片是横向的
smallScale = width.toFloat() / bitmap.width
bigScale = height.toFloat() / bitmap.height
} else {
smallScale = height.toFloat() / bitmap.height
bigScale = width.toFloat() / bitmap.width
}
// 在双击的回调里进行切换
override fun onDoubleTap(e: MotionEvent): Boolean {
isEnlarge = !isEnlarge
if (isEnlarge) {
startScaleAnimator(smallScale, bigScale)
} else {
startScaleAnimator(currentScale, smallScale)
}
return true
}
// 使用到了属性动画,让缩放平滑些
private fun startScaleAnimator(fromScale: Float, toScale: Float) =
ObjectAnimator.ofFloat(this, "currentScale", fromScale, toScale).start()
- 图片放大状态时左右滑动图片,在没放大的情况下下滑缩小关闭图片,改变背景透明度
/** 滑动 */
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
if (isEnlarge) {
offsetX -= distanceX
offsetY -= distanceY
fixOffsets()
invalidate()
} else {
// 在没放大的情况下
val scrollDeltaY = e2.rawY - lastScrollY
val scrollDeltaX = e2.rawX - lastScrollX
val translationY = this@PhotoView.translationY + scrollDeltaY
val translationX = this@PhotoView.translationX + scrollDeltaX
if (lastScrollY != 0f) {
currentScale *= (1 - (scrollDeltaY / height))
Log.d("滑动事件", "height:${height},translation:${translationY},height/translation:${translationY / height}")
Log.d("滑动事件", "$currentScale")
onAlphaChange(1 - (this@PhotoView.translationY / height)) // 回调修改透明度
this@PhotoView.translationY = translationY
this@PhotoView.translationX = translationX
}
lastScrollY = e2.rawY
lastScrollX = e2.rawX
}
return super.onScroll(e1, e2, distanceX, distanceY)
}
/** 处理边界 */
private fun fixOffsets() {
offsetX = min(offsetX, (bitmap.width * bigScale - width) / 2) // 相当于取绝对值,获得偏移的区间值
offsetX = max(offsetX, -(bitmap.width * bigScale - width) / 2)
offsetY = min(offsetY, (bitmap.height * bigScale - height) / 2)
offsetY = max(offsetY, -(bitmap.height * bigScale - height) / 2)
}
- 双指缩放,实现
ScaleGestureDetector.OnScaleGestureListener接口
private var initialScale: Float = 0f
override fun onScale(detector: ScaleGestureDetector?): Boolean {
if (currentScale > smallScale && !isEnlarge || currentScale == smallScale && isEnlarge) {
isEnlarge = !isEnlarge
}
// 缩放因子
currentScale = initialScale * detector!!.scaleFactor
if (currentScale > bigScale) currentScale = bigScale
if (currentScale < smallScale) currentScale = smallScale
invalidate()
return false
}
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
initialScale = currentScale
return true
}
- 其他关键代码
/** 这里必须要返回true,表示接收此事件序列,不然无法收到后面的ACTION_UP和ACTION_DOWN事件 */
override fun onDown(e: MotionEvent?): Boolean {
lastScrollX = 0f
lastScrollY = 0f
return true
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
performClick()
return super.onSingleTapConfirmed(e)
}