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 3b72982f1..de65e317a 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.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/gestures/MultiPointerGestureDetector.kt index e91f47311..84dccfc07 100644 --- 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 @@ -1,6 +1,6 @@ -package fr.free.nrw.commons.media.zoomControllers.gestures; +package fr.free.nrw.commons.media.zoomControllers.gestures -import android.view.MotionEvent; +import android.view.MotionEvent /** * Component that detects and tracks multiple pointers based on touch events. @@ -9,40 +9,42 @@ import android.view.MotionEvent; * 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 { +open class MultiPointerGestureDetector { /** The listener for receiving notifications when gestures occur. */ - public interface Listener { + interface Listener { /** A callback called right before the gesture is about to start. */ - public void onGestureBegin(MultiPointerGestureDetector detector); + fun onGestureBegin(detector: MultiPointerGestureDetector) /** A callback called each time the gesture gets updated. */ - public void onGestureUpdate(MultiPointerGestureDetector detector); + fun onGestureUpdate(detector: MultiPointerGestureDetector) /** A callback called right after the gesture has finished. */ - public void onGestureEnd(MultiPointerGestureDetector detector); + fun onGestureEnd(detector: MultiPointerGestureDetector) } - private static final int MAX_POINTERS = 2; + companion object { + private const val 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 */ + fun newInstance(): MultiPointerGestureDetector { + return MultiPointerGestureDetector() + } } - /** Factory method that creates a new instance of MultiPointerGestureDetector */ - public static MultiPointerGestureDetector newInstance() { - return new 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() } /** @@ -50,16 +52,16 @@ public class MultiPointerGestureDetector { * * @param listener listener to set */ - public void setListener(Listener listener) { - mListener = listener; + fun 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; + fun reset() { + mGestureInProgress = false + mPointerCount = 0 + for (i in 0 until MAX_POINTERS) { + mId[i] = MotionEvent.INVALID_POINTER_ID } } @@ -68,27 +70,23 @@ public class MultiPointerGestureDetector { * * @return whether or not to start a new gesture */ - protected boolean shouldStartGesture() { - return true; + protected open fun shouldStartGesture(): Boolean { + return true } /** Starts a new gesture and calls the listener just before starting it. */ - private void startGesture() { + private fun startGesture() { if (!mGestureInProgress) { - if (mListener != null) { - mListener.onGestureBegin(this); - } - mGestureInProgress = true; + mListener?.onGestureBegin(this) + mGestureInProgress = true } } /** Stops the current gesture and calls the listener right after stopping it. */ - private void stopGesture() { + private fun stopGesture() { if (mGestureInProgress) { - mGestureInProgress = false; - if (mListener != null) { - mListener.onGestureEnd(this); - } + mGestureInProgress = false + mListener?.onGestureEnd(this) } } @@ -98,49 +96,53 @@ public class MultiPointerGestureDetector { * * @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(); + 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 (i >= index) { - i++; + if (adjustedIndex >= index) { + adjustedIndex++ } } - return (i < count) ? i : -1; + return if (adjustedIndex < count) adjustedIndex else -1 } /** Gets the number of pressed pointers (fingers down). */ - private static int getPressedPointerCount(MotionEvent event) { - int count = event.getPointerCount(); - int action = event.getActionMasked(); + 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--; + count-- } - return count; + return count } - private void updatePointersOnTap(MotionEvent event) { - mPointerCount = 0; - for (int i = 0; i < MAX_POINTERS; i++) { - int index = getPressedPointerIndex(event, i); + 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; + 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++; + 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 void updatePointersOnMove(MotionEvent event) { - for (int i = 0; i < MAX_POINTERS; i++) { - int index = event.findPointerIndex(mId[i]); + 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); + mCurrentX[i] = event.getX(index) + mCurrentY[i] = event.getY(index) } } } @@ -151,106 +153,100 @@ public class MultiPointerGestureDetector { * @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: - { + fun onTouchEvent(event: MotionEvent): Boolean { + when (event.actionMasked) { + MotionEvent.ACTION_MOVE -> { // update pointers - updatePointersOnMove(event); + updatePointersOnMove(event) // start a new gesture if not already started if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) { - startGesture(); + startGesture() } // notify listener - if (mGestureInProgress && mListener != null) { - mListener.onGestureUpdate(this); + if (mGestureInProgress) { + mListener?.onGestureUpdate(this) } - break; } - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: - { + 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); + mNewPointerCount = getPressedPointerCount(event) + stopGesture() + updatePointersOnTap(event) if (mPointerCount > 0 && shouldStartGesture()) { - startGesture(); + startGesture() } - break; } - case MotionEvent.ACTION_CANCEL: - { - mNewPointerCount = 0; - stopGesture(); - reset(); - break; + MotionEvent.ACTION_CANCEL -> { + mNewPointerCount = 0 + stopGesture() + reset() } } - return true; + return true } /** Restarts the current gesture (if any). */ - public void restartGesture() { + fun restartGesture() { if (!mGestureInProgress) { - return; + return } - stopGesture(); - for (int i = 0; i < MAX_POINTERS; i++) { - mStartX[i] = mCurrentX[i]; - mStartY[i] = mCurrentY[i]; + stopGesture() + for (i in 0 until MAX_POINTERS) { + mStartX[i] = mCurrentX[i] + mStartY[i] = mCurrentY[i] } - startGesture(); + startGesture() } /** Gets whether there is a gesture in progress */ - public boolean isGestureInProgress() { - return mGestureInProgress; + fun isGestureInProgress(): Boolean { + return mGestureInProgress } /** Gets the number of pointers after the current gesture */ - public int getNewPointerCount() { - return mNewPointerCount; + fun getNewPointerCount(): Int { + return mNewPointerCount } /** Gets the number of pointers in the current gesture */ - public int getPointerCount() { - return mPointerCount; + fun getPointerCount(): Int { + return mPointerCount } /** - * Gets the start X coordinates for the all pointers Mutable array is exposed for performance + * Gets the start X coordinates for all pointers Mutable array is exposed for performance * reasons and is not to be modified by the callers. */ - public float[] getStartX() { - return mStartX; + fun getStartX(): FloatArray { + return mStartX } /** - * Gets the start Y coordinates for the all pointers Mutable array is exposed for performance + * Gets the start Y coordinates for all pointers Mutable array is exposed for performance * reasons and is not to be modified by the callers. */ - public float[] getStartY() { - return mStartY; + fun getStartY(): FloatArray { + return mStartY } /** - * Gets the current X coordinates for the all pointers Mutable array is exposed for performance + * Gets the current X coordinates for all pointers Mutable array is exposed for performance * reasons and is not to be modified by the callers. */ - public float[] getCurrentX() { - return mCurrentX; + fun getCurrentX(): FloatArray { + return mCurrentX } /** - * Gets the current Y coordinates for the all pointers Mutable array is exposed for performance + * Gets the current Y coordinates for all pointers Mutable array is exposed for performance * reasons and is not to be modified by the callers. */ - public float[] getCurrentY() { - return mCurrentY; + fun getCurrentY(): FloatArray { + return mCurrentY } } 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 index 3a6baeba2..9dd0b5813 100644 --- 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 @@ -1,6 +1,8 @@ -package fr.free.nrw.commons.media.zoomControllers.gestures; +package fr.free.nrw.commons.media.zoomControllers.gestures -import android.view.MotionEvent; +import android.view.MotionEvent +import kotlin.math.atan2 +import kotlin.math.hypot /** * Component that detects translation, scale and rotation based on touch events. @@ -9,32 +11,32 @@ import android.view.MotionEvent; * this detector is passed to the listeners, so it can be queried for pivot, translation, scale or * rotation. */ -public class TransformGestureDetector implements MultiPointerGestureDetector.Listener { +class TransformGestureDetector(private val mDetector: MultiPointerGestureDetector) : + MultiPointerGestureDetector.Listener { /** The listener for receiving notifications when gestures occur. */ - public interface Listener { + interface Listener { /** A callback called right before the gesture is about to start. */ - public void onGestureBegin(TransformGestureDetector detector); + fun onGestureBegin(detector: TransformGestureDetector) /** A callback called each time the gesture gets updated. */ - public void onGestureUpdate(TransformGestureDetector detector); + fun onGestureUpdate(detector: TransformGestureDetector) /** A callback called right after the gesture has finished. */ - public void onGestureEnd(TransformGestureDetector detector); + fun onGestureEnd(detector: TransformGestureDetector) } - private final MultiPointerGestureDetector mDetector; + private var mListener: Listener? = null - private Listener mListener = null; - - public TransformGestureDetector(MultiPointerGestureDetector multiPointerGestureDetector) { - mDetector = multiPointerGestureDetector; - mDetector.setListener(this); + init { + mDetector.setListener(this) } /** Factory method that creates a new instance of TransformGestureDetector */ - public static TransformGestureDetector newInstance() { - return new TransformGestureDetector(MultiPointerGestureDetector.newInstance()); + companion object { + fun newInstance(): TransformGestureDetector { + return TransformGestureDetector(MultiPointerGestureDetector.newInstance()) + } } /** @@ -42,13 +44,13 @@ public class TransformGestureDetector implements MultiPointerGestureDetector.Lis * * @param listener listener to set */ - public void setListener(Listener listener) { - mListener = listener; + fun setListener(listener: Listener?) { + mListener = listener } /** Resets the component to the initial state. */ - public void reset() { - mDetector.reset(); + fun reset() { + mDetector.reset() } /** @@ -57,108 +59,96 @@ public class TransformGestureDetector implements MultiPointerGestureDetector.Lis * @param event event to handle * @return whether or not the event was handled */ - public boolean onTouchEvent(final MotionEvent event) { - return mDetector.onTouchEvent(event); + fun onTouchEvent(event: MotionEvent): Boolean { + return mDetector.onTouchEvent(event) } - @Override - public void onGestureBegin(MultiPointerGestureDetector detector) { - if (mListener != null) { - mListener.onGestureBegin(this); - } + override fun onGestureBegin(detector: MultiPointerGestureDetector) { + mListener?.onGestureBegin(this) } - @Override - public void onGestureUpdate(MultiPointerGestureDetector detector) { - if (mListener != null) { - mListener.onGestureUpdate(this); - } + override fun onGestureUpdate(detector: MultiPointerGestureDetector) { + mListener?.onGestureUpdate(this) } - @Override - public void onGestureEnd(MultiPointerGestureDetector detector) { - if (mListener != null) { - mListener.onGestureEnd(this); - } + override fun onGestureEnd(detector: MultiPointerGestureDetector) { + 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; + 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). */ - public void restartGesture() { - mDetector.restartGesture(); + fun restartGesture() { + mDetector.restartGesture() } /** Gets whether there is a gesture in progress */ - public boolean isGestureInProgress() { - return mDetector.isGestureInProgress(); + fun isGestureInProgress(): Boolean { + return mDetector.isGestureInProgress() } /** Gets the number of pointers after the current gesture */ - public int getNewPointerCount() { - return mDetector.getNewPointerCount(); + fun getNewPointerCount(): Int { + return mDetector.getNewPointerCount() } /** Gets the number of pointers in the current gesture */ - public int getPointerCount() { - return mDetector.getPointerCount(); + fun getPointerCount(): Int { + return mDetector.getPointerCount() } /** Gets the X coordinate of the pivot point */ - public float getPivotX() { - return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); + fun getPivotX(): Float { + return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()) } /** Gets the Y coordinate of the pivot point */ - public float getPivotY() { - return calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); + fun getPivotY(): Float { + 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()); + fun getTranslationX(): Float { + 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()); + fun getTranslationY(): Float { + return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) - + calcAverage(mDetector.getStartY(), mDetector.getPointerCount()) } /** Gets the scale */ - public float getScale() { - if (mDetector.getPointerCount() < 2) { - return 1; + fun getScale(): Float { + return if (mDetector.getPointerCount() < 2) { + 1f } 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; + 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 */ - public float getRotation() { - if (mDetector.getPointerCount() < 2) { - return 0; + fun getRotation(): Float { + return if (mDetector.getPointerCount() < 2) { + 0f } 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; + 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 } } -} \ No newline at end of file +} 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 index 700991b9e..27aa7fe2f 100644 --- 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 @@ -1,64 +1,58 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable; +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; +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. */ -public abstract class AbstractAnimatedZoomableController extends DefaultZoomableController { +abstract class AbstractAnimatedZoomableController( + transformGestureDetector: TransformGestureDetector +) : DefaultZoomableController(transformGestureDetector) { - 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(); + 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() - public AbstractAnimatedZoomableController(TransformGestureDetector transformGestureDetector) { - super(transformGestureDetector); - } - - @Override - public void reset() { - FLog.v(getLogTag(), "reset"); - stopAnimation(); - mWorkingTransform.reset(); - mNewTransform.reset(); - super.reset(); + 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 - public boolean isIdentity() { - return !isAnimating() && super.isIdentity(); + 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. + * 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 + * 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); + 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. + * 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 + * 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 @@ -68,93 +62,86 @@ public abstract class AbstractAnimatedZoomableController extends DefaultZoomable * @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); + 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 + * 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); + private fun setTransform( + newTransform: Matrix, + durationMs: Long, + onAnimationComplete: Runnable? + ) { + FLog.v(logTag, "setTransform: duration $durationMs ms") if (durationMs <= 0) { - setTransformImmediate(newTransform); + setTransformImmediate(newTransform) } else { - setTransformAnimated(newTransform, durationMs, onAnimationComplete); + setTransformAnimated(newTransform, durationMs, onAnimationComplete) } } - private void setTransformImmediate(final Matrix newTransform) { - FLog.v(getLogTag(), "setTransformImmediate"); - stopAnimation(); - mWorkingTransform.set(newTransform); - super.setTransform(newTransform); - getDetector().restartGesture(); + private fun setTransformImmediate(newTransform: Matrix) { + FLog.v(logTag, "setTransformImmediate") + stopAnimation() + workingTransform.set(newTransform) + super.setTransform(newTransform) + getDetector().restartGesture() } - protected boolean isAnimating() { - return mIsAnimating; + protected fun getIsAnimating(): Boolean = isAnimating + + protected fun setAnimating(isAnimating: Boolean) { + this.isAnimating = isAnimating } - protected void setAnimating(boolean isAnimating) { - mIsAnimating = 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) } - protected float[] getStartValues() { - return mStartValues; + override fun onGestureUpdate(detector: TransformGestureDetector) { + FLog.v(logTag, "onGestureUpdate ${if (isAnimating) "(ignored)" else ""}") + if (isAnimating) return + super.onGestureUpdate(detector) } - 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; + protected fun calculateInterpolation(outMatrix: Matrix, fraction: Float) { + for (i in 0..8) { + currentValues[i] = (1 - fraction) * startValues[i] + fraction * stopValues[i] } - super.onGestureUpdate(detector); + outMatrix.setValues(currentValues) } - 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); - } + abstract fun setTransformAnimated( + newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable? + ) - public abstract void setTransformAnimated( - final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete); + protected abstract fun stopAnimation() - protected abstract void stopAnimation(); - - protected abstract Class> getLogTag(); + protected abstract val logTag: Class<*> } 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 index 471ceb973..6ba927007 100644 --- 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 @@ -1,96 +1,75 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable; +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; +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 */ -public class AnimatedZoomableController extends AbstractAnimatedZoomableController { +class AnimatedZoomableController private constructor() : + AbstractAnimatedZoomableController(TransformGestureDetector.newInstance()) { - private static final Class> TAG = AnimatedZoomableController.class; - - private final ValueAnimator mValueAnimator; - - public static AnimatedZoomableController newInstance() { - return new AnimatedZoomableController(TransformGestureDetector.newInstance()); + private val valueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + interpolator = DecelerateInterpolator() } - @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; + companion object { + fun newInstance(): AnimatedZoomableController { + return AnimatedZoomableController() } - FLog.v(getLogTag(), "stopAnimation"); - mValueAnimator.cancel(); - mValueAnimator.removeAllUpdateListeners(); - mValueAnimator.removeAllListeners(); } - @Override - protected Class> getLogTag() { - return TAG; + @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() } -} \ No newline at end of file + + @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.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/DefaultZoomableController.kt index 9c3538f05..3d6498f6a 100644 --- 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 @@ -1,139 +1,135 @@ -package fr.free.nrw.commons.media.zoomControllers.zoomable; +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; +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. */ -public class DefaultZoomableController - implements ZoomableController, TransformGestureDetector.Listener { +open class DefaultZoomableController( + private val mGestureDetector: TransformGestureDetector +) : ZoomableController, TransformGestureDetector.Listener { /** Interface for handling call backs when the image bounds are set. */ - public interface ImageBoundsListener { - void onImageBoundsSet(RectF imageBounds); + fun interface ImageBoundsListener { + fun onImageBoundsSet(imageBounds: RectF) } @IntDef( - flag = true, - value = {LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL}) - @Retention(RetentionPolicy.SOURCE) - public @interface LimitFlag {} + flag = true, + value = [LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL] + ) + @Retention + annotation class 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; + 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 static final float EPS = 1e-3f; + private const val EPS = 1e-3f - private static final Class> TAG = DefaultZoomableController.class; + private val TAG: Class<*> = DefaultZoomableController::class.java - private static final RectF IDENTITY_RECT = new RectF(0, 0, 1, 1); + private val IDENTITY_RECT = RectF(0f, 0f, 1f, 1f) - 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()); + fun newInstance(): DefaultZoomableController { + return DefaultZoomableController(TransformGestureDetector.newInstance()) + } } - public DefaultZoomableController(TransformGestureDetector gestureDetector) { - mGestureDetector = gestureDetector; - mGestureDetector.setListener(this); + 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. */ - public void reset() { - FLog.v(TAG, "reset"); - mGestureDetector.reset(); - mPreviousTransform.reset(); - mActiveTransform.reset(); - onTransformChanged(); + open fun reset() { + FLog.v(TAG, "reset") + mGestureDetector.reset() + mPreviousTransform.reset() + mActiveTransform.reset() + onTransformChanged() } /** Sets the zoomable listener. */ - @Override - public void setListener(Listener listener) { - mListener = listener; + override fun setListener(listener: ZoomableController.Listener?) { + mListener = listener } /** Sets whether the controller is enabled or not. */ - @Override - public void setEnabled(boolean enabled) { - mIsEnabled = enabled; + override fun setEnabled(enabled: Boolean) { + mIsEnabled = enabled if (!enabled) { - reset(); + reset() } } /** Gets whether the controller is enabled or not. */ - @Override - public boolean isEnabled() { - return mIsEnabled; + override fun isEnabled(): Boolean { + return mIsEnabled } /** Sets whether the rotation gesture is enabled or not. */ - public void setRotationEnabled(boolean enabled) { - mIsRotationEnabled = enabled; + fun setRotationEnabled(enabled: Boolean) { + mIsRotationEnabled = enabled } /** Gets whether the rotation gesture is enabled or not. */ - public boolean isRotationEnabled() { - return mIsRotationEnabled; + fun isRotationEnabled(): Boolean { + return mIsRotationEnabled } /** Sets whether the scale gesture is enabled or not. */ - public void setScaleEnabled(boolean enabled) { - mIsScaleEnabled = enabled; + fun setScaleEnabled(enabled: Boolean) { + mIsScaleEnabled = enabled } /** Gets whether the scale gesture is enabled or not. */ - public boolean isScaleEnabled() { - return mIsScaleEnabled; + fun isScaleEnabled(): Boolean { + return mIsScaleEnabled } /** Sets whether the translation gesture is enabled or not. */ - public void setTranslationEnabled(boolean enabled) { - mIsTranslationEnabled = enabled; + fun setTranslationEnabled(enabled: Boolean) { + mIsTranslationEnabled = enabled } /** Gets whether the translations gesture is enabled or not. */ - public boolean isTranslationEnabled() { - return mIsTranslationEnabled; + fun isTranslationEnabled(): Boolean { + return mIsTranslationEnabled } /** @@ -141,13 +137,13 @@ public class DefaultZoomableController * *
Hierarchy's scaling (if any) is not taken into account. */ - public void setMinScaleFactor(float minScaleFactor) { - mMinScaleFactor = minScaleFactor; + fun setMinScaleFactor(minScaleFactor: Float) { + mMinScaleFactor = minScaleFactor } /** Gets the minimum scale factor allowed. */ - public float getMinScaleFactor() { - return mMinScaleFactor; + fun getMinScaleFactor(): Float { + return mMinScaleFactor } /** @@ -155,78 +151,72 @@ public class DefaultZoomableController * *
Hierarchy's scaling (if any) is not taken into account. */ - public void setMaxScaleFactor(float maxScaleFactor) { - mMaxScaleFactor = maxScaleFactor; + fun setMaxScaleFactor(maxScaleFactor: Float) { + mMaxScaleFactor = maxScaleFactor } /** Gets the maximum scale factor allowed. */ - public float getMaxScaleFactor() { - return mMaxScaleFactor; + fun getMaxScaleFactor(): Float { + return mMaxScaleFactor } /** Sets whether gesture zooms are enabled or not. */ - public void setGestureZoomEnabled(boolean isGestureZoomEnabled) { - mIsGestureZoomEnabled = isGestureZoomEnabled; + fun setGestureZoomEnabled(isGestureZoomEnabled: Boolean) { + mIsGestureZoomEnabled = isGestureZoomEnabled } /** Gets whether gesture zooms are enabled or not. */ - public boolean isGestureZoomEnabled() { - return mIsGestureZoomEnabled; + fun isGestureZoomEnabled(): Boolean { + return mIsGestureZoomEnabled } /** Gets the current scale factor. */ - @Override - public float getScaleFactor() { - return getMatrixScaleFactor(mActiveTransform); + override fun getScaleFactor(): Float { + 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); - } + 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. */ - public RectF getImageBounds() { - return mImageBounds; + fun getImageBounds(): RectF { + return mImageBounds } /** Gets the transformed image bounds, in view-absolute coordinates */ - private RectF getTransformedImageBounds() { - return mTransformedImageBounds; + private fun getTransformedImageBounds(): RectF { + return mTransformedImageBounds } /** Sets the view bounds. */ - @Override - public void setViewBounds(RectF viewBounds) { - mViewBounds.set(viewBounds); + override fun setViewBounds(viewBounds: RectF) { + mViewBounds.set(viewBounds) } /** Gets the view bounds. */ - public RectF getViewBounds() { - return mViewBounds; + fun getViewBounds(): RectF { + return mViewBounds } /** Sets the image bounds listener. */ - public void setImageBoundsListener(@Nullable ImageBoundsListener imageBoundsListener) { - mImageBoundsListener = imageBoundsListener; + fun setImageBoundsListener(imageBoundsListener: ImageBoundsListener?) { + mImageBoundsListener = imageBoundsListener } /** Gets the image bounds listener. */ - public @Nullable ImageBoundsListener getImageBoundsListener() { - return mImageBoundsListener; + fun getImageBoundsListener(): ImageBoundsListener? { + return mImageBoundsListener } /** Returns true if the zoomable transform is identity matrix. */ - @Override - public boolean isIdentity() { - return isMatrixIdentity(mActiveTransform, 1e-3f); + override fun isIdentity(): Boolean { + return isMatrixIdentity(mActiveTransform, 1e-3f) } /** @@ -235,55 +225,54 @@ public class DefaultZoomableController *
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; + 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. + *
Internal matrix is exposed for performance reasons and is not to be modified by the + * callers. */ - @Override - public Matrix getTransform() { - return mActiveTransform; + 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. */ - public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) { - outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL); + 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. */ - 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]); + 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. */ - 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]); + 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]) } /** @@ -295,46 +284,55 @@ public class DefaultZoomableController * @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(); + 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, ...]. + * 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; + 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. + * 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(); + 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. + * 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 @@ -343,78 +341,72 @@ public class DefaultZoomableController * @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; + 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. */ - public void setTransform(Matrix newTransform) { - FLog.v(TAG, "setTransform"); - mActiveTransform.set(newTransform); - onTransformChanged(); + fun setTransform(newTransform: Matrix) { + FLog.v(TAG, "setTransform") + mActiveTransform.set(newTransform) + onTransformChanged() } /** Gets the gesture detector. */ - protected TransformGestureDetector getDetector() { - return mGestureDetector; + protected fun getDetector(): TransformGestureDetector { + 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); + override fun onTouchEvent(event: MotionEvent): Boolean { + FLog.v(TAG, "onTouchEvent: action: ", event.action) + return if (mIsEnabled && mIsGestureZoomEnabled) { + mGestureDetector.onTouchEvent(event) + } else { + false } - 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 fun onGestureBegin(detector: TransformGestureDetector) { + FLog.v(TAG, "onGestureBegin") + mPreviousTransform.set(mActiveTransform) + onTransformBegin() + mWasTransformCorrected = !canScrollInAllDirection() } - @Override - public void onGestureUpdate(TransformGestureDetector detector) { - FLog.v(TAG, "onGestureUpdate"); - boolean transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL); - onTransformChanged(); + override fun onGestureUpdate(detector: TransformGestureDetector) { + FLog.v(TAG, "onGestureUpdate") + val transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL) + onTransformChanged() if (transformCorrected) { - mGestureDetector.restartGesture(); + mGestureDetector.restartGesture() } - // A transformation happened, but was it without correction? - mWasTransformCorrected = transformCorrected; + mWasTransformCorrected = transformCorrected } - @Override - public void onGestureEnd(TransformGestureDetector detector) { - FLog.v(TAG, "onGestureEnd"); - onTransformEnd(); + override fun onGestureEnd(detector: TransformGestureDetector) { + FLog.v(TAG, "onGestureEnd") + onTransformEnd() } /** @@ -424,43 +416,50 @@ public class DefaultZoomableController * @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); + private fun calculateGestureTransform( + outTransform: Matrix, + @LimitFlag limitTypes: Int + ): Boolean { + val detector = mGestureDetector + var transformCorrected = false + outTransform.set(mPreviousTransform) if (mIsRotationEnabled) { - float angle = detector.getRotation() * (float) (180 / Math.PI); - outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()); + val angle = detector.getRotation() * (180 / Math.PI).toFloat() + outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()) } if (mIsScaleEnabled) { - float scale = detector.getScale(); - outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()); + val scale = detector.getScale() + outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()) } - transformCorrected |= - limitScale(outTransform, detector.getPivotX(), detector.getPivotY(), limitTypes); + transformCorrected = transformCorrected or limitScale( + outTransform, + detector.getPivotX(), + detector.getPivotY(), + limitTypes + ) if (mIsTranslationEnabled) { - outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()); + outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()) } - transformCorrected |= limitTranslation(outTransform, limitTypes); - return transformCorrected; + transformCorrected = transformCorrected or limitTranslation(outTransform, limitTypes) + return transformCorrected } - private void onTransformBegin() { + private fun onTransformBegin() { if (mListener != null && isEnabled()) { - mListener.onTransformBegin(mActiveTransform); + mListener?.onTransformBegin(mActiveTransform) } } - private void onTransformChanged() { - mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds); + private fun onTransformChanged() { + mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds) if (mListener != null && isEnabled()) { - mListener.onTransformChanged(mActiveTransform); + mListener?.onTransformChanged(mActiveTransform) } } - private void onTransformEnd() { + private fun onTransformEnd() { if (mListener != null && isEnabled()) { - mListener.onTransformEnd(mActiveTransform); + mListener?.onTransformEnd(mActiveTransform) } } @@ -472,175 +471,137 @@ public class DefaultZoomableController * @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) { + private fun limitScale( + transform: Matrix, pivotX: Float, pivotY: Float, @LimitFlag limitTypes: Int + ): Boolean { if (!shouldLimit(limitTypes, LIMIT_SCALE)) { - return false; + 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; + 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 } - 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; + private fun limitTranslation(transform: Matrix, @LimitFlag limitTypes: Int): Boolean { + if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X or 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; + 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 } - 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;
+ 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 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
+ * 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 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;
+ 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 {
- return limitEnd - imageEnd;
+ limitEnd - imageEnd
}
+ imageStart > limitStart -> limitStart - imageStart
+ imageEnd < limitEnd -> limitEnd - imageEnd
+ else -> 0f
}
- // 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);
+ private fun limit(value: Float, min: Float, max: Float): Float {
+ return min.coerceAtLeast(value).coerceAtMost(max)
}
/**
- * Gets the scale factor for the given matrix. This method assumes the equal scaling factor for X
- * and Y axis.
+ * Gets the scale factor for the given matrix. Assumes equal scaling for X and Y axis.
*/
- private float getMatrixScaleFactor(Matrix transform) {
- transform.getValues(mTempValues);
- return mTempValues[Matrix.MSCALE_X];
+ private fun getMatrixScaleFactor(transform: Matrix): Float {
+ 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;
+ /** 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. 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;
+ /** 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
- public int computeHorizontalScrollRange() {
- return (int) mTransformedImageBounds.width();
+ override fun computeHorizontalScrollRange(): Int {
+ return mTransformedImageBounds.width().toInt()
}
- @Override
- public int computeHorizontalScrollOffset() {
- return (int) (mViewBounds.left - mTransformedImageBounds.left);
+ override fun computeHorizontalScrollOffset(): Int {
+ return (mViewBounds.left - mTransformedImageBounds.left).toInt()
}
- @Override
- public int computeHorizontalScrollExtent() {
- return (int) mViewBounds.width();
+ override fun computeHorizontalScrollExtent(): Int {
+ return mViewBounds.width().toInt()
}
- @Override
- public int computeVerticalScrollRange() {
- return (int) mTransformedImageBounds.height();
+ override fun computeVerticalScrollRange(): Int {
+ return mTransformedImageBounds.height().toInt()
}
- @Override
- public int computeVerticalScrollOffset() {
- return (int) (mViewBounds.top - mTransformedImageBounds.top);
+ override fun computeVerticalScrollOffset(): Int {
+ return (mViewBounds.top - mTransformedImageBounds.top).toInt()
}
- @Override
- public int computeVerticalScrollExtent() {
- return (int) mViewBounds.height();
+ override fun computeVerticalScrollExtent(): Int {
+ return mViewBounds.height().toInt()
}
- public Listener getListener() {
- return mListener;
+ fun getListener(): ZoomableController.Listener? {
+ return mListener
}
-}
\ 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
index 395b5c388..72082abf5 100644
--- 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
@@ -1,77 +1,85 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
+package fr.free.nrw.commons.media.zoomControllers.zoomable
-import android.graphics.PointF;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
+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.
+ * Tap gesture listener for double tap to zoom/unzoom and double-tap-and-drag to zoom.
*
- * @see ZoomableDraweeView#setTapListener(GestureDetector.SimpleOnGestureListener)
+ * @see ZoomableDraweeView.setTapListener
*/
-public class DoubleTapGestureListener extends GestureDetector.SimpleOnGestureListener {
- private static final int DURATION_MS = 300;
- private static final int DOUBLE_TAP_SCROLL_THRESHOLD = 20;
+class DoubleTapGestureListener(private val draweeView: ZoomableDraweeView) :
+ GestureDetector.SimpleOnGestureListener() {
- 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;
+ companion object {
+ private const val DURATION_MS = 300L
+ private const val DOUBLE_TAP_SCROLL_THRESHOLD = 20
}
- @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);
+ 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)
}
- break;
- case MotionEvent.ACTION_UP:
- if (mDoubleTapScroll) {
- float scale = calcScale(vp);
- zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint);
+ }
+
+ MotionEvent.ACTION_UP -> {
+ if (doubleTapScroll) {
+ val scale = calcScale(vp)
+ zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint)
} 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);
- }
+ 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
+ )
}
- mDoubleTapScroll = false;
- break;
+ doubleTapScroll = false
+ }
}
- return true;
+ 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 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 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;
+ 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
}
-}
\ No newline at end of file
+}
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
index b8433de90..994e98cab 100644
--- 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
@@ -1,63 +1,61 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
+package fr.free.nrw.commons.media.zoomControllers.zoomable
-import android.view.GestureDetector;
-import android.view.MotionEvent;
+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 {
+class GestureListenerWrapper : GestureDetector.SimpleOnGestureListener() {
- private GestureDetector.SimpleOnGestureListener mDelegate;
+ private var delegate: GestureDetector.SimpleOnGestureListener =
+ GestureDetector.SimpleOnGestureListener()
- public GestureListenerWrapper() {
- mDelegate = new GestureDetector.SimpleOnGestureListener();
+ fun setListener(listener: GestureDetector.SimpleOnGestureListener) {
+ delegate = listener
}
- public void setListener(GestureDetector.SimpleOnGestureListener listener) {
- mDelegate = listener;
+ override fun onLongPress(e: MotionEvent) {
+ delegate.onLongPress(e)
}
- @Override
- public void onLongPress(MotionEvent e) {
- mDelegate.onLongPress(e);
+ override fun onScroll(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ return delegate.onScroll(e1, e2, distanceX, distanceY)
}
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- return mDelegate.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
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- return mDelegate.onFling(e1, e2, velocityX, velocityY);
+ override fun onShowPress(e: MotionEvent) {
+ delegate.onShowPress(e)
}
- @Override
- public void onShowPress(MotionEvent e) {
- mDelegate.onShowPress(e);
+ override fun onDown(e: MotionEvent): Boolean {
+ return delegate.onDown(e)
}
- @Override
- public boolean onDown(MotionEvent e) {
- return mDelegate.onDown(e);
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ return delegate.onDoubleTap(e)
}
- @Override
- public boolean onDoubleTap(MotionEvent e) {
- return mDelegate.onDoubleTap(e);
+ override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+ return delegate.onDoubleTapEvent(e)
}
- @Override
- public boolean onDoubleTapEvent(MotionEvent e) {
- return mDelegate.onDoubleTapEvent(e);
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ return delegate.onSingleTapConfirmed(e)
}
- @Override
- public boolean onSingleTapConfirmed(MotionEvent e) {
- return mDelegate.onSingleTapConfirmed(e);
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- return mDelegate.onSingleTapUp(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.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiGestureListener.kt
index 05be602a7..70f7921c1 100644
--- 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
@@ -1,148 +1,150 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
+package fr.free.nrw.commons.media.zoomControllers.zoomable
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import java.util.ArrayList;
-import java.util.List;
+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 {@link #onLongPress(MotionEvent)} (the listener
- * returned true), subsequent listeners will not be notified about the event any more since it has
- * been consumed.
+ * 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.
*/
-public class MultiGestureListener extends GestureDetector.SimpleOnGestureListener {
+class MultiGestureListener : GestureDetector.SimpleOnGestureListener() {
- private final List NOTE: The order of the listeners is important since gesture events can be consumed.
+ * 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);
+ @Synchronized
+ fun addListener(listener: GestureDetector.SimpleOnGestureListener) {
+ listeners.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.
+ * 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);
+ @Synchronized
+ fun removeListener(listener: GestureDetector.SimpleOnGestureListener) {
+ listeners.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;
+ @Synchronized
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
+ for (listener in listeners) {
+ if (listener.onSingleTapUp(e)) {
+ return true
}
}
- return false;
+ 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);
+ @Synchronized
+ override fun onLongPress(e: MotionEvent) {
+ for (listener in listeners) {
+ listener.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;
+ @Synchronized
+ override fun onScroll(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ for (listener in listeners) {
+ if (listener.onScroll(e1, e2, distanceX, distanceY)) {
+ return true
}
}
- return false;
+ 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;
+ @Synchronized
+ override fun onFling(
+ e1: MotionEvent?,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ for (listener in listeners) {
+ if (listener.onFling(e1, e2, velocityX, velocityY)) {
+ return true
}
}
- return false;
+ 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);
+ @Synchronized
+ override fun onShowPress(e: MotionEvent) {
+ for (listener in listeners) {
+ listener.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;
+ @Synchronized
+ override fun onDown(e: MotionEvent): Boolean {
+ for (listener in listeners) {
+ if (listener.onDown(e)) {
+ return true
}
}
- return false;
+ 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;
+ @Synchronized
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ for (listener in listeners) {
+ if (listener.onDoubleTap(e)) {
+ return true
}
}
- return false;
+ 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;
+ @Synchronized
+ override fun onDoubleTapEvent(e: MotionEvent): Boolean {
+ for (listener in listeners) {
+ if (listener.onDoubleTapEvent(e)) {
+ return true
}
}
- return false;
+ 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;
+ @Synchronized
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ for (listener in listeners) {
+ if (listener.onSingleTapConfirmed(e)) {
+ return true
}
}
- return false;
+ 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;
+ @RequiresApi(Build.VERSION_CODES.M)
+ @Synchronized
+ override fun onContextClick(e: MotionEvent): Boolean {
+ for (listener in listeners) {
+ if (listener.onContextClick(e)) {
+ return true
}
}
- return false;
+ return false
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiZoomableControllerListener.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiZoomableControllerListener.kt
index 33268ed29..3bd7c00b9 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiZoomableControllerListener.kt
+++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/MultiZoomableControllerListener.kt
@@ -1,40 +1,46 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
+package fr.free.nrw.commons.media.zoomControllers.zoomable
-import android.graphics.Matrix;
-import java.util.ArrayList;
-import java.util.List;
+import android.graphics.Matrix
+import java.util.ArrayList
+/**
+ * MultiZoomableControllerListener that allows multiple listeners to be added and notified about
+ * transform events.
+ *
+ * NOTE: The order of the listeners is important. Listeners can consume transform events.
+ */
+class MultiZoomableControllerListener : ZoomableController.Listener {
-public class MultiZoomableControllerListener implements ZoomableController.Listener {
+ 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 @@ public interface ZoomableController {
*
* @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 @@ public interface ZoomableController {
* @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.kt b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.kt
index 02d93622b..5df63668b 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.kt
+++ b/app/src/main/java/fr/free/nrw/commons/media/zoomControllers/zoomable/ZoomableDraweeView.kt
@@ -1,417 +1,320 @@
-package fr.free.nrw.commons.media.zoomControllers.zoomable;
+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;
+import android.annotation.SuppressLint
+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.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.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