diff options
Diffstat (limited to 'src/com/android/gallery3d/ui/PhotoView.java')
-rw-r--r-- | src/com/android/gallery3d/ui/PhotoView.java | 1191 |
1 files changed, 1191 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java new file mode 100644 index 000000000..aba572b00 --- /dev/null +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -0,0 +1,1191 @@ +/* + * Copyright (C) 2010 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.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.GalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.PositionRepository.Position; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.RectF; +import android.os.Message; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +public class PhotoView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "PhotoView"; + + public static final int INVALID_SIZE = -1; + + private static final int MSG_TRANSITION_COMPLETE = 1; + private static final int MSG_SHOW_LOADING = 2; + + private static final long DELAY_SHOW_LOADING = 250; // 250ms; + + private static final int TRANS_NONE = 0; + private static final int TRANS_SWITCH_NEXT = 3; + private static final int TRANS_SWITCH_PREVIOUS = 4; + + public static final int TRANS_SLIDE_IN_RIGHT = 1; + public static final int TRANS_SLIDE_IN_LEFT = 2; + public static final int TRANS_OPEN_ANIMATION = 5; + + private static final int LOADING_INIT = 0; + private static final int LOADING_TIMEOUT = 1; + private static final int LOADING_COMPLETE = 2; + private static final int LOADING_FAIL = 3; + + private static final int ENTRY_PREVIOUS = 0; + private static final int ENTRY_NEXT = 1; + + private static final int IMAGE_GAP = 96; + private static final int SWITCH_THRESHOLD = 256; + private static final float SWIPE_THRESHOLD = 300f; + + private static final float DEFAULT_TEXT_SIZE = 20; + + // We try to scale up the image to fill the screen. But in order not to + // scale too much for small icons, we limit the max up-scaling factor here. + private static final float SCALE_LIMIT = 4; + + public interface PhotoTapListener { + public void onSingleTapUp(int x, int y); + } + + // the previous/next image entries + private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2]; + + private final ScaleGestureDetector mScaleDetector; + private final GestureDetector mGestureDetector; + private final DownUpDetector mDownUpDetector; + + private PhotoTapListener mPhotoTapListener; + + private final PositionController mPositionController; + + private Model mModel; + private StringTexture mLoadingText; + private StringTexture mNoThumbnailText; + private int mTransitionMode = TRANS_NONE; + private final TileImageView mTileView; + private Texture mVideoPlayIcon; + + private boolean mShowVideoPlayIcon; + private ProgressSpinner mLoadingSpinner; + + private SynchronizedHandler mHandler; + + private int mLoadingState = LOADING_COMPLETE; + + private RectF mTempRect = new RectF(); + private float[] mTempPoints = new float[8]; + + private int mImageRotation; + + private Path mOpenedItemPath; + private GalleryActivity mActivity; + + public PhotoView(GalleryActivity activity) { + mActivity = activity; + mTileView = new TileImageView(activity); + addComponent(mTileView); + Context context = activity.getAndroidContext(); + mLoadingSpinner = new ProgressSpinner(context); + mLoadingText = StringTexture.newInstance( + context.getString(R.string.loading), + DEFAULT_TEXT_SIZE, Color.WHITE); + mNoThumbnailText = StringTexture.newInstance( + context.getString(R.string.no_thumbnail), + DEFAULT_TEXT_SIZE, Color.WHITE); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TRANSITION_COMPLETE: { + onTransitionComplete(); + break; + } + case MSG_SHOW_LOADING: { + if (mLoadingState == LOADING_INIT) { + // We don't need the opening animation + mOpenedItemPath = null; + + mLoadingSpinner.startAnimation(); + mLoadingState = LOADING_TIMEOUT; + invalidate(); + } + break; + } + default: throw new AssertionError(message.what); + } + } + }; + + mGestureDetector = new GestureDetector(context, + new MyGestureListener(), null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener()); + mDownUpDetector = new DownUpDetector(new MyDownUpListener()); + + for (int i = 0, n = mScreenNails.length; i < n; ++i) { + mScreenNails[i] = new ScreenNailEntry(); + } + + mPositionController = new PositionController(this); + mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); + } + + + public void setModel(Model model) { + if (mModel == model) return; + mModel = model; + mTileView.setModel(model); + if (model != null) notifyOnNewImage(); + } + + public void setPhotoTapListener(PhotoTapListener listener) { + mPhotoTapListener = listener; + } + + private boolean setTileViewPosition(int centerX, int centerY, float scale) { + int inverseX = mPositionController.mImageW - centerX; + int inverseY = mPositionController.mImageH - centerY; + TileImageView t = mTileView; + int rotation = mImageRotation; + switch (rotation) { + case 0: return t.setPosition(centerX, centerY, scale, 0); + case 90: return t.setPosition(centerY, inverseX, scale, 90); + case 180: return t.setPosition(inverseX, inverseY, scale, 180); + case 270: return t.setPosition(inverseY, centerX, scale, 270); + default: throw new IllegalArgumentException(String.valueOf(rotation)); + } + } + + public void setPosition(int centerX, int centerY, float scale) { + if (setTileViewPosition(centerX, centerY, scale)) { + layoutScreenNails(); + } + } + + private void updateScreenNailEntry(int which, ImageData data) { + if (mTransitionMode == TRANS_SWITCH_NEXT + || mTransitionMode == TRANS_SWITCH_PREVIOUS) { + // ignore screen nail updating during switching + return; + } + ScreenNailEntry entry = mScreenNails[which]; + if (data == null) { + entry.set(false, null, 0); + } else { + entry.set(true, data.bitmap, data.rotation); + } + } + + // -1 previous, 0 current, 1 next + public void notifyImageInvalidated(int which) { + switch (which) { + case -1: { + updateScreenNailEntry( + ENTRY_PREVIOUS, mModel.getPreviousImage()); + layoutScreenNails(); + invalidate(); + break; + } + case 1: { + updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage()); + layoutScreenNails(); + invalidate(); + break; + } + case 0: { + // mImageWidth and mImageHeight will get updated + mTileView.notifyModelInvalidated(); + + mImageRotation = mModel.getImageRotation(); + if (((mImageRotation / 90) & 1) == 0) { + mPositionController.setImageSize( + mTileView.mImageWidth, mTileView.mImageHeight); + } else { + mPositionController.setImageSize( + mTileView.mImageHeight, mTileView.mImageWidth); + } + updateLoadingState(); + break; + } + } + } + + private void updateLoadingState() { + // Possible transitions of mLoadingState: + // INIT --> TIMEOUT, COMPLETE, FAIL + // TIMEOUT --> COMPLETE, FAIL, INIT + // COMPLETE --> INIT + // FAIL --> INIT + if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) { + mHandler.removeMessages(MSG_SHOW_LOADING); + mLoadingState = LOADING_COMPLETE; + } else if (mModel.isFailedToLoad()) { + mHandler.removeMessages(MSG_SHOW_LOADING); + mLoadingState = LOADING_FAIL; + } else if (mLoadingState != LOADING_INIT) { + mLoadingState = LOADING_INIT; + mHandler.removeMessages(MSG_SHOW_LOADING); + mHandler.sendEmptyMessageDelayed( + MSG_SHOW_LOADING, DELAY_SHOW_LOADING); + } + } + + public void notifyModelInvalidated() { + if (mModel == null) { + updateScreenNailEntry(ENTRY_PREVIOUS, null); + updateScreenNailEntry(ENTRY_NEXT, null); + } else { + updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage()); + updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage()); + } + layoutScreenNails(); + + if (mModel == null) { + mTileView.notifyModelInvalidated(); + mImageRotation = 0; + mPositionController.setImageSize(0, 0); + updateLoadingState(); + } else { + notifyImageInvalidated(0); + } + } + + @Override + protected boolean onTouch(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + mDownUpDetector.onTouchEvent(event); + return true; + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + mTileView.layout(left, top, right, bottom); + if (changeSize) { + mPositionController.setViewSize(getWidth(), getHeight()); + for (ScreenNailEntry entry : mScreenNails) { + entry.updateDrawingSize(); + } + } + } + + private static int gapToSide(int imageWidth, int viewWidth) { + return Math.max(0, (viewWidth - imageWidth) / 2); + } + + private RectF getImageBounds() { + PositionController p = mPositionController; + float points[] = mTempPoints; + + /* + * (p0,p1)----------(p2,p3) + * | | + * | | + * (p4,p5)----------(p6,p7) + */ + points[0] = points[4] = -p.mCurrentX; + points[1] = points[3] = -p.mCurrentY; + points[2] = points[6] = p.mImageW - p.mCurrentX; + points[5] = points[7] = p.mImageH - p.mCurrentY; + + RectF rect = mTempRect; + rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, + Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + + float scale = p.mCurrentScale; + float offsetX = p.mViewW / 2; + float offsetY = p.mViewH / 2; + for (int i = 0; i < 4; ++i) { + float x = points[i + i] * scale + offsetX; + float y = points[i + i + 1] * scale + offsetY; + if (x < rect.left) rect.left = x; + if (x > rect.right) rect.right = x; + if (y < rect.top) rect.top = y; + if (y > rect.bottom) rect.bottom = y; + } + return rect; + } + + + /* + * Here is how we layout the screen nails + * + * previous current next + * ___________ ________________ __________ + * | _______ | | __________ | | ______ | + * | | | | | | right->| | | | | | + * | | |<-------->|<--left | | | | | | + * | |_______| | | | |__________| | | |______| | + * |___________| | |________________| |__________| + * | <--> gapToSide() + * | + * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide) + */ + private void layoutScreenNails() { + int width = getWidth(); + int height = getHeight(); + + // Use the image width in AC, since we may fake the size if the + // image is unavailable + RectF bounds = getImageBounds(); + int left = Math.round(bounds.left); + int right = Math.round(bounds.right); + int gap = gapToSide(right - left, width); + + // layout the previous image + ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS]; + + if (entry.isEnabled()) { + entry.layoutRightEdgeAt(left - ( + IMAGE_GAP + Math.max(gap, entry.gapToSide()))); + } + + // layout the next image + entry = mScreenNails[ENTRY_NEXT]; + if (entry.isEnabled()) { + entry.layoutLeftEdgeAt(right + ( + IMAGE_GAP + Math.max(gap, entry.gapToSide()))); + } + } + + private static class PositionController { + private long mAnimationStartTime = NO_ANIMATION; + private static final long NO_ANIMATION = -1; + private static final long LAST_ANIMATION = -2; + + // Animation time in milliseconds. + private static final float ANIM_TIME_SCROLL = 0; + private static final float ANIM_TIME_SCALE = 50; + private static final float ANIM_TIME_SNAPBACK = 600; + private static final float ANIM_TIME_SLIDE = 400; + private static final float ANIM_TIME_ZOOM = 300; + + private int mAnimationKind; + private final static int ANIM_KIND_SCROLL = 0; + private final static int ANIM_KIND_SCALE = 1; + private final static int ANIM_KIND_SNAPBACK = 2; + private final static int ANIM_KIND_SLIDE = 3; + private final static int ANIM_KIND_ZOOM = 4; + + private PhotoView mViewer; + private int mImageW, mImageH; + private int mViewW, mViewH; + + // The X, Y are the coordinate on bitmap which shows on the center of + // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual + // values used currently. + private int mCurrentX, mFromX, mToX; + private int mCurrentY, mFromY, mToY; + private float mCurrentScale, mFromScale, mToScale; + + // The offsets from the center of the view to the user's focus point, + // converted to the bitmap domain. + private float mPrevOffsetX; + private float mPrevOffsetY; + private boolean mInScale; + private boolean mUseViewSize = true; + + // The limits for position and scale. + private float mScaleMin, mScaleMax = 4f; + + PositionController(PhotoView viewer) { + mViewer = viewer; + } + + public void setImageSize(int width, int height) { + + // If no image available, use view size. + if (width == 0 || height == 0) { + mUseViewSize = true; + mImageW = mViewW; + mImageH = mViewH; + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = 1; + mScaleMin = 1; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + return; + } + + mUseViewSize = false; + + float ratio = Math.min( + (float) mImageW / width, (float) mImageH / height); + + mCurrentX = translate(mCurrentX, mImageW, width, ratio); + mCurrentY = translate(mCurrentY, mImageH, height, ratio); + mCurrentScale = mCurrentScale * ratio; + + mFromX = translate(mFromX, mImageW, width, ratio); + mFromY = translate(mFromY, mImageH, height, ratio); + mFromScale = mFromScale * ratio; + + mToX = translate(mToX, mImageW, width, ratio); + mToY = translate(mToY, mImageH, height, ratio); + mToScale = mToScale * ratio; + + mImageW = width; + mImageH = height; + + mScaleMin = getMinimalScale(width, height, 0); + + // Scale the new image to fit into the old one + if (mViewer.mOpenedItemPath != null) { + Position position = PositionRepository + .getInstance(mViewer.mActivity).get(Long.valueOf( + System.identityHashCode(mViewer.mOpenedItemPath))); + mViewer.mOpenedItemPath = null; + if (position != null) { + float scale = 240f / Math.min(width, height); + mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2; + mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2; + mCurrentScale = scale; + mViewer.mTransitionMode = TRANS_OPEN_ANIMATION; + startSnapback(); + } + } else if (mAnimationStartTime == NO_ANIMATION) { + mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); + } + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } + + public void zoomIn(float tapX, float tapY, float targetScale) { + if (targetScale > mScaleMax) targetScale = mScaleMax; + float scale = mCurrentScale; + float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX; + float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY; + + // mCurrentX + (mViewW / 2) * (1 / targetScale) < mImageW + // mCurrentX - (mViewW / 2) * (1 / targetScale) > 0 + float min = mViewW / 2.0f / targetScale; + float max = mImageW - mViewW / 2.0f / targetScale; + int targetX = (int) Utils.clamp(tempX, min, max); + + min = mViewH / 2.0f / targetScale; + max = mImageH - mViewH / 2.0f / targetScale; + int targetY = (int) Utils.clamp(tempY, min, max); + + // If the width of the image is less then the view, center the image + if (mImageW * targetScale < mViewW) targetX = mImageW / 2; + if (mImageH * targetScale < mViewH) targetY = mImageH / 2; + + startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); + } + + public void resetToFullView() { + startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM); + } + + private float getMinimalScale(int w, int h, int rotation) { + return Math.min(SCALE_LIMIT, ((rotation / 90) & 0x01) == 0 + ? Math.min((float) mViewW / w, (float) mViewH / h) + : Math.min((float) mViewW / h, (float) mViewH / w)); + } + + private static int translate(int value, int size, int updateSize, float ratio) { + return Math.round( + (value + (updateSize * ratio - size) / 2f) / ratio); + } + + public void setViewSize(int viewW, int viewH) { + boolean needLayout = mViewW == 0 || mViewH == 0; + + mViewW = viewW; + mViewH = viewH; + + if (mUseViewSize) { + mImageW = viewW; + mImageH = viewH; + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = 1; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } else { + boolean wasMinScale = (mCurrentScale == mScaleMin); + mScaleMin = Math.min(SCALE_LIMIT, Math.min( + (float) viewW / mImageW, (float) viewH / mImageH)); + if (needLayout || mCurrentScale < mScaleMin || wasMinScale) { + mCurrentX = mImageW / 2; + mCurrentY = mImageH / 2; + mCurrentScale = mScaleMin; + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + } + } + } + + public void stopAnimation() { + mAnimationStartTime = NO_ANIMATION; + } + + public void skipAnimation() { + if (mAnimationStartTime == NO_ANIMATION) return; + mAnimationStartTime = NO_ANIMATION; + mCurrentX = mToX; + mCurrentY = mToY; + mCurrentScale = mToScale; + } + + public void scrollBy(float dx, float dy, int type) { + startAnimation(getTargetX() + Math.round(dx / mCurrentScale), + getTargetY() + Math.round(dy / mCurrentScale), + mCurrentScale, type); + } + + public void beginScale(float focusX, float focusY) { + mInScale = true; + mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale; + mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale; + } + + public void scaleBy(float s, float focusX, float focusY) { + + // The focus point should keep this position on the ImageView. + // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX. + // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY. + float offsetX = (focusX - mViewW / 2f) / mCurrentScale; + float offsetY = (focusY - mViewH / 2f) / mCurrentScale; + + startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX), + getTargetY() - Math.round(offsetY - mPrevOffsetY), + getTargetScale() * s, ANIM_KIND_SCALE); + mPrevOffsetX = offsetX; + mPrevOffsetY = offsetY; + } + + public void endScale() { + mInScale = false; + startSnapbackIfNeeded(); + } + + public void up() { + startSnapback(); + } + + public void startSlideInAnimation(int fromX) { + mFromX = Math.round(fromX + (mImageW - mViewW) / 2f); + mFromY = Math.round(mImageH / 2f); + mCurrentX = mFromX; + mCurrentY = mFromY; + startAnimation(mImageW / 2, mImageH / 2, mCurrentScale, + ANIM_KIND_SLIDE); + } + + public void startHorizontalSlide(int distance) { + scrollBy(distance, 0, ANIM_KIND_SLIDE); + } + + private void startAnimation( + int centerX, int centerY, float scale, int kind) { + if (centerX == mCurrentX && centerY == mCurrentY + && scale == mCurrentScale) return; + + mFromX = mCurrentX; + mFromY = mCurrentY; + mFromScale = mCurrentScale; + + mToX = centerX; + mToY = centerY; + mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax); + + // If the scaled dimension is smaller than the view, + // force it to be in the center. + if (Math.floor(mImageH * mToScale) <= mViewH) { + mToY = mImageH / 2; + } + + mAnimationStartTime = SystemClock.uptimeMillis(); + mAnimationKind = kind; + if (advanceAnimation()) mViewer.invalidate(); + } + + // Returns true if redraw is needed. + public boolean advanceAnimation() { + if (mAnimationStartTime == NO_ANIMATION) { + return false; + } else if (mAnimationStartTime == LAST_ANIMATION) { + mAnimationStartTime = NO_ANIMATION; + if (mViewer.mTransitionMode != TRANS_NONE) { + mViewer.mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE); + return false; + } else { + return startSnapbackIfNeeded(); + } + } + + float animationTime; + if (mAnimationKind == ANIM_KIND_SCROLL) { + animationTime = ANIM_TIME_SCROLL; + } else if (mAnimationKind == ANIM_KIND_SCALE) { + animationTime = ANIM_TIME_SCALE; + } else if (mAnimationKind == ANIM_KIND_SLIDE) { + animationTime = ANIM_TIME_SLIDE; + } else if (mAnimationKind == ANIM_KIND_ZOOM) { + animationTime = ANIM_TIME_ZOOM; + } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ { + animationTime = ANIM_TIME_SNAPBACK; + } + + float progress; + if (animationTime == 0) { + progress = 1; + } else { + long now = SystemClock.uptimeMillis(); + progress = (now - mAnimationStartTime) / animationTime; + } + + if (progress >= 1) { + progress = 1; + mCurrentX = mToX; + mCurrentY = mToY; + mCurrentScale = mToScale; + mAnimationStartTime = LAST_ANIMATION; + } else { + float f = 1 - progress; + if (mAnimationKind == ANIM_KIND_SCROLL) { + progress = 1 - f; // linear + } else if (mAnimationKind == ANIM_KIND_SCALE) { + progress = 1 - f * f; // quadratic + } else /* if mAnimationKind is ANIM_KIND_SNAPBACK, + ANIM_KIND_ZOOM or ANIM_KIND_SLIDE */ { + progress = 1 - f * f * f * f * f; // x^5 + } + linearInterpolate(progress); + } + mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale); + return true; + } + + private void linearInterpolate(float progress) { + // To linearly interpolate the position, we have to translate the + // coordinates. The meaning of the translated point (x, y) is the + // coordinates of the center of the bitmap on the view component. + float fromX = mViewW / 2f + (mImageW / 2f - mFromX) * mFromScale; + float toX = mViewW / 2f + (mImageW / 2f - mToX) * mToScale; + float currentX = fromX + progress * (toX - fromX); + + float fromY = mViewH / 2f + (mImageH / 2f - mFromY) * mFromScale; + float toY = mViewH / 2f + (mImageH / 2f - mToY) * mToScale; + float currentY = fromY + progress * (toY - fromY); + + mCurrentScale = mFromScale + progress * (mToScale - mFromScale); + mCurrentX = Math.round( + mImageW / 2f + (mViewW / 2f - currentX) / mCurrentScale); + mCurrentY = Math.round( + mImageH / 2f + (mViewH / 2f - currentY) / mCurrentScale); + } + + // Returns true if redraw is needed. + private boolean startSnapbackIfNeeded() { + if (mAnimationStartTime != NO_ANIMATION) return false; + if (mInScale) return false; + if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) { + return false; + } + return startSnapback(); + } + + public boolean startSnapback() { + boolean needAnimation = false; + int x = mCurrentX; + int y = mCurrentY; + float scale = mCurrentScale; + + if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) { + needAnimation = true; + scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax); + } + + // The number of pixels when the edge is aligned. + int left = (int) Math.ceil(mViewW / (2 * scale)); + int right = mImageW - left; + int top = (int) Math.ceil(mViewH / (2 * scale)); + int bottom = mImageH - top; + + if (mImageW * scale > mViewW) { + if (mCurrentX < left) { + needAnimation = true; + x = left; + } else if (mCurrentX > right) { + needAnimation = true; + x = right; + } + } else if (mCurrentX != mImageW / 2) { + needAnimation = true; + x = mImageW / 2; + } + + if (mImageH * scale > mViewH) { + if (mCurrentY < top) { + needAnimation = true; + y = top; + } else if (mCurrentY > bottom) { + needAnimation = true; + y = bottom; + } + } else if (mCurrentY != mImageH / 2) { + needAnimation = true; + y = mImageH / 2; + } + + if (needAnimation) { + startAnimation(x, y, scale, ANIM_KIND_SNAPBACK); + } + + return needAnimation; + } + + private float getTargetScale() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale; + return mToScale; + } + + private int getTargetX() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX; + return mToX; + } + + private int getTargetY() { + if (mAnimationStartTime == NO_ANIMATION + || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY; + return mToY; + } + } + + @Override + protected void render(GLCanvas canvas) { + PositionController p = mPositionController; + + // Draw the current photo + if (mLoadingState == LOADING_COMPLETE) { + super.render(canvas); + } + + // Draw the previous and the next photo + if (mTransitionMode != TRANS_SLIDE_IN_LEFT + && mTransitionMode != TRANS_SLIDE_IN_RIGHT + && mTransitionMode != TRANS_OPEN_ANIMATION) { + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + + if (prevNail.mVisible) prevNail.draw(canvas); + if (nextNail.mVisible) nextNail.draw(canvas); + } + + // Draw the progress spinner and the text below it + // + // (x, y) is where we put the center of the spinner. + // s is the size of the video play icon, and we use s to layout text + // because we want to keep the text at the same place when the video + // play icon is shown instead of the spinner. + int w = getWidth(); + int h = getHeight(); + int x = Math.round(getImageBounds().centerX()); + int y = h / 2; + int s = Math.min(getWidth(), getHeight()) / 6; + + if (mLoadingState == LOADING_TIMEOUT) { + StringTexture m = mLoadingText; + ProgressSpinner r = mLoadingSpinner; + r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2); + m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); + invalidate(); // we need to keep the spinner rotating + } else if (mLoadingState == LOADING_FAIL) { + StringTexture m = mNoThumbnailText; + m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5); + } + + // Draw the video play icon (in the place where the spinner was) + if (mShowVideoPlayIcon + && mLoadingState != LOADING_INIT + && mLoadingState != LOADING_TIMEOUT) { + mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s); + } + + if (mPositionController.advanceAnimation()) invalidate(); + } + + private void stopCurrentSwipingIfNeeded() { + // Enable fast sweeping + if (mTransitionMode == TRANS_SWITCH_NEXT) { + mTransitionMode = TRANS_NONE; + mPositionController.stopAnimation(); + switchToNextImage(); + } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) { + mTransitionMode = TRANS_NONE; + mPositionController.stopAnimation(); + switchToPreviousImage(); + } + } + + private static boolean isAlmostEquals(float a, float b) { + float diff = a - b; + return (diff < 0 ? -diff : diff) < 0.02f; + } + + private boolean swipeImages(float velocity) { + if (mTransitionMode != TRANS_NONE + && mTransitionMode != TRANS_SWITCH_NEXT + && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false; + + ScreenNailEntry next = mScreenNails[ENTRY_NEXT]; + ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS]; + + int width = getWidth(); + + // If the edge of the current photo is visible and the sweeping velocity + // exceed the threshold, switch to next / previous image + PositionController controller = mPositionController; + if (isAlmostEquals(controller.mCurrentScale, controller.mScaleMin)) { + if (velocity < -SWIPE_THRESHOLD) { + stopCurrentSwipingIfNeeded(); + if (next.isEnabled()) { + mTransitionMode = TRANS_SWITCH_NEXT; + controller.startHorizontalSlide(next.mOffsetX - width / 2); + return true; + } + return false; + } + if (velocity > SWIPE_THRESHOLD) { + stopCurrentSwipingIfNeeded(); + if (prev.isEnabled()) { + mTransitionMode = TRANS_SWITCH_PREVIOUS; + controller.startHorizontalSlide(prev.mOffsetX - width / 2); + return true; + } + return false; + } + } + + if (mTransitionMode != TRANS_NONE) return false; + + // Decide whether to swiping to the next/prev image in the zoom-in case + RectF bounds = getImageBounds(); + int left = Math.round(bounds.left); + int right = Math.round(bounds.right); + int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width); + + // If we have moved the picture a lot, switching. + if (next.isEnabled() && threshold < width - right) { + mTransitionMode = TRANS_SWITCH_NEXT; + controller.startHorizontalSlide(next.mOffsetX - width / 2); + return true; + } + if (prev.isEnabled() && threshold < left) { + mTransitionMode = TRANS_SWITCH_PREVIOUS; + controller.startHorizontalSlide(prev.mOffsetX - width / 2); + return true; + } + + return false; + } + + private boolean mIgnoreUpEvent = false; + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + if (mTransitionMode != TRANS_NONE) return true; + mPositionController.scrollBy( + dx, dy, PositionController.ANIM_KIND_SCROLL); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (mPhotoTapListener != null) { + mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY()); + } + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + mIgnoreUpEvent = true; + if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) { + mPositionController.up(); + } + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (mTransitionMode != TRANS_NONE) return true; + PositionController controller = mPositionController; + float scale = controller.mCurrentScale; + // onDoubleTap happened on the second ACTION_DOWN. + // We need to ignore the next UP event. + mIgnoreUpEvent = true; + if (scale <= 1.0f || isAlmostEquals(scale, controller.mScaleMin)) { + controller.zoomIn( + e.getX(), e.getY(), Math.max(1.5f, scale * 1.5f)); + } else { + controller.resetToFullView(); + } + return true; + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scale = detector.getScaleFactor(); + if (Float.isNaN(scale) || Float.isInfinite(scale) + || mTransitionMode != TRANS_NONE) return true; + mPositionController.scaleBy(scale, + detector.getFocusX(), detector.getFocusY()); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (mTransitionMode != TRANS_NONE) return false; + mPositionController.beginScale( + detector.getFocusX(), detector.getFocusY()); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mPositionController.endScale(); + swipeImages(0); + } + } + + public void notifyOnNewImage() { + mPositionController.setImageSize(0, 0); + } + + public void startSlideInAnimation(int direction) { + PositionController a = mPositionController; + a.stopAnimation(); + switch (direction) { + case TRANS_SLIDE_IN_LEFT: { + mTransitionMode = TRANS_SLIDE_IN_LEFT; + a.startSlideInAnimation(a.mViewW); + break; + } + case TRANS_SLIDE_IN_RIGHT: { + mTransitionMode = TRANS_SLIDE_IN_RIGHT; + a.startSlideInAnimation(-a.mViewW); + break; + } + default: throw new IllegalArgumentException(String.valueOf(direction)); + } + } + + private class MyDownUpListener implements DownUpDetector.DownUpListener { + public void onDown(MotionEvent e) { + } + + public void onUp(MotionEvent e) { + if (mIgnoreUpEvent) { + mIgnoreUpEvent = false; + return; + } + if (!swipeImages(0) && mTransitionMode == TRANS_NONE) { + mPositionController.up(); + } + } + } + + private void switchToNextImage() { + // We update the texture here directly to prevent texture uploading. + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + mTileView.invalidateTiles(); + if (prevNail.mTexture != null) prevNail.mTexture.recycle(); + prevNail.mTexture = mTileView.mBackupImage; + mTileView.mBackupImage = nextNail.mTexture; + nextNail.mTexture = null; + mModel.next(); + } + + private void switchToPreviousImage() { + // We update the texture here directly to prevent texture uploading. + ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS]; + ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT]; + mTileView.invalidateTiles(); + if (nextNail.mTexture != null) nextNail.mTexture.recycle(); + nextNail.mTexture = mTileView.mBackupImage; + mTileView.mBackupImage = prevNail.mTexture; + nextNail.mTexture = null; + mModel.previous(); + } + + private void onTransitionComplete() { + int mode = mTransitionMode; + mTransitionMode = TRANS_NONE; + + if (mModel == null) return; + if (mode == TRANS_SWITCH_NEXT) { + switchToNextImage(); + } else if (mode == TRANS_SWITCH_PREVIOUS) { + switchToPreviousImage(); + } + } + + private boolean isDown() { + return mDownUpDetector.isDown(); + } + + public static interface Model extends TileImageView.Model { + public void next(); + public void previous(); + public int getImageRotation(); + + // Return null if the specified image is unavailable. + public ImageData getNextImage(); + public ImageData getPreviousImage(); + } + + public static class ImageData { + public int rotation; + public Bitmap bitmap; + + public ImageData(Bitmap bitmap, int rotation) { + this.bitmap = bitmap; + this.rotation = rotation; + } + } + + private static int getRotated(int degree, int original, int theother) { + return ((degree / 90) & 1) == 0 ? original : theother; + } + + private class ScreenNailEntry { + private boolean mVisible; + private boolean mEnabled; + + private int mRotation; + private int mDrawWidth; + private int mDrawHeight; + private int mOffsetX; + + private BitmapTexture mTexture; + + public void set(boolean enabled, Bitmap bitmap, int rotation) { + mEnabled = enabled; + mRotation = rotation; + if (bitmap == null) { + if (mTexture != null) mTexture.recycle(); + mTexture = null; + } else { + if (mTexture != null) { + if (mTexture.getBitmap() != bitmap) { + mTexture.recycle(); + mTexture = new BitmapTexture(bitmap); + } + } else { + mTexture = new BitmapTexture(bitmap); + } + updateDrawingSize(); + } + } + + public void layoutRightEdgeAt(int x) { + mVisible = x > 0; + mOffsetX = x - getRotated( + mRotation, mDrawWidth, mDrawHeight) / 2; + } + + public void layoutLeftEdgeAt(int x) { + mVisible = x < getWidth(); + mOffsetX = x + getRotated( + mRotation, mDrawWidth, mDrawHeight) / 2; + } + + public int gapToSide() { + return ((mRotation / 90) & 1) != 0 + ? PhotoView.gapToSide(mDrawHeight, getWidth()) + : PhotoView.gapToSide(mDrawWidth, getWidth()); + } + + public void updateDrawingSize() { + if (mTexture == null) return; + + int width = mTexture.getWidth(); + int height = mTexture.getHeight(); + float s = mPositionController.getMinimalScale(width, height, mRotation); + mDrawWidth = Math.round(width * s); + mDrawHeight = Math.round(height * s); + } + + public boolean isEnabled() { + return mEnabled; + } + + public void draw(GLCanvas canvas) { + int x = mOffsetX; + int y = getHeight() / 2; + + if (mTexture != null) { + if (mRotation != 0) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.translate(x, y, 0); + canvas.rotate(mRotation, 0, 0, 1); //mRotation + canvas.translate(-x, -y, 0); + } + mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2, + mDrawWidth, mDrawHeight); + if (mRotation != 0) { + canvas.restore(); + } + } + } + } + + public void pause() { + mPositionController.skipAnimation(); + mTransitionMode = TRANS_NONE; + mTileView.freeTextures(); + for (ScreenNailEntry entry : mScreenNails) { + entry.set(false, null, 0); + } + } + + public void resume() { + mTileView.prepareTextures(); + } + + public void setOpenedItem(Path itemPath) { + mOpenedItemPath = itemPath; + } + + public void showVideoPlayIcon(boolean show) { + mShowVideoPlayIcon = show; + } +} |