diff --git a/app/src/main/java/se/ajpanton/notificationlog/ImageViewerDialogFragment.kt b/app/src/main/java/se/ajpanton/notificationlog/ImageViewerDialogFragment.kt index 3c1c5a0..ecca03a 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/ImageViewerDialogFragment.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/ImageViewerDialogFragment.kt @@ -10,6 +10,7 @@ import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.Window +import android.view.WindowManager import android.widget.FrameLayout import android.widget.ProgressBar import android.widget.TextView @@ -48,7 +49,15 @@ class ImageViewerDialogFragment : DialogFragment() { setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) setBackgroundDrawable(ColorDrawable(Color.BLACK)) WindowCompat.setDecorFitsSystemWindows(this, false) - addFlags(android.view.WindowManager.LayoutParams.FLAG_SECURE) + statusBarColor = Color.TRANSPARENT + navigationBarColor = Color.TRANSPARENT + decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + attributes = attributes.apply { + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + } + addFlags(WindowManager.LayoutParams.FLAG_SECURE) } hideSystemControls() loadImage(requireArguments().getString(ARGUMENT_IMAGE_ID)!!) diff --git a/app/src/main/java/se/ajpanton/notificationlog/ZoomImageView.kt b/app/src/main/java/se/ajpanton/notificationlog/ZoomImageView.kt index 9f4b171..df1fb6b 100644 --- a/app/src/main/java/se/ajpanton/notificationlog/ZoomImageView.kt +++ b/app/src/main/java/se/ajpanton/notificationlog/ZoomImageView.kt @@ -8,7 +8,10 @@ import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent import android.view.ScaleGestureDetector +import android.view.ViewConfiguration import androidx.appcompat.widget.AppCompatImageView +import kotlin.math.abs +import kotlin.math.exp /** A fit-to-screen image which supports pinch zoom and one-finger panning. */ class ZoomImageView @JvmOverloads constructor( @@ -24,11 +27,29 @@ class ZoomImageView @JvmOverloads constructor( private var lastX = 0f private var lastY = 0f private var dragging = false + private var lastFocusX = 0f + private var lastFocusY = 0f + private var doubleTapZooming = false + private var doubleTapMoved = false + private var doubleTapX = 0f + private var doubleTapY = 0f + private var doubleTapStartY = 0f + private var doubleTapBaseScale = 1f + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop 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 { - val scale = (currentScale * detector.scaleFactor).coerceIn(minimumScale, minimumScale * MAX_ZOOM) - transform.postScale(scale / currentScale, scale / currentScale, detector.focusX, detector.focusY) - currentScale = scale + // A ScaleGestureDetector reports focus movement even when the two + // fingers stay the same distance apart, which is two-finger panning. + setScaleAround((currentScale * detector.scaleFactor).coerceIn(minimumScale, minimumScale * MAX_ZOOM), detector.focusX, detector.focusY) + transform.postTranslate(detector.focusX - lastFocusX, detector.focusY - lastFocusY) + lastFocusX = detector.focusX + lastFocusY = detector.focusY constrain() return true } @@ -39,11 +60,36 @@ class ZoomImageView @JvmOverloads constructor( return true } - override fun onDoubleTap(event: MotionEvent): Boolean { - val target = if (currentScale > minimumScale * 1.1f) minimumScale else minimumScale * DOUBLE_TAP_ZOOM - transform.postScale(target / currentScale, target / currentScale, event.x, event.y) - currentScale = target - constrain() + override fun onDoubleTap(event: MotionEvent): Boolean = true + + override fun onDoubleTapEvent(event: MotionEvent): Boolean { + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + doubleTapZooming = true + doubleTapMoved = false + doubleTapX = event.x + doubleTapY = event.y + doubleTapStartY = event.y + doubleTapBaseScale = currentScale + } + MotionEvent.ACTION_MOVE -> if (doubleTapZooming) { + val distance = doubleTapStartY - event.y + if (abs(distance) > touchSlop) doubleTapMoved = true + val target = (doubleTapBaseScale * exp((distance / DOUBLE_TAP_DRAG_DISTANCE).toDouble()).toFloat()) + .coerceIn(minimumScale, minimumScale * MAX_ZOOM) + setScaleAround(target, doubleTapX, doubleTapY) + constrain() + } + MotionEvent.ACTION_UP -> if (doubleTapZooming) { + if (!doubleTapMoved) { + val target = if (currentScale > minimumScale * 1.1f) minimumScale else minimumScale * DOUBLE_TAP_ZOOM + setScaleAround(target, doubleTapX, doubleTapY) + constrain() + } + doubleTapZooming = false + } + MotionEvent.ACTION_CANCEL -> doubleTapZooming = false + } return true } }) @@ -71,7 +117,7 @@ class ZoomImageView @JvmOverloads constructor( lastY = event.y dragging = false } - MotionEvent.ACTION_MOVE -> if (!scaleDetector.isInProgress) { + MotionEvent.ACTION_MOVE -> if (event.pointerCount == 1 && !scaleDetector.isInProgress && !doubleTapZooming) { val dx = event.x - lastX val dy = event.y - lastY if (dx != 0f || dy != 0f) { @@ -82,6 +128,11 @@ class ZoomImageView @JvmOverloads constructor( 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() } @@ -91,6 +142,11 @@ class ZoomImageView @JvmOverloads constructor( 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 @@ -128,5 +184,6 @@ class ZoomImageView @JvmOverloads constructor( private companion object { const val MAX_ZOOM = 6f const val DOUBLE_TAP_ZOOM = 2.5f + const val DOUBLE_TAP_DRAG_DISTANCE = 280f } }