package se.ajpanton.notificationlog import android.content.Context import android.graphics.Bitmap import android.graphics.Matrix import android.graphics.RectF import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector import androidx.appcompat.widget.AppCompatImageView /** A fit-to-screen image which supports pinch zoom and one-finger panning. */ class ZoomImageView @JvmOverloads constructor( context: Context, attributes: AttributeSet? = null, ) : AppCompatImageView(context, attributes) { var onSingleTap: (() -> Unit)? = null private val transform = Matrix() private val bounds = RectF() private var minimumScale = 1f private var currentScale = 1f private var lastX = 0f private var lastY = 0f private var dragging = false private var lastFocusX = 0f private var lastFocusY = 0f private var pointerCount = 0 private val scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { lastFocusX = detector.focusX lastFocusY = detector.focusY return true } override fun onScale(detector: ScaleGestureDetector): Boolean { // Focus movement while two fingers are down is two-finger panning. // Quick scale has one pointer, so it remains pure zoom around its // built-in double-tap anchor. setScaleAround((currentScale * detector.scaleFactor).coerceIn(minimumScale, minimumScale * MAX_ZOOM), detector.focusX, detector.focusY) if (pointerCount >= 2) { transform.postTranslate(detector.focusX - lastFocusX, detector.focusY - lastFocusY) } lastFocusX = detector.focusX lastFocusY = detector.focusY constrain() return true } }) private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onSingleTapConfirmed(event: MotionEvent): Boolean { onSingleTap?.invoke() return true } }) init { scaleType = ScaleType.MATRIX scaleDetector.isQuickScaleEnabled = true } override fun setImageBitmap(bitmap: Bitmap?) { super.setImageBitmap(bitmap) post(::resetToFit) } override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { super.onSizeChanged(width, height, oldWidth, oldHeight) resetToFit() } override fun onTouchEvent(event: MotionEvent): Boolean { pointerCount = event.pointerCount scaleDetector.onTouchEvent(event) gestureDetector.onTouchEvent(event) when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { lastX = event.x lastY = event.y dragging = false } MotionEvent.ACTION_MOVE -> if (event.pointerCount == 1 && !scaleDetector.isInProgress) { val dx = event.x - lastX val dy = event.y - lastY if (dx != 0f || dy != 0f) { dragging = true transform.postTranslate(dx, dy) constrain() } lastX = event.x lastY = event.y } MotionEvent.ACTION_POINTER_UP -> { val remainingPointer = if (event.actionIndex == 0) 1 else 0 lastX = event.getX(remainingPointer) lastY = event.getY(remainingPointer) } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { if (dragging) performClick() } } return true } override fun performClick(): Boolean = super.performClick() private fun setScaleAround(scale: Float, pivotX: Float, pivotY: Float) { transform.postScale(scale / currentScale, scale / currentScale, pivotX, pivotY) currentScale = scale } private fun resetToFit() { val drawable = drawable ?: return if (width == 0 || height == 0 || drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) return minimumScale = minOf(width.toFloat() / drawable.intrinsicWidth, height.toFloat() / drawable.intrinsicHeight) currentScale = minimumScale transform.reset() transform.postScale(minimumScale, minimumScale) transform.postTranslate( (width - drawable.intrinsicWidth * minimumScale) / 2f, (height - drawable.intrinsicHeight * minimumScale) / 2f, ) imageMatrix = transform } private fun constrain() { val drawable = drawable ?: return bounds.set(0f, 0f, drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat()) transform.mapRect(bounds) val dx = when { bounds.width() <= width -> width / 2f - bounds.centerX() bounds.left > 0f -> -bounds.left bounds.right < width -> width - bounds.right else -> 0f } val dy = when { bounds.height() <= height -> height / 2f - bounds.centerY() bounds.top > 0f -> -bounds.top bounds.bottom < height -> height - bounds.bottom else -> 0f } transform.postTranslate(dx, dy) imageMatrix = transform } private companion object { const val MAX_ZOOM = 6f } }