diff --git a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt index 3b72982f1e..de65e317a3 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt @@ -272,7 +272,7 @@ class ZoomableActivity : BaseActivity() { * Handles down swipe action */ private fun onDownSwiped() { - if (binding.zoomable.zoomableController?.isIdentity == false) { + if (!binding.zoomable.getZoomableController().isIdentity()) { return } @@ -342,7 +342,7 @@ class ZoomableActivity : BaseActivity() { * Handles up swipe action */ private fun onUpSwiped() { - if (binding.zoomable.zoomableController?.isIdentity == false) { + if (!binding.zoomable.getZoomableController().isIdentity()) { return } @@ -415,7 +415,7 @@ class ZoomableActivity : BaseActivity() { * Handles right swipe action */ private fun onRightSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable.zoomableController?.isIdentity == false) { + if (!binding.zoomable.getZoomableController().isIdentity()) { return } @@ -452,7 +452,7 @@ class ZoomableActivity : BaseActivity() { * Handles left swipe action */ private fun onLeftSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable.zoomableController?.isIdentity == false) { + if (!binding.zoomable.getZoomableController().isIdentity()) { return } diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.java deleted file mode 100644 index e91f47311e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.java +++ /dev/null @@ -1,256 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.gestures; - -import android.view.MotionEvent; - -/** - * Component that detects and tracks multiple pointers based on touch events. - * - * Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new - * one will be started (if there are still pressed pointers left). It is guaranteed that the number - * of pointers within the single gesture will remain the same during the whole gesture. - */ -public class MultiPointerGestureDetector { - - /** The listener for receiving notifications when gestures occur. */ - public interface Listener { - /** A callback called right before the gesture is about to start. */ - public void onGestureBegin(MultiPointerGestureDetector detector); - - /** A callback called each time the gesture gets updated. */ - public void onGestureUpdate(MultiPointerGestureDetector detector); - - /** A callback called right after the gesture has finished. */ - public void onGestureEnd(MultiPointerGestureDetector detector); - } - - private static final int MAX_POINTERS = 2; - - private boolean mGestureInProgress; - private int mPointerCount; - private int mNewPointerCount; - private final int mId[] = new int[MAX_POINTERS]; - private final float mStartX[] = new float[MAX_POINTERS]; - private final float mStartY[] = new float[MAX_POINTERS]; - private final float mCurrentX[] = new float[MAX_POINTERS]; - private final float mCurrentY[] = new float[MAX_POINTERS]; - - private Listener mListener = null; - - public MultiPointerGestureDetector() { - reset(); - } - - /** Factory method that creates a new instance of MultiPointerGestureDetector */ - public static MultiPointerGestureDetector newInstance() { - return new MultiPointerGestureDetector(); - } - - /** - * Sets the listener. - * - * @param listener listener to set - */ - public void setListener(Listener listener) { - mListener = listener; - } - - /** Resets the component to the initial state. */ - public void reset() { - mGestureInProgress = false; - mPointerCount = 0; - for (int i = 0; i < MAX_POINTERS; i++) { - mId[i] = MotionEvent.INVALID_POINTER_ID; - } - } - - /** - * This method can be overridden in order to perform threshold check or something similar. - * - * @return whether or not to start a new gesture - */ - protected boolean shouldStartGesture() { - return true; - } - - /** Starts a new gesture and calls the listener just before starting it. */ - private void startGesture() { - if (!mGestureInProgress) { - if (mListener != null) { - mListener.onGestureBegin(this); - } - mGestureInProgress = true; - } - } - - /** Stops the current gesture and calls the listener right after stopping it. */ - private void stopGesture() { - if (mGestureInProgress) { - mGestureInProgress = false; - if (mListener != null) { - mListener.onGestureEnd(this); - } - } - } - - /** - * Gets the index of the i-th pressed pointer. Normally, the index will be equal to i, except in - * the case when the pointer is released. - * - * @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down) - */ - private int getPressedPointerIndex(MotionEvent event, int i) { - final int count = event.getPointerCount(); - final int action = event.getActionMasked(); - final int index = event.getActionIndex(); - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { - if (i >= index) { - i++; - } - } - return (i < count) ? i : -1; - } - - /** Gets the number of pressed pointers (fingers down). */ - private static int getPressedPointerCount(MotionEvent event) { - int count = event.getPointerCount(); - int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { - count--; - } - return count; - } - - private void updatePointersOnTap(MotionEvent event) { - mPointerCount = 0; - for (int i = 0; i < MAX_POINTERS; i++) { - int index = getPressedPointerIndex(event, i); - if (index == -1) { - mId[i] = MotionEvent.INVALID_POINTER_ID; - } else { - mId[i] = event.getPointerId(index); - mCurrentX[i] = mStartX[i] = event.getX(index); - mCurrentY[i] = mStartY[i] = event.getY(index); - mPointerCount++; - } - } - } - - private void updatePointersOnMove(MotionEvent event) { - for (int i = 0; i < MAX_POINTERS; i++) { - int index = event.findPointerIndex(mId[i]); - if (index != -1) { - mCurrentX[i] = event.getX(index); - mCurrentY[i] = event.getY(index); - } - } - } - - /** - * Handles the given motion event. - * - * @param event event to handle - * @return whether or not the event was handled - */ - public boolean onTouchEvent(final MotionEvent event) { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_MOVE: - { - // update pointers - updatePointersOnMove(event); - // start a new gesture if not already started - if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) { - startGesture(); - } - // notify listener - if (mGestureInProgress && mListener != null) { - mListener.onGestureUpdate(this); - } - break; - } - - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: - { - // restart gesture whenever the number of pointers changes - mNewPointerCount = getPressedPointerCount(event); - stopGesture(); - updatePointersOnTap(event); - if (mPointerCount > 0 && shouldStartGesture()) { - startGesture(); - } - break; - } - - case MotionEvent.ACTION_CANCEL: - { - mNewPointerCount = 0; - stopGesture(); - reset(); - break; - } - } - return true; - } - - /** Restarts the current gesture (if any). */ - public void restartGesture() { - if (!mGestureInProgress) { - return; - } - stopGesture(); - for (int i = 0; i < MAX_POINTERS; i++) { - mStartX[i] = mCurrentX[i]; - mStartY[i] = mCurrentY[i]; - } - startGesture(); - } - - /** Gets whether there is a gesture in progress */ - public boolean isGestureInProgress() { - return mGestureInProgress; - } - - /** Gets the number of pointers after the current gesture */ - public int getNewPointerCount() { - return mNewPointerCount; - } - - /** Gets the number of pointers in the current gesture */ - public int getPointerCount() { - return mPointerCount; - } - - /** - * Gets the start X coordinates for the all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - public float[] getStartX() { - return mStartX; - } - - /** - * Gets the start Y coordinates for the all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - public float[] getStartY() { - return mStartY; - } - - /** - * Gets the current X coordinates for the all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - public float[] getCurrentX() { - return mCurrentX; - } - - /** - * Gets the current Y coordinates for the all pointers Mutable array is exposed for performance - * reasons and is not to be modified by the callers. - */ - public float[] getCurrentY() { - return mCurrentY; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.kt new file mode 100644 index 0000000000..84dccfc073 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.kt @@ -0,0 +1,252 @@ +package fr.free.nrw.commons.media.zoomControllers.gestures + +import android.view.MotionEvent + +/** + * Component that detects and tracks multiple pointers based on touch events. + * + * Each time a pointer gets pressed or released, the current gesture (if any) will end, and a new + * one will be started (if there are still pressed pointers left). It is guaranteed that the number + * of pointers within the single gesture will remain the same during the whole gesture. + */ +open class MultiPointerGestureDetector { + + /** The listener for receiving notifications when gestures occur. */ + interface Listener { + /** A callback called right before the gesture is about to start. */ + fun onGestureBegin(detector: MultiPointerGestureDetector) + + /** A callback called each time the gesture gets updated. */ + fun onGestureUpdate(detector: MultiPointerGestureDetector) + + /** A callback called right after the gesture has finished. */ + fun onGestureEnd(detector: MultiPointerGestureDetector) + } + + companion object { + private const val MAX_POINTERS = 2 + + /** Factory method that creates a new instance of MultiPointerGestureDetector */ + fun newInstance(): MultiPointerGestureDetector { + return MultiPointerGestureDetector() + } + } + + private var mGestureInProgress = false + private var mPointerCount = 0 + private var mNewPointerCount = 0 + private val mId = IntArray(MAX_POINTERS) { MotionEvent.INVALID_POINTER_ID } + private val mStartX = FloatArray(MAX_POINTERS) + private val mStartY = FloatArray(MAX_POINTERS) + private val mCurrentX = FloatArray(MAX_POINTERS) + private val mCurrentY = FloatArray(MAX_POINTERS) + + private var mListener: Listener? = null + + init { + reset() + } + + /** + * Sets the listener. + * + * @param listener listener to set + */ + fun setListener(listener: Listener?) { + mListener = listener + } + + /** Resets the component to the initial state. */ + fun reset() { + mGestureInProgress = false + mPointerCount = 0 + for (i in 0 until MAX_POINTERS) { + mId[i] = MotionEvent.INVALID_POINTER_ID + } + } + + /** + * This method can be overridden in order to perform threshold check or something similar. + * + * @return whether or not to start a new gesture + */ + protected open fun shouldStartGesture(): Boolean { + return true + } + + /** Starts a new gesture and calls the listener just before starting it. */ + private fun startGesture() { + if (!mGestureInProgress) { + mListener?.onGestureBegin(this) + mGestureInProgress = true + } + } + + /** Stops the current gesture and calls the listener right after stopping it. */ + private fun stopGesture() { + if (mGestureInProgress) { + mGestureInProgress = false + mListener?.onGestureEnd(this) + } + } + + /** + * Gets the index of the i-th pressed pointer. Normally, the index will be equal to i, except in + * the case when the pointer is released. + * + * @return index of the specified pointer or -1 if not found (i.e. not enough pointers are down) + */ + private fun getPressedPointerIndex(event: MotionEvent, i: Int): Int { + val count = event.pointerCount + val action = event.actionMasked + val index = event.actionIndex + var adjustedIndex = i + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + if (adjustedIndex >= index) { + adjustedIndex++ + } + } + return if (adjustedIndex < count) adjustedIndex else -1 + } + + /** Gets the number of pressed pointers (fingers down). */ + private fun getPressedPointerCount(event: MotionEvent): Int { + var count = event.pointerCount + val action = event.actionMasked + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + count-- + } + return count + } + + private fun updatePointersOnTap(event: MotionEvent) { + mPointerCount = 0 + for (i in 0 until MAX_POINTERS) { + val index = getPressedPointerIndex(event, i) + if (index == -1) { + mId[i] = MotionEvent.INVALID_POINTER_ID + } else { + mId[i] = event.getPointerId(index) + mCurrentX[i] = event.getX(index) + mStartX[i] = mCurrentX[i] + mCurrentY[i] = event.getY(index) + mStartY[i] = mCurrentY[i] + mPointerCount++ + } + } + } + + private fun updatePointersOnMove(event: MotionEvent) { + for (i in 0 until MAX_POINTERS) { + val index = event.findPointerIndex(mId[i]) + if (index != -1) { + mCurrentX[i] = event.getX(index) + mCurrentY[i] = event.getY(index) + } + } + } + + /** + * Handles the given motion event. + * + * @param event event to handle + * @return whether or not the event was handled + */ + fun onTouchEvent(event: MotionEvent): Boolean { + when (event.actionMasked) { + MotionEvent.ACTION_MOVE -> { + // update pointers + updatePointersOnMove(event) + // start a new gesture if not already started + if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) { + startGesture() + } + // notify listener + if (mGestureInProgress) { + mListener?.onGestureUpdate(this) + } + } + + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN, + MotionEvent.ACTION_POINTER_UP, + MotionEvent.ACTION_UP -> { + // restart gesture whenever the number of pointers changes + mNewPointerCount = getPressedPointerCount(event) + stopGesture() + updatePointersOnTap(event) + if (mPointerCount > 0 && shouldStartGesture()) { + startGesture() + } + } + + MotionEvent.ACTION_CANCEL -> { + mNewPointerCount = 0 + stopGesture() + reset() + } + } + return true + } + + /** Restarts the current gesture (if any). */ + fun restartGesture() { + if (!mGestureInProgress) { + return + } + stopGesture() + for (i in 0 until MAX_POINTERS) { + mStartX[i] = mCurrentX[i] + mStartY[i] = mCurrentY[i] + } + startGesture() + } + + /** Gets whether there is a gesture in progress */ + fun isGestureInProgress(): Boolean { + return mGestureInProgress + } + + /** Gets the number of pointers after the current gesture */ + fun getNewPointerCount(): Int { + return mNewPointerCount + } + + /** Gets the number of pointers in the current gesture */ + fun getPointerCount(): Int { + return mPointerCount + } + + /** + * Gets the start X coordinates for all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + fun getStartX(): FloatArray { + return mStartX + } + + /** + * Gets the start Y coordinates for all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + fun getStartY(): FloatArray { + return mStartY + } + + /** + * Gets the current X coordinates for all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + fun getCurrentX(): FloatArray { + return mCurrentX + } + + /** + * Gets the current Y coordinates for all pointers Mutable array is exposed for performance + * reasons and is not to be modified by the callers. + */ + fun getCurrentY(): FloatArray { + return mCurrentY + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.java deleted file mode 100644 index 3a6baeba2e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.java +++ /dev/null @@ -1,164 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.gestures; - -import android.view.MotionEvent; - -/** - * Component that detects translation, scale and rotation based on touch events. - * - * This class notifies its listeners whenever a gesture begins, updates or ends. The instance of - * this detector is passed to the listeners, so it can be queried for pivot, translation, scale or - * rotation. - */ -public class TransformGestureDetector implements MultiPointerGestureDetector.Listener { - - /** The listener for receiving notifications when gestures occur. */ - public interface Listener { - /** A callback called right before the gesture is about to start. */ - public void onGestureBegin(TransformGestureDetector detector); - - /** A callback called each time the gesture gets updated. */ - public void onGestureUpdate(TransformGestureDetector detector); - - /** A callback called right after the gesture has finished. */ - public void onGestureEnd(TransformGestureDetector detector); - } - - private final MultiPointerGestureDetector mDetector; - - private Listener mListener = null; - - public TransformGestureDetector(MultiPointerGestureDetector multiPointerGestureDetector) { - mDetector = multiPointerGestureDetector; - mDetector.setListener(this); - } - - /** Factory method that creates a new instance of TransformGestureDetector */ - public static TransformGestureDetector newInstance() { - return new TransformGestureDetector(MultiPointerGestureDetector.newInstance()); - } - - /** - * Sets the listener. - * - * @param listener listener to set - */ - public void setListener(Listener listener) { - mListener = listener; - } - - /** Resets the component to the initial state. */ - public void reset() { - mDetector.reset(); - } - - /** - * Handles the given motion event. - * - * @param event event to handle - * @return whether or not the event was handled - */ - public boolean onTouchEvent(final MotionEvent event) { - return mDetector.onTouchEvent(event); - } - - @Override - public void onGestureBegin(MultiPointerGestureDetector detector) { - if (mListener != null) { - mListener.onGestureBegin(this); - } - } - - @Override - public void onGestureUpdate(MultiPointerGestureDetector detector) { - if (mListener != null) { - mListener.onGestureUpdate(this); - } - } - - @Override - public void onGestureEnd(MultiPointerGestureDetector detector) { - if (mListener != null) { - mListener.onGestureEnd(this); - } - } - - private float calcAverage(float[] arr, int len) { - float sum = 0; - for (int i = 0; i < len; i++) { - sum += arr[i]; - } - return (len > 0) ? sum / len : 0; - } - - /** Restarts the current gesture (if any). */ - public void restartGesture() { - mDetector.restartGesture(); - } - - /** Gets whether there is a gesture in progress */ - public boolean isGestureInProgress() { - return mDetector.isGestureInProgress(); - } - - /** Gets the number of pointers after the current gesture */ - public int getNewPointerCount() { - return mDetector.getNewPointerCount(); - } - - /** Gets the number of pointers in the current gesture */ - public int getPointerCount() { - return mDetector.getPointerCount(); - } - - /** Gets the X coordinate of the pivot point */ - public float getPivotX() { - return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); - } - - /** Gets the Y coordinate of the pivot point */ - public float getPivotY() { - return calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); - } - - /** Gets the X component of the translation */ - public float getTranslationX() { - return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) - - calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); - } - - /** Gets the Y component of the translation */ - public float getTranslationY() { - return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) - - calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); - } - - /** Gets the scale */ - public float getScale() { - if (mDetector.getPointerCount() < 2) { - return 1; - } else { - float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; - float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; - float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; - float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; - float startDist = (float) Math.hypot(startDeltaX, startDeltaY); - float currentDist = (float) Math.hypot(currentDeltaX, currentDeltaY); - return currentDist / startDist; - } - } - - /** Gets the rotation in radians */ - public float getRotation() { - if (mDetector.getPointerCount() < 2) { - return 0; - } else { - float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; - float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; - float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; - float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; - float startAngle = (float) Math.atan2(startDeltaY, startDeltaX); - float currentAngle = (float) Math.atan2(currentDeltaY, currentDeltaX); - return currentAngle - startAngle; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.kt new file mode 100644 index 0000000000..9dd0b5813f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/TransformGestureDetector.kt @@ -0,0 +1,154 @@ +package fr.free.nrw.commons.media.zoomControllers.gestures + +import android.view.MotionEvent +import kotlin.math.atan2 +import kotlin.math.hypot + +/** + * Component that detects translation, scale and rotation based on touch events. + * + * This class notifies its listeners whenever a gesture begins, updates or ends. The instance of + * this detector is passed to the listeners, so it can be queried for pivot, translation, scale or + * rotation. + */ +class TransformGestureDetector(private val mDetector: MultiPointerGestureDetector) : + MultiPointerGestureDetector.Listener { + + /** The listener for receiving notifications when gestures occur. */ + interface Listener { + /** A callback called right before the gesture is about to start. */ + fun onGestureBegin(detector: TransformGestureDetector) + + /** A callback called each time the gesture gets updated. */ + fun onGestureUpdate(detector: TransformGestureDetector) + + /** A callback called right after the gesture has finished. */ + fun onGestureEnd(detector: TransformGestureDetector) + } + + private var mListener: Listener? = null + + init { + mDetector.setListener(this) + } + + /** Factory method that creates a new instance of TransformGestureDetector */ + companion object { + fun newInstance(): TransformGestureDetector { + return TransformGestureDetector(MultiPointerGestureDetector.newInstance()) + } + } + + /** + * Sets the listener. + * + * @param listener listener to set + */ + fun setListener(listener: Listener?) { + mListener = listener + } + + /** Resets the component to the initial state. */ + fun reset() { + mDetector.reset() + } + + /** + * Handles the given motion event. + * + * @param event event to handle + * @return whether or not the event was handled + */ + fun onTouchEvent(event: MotionEvent): Boolean { + return mDetector.onTouchEvent(event) + } + + override fun onGestureBegin(detector: MultiPointerGestureDetector) { + mListener?.onGestureBegin(this) + } + + override fun onGestureUpdate(detector: MultiPointerGestureDetector) { + mListener?.onGestureUpdate(this) + } + + override fun onGestureEnd(detector: MultiPointerGestureDetector) { + mListener?.onGestureEnd(this) + } + + private fun calcAverage(arr: FloatArray, len: Int): Float { + val sum = arr.take(len).sum() + return if (len > 0) sum / len else 0f + } + + /** Restarts the current gesture (if any). */ + fun restartGesture() { + mDetector.restartGesture() + } + + /** Gets whether there is a gesture in progress */ + fun isGestureInProgress(): Boolean { + return mDetector.isGestureInProgress() + } + + /** Gets the number of pointers after the current gesture */ + fun getNewPointerCount(): Int { + return mDetector.getNewPointerCount() + } + + /** Gets the number of pointers in the current gesture */ + fun getPointerCount(): Int { + return mDetector.getPointerCount() + } + + /** Gets the X coordinate of the pivot point */ + fun getPivotX(): Float { + return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()) + } + + /** Gets the Y coordinate of the pivot point */ + fun getPivotY(): Float { + return calcAverage(mDetector.getStartY(), mDetector.getPointerCount()) + } + + /** Gets the X component of the translation */ + fun getTranslationX(): Float { + return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) - + calcAverage(mDetector.getStartX(), mDetector.getPointerCount()) + } + + /** Gets the Y component of the translation */ + fun getTranslationY(): Float { + return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) - + calcAverage(mDetector.getStartY(), mDetector.getPointerCount()) + } + + /** Gets the scale */ + fun getScale(): Float { + return if (mDetector.getPointerCount() < 2) { + 1f + } else { + val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0] + val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0] + val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0] + val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0] + val startDist = hypot(startDeltaX, startDeltaY) + val currentDist = hypot(currentDeltaX, currentDeltaY) + currentDist / startDist + } + } + + /** Gets the rotation in radians */ + fun getRotation(): Float { + return if (mDetector.getPointerCount() < 2) { + 0f + } else { + val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0] + val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0] + val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0] + val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0] + val startAngle = atan2(startDeltaY, startDeltaX) + val currentAngle = atan2(currentDeltaY, currentDeltaX) + currentAngle - startAngle + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.java deleted file mode 100644 index 700991b9e0..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.java +++ /dev/null @@ -1,160 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable; - -import android.graphics.Matrix; -import android.graphics.PointF; -import com.facebook.common.logging.FLog; -import androidx.annotation.Nullable; -import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector; - -/** - * Abstract class for ZoomableController that adds animation capabilities to - * DefaultZoomableController. - */ -public abstract class AbstractAnimatedZoomableController extends DefaultZoomableController { - - private boolean mIsAnimating; - private final float[] mStartValues = new float[9]; - private final float[] mStopValues = new float[9]; - private final float[] mCurrentValues = new float[9]; - private final Matrix mNewTransform = new Matrix(); - private final Matrix mWorkingTransform = new Matrix(); - - public AbstractAnimatedZoomableController(TransformGestureDetector transformGestureDetector) { - super(transformGestureDetector); - } - - @Override - public void reset() { - FLog.v(getLogTag(), "reset"); - stopAnimation(); - mWorkingTransform.reset(); - mNewTransform.reset(); - super.reset(); - } - - /** Returns true if the zoomable transform is identity matrix, and the controller is idle. */ - @Override - public boolean isIdentity() { - return !isAnimating() && super.isIdentity(); - } - - /** - * Zooms to the desired scale and positions the image so that the given image point corresponds to - * the given view point. - * - *
If this method is called while an animation or gesture is already in progress, the current - * animation or gesture will be stopped first. - * - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - */ - @Override - public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { - zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null); - } - - /** - * Zooms to the desired scale and positions the image so that the given image point corresponds to - * the given view point. - * - *
If this method is called while an animation or gesture is already in progress, the current - * animation or gesture will be stopped first. - * - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - * @param limitFlags whether to limit translation and/or scale. - * @param durationMs length of animation of the zoom, or 0 if no animation desired - * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 - */ - public void zoomToPoint( - float scale, - PointF imagePoint, - PointF viewPoint, - @LimitFlag int limitFlags, - long durationMs, - @Nullable Runnable onAnimationComplete) { - FLog.v(getLogTag(), "zoomToPoint: duration %d ms", durationMs); - calculateZoomToPointTransform(mNewTransform, scale, imagePoint, viewPoint, limitFlags); - setTransform(mNewTransform, durationMs, onAnimationComplete); - } - - /** - * Sets a new zoomable transformation and animates to it if desired. - * - *
If this method is called while an animation or gesture is already in progress, the current - * animation or gesture will be stopped first. - * - * @param newTransform new transform to make active - * @param durationMs duration of the animation, or 0 to not animate - * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 - */ - public void setTransform( - Matrix newTransform, long durationMs, @Nullable Runnable onAnimationComplete) { - FLog.v(getLogTag(), "setTransform: duration %d ms", durationMs); - if (durationMs <= 0) { - setTransformImmediate(newTransform); - } else { - setTransformAnimated(newTransform, durationMs, onAnimationComplete); - } - } - - private void setTransformImmediate(final Matrix newTransform) { - FLog.v(getLogTag(), "setTransformImmediate"); - stopAnimation(); - mWorkingTransform.set(newTransform); - super.setTransform(newTransform); - getDetector().restartGesture(); - } - - protected boolean isAnimating() { - return mIsAnimating; - } - - protected void setAnimating(boolean isAnimating) { - mIsAnimating = isAnimating; - } - - protected float[] getStartValues() { - return mStartValues; - } - - protected float[] getStopValues() { - return mStopValues; - } - - protected Matrix getWorkingTransform() { - return mWorkingTransform; - } - - @Override - public void onGestureBegin(TransformGestureDetector detector) { - FLog.v(getLogTag(), "onGestureBegin"); - stopAnimation(); - super.onGestureBegin(detector); - } - - @Override - public void onGestureUpdate(TransformGestureDetector detector) { - FLog.v(getLogTag(), "onGestureUpdate %s", isAnimating() ? "(ignored)" : ""); - if (isAnimating()) { - return; - } - super.onGestureUpdate(detector); - } - - protected void calculateInterpolation(Matrix outMatrix, float fraction) { - for (int i = 0; i < 9; i++) { - mCurrentValues[i] = (1 - fraction) * mStartValues[i] + fraction * mStopValues[i]; - } - outMatrix.setValues(mCurrentValues); - } - - public abstract void setTransformAnimated( - final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete); - - protected abstract void stopAnimation(); - - protected abstract Class> getLogTag(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.kt new file mode 100644 index 0000000000..27aa7fe2f6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AbstractAnimatedZoomableController.kt @@ -0,0 +1,147 @@ +package fr.free.nrw.commons.media.zoomControllers.zoomable + +import android.graphics.Matrix +import android.graphics.PointF +import com.facebook.common.logging.FLog +import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector + +/** + * Abstract class for ZoomableController that adds animation capabilities to + * DefaultZoomableController. + */ +abstract class AbstractAnimatedZoomableController( + transformGestureDetector: TransformGestureDetector +) : DefaultZoomableController(transformGestureDetector) { + + private var isAnimating: Boolean = false + private val startValues = FloatArray(9) + private val stopValues = FloatArray(9) + private val currentValues = FloatArray(9) + private val newTransform = Matrix() + private val workingTransform = Matrix() + + override fun reset() { + FLog.v(logTag, "reset") + stopAnimation() + workingTransform.reset() + newTransform.reset() + super.reset() + } + + /** Returns true if the zoomable transform is identity matrix, and the controller is idle. */ + override fun isIdentity(): Boolean { + return !isAnimating && super.isIdentity() + } + + /** + * Zooms to the desired scale and positions the image so that the given image point corresponds + * to the given view point. + * + * If this method is called while an animation or gesture is already in progress, the current + * animation or gesture will be stopped first. + * + * @param scale desired scale, will be limited to {min, max} scale factor + * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) + * @param viewPoint 2D point in view's absolute coordinate system + */ + override fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) { + zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null) + } + + /** + * Zooms to the desired scale and positions the image so that the given image point corresponds + * to the given view point. + * + * If this method is called while an animation or gesture is already in progress, the current + * animation or gesture will be stopped first. + * + * @param scale desired scale, will be limited to {min, max} scale factor + * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) + * @param viewPoint 2D point in view's absolute coordinate system + * @param limitFlags whether to limit translation and/or scale. + * @param durationMs length of animation of the zoom, or 0 if no animation desired + * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 + */ + fun zoomToPoint( + scale: Float, + imagePoint: PointF, + viewPoint: PointF, + @LimitFlag limitFlags: Int, + durationMs: Long, + onAnimationComplete: Runnable? + ) { + FLog.v(logTag, "zoomToPoint: duration $durationMs ms") + calculateZoomToPointTransform(newTransform, scale, imagePoint, viewPoint, limitFlags) + setTransform(newTransform, durationMs, onAnimationComplete) + } + + /** + * Sets a new zoomable transformation and animates to it if desired. + * + * If this method is called while an animation or gesture is already in progress, the current + * animation or gesture will be stopped first. + * + * @param newTransform new transform to make active + * @param durationMs duration of the animation, or 0 to not animate + * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 + */ + private fun setTransform( + newTransform: Matrix, + durationMs: Long, + onAnimationComplete: Runnable? + ) { + FLog.v(logTag, "setTransform: duration $durationMs ms") + if (durationMs <= 0) { + setTransformImmediate(newTransform) + } else { + setTransformAnimated(newTransform, durationMs, onAnimationComplete) + } + } + + private fun setTransformImmediate(newTransform: Matrix) { + FLog.v(logTag, "setTransformImmediate") + stopAnimation() + workingTransform.set(newTransform) + super.setTransform(newTransform) + getDetector().restartGesture() + } + + protected fun getIsAnimating(): Boolean = isAnimating + + protected fun setAnimating(isAnimating: Boolean) { + this.isAnimating = isAnimating + } + + protected fun getStartValues(): FloatArray = startValues + + protected fun getStopValues(): FloatArray = stopValues + + protected fun getWorkingTransform(): Matrix = workingTransform + + override fun onGestureBegin(detector: TransformGestureDetector) { + FLog.v(logTag, "onGestureBegin") + stopAnimation() + super.onGestureBegin(detector) + } + + override fun onGestureUpdate(detector: TransformGestureDetector) { + FLog.v(logTag, "onGestureUpdate ${if (isAnimating) "(ignored)" else ""}") + if (isAnimating) return + super.onGestureUpdate(detector) + } + + protected fun calculateInterpolation(outMatrix: Matrix, fraction: Float) { + for (i in 0..8) { + currentValues[i] = (1 - fraction) * startValues[i] + fraction * stopValues[i] + } + outMatrix.setValues(currentValues) + } + + abstract fun setTransformAnimated( + newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable? + ) + + protected abstract fun stopAnimation() + + protected abstract val logTag: Class<*> +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.java deleted file mode 100644 index 471ceb973d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.java +++ /dev/null @@ -1,96 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.graphics.Matrix; -import android.view.animation.DecelerateInterpolator; -import com.facebook.common.internal.Preconditions; -import com.facebook.common.logging.FLog; -import androidx.annotation.Nullable; -import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector; - -/** - * ZoomableController that adds animation capabilities to DefaultZoomableController using standard - * Android animation classes - */ -public class AnimatedZoomableController extends AbstractAnimatedZoomableController { - - private static final Class> TAG = AnimatedZoomableController.class; - - private final ValueAnimator mValueAnimator; - - public static AnimatedZoomableController newInstance() { - return new AnimatedZoomableController(TransformGestureDetector.newInstance()); - } - - @SuppressLint("NewApi") - public AnimatedZoomableController(TransformGestureDetector transformGestureDetector) { - super(transformGestureDetector); - mValueAnimator = ValueAnimator.ofFloat(0, 1); - mValueAnimator.setInterpolator(new DecelerateInterpolator()); - } - - @SuppressLint("NewApi") - @Override - public void setTransformAnimated( - final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete) { - FLog.v(getLogTag(), "setTransformAnimated: duration %d ms", durationMs); - stopAnimation(); - Preconditions.checkArgument(durationMs > 0); - Preconditions.checkState(!isAnimating()); - setAnimating(true); - mValueAnimator.setDuration(durationMs); - getTransform().getValues(getStartValues()); - newTransform.getValues(getStopValues()); - mValueAnimator.addUpdateListener( - new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator valueAnimator) { - calculateInterpolation(getWorkingTransform(), (float) valueAnimator.getAnimatedValue()); - AnimatedZoomableController.super.setTransform(getWorkingTransform()); - } - }); - mValueAnimator.addListener( - new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - FLog.v(getLogTag(), "setTransformAnimated: animation cancelled"); - onAnimationStopped(); - } - - @Override - public void onAnimationEnd(Animator animation) { - FLog.v(getLogTag(), "setTransformAnimated: animation finished"); - onAnimationStopped(); - } - - private void onAnimationStopped() { - if (onAnimationComplete != null) { - onAnimationComplete.run(); - } - setAnimating(false); - getDetector().restartGesture(); - } - }); - mValueAnimator.start(); - } - - @SuppressLint("NewApi") - @Override - public void stopAnimation() { - if (!isAnimating()) { - return; - } - FLog.v(getLogTag(), "stopAnimation"); - mValueAnimator.cancel(); - mValueAnimator.removeAllUpdateListeners(); - mValueAnimator.removeAllListeners(); - } - - @Override - protected Class> getLogTag() { - return TAG; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.kt new file mode 100644 index 0000000000..6ba9270078 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/AnimatedZoomableController.kt @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.media.zoomControllers.zoomable + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.annotation.SuppressLint +import android.graphics.Matrix +import android.view.animation.DecelerateInterpolator +import com.facebook.common.logging.FLog +import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector + +/** + * ZoomableController that adds animation capabilities to DefaultZoomableController using standard + * Android animation classes + */ +class AnimatedZoomableController private constructor() : + AbstractAnimatedZoomableController(TransformGestureDetector.newInstance()) { + + private val valueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + interpolator = DecelerateInterpolator() + } + + companion object { + fun newInstance(): AnimatedZoomableController { + return AnimatedZoomableController() + } + } + + @SuppressLint("NewApi") + override fun setTransformAnimated( + newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable? + ) { + FLog.v(logTag, "setTransformAnimated: duration $durationMs ms") + stopAnimation() + require(durationMs > 0) { "Duration must be greater than zero" } + check(!getIsAnimating()) { "Animation is already in progress" } + setAnimating(true) + valueAnimator.duration = durationMs + getTransform().getValues(getStartValues()) + newTransform.getValues(getStopValues()) + valueAnimator.addUpdateListener { animator -> + calculateInterpolation(getWorkingTransform(), animator.animatedValue as Float) + super.setTransform(getWorkingTransform()) + } + valueAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationCancel(animation: Animator) { + FLog.v(logTag, "setTransformAnimated: animation cancelled") + onAnimationStopped() + } + + override fun onAnimationEnd(animation: Animator) { + FLog.v(logTag, "setTransformAnimated: animation finished") + onAnimationStopped() + } + + private fun onAnimationStopped() { + onAnimationComplete?.run() + setAnimating(false) + getDetector().restartGesture() + } + }) + valueAnimator.start() + } + + @SuppressLint("NewApi") + override fun stopAnimation() { + if (!getIsAnimating()) return + FLog.v(logTag, "stopAnimation") + valueAnimator.cancel() + valueAnimator.removeAllUpdateListeners() + valueAnimator.removeAllListeners() + } + + override val logTag: Class<*> = AnimatedZoomableController::class.java +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.java deleted file mode 100644 index 9c3538f05d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.java +++ /dev/null @@ -1,646 +0,0 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable; - -import android.graphics.Matrix; -import android.graphics.PointF; -import android.graphics.RectF; -import android.view.MotionEvent; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector; -import com.facebook.common.logging.FLog; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** Zoomable controller that calculates transformation based on touch events. */ -public class DefaultZoomableController - implements ZoomableController, TransformGestureDetector.Listener { - - /** Interface for handling call backs when the image bounds are set. */ - public interface ImageBoundsListener { - void onImageBoundsSet(RectF imageBounds); - } - - @IntDef( - flag = true, - value = {LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL}) - @Retention(RetentionPolicy.SOURCE) - public @interface LimitFlag {} - - public static final int LIMIT_NONE = 0; - public static final int LIMIT_TRANSLATION_X = 1; - public static final int LIMIT_TRANSLATION_Y = 2; - public static final int LIMIT_SCALE = 4; - public static final int LIMIT_ALL = LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y | LIMIT_SCALE; - - private static final float EPS = 1e-3f; - - private static final Class> TAG = DefaultZoomableController.class; - - private static final RectF IDENTITY_RECT = new RectF(0, 0, 1, 1); - - private TransformGestureDetector mGestureDetector; - - private @Nullable ImageBoundsListener mImageBoundsListener; - - private @Nullable Listener mListener = null; - - private boolean mIsEnabled = false; - private boolean mIsRotationEnabled = false; - private boolean mIsScaleEnabled = true; - private boolean mIsTranslationEnabled = true; - private boolean mIsGestureZoomEnabled = true; - - private float mMinScaleFactor = 1.0f; - private float mMaxScaleFactor = 2.0f; - - // View bounds, in view-absolute coordinates - private final RectF mViewBounds = new RectF(); - // Non-transformed image bounds, in view-absolute coordinates - private final RectF mImageBounds = new RectF(); - // Transformed image bounds, in view-absolute coordinates - private final RectF mTransformedImageBounds = new RectF(); - - private final Matrix mPreviousTransform = new Matrix(); - private final Matrix mActiveTransform = new Matrix(); - private final Matrix mActiveTransformInverse = new Matrix(); - private final float[] mTempValues = new float[9]; - private final RectF mTempRect = new RectF(); - private boolean mWasTransformCorrected; - - public static DefaultZoomableController newInstance() { - return new DefaultZoomableController(TransformGestureDetector.newInstance()); - } - - public DefaultZoomableController(TransformGestureDetector gestureDetector) { - mGestureDetector = gestureDetector; - mGestureDetector.setListener(this); - } - - /** Rests the controller. */ - public void reset() { - FLog.v(TAG, "reset"); - mGestureDetector.reset(); - mPreviousTransform.reset(); - mActiveTransform.reset(); - onTransformChanged(); - } - - /** Sets the zoomable listener. */ - @Override - public void setListener(Listener listener) { - mListener = listener; - } - - /** Sets whether the controller is enabled or not. */ - @Override - public void setEnabled(boolean enabled) { - mIsEnabled = enabled; - if (!enabled) { - reset(); - } - } - - /** Gets whether the controller is enabled or not. */ - @Override - public boolean isEnabled() { - return mIsEnabled; - } - - /** Sets whether the rotation gesture is enabled or not. */ - public void setRotationEnabled(boolean enabled) { - mIsRotationEnabled = enabled; - } - - /** Gets whether the rotation gesture is enabled or not. */ - public boolean isRotationEnabled() { - return mIsRotationEnabled; - } - - /** Sets whether the scale gesture is enabled or not. */ - public void setScaleEnabled(boolean enabled) { - mIsScaleEnabled = enabled; - } - - /** Gets whether the scale gesture is enabled or not. */ - public boolean isScaleEnabled() { - return mIsScaleEnabled; - } - - /** Sets whether the translation gesture is enabled or not. */ - public void setTranslationEnabled(boolean enabled) { - mIsTranslationEnabled = enabled; - } - - /** Gets whether the translations gesture is enabled or not. */ - public boolean isTranslationEnabled() { - return mIsTranslationEnabled; - } - - /** - * Sets the minimum scale factor allowed. - * - *
Hierarchy's scaling (if any) is not taken into account. - */ - public void setMinScaleFactor(float minScaleFactor) { - mMinScaleFactor = minScaleFactor; - } - - /** Gets the minimum scale factor allowed. */ - public float getMinScaleFactor() { - return mMinScaleFactor; - } - - /** - * Sets the maximum scale factor allowed. - * - *
Hierarchy's scaling (if any) is not taken into account. - */ - public void setMaxScaleFactor(float maxScaleFactor) { - mMaxScaleFactor = maxScaleFactor; - } - - /** Gets the maximum scale factor allowed. */ - public float getMaxScaleFactor() { - return mMaxScaleFactor; - } - - /** Sets whether gesture zooms are enabled or not. */ - public void setGestureZoomEnabled(boolean isGestureZoomEnabled) { - mIsGestureZoomEnabled = isGestureZoomEnabled; - } - - /** Gets whether gesture zooms are enabled or not. */ - public boolean isGestureZoomEnabled() { - return mIsGestureZoomEnabled; - } - - /** Gets the current scale factor. */ - @Override - public float getScaleFactor() { - return getMatrixScaleFactor(mActiveTransform); - } - - /** Sets the image bounds, in view-absolute coordinates. */ - @Override - public void setImageBounds(RectF imageBounds) { - if (!imageBounds.equals(mImageBounds)) { - mImageBounds.set(imageBounds); - onTransformChanged(); - if (mImageBoundsListener != null) { - mImageBoundsListener.onImageBoundsSet(mImageBounds); - } - } - } - - /** Gets the non-transformed image bounds, in view-absolute coordinates. */ - public RectF getImageBounds() { - return mImageBounds; - } - - /** Gets the transformed image bounds, in view-absolute coordinates */ - private RectF getTransformedImageBounds() { - return mTransformedImageBounds; - } - - /** Sets the view bounds. */ - @Override - public void setViewBounds(RectF viewBounds) { - mViewBounds.set(viewBounds); - } - - /** Gets the view bounds. */ - public RectF getViewBounds() { - return mViewBounds; - } - - /** Sets the image bounds listener. */ - public void setImageBoundsListener(@Nullable ImageBoundsListener imageBoundsListener) { - mImageBoundsListener = imageBoundsListener; - } - - /** Gets the image bounds listener. */ - public @Nullable ImageBoundsListener getImageBoundsListener() { - return mImageBoundsListener; - } - - /** Returns true if the zoomable transform is identity matrix. */ - @Override - public boolean isIdentity() { - return isMatrixIdentity(mActiveTransform, 1e-3f); - } - - /** - * Returns true if the transform was corrected during the last update. - * - *
We should rename this method to `wasTransformedWithoutCorrection` and just return the - * internal flag directly. However, this requires interface change and negation of meaning. - */ - @Override - public boolean wasTransformCorrected() { - return mWasTransformCorrected; - } - - /** - * Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The - * zoomable transformation is taken into account. - * - *
Internal matrix is exposed for performance reasons and is not to be modified by the callers. - */ - @Override - public Matrix getTransform() { - return mActiveTransform; - } - - /** - * Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The - * zoomable transformation is taken into account. - */ - public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) { - outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL); - } - - /** - * Maps point from view-absolute to image-relative coordinates. This takes into account the - * zoomable transformation. - */ - public PointF mapViewToImage(PointF viewPoint) { - float[] points = mTempValues; - points[0] = viewPoint.x; - points[1] = viewPoint.y; - mActiveTransform.invert(mActiveTransformInverse); - mActiveTransformInverse.mapPoints(points, 0, points, 0, 1); - mapAbsoluteToRelative(points, points, 1); - return new PointF(points[0], points[1]); - } - - /** - * Maps point from image-relative to view-absolute coordinates. This takes into account the - * zoomable transformation. - */ - public PointF mapImageToView(PointF imagePoint) { - float[] points = mTempValues; - points[0] = imagePoint.x; - points[1] = imagePoint.y; - mapRelativeToAbsolute(points, points, 1); - mActiveTransform.mapPoints(points, 0, points, 0, 1); - return new PointF(points[0], points[1]); - } - - /** - * Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take - * into account the zoomable transformation. Points are represented by a float array of [x0, y0, - * x1, y1, ...]. - * - * @param destPoints destination array (may be the same as source array) - * @param srcPoints source array - * @param numPoints number of points to map - */ - private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) { - for (int i = 0; i < numPoints; i++) { - destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width(); - destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height(); - } - } - - /** - * Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take - * into account the zoomable transformation. Points are represented by float array of [x0, y0, x1, - * y1, ...]. - * - * @param destPoints destination array (may be the same as source array) - * @param srcPoints source array - * @param numPoints number of points to map - */ - private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) { - for (int i = 0; i < numPoints; i++) { - destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left; - destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top; - } - } - - /** - * Zooms to the desired scale and positions the image so that the given image point corresponds to - * the given view point. - * - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - */ - public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { - FLog.v(TAG, "zoomToPoint"); - calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL); - onTransformChanged(); - } - - /** - * Calculates the zoom transformation that would zoom to the desired scale and position the image - * so that the given image point corresponds to the given view point. - * - * @param outTransform the matrix to store the result to - * @param scale desired scale, will be limited to {min, max} scale factor - * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1) - * @param viewPoint 2D point in view's absolute coordinate system - * @param limitFlags whether to limit translation and/or scale. - * @return whether or not the transform has been corrected due to limitation - */ - protected boolean calculateZoomToPointTransform( - Matrix outTransform, - float scale, - PointF imagePoint, - PointF viewPoint, - @LimitFlag int limitFlags) { - float[] viewAbsolute = mTempValues; - viewAbsolute[0] = imagePoint.x; - viewAbsolute[1] = imagePoint.y; - mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1); - float distanceX = viewPoint.x - viewAbsolute[0]; - float distanceY = viewPoint.y - viewAbsolute[1]; - boolean transformCorrected = false; - outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]); - transformCorrected |= limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags); - outTransform.postTranslate(distanceX, distanceY); - transformCorrected |= limitTranslation(outTransform, limitFlags); - return transformCorrected; - } - - /** Sets a new zoom transformation. */ - public void setTransform(Matrix newTransform) { - FLog.v(TAG, "setTransform"); - mActiveTransform.set(newTransform); - onTransformChanged(); - } - - /** Gets the gesture detector. */ - protected TransformGestureDetector getDetector() { - return mGestureDetector; - } - - /** Notifies controller of the received touch event. */ - @Override - public boolean onTouchEvent(MotionEvent event) { - FLog.v(TAG, "onTouchEvent: action: ", event.getAction()); - if (mIsEnabled && mIsGestureZoomEnabled) { - return mGestureDetector.onTouchEvent(event); - } - return false; - } - - /* TransformGestureDetector.Listener methods */ - - @Override - public void onGestureBegin(TransformGestureDetector detector) { - FLog.v(TAG, "onGestureBegin"); - mPreviousTransform.set(mActiveTransform); - onTransformBegin(); - // We only received a touch down event so far, and so we don't know yet in which direction a - // future move event will follow. Therefore, if we can't scroll in all directions, we have to - // assume the worst case where the user tries to scroll out of edge, which would cause - // transformation to be corrected. - mWasTransformCorrected = !canScrollInAllDirection(); - } - - @Override - public void onGestureUpdate(TransformGestureDetector detector) { - FLog.v(TAG, "onGestureUpdate"); - boolean transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL); - onTransformChanged(); - if (transformCorrected) { - mGestureDetector.restartGesture(); - } - // A transformation happened, but was it without correction? - mWasTransformCorrected = transformCorrected; - } - - @Override - public void onGestureEnd(TransformGestureDetector detector) { - FLog.v(TAG, "onGestureEnd"); - onTransformEnd(); - } - - /** - * Calculates the zoom transformation based on the current gesture. - * - * @param outTransform the matrix to store the result to - * @param limitTypes whether to limit translation and/or scale. - * @return whether or not the transform has been corrected due to limitation - */ - protected boolean calculateGestureTransform(Matrix outTransform, @LimitFlag int limitTypes) { - TransformGestureDetector detector = mGestureDetector; - boolean transformCorrected = false; - outTransform.set(mPreviousTransform); - if (mIsRotationEnabled) { - float angle = detector.getRotation() * (float) (180 / Math.PI); - outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()); - } - if (mIsScaleEnabled) { - float scale = detector.getScale(); - outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()); - } - transformCorrected |= - limitScale(outTransform, detector.getPivotX(), detector.getPivotY(), limitTypes); - if (mIsTranslationEnabled) { - outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()); - } - transformCorrected |= limitTranslation(outTransform, limitTypes); - return transformCorrected; - } - - private void onTransformBegin() { - if (mListener != null && isEnabled()) { - mListener.onTransformBegin(mActiveTransform); - } - } - - private void onTransformChanged() { - mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds); - if (mListener != null && isEnabled()) { - mListener.onTransformChanged(mActiveTransform); - } - } - - private void onTransformEnd() { - if (mListener != null && isEnabled()) { - mListener.onTransformEnd(mActiveTransform); - } - } - - /** - * Keeps the scaling factor within the specified limits. - * - * @param pivotX x coordinate of the pivot point - * @param pivotY y coordinate of the pivot point - * @param limitTypes whether to limit scale. - * @return whether limiting has been applied or not - */ - private boolean limitScale( - Matrix transform, float pivotX, float pivotY, @LimitFlag int limitTypes) { - if (!shouldLimit(limitTypes, LIMIT_SCALE)) { - return false; - } - float currentScale = getMatrixScaleFactor(transform); - float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor); - if (targetScale != currentScale) { - float scale = targetScale / currentScale; - transform.postScale(scale, scale, pivotX, pivotY); - return true; - } - return false; - } - - /** - * Limits the translation so that there are no empty spaces on the sides if possible. - * - *
The image is attempted to be centered within the view bounds if the transformed image is - * smaller. There will be no empty spaces within the view bounds if the transformed image is - * bigger. This applies to each dimension (horizontal and vertical) independently. - * - * @param limitTypes whether to limit translation along the specific axis. - * @return whether limiting has been applied or not - */ - private boolean limitTranslation(Matrix transform, @LimitFlag int limitTypes) { - if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y)) { - return false; - } - RectF b = mTempRect; - b.set(mImageBounds); - transform.mapRect(b); - float offsetLeft = - shouldLimit(limitTypes, LIMIT_TRANSLATION_X) - ? getOffset( - b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()) - : 0; - float offsetTop = - shouldLimit(limitTypes, LIMIT_TRANSLATION_Y) - ? getOffset( - b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY()) - : 0; - if (offsetLeft != 0 || offsetTop != 0) { - transform.postTranslate(offsetLeft, offsetTop); - return true; - } - return false; - } - - /** - * Checks whether the specified limit flag is present in the limits provided. - * - *
If the flag contains multiple flags together using a bitwise OR, this only checks that at - * least one of the flags is included. - * - * @param limits the limits to apply - * @param flag the limit flag(s) to check for - * @return true if the flag (or one of the flags) is included in the limits - */ - private static boolean shouldLimit(@LimitFlag int limits, @LimitFlag int flag) { - return (limits & flag) != LIMIT_NONE; - } - - /** - * Returns the offset necessary to make sure that: - the image is centered within the limit if the - * image is smaller than the limit - there is no empty space on left/right if the image is bigger - * than the limit - */ - private float getOffset( - float imageStart, float imageEnd, float limitStart, float limitEnd, float limitCenter) { - float imageWidth = imageEnd - imageStart, limitWidth = limitEnd - limitStart; - float limitInnerWidth = Math.min(limitCenter - limitStart, limitEnd - limitCenter) * 2; - // center if smaller than limitInnerWidth - if (imageWidth < limitInnerWidth) { - return limitCenter - (imageEnd + imageStart) / 2; - } - // to the edge if in between and limitCenter is not (limitLeft + limitRight) / 2 - if (imageWidth < limitWidth) { - if (limitCenter < (limitStart + limitEnd) / 2) { - return limitStart - imageStart; - } else { - return limitEnd - imageEnd; - } - } - // to the edge if larger than limitWidth and empty space visible - if (imageStart > limitStart) { - return limitStart - imageStart; - } - if (imageEnd < limitEnd) { - return limitEnd - imageEnd; - } - return 0; - } - - /** Limits the value to the given min and max range. */ - private float limit(float value, float min, float max) { - return Math.min(Math.max(min, value), max); - } - - /** - * Gets the scale factor for the given matrix. This method assumes the equal scaling factor for X - * and Y axis. - */ - private float getMatrixScaleFactor(Matrix transform) { - transform.getValues(mTempValues); - return mTempValues[Matrix.MSCALE_X]; - } - - /** Same as {@code Matrix.isIdentity()}, but with tolerance {@code eps}. */ - private boolean isMatrixIdentity(Matrix transform, float eps) { - // Checks whether the given matrix is close enough to the identity matrix: - // 1 0 0 - // 0 1 0 - // 0 0 1 - // Or equivalently to the zero matrix, after subtracting 1.0f from the diagonal elements: - // 0 0 0 - // 0 0 0 - // 0 0 0 - transform.getValues(mTempValues); - mTempValues[0] -= 1.0f; // m00 - mTempValues[4] -= 1.0f; // m11 - mTempValues[8] -= 1.0f; // m22 - for (int i = 0; i < 9; i++) { - if (Math.abs(mTempValues[i]) > eps) { - return false; - } - } - return true; - } - - /** Returns whether the scroll can happen in all directions. I.e. the image is not on any edge. */ - private boolean canScrollInAllDirection() { - return mTransformedImageBounds.left < mViewBounds.left - EPS - && mTransformedImageBounds.top < mViewBounds.top - EPS - && mTransformedImageBounds.right > mViewBounds.right + EPS - && mTransformedImageBounds.bottom > mViewBounds.bottom + EPS; - } - - @Override - public int computeHorizontalScrollRange() { - return (int) mTransformedImageBounds.width(); - } - - @Override - public int computeHorizontalScrollOffset() { - return (int) (mViewBounds.left - mTransformedImageBounds.left); - } - - @Override - public int computeHorizontalScrollExtent() { - return (int) mViewBounds.width(); - } - - @Override - public int computeVerticalScrollRange() { - return (int) mTransformedImageBounds.height(); - } - - @Override - public int computeVerticalScrollOffset() { - return (int) (mViewBounds.top - mTransformedImageBounds.top); - } - - @Override - public int computeVerticalScrollExtent() { - return (int) mViewBounds.height(); - } - - public Listener getListener() { - return mListener; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.kt new file mode 100644 index 0000000000..3d6498f6a2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.kt @@ -0,0 +1,607 @@ +package fr.free.nrw.commons.media.zoomControllers.zoomable + +import android.graphics.Matrix +import android.graphics.PointF +import android.graphics.RectF +import android.view.MotionEvent +import androidx.annotation.IntDef +import com.facebook.common.logging.FLog +import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector +import kotlin.math.abs + +/** Zoomable controller that calculates transformation based on touch events. */ +open class DefaultZoomableController( + private val mGestureDetector: TransformGestureDetector +) : ZoomableController, TransformGestureDetector.Listener { + + /** Interface for handling call backs when the image bounds are set. */ + fun interface ImageBoundsListener { + fun onImageBoundsSet(imageBounds: RectF) + } + + @IntDef( + flag = true, + value = [LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL] + ) + @Retention + annotation class LimitFlag + + companion object { + const val LIMIT_NONE = 0 + const val LIMIT_TRANSLATION_X = 1 + const val LIMIT_TRANSLATION_Y = 2 + const val LIMIT_SCALE = 4 + const val LIMIT_ALL = LIMIT_TRANSLATION_X or LIMIT_TRANSLATION_Y or LIMIT_SCALE + + private const val EPS = 1e-3f + + private val TAG: Class<*> = DefaultZoomableController::class.java + + private val IDENTITY_RECT = RectF(0f, 0f, 1f, 1f) + + fun newInstance(): DefaultZoomableController { + return DefaultZoomableController(TransformGestureDetector.newInstance()) + } + } + + private var mImageBoundsListener: ImageBoundsListener? = null + + private var mListener: ZoomableController.Listener? = null + + private var mIsEnabled = false + private var mIsRotationEnabled = false + private var mIsScaleEnabled = true + private var mIsTranslationEnabled = true + private var mIsGestureZoomEnabled = true + + private var mMinScaleFactor = 1.0f + private var mMaxScaleFactor = 2.0f + + // View bounds, in view-absolute coordinates + private val mViewBounds = RectF() + // Non-transformed image bounds, in view-absolute coordinates + private val mImageBounds = RectF() + // Transformed image bounds, in view-absolute coordinates + private val mTransformedImageBounds = RectF() + + private val mPreviousTransform = Matrix() + private val mActiveTransform = Matrix() + private val mActiveTransformInverse = Matrix() + private val mTempValues = FloatArray(9) + private val mTempRect = RectF() + private var mWasTransformCorrected = false + + init { + mGestureDetector.setListener(this) + } + + /** Rests the controller. */ + open fun reset() { + FLog.v(TAG, "reset") + mGestureDetector.reset() + mPreviousTransform.reset() + mActiveTransform.reset() + onTransformChanged() + } + + /** Sets the zoomable listener. */ + override fun setListener(listener: ZoomableController.Listener?) { + mListener = listener + } + + /** Sets whether the controller is enabled or not. */ + override fun setEnabled(enabled: Boolean) { + mIsEnabled = enabled + if (!enabled) { + reset() + } + } + + /** Gets whether the controller is enabled or not. */ + override fun isEnabled(): Boolean { + return mIsEnabled + } + + /** Sets whether the rotation gesture is enabled or not. */ + fun setRotationEnabled(enabled: Boolean) { + mIsRotationEnabled = enabled + } + + /** Gets whether the rotation gesture is enabled or not. */ + fun isRotationEnabled(): Boolean { + return mIsRotationEnabled + } + + /** Sets whether the scale gesture is enabled or not. */ + fun setScaleEnabled(enabled: Boolean) { + mIsScaleEnabled = enabled + } + + /** Gets whether the scale gesture is enabled or not. */ + fun isScaleEnabled(): Boolean { + return mIsScaleEnabled + } + + /** Sets whether the translation gesture is enabled or not. */ + fun setTranslationEnabled(enabled: Boolean) { + mIsTranslationEnabled = enabled + } + + /** Gets whether the translations gesture is enabled or not. */ + fun isTranslationEnabled(): Boolean { + return mIsTranslationEnabled + } + + /** + * Sets the minimum scale factor allowed. + * + *
Hierarchy's scaling (if any) is not taken into account. + */ + fun setMinScaleFactor(minScaleFactor: Float) { + mMinScaleFactor = minScaleFactor + } + + /** Gets the minimum scale factor allowed. */ + fun getMinScaleFactor(): Float { + return mMinScaleFactor + } + + /** + * Sets the maximum scale factor allowed. + * + *
Hierarchy's scaling (if any) is not taken into account. + */ + fun setMaxScaleFactor(maxScaleFactor: Float) { + mMaxScaleFactor = maxScaleFactor + } + + /** Gets the maximum scale factor allowed. */ + fun getMaxScaleFactor(): Float { + return mMaxScaleFactor + } + + /** Sets whether gesture zooms are enabled or not. */ + fun setGestureZoomEnabled(isGestureZoomEnabled: Boolean) { + mIsGestureZoomEnabled = isGestureZoomEnabled + } + + /** Gets whether gesture zooms are enabled or not. */ + fun isGestureZoomEnabled(): Boolean { + return mIsGestureZoomEnabled + } + + /** Gets the current scale factor. */ + override fun getScaleFactor(): Float { + return getMatrixScaleFactor(mActiveTransform) + } + + /** Sets the image bounds, in view-absolute coordinates. */ + override fun setImageBounds(imageBounds: RectF) { + if (imageBounds != mImageBounds) { + mImageBounds.set(imageBounds) + onTransformChanged() + mImageBoundsListener?.onImageBoundsSet(mImageBounds) + } + } + + /** Gets the non-transformed image bounds, in view-absolute coordinates. */ + fun getImageBounds(): RectF { + return mImageBounds + } + + /** Gets the transformed image bounds, in view-absolute coordinates */ + private fun getTransformedImageBounds(): RectF { + return mTransformedImageBounds + } + + /** Sets the view bounds. */ + override fun setViewBounds(viewBounds: RectF) { + mViewBounds.set(viewBounds) + } + + /** Gets the view bounds. */ + fun getViewBounds(): RectF { + return mViewBounds + } + + /** Sets the image bounds listener. */ + fun setImageBoundsListener(imageBoundsListener: ImageBoundsListener?) { + mImageBoundsListener = imageBoundsListener + } + + /** Gets the image bounds listener. */ + fun getImageBoundsListener(): ImageBoundsListener? { + return mImageBoundsListener + } + + /** Returns true if the zoomable transform is identity matrix. */ + override fun isIdentity(): Boolean { + return isMatrixIdentity(mActiveTransform, 1e-3f) + } + + /** + * Returns true if the transform was corrected during the last update. + * + *
We should rename this method to `wasTransformedWithoutCorrection` and just return the + * internal flag directly. However, this requires interface change and negation of meaning. + */ + override fun wasTransformCorrected(): Boolean { + return mWasTransformCorrected + } + + /** + * Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The + * zoomable transformation is taken into account. + * + *
Internal matrix is exposed for performance reasons and is not to be modified by the
+ * callers.
+ */
+ override fun getTransform(): Matrix {
+ return mActiveTransform
+ }
+
+ /**
+ * Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The
+ * zoomable transformation is taken into account.
+ */
+ fun getImageRelativeToViewAbsoluteTransform(outMatrix: Matrix) {
+ outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL)
+ }
+
+ /**
+ * Maps point from view-absolute to image-relative coordinates. This takes into account the
+ * zoomable transformation.
+ */
+ fun mapViewToImage(viewPoint: PointF): PointF {
+ val points = mTempValues
+ points[0] = viewPoint.x
+ points[1] = viewPoint.y
+ mActiveTransform.invert(mActiveTransformInverse)
+ mActiveTransformInverse.mapPoints(points, 0, points, 0, 1)
+ mapAbsoluteToRelative(points, points, 1)
+ return PointF(points[0], points[1])
+ }
+
+ /**
+ * Maps point from image-relative to view-absolute coordinates. This takes into account the
+ * zoomable transformation.
+ */
+ fun mapImageToView(imagePoint: PointF): PointF {
+ val points = mTempValues
+ points[0] = imagePoint.x
+ points[1] = imagePoint.y
+ mapRelativeToAbsolute(points, points, 1)
+ mActiveTransform.mapPoints(points, 0, points, 0, 1)
+ return PointF(points[0], points[1])
+ }
+
+ /**
+ * Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take
+ * into account the zoomable transformation. Points are represented by a float array of [x0, y0,
+ * x1, y1, ...].
+ *
+ * @param destPoints destination array (may be the same as source array)
+ * @param srcPoints source array
+ * @param numPoints number of points to map
+ */
+ private fun mapAbsoluteToRelative(
+ destPoints: FloatArray,
+ srcPoints: FloatArray,
+ numPoints: Int
+ ) {
+ for (i in 0 until numPoints) {
+ destPoints[i * 2] = (srcPoints[i * 2] - mImageBounds.left) / mImageBounds.width()
+ destPoints[i * 2 + 1] =
+ (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height()
+ }
+ }
+
+ /**
+ * Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take
+ * into account the zoomable transformation. Points are represented by float array of
+ * [x0, y0, x1, y1, ...].
+ *
+ * @param destPoints destination array (may be the same as source array)
+ * @param srcPoints source array
+ * @param numPoints number of points to map
+ */
+ private fun mapRelativeToAbsolute(
+ destPoints: FloatArray,
+ srcPoints: FloatArray,
+ numPoints: Int
+ ) {
+ for (i in 0 until numPoints) {
+ destPoints[i * 2] = srcPoints[i * 2] * mImageBounds.width() + mImageBounds.left
+ destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top
+ }
+ }
+
+ /**
+ * Zooms to the desired scale and positions the image so that the given image point
+ * corresponds to the given view point.
+ *
+ * @param scale desired scale, will be limited to {min, max} scale factor
+ * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
+ * @param viewPoint 2D point in view's absolute coordinate system
+ */
+ open fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) {
+ FLog.v(TAG, "zoomToPoint")
+ calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL)
+ onTransformChanged()
+ }
+
+ /**
+ * Calculates the zoom transformation that would zoom to the desired scale and position
+ * the image so that the given image point corresponds to the given view point.
+ *
+ * @param outTransform the matrix to store the result to
+ * @param scale desired scale, will be limited to {min, max} scale factor
+ * @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
+ * @param viewPoint 2D point in view's absolute coordinate system
+ * @param limitFlags whether to limit translation and/or scale.
+ * @return whether or not the transform has been corrected due to limitation
+ */
+ protected fun calculateZoomToPointTransform(
+ outTransform: Matrix,
+ scale: Float,
+ imagePoint: PointF,
+ viewPoint: PointF,
+ @LimitFlag limitFlags: Int
+ ): Boolean {
+ val viewAbsolute = mTempValues
+ viewAbsolute[0] = imagePoint.x
+ viewAbsolute[1] = imagePoint.y
+ mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1)
+ val distanceX = viewPoint.x - viewAbsolute[0]
+ val distanceY = viewPoint.y - viewAbsolute[1]
+ var transformCorrected = false
+ outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1])
+ transformCorrected = transformCorrected or
+ limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags)
+ outTransform.postTranslate(distanceX, distanceY)
+ transformCorrected = transformCorrected or limitTranslation(outTransform, limitFlags)
+ return transformCorrected
+ }
+
+ /** Sets a new zoom transformation. */
+ fun setTransform(newTransform: Matrix) {
+ FLog.v(TAG, "setTransform")
+ mActiveTransform.set(newTransform)
+ onTransformChanged()
+ }
+
+ /** Gets the gesture detector. */
+ protected fun getDetector(): TransformGestureDetector {
+ return mGestureDetector
+ }
+
+ /** Notifies controller of the received touch event. */
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ FLog.v(TAG, "onTouchEvent: action: ", event.action)
+ return if (mIsEnabled && mIsGestureZoomEnabled) {
+ mGestureDetector.onTouchEvent(event)
+ } else {
+ false
+ }
+ }
+
+ /* TransformGestureDetector.Listener methods */
+
+ override fun onGestureBegin(detector: TransformGestureDetector) {
+ FLog.v(TAG, "onGestureBegin")
+ mPreviousTransform.set(mActiveTransform)
+ onTransformBegin()
+ mWasTransformCorrected = !canScrollInAllDirection()
+ }
+
+ override fun onGestureUpdate(detector: TransformGestureDetector) {
+ FLog.v(TAG, "onGestureUpdate")
+ val transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL)
+ onTransformChanged()
+ if (transformCorrected) {
+ mGestureDetector.restartGesture()
+ }
+ mWasTransformCorrected = transformCorrected
+ }
+
+ override fun onGestureEnd(detector: TransformGestureDetector) {
+ FLog.v(TAG, "onGestureEnd")
+ onTransformEnd()
+ }
+
+ /**
+ * Calculates the zoom transformation based on the current gesture.
+ *
+ * @param outTransform the matrix to store the result to
+ * @param limitTypes whether to limit translation and/or scale.
+ * @return whether or not the transform has been corrected due to limitation
+ */
+ private fun calculateGestureTransform(
+ outTransform: Matrix,
+ @LimitFlag limitTypes: Int
+ ): Boolean {
+ val detector = mGestureDetector
+ var transformCorrected = false
+ outTransform.set(mPreviousTransform)
+ if (mIsRotationEnabled) {
+ val angle = detector.getRotation() * (180 / Math.PI).toFloat()
+ outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY())
+ }
+ if (mIsScaleEnabled) {
+ val scale = detector.getScale()
+ outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY())
+ }
+ transformCorrected = transformCorrected or limitScale(
+ outTransform,
+ detector.getPivotX(),
+ detector.getPivotY(),
+ limitTypes
+ )
+ if (mIsTranslationEnabled) {
+ outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY())
+ }
+ transformCorrected = transformCorrected or limitTranslation(outTransform, limitTypes)
+ return transformCorrected
+ }
+
+ private fun onTransformBegin() {
+ if (mListener != null && isEnabled()) {
+ mListener?.onTransformBegin(mActiveTransform)
+ }
+ }
+
+ private fun onTransformChanged() {
+ mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds)
+ if (mListener != null && isEnabled()) {
+ mListener?.onTransformChanged(mActiveTransform)
+ }
+ }
+
+ private fun onTransformEnd() {
+ if (mListener != null && isEnabled()) {
+ mListener?.onTransformEnd(mActiveTransform)
+ }
+ }
+
+ /**
+ * Keeps the scaling factor within the specified limits.
+ *
+ * @param pivotX x coordinate of the pivot point
+ * @param pivotY y coordinate of the pivot point
+ * @param limitTypes whether to limit scale.
+ * @return whether limiting has been applied or not
+ */
+ private fun limitScale(
+ transform: Matrix, pivotX: Float, pivotY: Float, @LimitFlag limitTypes: Int
+ ): Boolean {
+ if (!shouldLimit(limitTypes, LIMIT_SCALE)) {
+ return false
+ }
+ val currentScale = getMatrixScaleFactor(transform)
+ val targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor)
+ return if (targetScale != currentScale) {
+ val scale = targetScale / currentScale
+ transform.postScale(scale, scale, pivotX, pivotY)
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Limits the translation so that there are no empty spaces on the sides if possible.
+ */
+ private fun limitTranslation(transform: Matrix, @LimitFlag limitTypes: Int): Boolean {
+ if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X or LIMIT_TRANSLATION_Y)) {
+ return false
+ }
+ val b = mTempRect
+ b.set(mImageBounds)
+ transform.mapRect(b)
+ val offsetLeft =
+ if (shouldLimit(limitTypes, LIMIT_TRANSLATION_X)) getOffset(
+ b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()
+ ) else 0f
+ val offsetTop =
+ if (shouldLimit(limitTypes, LIMIT_TRANSLATION_Y)) getOffset(
+ b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY()
+ ) else 0f
+
+ return if (offsetLeft != 0f || offsetTop != 0f) {
+ transform.postTranslate(offsetLeft, offsetTop)
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Checks whether the specified limit flag is present in the limits provided.
+ */
+ private fun shouldLimit(@LimitFlag limits: Int, @LimitFlag flag: Int): Boolean {
+ return (limits and flag) != LIMIT_NONE
+ }
+
+ /**
+ * Returns the offset necessary to make sure that:
+ * - The image is centered if it's smaller than the limit
+ * - There is no empty space if the image is bigger than the limit
+ */
+ private fun getOffset(
+ imageStart: Float, imageEnd: Float, limitStart: Float, limitEnd: Float, limitCenter: Float
+ ): Float {
+ val imageWidth = imageEnd - imageStart
+ val limitWidth = limitEnd - limitStart
+ val limitInnerWidth = minOf(limitCenter - limitStart, limitEnd - limitCenter) * 2
+
+ return when {
+ imageWidth < limitInnerWidth -> limitCenter - (imageEnd + imageStart) / 2
+ imageWidth < limitWidth -> if (limitCenter < (limitStart + limitEnd) / 2) {
+ limitStart - imageStart
+ } else {
+ limitEnd - imageEnd
+ }
+ imageStart > limitStart -> limitStart - imageStart
+ imageEnd < limitEnd -> limitEnd - imageEnd
+ else -> 0f
+ }
+ }
+
+ /** Limits the value to the given min and max range. */
+ private fun limit(value: Float, min: Float, max: Float): Float {
+ return min.coerceAtLeast(value).coerceAtMost(max)
+ }
+
+ /**
+ * Gets the scale factor for the given matrix. Assumes equal scaling for X and Y axis.
+ */
+ private fun getMatrixScaleFactor(transform: Matrix): Float {
+ transform.getValues(mTempValues)
+ return mTempValues[Matrix.MSCALE_X]
+ }
+
+ /** Checks if the matrix is an identity matrix within a given tolerance `eps`. */
+ private fun isMatrixIdentity(transform: Matrix, eps: Float): Boolean {
+ transform.getValues(mTempValues)
+ mTempValues[0] -= 1.0f // m00
+ mTempValues[4] -= 1.0f // m11
+ mTempValues[8] -= 1.0f // m22
+ return mTempValues.all { abs(it) <= eps }
+ }
+
+ /** Returns whether the scroll can happen in all directions. */
+ private fun canScrollInAllDirection(): Boolean {
+ return mTransformedImageBounds.left < mViewBounds.left - EPS &&
+ mTransformedImageBounds.top < mViewBounds.top - EPS &&
+ mTransformedImageBounds.right > mViewBounds.right + EPS &&
+ mTransformedImageBounds.bottom > mViewBounds.bottom + EPS
+ }
+
+ override fun computeHorizontalScrollRange(): Int {
+ return mTransformedImageBounds.width().toInt()
+ }
+
+ override fun computeHorizontalScrollOffset(): Int {
+ return (mViewBounds.left - mTransformedImageBounds.left).toInt()
+ }
+
+ override fun computeHorizontalScrollExtent(): Int {
+ return mViewBounds.width().toInt()
+ }
+
+ override fun computeVerticalScrollRange(): Int {
+ return mTransformedImageBounds.height().toInt()
+ }
+
+ override fun computeVerticalScrollOffset(): Int {
+ return (mViewBounds.top - mTransformedImageBounds.top).toInt()
+ }
+
+ override fun computeVerticalScrollExtent(): Int {
+ return mViewBounds.height().toInt()
+ }
+
+ fun getListener(): ZoomableController.Listener? {
+ return mListener
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.java
deleted file mode 100644
index 395b5c3881..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
-
-import android.graphics.PointF;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-/**
- * Tap gesture listener for double tap to zoom / unzoom and double-tap-and-drag to zoom.
- *
- * @see ZoomableDraweeView#setTapListener(GestureDetector.SimpleOnGestureListener)
- */
-public class DoubleTapGestureListener extends GestureDetector.SimpleOnGestureListener {
- private static final int DURATION_MS = 300;
- private static final int DOUBLE_TAP_SCROLL_THRESHOLD = 20;
-
- private final ZoomableDraweeView mDraweeView;
- private final PointF mDoubleTapViewPoint = new PointF();
- private final PointF mDoubleTapImagePoint = new PointF();
- private float mDoubleTapScale = 1;
- private boolean mDoubleTapScroll = false;
-
- public DoubleTapGestureListener(ZoomableDraweeView zoomableDraweeView) {
- mDraweeView = zoomableDraweeView;
- }
-
- @Override
- public boolean onDoubleTapEvent(MotionEvent e) {
- AbstractAnimatedZoomableController zc =
- (AbstractAnimatedZoomableController) mDraweeView.getZoomableController();
- PointF vp = new PointF(e.getX(), e.getY());
- PointF ip = zc.mapViewToImage(vp);
- switch (e.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- mDoubleTapViewPoint.set(vp);
- mDoubleTapImagePoint.set(ip);
- mDoubleTapScale = zc.getScaleFactor();
- break;
- case MotionEvent.ACTION_MOVE:
- mDoubleTapScroll = mDoubleTapScroll || shouldStartDoubleTapScroll(vp);
- if (mDoubleTapScroll) {
- float scale = calcScale(vp);
- zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint);
- }
- break;
- case MotionEvent.ACTION_UP:
- if (mDoubleTapScroll) {
- float scale = calcScale(vp);
- zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint);
- } else {
- final float maxScale = zc.getMaxScaleFactor();
- final float minScale = zc.getMinScaleFactor();
- if (zc.getScaleFactor() < (maxScale + minScale) / 2) {
- zc.zoomToPoint(
- maxScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null);
- } else {
- zc.zoomToPoint(
- minScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null);
- }
- }
- mDoubleTapScroll = false;
- break;
- }
- return true;
- }
-
- private boolean shouldStartDoubleTapScroll(PointF viewPoint) {
- double dist =
- Math.hypot(viewPoint.x - mDoubleTapViewPoint.x, viewPoint.y - mDoubleTapViewPoint.y);
- return dist > DOUBLE_TAP_SCROLL_THRESHOLD;
- }
-
- private float calcScale(PointF currentViewPoint) {
- float dy = (currentViewPoint.y - mDoubleTapViewPoint.y);
- float t = 1 + Math.abs(dy) * 0.001f;
- return (dy < 0) ? mDoubleTapScale / t : mDoubleTapScale * t;
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.kt
new file mode 100644
index 0000000000..72082abf55
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DoubleTapGestureListener.kt
@@ -0,0 +1,85 @@
+package fr.free.nrw.commons.media.zoomControllers.zoomable
+
+import android.graphics.PointF
+import android.view.GestureDetector
+import android.view.MotionEvent
+import kotlin.math.abs
+import kotlin.math.hypot
+
+/**
+ * Tap gesture listener for double tap to zoom/unzoom and double-tap-and-drag to zoom.
+ *
+ * @see ZoomableDraweeView.setTapListener
+ */
+class DoubleTapGestureListener(private val draweeView: ZoomableDraweeView) :
+ GestureDetector.SimpleOnGestureListener() {
+
+ companion object {
+ private const val DURATION_MS = 300L
+ private const val DOUBLE_TAP_SCROLL_THRESHOLD = 20
+ }
+
+ private val doubleTapViewPoint = PointF()
+ private val doubleTapImagePoint = PointF()
+ private var doubleTapScale = 1f
+ private var doubleTapScroll = false
+
+ override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+ val zc = draweeView.getZoomableController() as AbstractAnimatedZoomableController
+ val vp = PointF(e.x, e.y)
+ val ip = zc.mapViewToImage(vp)
+
+ when (e.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ doubleTapViewPoint.set(vp)
+ doubleTapImagePoint.set(ip)
+ doubleTapScale = zc.getScaleFactor()
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ doubleTapScroll = doubleTapScroll || shouldStartDoubleTapScroll(vp)
+ if (doubleTapScroll) {
+ val scale = calcScale(vp)
+ zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint)
+ }
+ }
+
+ MotionEvent.ACTION_UP -> {
+ if (doubleTapScroll) {
+ val scale = calcScale(vp)
+ zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint)
+ } else {
+ val maxScale = zc.getMaxScaleFactor()
+ val minScale = zc.getMinScaleFactor()
+ val targetScale =
+ if (zc.getScaleFactor() < (maxScale + minScale) / 2) maxScale else minScale
+
+ zc.zoomToPoint(
+ targetScale,
+ ip,
+ vp,
+ DefaultZoomableController.LIMIT_ALL,
+ DURATION_MS,
+ null
+ )
+ }
+ doubleTapScroll = false
+ }
+ }
+ return true
+ }
+
+ private fun shouldStartDoubleTapScroll(viewPoint: PointF): Boolean {
+ val dist = hypot(
+ (viewPoint.x - doubleTapViewPoint.x).toDouble(),
+ (viewPoint.y - doubleTapViewPoint.y).toDouble()
+ )
+ return dist > DOUBLE_TAP_SCROLL_THRESHOLD
+ }
+
+ private fun calcScale(currentViewPoint: PointF): Float {
+ val dy = currentViewPoint.y - doubleTapViewPoint.y
+ val t = 1 + abs(dy) * 0.001f
+ return if (dy < 0) doubleTapScale / t else doubleTapScale * t
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.java
deleted file mode 100644
index b8433de905..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
-
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-/** Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. */
-public class GestureListenerWrapper extends GestureDetector.SimpleOnGestureListener {
-
- private GestureDetector.SimpleOnGestureListener mDelegate;
-
- public GestureListenerWrapper() {
- mDelegate = new GestureDetector.SimpleOnGestureListener();
- }
-
- public void setListener(GestureDetector.SimpleOnGestureListener listener) {
- mDelegate = listener;
- }
-
- @Override
- public void onLongPress(MotionEvent e) {
- mDelegate.onLongPress(e);
- }
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- return mDelegate.onScroll(e1, e2, distanceX, distanceY);
- }
-
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- return mDelegate.onFling(e1, e2, velocityX, velocityY);
- }
-
- @Override
- public void onShowPress(MotionEvent e) {
- mDelegate.onShowPress(e);
- }
-
- @Override
- public boolean onDown(MotionEvent e) {
- return mDelegate.onDown(e);
- }
-
- @Override
- public boolean onDoubleTap(MotionEvent e) {
- return mDelegate.onDoubleTap(e);
- }
-
- @Override
- public boolean onDoubleTapEvent(MotionEvent e) {
- return mDelegate.onDoubleTapEvent(e);
- }
-
- @Override
- public boolean onSingleTapConfirmed(MotionEvent e) {
- return mDelegate.onSingleTapConfirmed(e);
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- return mDelegate.onSingleTapUp(e);
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.kt
new file mode 100644
index 0000000000..994e98cabf
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/GestureListenerWrapper.kt
@@ -0,0 +1,61 @@
+package fr.free.nrw.commons.media.zoomControllers.zoomable
+
+import android.view.GestureDetector
+import android.view.MotionEvent
+
+/** Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. */
+class GestureListenerWrapper : GestureDetector.SimpleOnGestureListener() {
+
+ private var delegate: GestureDetector.SimpleOnGestureListener =
+ GestureDetector.SimpleOnGestureListener()
+
+ fun setListener(listener: GestureDetector.SimpleOnGestureListener) {
+ delegate = listener
+ }
+
+ override fun onLongPress(e: MotionEvent) {
+ delegate.onLongPress(e)
+ }
+
+ override fun onScroll(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ return delegate.onScroll(e1, e2, distanceX, distanceY)
+ }
+
+ override fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ return delegate.onFling(e1, e2, velocityX, velocityY)
+ }
+
+ override fun onShowPress(e: MotionEvent) {
+ delegate.onShowPress(e)
+ }
+
+ override fun onDown(e: MotionEvent): Boolean {
+ return delegate.onDown(e)
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ return delegate.onDoubleTap(e)
+ }
+
+ override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+ return delegate.onDoubleTapEvent(e)
+ }
+
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ return delegate.onSingleTapConfirmed(e)
+ }
+
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
+ return delegate.onSingleTapUp(e)
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.java
deleted file mode 100644
index 05be602a7e..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
-
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Gesture listener that allows multiple child listeners to be added and notified about gesture
- * events.
- *
- * NOTE: The order of the listeners is important. Listeners can consume gesture events. For
- * example, if one of the child listeners consumes {@link #onLongPress(MotionEvent)} (the listener
- * returned true), subsequent listeners will not be notified about the event any more since it has
- * been consumed.
- */
-public class MultiGestureListener extends GestureDetector.SimpleOnGestureListener {
-
- private final List NOTE: The order of the listeners is important since gesture events can be consumed.
- *
- * @param listener the listener to be added
- */
- public synchronized void addListener(GestureDetector.SimpleOnGestureListener listener) {
- mListeners.add(listener);
- }
-
- /**
- * Removes the given listener so that it will not be notified about future events.
- *
- * NOTE: The order of the listeners is important since gesture events can be consumed.
- *
- * @param listener the listener to remove
- */
- public synchronized void removeListener(GestureDetector.SimpleOnGestureListener listener) {
- mListeners.remove(listener);
- }
-
- @Override
- public synchronized boolean onSingleTapUp(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onSingleTapUp(e)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public synchronized void onLongPress(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- mListeners.get(i).onLongPress(e);
- }
- }
-
- @Override
- public synchronized boolean onScroll(
- MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onScroll(e1, e2, distanceX, distanceY)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public synchronized boolean onFling(
- MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onFling(e1, e2, velocityX, velocityY)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public synchronized void onShowPress(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- mListeners.get(i).onShowPress(e);
- }
- }
-
- @Override
- public synchronized boolean onDown(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onDown(e)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public synchronized boolean onDoubleTap(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onDoubleTap(e)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public synchronized boolean onDoubleTapEvent(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onDoubleTapEvent(e)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public synchronized boolean onSingleTapConfirmed(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onSingleTapConfirmed(e)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public synchronized boolean onContextClick(MotionEvent e) {
- final int size = mListeners.size();
- for (int i = 0; i < size; i++) {
- if (mListeners.get(i).onContextClick(e)) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.kt
new file mode 100644
index 0000000000..70f7921c12
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.kt
@@ -0,0 +1,150 @@
+package fr.free.nrw.commons.media.zoomControllers.zoomable
+
+import android.os.Build
+import android.view.GestureDetector
+import android.view.MotionEvent
+import androidx.annotation.RequiresApi
+import java.util.Collections.synchronizedList
+
+/**
+ * Gesture listener that allows multiple child listeners to be added and notified about gesture
+ * events.
+ *
+ * NOTE: The order of the listeners is important. Listeners can consume gesture events. For
+ * example, if one of the child listeners consumes [onLongPress] (the listener returned true),
+ * subsequent listeners will not be notified about the event anymore since it has been consumed.
+ */
+class MultiGestureListener : GestureDetector.SimpleOnGestureListener() {
+
+ private val listeners: MutableList This mainly happens when a gesture would cause the image to get out of limits and the
* transform gets corrected in order to prevent that.
*/
- boolean wasTransformCorrected();
+ fun wasTransformCorrected(): Boolean
- /** See {@link androidx.core.view.ScrollingView}. */
- int computeHorizontalScrollRange();
+ /** See [androidx.core.view.ScrollingView]. */
+ fun computeHorizontalScrollRange(): Int
- int computeHorizontalScrollOffset();
+ fun computeHorizontalScrollOffset(): Int
- int computeHorizontalScrollExtent();
+ fun computeHorizontalScrollExtent(): Int
- int computeVerticalScrollRange();
+ fun computeVerticalScrollRange(): Int
- int computeVerticalScrollOffset();
+ fun computeVerticalScrollOffset(): Int
- int computeVerticalScrollExtent();
+ fun computeVerticalScrollExtent(): Int
/**
* Gets the current transform.
*
* @return the transform
*/
- Matrix getTransform();
+ fun getTransform(): Matrix
/**
* Sets the bounds of the image post transform prior to application of the zoomable
@@ -102,14 +101,14 @@ interface Listener {
*
* @param imageBounds the bounds of the image
*/
- void setImageBounds(RectF imageBounds);
+ fun setImageBounds(imageBounds: RectF)
/**
* Sets the bounds of the view.
*
* @param viewBounds the bounds of the view
*/
- void setViewBounds(RectF viewBounds);
+ fun setViewBounds(viewBounds: RectF)
/**
* Allows the controller to handle a touch event.
@@ -117,5 +116,5 @@ interface Listener {
* @param event the touch event
* @return whether the controller handled the event
*/
- boolean onTouchEvent(MotionEvent event);
+ fun onTouchEvent(event: MotionEvent): Boolean
}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.java b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.java
deleted file mode 100644
index 02d93622b8..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.java
+++ /dev/null
@@ -1,417 +0,0 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.RectF;
-import android.graphics.drawable.Animatable;
-import android.util.AttributeSet;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-
-import androidx.annotation.Nullable;
-import androidx.core.view.ScrollingView;
-import com.facebook.common.internal.Preconditions;
-import com.facebook.common.logging.FLog;
-import com.facebook.drawee.controller.AbstractDraweeController;
-import com.facebook.drawee.controller.BaseControllerListener;
-import com.facebook.drawee.controller.ControllerListener;
-import com.facebook.drawee.drawable.ScalingUtils;
-import com.facebook.drawee.generic.GenericDraweeHierarchy;
-import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
-import com.facebook.drawee.generic.GenericDraweeHierarchyInflater;
-import com.facebook.drawee.interfaces.DraweeController;
-import com.facebook.drawee.view.DraweeView;
-
-
-/**
- * DraweeView that has zoomable capabilities.
- *
- * Once the image loads, pinch-to-zoom and translation gestures are enabled.
- */
-public class ZoomableDraweeView extends DraweeView