Migrated media/zoomControllers package to kotlin

This commit is contained in:
Saifuddin 2025-02-23 11:05:04 +05:30
parent fe925b0e29
commit fad6b191c1
14 changed files with 1221 additions and 1392 deletions

View file

@ -272,7 +272,7 @@ class ZoomableActivity : BaseActivity() {
* Handles down swipe action * Handles down swipe action
*/ */
private fun onDownSwiped() { private fun onDownSwiped() {
if (binding.zoomable.zoomableController?.isIdentity == false) { if (!binding.zoomable.getZoomableController().isIdentity()) {
return return
} }
@ -342,7 +342,7 @@ class ZoomableActivity : BaseActivity() {
* Handles up swipe action * Handles up swipe action
*/ */
private fun onUpSwiped() { private fun onUpSwiped() {
if (binding.zoomable.zoomableController?.isIdentity == false) { if (!binding.zoomable.getZoomableController().isIdentity()) {
return return
} }
@ -415,7 +415,7 @@ class ZoomableActivity : BaseActivity() {
* Handles right swipe action * Handles right swipe action
*/ */
private fun onRightSwiped(showAlreadyActionedImages: Boolean) { private fun onRightSwiped(showAlreadyActionedImages: Boolean) {
if (binding.zoomable.zoomableController?.isIdentity == false) { if (!binding.zoomable.getZoomableController().isIdentity()) {
return return
} }
@ -452,7 +452,7 @@ class ZoomableActivity : BaseActivity() {
* Handles left swipe action * Handles left swipe action
*/ */
private fun onLeftSwiped(showAlreadyActionedImages: Boolean) { private fun onLeftSwiped(showAlreadyActionedImages: Boolean) {
if (binding.zoomable.zoomableController?.isIdentity == false) { if (!binding.zoomable.getZoomableController().isIdentity()) {
return return
} }

View file

@ -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. * 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 * 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. * 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. */ /** The listener for receiving notifications when gestures occur. */
public interface Listener { interface Listener {
/** A callback called right before the gesture is about to start. */ /** 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. */ /** 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. */ /** 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; /** Factory method that creates a new instance of MultiPointerGestureDetector */
private int mPointerCount; fun newInstance(): MultiPointerGestureDetector {
private int mNewPointerCount; return MultiPointerGestureDetector()
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 */ private var mGestureInProgress = false
public static MultiPointerGestureDetector newInstance() { private var mPointerCount = 0
return new MultiPointerGestureDetector(); 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 * @param listener listener to set
*/ */
public void setListener(Listener listener) { fun setListener(listener: Listener?) {
mListener = listener; mListener = listener
} }
/** Resets the component to the initial state. */ /** Resets the component to the initial state. */
public void reset() { fun reset() {
mGestureInProgress = false; mGestureInProgress = false
mPointerCount = 0; mPointerCount = 0
for (int i = 0; i < MAX_POINTERS; i++) { for (i in 0 until MAX_POINTERS) {
mId[i] = MotionEvent.INVALID_POINTER_ID; mId[i] = MotionEvent.INVALID_POINTER_ID
} }
} }
@ -68,27 +70,23 @@ public class MultiPointerGestureDetector {
* *
* @return whether or not to start a new gesture * @return whether or not to start a new gesture
*/ */
protected boolean shouldStartGesture() { protected open fun shouldStartGesture(): Boolean {
return true; return true
} }
/** Starts a new gesture and calls the listener just before starting it. */ /** Starts a new gesture and calls the listener just before starting it. */
private void startGesture() { private fun startGesture() {
if (!mGestureInProgress) { if (!mGestureInProgress) {
if (mListener != null) { mListener?.onGestureBegin(this)
mListener.onGestureBegin(this); mGestureInProgress = true
}
mGestureInProgress = true;
} }
} }
/** Stops the current gesture and calls the listener right after stopping it. */ /** Stops the current gesture and calls the listener right after stopping it. */
private void stopGesture() { private fun stopGesture() {
if (mGestureInProgress) { if (mGestureInProgress) {
mGestureInProgress = false; mGestureInProgress = false
if (mListener != null) { mListener?.onGestureEnd(this)
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) * @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) { private fun getPressedPointerIndex(event: MotionEvent, i: Int): Int {
final int count = event.getPointerCount(); val count = event.pointerCount
final int action = event.getActionMasked(); val action = event.actionMasked
final int index = event.getActionIndex(); val index = event.actionIndex
var adjustedIndex = i
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
if (i >= index) { if (adjustedIndex >= index) {
i++; adjustedIndex++
} }
} }
return (i < count) ? i : -1; return if (adjustedIndex < count) adjustedIndex else -1
} }
/** Gets the number of pressed pointers (fingers down). */ /** Gets the number of pressed pointers (fingers down). */
private static int getPressedPointerCount(MotionEvent event) { private fun getPressedPointerCount(event: MotionEvent): Int {
int count = event.getPointerCount(); var count = event.pointerCount
int action = event.getActionMasked(); val action = event.actionMasked
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
count--; count--
} }
return count; return count
} }
private void updatePointersOnTap(MotionEvent event) { private fun updatePointersOnTap(event: MotionEvent) {
mPointerCount = 0; mPointerCount = 0
for (int i = 0; i < MAX_POINTERS; i++) { for (i in 0 until MAX_POINTERS) {
int index = getPressedPointerIndex(event, i); val index = getPressedPointerIndex(event, i)
if (index == -1) { if (index == -1) {
mId[i] = MotionEvent.INVALID_POINTER_ID; mId[i] = MotionEvent.INVALID_POINTER_ID
} else { } else {
mId[i] = event.getPointerId(index); mId[i] = event.getPointerId(index)
mCurrentX[i] = mStartX[i] = event.getX(index); mCurrentX[i] = event.getX(index)
mCurrentY[i] = mStartY[i] = event.getY(index); mStartX[i] = mCurrentX[i]
mPointerCount++; mCurrentY[i] = event.getY(index)
mStartY[i] = mCurrentY[i]
mPointerCount++
} }
} }
} }
private void updatePointersOnMove(MotionEvent event) { private fun updatePointersOnMove(event: MotionEvent) {
for (int i = 0; i < MAX_POINTERS; i++) { for (i in 0 until MAX_POINTERS) {
int index = event.findPointerIndex(mId[i]); val index = event.findPointerIndex(mId[i])
if (index != -1) { if (index != -1) {
mCurrentX[i] = event.getX(index); mCurrentX[i] = event.getX(index)
mCurrentY[i] = event.getY(index); mCurrentY[i] = event.getY(index)
} }
} }
} }
@ -151,106 +153,100 @@ public class MultiPointerGestureDetector {
* @param event event to handle * @param event event to handle
* @return whether or not the event was handled * @return whether or not the event was handled
*/ */
public boolean onTouchEvent(final MotionEvent event) { fun onTouchEvent(event: MotionEvent): Boolean {
switch (event.getActionMasked()) { when (event.actionMasked) {
case MotionEvent.ACTION_MOVE: MotionEvent.ACTION_MOVE -> {
{
// update pointers // update pointers
updatePointersOnMove(event); updatePointersOnMove(event)
// start a new gesture if not already started // start a new gesture if not already started
if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) { if (!mGestureInProgress && mPointerCount > 0 && shouldStartGesture()) {
startGesture(); startGesture()
} }
// notify listener // notify listener
if (mGestureInProgress && mListener != null) { if (mGestureInProgress) {
mListener.onGestureUpdate(this); mListener?.onGestureUpdate(this)
} }
break;
} }
case MotionEvent.ACTION_DOWN: MotionEvent.ACTION_DOWN,
case MotionEvent.ACTION_POINTER_DOWN: MotionEvent.ACTION_POINTER_DOWN,
case MotionEvent.ACTION_POINTER_UP: MotionEvent.ACTION_POINTER_UP,
case MotionEvent.ACTION_UP: MotionEvent.ACTION_UP -> {
{
// restart gesture whenever the number of pointers changes // restart gesture whenever the number of pointers changes
mNewPointerCount = getPressedPointerCount(event); mNewPointerCount = getPressedPointerCount(event)
stopGesture(); stopGesture()
updatePointersOnTap(event); updatePointersOnTap(event)
if (mPointerCount > 0 && shouldStartGesture()) { if (mPointerCount > 0 && shouldStartGesture()) {
startGesture(); startGesture()
} }
break;
} }
case MotionEvent.ACTION_CANCEL: MotionEvent.ACTION_CANCEL -> {
{ mNewPointerCount = 0
mNewPointerCount = 0; stopGesture()
stopGesture(); reset()
reset();
break;
} }
} }
return true; return true
} }
/** Restarts the current gesture (if any). */ /** Restarts the current gesture (if any). */
public void restartGesture() { fun restartGesture() {
if (!mGestureInProgress) { if (!mGestureInProgress) {
return; return
} }
stopGesture(); stopGesture()
for (int i = 0; i < MAX_POINTERS; i++) { for (i in 0 until MAX_POINTERS) {
mStartX[i] = mCurrentX[i]; mStartX[i] = mCurrentX[i]
mStartY[i] = mCurrentY[i]; mStartY[i] = mCurrentY[i]
} }
startGesture(); startGesture()
} }
/** Gets whether there is a gesture in progress */ /** Gets whether there is a gesture in progress */
public boolean isGestureInProgress() { fun isGestureInProgress(): Boolean {
return mGestureInProgress; return mGestureInProgress
} }
/** Gets the number of pointers after the current gesture */ /** Gets the number of pointers after the current gesture */
public int getNewPointerCount() { fun getNewPointerCount(): Int {
return mNewPointerCount; return mNewPointerCount
} }
/** Gets the number of pointers in the current gesture */ /** Gets the number of pointers in the current gesture */
public int getPointerCount() { fun getPointerCount(): Int {
return mPointerCount; 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. * reasons and is not to be modified by the callers.
*/ */
public float[] getStartX() { fun getStartX(): FloatArray {
return mStartX; 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. * reasons and is not to be modified by the callers.
*/ */
public float[] getStartY() { fun getStartY(): FloatArray {
return mStartY; 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. * reasons and is not to be modified by the callers.
*/ */
public float[] getCurrentX() { fun getCurrentX(): FloatArray {
return mCurrentX; 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. * reasons and is not to be modified by the callers.
*/ */
public float[] getCurrentY() { fun getCurrentY(): FloatArray {
return mCurrentY; return mCurrentY
} }
} }

View file

@ -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. * 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 * this detector is passed to the listeners, so it can be queried for pivot, translation, scale or
* rotation. * rotation.
*/ */
public class TransformGestureDetector implements MultiPointerGestureDetector.Listener { class TransformGestureDetector(private val mDetector: MultiPointerGestureDetector) :
MultiPointerGestureDetector.Listener {
/** The listener for receiving notifications when gestures occur. */ /** The listener for receiving notifications when gestures occur. */
public interface Listener { interface Listener {
/** A callback called right before the gesture is about to start. */ /** 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. */ /** 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. */ /** 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; init {
mDetector.setListener(this)
public TransformGestureDetector(MultiPointerGestureDetector multiPointerGestureDetector) {
mDetector = multiPointerGestureDetector;
mDetector.setListener(this);
} }
/** Factory method that creates a new instance of TransformGestureDetector */ /** Factory method that creates a new instance of TransformGestureDetector */
public static TransformGestureDetector newInstance() { companion object {
return new TransformGestureDetector(MultiPointerGestureDetector.newInstance()); fun newInstance(): TransformGestureDetector {
return TransformGestureDetector(MultiPointerGestureDetector.newInstance())
}
} }
/** /**
@ -42,13 +44,13 @@ public class TransformGestureDetector implements MultiPointerGestureDetector.Lis
* *
* @param listener listener to set * @param listener listener to set
*/ */
public void setListener(Listener listener) { fun setListener(listener: Listener?) {
mListener = listener; mListener = listener
} }
/** Resets the component to the initial state. */ /** Resets the component to the initial state. */
public void reset() { fun reset() {
mDetector.reset(); mDetector.reset()
} }
/** /**
@ -57,108 +59,96 @@ public class TransformGestureDetector implements MultiPointerGestureDetector.Lis
* @param event event to handle * @param event event to handle
* @return whether or not the event was handled * @return whether or not the event was handled
*/ */
public boolean onTouchEvent(final MotionEvent event) { fun onTouchEvent(event: MotionEvent): Boolean {
return mDetector.onTouchEvent(event); return mDetector.onTouchEvent(event)
} }
@Override override fun onGestureBegin(detector: MultiPointerGestureDetector) {
public void onGestureBegin(MultiPointerGestureDetector detector) { mListener?.onGestureBegin(this)
if (mListener != null) {
mListener.onGestureBegin(this);
}
} }
@Override override fun onGestureUpdate(detector: MultiPointerGestureDetector) {
public void onGestureUpdate(MultiPointerGestureDetector detector) { mListener?.onGestureUpdate(this)
if (mListener != null) {
mListener.onGestureUpdate(this);
}
} }
@Override override fun onGestureEnd(detector: MultiPointerGestureDetector) {
public void onGestureEnd(MultiPointerGestureDetector detector) { mListener?.onGestureEnd(this)
if (mListener != null) {
mListener.onGestureEnd(this);
}
} }
private float calcAverage(float[] arr, int len) { private fun calcAverage(arr: FloatArray, len: Int): Float {
float sum = 0; val sum = arr.take(len).sum()
for (int i = 0; i < len; i++) { return if (len > 0) sum / len else 0f
sum += arr[i];
}
return (len > 0) ? sum / len : 0;
} }
/** Restarts the current gesture (if any). */ /** Restarts the current gesture (if any). */
public void restartGesture() { fun restartGesture() {
mDetector.restartGesture(); mDetector.restartGesture()
} }
/** Gets whether there is a gesture in progress */ /** Gets whether there is a gesture in progress */
public boolean isGestureInProgress() { fun isGestureInProgress(): Boolean {
return mDetector.isGestureInProgress(); return mDetector.isGestureInProgress()
} }
/** Gets the number of pointers after the current gesture */ /** Gets the number of pointers after the current gesture */
public int getNewPointerCount() { fun getNewPointerCount(): Int {
return mDetector.getNewPointerCount(); return mDetector.getNewPointerCount()
} }
/** Gets the number of pointers in the current gesture */ /** Gets the number of pointers in the current gesture */
public int getPointerCount() { fun getPointerCount(): Int {
return mDetector.getPointerCount(); return mDetector.getPointerCount()
} }
/** Gets the X coordinate of the pivot point */ /** Gets the X coordinate of the pivot point */
public float getPivotX() { fun getPivotX(): Float {
return calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); return calcAverage(mDetector.getStartX(), mDetector.getPointerCount())
} }
/** Gets the Y coordinate of the pivot point */ /** Gets the Y coordinate of the pivot point */
public float getPivotY() { fun getPivotY(): Float {
return calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); return calcAverage(mDetector.getStartY(), mDetector.getPointerCount())
} }
/** Gets the X component of the translation */ /** Gets the X component of the translation */
public float getTranslationX() { fun getTranslationX(): Float {
return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) return calcAverage(mDetector.getCurrentX(), mDetector.getPointerCount()) -
- calcAverage(mDetector.getStartX(), mDetector.getPointerCount()); calcAverage(mDetector.getStartX(), mDetector.getPointerCount())
} }
/** Gets the Y component of the translation */ /** Gets the Y component of the translation */
public float getTranslationY() { fun getTranslationY(): Float {
return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) return calcAverage(mDetector.getCurrentY(), mDetector.getPointerCount()) -
- calcAverage(mDetector.getStartY(), mDetector.getPointerCount()); calcAverage(mDetector.getStartY(), mDetector.getPointerCount())
} }
/** Gets the scale */ /** Gets the scale */
public float getScale() { fun getScale(): Float {
if (mDetector.getPointerCount() < 2) { return if (mDetector.getPointerCount() < 2) {
return 1; 1f
} else { } else {
float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]
float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]
float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]
float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]
float startDist = (float) Math.hypot(startDeltaX, startDeltaY); val startDist = hypot(startDeltaX, startDeltaY)
float currentDist = (float) Math.hypot(currentDeltaX, currentDeltaY); val currentDist = hypot(currentDeltaX, currentDeltaY)
return currentDist / startDist; currentDist / startDist
} }
} }
/** Gets the rotation in radians */ /** Gets the rotation in radians */
public float getRotation() { fun getRotation(): Float {
if (mDetector.getPointerCount() < 2) { return if (mDetector.getPointerCount() < 2) {
return 0; 0f
} else { } else {
float startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]; val startDeltaX = mDetector.getStartX()[1] - mDetector.getStartX()[0]
float startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]; val startDeltaY = mDetector.getStartY()[1] - mDetector.getStartY()[0]
float currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]; val currentDeltaX = mDetector.getCurrentX()[1] - mDetector.getCurrentX()[0]
float currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]; val currentDeltaY = mDetector.getCurrentY()[1] - mDetector.getCurrentY()[0]
float startAngle = (float) Math.atan2(startDeltaY, startDeltaX); val startAngle = atan2(startDeltaY, startDeltaX)
float currentAngle = (float) Math.atan2(currentDeltaY, currentDeltaX); val currentAngle = atan2(currentDeltaY, currentDeltaX)
return currentAngle - startAngle; currentAngle - startAngle
} }
} }
} }

View file

@ -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.Matrix
import android.graphics.PointF; import android.graphics.PointF
import com.facebook.common.logging.FLog; import com.facebook.common.logging.FLog
import androidx.annotation.Nullable; import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector;
/** /**
* Abstract class for ZoomableController that adds animation capabilities to * Abstract class for ZoomableController that adds animation capabilities to
* DefaultZoomableController. * DefaultZoomableController.
*/ */
public abstract class AbstractAnimatedZoomableController extends DefaultZoomableController { abstract class AbstractAnimatedZoomableController(
transformGestureDetector: TransformGestureDetector
) : DefaultZoomableController(transformGestureDetector) {
private boolean mIsAnimating; private var isAnimating: Boolean = false
private final float[] mStartValues = new float[9]; private val startValues = FloatArray(9)
private final float[] mStopValues = new float[9]; private val stopValues = FloatArray(9)
private final float[] mCurrentValues = new float[9]; private val currentValues = FloatArray(9)
private final Matrix mNewTransform = new Matrix(); private val newTransform = Matrix()
private final Matrix mWorkingTransform = new Matrix(); private val workingTransform = Matrix()
public AbstractAnimatedZoomableController(TransformGestureDetector transformGestureDetector) { override fun reset() {
super(transformGestureDetector); FLog.v(logTag, "reset")
} stopAnimation()
workingTransform.reset()
@Override newTransform.reset()
public void reset() { super.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. */ /** Returns true if the zoomable transform is identity matrix, and the controller is idle. */
@Override override fun isIdentity(): Boolean {
public boolean isIdentity() { return !isAnimating && super.isIdentity()
return !isAnimating() && super.isIdentity();
} }
/** /**
* Zooms to the desired scale and positions the image so that the given image point corresponds to * Zooms to the desired scale and positions the image so that the given image point corresponds
* the given view point. * to the given view point.
* *
* <p>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. * animation or gesture will be stopped first.
* *
* @param scale desired scale, will be limited to {min, max} scale factor * @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 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 viewPoint 2D point in view's absolute coordinate system
*/ */
@Override override fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) {
public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { zoomToPoint(scale, imagePoint, viewPoint, LIMIT_ALL, 0, null)
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 * Zooms to the desired scale and positions the image so that the given image point corresponds
* the given view point. * to the given view point.
* *
* <p>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. * animation or gesture will be stopped first.
* *
* @param scale desired scale, will be limited to {min, max} scale factor * @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 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 * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0
*/ */
public void zoomToPoint( fun zoomToPoint(
float scale, scale: Float,
PointF imagePoint, imagePoint: PointF,
PointF viewPoint, viewPoint: PointF,
@LimitFlag int limitFlags, @LimitFlag limitFlags: Int,
long durationMs, durationMs: Long,
@Nullable Runnable onAnimationComplete) { onAnimationComplete: Runnable?
FLog.v(getLogTag(), "zoomToPoint: duration %d ms", durationMs); ) {
calculateZoomToPointTransform(mNewTransform, scale, imagePoint, viewPoint, limitFlags); FLog.v(logTag, "zoomToPoint: duration $durationMs ms")
setTransform(mNewTransform, durationMs, onAnimationComplete); calculateZoomToPointTransform(newTransform, scale, imagePoint, viewPoint, limitFlags)
setTransform(newTransform, durationMs, onAnimationComplete)
} }
/** /**
* Sets a new zoomable transformation and animates to it if desired. * Sets a new zoomable transformation and animates to it if desired.
* *
* <p>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. * animation or gesture will be stopped first.
* *
* @param newTransform new transform to make active * @param newTransform new transform to make active
* @param durationMs duration of the animation, or 0 to not animate * @param durationMs duration of the animation, or 0 to not animate
* @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0 * @param onAnimationComplete code to run when the animation completes. Ignored if durationMs=0
*/ */
public void setTransform( private fun setTransform(
Matrix newTransform, long durationMs, @Nullable Runnable onAnimationComplete) { newTransform: Matrix,
FLog.v(getLogTag(), "setTransform: duration %d ms", durationMs); durationMs: Long,
onAnimationComplete: Runnable?
) {
FLog.v(logTag, "setTransform: duration $durationMs ms")
if (durationMs <= 0) { if (durationMs <= 0) {
setTransformImmediate(newTransform); setTransformImmediate(newTransform)
} else { } else {
setTransformAnimated(newTransform, durationMs, onAnimationComplete); setTransformAnimated(newTransform, durationMs, onAnimationComplete)
} }
} }
private void setTransformImmediate(final Matrix newTransform) { private fun setTransformImmediate(newTransform: Matrix) {
FLog.v(getLogTag(), "setTransformImmediate"); FLog.v(logTag, "setTransformImmediate")
stopAnimation(); stopAnimation()
mWorkingTransform.set(newTransform); workingTransform.set(newTransform)
super.setTransform(newTransform); super.setTransform(newTransform)
getDetector().restartGesture(); getDetector().restartGesture()
} }
protected boolean isAnimating() { protected fun getIsAnimating(): Boolean = isAnimating
return mIsAnimating;
protected fun setAnimating(isAnimating: Boolean) {
this.isAnimating = isAnimating
} }
protected void setAnimating(boolean isAnimating) { protected fun getStartValues(): FloatArray = startValues
mIsAnimating = isAnimating;
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() { override fun onGestureUpdate(detector: TransformGestureDetector) {
return mStartValues; FLog.v(logTag, "onGestureUpdate ${if (isAnimating) "(ignored)" else ""}")
if (isAnimating) return
super.onGestureUpdate(detector)
} }
protected float[] getStopValues() { protected fun calculateInterpolation(outMatrix: Matrix, fraction: Float) {
return mStopValues; for (i in 0..8) {
} currentValues[i] = (1 - fraction) * startValues[i] + fraction * stopValues[i]
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); outMatrix.setValues(currentValues)
} }
protected void calculateInterpolation(Matrix outMatrix, float fraction) { abstract fun setTransformAnimated(
for (int i = 0; i < 9; i++) { newTransform: Matrix, durationMs: Long, onAnimationComplete: Runnable?
mCurrentValues[i] = (1 - fraction) * mStartValues[i] + fraction * mStopValues[i]; )
}
outMatrix.setValues(mCurrentValues);
}
public abstract void setTransformAnimated( protected abstract fun stopAnimation()
final Matrix newTransform, long durationMs, @Nullable final Runnable onAnimationComplete);
protected abstract void stopAnimation(); protected abstract val logTag: Class<*>
protected abstract Class<?> getLogTag();
} }

View file

@ -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.Animator
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator; import android.animation.ValueAnimator
import android.annotation.SuppressLint; import android.annotation.SuppressLint
import android.graphics.Matrix; import android.graphics.Matrix
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator
import com.facebook.common.internal.Preconditions; import com.facebook.common.logging.FLog
import com.facebook.common.logging.FLog; import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector
import androidx.annotation.Nullable;
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector;
/** /**
* ZoomableController that adds animation capabilities to DefaultZoomableController using standard * ZoomableController that adds animation capabilities to DefaultZoomableController using standard
* Android animation classes * Android animation classes
*/ */
public class AnimatedZoomableController extends AbstractAnimatedZoomableController { class AnimatedZoomableController private constructor() :
AbstractAnimatedZoomableController(TransformGestureDetector.newInstance()) {
private static final Class<?> TAG = AnimatedZoomableController.class; private val valueAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
interpolator = DecelerateInterpolator()
private final ValueAnimator mValueAnimator;
public static AnimatedZoomableController newInstance() {
return new AnimatedZoomableController(TransformGestureDetector.newInstance());
} }
@SuppressLint("NewApi") companion object {
public AnimatedZoomableController(TransformGestureDetector transformGestureDetector) { fun newInstance(): AnimatedZoomableController {
super(transformGestureDetector); return AnimatedZoomableController()
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 @SuppressLint("NewApi")
protected Class<?> getLogTag() { override fun setTransformAnimated(
return TAG; 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
}

View file

@ -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.Matrix
import android.graphics.PointF; import android.graphics.PointF
import android.graphics.RectF; import android.graphics.RectF
import android.view.MotionEvent; import android.view.MotionEvent
import androidx.annotation.IntDef; import androidx.annotation.IntDef
import androidx.annotation.Nullable; import com.facebook.common.logging.FLog
import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector; import fr.free.nrw.commons.media.zoomControllers.gestures.TransformGestureDetector
import com.facebook.common.logging.FLog; import kotlin.math.abs
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Zoomable controller that calculates transformation based on touch events. */ /** Zoomable controller that calculates transformation based on touch events. */
public class DefaultZoomableController open class DefaultZoomableController(
implements ZoomableController, TransformGestureDetector.Listener { private val mGestureDetector: TransformGestureDetector
) : ZoomableController, TransformGestureDetector.Listener {
/** Interface for handling call backs when the image bounds are set. */ /** Interface for handling call backs when the image bounds are set. */
public interface ImageBoundsListener { fun interface ImageBoundsListener {
void onImageBoundsSet(RectF imageBounds); fun onImageBoundsSet(imageBounds: RectF)
} }
@IntDef( @IntDef(
flag = true, flag = true,
value = {LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL}) value = [LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL]
@Retention(RetentionPolicy.SOURCE) )
public @interface LimitFlag {} @Retention
annotation class LimitFlag
public static final int LIMIT_NONE = 0; companion object {
public static final int LIMIT_TRANSLATION_X = 1; const val LIMIT_NONE = 0
public static final int LIMIT_TRANSLATION_Y = 2; const val LIMIT_TRANSLATION_X = 1
public static final int LIMIT_SCALE = 4; const val LIMIT_TRANSLATION_Y = 2
public static final int LIMIT_ALL = LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y | LIMIT_SCALE; 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; fun newInstance(): DefaultZoomableController {
return DefaultZoomableController(TransformGestureDetector.newInstance())
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) { private var mImageBoundsListener: ImageBoundsListener? = null
mGestureDetector = gestureDetector;
mGestureDetector.setListener(this); 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. */ /** Rests the controller. */
public void reset() { open fun reset() {
FLog.v(TAG, "reset"); FLog.v(TAG, "reset")
mGestureDetector.reset(); mGestureDetector.reset()
mPreviousTransform.reset(); mPreviousTransform.reset()
mActiveTransform.reset(); mActiveTransform.reset()
onTransformChanged(); onTransformChanged()
} }
/** Sets the zoomable listener. */ /** Sets the zoomable listener. */
@Override override fun setListener(listener: ZoomableController.Listener?) {
public void setListener(Listener listener) { mListener = listener
mListener = listener;
} }
/** Sets whether the controller is enabled or not. */ /** Sets whether the controller is enabled or not. */
@Override override fun setEnabled(enabled: Boolean) {
public void setEnabled(boolean enabled) { mIsEnabled = enabled
mIsEnabled = enabled;
if (!enabled) { if (!enabled) {
reset(); reset()
} }
} }
/** Gets whether the controller is enabled or not. */ /** Gets whether the controller is enabled or not. */
@Override override fun isEnabled(): Boolean {
public boolean isEnabled() { return mIsEnabled
return mIsEnabled;
} }
/** Sets whether the rotation gesture is enabled or not. */ /** Sets whether the rotation gesture is enabled or not. */
public void setRotationEnabled(boolean enabled) { fun setRotationEnabled(enabled: Boolean) {
mIsRotationEnabled = enabled; mIsRotationEnabled = enabled
} }
/** Gets whether the rotation gesture is enabled or not. */ /** Gets whether the rotation gesture is enabled or not. */
public boolean isRotationEnabled() { fun isRotationEnabled(): Boolean {
return mIsRotationEnabled; return mIsRotationEnabled
} }
/** Sets whether the scale gesture is enabled or not. */ /** Sets whether the scale gesture is enabled or not. */
public void setScaleEnabled(boolean enabled) { fun setScaleEnabled(enabled: Boolean) {
mIsScaleEnabled = enabled; mIsScaleEnabled = enabled
} }
/** Gets whether the scale gesture is enabled or not. */ /** Gets whether the scale gesture is enabled or not. */
public boolean isScaleEnabled() { fun isScaleEnabled(): Boolean {
return mIsScaleEnabled; return mIsScaleEnabled
} }
/** Sets whether the translation gesture is enabled or not. */ /** Sets whether the translation gesture is enabled or not. */
public void setTranslationEnabled(boolean enabled) { fun setTranslationEnabled(enabled: Boolean) {
mIsTranslationEnabled = enabled; mIsTranslationEnabled = enabled
} }
/** Gets whether the translations gesture is enabled or not. */ /** Gets whether the translations gesture is enabled or not. */
public boolean isTranslationEnabled() { fun isTranslationEnabled(): Boolean {
return mIsTranslationEnabled; return mIsTranslationEnabled
} }
/** /**
@ -141,13 +137,13 @@ public class DefaultZoomableController
* *
* <p>Hierarchy's scaling (if any) is not taken into account. * <p>Hierarchy's scaling (if any) is not taken into account.
*/ */
public void setMinScaleFactor(float minScaleFactor) { fun setMinScaleFactor(minScaleFactor: Float) {
mMinScaleFactor = minScaleFactor; mMinScaleFactor = minScaleFactor
} }
/** Gets the minimum scale factor allowed. */ /** Gets the minimum scale factor allowed. */
public float getMinScaleFactor() { fun getMinScaleFactor(): Float {
return mMinScaleFactor; return mMinScaleFactor
} }
/** /**
@ -155,78 +151,72 @@ public class DefaultZoomableController
* *
* <p>Hierarchy's scaling (if any) is not taken into account. * <p>Hierarchy's scaling (if any) is not taken into account.
*/ */
public void setMaxScaleFactor(float maxScaleFactor) { fun setMaxScaleFactor(maxScaleFactor: Float) {
mMaxScaleFactor = maxScaleFactor; mMaxScaleFactor = maxScaleFactor
} }
/** Gets the maximum scale factor allowed. */ /** Gets the maximum scale factor allowed. */
public float getMaxScaleFactor() { fun getMaxScaleFactor(): Float {
return mMaxScaleFactor; return mMaxScaleFactor
} }
/** Sets whether gesture zooms are enabled or not. */ /** Sets whether gesture zooms are enabled or not. */
public void setGestureZoomEnabled(boolean isGestureZoomEnabled) { fun setGestureZoomEnabled(isGestureZoomEnabled: Boolean) {
mIsGestureZoomEnabled = isGestureZoomEnabled; mIsGestureZoomEnabled = isGestureZoomEnabled
} }
/** Gets whether gesture zooms are enabled or not. */ /** Gets whether gesture zooms are enabled or not. */
public boolean isGestureZoomEnabled() { fun isGestureZoomEnabled(): Boolean {
return mIsGestureZoomEnabled; return mIsGestureZoomEnabled
} }
/** Gets the current scale factor. */ /** Gets the current scale factor. */
@Override override fun getScaleFactor(): Float {
public float getScaleFactor() { return getMatrixScaleFactor(mActiveTransform)
return getMatrixScaleFactor(mActiveTransform);
} }
/** Sets the image bounds, in view-absolute coordinates. */ /** Sets the image bounds, in view-absolute coordinates. */
@Override override fun setImageBounds(imageBounds: RectF) {
public void setImageBounds(RectF imageBounds) { if (imageBounds != mImageBounds) {
if (!imageBounds.equals(mImageBounds)) { mImageBounds.set(imageBounds)
mImageBounds.set(imageBounds); onTransformChanged()
onTransformChanged(); mImageBoundsListener?.onImageBoundsSet(mImageBounds)
if (mImageBoundsListener != null) {
mImageBoundsListener.onImageBoundsSet(mImageBounds);
}
} }
} }
/** Gets the non-transformed image bounds, in view-absolute coordinates. */ /** Gets the non-transformed image bounds, in view-absolute coordinates. */
public RectF getImageBounds() { fun getImageBounds(): RectF {
return mImageBounds; return mImageBounds
} }
/** Gets the transformed image bounds, in view-absolute coordinates */ /** Gets the transformed image bounds, in view-absolute coordinates */
private RectF getTransformedImageBounds() { private fun getTransformedImageBounds(): RectF {
return mTransformedImageBounds; return mTransformedImageBounds
} }
/** Sets the view bounds. */ /** Sets the view bounds. */
@Override override fun setViewBounds(viewBounds: RectF) {
public void setViewBounds(RectF viewBounds) { mViewBounds.set(viewBounds)
mViewBounds.set(viewBounds);
} }
/** Gets the view bounds. */ /** Gets the view bounds. */
public RectF getViewBounds() { fun getViewBounds(): RectF {
return mViewBounds; return mViewBounds
} }
/** Sets the image bounds listener. */ /** Sets the image bounds listener. */
public void setImageBoundsListener(@Nullable ImageBoundsListener imageBoundsListener) { fun setImageBoundsListener(imageBoundsListener: ImageBoundsListener?) {
mImageBoundsListener = imageBoundsListener; mImageBoundsListener = imageBoundsListener
} }
/** Gets the image bounds listener. */ /** Gets the image bounds listener. */
public @Nullable ImageBoundsListener getImageBoundsListener() { fun getImageBoundsListener(): ImageBoundsListener? {
return mImageBoundsListener; return mImageBoundsListener
} }
/** Returns true if the zoomable transform is identity matrix. */ /** Returns true if the zoomable transform is identity matrix. */
@Override override fun isIdentity(): Boolean {
public boolean isIdentity() { return isMatrixIdentity(mActiveTransform, 1e-3f)
return isMatrixIdentity(mActiveTransform, 1e-3f);
} }
/** /**
@ -235,55 +225,54 @@ public class DefaultZoomableController
* <p>We should rename this method to `wasTransformedWithoutCorrection` and just return the * <p>We should rename this method to `wasTransformedWithoutCorrection` and just return the
* internal flag directly. However, this requires interface change and negation of meaning. * internal flag directly. However, this requires interface change and negation of meaning.
*/ */
@Override override fun wasTransformCorrected(): Boolean {
public boolean wasTransformCorrected() { return mWasTransformCorrected
return mWasTransformCorrected;
} }
/** /**
* Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The * Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The
* zoomable transformation is taken into account. * zoomable transformation is taken into account.
* *
* <p>Internal matrix is exposed for performance reasons and is not to be modified by the callers. * <p>Internal matrix is exposed for performance reasons and is not to be modified by the
* callers.
*/ */
@Override override fun getTransform(): Matrix {
public Matrix getTransform() { return mActiveTransform
return mActiveTransform;
} }
/** /**
* Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The * Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The
* zoomable transformation is taken into account. * zoomable transformation is taken into account.
*/ */
public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) { fun getImageRelativeToViewAbsoluteTransform(outMatrix: Matrix) {
outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL); outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL)
} }
/** /**
* Maps point from view-absolute to image-relative coordinates. This takes into account the * Maps point from view-absolute to image-relative coordinates. This takes into account the
* zoomable transformation. * zoomable transformation.
*/ */
public PointF mapViewToImage(PointF viewPoint) { fun mapViewToImage(viewPoint: PointF): PointF {
float[] points = mTempValues; val points = mTempValues
points[0] = viewPoint.x; points[0] = viewPoint.x
points[1] = viewPoint.y; points[1] = viewPoint.y
mActiveTransform.invert(mActiveTransformInverse); mActiveTransform.invert(mActiveTransformInverse)
mActiveTransformInverse.mapPoints(points, 0, points, 0, 1); mActiveTransformInverse.mapPoints(points, 0, points, 0, 1)
mapAbsoluteToRelative(points, points, 1); mapAbsoluteToRelative(points, points, 1)
return new PointF(points[0], points[1]); return PointF(points[0], points[1])
} }
/** /**
* Maps point from image-relative to view-absolute coordinates. This takes into account the * Maps point from image-relative to view-absolute coordinates. This takes into account the
* zoomable transformation. * zoomable transformation.
*/ */
public PointF mapImageToView(PointF imagePoint) { fun mapImageToView(imagePoint: PointF): PointF {
float[] points = mTempValues; val points = mTempValues
points[0] = imagePoint.x; points[0] = imagePoint.x
points[1] = imagePoint.y; points[1] = imagePoint.y
mapRelativeToAbsolute(points, points, 1); mapRelativeToAbsolute(points, points, 1)
mActiveTransform.mapPoints(points, 0, points, 0, 1); mActiveTransform.mapPoints(points, 0, points, 0, 1)
return new PointF(points[0], points[1]); return PointF(points[0], points[1])
} }
/** /**
@ -295,46 +284,55 @@ public class DefaultZoomableController
* @param srcPoints source array * @param srcPoints source array
* @param numPoints number of points to map * @param numPoints number of points to map
*/ */
private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) { private fun mapAbsoluteToRelative(
for (int i = 0; i < numPoints; i++) { destPoints: FloatArray,
destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width(); srcPoints: FloatArray,
destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height(); 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 * 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, * into account the zoomable transformation. Points are represented by float array of
* y1, ...]. * [x0, y0, x1, y1, ...].
* *
* @param destPoints destination array (may be the same as source array) * @param destPoints destination array (may be the same as source array)
* @param srcPoints source array * @param srcPoints source array
* @param numPoints number of points to map * @param numPoints number of points to map
*/ */
private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) { private fun mapRelativeToAbsolute(
for (int i = 0; i < numPoints; i++) { destPoints: FloatArray,
destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left; srcPoints: FloatArray,
destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top; 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 * Zooms to the desired scale and positions the image so that the given image point
* the given view point. * corresponds to the given view point.
* *
* @param scale desired scale, will be limited to {min, max} scale factor * @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 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 viewPoint 2D point in view's absolute coordinate system
*/ */
public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) { open fun zoomToPoint(scale: Float, imagePoint: PointF, viewPoint: PointF) {
FLog.v(TAG, "zoomToPoint"); FLog.v(TAG, "zoomToPoint")
calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL); calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL)
onTransformChanged(); onTransformChanged()
} }
/** /**
* Calculates the zoom transformation that would zoom to the desired scale and position the image * Calculates the zoom transformation that would zoom to the desired scale and position
* so that the given image point corresponds to the given view point. * the image so that the given image point corresponds to the given view point.
* *
* @param outTransform the matrix to store the result to * @param outTransform the matrix to store the result to
* @param scale desired scale, will be limited to {min, max} scale factor * @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. * @param limitFlags whether to limit translation and/or scale.
* @return whether or not the transform has been corrected due to limitation * @return whether or not the transform has been corrected due to limitation
*/ */
protected boolean calculateZoomToPointTransform( protected fun calculateZoomToPointTransform(
Matrix outTransform, outTransform: Matrix,
float scale, scale: Float,
PointF imagePoint, imagePoint: PointF,
PointF viewPoint, viewPoint: PointF,
@LimitFlag int limitFlags) { @LimitFlag limitFlags: Int
float[] viewAbsolute = mTempValues; ): Boolean {
viewAbsolute[0] = imagePoint.x; val viewAbsolute = mTempValues
viewAbsolute[1] = imagePoint.y; viewAbsolute[0] = imagePoint.x
mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1); viewAbsolute[1] = imagePoint.y
float distanceX = viewPoint.x - viewAbsolute[0]; mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1)
float distanceY = viewPoint.y - viewAbsolute[1]; val distanceX = viewPoint.x - viewAbsolute[0]
boolean transformCorrected = false; val distanceY = viewPoint.y - viewAbsolute[1]
outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]); var transformCorrected = false
transformCorrected |= limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags); outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1])
outTransform.postTranslate(distanceX, distanceY); transformCorrected = transformCorrected or
transformCorrected |= limitTranslation(outTransform, limitFlags); limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags)
return transformCorrected; outTransform.postTranslate(distanceX, distanceY)
transformCorrected = transformCorrected or limitTranslation(outTransform, limitFlags)
return transformCorrected
} }
/** Sets a new zoom transformation. */ /** Sets a new zoom transformation. */
public void setTransform(Matrix newTransform) { fun setTransform(newTransform: Matrix) {
FLog.v(TAG, "setTransform"); FLog.v(TAG, "setTransform")
mActiveTransform.set(newTransform); mActiveTransform.set(newTransform)
onTransformChanged(); onTransformChanged()
} }
/** Gets the gesture detector. */ /** Gets the gesture detector. */
protected TransformGestureDetector getDetector() { protected fun getDetector(): TransformGestureDetector {
return mGestureDetector; return mGestureDetector
} }
/** Notifies controller of the received touch event. */ /** Notifies controller of the received touch event. */
@Override override fun onTouchEvent(event: MotionEvent): Boolean {
public boolean onTouchEvent(MotionEvent event) { FLog.v(TAG, "onTouchEvent: action: ", event.action)
FLog.v(TAG, "onTouchEvent: action: ", event.getAction()); return if (mIsEnabled && mIsGestureZoomEnabled) {
if (mIsEnabled && mIsGestureZoomEnabled) { mGestureDetector.onTouchEvent(event)
return mGestureDetector.onTouchEvent(event); } else {
false
} }
return false;
} }
/* TransformGestureDetector.Listener methods */ /* TransformGestureDetector.Listener methods */
@Override override fun onGestureBegin(detector: TransformGestureDetector) {
public void onGestureBegin(TransformGestureDetector detector) { FLog.v(TAG, "onGestureBegin")
FLog.v(TAG, "onGestureBegin"); mPreviousTransform.set(mActiveTransform)
mPreviousTransform.set(mActiveTransform); onTransformBegin()
onTransformBegin(); mWasTransformCorrected = !canScrollInAllDirection()
// 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 override fun onGestureUpdate(detector: TransformGestureDetector) {
public void onGestureUpdate(TransformGestureDetector detector) { FLog.v(TAG, "onGestureUpdate")
FLog.v(TAG, "onGestureUpdate"); val transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL)
boolean transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL); onTransformChanged()
onTransformChanged();
if (transformCorrected) { if (transformCorrected) {
mGestureDetector.restartGesture(); mGestureDetector.restartGesture()
} }
// A transformation happened, but was it without correction? mWasTransformCorrected = transformCorrected
mWasTransformCorrected = transformCorrected;
} }
@Override override fun onGestureEnd(detector: TransformGestureDetector) {
public void onGestureEnd(TransformGestureDetector detector) { FLog.v(TAG, "onGestureEnd")
FLog.v(TAG, "onGestureEnd"); onTransformEnd()
onTransformEnd();
} }
/** /**
@ -424,43 +416,50 @@ public class DefaultZoomableController
* @param limitTypes whether to limit translation and/or scale. * @param limitTypes whether to limit translation and/or scale.
* @return whether or not the transform has been corrected due to limitation * @return whether or not the transform has been corrected due to limitation
*/ */
protected boolean calculateGestureTransform(Matrix outTransform, @LimitFlag int limitTypes) { private fun calculateGestureTransform(
TransformGestureDetector detector = mGestureDetector; outTransform: Matrix,
boolean transformCorrected = false; @LimitFlag limitTypes: Int
outTransform.set(mPreviousTransform); ): Boolean {
val detector = mGestureDetector
var transformCorrected = false
outTransform.set(mPreviousTransform)
if (mIsRotationEnabled) { if (mIsRotationEnabled) {
float angle = detector.getRotation() * (float) (180 / Math.PI); val angle = detector.getRotation() * (180 / Math.PI).toFloat()
outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY()); outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY())
} }
if (mIsScaleEnabled) { if (mIsScaleEnabled) {
float scale = detector.getScale(); val scale = detector.getScale()
outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY()); outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY())
} }
transformCorrected |= transformCorrected = transformCorrected or limitScale(
limitScale(outTransform, detector.getPivotX(), detector.getPivotY(), limitTypes); outTransform,
detector.getPivotX(),
detector.getPivotY(),
limitTypes
)
if (mIsTranslationEnabled) { if (mIsTranslationEnabled) {
outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY()); outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY())
} }
transformCorrected |= limitTranslation(outTransform, limitTypes); transformCorrected = transformCorrected or limitTranslation(outTransform, limitTypes)
return transformCorrected; return transformCorrected
} }
private void onTransformBegin() { private fun onTransformBegin() {
if (mListener != null && isEnabled()) { if (mListener != null && isEnabled()) {
mListener.onTransformBegin(mActiveTransform); mListener?.onTransformBegin(mActiveTransform)
} }
} }
private void onTransformChanged() { private fun onTransformChanged() {
mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds); mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds)
if (mListener != null && isEnabled()) { if (mListener != null && isEnabled()) {
mListener.onTransformChanged(mActiveTransform); mListener?.onTransformChanged(mActiveTransform)
} }
} }
private void onTransformEnd() { private fun onTransformEnd() {
if (mListener != null && isEnabled()) { if (mListener != null && isEnabled()) {
mListener.onTransformEnd(mActiveTransform); mListener?.onTransformEnd(mActiveTransform)
} }
} }
@ -472,175 +471,137 @@ public class DefaultZoomableController
* @param limitTypes whether to limit scale. * @param limitTypes whether to limit scale.
* @return whether limiting has been applied or not * @return whether limiting has been applied or not
*/ */
private boolean limitScale( private fun limitScale(
Matrix transform, float pivotX, float pivotY, @LimitFlag int limitTypes) { transform: Matrix, pivotX: Float, pivotY: Float, @LimitFlag limitTypes: Int
): Boolean {
if (!shouldLimit(limitTypes, LIMIT_SCALE)) { if (!shouldLimit(limitTypes, LIMIT_SCALE)) {
return false; return false
} }
float currentScale = getMatrixScaleFactor(transform); val currentScale = getMatrixScaleFactor(transform)
float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor); val targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor)
if (targetScale != currentScale) { return if (targetScale != currentScale) {
float scale = targetScale / currentScale; val scale = targetScale / currentScale
transform.postScale(scale, scale, pivotX, pivotY); transform.postScale(scale, scale, pivotX, pivotY)
return true; true
} else {
false
} }
return false;
} }
/** /**
* Limits the translation so that there are no empty spaces on the sides if possible. * Limits the translation so that there are no empty spaces on the sides if possible.
*
* <p>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) { private fun limitTranslation(transform: Matrix, @LimitFlag limitTypes: Int): Boolean {
if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y)) { if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X or LIMIT_TRANSLATION_Y)) {
return false; return false
} }
RectF b = mTempRect; val b = mTempRect
b.set(mImageBounds); b.set(mImageBounds)
transform.mapRect(b); transform.mapRect(b)
float offsetLeft = val offsetLeft =
shouldLimit(limitTypes, LIMIT_TRANSLATION_X) if (shouldLimit(limitTypes, LIMIT_TRANSLATION_X)) getOffset(
? getOffset( b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()
b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX()) ) else 0f
: 0; val offsetTop =
float offsetTop = if (shouldLimit(limitTypes, LIMIT_TRANSLATION_Y)) getOffset(
shouldLimit(limitTypes, LIMIT_TRANSLATION_Y) b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY()
? getOffset( ) else 0f
b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY())
: 0; return if (offsetLeft != 0f || offsetTop != 0f) {
if (offsetLeft != 0 || offsetTop != 0) { transform.postTranslate(offsetLeft, offsetTop)
transform.postTranslate(offsetLeft, offsetTop); true
return true; } else {
false
} }
return false;
} }
/** /**
* Checks whether the specified limit flag is present in the limits provided. * Checks whether the specified limit flag is present in the limits provided.
*
* <p>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) { private fun shouldLimit(@LimitFlag limits: Int, @LimitFlag flag: Int): Boolean {
return (limits & flag) != LIMIT_NONE; return (limits and flag) != LIMIT_NONE
} }
/** /**
* Returns the offset necessary to make sure that: - the image is centered within the limit if the * Returns the offset necessary to make sure that:
* image is smaller than the limit - there is no empty space on left/right if the image is bigger * - The image is centered if it's smaller than the limit
* than the limit * - There is no empty space if the image is bigger than the limit
*/ */
private float getOffset( private fun getOffset(
float imageStart, float imageEnd, float limitStart, float limitEnd, float limitCenter) { imageStart: Float, imageEnd: Float, limitStart: Float, limitEnd: Float, limitCenter: Float
float imageWidth = imageEnd - imageStart, limitWidth = limitEnd - limitStart; ): Float {
float limitInnerWidth = Math.min(limitCenter - limitStart, limitEnd - limitCenter) * 2; val imageWidth = imageEnd - imageStart
// center if smaller than limitInnerWidth val limitWidth = limitEnd - limitStart
if (imageWidth < limitInnerWidth) { val limitInnerWidth = minOf(limitCenter - limitStart, limitEnd - limitCenter) * 2
return limitCenter - (imageEnd + imageStart) / 2;
} return when {
// to the edge if in between and limitCenter is not (limitLeft + limitRight) / 2 imageWidth < limitInnerWidth -> limitCenter - (imageEnd + imageStart) / 2
if (imageWidth < limitWidth) { imageWidth < limitWidth -> if (limitCenter < (limitStart + limitEnd) / 2) {
if (limitCenter < (limitStart + limitEnd) / 2) { limitStart - imageStart
return limitStart - imageStart;
} else { } 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. */ /** Limits the value to the given min and max range. */
private float limit(float value, float min, float max) { private fun limit(value: Float, min: Float, max: Float): Float {
return Math.min(Math.max(min, value), max); return min.coerceAtLeast(value).coerceAtMost(max)
} }
/** /**
* Gets the scale factor for the given matrix. This method assumes the equal scaling factor for X * Gets the scale factor for the given matrix. Assumes equal scaling for X and Y axis.
* and Y axis.
*/ */
private float getMatrixScaleFactor(Matrix transform) { private fun getMatrixScaleFactor(transform: Matrix): Float {
transform.getValues(mTempValues); transform.getValues(mTempValues)
return mTempValues[Matrix.MSCALE_X]; return mTempValues[Matrix.MSCALE_X]
} }
/** Same as {@code Matrix.isIdentity()}, but with tolerance {@code eps}. */ /** Checks if the matrix is an identity matrix within a given tolerance `eps`. */
private boolean isMatrixIdentity(Matrix transform, float eps) { private fun isMatrixIdentity(transform: Matrix, eps: Float): Boolean {
// Checks whether the given matrix is close enough to the identity matrix: transform.getValues(mTempValues)
// 1 0 0 mTempValues[0] -= 1.0f // m00
// 0 1 0 mTempValues[4] -= 1.0f // m11
// 0 0 1 mTempValues[8] -= 1.0f // m22
// Or equivalently to the zero matrix, after subtracting 1.0f from the diagonal elements: return mTempValues.all { abs(it) <= eps }
// 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. */ /** Returns whether the scroll can happen in all directions. */
private boolean canScrollInAllDirection() { private fun canScrollInAllDirection(): Boolean {
return mTransformedImageBounds.left < mViewBounds.left - EPS return mTransformedImageBounds.left < mViewBounds.left - EPS &&
&& mTransformedImageBounds.top < mViewBounds.top - EPS mTransformedImageBounds.top < mViewBounds.top - EPS &&
&& mTransformedImageBounds.right > mViewBounds.right + EPS mTransformedImageBounds.right > mViewBounds.right + EPS &&
&& mTransformedImageBounds.bottom > mViewBounds.bottom + EPS; mTransformedImageBounds.bottom > mViewBounds.bottom + EPS
} }
@Override override fun computeHorizontalScrollRange(): Int {
public int computeHorizontalScrollRange() { return mTransformedImageBounds.width().toInt()
return (int) mTransformedImageBounds.width();
} }
@Override override fun computeHorizontalScrollOffset(): Int {
public int computeHorizontalScrollOffset() { return (mViewBounds.left - mTransformedImageBounds.left).toInt()
return (int) (mViewBounds.left - mTransformedImageBounds.left);
} }
@Override override fun computeHorizontalScrollExtent(): Int {
public int computeHorizontalScrollExtent() { return mViewBounds.width().toInt()
return (int) mViewBounds.width();
} }
@Override override fun computeVerticalScrollRange(): Int {
public int computeVerticalScrollRange() { return mTransformedImageBounds.height().toInt()
return (int) mTransformedImageBounds.height();
} }
@Override override fun computeVerticalScrollOffset(): Int {
public int computeVerticalScrollOffset() { return (mViewBounds.top - mTransformedImageBounds.top).toInt()
return (int) (mViewBounds.top - mTransformedImageBounds.top);
} }
@Override override fun computeVerticalScrollExtent(): Int {
public int computeVerticalScrollExtent() { return mViewBounds.height().toInt()
return (int) mViewBounds.height();
} }
public Listener getListener() { fun getListener(): ZoomableController.Listener? {
return mListener; return mListener
} }
} }

View file

@ -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.graphics.PointF
import android.view.GestureDetector; import android.view.GestureDetector
import android.view.MotionEvent; 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 { class DoubleTapGestureListener(private val draweeView: ZoomableDraweeView) :
private static final int DURATION_MS = 300; GestureDetector.SimpleOnGestureListener() {
private static final int DOUBLE_TAP_SCROLL_THRESHOLD = 20;
private final ZoomableDraweeView mDraweeView; companion object {
private final PointF mDoubleTapViewPoint = new PointF(); private const val DURATION_MS = 300L
private final PointF mDoubleTapImagePoint = new PointF(); private const val DOUBLE_TAP_SCROLL_THRESHOLD = 20
private float mDoubleTapScale = 1;
private boolean mDoubleTapScroll = false;
public DoubleTapGestureListener(ZoomableDraweeView zoomableDraweeView) {
mDraweeView = zoomableDraweeView;
} }
@Override private val doubleTapViewPoint = PointF()
public boolean onDoubleTapEvent(MotionEvent e) { private val doubleTapImagePoint = PointF()
AbstractAnimatedZoomableController zc = private var doubleTapScale = 1f
(AbstractAnimatedZoomableController) mDraweeView.getZoomableController(); private var doubleTapScroll = false
PointF vp = new PointF(e.getX(), e.getY());
PointF ip = zc.mapViewToImage(vp); override fun onDoubleTapEvent(e: MotionEvent): Boolean {
switch (e.getActionMasked()) { val zc = draweeView.getZoomableController() as AbstractAnimatedZoomableController
case MotionEvent.ACTION_DOWN: val vp = PointF(e.x, e.y)
mDoubleTapViewPoint.set(vp); val ip = zc.mapViewToImage(vp)
mDoubleTapImagePoint.set(ip);
mDoubleTapScale = zc.getScaleFactor(); when (e.actionMasked) {
break; MotionEvent.ACTION_DOWN -> {
case MotionEvent.ACTION_MOVE: doubleTapViewPoint.set(vp)
mDoubleTapScroll = mDoubleTapScroll || shouldStartDoubleTapScroll(vp); doubleTapImagePoint.set(ip)
if (mDoubleTapScroll) { doubleTapScale = zc.getScaleFactor()
float scale = calcScale(vp); }
zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint);
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) { MotionEvent.ACTION_UP -> {
float scale = calcScale(vp); if (doubleTapScroll) {
zc.zoomToPoint(scale, mDoubleTapImagePoint, mDoubleTapViewPoint); val scale = calcScale(vp)
zc.zoomToPoint(scale, doubleTapImagePoint, doubleTapViewPoint)
} else { } else {
final float maxScale = zc.getMaxScaleFactor(); val maxScale = zc.getMaxScaleFactor()
final float minScale = zc.getMinScaleFactor(); val minScale = zc.getMinScaleFactor()
if (zc.getScaleFactor() < (maxScale + minScale) / 2) { val targetScale =
zc.zoomToPoint( if (zc.getScaleFactor() < (maxScale + minScale) / 2) maxScale else minScale
maxScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null);
} else { zc.zoomToPoint(
zc.zoomToPoint( targetScale,
minScale, ip, vp, DefaultZoomableController.LIMIT_ALL, DURATION_MS, null); ip,
} vp,
DefaultZoomableController.LIMIT_ALL,
DURATION_MS,
null
)
} }
mDoubleTapScroll = false; doubleTapScroll = false
break; }
} }
return true; return true
} }
private boolean shouldStartDoubleTapScroll(PointF viewPoint) { private fun shouldStartDoubleTapScroll(viewPoint: PointF): Boolean {
double dist = val dist = hypot(
Math.hypot(viewPoint.x - mDoubleTapViewPoint.x, viewPoint.y - mDoubleTapViewPoint.y); (viewPoint.x - doubleTapViewPoint.x).toDouble(),
return dist > DOUBLE_TAP_SCROLL_THRESHOLD; (viewPoint.y - doubleTapViewPoint.y).toDouble()
)
return dist > DOUBLE_TAP_SCROLL_THRESHOLD
} }
private float calcScale(PointF currentViewPoint) { private fun calcScale(currentViewPoint: PointF): Float {
float dy = (currentViewPoint.y - mDoubleTapViewPoint.y); val dy = currentViewPoint.y - doubleTapViewPoint.y
float t = 1 + Math.abs(dy) * 0.001f; val t = 1 + abs(dy) * 0.001f
return (dy < 0) ? mDoubleTapScale / t : mDoubleTapScale * t; return if (dy < 0) doubleTapScale / t else doubleTapScale * t
} }
} }

View file

@ -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.GestureDetector
import android.view.MotionEvent; import android.view.MotionEvent
/** Wrapper for SimpleOnGestureListener as GestureDetector does not allow changing its listener. */ /** 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() { fun setListener(listener: GestureDetector.SimpleOnGestureListener) {
mDelegate = new GestureDetector.SimpleOnGestureListener(); delegate = listener
} }
public void setListener(GestureDetector.SimpleOnGestureListener listener) { override fun onLongPress(e: MotionEvent) {
mDelegate = listener; delegate.onLongPress(e)
} }
@Override override fun onScroll(
public void onLongPress(MotionEvent e) { e1: MotionEvent?,
mDelegate.onLongPress(e); e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
return delegate.onScroll(e1, e2, distanceX, distanceY)
} }
@Override override fun onFling(
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { e1: MotionEvent?,
return mDelegate.onScroll(e1, e2, distanceX, distanceY); e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
return delegate.onFling(e1, e2, velocityX, velocityY)
} }
@Override override fun onShowPress(e: MotionEvent) {
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { delegate.onShowPress(e)
return mDelegate.onFling(e1, e2, velocityX, velocityY);
} }
@Override override fun onDown(e: MotionEvent): Boolean {
public void onShowPress(MotionEvent e) { return delegate.onDown(e)
mDelegate.onShowPress(e);
} }
@Override override fun onDoubleTap(e: MotionEvent): Boolean {
public boolean onDown(MotionEvent e) { return delegate.onDoubleTap(e)
return mDelegate.onDown(e);
} }
@Override override fun onDoubleTapEvent(e: MotionEvent): Boolean {
public boolean onDoubleTap(MotionEvent e) { return delegate.onDoubleTapEvent(e)
return mDelegate.onDoubleTap(e);
} }
@Override override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
public boolean onDoubleTapEvent(MotionEvent e) { return delegate.onSingleTapConfirmed(e)
return mDelegate.onDoubleTapEvent(e);
} }
@Override override fun onSingleTapUp(e: MotionEvent): Boolean {
public boolean onSingleTapConfirmed(MotionEvent e) { return delegate.onSingleTapUp(e)
return mDelegate.onSingleTapConfirmed(e);
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return mDelegate.onSingleTapUp(e);
} }
} }

View file

@ -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.os.Build
import android.view.MotionEvent; import android.view.GestureDetector
import java.util.ArrayList; import android.view.MotionEvent
import java.util.List; import androidx.annotation.RequiresApi
import java.util.Collections.synchronizedList
/** /**
* Gesture listener that allows multiple child listeners to be added and notified about gesture * Gesture listener that allows multiple child listeners to be added and notified about gesture
* events. * events.
* *
* NOTE: The order of the listeners is important. Listeners can consume gesture events. For * 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 * example, if one of the child listeners consumes [onLongPress] (the listener returned true),
* returned true), subsequent listeners will not be notified about the event any more since it has * subsequent listeners will not be notified about the event anymore since it has been consumed.
* been consumed.
*/ */
public class MultiGestureListener extends GestureDetector.SimpleOnGestureListener { class MultiGestureListener : GestureDetector.SimpleOnGestureListener() {
private final List<GestureDetector.SimpleOnGestureListener> mListeners = new ArrayList<>(); private val listeners: MutableList<GestureDetector.SimpleOnGestureListener> =
synchronizedList(mutableListOf())
/** /**
* Adds a listener to the multi gesture listener. * Adds a listener to the multi-gesture listener.
* *
* <p>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 * @param listener the listener to be added
*/ */
public synchronized void addListener(GestureDetector.SimpleOnGestureListener listener) { @Synchronized
mListeners.add(listener); fun addListener(listener: GestureDetector.SimpleOnGestureListener) {
listeners.add(listener)
} }
/** /**
* Removes the given listener so that it will not be notified about future events. * Removes the given listener so that it will not be notified about future events.
* *
* <p>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 * @param listener the listener to remove
*/ */
public synchronized void removeListener(GestureDetector.SimpleOnGestureListener listener) { @Synchronized
mListeners.remove(listener); fun removeListener(listener: GestureDetector.SimpleOnGestureListener) {
listeners.remove(listener)
} }
@Override @Synchronized
public synchronized boolean onSingleTapUp(MotionEvent e) { override fun onSingleTapUp(e: MotionEvent): Boolean {
final int size = mListeners.size(); for (listener in listeners) {
for (int i = 0; i < size; i++) { if (listener.onSingleTapUp(e)) {
if (mListeners.get(i).onSingleTapUp(e)) { return true
return true;
} }
} }
return false; return false
} }
@Override @Synchronized
public synchronized void onLongPress(MotionEvent e) { override fun onLongPress(e: MotionEvent) {
final int size = mListeners.size(); for (listener in listeners) {
for (int i = 0; i < size; i++) { listener.onLongPress(e)
mListeners.get(i).onLongPress(e);
} }
} }
@Override @Synchronized
public synchronized boolean onScroll( override fun onScroll(
MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { e1: MotionEvent?,
final int size = mListeners.size(); e2: MotionEvent,
for (int i = 0; i < size; i++) { distanceX: Float,
if (mListeners.get(i).onScroll(e1, e2, distanceX, distanceY)) { distanceY: Float
return true; ): Boolean {
for (listener in listeners) {
if (listener.onScroll(e1, e2, distanceX, distanceY)) {
return true
} }
} }
return false; return false
} }
@Override @Synchronized
public synchronized boolean onFling( override fun onFling(
MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { e1: MotionEvent?,
final int size = mListeners.size(); e2: MotionEvent,
for (int i = 0; i < size; i++) { velocityX: Float,
if (mListeners.get(i).onFling(e1, e2, velocityX, velocityY)) { velocityY: Float
return true; ): Boolean {
for (listener in listeners) {
if (listener.onFling(e1, e2, velocityX, velocityY)) {
return true
} }
} }
return false; return false
} }
@Override @Synchronized
public synchronized void onShowPress(MotionEvent e) { override fun onShowPress(e: MotionEvent) {
final int size = mListeners.size(); for (listener in listeners) {
for (int i = 0; i < size; i++) { listener.onShowPress(e)
mListeners.get(i).onShowPress(e);
} }
} }
@Override @Synchronized
public synchronized boolean onDown(MotionEvent e) { override fun onDown(e: MotionEvent): Boolean {
final int size = mListeners.size(); for (listener in listeners) {
for (int i = 0; i < size; i++) { if (listener.onDown(e)) {
if (mListeners.get(i).onDown(e)) { return true
return true;
} }
} }
return false; return false
} }
@Override @Synchronized
public synchronized boolean onDoubleTap(MotionEvent e) { override fun onDoubleTap(e: MotionEvent): Boolean {
final int size = mListeners.size(); for (listener in listeners) {
for (int i = 0; i < size; i++) { if (listener.onDoubleTap(e)) {
if (mListeners.get(i).onDoubleTap(e)) { return true
return true;
} }
} }
return false; return false
} }
@Override @Synchronized
public synchronized boolean onDoubleTapEvent(MotionEvent e) { override fun onDoubleTapEvent(e: MotionEvent): Boolean {
final int size = mListeners.size(); for (listener in listeners) {
for (int i = 0; i < size; i++) { if (listener.onDoubleTapEvent(e)) {
if (mListeners.get(i).onDoubleTapEvent(e)) { return true
return true;
} }
} }
return false; return false
} }
@Override @Synchronized
public synchronized boolean onSingleTapConfirmed(MotionEvent e) { override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
final int size = mListeners.size(); for (listener in listeners) {
for (int i = 0; i < size; i++) { if (listener.onSingleTapConfirmed(e)) {
if (mListeners.get(i).onSingleTapConfirmed(e)) { return true
return true;
} }
} }
return false; return false
} }
@Override @RequiresApi(Build.VERSION_CODES.M)
public synchronized boolean onContextClick(MotionEvent e) { @Synchronized
final int size = mListeners.size(); override fun onContextClick(e: MotionEvent): Boolean {
for (int i = 0; i < size; i++) { for (listener in listeners) {
if (mListeners.get(i).onContextClick(e)) { if (listener.onContextClick(e)) {
return true; return true
} }
} }
return false; return false
} }
} }

View file

@ -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 android.graphics.Matrix
import java.util.ArrayList; import java.util.ArrayList
import java.util.List;
/**
* 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<ZoomableController.Listener> = mutableListOf()
private final List<ZoomableController.Listener> mListeners = new ArrayList<>(); @Synchronized
override fun onTransformBegin(transform: Matrix) {
@Override for (listener in listeners) {
public synchronized void onTransformBegin(Matrix transform) { listener.onTransformBegin(transform)
for (ZoomableController.Listener listener : mListeners) {
listener.onTransformBegin(transform);
} }
} }
@Override @Synchronized
public synchronized void onTransformChanged(Matrix transform) { override fun onTransformChanged(transform: Matrix) {
for (ZoomableController.Listener listener : mListeners) { for (listener in listeners) {
listener.onTransformChanged(transform); listener.onTransformChanged(transform)
} }
} }
@Override @Synchronized
public synchronized void onTransformEnd(Matrix transform) { override fun onTransformEnd(transform: Matrix) {
for (ZoomableController.Listener listener : mListeners) { for (listener in listeners) {
listener.onTransformEnd(transform); listener.onTransformEnd(transform)
} }
} }
public synchronized void addListener(ZoomableController.Listener listener) { @Synchronized
mListeners.add(listener); fun addListener(listener: ZoomableController.Listener) {
listeners.add(listener)
} }
public synchronized void removeListener(ZoomableController.Listener listener) { @Synchronized
mListeners.remove(listener); fun removeListener(listener: ZoomableController.Listener) {
listeners.remove(listener)
} }
} }

View file

@ -1,14 +1,14 @@
package fr.free.nrw.commons.media.zoomControllers.zoomable; package fr.free.nrw.commons.media.zoomControllers.zoomable
import android.graphics.Matrix; import android.graphics.Matrix
import android.graphics.RectF; import android.graphics.RectF
import android.view.MotionEvent; import android.view.MotionEvent
/** /**
* Interface for implementing a controller that works with {@link ZoomableDraweeView} to control the * Interface for implementing a controller that works with [ZoomableDraweeView] to control the
* zoom. * zoom.
*/ */
public interface ZoomableController { interface ZoomableController {
/** Listener interface. */ /** Listener interface. */
interface Listener { interface Listener {
@ -18,21 +18,21 @@ public interface ZoomableController {
* *
* @param transform the current transform matrix * @param transform the current transform matrix
*/ */
void onTransformBegin(Matrix transform); fun onTransformBegin(transform: Matrix)
/** /**
* Notifies the view that the transform changed. * Notifies the view that the transform changed.
* *
* @param transform the new matrix * @param transform the new matrix
*/ */
void onTransformChanged(Matrix transform); fun onTransformChanged(transform: Matrix)
/** /**
* Notifies the view that the transform ended. * Notifies the view that the transform ended.
* *
* @param transform the current transform matrix * @param transform the current transform matrix
*/ */
void onTransformEnd(Matrix transform); fun onTransformEnd(transform: Matrix)
} }
/** /**
@ -40,22 +40,21 @@ public interface ZoomableController {
* *
* @param enabled whether to enable the controller * @param enabled whether to enable the controller
*/ */
void setEnabled(boolean enabled); fun setEnabled(enabled: Boolean)
/** /**
* Gets whether the controller is enabled. This should return the last value passed to {@link * Gets whether the controller is enabled. This should return the last value passed
* #setEnabled}. * to [setEnabled].
*
* @return whether the controller is enabled. * @return whether the controller is enabled.
*/ */
boolean isEnabled(); fun isEnabled(): Boolean
/** /**
* Sets the listener for the controller to call back when the matrix changes. * Sets the listener for the controller to call back when the matrix changes.
* *
* @param listener the listener * @param listener the listener
*/ */
void setListener(Listener listener); fun setListener(listener: Listener?)
/** /**
* Gets the current scale factor. A convenience method for calculating the scale from the * Gets the current scale factor. A convenience method for calculating the scale from the
@ -63,10 +62,10 @@ public interface ZoomableController {
* *
* @return the current scale factor * @return the current scale factor
*/ */
float getScaleFactor(); fun getScaleFactor(): Float
/** Returns true if the zoomable transform is identity matrix, and the controller is idle. */ /** Returns true if the zoomable transform is identity matrix, and the controller is idle. */
boolean isIdentity(); fun isIdentity(): Boolean
/** /**
* Returns true if the transform was corrected during the last update. * Returns true if the transform was corrected during the last update.
@ -74,27 +73,27 @@ public interface ZoomableController {
* <p>This mainly happens when a gesture would cause the image to get out of limits and the * <p>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. * transform gets corrected in order to prevent that.
*/ */
boolean wasTransformCorrected(); fun wasTransformCorrected(): Boolean
/** See {@link androidx.core.view.ScrollingView}. */ /** See [androidx.core.view.ScrollingView]. */
int computeHorizontalScrollRange(); 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. * Gets the current transform.
* *
* @return the transform * @return the transform
*/ */
Matrix getTransform(); fun getTransform(): Matrix
/** /**
* Sets the bounds of the image post transform prior to application of the zoomable * 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 * @param imageBounds the bounds of the image
*/ */
void setImageBounds(RectF imageBounds); fun setImageBounds(imageBounds: RectF)
/** /**
* Sets the bounds of the view. * Sets the bounds of the view.
* *
* @param viewBounds 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. * Allows the controller to handle a touch event.
@ -117,5 +116,5 @@ public interface ZoomableController {
* @param event the touch event * @param event the touch event
* @return whether the controller handled the event * @return whether the controller handled the event
*/ */
boolean onTouchEvent(MotionEvent event); fun onTouchEvent(event: MotionEvent): Boolean
} }

View file

@ -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.annotation.SuppressLint
import android.content.res.Resources; import android.content.Context
import android.graphics.Canvas; import android.content.res.Resources
import android.graphics.Matrix; import android.graphics.Canvas
import android.graphics.RectF; import android.graphics.Matrix
import android.graphics.drawable.Animatable; import android.graphics.RectF
import android.util.AttributeSet; import android.graphics.drawable.Animatable
import android.view.GestureDetector; import android.util.AttributeSet
import android.view.MotionEvent; 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 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. * DraweeView that has zoomable capabilities.
* *
* <p>Once the image loads, pinch-to-zoom and translation gestures are enabled. * <p>Once the image loads, pinch-to-zoom and translation gestures are enabled.
*/ */
public class ZoomableDraweeView extends DraweeView<GenericDraweeHierarchy> open class ZoomableDraweeView : DraweeView<GenericDraweeHierarchy>, ScrollingView {
implements ScrollingView {
private static final Class<?> TAG = ZoomableDraweeView.class; companion object {
private val TAG = ZoomableDraweeView::class.java
private static final float HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f; private const val HUGE_IMAGE_SCALE_FACTOR_THRESHOLD = 1.1f
private final RectF mImageBounds = new RectF();
private final RectF mViewBounds = new RectF();
private DraweeController mHugeImageController;
private ZoomableController mZoomableController;
private GestureDetector mTapGestureDetector;
private boolean mAllowTouchInterceptionWhileZoomed = true;
private boolean mIsDialtoneEnabled = false;
private boolean mZoomingEnabled = true;
private TransformationListener transformationListener;
private final ControllerListener mControllerListener =
new BaseControllerListener<Object>() {
@Override
public void onFinalImageSet(
String id, @Nullable Object imageInfo, @Nullable Animatable animatable) {
ZoomableDraweeView.this.onFinalImageSet();
}
@Override
public void onRelease(String id) {
ZoomableDraweeView.this.onRelease();
}
};
private final ZoomableController.Listener mZoomableListener =
new ZoomableController.Listener() {
@Override
public void onTransformBegin(Matrix transform) {}
@Override
public void onTransformChanged(Matrix transform) {
ZoomableDraweeView.this.onTransformChanged(transform);
}
@Override
public void onTransformEnd(Matrix transform) {
if (null != transformationListener) {
transformationListener.onTransformationEnd();
}
}
};
public void setTransformationListener(
TransformationListener transformationListener) {
this.transformationListener = transformationListener;
} }
private final GestureListenerWrapper mTapListenerWrapper = new GestureListenerWrapper(); private val imageBounds = RectF()
private val viewBounds = RectF()
public ZoomableDraweeView(Context context, GenericDraweeHierarchy hierarchy) { private var hugeImageController: DraweeController? = null
super(context); private var zoomableController: ZoomableController = createZoomableController()
setHierarchy(hierarchy); private var tapGestureDetector: GestureDetector? = null
init(); private var allowTouchInterceptionWhileZoomed = true
} private var isDialToneEnabled = false
private var zoomingEnabled = true
private var transformationListener: TransformationListener? = null
public ZoomableDraweeView(Context context) { private val controllerListener = object : BaseControllerListener<Any>() {
super(context); override fun onFinalImageSet(id: String, imageInfo: Any?, animatable: Animatable?) {
inflateHierarchy(context, null); this@ZoomableDraweeView.onFinalImageSet()
init(); }
}
public ZoomableDraweeView(Context context, AttributeSet attrs) { override fun onRelease(id: String) {
super(context, attrs); this@ZoomableDraweeView.onRelease()
inflateHierarchy(context, attrs);
init();
}
public ZoomableDraweeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflateHierarchy(context, attrs);
init();
}
protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) {
Resources resources = context.getResources();
GenericDraweeHierarchyBuilder builder =
new GenericDraweeHierarchyBuilder(resources)
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER);
GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs);
setAspectRatio(builder.getDesiredAspectRatio());
setHierarchy(builder.build());
}
private void init() {
mZoomableController = createZoomableController();
mZoomableController.setListener(mZoomableListener);
mTapGestureDetector = new GestureDetector(getContext(), mTapListenerWrapper);
}
public void setIsDialtoneEnabled(boolean isDialtoneEnabled) {
mIsDialtoneEnabled = isDialtoneEnabled;
}
/**
* Gets the original image bounds, in view-absolute coordinates.
*
* <p>The original image bounds are those reported by the hierarchy. The hierarchy itself may
* apply scaling on its own (e.g. due to scale type) so the reported bounds are not necessarily
* the same as the actual bitmap dimensions. In other words, the original image bounds correspond
* to the image bounds within this view when no zoomable transformation is applied, but including
* the potential scaling of the hierarchy. Having the actual bitmap dimensions abstracted away
* from this view greatly simplifies implementation because the actual bitmap may change (e.g.
* when a high-res image arrives and replaces the previously set low-res image). With proper
* hierarchy scaling (e.g. FIT_CENTER), this underlying change will not affect this view nor the
* zoomable transformation in any way.
*/
protected void getImageBounds(RectF outBounds) {
getHierarchy().getActualImageBounds(outBounds);
}
/**
* Gets the bounds used to limit the translation, in view-absolute coordinates.
*
* <p>These bounds are passed to the zoomable controller in order to limit the translation. The
* image is attempted to be centered within the limit bounds if the transformed image is smaller.
* There will be no empty spaces within the limit bounds if the transformed image is bigger. This
* applies to each dimension (horizontal and vertical) independently.
*
* <p>Unless overridden by a subclass, these bounds are same as the view bounds.
*/
protected void getLimitBounds(RectF outBounds) {
outBounds.set(0, 0, getWidth(), getHeight());
}
/** Sets a custom zoomable controller, instead of using the default one. */
public void setZoomableController(ZoomableController zoomableController) {
Preconditions.checkNotNull(zoomableController);
mZoomableController.setListener(null);
mZoomableController = zoomableController;
mZoomableController.setListener(mZoomableListener);
}
/**
* Gets the zoomable controller.
*
* <p>Zoomable controller can be used to zoom to point, or to map point from view to image
* coordinates for instance.
*/
public ZoomableController getZoomableController() {
return mZoomableController;
}
/**
* Check whether the parent view can intercept touch events while zoomed. This can be used, for
* example, to swipe between images in a view pager while zoomed.
*
* @return true if touch events can be intercepted
*/
public boolean allowsTouchInterceptionWhileZoomed() {
return mAllowTouchInterceptionWhileZoomed;
}
/**
* If this is set to true, parent views can intercept touch events while the view is zoomed. For
* example, this can be used to swipe between images in a view pager while zoomed.
*
* @param allowTouchInterceptionWhileZoomed true if the parent needs to intercept touches
*/
public void setAllowTouchInterceptionWhileZoomed(boolean allowTouchInterceptionWhileZoomed) {
mAllowTouchInterceptionWhileZoomed = allowTouchInterceptionWhileZoomed;
}
/** Sets the tap listener. */
public void setTapListener(GestureDetector.SimpleOnGestureListener tapListener) {
mTapListenerWrapper.setListener(tapListener);
}
/**
* Sets whether long-press tap detection is enabled. Unfortunately, long-press conflicts with
* onDoubleTapEvent.
*/
public void setIsLongpressEnabled(boolean enabled) {
mTapGestureDetector.setIsLongpressEnabled(enabled);
}
public void setZoomingEnabled(boolean zoomingEnabled) {
mZoomingEnabled = zoomingEnabled;
mZoomableController.setEnabled(false);
}
/** Sets the image controller. */
@Override
public void setController(@Nullable DraweeController controller) {
setControllers(controller, null);
}
/**
* Sets the controllers for the normal and huge image.
*
* <p>The huge image controller is used after the image gets scaled above a certain threshold.
*
* <p>IMPORTANT: in order to avoid a flicker when switching to the huge image, the huge image
* controller should have the normal-image-uri set as its low-res-uri.
*
* @param controller controller to be initially used
* @param hugeImageController controller to be used after the client starts zooming-in
*/
public void setControllers(
@Nullable DraweeController controller, @Nullable DraweeController hugeImageController) {
setControllersInternal(null, null);
mZoomableController.setEnabled(false);
setControllersInternal(controller, hugeImageController);
}
private void setControllersInternal(
@Nullable DraweeController controller, @Nullable DraweeController hugeImageController) {
removeControllerListener(getController());
addControllerListener(controller);
mHugeImageController = hugeImageController;
super.setController(controller);
}
private void maybeSetHugeImageController() {
if (mHugeImageController != null
&& mZoomableController.getScaleFactor() > HUGE_IMAGE_SCALE_FACTOR_THRESHOLD) {
setControllersInternal(mHugeImageController, null);
} }
} }
private void removeControllerListener(DraweeController controller) { private val zoomableListener = object : ZoomableController.Listener {
if (controller instanceof AbstractDraweeController) { override fun onTransformBegin(transform: Matrix) {}
((AbstractDraweeController) controller).removeControllerListener(mControllerListener);
override fun onTransformChanged(transform: Matrix) {
this@ZoomableDraweeView.onTransformChanged(transform)
}
override fun onTransformEnd(transform: Matrix) {
transformationListener?.onTransformationEnd()
} }
} }
private void addControllerListener(DraweeController controller) { private val tapListenerWrapper = GestureListenerWrapper()
if (controller instanceof AbstractDraweeController) {
((AbstractDraweeController) controller).addControllerListener(mControllerListener); constructor(context: Context, hierarchy: GenericDraweeHierarchy) : super(context) {
setHierarchy(hierarchy)
init()
}
constructor(context: Context) : super(context) {
inflateHierarchy(context, null)
init()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
inflateHierarchy(context, attrs)
init()
}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int)
: super(context, attrs, defStyle) {
inflateHierarchy(context, attrs)
init()
}
fun setTransformationListener(transformationListener: TransformationListener) {
this.transformationListener = transformationListener
}
protected fun inflateHierarchy(context: Context, attrs: AttributeSet?) {
val resources: Resources = context.resources
val builder = GenericDraweeHierarchyBuilder(resources)
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
GenericDraweeHierarchyInflater.updateBuilder(builder, context, attrs)
aspectRatio = builder.desiredAspectRatio
setHierarchy(builder.build())
}
private fun init() {
zoomableController.setListener(zoomableListener)
tapGestureDetector = GestureDetector(context, tapListenerWrapper)
}
fun setIsDialToneEnabled(isDialtoneEnabled: Boolean) {
this.isDialToneEnabled = isDialtoneEnabled
}
protected fun getImageBounds(outBounds: RectF) {
hierarchy.getActualImageBounds(outBounds)
}
protected fun getLimitBounds(outBounds: RectF) {
outBounds.set(0f, 0f, width.toFloat(), height.toFloat())
}
fun setZoomableController(zoomableController: ZoomableController) {
Preconditions.checkNotNull(zoomableController)
this.zoomableController.setListener(null)
this.zoomableController = zoomableController
this.zoomableController.setListener(zoomableListener)
}
fun getZoomableController(): ZoomableController = zoomableController
fun allowsTouchInterceptionWhileZoomed(): Boolean = allowTouchInterceptionWhileZoomed
fun setAllowTouchInterceptionWhileZoomed(allow: Boolean) {
allowTouchInterceptionWhileZoomed = allow
}
fun setTapListener(tapListener: GestureDetector.SimpleOnGestureListener) {
tapListenerWrapper.setListener(tapListener)
}
fun setIsLongpressEnabled(enabled: Boolean) {
tapGestureDetector?.setIsLongpressEnabled(enabled)
}
fun setZoomingEnabled(zoomingEnabled: Boolean) {
this.zoomingEnabled = zoomingEnabled
zoomableController.setEnabled(false)
}
override fun setController(controller: DraweeController?) {
setControllers(controller, null)
}
fun setControllers(controller: DraweeController?, hugeImageController: DraweeController?) {
setControllersInternal(null, null)
zoomableController.setEnabled(false)
setControllersInternal(controller, hugeImageController)
}
private fun setControllersInternal(
controller: DraweeController?,
hugeImageController: DraweeController?
) {
removeControllerListener(getController())
addControllerListener(controller)
this.hugeImageController = hugeImageController
super.setController(controller)
}
private fun maybeSetHugeImageController() {
if (
hugeImageController != null
&&
zoomableController.getScaleFactor() > HUGE_IMAGE_SCALE_FACTOR_THRESHOLD
) {
setControllersInternal(hugeImageController, null)
} }
} }
@Override private fun removeControllerListener(controller: DraweeController?) {
protected void onDraw(Canvas canvas) { if (controller is AbstractDraweeController<*, *>) {
int saveCount = canvas.save(); controller.removeControllerListener(controllerListener)
canvas.concat(mZoomableController.getTransform()); }
}
private fun addControllerListener(controller: DraweeController?) {
if (controller is AbstractDraweeController<*, *>) {
controller.addControllerListener(controllerListener)
}
}
override fun onDraw(canvas: Canvas) {
val saveCount = canvas.save()
canvas.concat(zoomableController.getTransform())
try { try {
super.onDraw(canvas); super.onDraw(canvas)
} catch (Exception e) { } catch (e: Exception) {
DraweeController controller = getController(); val controller = controller
if (controller != null && controller instanceof AbstractDraweeController) { if (controller is AbstractDraweeController<*, *>) {
Object callerContext = ((AbstractDraweeController) controller).getCallerContext(); val callerContext = controller.callerContext
if (callerContext != null) { if (callerContext != null) {
throw new RuntimeException( throw RuntimeException("Exception in onDraw, callerContext=${callerContext}", e)
String.format("Exception in onDraw, callerContext=%s", callerContext.toString()), e);
} }
} }
throw e; throw e
} }
canvas.restoreToCount(saveCount); canvas.restoreToCount(saveCount)
} }
@Override @SuppressLint("ClickableViewAccessibility")
public boolean onTouchEvent(MotionEvent event) { override fun onTouchEvent(event: MotionEvent): Boolean {
int a = event.getActionMasked(); var action = event.actionMasked
FLog.v(getLogTag(), "onTouchEvent: %d, view %x, received", a, this.hashCode()); FLog.v(getLogTag(), "onTouchEvent: $action, view ${hashCode()}, received")
if (!mIsDialtoneEnabled && mTapGestureDetector.onTouchEvent(event)) {
if (!isDialToneEnabled && tapGestureDetector?.onTouchEvent(event) == true) {
FLog.v( FLog.v(
getLogTag(),
"onTouchEvent: %d, view %x, handled by tap gesture detector",
a,
this.hashCode());
return true;
}
if (!mIsDialtoneEnabled && mZoomableController.onTouchEvent(event)) {
FLog.v(
getLogTag(),
"onTouchEvent: %d, view %x, handled by zoomable controller",
a,
this.hashCode());
if (!mAllowTouchInterceptionWhileZoomed && !mZoomableController.isIdentity()) {
getParent().requestDisallowInterceptTouchEvent(true);
}
return true;
}
if (super.onTouchEvent(event)) {
FLog.v(getLogTag(), "onTouchEvent: %d, view %x, handled by the super", a, this.hashCode());
return true;
}
// None of our components reported that they handled the touch event. Upon returning false
// from this method, our parent won't send us any more events for this gesture. Unfortunately,
// some components may have started a delayed action, such as a long-press timer, and since we
// won't receive an ACTION_UP that would cancel that timer, a false event may be triggered.
// To prevent that we explicitly send one last cancel event when returning false.
MotionEvent cancelEvent = MotionEvent.obtain(event);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
mTapGestureDetector.onTouchEvent(cancelEvent);
mZoomableController.onTouchEvent(cancelEvent);
cancelEvent.recycle();
return false;
}
@Override
public int computeHorizontalScrollRange() {
return mZoomableController.computeHorizontalScrollRange();
}
@Override
public int computeHorizontalScrollOffset() {
return mZoomableController.computeHorizontalScrollOffset();
}
@Override
public int computeHorizontalScrollExtent() {
return mZoomableController.computeHorizontalScrollExtent();
}
@Override
public int computeVerticalScrollRange() {
return mZoomableController.computeVerticalScrollRange();
}
@Override
public int computeVerticalScrollOffset() {
return mZoomableController.computeVerticalScrollOffset();
}
@Override
public int computeVerticalScrollExtent() {
return mZoomableController.computeVerticalScrollExtent();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
FLog.v(getLogTag(), "onLayout: view %x", this.hashCode());
super.onLayout(changed, left, top, right, bottom);
updateZoomableControllerBounds();
}
private void onFinalImageSet() {
FLog.v(getLogTag(), "onFinalImageSet: view %x", this.hashCode());
if (!mZoomableController.isEnabled() && mZoomingEnabled) {
mZoomableController.setEnabled(true);
updateZoomableControllerBounds();
}
}
private void onRelease() {
FLog.v(getLogTag(), "onRelease: view %x", this.hashCode());
mZoomableController.setEnabled(false);
}
protected void onTransformChanged(Matrix transform) {
FLog.v(getLogTag(), "onTransformChanged: view %x, transform: %s", this.hashCode(), transform);
maybeSetHugeImageController();
invalidate();
}
protected void updateZoomableControllerBounds() {
getImageBounds(mImageBounds);
getLimitBounds(mViewBounds);
mZoomableController.setImageBounds(mImageBounds);
mZoomableController.setViewBounds(mViewBounds);
FLog.v(
getLogTag(), getLogTag(),
"updateZoomableControllerBounds: view %x, view bounds: %s, image bounds: %s", "onTouchEvent: $action, view ${hashCode()}, handled by tap gesture detector"
this.hashCode(), )
mViewBounds, return true
mImageBounds); }
if (!isDialToneEnabled && zoomableController.onTouchEvent(event)) {
FLog.v(
getLogTag(),
"onTouchEvent: $action, view ${hashCode()}, handled by zoomable controller"
)
if (!allowTouchInterceptionWhileZoomed && !zoomableController.isIdentity()) {
parent.requestDisallowInterceptTouchEvent(true)
}
return true
}
if (super.onTouchEvent(event)) {
FLog.v(
getLogTag(),
"onTouchEvent: $action, view ${hashCode()}, handled by the super"
)
return true
}
// If none of our components handled the event, we send a cancel event to avoid unwanted actions.
val cancelEvent = MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_CANCEL }
tapGestureDetector?.onTouchEvent(cancelEvent)
zoomableController.onTouchEvent(cancelEvent)
cancelEvent.recycle()
return false
} }
protected Class<?> getLogTag() { override fun computeHorizontalScrollRange(): Int =
return TAG; zoomableController.computeHorizontalScrollRange()
override fun computeHorizontalScrollOffset(): Int =
zoomableController.computeHorizontalScrollOffset()
override fun computeHorizontalScrollExtent(): Int =
zoomableController.computeHorizontalScrollExtent()
override fun computeVerticalScrollRange(): Int =
zoomableController.computeVerticalScrollRange()
override fun computeVerticalScrollOffset(): Int =
zoomableController.computeVerticalScrollOffset()
override fun computeVerticalScrollExtent(): Int =
zoomableController.computeVerticalScrollExtent()
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
FLog.v(getLogTag(), "onLayout: view ${hashCode()}")
super.onLayout(changed, left, top, right, bottom)
updateZoomableControllerBounds()
} }
protected ZoomableController createZoomableController() { private fun onFinalImageSet() {
return AnimatedZoomableController.newInstance(); FLog.v(getLogTag(), "onFinalImageSet: view ${hashCode()}")
if (!zoomableController.isEnabled() && zoomingEnabled) {
zoomableController.setEnabled(true)
updateZoomableControllerBounds()
}
} }
private fun onRelease() {
FLog.v(getLogTag(), "onRelease: view ${hashCode()}")
zoomableController.setEnabled(false)
}
protected fun onTransformChanged(transform: Matrix) {
FLog.v(getLogTag(), "onTransformChanged: view ${hashCode()}, transform: $transform")
maybeSetHugeImageController()
invalidate()
}
protected fun updateZoomableControllerBounds() {
getImageBounds(imageBounds)
getLimitBounds(viewBounds)
zoomableController.setImageBounds(imageBounds)
zoomableController.setViewBounds(viewBounds)
FLog.v(
getLogTag(),
"updateZoomableControllerBounds: view ${hashCode()}, " +
"view bounds: $viewBounds, image bounds: $imageBounds"
)
}
protected fun getLogTag(): Class<*> = TAG
protected fun createZoomableController(): ZoomableController = AnimatedZoomableController.newInstance()
/** /**
* Use this, If someone is willing to listen to scale change * Interface to listen for scale change events.
*/ */
public interface TransformationListener{ interface TransformationListener {
void onTransformationEnd(); fun onTransformationEnd()
} }
} }

View file

@ -107,43 +107,43 @@ class MultiPointerGestureDetectorUnitTest {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testIsGestureInProgress() { fun testIsGestureInProgress() {
Assert.assertEquals(detector.isGestureInProgress, false) Assert.assertEquals(detector.isGestureInProgress(), false)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetNewPointerCount() { fun testGetNewPointerCount() {
Assert.assertEquals(detector.newPointerCount, 0) Assert.assertEquals(detector.getNewPointerCount(), 0)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetPointerCount() { fun testGetPointerCount() {
Assert.assertEquals(detector.pointerCount, 0) Assert.assertEquals(detector.getPointerCount(), 0)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetStartX() { fun testGetStartX() {
Assert.assertEquals(detector.startX[0], 0.0f) Assert.assertEquals(detector.getStartX()[0], 0.0f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetStartY() { fun testGetStartY() {
Assert.assertEquals(detector.startY[0], 0.0f) Assert.assertEquals(detector.getStartY()[0], 0.0f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetCurrentX() { fun testGetCurrentX() {
Assert.assertEquals(detector.currentX[0], 0.0f) Assert.assertEquals(detector.getCurrentX()[0], 0.0f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetCurrentY() { fun testGetCurrentY() {
Assert.assertEquals(detector.currentY[0], 0.0f) Assert.assertEquals(detector.getCurrentY()[0], 0.0f)
} }
@Test @Test

View file

@ -84,51 +84,51 @@ class TransformGestureDetectorUnitTest {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testIsGestureInProgress() { fun testIsGestureInProgress() {
Assert.assertEquals(detector.isGestureInProgress, false) Assert.assertEquals(detector.isGestureInProgress(), false)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetNewPointerCount() { fun testGetNewPointerCount() {
Assert.assertEquals(detector.newPointerCount, 0) Assert.assertEquals(detector.getNewPointerCount(), 0)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetPointerCount() { fun testGetPointerCount() {
Assert.assertEquals(detector.pointerCount, 0) Assert.assertEquals(detector.getPointerCount(), 0)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetPivotX() { fun testGetPivotX() {
Assert.assertEquals(detector.pivotX, 0.0f) Assert.assertEquals(detector.getPivotX(), 0.0f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetPivotY() { fun testGetPivotY() {
Assert.assertEquals(detector.pivotY, 0.0f) Assert.assertEquals(detector.getPivotY(), 0.0f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetTranslationX() { fun testGetTranslationX() {
Assert.assertEquals(detector.translationX, 0.0f) Assert.assertEquals(detector.getTranslationX(), 0.0f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetTranslationY() { fun testGetTranslationY() {
Assert.assertEquals(detector.translationY, 0.0f) Assert.assertEquals(detector.getTranslationY(), 0.0f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetScaleCaseLessThan2() { fun testGetScaleCaseLessThan2() {
Whitebox.setInternalState(detector, "mDetector", mDetector) Whitebox.setInternalState(detector, "mDetector", mDetector)
whenever(mDetector.pointerCount).thenReturn(1) whenever(mDetector.getPointerCount()).thenReturn(1)
Assert.assertEquals(detector.scale, 1f) Assert.assertEquals(detector.getScale(), 1f)
} }
@Test @Test
@ -138,20 +138,20 @@ class TransformGestureDetectorUnitTest {
array[0] = 0.0f array[0] = 0.0f
array[1] = 1.0f array[1] = 1.0f
Whitebox.setInternalState(detector, "mDetector", mDetector) Whitebox.setInternalState(detector, "mDetector", mDetector)
whenever(mDetector.pointerCount).thenReturn(2) whenever(mDetector.getPointerCount()).thenReturn(2)
whenever(mDetector.startX).thenReturn(array) whenever(mDetector.getStartX()).thenReturn(array)
whenever(mDetector.startY).thenReturn(array) whenever(mDetector.getStartY()).thenReturn(array)
whenever(mDetector.currentX).thenReturn(array) whenever(mDetector.getCurrentX()).thenReturn(array)
whenever(mDetector.currentY).thenReturn(array) whenever(mDetector.getCurrentY()).thenReturn(array)
Assert.assertEquals(detector.scale, 1f) Assert.assertEquals(detector.getScale(), 1f)
} }
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetRotationCaseLessThan2() { fun testGetRotationCaseLessThan2() {
Whitebox.setInternalState(detector, "mDetector", mDetector) Whitebox.setInternalState(detector, "mDetector", mDetector)
whenever(mDetector.pointerCount).thenReturn(1) whenever(mDetector.getPointerCount()).thenReturn(1)
Assert.assertEquals(detector.rotation, 0f) Assert.assertEquals(detector.getRotation(), 0f)
} }
@Test @Test
@ -161,12 +161,12 @@ class TransformGestureDetectorUnitTest {
array[0] = 0.0f array[0] = 0.0f
array[1] = 1.0f array[1] = 1.0f
Whitebox.setInternalState(detector, "mDetector", mDetector) Whitebox.setInternalState(detector, "mDetector", mDetector)
whenever(mDetector.pointerCount).thenReturn(2) whenever(mDetector.getPointerCount()).thenReturn(2)
whenever(mDetector.startX).thenReturn(array) whenever(mDetector.getStartX()).thenReturn(array)
whenever(mDetector.startY).thenReturn(array) whenever(mDetector.getStartY()).thenReturn(array)
whenever(mDetector.currentX).thenReturn(array) whenever(mDetector.getCurrentX()).thenReturn(array)
whenever(mDetector.currentY).thenReturn(array) whenever(mDetector.getCurrentY()).thenReturn(array)
Assert.assertEquals(detector.rotation, 0f) Assert.assertEquals(detector.getRotation(), 0f)
} }
@Test @Test