步骤
-
onDraw方法通过drawBitmap画出图片
-
通过x,y轴偏移量动态计算将图片布局到屏幕中间
-
计算出图片的最小和最大放缩比
-
-
缩放测试,测试图片放缩效果
-
-
手势监听事件挂载,覆盖原生onTouchEvent,返回GestureDetectorCompat的onTouchEvent监听,实现GestureDetector.
private gestureDetector = GestureDetectorCompat(context,gestureListener)- onDown:重写,要返回true
- onShowPress:按下和预按下结合的一个回调方法
- onSingleTapUp:单机抬起,实现逻辑与onClick有细微差别,在本例中可以作为onClick的一个替代,重写,返回true或false不影响流程(我们的事件是否被消费以onDown为准)
- onScroll:滑动,可以监测滑动距离等,重写,返回true或false不影响(由于我们本例是缩放ImageView)
- onFling:快速滑动会惯性滑动,如微信列表快速滑动,当我们抬起之后,列表继续滑动,这时候会触发onFling, onScroll是手指拖着滑动,一个是手指松开之后的滑动
- onLongPress:与onLongClick类似
override fun onDown(e: MotionEvent): Boolean { // 每次 ACTION_DOWN 事件出现的时候都会被调用,在这里返回 true可以保证必然消费掉事件 return true } override fun onShowPress(e: MotionEvent) { // 用户按下 100ms 不松手后会被调用,用于标记「可以显示按下状态了」 } override fun onSingleTapUp(e: MotionEvent): Boolean { // 用户单击时被调用(支持⻓按时⻓按后松手不会调用、双击的第二下时不会被调用) return false } override fun onScroll(downEvent: MotionEvent, currentEvent: MotionEvent, distanceX: Float, distanceY: Float): Boolean { // 用户滑动时被调用 // 第一个事件是用户按下时的 ACTION_DOWN 事件,第二个事件是当前 事件 // 偏移是按下时的位置 - 当前事件的位置 return false } override fun onLongPress(e: MotionEvent) { // 用户⻓按(按下 500ms 不松手)后会被调用 // 这个 500ms 在 GestureDetectorCompat 中变成了 600ms(???) } override fun onFling(downEvent: MotionEvent, currentEvent: MotionEvent, velocityX: Float, velocityY: Float): Boolean { // 用于滑动时迅速抬起时被调用,用于用户希望控件进行惯性滑动的场景 return false } -
添加双击监听器GestureDetector.OnDoubleTapListener
gestureDetector.setOnDoubleTapListener(doubleTapListener);- 那为什么还要实现OnGestureListener呢:需要使用其中的onDown回调
- onDoubleTap:两次点击大于40ms并且小于300ms被认定是双击,否则false
- 代码
if (deltaTime > 300 || deltaTime < 40) { return false; }
- 代码
- onDoubleTapEvent:用户双击第二次按下时、第二次按下后移动时、第二次按下后抬起时都会被调用
- onSingleTapConfirmed:单击确认回调,支持双击的时候,onSingleTapConfirmed要比onSingleTapUp更准确,因为onSingleTapConfirmed总要等待300ms
override fun onSingleTapConfirmed(e: MotionEvent): Boolean { // 用户单击时被调用 // 和 onSingltTapUp() 的区别在于,用户的一次点击不会立即调用 //这个方法,而是在一定时间后(300ms),确认用户没有进行双击,这个方法才会被调用 return false } override fun onDoubleTap(e: MotionEvent): Boolean { // 用户双击时被调用 // 注意:第二次触摸到屏幕时就调用,而不是抬起时 return false } override fun onDoubleTapEvent(e: MotionEvent): Boolean { // 用户双击第二次按下时、第二次按下后移动时、第二次按下后抬起时都会被调用 // 常用于「双击拖拽」的场景 return false } -
在双击事件回调中标记状态重新绘制图片
override fun onDoubleTap(e: MotionEvent?): Boolean { //7. 在双击事件回调中标记状态重新绘制图片 big = !big invalidate() return true } -
加入属性动画放缩效果
ObjectAnimator.ofFloat(this,"scaleFraction",0f,1f) -
加入图片滑动支持,监听滑动事件并处理
- 首先在onScroll中监听滑动距离
- 然后在绘制的时候通过canvas.translate(x偏移,y偏移)移动图片
-
现在双击图片并触摸移动图片,发现图片会有白边,下面需要通过overScroller计算滑动偏移,设置图片惯性滑动距离,计算偏移量,并在下一帧刷新
-
动画结束的回调函数中将偏移量进行重置
-
现在双击图片,不管在哪里双击都只会沿着中心点进行放大,而我们要做的是点哪里从哪里进行放大,所以要在双击事件中进行双击触点偏移修正
-
边缘修正
现在,我们基本实现了双击放大缩小的功能,下面我们要实现双指捏撑缩小放大效果,将ScalableImageView代码保存,新建ScalableImageView2实现双指捏撑放大缩小效果
-
将接口抽离为内部类,有效减少无用的接口实现代码
DSHGuestureListener:GestureDetector.SimpleOnGestureListener -
添加放缩手势操作监听器
- ① 内部类监听放缩手势事件
- ② 用scaleGestureDetector替代gestureDetector,监听onTouch事件
- ③ currentScale放缩系数值要改变,onDraw和onSizeChanged也需要修改
- ④ onScale监听事件中动态计算currentScale的值
-
放缩触点偏移修正,在onScaleBegin中修正offsetX和offsetY
-
放缩比不能无限大或无限小,要限制范围在minScale和maxScale之间,否则将可以无限放大或缩小
-
onScale中对事件消费返回值处理 只有放缩比在合理范围才消费,并且注掉15④和17步骤的代码
-
双击和放缩手势兼容处理,放缩具有较高的优先级,优先处理放缩触摸事件,并且注掉15②的代码
完整代码如下
/** 双击和捏撑可以放大缩小的ImageView
* @author dongshuhuan
* date 2020/12/18
* version
*/
private val IMG_SIEZ = 300.dp.toInt()
private const val EXTRA_SACLE_FACTOR = 1.5F
class ScalableImageView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val bitmap = getAvatar(resources,IMG_SIEZ)
private var originalOffsetX = 0f //原始横向偏移量
private var originalOffsetY = 0f //原始纵向偏移量
private var offsetX = 0f //额外滑动横向偏移量
private var offsetY = 0f //额外滑动纵向偏移量
private var minScale = 0f //最小缩放比
private var maxScale = 0f //最大缩放比
private val dshScaleGestureListener = DSHScaleGestureListener()
private val dshGuestureListener = DSHGuestureListener()
private val dshFlingRunnner = DshFlingRunnner()
private val scaleGestureDetector = ScaleGestureDetector(context,dshScaleGestureListener)
var big = false//放大了么
//5&6 手势监听
private val gestureDetector = GestureDetectorCompat(context,dshGuestureListener)
//15 ③ 放缩系数值要改变,onDraw和onSizeChanged也需要修改
private var currentScale = 0f
set(value) {
field = value
invalidate()
}
private val scaleAnimator:ObjectAnimator = ObjectAnimator.ofFloat(this,"currentScale",minScale,maxScale)
private val scroller = OverScroller(context)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val scaleFraction = (currentScale-minScale)/(maxScale-minScale)
//9.移动图片②绘制时横移
canvas.translate(offsetX*scaleFraction,offsetY*scaleFraction)
//4. 缩放
canvas.scale(currentScale,currentScale,width/2f,height/2f)
//1. 绘制图片
canvas.drawBitmap(bitmap,originalOffsetX,originalOffsetY,paint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
//2. 图片位置x,y位置偏移量动态计算
originalOffsetX = (width- IMG_SIEZ)/2f
originalOffsetY = (height- IMG_SIEZ)/2f
//3. 要算出较小缩放比值和较大缩放比值
//宽高比大于屏幕宽高比的,胖图
if (bitmap.width/bitmap.height.toFloat()>width/height.toFloat()){
minScale = width/bitmap.width.toFloat()
maxScale = height/bitmap.height.toFloat()*EXTRA_SACLE_FACTOR
}else{
//瘦图
minScale = height/bitmap.height.toFloat()
maxScale = width/bitmap.width.toFloat()*EXTRA_SACLE_FACTOR
}
currentScale = minScale
scaleAnimator.setFloatValues(minScale,maxScale)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
// //5. 覆盖原生onTouchEvent, 挂载手势监听事件
//// return gestureDetector.onTouchEvent(event)
// //15 ②用scaleGestureDetector替代gestureDetector,监听onTouch事件
// return scaleGestureDetector.onTouchEvent(event)
//19. 双击和放缩手势兼容处理,放缩具有较高的优先级,优先处理放缩触摸事件
scaleGestureDetector.onTouchEvent(event)
if (!scaleGestureDetector.isInProgress){
gestureDetector.onTouchEvent(event)
}
return true
}
private fun fixOffset() {
//横向滑动边界限制
offsetX = min(offsetX,(bitmap.width*maxScale-width)/2)
offsetX = max(offsetX,-(bitmap.width*maxScale-width)/2)
//纵向滑动边界限制
offsetY = min(offsetY, (bitmap.height * maxScale - height) / 2)
offsetY = max(offsetY, -(bitmap.height * maxScale - height) / 2)
}
//14 将多个接口抽离为一个类
inner class DSHGuestureListener:GestureDetector.SimpleOnGestureListener(){
override fun onDown(e: MotionEvent?): Boolean {
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
// 惯性滑动
// 或快速滑动 如微信列表快速滑动,当我们抬起之后,列表继续滑动,这时候会触发onFling
if (big){
//10. ①防止白边处理,设置图片惯性滑动距离,并在下一帧刷新
scroller.fling(offsetX.toInt(),offsetY.toInt(),velocityX.toInt(),velocityY.toInt(),
-((bitmap.width*maxScale-width)/2).toInt(), ((bitmap.width*maxScale-width)/2).toInt(),
-((bitmap.height*maxScale-height)/2).toInt(),((bitmap.height*maxScale-height)/2).toInt(),
40.dp.toInt(),40.dp.toInt())//两个40.dp代表惯性回弹距离
//10. ②下一帧刷新
ViewCompat.postOnAnimation(this@ScalableImageView2,dshFlingRunnner)
}
return false
}
override fun onScroll(
downEvent: MotionEvent?,
currentEvent: MotionEvent?,
distanceX: Float,
distanceY: Float): Boolean {
//9.移动图片①监听滑动距离
if (big){
offsetX -= distanceX
offsetY -= distanceY
fixOffset()
invalidate()
}
return false;
}
override fun onDoubleTap(e: MotionEvent): Boolean {
//两次点击大于40ms并且小于300ms被认定是双击,否则false
//注意:第二次触摸到屏幕时就调用,而不是抬起时
//7. 在双击事件回调中标记状态重新绘制图片
big = !big
//8. 放缩动画
if (big){
//12. 双击触点偏移修正(防止在任何地方双击都从中心点开始放大),偏移量处理
offsetX = (e.x-width/2f)*(1-maxScale/minScale)
offsetY = (e.y-height/2f)*(1-maxScale/minScale)
//12 end
//13 边缘修正
fixOffset()
scaleAnimator.start()
}else{
scaleAnimator.reverse()
}
return true
}
}
inner class DshFlingRunnner:Runnable{
override fun run() {
if (scroller.computeScrollOffset()) {
// 把此时的位置应用于界面
offsetX = scroller.currX.toFloat()
offsetY = scroller.currY.toFloat()
invalidate()
// 下一帧刷新
ViewCompat.postOnAnimation(this@ScalableImageView2,this)
}
}
}
//15 ①内部类监听放缩手势事件
inner class DSHScaleGestureListener:ScaleGestureDetector.OnScaleGestureListener{
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
//16 放缩触点偏移量修正
offsetX = (detector.focusX-width/2f)*(1-maxScale/minScale)
offsetY = (detector.focusY-height/2f)*(1-maxScale/minScale)
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector?) {
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
//18 事件消费返回值处理 只有放缩比在合理范围才消费
val tempCurrentScale = currentScale * detector.scaleFactor
if (tempCurrentScale<minScale||tempCurrentScale>maxScale){
return false
}else{
currentScale *= detector.scaleFactor//0 1; 0 无穷
return true
}
// //15 ④onScale监听事件中动态计算currentScale的值
// currentScale *= detector.scaleFactor//0 1; 0 无穷
// //17 放缩比不能无限大或无限小,要限制范围
// //coerceAtLeast等效Math.min, coerceAtMost等效Math.max
// currentScale = currentScale.coerceAtLeast(minScale).coerceAtMost(maxScale)
// return true
}
}
}