Improve full-screen image viewer gestures
This commit is contained in:
@@ -10,6 +10,7 @@ import android.view.Gravity
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -48,7 +49,15 @@ class ImageViewerDialogFragment : DialogFragment() {
|
|||||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
setBackgroundDrawable(ColorDrawable(Color.BLACK))
|
setBackgroundDrawable(ColorDrawable(Color.BLACK))
|
||||||
WindowCompat.setDecorFitsSystemWindows(this, false)
|
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()
|
hideSystemControls()
|
||||||
loadImage(requireArguments().getString(ARGUMENT_IMAGE_ID)!!)
|
loadImage(requireArguments().getString(ARGUMENT_IMAGE_ID)!!)
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import android.util.AttributeSet
|
|||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.ScaleGestureDetector
|
import android.view.ScaleGestureDetector
|
||||||
|
import android.view.ViewConfiguration
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
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. */
|
/** A fit-to-screen image which supports pinch zoom and one-finger panning. */
|
||||||
class ZoomImageView @JvmOverloads constructor(
|
class ZoomImageView @JvmOverloads constructor(
|
||||||
@@ -24,11 +27,29 @@ class ZoomImageView @JvmOverloads constructor(
|
|||||||
private var lastX = 0f
|
private var lastX = 0f
|
||||||
private var lastY = 0f
|
private var lastY = 0f
|
||||||
private var dragging = false
|
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() {
|
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 {
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
val scale = (currentScale * detector.scaleFactor).coerceIn(minimumScale, minimumScale * MAX_ZOOM)
|
// A ScaleGestureDetector reports focus movement even when the two
|
||||||
transform.postScale(scale / currentScale, scale / currentScale, detector.focusX, detector.focusY)
|
// fingers stay the same distance apart, which is two-finger panning.
|
||||||
currentScale = scale
|
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()
|
constrain()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -39,11 +60,36 @@ class ZoomImageView @JvmOverloads constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDoubleTap(event: MotionEvent): Boolean {
|
override fun onDoubleTap(event: MotionEvent): Boolean = true
|
||||||
val target = if (currentScale > minimumScale * 1.1f) minimumScale else minimumScale * DOUBLE_TAP_ZOOM
|
|
||||||
transform.postScale(target / currentScale, target / currentScale, event.x, event.y)
|
override fun onDoubleTapEvent(event: MotionEvent): Boolean {
|
||||||
currentScale = target
|
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()
|
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
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -71,7 +117,7 @@ class ZoomImageView @JvmOverloads constructor(
|
|||||||
lastY = event.y
|
lastY = event.y
|
||||||
dragging = false
|
dragging = false
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> if (!scaleDetector.isInProgress) {
|
MotionEvent.ACTION_MOVE -> if (event.pointerCount == 1 && !scaleDetector.isInProgress && !doubleTapZooming) {
|
||||||
val dx = event.x - lastX
|
val dx = event.x - lastX
|
||||||
val dy = event.y - lastY
|
val dy = event.y - lastY
|
||||||
if (dx != 0f || dy != 0f) {
|
if (dx != 0f || dy != 0f) {
|
||||||
@@ -82,6 +128,11 @@ class ZoomImageView @JvmOverloads constructor(
|
|||||||
lastX = event.x
|
lastX = event.x
|
||||||
lastY = event.y
|
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 -> {
|
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
if (dragging) performClick()
|
if (dragging) performClick()
|
||||||
}
|
}
|
||||||
@@ -91,6 +142,11 @@ class ZoomImageView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun performClick(): Boolean = super.performClick()
|
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() {
|
private fun resetToFit() {
|
||||||
val drawable = drawable ?: return
|
val drawable = drawable ?: return
|
||||||
if (width == 0 || height == 0 || drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) return
|
if (width == 0 || height == 0 || drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) return
|
||||||
@@ -128,5 +184,6 @@ class ZoomImageView @JvmOverloads constructor(
|
|||||||
private companion object {
|
private companion object {
|
||||||
const val MAX_ZOOM = 6f
|
const val MAX_ZOOM = 6f
|
||||||
const val DOUBLE_TAP_ZOOM = 2.5f
|
const val DOUBLE_TAP_ZOOM = 2.5f
|
||||||
|
const val DOUBLE_TAP_DRAG_DISTANCE = 280f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user