Created
June 22, 2020 21:32
-
-
Save Nataland/c5cf491aec0534041b3d93aa42eef9b7 to your computer and use it in GitHub Desktop.
Put this outside of a drawing view
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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