152 lines
5.5 KiB
Kotlin
152 lines
5.5 KiB
Kotlin
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
|
|
}
|
|
}
|