preface
In view, we can rewrite onTouchEvent to define click events, but MotionEvent gives us too few choices to meet some personalized needs, such as double clicking, inertial sliding, etc., so we introduce GestureDetectorCompat listener to realize some additional functions
1, What is GestureDetectorCompat?
GestureDetectorCompat, translated as gesture detector, is similar to plug-in and hook. It intercepts your touch and click on the screen to replace the default super.onTouchEvent(event), but follow the touch effect customized in the gesture detector
2, Use steps
1. Define an instance of GestureDetectorCompat
The code is as follows:
private val gestureDetectorCompat = GestureDetectorCompat(context, this)
The second parameter is listener. Let the view implement the GestureDetector.OnGestureListener interface. We can directly fill in this, but we need to rewrite the abstract method.
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. Rewrite the method to achieve custom effect
① If you want the detector to consume a series of touch events, you must return true in the rewrite method onDown, and then the subsequent series of touch processes can be obtained by the gesture detector
The code is as follows:
override fun onDown(e: MotionEvent?): Boolean { return true }
② Since we want to achieve the double-click effect, we have to set a monitor for the gesture detector
private val gestureDetectorCompat = GestureDetectorCompat(context, this).apply { setOnDoubleTapListener(this@ScalableImageView) }
The parameter of setOnDoubleTapListener is to pass in a listener, so let's fill in the view and let the view rewrite the method of the interface GestureDetector.OnDoubleTapListener
Override method:
override fun onSingleTapConfirmed(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") }
simplify:
Tracking the source code of the constructor of GestureDetectorCompat, we can see that it helps us determine the type of listener internally. If it is the implementation class of gesturedetector.ondoubletaplistener, it will help us execute the setOnDoubleTapListener method. We don't need to configure it, so the code can be simplified as follows:
private val gestureDetectorCompat = GestureDetectorCompat(context, this)
③ Double click to make it bigger and smaller
effect:
④ Override onDoubleTap
The purpose is to double-click to enlarge and shrink the picture, so we make an animation to control the size of the picture
private var scaleFraction = 0f set(value) { field = value invalidate() } private val animator by lazy { ObjectAnimator.ofFloat(this, "scaleFraction", 0f, 1f) }
In ondraw, get the scale coefficient to be scaled to achieve from small picture to large picture
val scale = smallScale + (bigScale - smallScale) * scaleFraction scale(scale, scale, width / 2f, height / 2f) drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
Therefore, in the method of double clicking, the animation is played normally from small to large, and the animation is played reversely from large to small
override fun onDoubleTap(e: MotionEvent?): Boolean { if (isBig) { //It was originally a big picture, so it grew from small to large animator.reverse() } else { animator.start() } isBig = !isBig return true }
④ Realize inertial sliding
The core is to rewrite the onFling method
Create an OverScroller object
private val overScroller = OverScroller(context)
Function of OverScroller: control the inertial sliding of a point within a certain range
As shown in the figure
So how to control the inertial movement of a picture?
If the movement of the picture in the large frame is equivalent to the movement of the touch point in the small frame, and the displacement of the touch point on the X and Y axes is synchronized to the offset of the picture on the xy axis, the inertia of the picture can be realized
To put it bluntly, if you control your touch point within a small box, you can control the picture within a large box
Implementation steps:
① : define two variables as the offset of the small circle moving on the x axis and the offset of the small circle moving on the y axis
private var offsetX = 0f private var offsetY = 0f
② : override onFling method
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 }
The first parameter and the second parameter: the position when the finger clicks down
The third and fourth parameter: the speed in two directions when the finger slides hard
The fifth and sixth parameter: the range of the small box surrounding the touch point
The last two parameters refer to the range of recovery when sliding out of the small box boundary
The effect shown in the figure below
Summary: synchronize the displacement of the touch point to the picture
③ Smooth inertial sliding
Let the view implement the Runnable interface, rewrite the run method, and update the slider by frame
override fun run() { //As long as the inertia is not over, return to update the picture position if (overScroller.computeScrollOffset()) { offsetX = overScroller.currX.toFloat() offsetY = overScroller.currY.toFloat() invalidate() postOnAnimation(this) } }
④ Using the run method in onFling
postOnAnimation(this)
summary
A gesture detector is used to detect some additional gestures, such as double clicking, inertial sliding, etc., and then the abstract method of gesture detector is rewritten to realize the feedback of these gestures
Complete code
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() { //As long as the inertia is not over, return to update the picture position if (overScroller.computeScrollOffset()) { offsetX = overScroller.currX.toFloat() offsetY = overScroller.currY.toFloat() invalidate() postOnAnimation(this) } } override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { return false } override fun onDoubleTap(e: MotionEvent?): Boolean { if (isBig) { //It was originally a big picture, so it grew from small to large animator.reverse() } else { animator.start() } isBig = !isBig return true } override fun onDoubleTapEvent(e: MotionEvent?): Boolean { return false } }