721 lines
24 KiB
Java
721 lines
24 KiB
Java
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
package awais.instagrabber.customviews.drawee;
|
|
|
|
import android.graphics.Matrix;
|
|
import android.graphics.PointF;
|
|
import android.graphics.RectF;
|
|
import android.view.MotionEvent;
|
|
|
|
import androidx.annotation.IntDef;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.facebook.common.logging.FLog;
|
|
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
|
|
/**
|
|
* Zoomable controller that calculates transformation based on touch events.
|
|
*/
|
|
public class DefaultZoomableController
|
|
implements ZoomableController, TransformGestureDetector.Listener {
|
|
|
|
/**
|
|
* Interface for handling call backs when the image bounds are set.
|
|
*/
|
|
public interface ImageBoundsListener {
|
|
void onImageBoundsSet(RectF imageBounds);
|
|
}
|
|
|
|
@IntDef(
|
|
flag = true,
|
|
value = {LIMIT_NONE, LIMIT_TRANSLATION_X, LIMIT_TRANSLATION_Y, LIMIT_SCALE, LIMIT_ALL})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface LimitFlag {}
|
|
|
|
public static final int LIMIT_NONE = 0;
|
|
public static final int LIMIT_TRANSLATION_X = 1;
|
|
public static final int LIMIT_TRANSLATION_Y = 2;
|
|
public static final int LIMIT_SCALE = 4;
|
|
public static final int LIMIT_ALL = LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y | LIMIT_SCALE;
|
|
|
|
private static final float EPS = 1e-3f;
|
|
|
|
private static final Class<?> TAG = DefaultZoomableController.class;
|
|
|
|
private static final RectF IDENTITY_RECT = new RectF(0, 0, 1, 1);
|
|
|
|
private TransformGestureDetector mGestureDetector;
|
|
|
|
private @Nullable
|
|
ImageBoundsListener mImageBoundsListener;
|
|
|
|
private @Nullable
|
|
Listener mListener = null;
|
|
|
|
private boolean mIsEnabled = false;
|
|
private boolean mIsRotationEnabled = false;
|
|
private boolean mIsScaleEnabled = true;
|
|
private boolean mIsTranslationEnabled = true;
|
|
private boolean mIsGestureZoomEnabled = true;
|
|
|
|
private float mMinScaleFactor = 1.0f;
|
|
private float mMaxScaleFactor = 2.0f;
|
|
|
|
// View bounds, in view-absolute coordinates
|
|
private final RectF mViewBounds = new RectF();
|
|
// Non-transformed image bounds, in view-absolute coordinates
|
|
private final RectF mImageBounds = new RectF();
|
|
// Transformed image bounds, in view-absolute coordinates
|
|
private final RectF mTransformedImageBounds = new RectF();
|
|
|
|
private final Matrix mPreviousTransform = new Matrix();
|
|
private final Matrix mActiveTransform = new Matrix();
|
|
private final Matrix mActiveTransformInverse = new Matrix();
|
|
private final float[] mTempValues = new float[9];
|
|
private final RectF mTempRect = new RectF();
|
|
private boolean mWasTransformCorrected;
|
|
|
|
public static DefaultZoomableController newInstance() {
|
|
return new DefaultZoomableController(TransformGestureDetector.newInstance());
|
|
}
|
|
|
|
public DefaultZoomableController(TransformGestureDetector gestureDetector) {
|
|
mGestureDetector = gestureDetector;
|
|
mGestureDetector.setListener(this);
|
|
}
|
|
|
|
/**
|
|
* Rests the controller.
|
|
*/
|
|
public void reset() {
|
|
FLog.v(TAG, "reset");
|
|
mGestureDetector.reset();
|
|
mPreviousTransform.reset();
|
|
mActiveTransform.reset();
|
|
onTransformChanged();
|
|
}
|
|
|
|
/**
|
|
* Sets the zoomable listener.
|
|
*/
|
|
@Override
|
|
public void setListener(Listener listener) {
|
|
mListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the controller is enabled or not.
|
|
*/
|
|
@Override
|
|
public void setEnabled(boolean enabled) {
|
|
mIsEnabled = enabled;
|
|
if (!enabled) {
|
|
reset();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets whether the controller is enabled or not.
|
|
*/
|
|
@Override
|
|
public boolean isEnabled() {
|
|
return mIsEnabled;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the rotation gesture is enabled or not.
|
|
*/
|
|
public void setRotationEnabled(boolean enabled) {
|
|
mIsRotationEnabled = enabled;
|
|
}
|
|
|
|
/**
|
|
* Gets whether the rotation gesture is enabled or not.
|
|
*/
|
|
public boolean isRotationEnabled() {
|
|
return mIsRotationEnabled;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the scale gesture is enabled or not.
|
|
*/
|
|
public void setScaleEnabled(boolean enabled) {
|
|
mIsScaleEnabled = enabled;
|
|
}
|
|
|
|
/**
|
|
* Gets whether the scale gesture is enabled or not.
|
|
*/
|
|
public boolean isScaleEnabled() {
|
|
return mIsScaleEnabled;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the translation gesture is enabled or not.
|
|
*/
|
|
public void setTranslationEnabled(boolean enabled) {
|
|
mIsTranslationEnabled = enabled;
|
|
}
|
|
|
|
/**
|
|
* Gets whether the translations gesture is enabled or not.
|
|
*/
|
|
public boolean isTranslationEnabled() {
|
|
return mIsTranslationEnabled;
|
|
}
|
|
|
|
/**
|
|
* Sets the minimum scale factor allowed.
|
|
*
|
|
* <p>Hierarchy's scaling (if any) is not taken into account.
|
|
*/
|
|
public void setMinScaleFactor(float minScaleFactor) {
|
|
mMinScaleFactor = minScaleFactor;
|
|
}
|
|
|
|
/**
|
|
* Gets the minimum scale factor allowed.
|
|
*/
|
|
public float getMinScaleFactor() {
|
|
return mMinScaleFactor;
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum scale factor allowed.
|
|
*
|
|
* <p>Hierarchy's scaling (if any) is not taken into account.
|
|
*/
|
|
public void setMaxScaleFactor(float maxScaleFactor) {
|
|
mMaxScaleFactor = maxScaleFactor;
|
|
}
|
|
|
|
/**
|
|
* Gets the maximum scale factor allowed.
|
|
*/
|
|
public float getMaxScaleFactor() {
|
|
return mMaxScaleFactor;
|
|
}
|
|
|
|
/**
|
|
* Sets whether gesture zooms are enabled or not.
|
|
*/
|
|
public void setGestureZoomEnabled(boolean isGestureZoomEnabled) {
|
|
mIsGestureZoomEnabled = isGestureZoomEnabled;
|
|
}
|
|
|
|
/**
|
|
* Gets whether gesture zooms are enabled or not.
|
|
*/
|
|
public boolean isGestureZoomEnabled() {
|
|
return mIsGestureZoomEnabled;
|
|
}
|
|
|
|
/**
|
|
* Gets the current scale factor.
|
|
*/
|
|
@Override
|
|
public float getScaleFactor() {
|
|
return getMatrixScaleFactor(mActiveTransform);
|
|
}
|
|
|
|
/**
|
|
* Sets the image bounds, in view-absolute coordinates.
|
|
*/
|
|
@Override
|
|
public void setImageBounds(RectF imageBounds) {
|
|
if (!imageBounds.equals(mImageBounds)) {
|
|
mImageBounds.set(imageBounds);
|
|
onTransformChanged();
|
|
if (mImageBoundsListener != null) {
|
|
mImageBoundsListener.onImageBoundsSet(mImageBounds);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the non-transformed image bounds, in view-absolute coordinates.
|
|
*/
|
|
public RectF getImageBounds() {
|
|
return mImageBounds;
|
|
}
|
|
|
|
/**
|
|
* Gets the transformed image bounds, in view-absolute coordinates
|
|
*/
|
|
private RectF getTransformedImageBounds() {
|
|
return mTransformedImageBounds;
|
|
}
|
|
|
|
/**
|
|
* Sets the view bounds.
|
|
*/
|
|
@Override
|
|
public void setViewBounds(RectF viewBounds) {
|
|
mViewBounds.set(viewBounds);
|
|
}
|
|
|
|
/**
|
|
* Gets the view bounds.
|
|
*/
|
|
public RectF getViewBounds() {
|
|
return mViewBounds;
|
|
}
|
|
|
|
/**
|
|
* Sets the image bounds listener.
|
|
*/
|
|
public void setImageBoundsListener(@Nullable ImageBoundsListener imageBoundsListener) {
|
|
mImageBoundsListener = imageBoundsListener;
|
|
}
|
|
|
|
/**
|
|
* Gets the image bounds listener.
|
|
*/
|
|
public @Nullable
|
|
ImageBoundsListener getImageBoundsListener() {
|
|
return mImageBoundsListener;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the zoomable transform is identity matrix.
|
|
*/
|
|
@Override
|
|
public boolean isIdentity() {
|
|
return isMatrixIdentity(mActiveTransform, 1e-3f);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the transform was corrected during the last update.
|
|
*
|
|
* <p>We should rename this method to `wasTransformedWithoutCorrection` and just return the
|
|
* internal flag directly. However, this requires interface change and negation of meaning.
|
|
*/
|
|
@Override
|
|
public boolean wasTransformCorrected() {
|
|
return mWasTransformCorrected;
|
|
}
|
|
|
|
/**
|
|
* Gets the matrix that transforms image-absolute coordinates to view-absolute coordinates. The
|
|
* zoomable transformation is taken into account.
|
|
*
|
|
* <p>Internal matrix is exposed for performance reasons and is not to be modified by the callers.
|
|
*/
|
|
@Override
|
|
public Matrix getTransform() {
|
|
return mActiveTransform;
|
|
}
|
|
|
|
/**
|
|
* Gets the matrix that transforms image-relative coordinates to view-absolute coordinates. The
|
|
* zoomable transformation is taken into account.
|
|
*/
|
|
public void getImageRelativeToViewAbsoluteTransform(Matrix outMatrix) {
|
|
outMatrix.setRectToRect(IDENTITY_RECT, mTransformedImageBounds, Matrix.ScaleToFit.FILL);
|
|
}
|
|
|
|
/**
|
|
* Maps point from view-absolute to image-relative coordinates. This takes into account the
|
|
* zoomable transformation.
|
|
*/
|
|
public PointF mapViewToImage(PointF viewPoint) {
|
|
float[] points = mTempValues;
|
|
points[0] = viewPoint.x;
|
|
points[1] = viewPoint.y;
|
|
mActiveTransform.invert(mActiveTransformInverse);
|
|
mActiveTransformInverse.mapPoints(points, 0, points, 0, 1);
|
|
mapAbsoluteToRelative(points, points, 1);
|
|
return new PointF(points[0], points[1]);
|
|
}
|
|
|
|
/**
|
|
* Maps point from image-relative to view-absolute coordinates. This takes into account the
|
|
* zoomable transformation.
|
|
*/
|
|
public PointF mapImageToView(PointF imagePoint) {
|
|
float[] points = mTempValues;
|
|
points[0] = imagePoint.x;
|
|
points[1] = imagePoint.y;
|
|
mapRelativeToAbsolute(points, points, 1);
|
|
mActiveTransform.mapPoints(points, 0, points, 0, 1);
|
|
return new PointF(points[0], points[1]);
|
|
}
|
|
|
|
/**
|
|
* Maps array of 2D points from view-absolute to image-relative coordinates. This does NOT take
|
|
* into account the zoomable transformation. Points are represented by a float array of [x0, y0,
|
|
* x1, y1, ...].
|
|
*
|
|
* @param destPoints destination array (may be the same as source array)
|
|
* @param srcPoints source array
|
|
* @param numPoints number of points to map
|
|
*/
|
|
private void mapAbsoluteToRelative(float[] destPoints, float[] srcPoints, int numPoints) {
|
|
for (int i = 0; i < numPoints; i++) {
|
|
destPoints[i * 2 + 0] = (srcPoints[i * 2 + 0] - mImageBounds.left) / mImageBounds.width();
|
|
destPoints[i * 2 + 1] = (srcPoints[i * 2 + 1] - mImageBounds.top) / mImageBounds.height();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps array of 2D points from image-relative to view-absolute coordinates. This does NOT take
|
|
* into account the zoomable transformation. Points are represented by float array of [x0, y0, x1,
|
|
* y1, ...].
|
|
*
|
|
* @param destPoints destination array (may be the same as source array)
|
|
* @param srcPoints source array
|
|
* @param numPoints number of points to map
|
|
*/
|
|
private void mapRelativeToAbsolute(float[] destPoints, float[] srcPoints, int numPoints) {
|
|
for (int i = 0; i < numPoints; i++) {
|
|
destPoints[i * 2 + 0] = srcPoints[i * 2 + 0] * mImageBounds.width() + mImageBounds.left;
|
|
destPoints[i * 2 + 1] = srcPoints[i * 2 + 1] * mImageBounds.height() + mImageBounds.top;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zooms to the desired scale and positions the image so that the given image point corresponds to
|
|
* the given view point.
|
|
*
|
|
* @param scale desired scale, will be limited to {min, max} scale factor
|
|
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
|
* @param viewPoint 2D point in view's absolute coordinate system
|
|
*/
|
|
public void zoomToPoint(float scale, PointF imagePoint, PointF viewPoint) {
|
|
FLog.v(TAG, "zoomToPoint");
|
|
calculateZoomToPointTransform(mActiveTransform, scale, imagePoint, viewPoint, LIMIT_ALL);
|
|
onTransformChanged();
|
|
}
|
|
|
|
/**
|
|
* Calculates the zoom transformation that would zoom to the desired scale and position the image
|
|
* so that the given image point corresponds to the given view point.
|
|
*
|
|
* @param outTransform the matrix to store the result to
|
|
* @param scale desired scale, will be limited to {min, max} scale factor
|
|
* @param imagePoint 2D point in image's relative coordinate system (i.e. 0 <= x, y <= 1)
|
|
* @param viewPoint 2D point in view's absolute coordinate system
|
|
* @param limitFlags whether to limit translation and/or scale.
|
|
* @return whether or not the transform has been corrected due to limitation
|
|
*/
|
|
protected boolean calculateZoomToPointTransform(
|
|
Matrix outTransform,
|
|
float scale,
|
|
PointF imagePoint,
|
|
PointF viewPoint,
|
|
@LimitFlag int limitFlags) {
|
|
float[] viewAbsolute = mTempValues;
|
|
viewAbsolute[0] = imagePoint.x;
|
|
viewAbsolute[1] = imagePoint.y;
|
|
mapRelativeToAbsolute(viewAbsolute, viewAbsolute, 1);
|
|
float distanceX = viewPoint.x - viewAbsolute[0];
|
|
float distanceY = viewPoint.y - viewAbsolute[1];
|
|
boolean transformCorrected = false;
|
|
outTransform.setScale(scale, scale, viewAbsolute[0], viewAbsolute[1]);
|
|
transformCorrected |= limitScale(outTransform, viewAbsolute[0], viewAbsolute[1], limitFlags);
|
|
outTransform.postTranslate(distanceX, distanceY);
|
|
transformCorrected |= limitTranslation(outTransform, limitFlags);
|
|
return transformCorrected;
|
|
}
|
|
|
|
/**
|
|
* Sets a new zoom transformation.
|
|
*/
|
|
public void setTransform(Matrix newTransform) {
|
|
FLog.v(TAG, "setTransform");
|
|
mActiveTransform.set(newTransform);
|
|
onTransformChanged();
|
|
}
|
|
|
|
/**
|
|
* Gets the gesture detector.
|
|
*/
|
|
protected TransformGestureDetector getDetector() {
|
|
return mGestureDetector;
|
|
}
|
|
|
|
/**
|
|
* Notifies controller of the received touch event.
|
|
*/
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
FLog.v(TAG, "onTouchEvent: action: ", event.getAction());
|
|
if (mIsEnabled && mIsGestureZoomEnabled) {
|
|
return mGestureDetector.onTouchEvent(event);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* TransformGestureDetector.Listener methods */
|
|
|
|
@Override
|
|
public void onGestureBegin(TransformGestureDetector detector) {
|
|
FLog.v(TAG, "onGestureBegin");
|
|
mPreviousTransform.set(mActiveTransform);
|
|
onTransformBegin();
|
|
// We only received a touch down event so far, and so we don't know yet in which direction a
|
|
// future move event will follow. Therefore, if we can't scroll in all directions, we have to
|
|
// assume the worst case where the user tries to scroll out of edge, which would cause
|
|
// transformation to be corrected.
|
|
mWasTransformCorrected = !canScrollInAllDirection();
|
|
}
|
|
|
|
@Override
|
|
public void onGestureUpdate(TransformGestureDetector detector) {
|
|
FLog.v(TAG, "onGestureUpdate");
|
|
boolean transformCorrected = calculateGestureTransform(mActiveTransform, LIMIT_ALL);
|
|
onTransformChanged();
|
|
if (transformCorrected) {
|
|
mGestureDetector.restartGesture();
|
|
}
|
|
// A transformation happened, but was it without correction?
|
|
mWasTransformCorrected = transformCorrected;
|
|
}
|
|
|
|
@Override
|
|
public void onGestureEnd(TransformGestureDetector detector) {
|
|
FLog.v(TAG, "onGestureEnd");
|
|
onTransformEnd();
|
|
}
|
|
|
|
/**
|
|
* Calculates the zoom transformation based on the current gesture.
|
|
*
|
|
* @param outTransform the matrix to store the result to
|
|
* @param limitTypes whether to limit translation and/or scale.
|
|
* @return whether or not the transform has been corrected due to limitation
|
|
*/
|
|
protected boolean calculateGestureTransform(Matrix outTransform, @LimitFlag int limitTypes) {
|
|
TransformGestureDetector detector = mGestureDetector;
|
|
boolean transformCorrected = false;
|
|
outTransform.set(mPreviousTransform);
|
|
if (mIsRotationEnabled) {
|
|
float angle = detector.getRotation() * (float) (180 / Math.PI);
|
|
outTransform.postRotate(angle, detector.getPivotX(), detector.getPivotY());
|
|
}
|
|
if (mIsScaleEnabled) {
|
|
float scale = detector.getScale();
|
|
outTransform.postScale(scale, scale, detector.getPivotX(), detector.getPivotY());
|
|
}
|
|
transformCorrected |=
|
|
limitScale(outTransform, detector.getPivotX(), detector.getPivotY(), limitTypes);
|
|
if (mIsTranslationEnabled) {
|
|
outTransform.postTranslate(detector.getTranslationX(), detector.getTranslationY());
|
|
}
|
|
transformCorrected |= limitTranslation(outTransform, limitTypes);
|
|
return transformCorrected;
|
|
}
|
|
|
|
private void onTransformBegin() {
|
|
if (mListener != null && isEnabled()) {
|
|
mListener.onTransformBegin(mActiveTransform);
|
|
}
|
|
}
|
|
|
|
private void onTransformChanged() {
|
|
mActiveTransform.mapRect(mTransformedImageBounds, mImageBounds);
|
|
if (mListener != null && isEnabled()) {
|
|
mListener.onTransformChanged(mActiveTransform);
|
|
}
|
|
}
|
|
|
|
private void onTransformEnd() {
|
|
if (mListener != null && isEnabled()) {
|
|
mListener.onTransformEnd(mActiveTransform);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Keeps the scaling factor within the specified limits.
|
|
*
|
|
* @param pivotX x coordinate of the pivot point
|
|
* @param pivotY y coordinate of the pivot point
|
|
* @param limitTypes whether to limit scale.
|
|
* @return whether limiting has been applied or not
|
|
*/
|
|
private boolean limitScale(
|
|
Matrix transform, float pivotX, float pivotY, @LimitFlag int limitTypes) {
|
|
if (!shouldLimit(limitTypes, LIMIT_SCALE)) {
|
|
return false;
|
|
}
|
|
float currentScale = getMatrixScaleFactor(transform);
|
|
float targetScale = limit(currentScale, mMinScaleFactor, mMaxScaleFactor);
|
|
if (targetScale != currentScale) {
|
|
float scale = targetScale / currentScale;
|
|
transform.postScale(scale, scale, pivotX, pivotY);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Limits the translation so that there are no empty spaces on the sides if possible.
|
|
*
|
|
* <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) {
|
|
if (!shouldLimit(limitTypes, LIMIT_TRANSLATION_X | LIMIT_TRANSLATION_Y)) {
|
|
return false;
|
|
}
|
|
RectF b = mTempRect;
|
|
b.set(mImageBounds);
|
|
transform.mapRect(b);
|
|
float offsetLeft =
|
|
shouldLimit(limitTypes, LIMIT_TRANSLATION_X)
|
|
? getOffset(
|
|
b.left, b.right, mViewBounds.left, mViewBounds.right, mImageBounds.centerX())
|
|
: 0;
|
|
float offsetTop =
|
|
shouldLimit(limitTypes, LIMIT_TRANSLATION_Y)
|
|
? getOffset(
|
|
b.top, b.bottom, mViewBounds.top, mViewBounds.bottom, mImageBounds.centerY())
|
|
: 0;
|
|
if (offsetLeft != 0 || offsetTop != 0) {
|
|
transform.postTranslate(offsetLeft, offsetTop);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks whether the specified limit flag is present in the limits provided.
|
|
*
|
|
* <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) {
|
|
return (limits & flag) != LIMIT_NONE;
|
|
}
|
|
|
|
/**
|
|
* Returns the offset necessary to make sure that: - the image is centered within the limit if the
|
|
* image is smaller than the limit - there is no empty space on left/right if the image is bigger
|
|
* than the limit
|
|
*/
|
|
private float getOffset(
|
|
float imageStart, float imageEnd, float limitStart, float limitEnd, float limitCenter) {
|
|
float imageWidth = imageEnd - imageStart, limitWidth = limitEnd - limitStart;
|
|
float limitInnerWidth = Math.min(limitCenter - limitStart, limitEnd - limitCenter) * 2;
|
|
// center if smaller than limitInnerWidth
|
|
if (imageWidth < limitInnerWidth) {
|
|
return limitCenter - (imageEnd + imageStart) / 2;
|
|
}
|
|
// to the edge if in between and limitCenter is not (limitLeft + limitRight) / 2
|
|
if (imageWidth < limitWidth) {
|
|
if (limitCenter < (limitStart + limitEnd) / 2) {
|
|
return limitStart - imageStart;
|
|
} else {
|
|
return limitEnd - imageEnd;
|
|
}
|
|
}
|
|
// to the edge if larger than limitWidth and empty space visible
|
|
if (imageStart > limitStart) {
|
|
return limitStart - imageStart;
|
|
}
|
|
if (imageEnd < limitEnd) {
|
|
return limitEnd - imageEnd;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Limits the value to the given min and max range.
|
|
*/
|
|
private float limit(float value, float min, float max) {
|
|
return Math.min(Math.max(min, value), max);
|
|
}
|
|
|
|
/**
|
|
* Gets the scale factor for the given matrix. This method assumes the equal scaling factor for X
|
|
* and Y axis.
|
|
*/
|
|
private float getMatrixScaleFactor(Matrix transform) {
|
|
transform.getValues(mTempValues);
|
|
return mTempValues[Matrix.MSCALE_X];
|
|
}
|
|
|
|
/**
|
|
* Same as {@code Matrix.isIdentity()}, but with tolerance {@code eps}.
|
|
*/
|
|
private boolean isMatrixIdentity(Matrix transform, float eps) {
|
|
// Checks whether the given matrix is close enough to the identity matrix:
|
|
// 1 0 0
|
|
// 0 1 0
|
|
// 0 0 1
|
|
// Or equivalently to the zero matrix, after subtracting 1.0f from the diagonal elements:
|
|
// 0 0 0
|
|
// 0 0 0
|
|
// 0 0 0
|
|
transform.getValues(mTempValues);
|
|
mTempValues[0] -= 1.0f; // m00
|
|
mTempValues[4] -= 1.0f; // m11
|
|
mTempValues[8] -= 1.0f; // m22
|
|
for (int i = 0; i < 9; i++) {
|
|
if (Math.abs(mTempValues[i]) > eps) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the scroll can happen in all directions. I.e. the image is not on any edge.
|
|
*/
|
|
private boolean canScrollInAllDirection() {
|
|
return mTransformedImageBounds.left < mViewBounds.left - EPS
|
|
&& mTransformedImageBounds.top < mViewBounds.top - EPS
|
|
&& mTransformedImageBounds.right > mViewBounds.right + EPS
|
|
&& mTransformedImageBounds.bottom > mViewBounds.bottom + EPS;
|
|
}
|
|
|
|
@Override
|
|
public int computeHorizontalScrollRange() {
|
|
return (int) mTransformedImageBounds.width();
|
|
}
|
|
|
|
@Override
|
|
public int computeHorizontalScrollOffset() {
|
|
return (int) (mViewBounds.left - mTransformedImageBounds.left);
|
|
}
|
|
|
|
@Override
|
|
public int computeHorizontalScrollExtent() {
|
|
return (int) mViewBounds.width();
|
|
}
|
|
|
|
@Override
|
|
public int computeVerticalScrollRange() {
|
|
return (int) mTransformedImageBounds.height();
|
|
}
|
|
|
|
@Override
|
|
public int computeVerticalScrollOffset() {
|
|
return (int) (mViewBounds.top - mTransformedImageBounds.top);
|
|
}
|
|
|
|
@Override
|
|
public int computeVerticalScrollExtent() {
|
|
return (int) mViewBounds.height();
|
|
}
|
|
|
|
public Listener getListener() {
|
|
return mListener;
|
|
}
|
|
}
|