diff options
Diffstat (limited to 'photoviewer/src/com/android/ex/photo/views/PhotoView.java')
-rw-r--r-- | photoviewer/src/com/android/ex/photo/views/PhotoView.java | 1288 |
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); - } - } -} |