| /* |
| * 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; |
| } |
| } |