/*
* This file provided by Facebook is for non-commercial testing and evaluation
* purposes only. Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.facebook.samples.zoomable;
import com.facebook.samples.gestures.TransformGestureDetector;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.RectF;
import android.view.MotionEvent;
/**
* Zoomable controller that calculates transformation based on touch events.
*/
public class DefaultZoomableController
implements ZoomableController, TransformGestureDetector.Listener {
private TransformGestureDetector mGestureDetector;
private Listener mListener = null;
private boolean mIsEnabled = false;
private boolean mIsRotationEnabled = false;
private boolean mIsScaleEnabled = true;
private boolean mIsTranslationEnabled = true;
private float mMinScaleFactor = 1.0f;
private final RectF mViewBounds = new RectF();
private final RectF mImageBounds = new RectF();
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];
public DefaultZoomableController(TransformGestureDetector gestureDetector) {
mGestureDetector = gestureDetector;
mGestureDetector.setListener(this);
}
public static DefaultZoomableController newInstance() {
return new DefaultZoomableController(TransformGestureDetector.newInstance());
}
@Override
public void setListener(Listener listener) {
mListener = listener;
}
/** Rests the controller. */
public void reset() {
mGestureDetector.reset();
mPreviousTransform.reset();
mActiveTransform.reset();
}
/** Sets whether the controller is enabled or not. */
@Override
public void setEnabled(boolean enabled) {
mIsEnabled = enabled;
if (!enabled) {
reset();
}
}
/** Returns whether the controller is enabled or not. */
@Override
public boolean isEnabled() {
return mIsEnabled;
}
/** Sets whether the rotation gesture is enabled or not. */
public void setRotationEnabled(boolean enabled) {
mIsRotationEnabled = enabled;
}
/** Gets whether the rotation gesture is enabled or not. */
public boolean isRotationEnabled() {
return mIsRotationEnabled;
}
/** Sets whether the scale gesture is enabled or not. */
public void setScaleEnabled(boolean enabled) {
mIsScaleEnabled = enabled;
}
/** Gets whether the scale gesture is enabled or not. */
public boolean isScaleEnabled() {
return mIsScaleEnabled;
}
/** Sets whether the translation gesture is enabled or not. */
public void setTranslationEnabled(boolean enabled) {
mIsTranslationEnabled = enabled;
}
/** Gets whether the translations gesture is enabled or not. */
public boolean isTranslationEnabled() {
return mIsTranslationEnabled;
}
/** Sets the image bounds before zoomable transformation is applied. */
@Override
public void setImageBounds(RectF imageBounds) {
mImageBounds.set(imageBounds);
}
/** Sets the view bounds. */
@Override
public void setViewBounds(RectF viewBounds) {
mViewBounds.set(viewBounds);
}
/**
* Maps point from the view's to the image's relative coordinate system.
* This takes into account the zoomable transformation.
*/
public PointF mapViewToImage(PointF viewPoint) {
float[] points = mTempValues;
points[0] = viewPoint.x;
points[1] = viewPoint.y;
mActiveTransform.invert(mActiveTransformInverse);
mActiveTransformInverse.mapPoints(points, 0, points, 0, 1);
mapAbsoluteToRelative(points, points, 1);
return new PointF(points[0], points[1]);
}
/**
* Maps point from the image's relative to the view's coordinate system.
* This takes into account the zoomable transformation.
*/
public PointF mapImageToView(PointF imagePoint) {
float[] points = mTempValues;
points[0] = imagePoint.x;
points[1] = imagePoint.y;
mapRelativeToAbsolute(points, points, 1);
mActiveTransform.mapPoints(points, 0, points, 0, 1);
return new PointF(points[0], points[1]);
}
/**
* Maps array of 2D points from absolute to the image's relative coordinate system,
* and writes the transformed points back into the array.
* Points are represented by float array of [x0, y0, x1, y1, ...].
*
* @param destPoints destination array (may be the same as source array)
* @param srcPoints source array
* @param numPoints number of points to map
*/
private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) {
for (int i = 0; i < numPoints; i++) {
destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width();
destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height();
}
}
/**
* Maps array of 2D points from relative to the image's absolute coordinate system,
* and writes the transformed points back into the array
* Points are represented by float array of [x0, y0, x1, y1, ...].
*
* @param destPoints destination array (may be the same as source array)
* @param srcPoints source array
* @param numPoints number of points to map
*/
private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) {
for (int i = 0; i < numPoints; i++) {
destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left;
destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top;
}
}
/**
* Gets the zoomable transformation
* Internal matrix is exposed for performance reasons and is not to be modified by the callers.
*/
@Override
public Matrix getTransform() {
return mActiveTransform;
}
/** Notifies controller of the received touch event. */
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mIsEnabled) {
return mGestureDetector.onTouchEvent(event);
}
return false;
}
/* TransformGestureDetector.Listener methods */
@Override
public void onGestureBegin(TransformGestureDetector detector) {
}
@Override
public void onGestureUpdate(TransformGestureDetector detector) {
mActiveTransform.set(mPreviousTransform);
if (mIsRotationEnabled) {
float angle = detector.getRotation() * (float) (180 / Math.PI);
mActiveTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY());
}
if (mIsScaleEnabled) {
float scale = detector.getScale();
mActiveTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY());
}
limitScale(detector.getPivotX(), detector.getPivotY());
if (mIsTranslationEnabled) {
mActiveTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY());
}
limitTranslation();
if (mListener != null) {
mListener.onTransformChanged(mActiveTransform);
}
}
@Override
public void onGestureEnd(TransformGestureDetector detector) {
mPreviousTransform.set(mActiveTransform);
}
/** Gets the current scale factor. */
@Override
public float getScaleFactor() {
mActiveTransform.getValues(mTempValues);
return mTempValues[Matrix.MSCALE_X];
}
private void limitScale(float pivotX, float pivotY) {
float currentScale = getScaleFactor();
if (currentScale < mMinScaleFactor) {
float scale = mMinScaleFactor / currentScale;
mActiveTransform.postScale(scale, scale, pivotX, pivotY);
}
}
private void limitTranslation() {
RectF bounds = mTransformedImageBounds;
bounds.set(mImageBounds);
mActiveTransform.mapRect(bounds);
float offsetLeft = getOffset(bounds.left, bounds.width(), mViewBounds.width());
float offsetTop = getOffset(bounds.top, bounds.height(), mViewBounds.height());
if (offsetLeft != bounds.left || offsetTop != bounds.top) {
mActiveTransform.postTranslate(offsetLeft - bounds.left, offsetTop - bounds.top);
mGestureDetector.restartGesture();
}
}
private float getOffset(float offset, float imageDimension, float viewDimension) {
float diff = viewDimension - imageDimension;
return (diff > 0) ? diff / 2 : limit(offset, diff, 0);
}
private float limit(float value, float min, float max) {
return Math.min(Math.max(min, value), max);
}
}