| /* |
| * 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 android.content.Context; |
| import android.graphics.Color; |
| import android.graphics.Matrix; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.Message; |
| import android.view.MotionEvent; |
| import android.view.animation.AccelerateInterpolator; |
| |
| import com.android.gallery3d.R; |
| import com.android.gallery3d.app.GalleryActivity; |
| import com.android.gallery3d.common.Utils; |
| import com.android.gallery3d.data.MediaItem; |
| import com.android.gallery3d.data.MediaObject; |
| import com.android.gallery3d.util.RangeArray; |
| |
| public class PhotoView extends GLView { |
| @SuppressWarnings("unused") |
| private static final String TAG = "PhotoView"; |
| private static final int PLACEHOLDER_COLOR = 0xFF222222; |
| |
| public static final int INVALID_SIZE = -1; |
| public static final long INVALID_DATA_VERSION = |
| MediaObject.INVALID_DATA_VERSION; |
| |
| public static class Size { |
| public int width; |
| public int height; |
| } |
| |
| public interface Model extends TileImageView.Model { |
| public int getCurrentIndex(); |
| public void moveTo(int index); |
| |
| // Returns the size for the specified picture. If the size information is |
| // not avaiable, width = height = 0. |
| public void getImageSize(int offset, Size size); |
| |
| // Returns the media item for the specified picture. |
| public MediaItem getMediaItem(int offset); |
| |
| // Returns the rotation for the specified picture. |
| public int getImageRotation(int offset); |
| |
| // This amends the getScreenNail() method of TileImageView.Model to get |
| // ScreenNail at previous (negative offset) or next (positive offset) |
| // positions. Returns null if the specified ScreenNail is unavailable. |
| public ScreenNail getScreenNail(int offset); |
| |
| // Set this to true if we need the model to provide full images. |
| public void setNeedFullImage(boolean enabled); |
| |
| // Returns true if the item is the Camera preview. |
| public boolean isCamera(int offset); |
| |
| // Returns true if the item is the Panorama. |
| public boolean isPanorama(int offset); |
| |
| // Returns true if the item is a Video. |
| public boolean isVideo(int offset); |
| |
| public static final int LOADING_INIT = 0; |
| public static final int LOADING_COMPLETE = 1; |
| public static final int LOADING_FAIL = 2; |
| |
| public int getLoadingState(int offset); |
| } |
| |
| public interface Listener { |
| public void onSingleTapUp(int x, int y); |
| public void lockOrientation(); |
| public void unlockOrientation(); |
| public void onFullScreenChanged(boolean full); |
| public void onActionBarAllowed(boolean allowed); |
| public void onCurrentImageUpdated(); |
| } |
| |
| // Here is a graph showing the places we need to lock/unlock device |
| // orientation: |
| // |
| // +------------+ A +------------+ |
| // Page mode | Camera |<---| Photo | |
| // | [locked] |--->| [unlocked] | |
| // +------------+ B +------------+ |
| // ^ ^ |
| // | C | D |
| // +------------+ +------------+ |
| // | Camera | | Photo | |
| // Film mode | [*] | | [*] | |
| // +------------+ +------------+ |
| // |
| // In Page mode, we want to lock in Camera because we don't want the system |
| // rotation animation. We also want to unlock in Photo because we want to |
| // show the system action bar in the right place. |
| // |
| // We don't show action bar in Film mode, so it's fine for it to be locked |
| // or unlocked in Film mode. |
| // |
| // There are four transitions we need to check if we need to |
| // lock/unlock. Marked as A to D above and in the code. |
| |
| private static final int MSG_CANCEL_EXTRA_SCALING = 2; |
| private static final int MSG_SWITCH_FOCUS = 3; |
| private static final int MSG_CAPTURE_ANIMATION_DONE = 4; |
| |
| private static final int MOVE_THRESHOLD = 256; |
| private static final float SWIPE_THRESHOLD = 300f; |
| |
| private static final float DEFAULT_TEXT_SIZE = 20; |
| private static float TRANSITION_SCALE_FACTOR = 0.74f; |
| private static final int ICON_RATIO = 6; |
| |
| // whether we want to apply card deck effect in page mode. |
| private static final boolean CARD_EFFECT = true; |
| |
| // Used to calculate the scaling factor for the fading animation. |
| private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); |
| |
| // Used to calculate the alpha factor for the fading animation. |
| private AccelerateInterpolator mAlphaInterpolator = |
| new AccelerateInterpolator(0.9f); |
| |
| // We keep this many previous ScreenNails. (also this many next ScreenNails) |
| public static final int SCREEN_NAIL_MAX = 3; |
| |
| // The picture entries, the valid index is from -SCREEN_NAIL_MAX to |
| // SCREEN_NAIL_MAX. |
| private final RangeArray<Picture> mPictures = |
| new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX); |
| |
| private final MyGestureListener mGestureListener; |
| private final GestureRecognizer mGestureRecognizer; |
| private final PositionController mPositionController; |
| |
| private Listener mListener; |
| private Model mModel; |
| private StringTexture mLoadingText; |
| private StringTexture mNoThumbnailText; |
| private TileImageView mTileView; |
| private EdgeView mEdgeView; |
| private Texture mVideoPlayIcon; |
| |
| private SynchronizedHandler mHandler; |
| |
| private Point mImageCenter = new Point(); |
| private boolean mCancelExtraScalingPending; |
| private boolean mFilmMode = false; |
| private int mDisplayRotation = 0; |
| private int mCompensation = 0; |
| private boolean mFullScreen = true; |
| private Rect mCameraRelativeFrame = new Rect(); |
| private Rect mCameraRect = new Rect(); |
| |
| // [mPrevBound, mNextBound] is the range of index for all pictures in the |
| // model, if we assume the index of current focused picture is 0. So if |
| // there are some previous pictures, mPrevBound < 0, and if there are some |
| // next pictures, mNextBound > 0. |
| private int mPrevBound; |
| private int mNextBound; |
| |
| // This variable prevents us doing snapback until its values goes to 0. This |
| // happens if the user gesture is still in progress or we are in a capture |
| // animation. |
| private int mHolding; |
| private static final int HOLD_TOUCH_DOWN = 1; |
| private static final int HOLD_CAPTURE_ANIMATION = 2; |
| |
| public PhotoView(GalleryActivity activity) { |
| mTileView = new TileImageView(activity); |
| addComponent(mTileView); |
| Context context = activity.getAndroidContext(); |
| mEdgeView = new EdgeView(context); |
| addComponent(mEdgeView); |
| 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 MyHandler(activity.getGLRoot()); |
| |
| mGestureListener = new MyGestureListener(); |
| mGestureRecognizer = new GestureRecognizer(context, mGestureListener); |
| |
| mPositionController = new PositionController(context, |
| new PositionController.Listener() { |
| public void invalidate() { |
| PhotoView.this.invalidate(); |
| } |
| public boolean isHolding() { |
| return mHolding != 0; |
| } |
| public void onPull(int offset, int direction) { |
| mEdgeView.onPull(offset, direction); |
| } |
| public void onRelease() { |
| mEdgeView.onRelease(); |
| } |
| public void onAbsorb(int velocity, int direction) { |
| mEdgeView.onAbsorb(velocity, direction); |
| } |
| }); |
| mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play); |
| for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { |
| if (i == 0) { |
| mPictures.put(i, new FullPicture()); |
| } else { |
| mPictures.put(i, new ScreenNailPicture(i)); |
| } |
| } |
| } |
| |
| public void setModel(Model model) { |
| mModel = model; |
| mTileView.setModel(mModel); |
| } |
| |
| class MyHandler extends SynchronizedHandler { |
| public MyHandler(GLRoot root) { |
| super(root); |
| } |
| |
| @Override |
| public void handleMessage(Message message) { |
| switch (message.what) { |
| case MSG_CANCEL_EXTRA_SCALING: { |
| mGestureRecognizer.cancelScale(); |
| mPositionController.setExtraScalingRange(false); |
| mCancelExtraScalingPending = false; |
| break; |
| } |
| case MSG_SWITCH_FOCUS: { |
| switchFocus(); |
| break; |
| } |
| case MSG_CAPTURE_ANIMATION_DONE: { |
| // message.arg1 is the offset parameter passed to |
| // switchWithCaptureAnimation(). |
| captureAnimationDone(message.arg1); |
| break; |
| } |
| default: throw new AssertionError(message.what); |
| } |
| } |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Data/Image change notifications |
| //////////////////////////////////////////////////////////////////////////// |
| |
| public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) { |
| mPrevBound = prevBound; |
| mNextBound = nextBound; |
| |
| // Move the boxes |
| mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0, |
| mModel.isCamera(0)); |
| |
| // Update the ScreenNails. |
| for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { |
| mPictures.get(i).reload(); |
| } |
| |
| invalidate(); |
| } |
| |
| public void notifyImageChange(int index) { |
| if (index == 0) { |
| mListener.onCurrentImageUpdated(); |
| } |
| mPictures.get(index).reload(); |
| invalidate(); |
| } |
| |
| @Override |
| protected void onLayout( |
| boolean changeSize, int left, int top, int right, int bottom) { |
| int w = right - left; |
| int h = bottom - top; |
| mTileView.layout(0, 0, w, h); |
| mEdgeView.layout(0, 0, w, h); |
| |
| GLRoot root = getGLRoot(); |
| int displayRotation = root.getDisplayRotation(); |
| int compensation = root.getCompensation(); |
| if (mDisplayRotation != displayRotation |
| || mCompensation != compensation) { |
| mDisplayRotation = displayRotation; |
| mCompensation = compensation; |
| |
| // We need to change the size and rotation of the Camera ScreenNail, |
| // but we don't want it to animate because the size doen't actually |
| // change in the eye of the user. |
| for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { |
| Picture p = mPictures.get(i); |
| if (p.isCamera()) { |
| p.updateSize(true); |
| } |
| } |
| } |
| |
| updateConstrainedFrame(); |
| if (changeSize) { |
| mPositionController.setViewSize(getWidth(), getHeight()); |
| } |
| } |
| |
| // Update the constrained frame due to layout change. |
| private void updateConstrainedFrame() { |
| // Get the width and height in framework orientation because the given |
| // mCameraRelativeFrame is in that coordinates. |
| int w = getWidth(); |
| int h = getHeight(); |
| if (mCompensation % 180 != 0) { |
| int tmp = w; |
| w = h; |
| h = tmp; |
| } |
| int l = mCameraRelativeFrame.left; |
| int t = mCameraRelativeFrame.top; |
| int r = mCameraRelativeFrame.right; |
| int b = mCameraRelativeFrame.bottom; |
| |
| // Now convert it to the coordinates we are using. |
| switch (mCompensation) { |
| case 0: mCameraRect.set(l, t, r, b); break; |
| case 90: mCameraRect.set(h - b, l, h - t, r); break; |
| case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break; |
| case 270: mCameraRect.set(t, w - r, b, w - l); break; |
| } |
| |
| Log.d(TAG, "compensation = " + mCompensation |
| + ", CameraRelativeFrame = " + mCameraRelativeFrame |
| + ", mCameraRect = " + mCameraRect); |
| mPositionController.setConstrainedFrame(mCameraRect); |
| } |
| |
| public void setCameraRelativeFrame(Rect frame) { |
| mCameraRelativeFrame.set(frame); |
| updateConstrainedFrame(); |
| } |
| |
| // Returns the rotation we need to do to the camera texture before drawing |
| // it to the canvas, assuming the camera texture is correct when the device |
| // is in its natural orientation. |
| private int getCameraRotation() { |
| return (mCompensation - mDisplayRotation + 360) % 360; |
| } |
| |
| private int getPanoramaRotation() { |
| return mCompensation; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Pictures |
| //////////////////////////////////////////////////////////////////////////// |
| |
| private interface Picture { |
| void reload(); |
| void draw(GLCanvas canvas, Rect r); |
| void setScreenNail(ScreenNail s); |
| boolean isCamera(); // whether the picture is a camera preview |
| void updateSize(boolean force); // called when mCompensation changes |
| }; |
| |
| class FullPicture implements Picture { |
| private int mRotation; |
| private boolean mIsCamera; |
| private boolean mIsPanorama; |
| private boolean mIsVideo; |
| private int mLoadingState = Model.LOADING_INIT; |
| private boolean mWasCameraCenter; |
| |
| public void FullPicture(TileImageView tileView) { |
| mTileView = tileView; |
| } |
| |
| @Override |
| public void reload() { |
| // mImageWidth and mImageHeight will get updated |
| mTileView.notifyModelInvalidated(); |
| |
| mIsCamera = mModel.isCamera(0); |
| mIsPanorama = mModel.isPanorama(0); |
| mIsVideo = mModel.isVideo(0); |
| mLoadingState = mModel.getLoadingState(0); |
| setScreenNail(mModel.getScreenNail(0)); |
| updateSize(false); |
| } |
| |
| @Override |
| public void updateSize(boolean force) { |
| if (mIsPanorama) { |
| mRotation = getPanoramaRotation(); |
| } else if (mIsCamera) { |
| mRotation = getCameraRotation(); |
| } else { |
| mRotation = mModel.getImageRotation(0); |
| } |
| |
| int w = mTileView.mImageWidth; |
| int h = mTileView.mImageHeight; |
| mPositionController.setImageSize(0, |
| getRotated(mRotation, w, h), |
| getRotated(mRotation, h, w), |
| force); |
| } |
| |
| @Override |
| public void draw(GLCanvas canvas, Rect r) { |
| drawTileView(canvas, r); |
| |
| boolean isCenter = mPositionController.isCenter(); |
| if (mIsCamera) { |
| boolean full = !mFilmMode && isCenter |
| && mPositionController.isAtMinimalScale(); |
| if (full != mFullScreen) { |
| mFullScreen = full; |
| mListener.onFullScreenChanged(full); |
| } |
| } |
| |
| // We want to have the following transitions: |
| // (1) Move camera preview out of its place: switch to film mode |
| // (2) Move camera preview into its place: switch to page mode |
| // The extra mWasCenter check makes sure (1) does not apply if in |
| // page mode, we move _to_ the camera preview from another picture. |
| |
| // Holdings except touch-down prevent the transitions. |
| if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return; |
| |
| boolean isCameraCenter = mIsCamera && isCenter; |
| |
| if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) { |
| // Temporary disabled to de-emphasize filmstrip. |
| // setFilmMode(true); |
| } else if (!mWasCameraCenter && isCameraCenter && mFilmMode) { |
| setFilmMode(false); |
| } |
| |
| if (isCenter && !mFilmMode) { |
| if (mIsCamera) { |
| // move into camera, lock |
| mListener.lockOrientation(); // Transition A |
| } else { |
| // move out of camera, unlock |
| mListener.unlockOrientation(); // Transition B |
| } |
| } |
| |
| mWasCameraCenter = isCameraCenter; |
| } |
| |
| @Override |
| public void setScreenNail(ScreenNail s) { |
| mTileView.setScreenNail(s); |
| } |
| |
| @Override |
| public boolean isCamera() { |
| return mIsCamera; |
| } |
| |
| private void drawTileView(GLCanvas canvas, Rect r) { |
| float imageScale = mPositionController.getImageScale(); |
| int viewW = getWidth(); |
| int viewH = getHeight(); |
| float cx = r.exactCenterX(); |
| float cy = r.exactCenterY(); |
| float scale = 1f; // the scaling factor due to card effect |
| |
| canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); |
| float filmRatio = mPositionController.getFilmRatio(); |
| boolean wantsCardEffect = CARD_EFFECT && !mIsCamera |
| && filmRatio != 1f && !mPictures.get(-1).isCamera() |
| && !mPositionController.inOpeningAnimation(); |
| if (wantsCardEffect) { |
| // Calculate the move-out progress value. |
| int left = r.left; |
| int right = r.right; |
| float progress = calculateMoveOutProgress(left, right, viewW); |
| progress = Utils.clamp(progress, -1f, 1f); |
| |
| // We only want to apply the fading animation if the scrolling |
| // movement is to the right. |
| if (progress < 0) { |
| scale = getScrollScale(progress); |
| float alpha = getScrollAlpha(progress); |
| scale = interpolate(filmRatio, scale, 1f); |
| alpha = interpolate(filmRatio, alpha, 1f); |
| |
| imageScale *= scale; |
| canvas.multiplyAlpha(alpha); |
| |
| float cxPage; // the cx value in page mode |
| if (right - left <= viewW) { |
| // If the picture is narrower than the view, keep it at |
| // the center of the view. |
| cxPage = viewW / 2f; |
| } else { |
| // If the picture is wider than the view (it's |
| // zoomed-in), keep the left edge of the object align |
| // the the left edge of the view. |
| cxPage = (right - left) * scale / 2f; |
| } |
| cx = interpolate(filmRatio, cxPage, cx); |
| } |
| } |
| |
| // Draw the tile view. |
| setTileViewPosition(cx, cy, viewW, viewH, imageScale); |
| PhotoView.super.render(canvas); |
| |
| // Draw the play video icon and the message. |
| canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); |
| int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f); |
| if (mIsVideo) drawVideoPlayIcon(canvas, s); |
| if (mLoadingState == Model.LOADING_FAIL) { |
| drawLoadingFailMessage(canvas); |
| } |
| |
| // Draw a debug indicator showing which picture has focus (index == |
| // 0). |
| //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF); |
| |
| canvas.restore(); |
| } |
| |
| // Set the position of the tile view |
| private void setTileViewPosition(float cx, float cy, |
| int viewW, int viewH, float scale) { |
| // Find out the bitmap coordinates of the center of the view |
| int imageW = mPositionController.getImageWidth(); |
| int imageH = mPositionController.getImageHeight(); |
| int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f); |
| int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f); |
| |
| int inverseX = imageW - centerX; |
| int inverseY = imageH - centerY; |
| int x, y; |
| switch (mRotation) { |
| case 0: x = centerX; y = centerY; break; |
| case 90: x = centerY; y = inverseX; break; |
| case 180: x = inverseX; y = inverseY; break; |
| case 270: x = inverseY; y = centerX; break; |
| default: |
| throw new RuntimeException(String.valueOf(mRotation)); |
| } |
| mTileView.setPosition(x, y, scale, mRotation); |
| } |
| } |
| |
| private class ScreenNailPicture implements Picture { |
| private int mIndex; |
| private int mRotation; |
| private ScreenNail mScreenNail; |
| private Size mSize = new Size(); |
| private boolean mIsCamera; |
| private boolean mIsPanorama; |
| private boolean mIsVideo; |
| private int mLoadingState = Model.LOADING_INIT; |
| |
| public ScreenNailPicture(int index) { |
| mIndex = index; |
| } |
| |
| @Override |
| public void reload() { |
| mIsCamera = mModel.isCamera(mIndex); |
| mIsPanorama = mModel.isPanorama(mIndex); |
| mIsVideo = mModel.isVideo(mIndex); |
| mLoadingState = mModel.getLoadingState(mIndex); |
| setScreenNail(mModel.getScreenNail(mIndex)); |
| } |
| |
| @Override |
| public void draw(GLCanvas canvas, Rect r) { |
| if (mScreenNail == null) { |
| // Draw a placeholder rectange if there should be a picture in |
| // this position (but somehow there isn't). |
| if (mIndex >= mPrevBound && mIndex <= mNextBound) { |
| drawPlaceHolder(canvas, r); |
| } |
| return; |
| } |
| if (r.left >= getWidth() || r.right <= 0 || |
| r.top >= getHeight() || r.bottom <= 0) { |
| mScreenNail.noDraw(); |
| return; |
| } |
| |
| if (mIsCamera && mFullScreen != false) { |
| mFullScreen = false; |
| mListener.onFullScreenChanged(false); |
| } |
| |
| float filmRatio = mPositionController.getFilmRatio(); |
| boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 |
| && filmRatio != 1f && !mPictures.get(0).isCamera(); |
| int w = getWidth(); |
| int cx = wantsCardEffect |
| ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f) |
| : r.centerX(); |
| int cy = r.centerY(); |
| canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); |
| canvas.translate(cx, cy); |
| if (wantsCardEffect) { |
| float progress = (float) (w / 2 - r.centerX()) / w; |
| progress = Utils.clamp(progress, -1, 1); |
| float alpha = getScrollAlpha(progress); |
| float scale = getScrollScale(progress); |
| alpha = interpolate(filmRatio, alpha, 1f); |
| scale = interpolate(filmRatio, scale, 1f); |
| canvas.multiplyAlpha(alpha); |
| canvas.scale(scale, scale, 1); |
| } |
| if (mRotation != 0) { |
| canvas.rotate(mRotation, 0, 0, 1); |
| } |
| int drawW = getRotated(mRotation, r.width(), r.height()); |
| int drawH = getRotated(mRotation, r.height(), r.width()); |
| mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH); |
| if (isScreenNailAnimating()) { |
| invalidate(); |
| } |
| int s = Math.min(drawW, drawH); |
| if (mIsVideo) drawVideoPlayIcon(canvas, s); |
| if (mLoadingState == Model.LOADING_FAIL) { |
| drawLoadingFailMessage(canvas); |
| } |
| canvas.restore(); |
| } |
| |
| private boolean isScreenNailAnimating() { |
| return (mScreenNail instanceof BitmapScreenNail) |
| && ((BitmapScreenNail) mScreenNail).isAnimating(); |
| } |
| |
| @Override |
| public void setScreenNail(ScreenNail s) { |
| if (mScreenNail == s) return; |
| mScreenNail = s; |
| updateSize(false); |
| } |
| |
| @Override |
| public void updateSize(boolean force) { |
| if (mIsPanorama) { |
| mRotation = getPanoramaRotation(); |
| } else if (mIsCamera) { |
| mRotation = getCameraRotation(); |
| } else { |
| mRotation = mModel.getImageRotation(mIndex); |
| } |
| |
| int w = 0, h = 0; |
| if (mScreenNail != null) { |
| w = mScreenNail.getWidth(); |
| h = mScreenNail.getHeight(); |
| } else if (mModel != null) { |
| // If we don't have ScreenNail available, we can still try to |
| // get the size information of it. |
| mModel.getImageSize(mIndex, mSize); |
| w = mSize.width; |
| h = mSize.height; |
| } |
| |
| if (w != 0 && h != 0) { |
| mPositionController.setImageSize(mIndex, |
| getRotated(mRotation, w, h), |
| getRotated(mRotation, h, w), |
| force); |
| } |
| } |
| |
| @Override |
| public boolean isCamera() { |
| return mIsCamera; |
| } |
| } |
| |
| // Draw a gray placeholder in the specified rectangle. |
| private void drawPlaceHolder(GLCanvas canvas, Rect r) { |
| canvas.fillRect(r.left, r.top, r.width(), r.height(), PLACEHOLDER_COLOR); |
| } |
| |
| // Draw the video play icon (in the place where the spinner was) |
| private void drawVideoPlayIcon(GLCanvas canvas, int side) { |
| int s = side / ICON_RATIO; |
| // Draw the video play icon at the center |
| mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s); |
| } |
| |
| // Draw the "no thumbnail" message |
| private void drawLoadingFailMessage(GLCanvas canvas) { |
| StringTexture m = mNoThumbnailText; |
| m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2); |
| } |
| |
| private static int getRotated(int degree, int original, int theother) { |
| return (degree % 180 == 0) ? original : theother; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Gestures Handling |
| //////////////////////////////////////////////////////////////////////////// |
| |
| @Override |
| protected boolean onTouch(MotionEvent event) { |
| mGestureRecognizer.onTouchEvent(event); |
| return true; |
| } |
| |
| private class MyGestureListener implements GestureRecognizer.Listener { |
| private boolean mIgnoreUpEvent = false; |
| // If we can change mode for this scale gesture. |
| private boolean mCanChangeMode; |
| // If we have changed the film mode in this scaling gesture. |
| private boolean mModeChanged; |
| // If this scaling gesture should be ignored. |
| private boolean mIgnoreScalingGesture; |
| // whether the down action happened while the view is scrolling. |
| private boolean mDownInScrolling; |
| // If we should ignore all gestures other than onSingleTapUp. |
| private boolean mIgnoreSwipingGesture; |
| |
| @Override |
| public boolean onSingleTapUp(float x, float y) { |
| // We do this in addition to onUp() because we want the snapback of |
| // setFilmMode to happen. |
| mHolding &= ~HOLD_TOUCH_DOWN; |
| |
| if (mFilmMode && !mDownInScrolling) { |
| switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f)); |
| setFilmMode(false); |
| mIgnoreUpEvent = true; |
| return true; |
| } |
| |
| if (mListener != null) { |
| // Do the inverse transform of the touch coordinates. |
| Matrix m = getGLRoot().getCompensationMatrix(); |
| Matrix inv = new Matrix(); |
| m.invert(inv); |
| float[] pts = new float[] {x, y}; |
| inv.mapPoints(pts); |
| mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f)); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onDoubleTap(float x, float y) { |
| if (mIgnoreSwipingGesture) return true; |
| if (mPictures.get(0).isCamera()) return false; |
| PositionController controller = mPositionController; |
| float scale = controller.getImageScale(); |
| // onDoubleTap happened on the second ACTION_DOWN. |
| // We need to ignore the next UP event. |
| mIgnoreUpEvent = true; |
| if (scale <= 1.0f || controller.isAtMinimalScale()) { |
| controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f)); |
| } else { |
| controller.resetToFullView(); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onScroll(float dx, float dy) { |
| if (mIgnoreSwipingGesture) return true; |
| mPositionController.startScroll(-dx, -dy); |
| return true; |
| } |
| |
| @Override |
| public boolean onFling(float velocityX, float velocityY) { |
| if (mIgnoreSwipingGesture) return true; |
| if (swipeImages(velocityX, velocityY)) { |
| mIgnoreUpEvent = true; |
| } else if (mPositionController.fling(velocityX, velocityY)) { |
| mIgnoreUpEvent = true; |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onScaleBegin(float focusX, float focusY) { |
| if (mIgnoreSwipingGesture) return true; |
| // We ignore the scaling gesture if it is a camera preview. |
| mIgnoreScalingGesture = mPictures.get(0).isCamera(); |
| if (mIgnoreScalingGesture) { |
| return true; |
| } |
| mPositionController.beginScale(focusX, focusY); |
| // We can change mode if we are in film mode, or we are in page |
| // mode and at minimal scale. |
| mCanChangeMode = mFilmMode |
| || mPositionController.isAtMinimalScale(); |
| mModeChanged = false; |
| return true; |
| } |
| |
| @Override |
| public boolean onScale(float focusX, float focusY, float scale) { |
| if (mIgnoreSwipingGesture) return true; |
| if (mIgnoreScalingGesture) return true; |
| if (mModeChanged) return true; |
| if (Float.isNaN(scale) || Float.isInfinite(scale)) return false; |
| |
| // We wait for the scale change accumulated to a large enough change |
| // before reacting to it. Otherwise we may mistakenly treat a |
| // zoom-in gesture as zoom-out or vice versa. |
| if (scale > 0.99f && scale < 1.01f) return false; |
| |
| int outOfRange = mPositionController.scaleBy(scale, focusX, focusY); |
| |
| // If mode changes, we treat this scaling gesture has ended. |
| if (mCanChangeMode) { |
| if ((outOfRange < 0 && !mFilmMode) || |
| (outOfRange > 0 && mFilmMode)) { |
| stopExtraScalingIfNeeded(); |
| |
| // Removing the touch down flag allows snapback to happen |
| // for film mode change. |
| mHolding &= ~HOLD_TOUCH_DOWN; |
| setFilmMode(!mFilmMode); |
| |
| // We need to call onScaleEnd() before setting mModeChanged |
| // to true. |
| onScaleEnd(); |
| mModeChanged = true; |
| return true; |
| } |
| } |
| |
| if (outOfRange != 0) { |
| startExtraScalingIfNeeded(); |
| } else { |
| stopExtraScalingIfNeeded(); |
| } |
| return true; |
| } |
| |
| @Override |
| public void onScaleEnd() { |
| if (mIgnoreSwipingGesture) return; |
| if (mIgnoreScalingGesture) return; |
| if (mModeChanged) return; |
| mPositionController.endScale(); |
| } |
| |
| private void startExtraScalingIfNeeded() { |
| if (!mCancelExtraScalingPending) { |
| mHandler.sendEmptyMessageDelayed( |
| MSG_CANCEL_EXTRA_SCALING, 700); |
| mPositionController.setExtraScalingRange(true); |
| mCancelExtraScalingPending = true; |
| } |
| } |
| |
| private void stopExtraScalingIfNeeded() { |
| if (mCancelExtraScalingPending) { |
| mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING); |
| mPositionController.setExtraScalingRange(false); |
| mCancelExtraScalingPending = false; |
| } |
| } |
| |
| @Override |
| public void onDown() { |
| if (mIgnoreSwipingGesture) return; |
| |
| mHolding |= HOLD_TOUCH_DOWN; |
| |
| if (mFilmMode && mPositionController.isScrolling()) { |
| mDownInScrolling = true; |
| mPositionController.stopScrolling(); |
| } else { |
| mDownInScrolling = false; |
| } |
| } |
| |
| @Override |
| public void onUp() { |
| if (mIgnoreSwipingGesture) return; |
| |
| mHolding &= ~HOLD_TOUCH_DOWN; |
| mEdgeView.onRelease(); |
| |
| if (mIgnoreUpEvent) { |
| mIgnoreUpEvent = false; |
| return; |
| } |
| |
| snapback(); |
| } |
| |
| public void setSwipingEnabled(boolean enabled) { |
| mIgnoreSwipingGesture = !enabled; |
| } |
| } |
| |
| public void setSwipingEnabled(boolean enabled) { |
| mGestureListener.setSwipingEnabled(enabled); |
| } |
| |
| private void setFilmMode(boolean enabled) { |
| if (mFilmMode == enabled) return; |
| mFilmMode = enabled; |
| mPositionController.setFilmMode(mFilmMode); |
| mModel.setNeedFullImage(!enabled); |
| mListener.onActionBarAllowed(!enabled); |
| |
| // If we leave filmstrip mode, we should lock/unlock |
| if (!enabled) { |
| if (mPictures.get(0).isCamera()) { |
| mListener.lockOrientation(); // Transition C |
| } else { |
| mListener.unlockOrientation(); // Transition D |
| } |
| } |
| } |
| |
| public boolean getFilmMode() { |
| return mFilmMode; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Framework events |
| //////////////////////////////////////////////////////////////////////////// |
| |
| public void pause() { |
| mPositionController.skipAnimation(); |
| mTileView.freeTextures(); |
| for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { |
| mPictures.get(i).setScreenNail(null); |
| } |
| } |
| |
| public void resume() { |
| mTileView.prepareTextures(); |
| } |
| |
| // move to the camera preview and show controls after resume |
| public void resetToFirstPicture() { |
| mModel.moveTo(0); |
| setFilmMode(false); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Rendering |
| //////////////////////////////////////////////////////////////////////////// |
| |
| @Override |
| protected void render(GLCanvas canvas) { |
| // In page mode, we draw only one previous/next photo. But if we are |
| // doing capture animation, we want to draw all photos. |
| boolean inPageMode = (mPositionController.getFilmRatio() == 0f); |
| boolean inCaptureAnimation = ((mHolding & HOLD_CAPTURE_ANIMATION) != 0); |
| boolean drawOneNeighborOnly = inPageMode && !inCaptureAnimation; |
| int neighbors = drawOneNeighborOnly ? 1 : SCREEN_NAIL_MAX; |
| |
| // Draw photos from back to front |
| for (int i = neighbors; i >= -neighbors; i--) { |
| Rect r = mPositionController.getPosition(i); |
| mPictures.get(i).draw(canvas, r); |
| } |
| |
| mPositionController.advanceAnimation(); |
| checkFocusSwitching(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Film mode focus switching |
| //////////////////////////////////////////////////////////////////////////// |
| |
| // Runs in GL thread. |
| private void checkFocusSwitching() { |
| if (!mFilmMode) return; |
| if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return; |
| if (switchPosition() != 0) { |
| mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS); |
| } |
| } |
| |
| // Runs in main thread. |
| private void switchFocus() { |
| if (mHolding != 0) return; |
| switch (switchPosition()) { |
| case -1: |
| switchToPrevImage(); |
| break; |
| case 1: |
| switchToNextImage(); |
| break; |
| } |
| } |
| |
| // Returns -1 if we should switch focus to the previous picture, +1 if we |
| // should switch to the next, 0 otherwise. |
| private int switchPosition() { |
| Rect curr = mPositionController.getPosition(0); |
| int center = getWidth() / 2; |
| |
| if (curr.left > center && mPrevBound < 0) { |
| Rect prev = mPositionController.getPosition(-1); |
| int currDist = curr.left - center; |
| int prevDist = center - prev.right; |
| if (prevDist < currDist) { |
| return -1; |
| } |
| } else if (curr.right < center && mNextBound > 0) { |
| Rect next = mPositionController.getPosition(1); |
| int currDist = center - curr.right; |
| int nextDist = next.left - center; |
| if (nextDist < currDist) { |
| return 1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| // Switch to the previous or next picture if the hit position is inside |
| // one of their boxes. This runs in main thread. |
| private void switchToHitPicture(int x, int y) { |
| if (mPrevBound < 0) { |
| Rect r = mPositionController.getPosition(-1); |
| if (r.right >= x) { |
| slideToPrevPicture(); |
| return; |
| } |
| } |
| |
| if (mNextBound > 0) { |
| Rect r = mPositionController.getPosition(1); |
| if (r.left <= x) { |
| slideToNextPicture(); |
| return; |
| } |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Page mode focus switching |
| // |
| // We slide image to the next one or the previous one in two cases: 1: If |
| // the user did a fling gesture with enough velocity. 2 If the user has |
| // moved the picture a lot. |
| //////////////////////////////////////////////////////////////////////////// |
| |
| private boolean swipeImages(float velocityX, float velocityY) { |
| if (mFilmMode) return false; |
| |
| // Avoid swiping images if we're possibly flinging to view the |
| // zoomed in picture vertically. |
| PositionController controller = mPositionController; |
| boolean isMinimal = controller.isAtMinimalScale(); |
| int edges = controller.getImageAtEdges(); |
| if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX)) |
| if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0 |
| || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0) |
| return false; |
| |
| // If we are at the edge of the current photo and the sweeping velocity |
| // exceeds the threshold, slide to the next / previous image. |
| if (velocityX < -SWIPE_THRESHOLD && (isMinimal |
| || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) { |
| return slideToNextPicture(); |
| } else if (velocityX > SWIPE_THRESHOLD && (isMinimal |
| || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) { |
| return slideToPrevPicture(); |
| } |
| |
| return false; |
| } |
| |
| private void snapback() { |
| if (mHolding != 0) return; |
| if (!snapToNeighborImage()) { |
| mPositionController.snapback(); |
| } |
| } |
| |
| private boolean snapToNeighborImage() { |
| if (mFilmMode) return false; |
| |
| Rect r = mPositionController.getPosition(0); |
| int viewW = getWidth(); |
| int threshold = MOVE_THRESHOLD + gapToSide(r.width(), viewW); |
| |
| // If we have moved the picture a lot, switching. |
| if (viewW - r.right > threshold) { |
| return slideToNextPicture(); |
| } else if (r.left > threshold) { |
| return slideToPrevPicture(); |
| } |
| |
| return false; |
| } |
| |
| private boolean slideToNextPicture() { |
| if (mNextBound <= 0) return false; |
| switchToNextImage(); |
| mPositionController.startHorizontalSlide(); |
| return true; |
| } |
| |
| private boolean slideToPrevPicture() { |
| if (mPrevBound >= 0) return false; |
| switchToPrevImage(); |
| mPositionController.startHorizontalSlide(); |
| return true; |
| } |
| |
| private static int gapToSide(int imageWidth, int viewWidth) { |
| return Math.max(0, (viewWidth - imageWidth) / 2); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Focus switching |
| //////////////////////////////////////////////////////////////////////////// |
| |
| private void switchToNextImage() { |
| mModel.moveTo(mModel.getCurrentIndex() + 1); |
| } |
| |
| private void switchToPrevImage() { |
| mModel.moveTo(mModel.getCurrentIndex() - 1); |
| } |
| |
| private void switchToFirstImage() { |
| mModel.moveTo(0); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Opening Animation |
| //////////////////////////////////////////////////////////////////////////// |
| |
| public void setOpenAnimationRect(Rect rect) { |
| mPositionController.setOpenAnimationRect(rect); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Capture Animation |
| //////////////////////////////////////////////////////////////////////////// |
| |
| public boolean switchWithCaptureAnimation(int offset) { |
| GLRoot root = getGLRoot(); |
| root.lockRenderThread(); |
| try { |
| return switchWithCaptureAnimationLocked(offset); |
| } finally { |
| root.unlockRenderThread(); |
| } |
| } |
| |
| private boolean switchWithCaptureAnimationLocked(int offset) { |
| if (mHolding != 0) return true; |
| if (offset == 1) { |
| if (mNextBound <= 0) return false; |
| // Temporary disable action bar until the capture animation is done. |
| if (!mFilmMode) mListener.onActionBarAllowed(false); |
| switchToNextImage(); |
| mPositionController.startCaptureAnimationSlide(-1); |
| } else if (offset == -1) { |
| if (mPrevBound >= 0) return false; |
| if (mFilmMode) setFilmMode(false); |
| |
| // If we are too far away from the first image (so that we don't |
| // have all the ScreenNails in-between), we go directly without |
| // animation. |
| if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) { |
| switchToFirstImage(); |
| mPositionController.skipAnimation(); |
| return true; |
| } |
| |
| switchToFirstImage(); |
| mPositionController.startCaptureAnimationSlide(1); |
| } else { |
| return false; |
| } |
| mHolding |= HOLD_CAPTURE_ANIMATION; |
| Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0); |
| mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME); |
| return true; |
| } |
| |
| private void captureAnimationDone(int offset) { |
| mHolding &= ~HOLD_CAPTURE_ANIMATION; |
| if (offset == 1) { |
| // move out of camera, unlock |
| if (!mFilmMode) { |
| // Now the capture animation is done, enable the action bar. |
| mListener.onActionBarAllowed(true); |
| } |
| } |
| snapback(); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Card deck effect calculation |
| //////////////////////////////////////////////////////////////////////////// |
| |
| // Returns the scrolling progress value for an object moving out of a |
| // view. The progress value measures how much the object has moving out of |
| // the view. The object currently displays in [left, right), and the view is |
| // at [0, viewWidth]. |
| // |
| // The returned value is negative when the object is moving right, and |
| // positive when the object is moving left. The value goes to -1 or 1 when |
| // the object just moves out of the view completely. The value is 0 if the |
| // object currently fills the view. |
| private static float calculateMoveOutProgress(int left, int right, |
| int viewWidth) { |
| // w = object width |
| // viewWidth = view width |
| int w = right - left; |
| |
| // If the object width is smaller than the view width, |
| // |....view....| |
| // |<-->| progress = -1 when left = viewWidth |
| // |<-->| progress = 0 when left = viewWidth / 2 - w / 2 |
| // |<-->| progress = 1 when left = -w |
| if (w < viewWidth) { |
| int zx = viewWidth / 2 - w / 2; |
| if (left > zx) { |
| return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1] |
| } else { |
| return (left - zx) / (float) (-w - zx); // progress = [0, 1] |
| } |
| } |
| |
| // If the object width is larger than the view width, |
| // |..view..| |
| // |<--------->| progress = -1 when left = viewWidth |
| // |<--------->| progress = 0 between left = 0 |
| // |<--------->| and right = viewWidth |
| // |<--------->| progress = 1 when right = 0 |
| if (left > 0) { |
| return -left / (float) viewWidth; |
| } |
| |
| if (right < viewWidth) { |
| return (viewWidth - right) / (float) viewWidth; |
| } |
| |
| return 0; |
| } |
| |
| // Maps a scrolling progress value to the alpha factor in the fading |
| // animation. |
| private float getScrollAlpha(float scrollProgress) { |
| return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( |
| 1 - Math.abs(scrollProgress)) : 1.0f; |
| } |
| |
| // Maps a scrolling progress value to the scaling factor in the fading |
| // animation. |
| private float getScrollScale(float scrollProgress) { |
| float interpolatedProgress = mScaleInterpolator.getInterpolation( |
| Math.abs(scrollProgress)); |
| float scale = (1 - interpolatedProgress) + |
| interpolatedProgress * TRANSITION_SCALE_FACTOR; |
| return scale; |
| } |
| |
| |
| // This interpolator emulates the rate at which the perceived scale of an |
| // object changes as its distance from a camera increases. When this |
| // interpolator is applied to a scale animation on a view, it evokes the |
| // sense that the object is shrinking due to moving away from the camera. |
| private static class ZInterpolator { |
| private float focalLength; |
| |
| public ZInterpolator(float foc) { |
| focalLength = foc; |
| } |
| |
| public float getInterpolation(float input) { |
| return (1.0f - focalLength / (focalLength + input)) / |
| (1.0f - focalLength / (focalLength + 1.0f)); |
| } |
| } |
| |
| // Returns an interpolated value for the page/film transition. |
| // When ratio = 0, the result is from. |
| // When ratio = 1, the result is to. |
| private static float interpolate(float ratio, float from, float to) { |
| return from + (to - from) * ratio * ratio; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////// |
| // Simple public utilities |
| //////////////////////////////////////////////////////////////////////////// |
| |
| public void setListener(Listener listener) { |
| mListener = listener; |
| } |
| |
| public Rect getPhotoRect(int index) { |
| return mPositionController.getPosition(index); |
| } |
| |
| |
| public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) { |
| Rect location = new Rect(); |
| Utils.assertTrue(root.getBoundsOf(this, location)); |
| |
| Rect fullRect = bounds(); |
| PhotoFallbackEffect effect = new PhotoFallbackEffect(); |
| for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { |
| MediaItem item = mModel.getMediaItem(i); |
| if (item == null) continue; |
| ScreenNail sc = mModel.getScreenNail(i); |
| if (!(sc instanceof BitmapScreenNail) |
| || ((BitmapScreenNail) sc).isShowingPlaceholder()) continue; |
| |
| // Now, sc is BitmapScreenNail and is not showing placeholder |
| Rect rect = new Rect(getPhotoRect(i)); |
| if (!Rect.intersects(fullRect, rect)) continue; |
| rect.offset(location.left, location.top); |
| |
| RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true); |
| canvas.beginRenderTarget(texture); |
| sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight()); |
| canvas.endRenderTarget(); |
| effect.addEntry(item.getPath(), rect, texture); |
| } |
| return effect; |
| } |
| } |