summaryrefslogtreecommitdiffstats
path: root/photoviewer/src/com/android/ex/photo/views/PhotoView.java
diff options
context:
space:
mode:
Diffstat (limited to 'photoviewer/src/com/android/ex/photo/views/PhotoView.java')
-rw-r--r--photoviewer/src/com/android/ex/photo/views/PhotoView.java1288
1 files changed, 0 insertions, 1288 deletions
diff --git a/photoviewer/src/com/android/ex/photo/views/PhotoView.java b/photoviewer/src/com/android/ex/photo/views/PhotoView.java
deleted file mode 100644
index 8575bb1..0000000
--- a/photoviewer/src/com/android/ex/photo/views/PhotoView.java
+++ /dev/null
@@ -1,1288 +0,0 @@
-/*
- * Copyright (C) 2011 Google Inc.
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.ex.photo.views;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Paint.Style;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
-import android.support.v4.view.GestureDetectorCompat;
-import android.util.AttributeSet;
-import android.view.GestureDetector.OnGestureListener;
-import android.view.GestureDetector.OnDoubleTapListener;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.view.View;
-
-import com.android.ex.photo.R;
-import com.android.ex.photo.fragments.PhotoViewFragment.HorizontallyScrollable;
-
-/**
- * Layout for the photo list view header.
- */
-public class PhotoView extends View implements OnGestureListener,
- OnDoubleTapListener, ScaleGestureDetector.OnScaleGestureListener,
- HorizontallyScrollable {
- /** Zoom animation duration; in milliseconds */
- private final static long ZOOM_ANIMATION_DURATION = 300L;
- /** Rotate animation duration; in milliseconds */
- private final static long ROTATE_ANIMATION_DURATION = 500L;
- /** Snap animation duration; in milliseconds */
- private static final long SNAP_DURATION = 100L;
- /** Amount of time to wait before starting snap animation; in milliseconds */
- private static final long SNAP_DELAY = 250L;
- /** By how much to scale the image when double click occurs */
- private final static float DOUBLE_TAP_SCALE_FACTOR = 1.5f;
- /** Amount of translation needed before starting a snap animation */
- private final static float SNAP_THRESHOLD = 20.0f;
- /** The width & height of the bitmap returned by {@link #getCroppedPhoto()} */
- private final static float CROPPED_SIZE = 256.0f;
-
- /** If {@code true}, the static values have been initialized */
- private static boolean sInitialized;
-
- // Various dimensions
- /** Width & height of the crop region */
- private static int sCropSize;
-
- // Bitmaps
- /** Video icon */
- private static Bitmap sVideoImage;
- /** Video icon */
- private static Bitmap sVideoNotReadyImage;
-
- // Features
- private static boolean sHasMultitouchDistinct;
-
- // Paints
- /** Paint to partially dim the photo during crop */
- private static Paint sCropDimPaint;
- /** Paint to highlight the cropped portion of the photo */
- private static Paint sCropPaint;
-
- /** The photo to display */
- private BitmapDrawable mDrawable;
- /** The matrix used for drawing; this may be {@code null} */
- private Matrix mDrawMatrix;
- /** A matrix to apply the scaling of the photo */
- private Matrix mMatrix = new Matrix();
- /** The original matrix for this image; used to reset any transformations applied by the user */
- private Matrix mOriginalMatrix = new Matrix();
-
- /** The fixed height of this view. If {@code -1}, calculate the height */
- private int mFixedHeight = -1;
- /** When {@code true}, the view has been laid out */
- private boolean mHaveLayout;
- /** Whether or not the photo is full-screen */
- private boolean mFullScreen;
- /** Whether or not this is a still image of a video */
- private byte[] mVideoBlob;
- /** Whether or not this is a still image of a video */
- private boolean mVideoReady;
-
- /** Whether or not crop is allowed */
- private boolean mAllowCrop;
- /** The crop region */
- private Rect mCropRect = new Rect();
- /** Actual crop size; may differ from {@link #sCropSize} if the screen is smaller */
- private int mCropSize;
-
- /** Gesture detector */
- private GestureDetectorCompat mGestureDetector;
- /** Gesture detector that detects pinch gestures */
- private ScaleGestureDetector mScaleGetureDetector;
- /** An external click listener */
- private OnClickListener mExternalClickListener;
- /** When {@code true}, allows gestures to scale / pan the image */
- private boolean mTransformsEnabled;
-
- // To support zooming
- /** When {@code true}, a double tap scales the image by {@link #DOUBLE_TAP_SCALE_FACTOR} */
- private boolean mDoubleTapToZoomEnabled = true;
- /** When {@code true}, prevents scale end gesture from falsely triggering a double click. */
- private boolean mDoubleTapDebounce;
- /** When {@code false}, event is a scale gesture. Otherwise, event is a double touch. */
- private boolean mIsDoubleTouch;
- /** Runnable that scales the image */
- private ScaleRunnable mScaleRunnable;
- /** Minimum scale the image can have. */
- private float mMinScale;
- /** Maximum scale to limit scaling to, 0 means no limit. */
- private float mMaxScale;
-
- // To support translation [i.e. panning]
- /** Runnable that can move the image */
- private TranslateRunnable mTranslateRunnable;
- private SnapRunnable mSnapRunnable;
-
- // To support rotation
- /** The rotate runnable used to animate rotations of the image */
- private RotateRunnable mRotateRunnable;
- /** The current rotation amount, in degrees */
- private float mRotation;
-
- // Convenience fields
- // These are declared here not because they are important properties of the view. Rather, we
- // declare them here to avoid object allocation during critical graphics operations; such as
- // layout or drawing.
- /** Source (i.e. the photo size) bounds */
- private RectF mTempSrc = new RectF();
- /** Destination (i.e. the display) bounds. The image is scaled to this size. */
- private RectF mTempDst = new RectF();
- /** Rectangle to handle translations */
- private RectF mTranslateRect = new RectF();
- /** Array to store a copy of the matrix values */
- private float[] mValues = new float[9];
-
- public PhotoView(Context context) {
- super(context);
- initialize();
- }
-
- public PhotoView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initialize();
- }
-
- public PhotoView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initialize();
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (mScaleGetureDetector == null || mGestureDetector == null) {
- // We're being destroyed; ignore any touch events
- return true;
- }
-
- mScaleGetureDetector.onTouchEvent(event);
- mGestureDetector.onTouchEvent(event);
- final int action = event.getAction();
-
- switch (action) {
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- if (!mTranslateRunnable.mRunning) {
- snap();
- }
- break;
- }
-
- return true;
- }
-
- @Override
- public boolean onDoubleTap(MotionEvent e) {
- if (mDoubleTapToZoomEnabled && mTransformsEnabled) {
- if (!mDoubleTapDebounce) {
- float currentScale = getScale();
- float targetScale = currentScale * DOUBLE_TAP_SCALE_FACTOR;
-
- // Ensure the target scale is within our bounds
- targetScale = Math.max(mMinScale, targetScale);
- targetScale = Math.min(mMaxScale, targetScale);
-
- mScaleRunnable.start(currentScale, targetScale, e.getX(), e.getY());
- }
- mDoubleTapDebounce = false;
- }
- return true;
- }
-
- @Override
- public boolean onDoubleTapEvent(MotionEvent e) {
- return true;
- }
-
- @Override
- public boolean onSingleTapConfirmed(MotionEvent e) {
- if (mExternalClickListener != null && !mIsDoubleTouch) {
- mExternalClickListener.onClick(this);
- }
- mIsDoubleTouch = false;
- return true;
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- return false;
- }
-
- @Override
- public void onLongPress(MotionEvent e) {
- }
-
- @Override
- public void onShowPress(MotionEvent e) {
- }
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- if (mTransformsEnabled) {
- translate(-distanceX, -distanceY);
- }
- return true;
- }
-
- @Override
- public boolean onDown(MotionEvent e) {
- if (mTransformsEnabled) {
- mTranslateRunnable.stop();
- mSnapRunnable.stop();
- }
- return true;
- }
-
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- if (mTransformsEnabled) {
- mTranslateRunnable.start(velocityX, velocityY);
- }
- return true;
- }
-
- @Override
- public boolean onScale(ScaleGestureDetector detector) {
- if (mTransformsEnabled) {
- mIsDoubleTouch = false;
- float currentScale = getScale();
- float newScale = currentScale * detector.getScaleFactor();
- scale(newScale, detector.getFocusX(), detector.getFocusY());
- }
- return true;
- }
-
- @Override
- public boolean onScaleBegin(ScaleGestureDetector detector) {
- if (mTransformsEnabled) {
- mScaleRunnable.stop();
- mIsDoubleTouch = true;
- }
- return true;
- }
-
- @Override
- public void onScaleEnd(ScaleGestureDetector detector) {
- if (mTransformsEnabled && mIsDoubleTouch) {
- mDoubleTapDebounce = true;
- resetTransformations();
- }
- }
-
- @Override
- public void setOnClickListener(OnClickListener listener) {
- mExternalClickListener = listener;
- }
-
- @Override
- public boolean interceptMoveLeft(float origX, float origY) {
- if (!mTransformsEnabled) {
- // Allow intercept if we're not in transform mode
- return false;
- } else if (mTranslateRunnable.mRunning) {
- // Don't allow touch intercept until we've stopped flinging
- return true;
- } else {
- mMatrix.getValues(mValues);
- mTranslateRect.set(mTempSrc);
- mMatrix.mapRect(mTranslateRect);
-
- final float viewWidth = getWidth();
- final float transX = mValues[Matrix.MTRANS_X];
- final float drawWidth = mTranslateRect.right - mTranslateRect.left;
-
- if (!mTransformsEnabled || drawWidth <= viewWidth) {
- // Allow intercept if not in transform mode or the image is smaller than the view
- return false;
- } else if (transX == 0) {
- // We're at the left-side of the image; allow intercepting movements to the right
- return false;
- } else if (viewWidth >= drawWidth + transX) {
- // We're at the right-side of the image; allow intercepting movements to the left
- return true;
- } else {
- // We're in the middle of the image; don't allow touch intercept
- return true;
- }
- }
- }
-
- @Override
- public boolean interceptMoveRight(float origX, float origY) {
- if (!mTransformsEnabled) {
- // Allow intercept if we're not in transform mode
- return false;
- } else if (mTranslateRunnable.mRunning) {
- // Don't allow touch intercept until we've stopped flinging
- return true;
- } else {
- mMatrix.getValues(mValues);
- mTranslateRect.set(mTempSrc);
- mMatrix.mapRect(mTranslateRect);
-
- final float viewWidth = getWidth();
- final float transX = mValues[Matrix.MTRANS_X];
- final float drawWidth = mTranslateRect.right - mTranslateRect.left;
-
- if (!mTransformsEnabled || drawWidth <= viewWidth) {
- // Allow intercept if not in transform mode or the image is smaller than the view
- return false;
- } else if (transX == 0) {
- // We're at the left-side of the image; allow intercepting movements to the right
- return true;
- } else if (viewWidth >= drawWidth + transX) {
- // We're at the right-side of the image; allow intercepting movements to the left
- return false;
- } else {
- // We're in the middle of the image; don't allow touch intercept
- return true;
- }
- }
- }
-
- /**
- * Free all resources held by this view.
- * The view is on its way to be collected and will not be reused.
- */
- public void clear() {
- mGestureDetector = null;
- mScaleGetureDetector = null;
- mDrawable = null;
- mScaleRunnable.stop();
- mScaleRunnable = null;
- mTranslateRunnable.stop();
- mTranslateRunnable = null;
- mSnapRunnable.stop();
- mSnapRunnable = null;
- mRotateRunnable.stop();
- mRotateRunnable = null;
- setOnClickListener(null);
- mExternalClickListener = null;
- }
-
- /**
- * Binds a bitmap to the view.
- *
- * @param photoBitmap the bitmap to bind.
- */
- public void bindPhoto(Bitmap photoBitmap) {
- boolean changed = false;
- if (mDrawable != null) {
- final Bitmap drawableBitmap = mDrawable.getBitmap();
- if (photoBitmap == drawableBitmap) {
- // setting the same bitmap; do nothing
- return;
- }
-
- changed = photoBitmap != null &&
- (mDrawable.getIntrinsicWidth() != photoBitmap.getWidth() ||
- mDrawable.getIntrinsicHeight() != photoBitmap.getHeight());
-
- // Reset mMinScale to ensure the bounds / matrix are recalculated
- mMinScale = 0f;
- mDrawable = null;
- }
-
- if (mDrawable == null && photoBitmap != null) {
- mDrawable = new BitmapDrawable(getResources(), photoBitmap);
- }
-
- configureBounds(changed);
- invalidate();
- }
-
- /**
- * Returns the bound photo data if set. Otherwise, {@code null}.
- */
- public Bitmap getPhoto() {
- if (mDrawable != null) {
- return mDrawable.getBitmap();
- }
- return null;
- }
-
- /**
- * Gets video data associated with this item. Returns {@code null} if this is not a video.
- */
- public byte[] getVideoData() {
- return mVideoBlob;
- }
-
- /**
- * Returns {@code true} if the photo represents a video. Otherwise, {@code false}.
- */
- public boolean isVideo() {
- return mVideoBlob != null;
- }
-
- /**
- * Returns {@code true} if the video is ready to play. Otherwise, {@code false}.
- */
- public boolean isVideoReady() {
- return mVideoBlob != null && mVideoReady;
- }
-
- /**
- * Returns {@code true} if a photo has been bound. Otherwise, {@code false}.
- */
- public boolean isPhotoBound() {
- return mDrawable != null;
- }
-
- /**
- * Hides the photo info portion of the header. As a side effect, this automatically enables
- * or disables image transformations [eg zoom, pan, etc...] depending upon the value of
- * fullScreen. If this is not desirable, enable / disable image transformations manually.
- */
- public void setFullScreen(boolean fullScreen, boolean animate) {
- if (fullScreen != mFullScreen) {
- mFullScreen = fullScreen;
- requestLayout();
- invalidate();
- }
- }
-
- /**
- * Enable or disable cropping of the displayed image. Cropping can only be enabled
- * <em>before</em> the view has been laid out. Additionally, once cropping has been
- * enabled, it cannot be disabled.
- */
- public void enableAllowCrop(boolean allowCrop) {
- if (allowCrop && mHaveLayout) {
- throw new IllegalArgumentException("Cannot set crop after view has been laid out");
- }
- if (!allowCrop && mAllowCrop) {
- throw new IllegalArgumentException("Cannot unset crop mode");
- }
- mAllowCrop = allowCrop;
- }
-
- /**
- * Gets a bitmap of the cropped region. If cropping is not enabled, returns {@code null}.
- */
- public Bitmap getCroppedPhoto() {
- if (!mAllowCrop) {
- return null;
- }
-
- final Bitmap croppedBitmap = Bitmap.createBitmap(
- (int) CROPPED_SIZE, (int) CROPPED_SIZE, Bitmap.Config.ARGB_8888);
- final Canvas croppedCanvas = new Canvas(croppedBitmap);
-
- // scale for the final dimensions
- final int cropWidth = mCropRect.right - mCropRect.left;
- final float scaleWidth = CROPPED_SIZE / cropWidth;
- final float scaleHeight = CROPPED_SIZE / cropWidth;
-
- // translate to the origin & scale
- final Matrix matrix = new Matrix(mDrawMatrix);
- matrix.postTranslate(-mCropRect.left, -mCropRect.top);
- matrix.postScale(scaleWidth, scaleHeight);
-
- // draw the photo
- if (mDrawable != null) {
- croppedCanvas.concat(matrix);
- mDrawable.draw(croppedCanvas);
- }
- return croppedBitmap;
- }
-
- /**
- * Resets the image transformation to its original value.
- */
- public void resetTransformations() {
- // snap transformations; we don't animate
- mMatrix.set(mOriginalMatrix);
-
- // Invalidate the view because if you move off this PhotoView
- // to another one and come back, you want it to draw from scratch
- // in case you were zoomed in or translated (since those settings
- // are not preserved and probably shouldn't be).
- invalidate();
- }
-
- /**
- * Rotates the image 90 degrees, clockwise.
- */
- public void rotateClockwise() {
- rotate(90, true);
- }
-
- /**
- * Rotates the image 90 degrees, counter clockwise.
- */
- public void rotateCounterClockwise() {
- rotate(-90, true);
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- // draw the photo
- if (mDrawable != null) {
- int saveCount = canvas.getSaveCount();
- canvas.save();
-
- if (mDrawMatrix != null) {
- canvas.concat(mDrawMatrix);
- }
- mDrawable.draw(canvas);
-
- canvas.restoreToCount(saveCount);
-
- if (mVideoBlob != null) {
- final Bitmap videoImage = (mVideoReady ? sVideoImage : sVideoNotReadyImage);
- final int drawLeft = (getWidth() - videoImage.getWidth()) / 2;
- final int drawTop = (getHeight() - videoImage.getHeight()) / 2;
- canvas.drawBitmap(videoImage, drawLeft, drawTop, null);
- }
-
- // Extract the drawable's bounds (in our own copy, to not alter the image)
- mTranslateRect.set(mDrawable.getBounds());
- if (mDrawMatrix != null) {
- mDrawMatrix.mapRect(mTranslateRect);
- }
-
- if (mAllowCrop) {
- int previousSaveCount = canvas.getSaveCount();
- canvas.drawRect(0, 0, getWidth(), getHeight(), sCropDimPaint);
- canvas.save();
- canvas.clipRect(mCropRect);
-
- if (mDrawMatrix != null) {
- canvas.concat(mDrawMatrix);
- }
-
- mDrawable.draw(canvas);
- canvas.restoreToCount(previousSaveCount);
- canvas.drawRect(mCropRect, sCropPaint);
- }
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- mHaveLayout = true;
- final int layoutWidth = getWidth();
- final int layoutHeight = getHeight();
-
- if (mAllowCrop) {
- mCropSize = Math.min(sCropSize, Math.min(layoutWidth, layoutHeight));
- final int cropLeft = (layoutWidth - mCropSize) / 2;
- final int cropTop = (layoutHeight - mCropSize) / 2;
- final int cropRight = cropLeft + mCropSize;
- final int cropBottom = cropTop + mCropSize;
-
- // Create a crop region overlay. We need a separate canvas to be able to "punch
- // a hole" through to the underlying image.
- mCropRect.set(cropLeft, cropTop, cropRight, cropBottom);
- }
- configureBounds(changed);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (mFixedHeight != -1) {
- super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mFixedHeight,
- MeasureSpec.AT_MOST));
- setMeasuredDimension(getMeasuredWidth(), mFixedHeight);
- } else {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- /**
- * Forces a fixed height for this view.
- *
- * @param fixedHeight The height. If {@code -1}, use the measured height.
- */
- public void setFixedHeight(int fixedHeight) {
- final boolean adjustBounds = (fixedHeight != mFixedHeight);
- mFixedHeight = fixedHeight;
- setMeasuredDimension(getMeasuredWidth(), mFixedHeight);
- if (adjustBounds) {
- configureBounds(true);
- requestLayout();
- }
- }
-
- /**
- * Enable or disable image transformations. When transformations are enabled, this view
- * consumes all touch events.
- */
- public void enableImageTransforms(boolean enable) {
- mTransformsEnabled = enable;
- if (!mTransformsEnabled) {
- resetTransformations();
- }
- }
-
- /**
- * Configures the bounds of the photo. The photo will always be scaled to fit center.
- */
- private void configureBounds(boolean changed) {
- if (mDrawable == null || !mHaveLayout) {
- return;
- }
- final int dwidth = mDrawable.getIntrinsicWidth();
- final int dheight = mDrawable.getIntrinsicHeight();
-
- final int vwidth = getWidth();
- final int vheight = getHeight();
-
- final boolean fits = (dwidth < 0 || vwidth == dwidth) &&
- (dheight < 0 || vheight == dheight);
-
- // We need to do the scaling ourself, so have the drawable use its native size.
- mDrawable.setBounds(0, 0, dwidth, dheight);
-
- // Create a matrix with the proper transforms
- if (changed || (mMinScale == 0 && mDrawable != null && mHaveLayout)) {
- generateMatrix();
- generateScale();
- }
-
- if (fits || mMatrix.isIdentity()) {
- // The bitmap fits exactly, no transform needed.
- mDrawMatrix = null;
- } else {
- mDrawMatrix = mMatrix;
- }
- }
-
- /**
- * Generates the initial transformation matrix for drawing. Additionally, it sets the
- * minimum and maximum scale values.
- */
- private void generateMatrix() {
- final int dwidth = mDrawable.getIntrinsicWidth();
- final int dheight = mDrawable.getIntrinsicHeight();
-
- final int vwidth = mAllowCrop ? sCropSize : getWidth();
- final int vheight = mAllowCrop ? sCropSize : getHeight();
-
- final boolean fits = (dwidth < 0 || vwidth == dwidth) &&
- (dheight < 0 || vheight == dheight);
-
- if (fits && !mAllowCrop) {
- mMatrix.reset();
- } else {
- // Generate the required transforms for the photo
- mTempSrc.set(0, 0, dwidth, dheight);
- if (mAllowCrop) {
- mTempDst.set(mCropRect);
- } else {
- mTempDst.set(0, 0, vwidth, vheight);
- }
-
- if (dwidth < vwidth && dheight < vheight && !mAllowCrop) {
- mMatrix.setTranslate(vwidth / 2 - dwidth / 2, vheight / 2 - dheight / 2);
- } else {
- mMatrix.setRectToRect(mTempSrc, mTempDst, Matrix.ScaleToFit.CENTER);
- }
- }
- mOriginalMatrix.set(mMatrix);
- }
-
- private void generateScale() {
- final int dwidth = mDrawable.getIntrinsicWidth();
- final int dheight = mDrawable.getIntrinsicHeight();
-
- final int vwidth = mAllowCrop ? getCropSize() : getWidth();
- final int vheight = mAllowCrop ? getCropSize() : getHeight();
-
- if (dwidth < vwidth && dheight < vheight && !mAllowCrop) {
- mMinScale = 1.0f;
- } else {
- mMinScale = getScale();
- }
- mMaxScale = Math.max(mMinScale * 8, 8);
- }
-
- /**
- * @return the size of the crop regions
- */
- private int getCropSize() {
- return mCropSize > 0 ? mCropSize : sCropSize;
- }
-
- /**
- * Returns the currently applied scale factor for the image.
- * <p>
- * NOTE: This method overwrites any values stored in {@link #mValues}.
- */
- private float getScale() {
- mMatrix.getValues(mValues);
- return mValues[Matrix.MSCALE_X];
- }
-
- /**
- * Scales the image while keeping the aspect ratio.
- *
- * The given scale is capped so that the resulting scale of the image always remains
- * between {@link #mMinScale} and {@link #mMaxScale}.
- *
- * The scaled image is never allowed to be outside of the viewable area. If the image
- * is smaller than the viewable area, it will be centered.
- *
- * @param newScale the new scale
- * @param centerX the center horizontal point around which to scale
- * @param centerY the center vertical point around which to scale
- */
- private void scale(float newScale, float centerX, float centerY) {
- // rotate back to the original orientation
- mMatrix.postRotate(-mRotation, getWidth() / 2, getHeight() / 2);
-
- // ensure that mMixScale <= newScale <= mMaxScale
- newScale = Math.max(newScale, mMinScale);
- newScale = Math.min(newScale, mMaxScale);
-
- float currentScale = getScale();
- float factor = newScale / currentScale;
-
- // apply the scale factor
- mMatrix.postScale(factor, factor, centerX, centerY);
-
- // ensure the image is within the view bounds
- snap();
-
- // re-apply any rotation
- mMatrix.postRotate(mRotation, getWidth() / 2, getHeight() / 2);
-
- invalidate();
- }
-
- /**
- * Translates the image.
- *
- * This method will not allow the image to be translated outside of the visible area.
- *
- * @param tx how many pixels to translate horizontally
- * @param ty how many pixels to translate vertically
- * @return {@code true} if the translation was applied as specified. Otherwise, {@code false}
- * if the translation was modified.
- */
- private boolean translate(float tx, float ty) {
- mTranslateRect.set(mTempSrc);
- mMatrix.mapRect(mTranslateRect);
-
- final float maxLeft = mAllowCrop ? mCropRect.left : 0.0f;
- final float maxRight = mAllowCrop ? mCropRect.right : getWidth();
- float l = mTranslateRect.left;
- float r = mTranslateRect.right;
-
- final float translateX;
- if (mAllowCrop) {
- // If we're cropping, allow the image to scroll off the edge of the screen
- translateX = Math.max(maxLeft - mTranslateRect.right,
- Math.min(maxRight - mTranslateRect.left, tx));
- } else {
- // Otherwise, ensure the image never leaves the screen
- if (r - l < maxRight - maxLeft) {
- translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2;
- } else {
- translateX = Math.max(maxRight - r, Math.min(maxLeft - l, tx));
- }
- }
-
- float maxTop = mAllowCrop ? mCropRect.top: 0.0f;
- float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight();
- float t = mTranslateRect.top;
- float b = mTranslateRect.bottom;
-
- final float translateY;
-
- if (mAllowCrop) {
- // If we're cropping, allow the image to scroll off the edge of the screen
- translateY = Math.max(maxTop - mTranslateRect.bottom,
- Math.min(maxBottom - mTranslateRect.top, ty));
- } else {
- // Otherwise, ensure the image never leaves the screen
- if (b - t < maxBottom - maxTop) {
- translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2;
- } else {
- translateY = Math.max(maxBottom - b, Math.min(maxTop - t, ty));
- }
- }
-
- // Do the translation
- mMatrix.postTranslate(translateX, translateY);
- invalidate();
-
- return (translateX == tx) && (translateY == ty);
- }
-
- /**
- * Snaps the image so it touches all edges of the view.
- */
- private void snap() {
- mTranslateRect.set(mTempSrc);
- mMatrix.mapRect(mTranslateRect);
-
- // Determine how much to snap in the horizontal direction [if any]
- float maxLeft = mAllowCrop ? mCropRect.left : 0.0f;
- float maxRight = mAllowCrop ? mCropRect.right : getWidth();
- float l = mTranslateRect.left;
- float r = mTranslateRect.right;
-
- final float translateX;
- if (r - l < maxRight - maxLeft) {
- // Image is narrower than view; translate to the center of the view
- translateX = maxLeft + ((maxRight - maxLeft) - (r + l)) / 2;
- } else if (l > maxLeft) {
- // Image is off right-edge of screen; bring it into view
- translateX = maxLeft - l;
- } else if (r < maxRight) {
- // Image is off left-edge of screen; bring it into view
- translateX = maxRight - r;
- } else {
- translateX = 0.0f;
- }
-
- // Determine how much to snap in the vertical direction [if any]
- float maxTop = mAllowCrop ? mCropRect.top : 0.0f;
- float maxBottom = mAllowCrop ? mCropRect.bottom : getHeight();
- float t = mTranslateRect.top;
- float b = mTranslateRect.bottom;
-
- final float translateY;
- if (b - t < maxBottom - maxTop) {
- // Image is shorter than view; translate to the bottom edge of the view
- translateY = maxTop + ((maxBottom - maxTop) - (b + t)) / 2;
- } else if (t > maxTop) {
- // Image is off bottom-edge of screen; bring it into view
- translateY = maxTop - t;
- } else if (b < maxBottom) {
- // Image is off top-edge of screen; bring it into view
- translateY = maxBottom - b;
- } else {
- translateY = 0.0f;
- }
-
- if (Math.abs(translateX) > SNAP_THRESHOLD || Math.abs(translateY) > SNAP_THRESHOLD) {
- mSnapRunnable.start(translateX, translateY);
- } else {
- mMatrix.postTranslate(translateX, translateY);
- invalidate();
- }
- }
-
- /**
- * Rotates the image, either instantly or gradually
- *
- * @param degrees how many degrees to rotate the image, positive rotates clockwise
- * @param animate if {@code true}, animate during the rotation. Otherwise, snap rotate.
- */
- private void rotate(float degrees, boolean animate) {
- if (animate) {
- mRotateRunnable.start(degrees);
- } else {
- mRotation += degrees;
- mMatrix.postRotate(degrees, getWidth() / 2, getHeight() / 2);
- invalidate();
- }
- }
-
- /**
- * Initializes the header and any static values
- */
- private void initialize() {
- Context context = getContext();
-
- if (!sInitialized) {
- sInitialized = true;
-
- Resources resources = context.getApplicationContext().getResources();
-
- sCropSize = resources.getDimensionPixelSize(R.dimen.photo_crop_width);
-
- sCropDimPaint = new Paint();
- sCropDimPaint.setAntiAlias(true);
- sCropDimPaint.setColor(resources.getColor(R.color.photo_crop_dim_color));
- sCropDimPaint.setStyle(Style.FILL);
-
- sCropPaint = new Paint();
- sCropPaint.setAntiAlias(true);
- sCropPaint.setColor(resources.getColor(R.color.photo_crop_highlight_color));
- sCropPaint.setStyle(Style.STROKE);
- sCropPaint.setStrokeWidth(resources.getDimension(R.dimen.photo_crop_stroke_width));
- }
-
- mGestureDetector = new GestureDetectorCompat(context, this, null);
- mScaleGetureDetector = new ScaleGestureDetector(context, this);
- mScaleRunnable = new ScaleRunnable(this);
- mTranslateRunnable = new TranslateRunnable(this);
- mSnapRunnable = new SnapRunnable(this);
- mRotateRunnable = new RotateRunnable(this);
- }
-
- /**
- * Runnable that animates an image scale operation.
- */
- private static class ScaleRunnable implements Runnable {
-
- private final PhotoView mHeader;
-
- private float mCenterX;
- private float mCenterY;
-
- private boolean mZoomingIn;
-
- private float mTargetScale;
- private float mStartScale;
- private float mVelocity;
- private long mStartTime;
-
- private boolean mRunning;
- private boolean mStop;
-
- public ScaleRunnable(PhotoView header) {
- mHeader = header;
- }
-
- /**
- * Starts the animation. There is no target scale bounds check.
- */
- public boolean start(float startScale, float targetScale, float centerX, float centerY) {
- if (mRunning) {
- return false;
- }
-
- mCenterX = centerX;
- mCenterY = centerY;
-
- // Ensure the target scale is within the min/max bounds
- mTargetScale = targetScale;
- mStartTime = System.currentTimeMillis();
- mStartScale = startScale;
- mZoomingIn = mTargetScale > mStartScale;
- mVelocity = (mTargetScale - mStartScale) / ZOOM_ANIMATION_DURATION;
- mRunning = true;
- mStop = false;
- mHeader.post(this);
- return true;
- }
-
- /**
- * Stops the animation in place. It does not snap the image to its final zoom.
- */
- public void stop() {
- mRunning = false;
- mStop = true;
- }
-
- @Override
- public void run() {
- if (mStop) {
- return;
- }
-
- // Scale
- long now = System.currentTimeMillis();
- long ellapsed = now - mStartTime;
- float newScale = (mStartScale + mVelocity * ellapsed);
- mHeader.scale(newScale, mCenterX, mCenterY);
-
- // Stop when done
- if (newScale == mTargetScale || (mZoomingIn == (newScale > mTargetScale))) {
- mHeader.scale(mTargetScale, mCenterX, mCenterY);
- stop();
- }
-
- if (!mStop) {
- mHeader.post(this);
- }
- }
- }
-
- /**
- * Runnable that animates an image translation operation.
- */
- private static class TranslateRunnable implements Runnable {
-
- private static final float DECELERATION_RATE = 1000f;
- private static final long NEVER = -1L;
-
- private final PhotoView mHeader;
-
- private float mVelocityX;
- private float mVelocityY;
-
- private long mLastRunTime;
- private boolean mRunning;
- private boolean mStop;
-
- public TranslateRunnable(PhotoView header) {
- mLastRunTime = NEVER;
- mHeader = header;
- }
-
- /**
- * Starts the animation.
- */
- public boolean start(float velocityX, float velocityY) {
- if (mRunning) {
- return false;
- }
- mLastRunTime = NEVER;
- mVelocityX = velocityX;
- mVelocityY = velocityY;
- mStop = false;
- mRunning = true;
- mHeader.post(this);
- return true;
- }
-
- /**
- * Stops the animation in place. It does not snap the image to its final translation.
- */
- public void stop() {
- mRunning = false;
- mStop = true;
- }
-
- @Override
- public void run() {
- // See if we were told to stop:
- if (mStop) {
- return;
- }
-
- // Translate according to current velocities and time delta:
- long now = System.currentTimeMillis();
- float delta = (mLastRunTime != NEVER) ? (now - mLastRunTime) / 1000f : 0f;
- final boolean didTranslate = mHeader.translate(mVelocityX * delta, mVelocityY * delta);
- mLastRunTime = now;
- // Slow down:
- float slowDown = DECELERATION_RATE * delta;
- if (mVelocityX > 0f) {
- mVelocityX -= slowDown;
- if (mVelocityX < 0f) {
- mVelocityX = 0f;
- }
- } else {
- mVelocityX += slowDown;
- if (mVelocityX > 0f) {
- mVelocityX = 0f;
- }
- }
- if (mVelocityY > 0f) {
- mVelocityY -= slowDown;
- if (mVelocityY < 0f) {
- mVelocityY = 0f;
- }
- } else {
- mVelocityY += slowDown;
- if (mVelocityY > 0f) {
- mVelocityY = 0f;
- }
- }
-
- // Stop when done
- if ((mVelocityX == 0f && mVelocityY == 0f) || !didTranslate) {
- stop();
- mHeader.snap();
- }
-
- // See if we need to continue flinging:
- if (mStop) {
- return;
- }
- mHeader.post(this);
- }
- }
-
- /**
- * Runnable that animates an image translation operation.
- */
- private static class SnapRunnable implements Runnable {
-
- private static final long NEVER = -1L;
-
- private final PhotoView mHeader;
-
- private float mTranslateX;
- private float mTranslateY;
-
- private long mStartRunTime;
- private boolean mRunning;
- private boolean mStop;
-
- public SnapRunnable(PhotoView header) {
- mStartRunTime = NEVER;
- mHeader = header;
- }
-
- /**
- * Starts the animation.
- */
- public boolean start(float translateX, float translateY) {
- if (mRunning) {
- return false;
- }
- mStartRunTime = NEVER;
- mTranslateX = translateX;
- mTranslateY = translateY;
- mStop = false;
- mRunning = true;
- mHeader.postDelayed(this, SNAP_DELAY);
- return true;
- }
-
- /**
- * Stops the animation in place. It does not snap the image to its final translation.
- */
- public void stop() {
- mRunning = false;
- mStop = true;
- }
-
- @Override
- public void run() {
- // See if we were told to stop:
- if (mStop) {
- return;
- }
-
- // Translate according to current velocities and time delta:
- long now = System.currentTimeMillis();
- float delta = (mStartRunTime != NEVER) ? (now - mStartRunTime) : 0f;
-
- if (mStartRunTime == NEVER) {
- mStartRunTime = now;
- }
-
- float transX;
- float transY;
- if (delta >= SNAP_DURATION) {
- transX = mTranslateX;
- transY = mTranslateY;
- } else {
- transX = (mTranslateX / (SNAP_DURATION - delta)) * 10f;
- transY = (mTranslateY / (SNAP_DURATION - delta)) * 10f;
- if (Math.abs(transX) > Math.abs(mTranslateX) || transX == Float.NaN) {
- transX = mTranslateX;
- }
- if (Math.abs(transY) > Math.abs(mTranslateY) || transY == Float.NaN) {
- transY = mTranslateY;
- }
- }
-
- mHeader.translate(transX, transY);
- mTranslateX -= transX;
- mTranslateY -= transY;
-
- if (mTranslateX == 0 && mTranslateY == 0) {
- stop();
- }
-
- // See if we need to continue flinging:
- if (mStop) {
- return;
- }
- mHeader.post(this);
- }
- }
-
- /**
- * Runnable that animates an image rotation operation.
- */
- private static class RotateRunnable implements Runnable {
-
- private static final long NEVER = -1L;
-
- private final PhotoView mHeader;
-
- private float mTargetRotation;
- private float mAppliedRotation;
- private float mVelocity;
- private long mLastRuntime;
-
- private boolean mRunning;
- private boolean mStop;
-
- public RotateRunnable(PhotoView header) {
- mHeader = header;
- }
-
- /**
- * Starts the animation.
- */
- public void start(float rotation) {
- if (mRunning) {
- return;
- }
-
- mTargetRotation = rotation;
- mVelocity = mTargetRotation / ROTATE_ANIMATION_DURATION;
- mAppliedRotation = 0f;
- mLastRuntime = NEVER;
- mStop = false;
- mRunning = true;
- mHeader.post(this);
- }
-
- /**
- * Stops the animation in place. It does not snap the image to its final rotation.
- */
- public void stop() {
- mRunning = false;
- mStop = true;
- }
-
- @Override
- public void run() {
- if (mStop) {
- return;
- }
-
- if (mAppliedRotation != mTargetRotation) {
- long now = System.currentTimeMillis();
- long delta = mLastRuntime != NEVER ? now - mLastRuntime : 0L;
- float rotationAmount = mVelocity * delta;
- if (mAppliedRotation < mTargetRotation
- && mAppliedRotation + rotationAmount > mTargetRotation
- || mAppliedRotation > mTargetRotation
- && mAppliedRotation + rotationAmount < mTargetRotation) {
- rotationAmount = mTargetRotation - mAppliedRotation;
- }
- mHeader.rotate(rotationAmount, false);
- mAppliedRotation += rotationAmount;
- if (mAppliedRotation == mTargetRotation) {
- stop();
- }
- mLastRuntime = now;
- }
-
- if (mStop) {
- return;
- }
- mHeader.post(this);
- }
- }
-}