package com.github.libretube.ui.views import android.content.Context import android.graphics.Bitmap import android.graphics.Matrix import android.graphics.PointF import android.util.AttributeSet import android.view.MotionEvent import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener import androidx.appcompat.widget.AppCompatImageView import kotlin.math.abs import kotlin.math.roundToInt /** * An image view that allows zooming the image inside * credits: https://stackoverflow.com/posts/6650473 */ class ZoomableImageView(context: Context, attr: AttributeSet?) : AppCompatImageView(context, attr) { var bitmapMatrix = Matrix() var mode = NONE var last = PointF() var start = PointF() var minScale = 1f var maxScale = 4f var m: FloatArray var redundantXSpace = 0f var redundantYSpace = 0f var width = 0f var height = 0f var saveScale = 1f var right = 0f var bottom = 0f var origWidth = 0f var origHeight = 0f private var bmWidth = 0f private var bmHeight = 0f private var mScaleDetector: ScaleGestureDetector init { super.setClickable(true) mScaleDetector = ScaleGestureDetector(context, ScaleListener()) bitmapMatrix.setTranslate(1f, 1f) m = FloatArray(9) imageMatrix = bitmapMatrix scaleType = ScaleType.MATRIX setOnTouchListener { _, event -> mScaleDetector.onTouchEvent(event) bitmapMatrix.getValues(m) val x = m[Matrix.MTRANS_X] val y = m[Matrix.MTRANS_Y] val curr = PointF(event.x, event.y) when (event.action) { MotionEvent.ACTION_DOWN -> { last[event.x] = event.y start.set(last) mode = DRAG } MotionEvent.ACTION_POINTER_DOWN -> { last[event.x] = event.y start.set(last) mode = ZOOM } MotionEvent.ACTION_MOVE -> // if the mode is ZOOM or // if the mode is DRAG and already zoomed if (mode == ZOOM || mode == DRAG && saveScale > minScale) { var deltaX = curr.x - last.x // x difference var deltaY = curr.y - last.y // y difference val scaleWidth = (origWidth * saveScale).roundToInt() .toFloat() // width after applying current scale val scaleHeight = (origHeight * saveScale).roundToInt() .toFloat() // height after applying current scale // if scaleWidth is smaller than the views width // in other words if the image width fits in the view // limit left and right movement if (scaleWidth < width) { deltaX = 0f if (y + deltaY > 0) { deltaY = -y } else if (y + deltaY < -bottom) { deltaY = -(y + bottom) } } else if (scaleHeight < height) { deltaY = 0f if (x + deltaX > 0) { deltaX = -x } else if (x + deltaX < -right) { deltaX = -(x + right) } } else { if (x + deltaX > 0) { deltaX = -x } else if (x + deltaX < -right) { deltaX = -(x + right) } if (y + deltaY > 0) { deltaY = -y } else if (y + deltaY < -bottom) { deltaY = -(y + bottom) } } // move the image with the matrix bitmapMatrix.postTranslate(deltaX, deltaY) // set the last touch location to the current last[curr.x] = curr.y } MotionEvent.ACTION_UP -> { mode = NONE val xDiff = abs(curr.x - start.x).toInt() val yDiff = abs(curr.y - start.y).toInt() if (xDiff < CLICK && yDiff < CLICK) performClick() } MotionEvent.ACTION_POINTER_UP -> mode = NONE } imageMatrix = bitmapMatrix invalidate() true } } override fun setImageBitmap(bm: Bitmap) { super.setImageBitmap(bm) bmWidth = bm.width.toFloat() bmHeight = bm.height.toFloat() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) width = MeasureSpec.getSize(widthMeasureSpec).toFloat() height = MeasureSpec.getSize(heightMeasureSpec).toFloat() // Fit to screen. val scaleX = width / bmWidth val scaleY = height / bmHeight val scale = scaleX.coerceAtMost(scaleY) bitmapMatrix.setScale(scale, scale) imageMatrix = bitmapMatrix saveScale = 1f // Center the image redundantYSpace = height - scale * bmHeight redundantXSpace = width - scale * bmWidth redundantYSpace /= 2f redundantXSpace /= 2f bitmapMatrix.postTranslate(redundantXSpace, redundantYSpace) origWidth = width - 2 * redundantXSpace origHeight = height - 2 * redundantYSpace right = width * saveScale - width - 2 * redundantXSpace * saveScale bottom = height * saveScale - height - 2 * redundantYSpace * saveScale imageMatrix = bitmapMatrix } private inner class ScaleListener : SimpleOnScaleGestureListener() { override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { mode = ZOOM return true } override fun onScale(detector: ScaleGestureDetector): Boolean { var mScaleFactor = detector.scaleFactor val origScale = saveScale saveScale *= mScaleFactor if (saveScale > maxScale) { saveScale = maxScale mScaleFactor = maxScale / origScale } else if (saveScale < minScale) { saveScale = minScale mScaleFactor = minScale / origScale } right = width * saveScale - width - 2 * redundantXSpace * saveScale bottom = height * saveScale - height - 2 * redundantYSpace * saveScale if (origWidth * saveScale <= width || origHeight * saveScale <= height) { bitmapMatrix.postScale(mScaleFactor, mScaleFactor, width / 2, height / 2) if (mScaleFactor < 1) { bitmapMatrix.getValues(m) val x = m[Matrix.MTRANS_X] val y = m[Matrix.MTRANS_Y] if ((origWidth * saveScale).roundToInt() < width) { if (y < -bottom) { bitmapMatrix.postTranslate( 0f, -(y + bottom) ) } else if (y > 0) bitmapMatrix.postTranslate(0f, -y) } else { if (x < -right) { bitmapMatrix.postTranslate( -(x + right), 0f ) } else if (x > 0) bitmapMatrix.postTranslate(-x, 0f) } } } else { bitmapMatrix.postScale(mScaleFactor, mScaleFactor, detector.focusX, detector.focusY) bitmapMatrix.getValues(m) val x = m[Matrix.MTRANS_X] val y = m[Matrix.MTRANS_Y] if (mScaleFactor < 1) { if (x < -right) { bitmapMatrix.postTranslate( -(x + right), 0f ) } else if (x > 0) bitmapMatrix.postTranslate(-x, 0f) if (y < -bottom) { bitmapMatrix.postTranslate( 0f, -(y + bottom) ) } else if (y > 0) bitmapMatrix.postTranslate(0f, -y) } } return true } } companion object { const val NONE = 0 const val DRAG = 1 const val ZOOM = 2 const val CLICK = 3 } }