Skip to content

Instantly share code, notes, and snippets.

@Nataland
Created June 22, 2020 21:32
Show Gist options
  • Save Nataland/c5cf491aec0534041b3d93aa42eef9b7 to your computer and use it in GitHub Desktop.
Save Nataland/c5cf491aec0534041b3d93aa42eef9b7 to your computer and use it in GitHub Desktop.
Put this outside of a drawing view
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.ScaleGestureDetector.OnScaleGestureListener
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
import android.widget.FrameLayout
import androidx.core.view.GestureDetectorCompat
class ZoomableLayout(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
companion object {
private const val MIN_ZOOM = 1.0f
private const val MAX_ZOOM = 4.0f
}
var isZoomAndPanEnabled: Boolean = false
private val zoomMatrix = Matrix()
private val inverseMatrix = Matrix()
private val savedMatrix = Matrix()
private var scale = 1f
private val start = PointF()
private val mid = PointF()
private var oldDistance = 1f
private var distanceX = 0f
private var distanceY = 0f
private var contentSize: RectF? = null
private var mDispatchTouchEventWorkingArray = FloatArray(2)
private val mScaleGestureListener: OnScaleGestureListener = object : SimpleOnScaleGestureListener() {
override fun onScaleBegin(scaleGestureDetector: ScaleGestureDetector): Boolean {
oldDistance = scaleGestureDetector.currentSpan
if (oldDistance > 10f) {
savedMatrix.set(zoomMatrix)
mid[scaleGestureDetector.focusX] = scaleGestureDetector.focusY
}
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) {}
override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean {
scale = scaleGestureDetector.scaleFactor
return true
}
}
private val mGestureListener: SimpleOnGestureListener = object : SimpleOnGestureListener() {
override fun onDown(event: MotionEvent): Boolean {
savedMatrix.set(zoomMatrix)
start[event.x] = event.y
return true
}
override fun onScroll(e1: MotionEvent, e2: MotionEvent, dX: Float, dY: Float): Boolean {
setupTranslation(dX, dY)
zoomMatrix.postTranslate(distanceX, distanceY)
return true
}
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
return true
}
}
private var mScaleGestureDetector: ScaleGestureDetector = ScaleGestureDetector(context, mScaleGestureListener)
private var mGestureDetector: GestureDetectorCompat = GestureDetectorCompat(context, mGestureListener)
fun reset(rect: RectF) {
zoomMatrix.reset()
inverseMatrix.reset()
savedMatrix.reset()
start.x = 0f
start.y = 0f
mid.x = 0f
mid.y = 0f
oldDistance = 1f
distanceX = 0f
distanceY = 0f
contentSize = rect
mDispatchTouchEventWorkingArray = FloatArray(2)
scale = 1f
}
override fun dispatchDraw(canvas: Canvas) {
val values = FloatArray(9)
zoomMatrix.getValues(values)
canvas.save()
canvas.translate(values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y])
canvas.scale(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y])
super.dispatchDraw(canvas)
canvas.restore()
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (isZoomAndPanEnabled) {
return super.onInterceptTouchEvent(ev)
}
mDispatchTouchEventWorkingArray[0] = ev.x
mDispatchTouchEventWorkingArray[1] = ev.y
mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray)
ev.setLocation(mDispatchTouchEventWorkingArray[0], mDispatchTouchEventWorkingArray[1])
return false
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
zoomMatrix.set(savedMatrix)
var gestureDetected = mGestureDetector.onTouchEvent(event)
if (event.pointerCount > 1) {
gestureDetected = mScaleGestureDetector.onTouchEvent(event) or gestureDetected
if (checkScaleBounds()) {
zoomMatrix.postScale(scale, scale, mid.x, mid.y)
}
}
zoomMatrix.invert(inverseMatrix)
savedMatrix.set(zoomMatrix)
invalidate()
return gestureDetected
}
private fun checkScaleBounds(): Boolean {
val values = FloatArray(9)
zoomMatrix.getValues(values)
val sx = values[Matrix.MSCALE_X] * scale
val sy = values[Matrix.MSCALE_Y] * scale
return sx > MIN_ZOOM && sx < MAX_ZOOM && sy > MIN_ZOOM && sy < MAX_ZOOM
}
private fun screenPointsToScaledPoints(a: FloatArray): FloatArray {
inverseMatrix.mapPoints(a)
return a
}
private fun setupTranslation(dX: Float, dY: Float) {
distanceX = -1 * dX
distanceY = -1 * dY
contentSize?.run {
val values = FloatArray(9)
zoomMatrix.getValues(values)
val totX = values[Matrix.MTRANS_X] + distanceX
val totY = values[Matrix.MTRANS_Y] + distanceY
val sx = values[Matrix.MSCALE_X]
val viewableRect = Rect()
getDrawingRect(viewableRect)
val offscreenWidth = width() - (viewableRect.right - viewableRect.left)
val offscreenHeight = height() - (viewableRect.bottom - viewableRect.top)
val maxDx = (width() - width() / sx) * sx
val maxDy = (height() - height() / sx) * sx
if (totX > 0 && distanceX > 0) {
distanceX = 0f
}
if (totY > 0 && distanceY > 0) {
distanceY = 0f
}
if (totX * -1 > offscreenWidth + maxDx && distanceX < 0) {
distanceX = 0f
}
if (totY * -1 > offscreenHeight + maxDy && distanceY < 0) {
distanceY = 0f
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment