summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/ui')
-rw-r--r--src/com/android/gallery3d/ui/AbstractSlotRenderer.java119
-rw-r--r--src/com/android/gallery3d/ui/ActionModeHandler.java501
-rw-r--r--src/com/android/gallery3d/ui/AlbumLabelMaker.java206
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java549
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java242
-rw-r--r--src/com/android/gallery3d/ui/AlbumSlidingWindow.java365
-rw-r--r--src/com/android/gallery3d/ui/AlbumSlotRenderer.java201
-rw-r--r--src/com/android/gallery3d/ui/AnimationTime.java45
-rw-r--r--src/com/android/gallery3d/ui/BitmapLoader.java108
-rw-r--r--src/com/android/gallery3d/ui/BitmapScreenNail.java61
-rw-r--r--src/com/android/gallery3d/ui/BitmapTileProvider.java103
-rw-r--r--src/com/android/gallery3d/ui/CacheStorageUsageInfo.java90
-rw-r--r--src/com/android/gallery3d/ui/CaptureAnimation.java56
-rw-r--r--src/com/android/gallery3d/ui/DetailsAddressResolver.java118
-rw-r--r--src/com/android/gallery3d/ui/DetailsHelper.java148
-rw-r--r--src/com/android/gallery3d/ui/DialogDetailsView.java288
-rw-r--r--src/com/android/gallery3d/ui/DownUpDetector.java61
-rw-r--r--src/com/android/gallery3d/ui/EdgeEffect.java443
-rw-r--r--src/com/android/gallery3d/ui/EdgeView.java132
-rw-r--r--src/com/android/gallery3d/ui/FlingScroller.java141
-rw-r--r--src/com/android/gallery3d/ui/GLRoot.java53
-rw-r--r--src/com/android/gallery3d/ui/GLRootView.java630
-rw-r--r--src/com/android/gallery3d/ui/GLView.java465
-rw-r--r--src/com/android/gallery3d/ui/GestureRecognizer.java132
-rw-r--r--src/com/android/gallery3d/ui/Log.java54
-rw-r--r--src/com/android/gallery3d/ui/ManageCacheDrawer.java116
-rw-r--r--src/com/android/gallery3d/ui/MeasureHelper.java65
-rw-r--r--src/com/android/gallery3d/ui/MenuExecutor.java448
-rw-r--r--src/com/android/gallery3d/ui/OrientationSource.java22
-rw-r--r--src/com/android/gallery3d/ui/Paper.java183
-rw-r--r--src/com/android/gallery3d/ui/PhotoFallbackEffect.java179
-rw-r--r--src/com/android/gallery3d/ui/PhotoView.java1858
-rw-r--r--src/com/android/gallery3d/ui/PopupList.java206
-rw-r--r--src/com/android/gallery3d/ui/PositionController.java1821
-rw-r--r--src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java85
-rw-r--r--src/com/android/gallery3d/ui/ProgressSpinner.java80
-rw-r--r--src/com/android/gallery3d/ui/RelativePosition.java42
-rw-r--r--src/com/android/gallery3d/ui/ScreenNail.java35
-rw-r--r--src/com/android/gallery3d/ui/ScrollBarView.java97
-rw-r--r--src/com/android/gallery3d/ui/ScrollerHelper.java97
-rw-r--r--src/com/android/gallery3d/ui/SelectionManager.java251
-rw-r--r--src/com/android/gallery3d/ui/SelectionMenu.java61
-rw-r--r--src/com/android/gallery3d/ui/SlideshowView.java163
-rw-r--r--src/com/android/gallery3d/ui/SlotView.java788
-rw-r--r--src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java142
-rw-r--r--src/com/android/gallery3d/ui/SynchronizedHandler.java41
-rw-r--r--src/com/android/gallery3d/ui/TileImageView.java786
-rw-r--r--src/com/android/gallery3d/ui/TileImageViewAdapter.java200
-rw-r--r--src/com/android/gallery3d/ui/TiledScreenNail.java218
-rw-r--r--src/com/android/gallery3d/ui/UndoBarView.java211
-rw-r--r--src/com/android/gallery3d/ui/UserInteractionListener.java26
-rw-r--r--src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java66
52 files changed, 13598 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/ui/AbstractSlotRenderer.java b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
new file mode 100644
index 000000000..729439dc3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 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.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.FadeOutTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.Texture;
+
+public abstract class AbstractSlotRenderer implements SlotView.SlotRenderer {
+
+ private final ResourceTexture mVideoOverlay;
+ private final ResourceTexture mVideoPlayIcon;
+ private final ResourceTexture mPanoramaIcon;
+ private final NinePatchTexture mFramePressed;
+ private final NinePatchTexture mFrameSelected;
+ private FadeOutTexture mFramePressedUp;
+
+ protected AbstractSlotRenderer(Context context) {
+ mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb);
+ mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play);
+ mPanoramaIcon = new ResourceTexture(context, R.drawable.ic_360pano_holo_light);
+ mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
+ mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+ }
+
+ protected void drawContent(GLCanvas canvas,
+ Texture content, int width, int height, int rotation) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+ // The content is always rendered in to the largest square that fits
+ // inside the slot, aligned to the top of the slot.
+ width = height = Math.min(width, height);
+ if (rotation != 0) {
+ canvas.translate(width / 2, height / 2);
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-width / 2, -height / 2);
+ }
+
+ // Fit the content into the box
+ float scale = Math.min(
+ (float) width / content.getWidth(),
+ (float) height / content.getHeight());
+ canvas.scale(scale, scale, 1);
+ content.draw(canvas, 0, 0);
+
+ canvas.restore();
+ }
+
+ protected void drawVideoOverlay(GLCanvas canvas, int width, int height) {
+ // Scale the video overlay to the height of the thumbnail and put it
+ // on the left side.
+ ResourceTexture v = mVideoOverlay;
+ float scale = (float) height / v.getHeight();
+ int w = Math.round(scale * v.getWidth());
+ int h = Math.round(scale * v.getHeight());
+ v.draw(canvas, 0, 0, w, h);
+
+ int s = Math.min(width, height) / 6;
+ mVideoPlayIcon.draw(canvas, (width - s) / 2, (height - s) / 2, s, s);
+ }
+
+ protected void drawPanoramaIcon(GLCanvas canvas, int width, int height) {
+ int iconSize = Math.min(width, height) / 6;
+ mPanoramaIcon.draw(canvas, (width - iconSize) / 2, (height - iconSize) / 2,
+ iconSize, iconSize);
+ }
+
+ protected boolean isPressedUpFrameFinished() {
+ if (mFramePressedUp != null) {
+ if (mFramePressedUp.isAnimating()) {
+ return false;
+ } else {
+ mFramePressedUp = null;
+ }
+ }
+ return true;
+ }
+
+ protected void drawPressedUpFrame(GLCanvas canvas, int width, int height) {
+ if (mFramePressedUp == null) {
+ mFramePressedUp = new FadeOutTexture(mFramePressed);
+ }
+ drawFrame(canvas, mFramePressed.getPaddings(), mFramePressedUp, 0, 0, width, height);
+ }
+
+ protected void drawPressedFrame(GLCanvas canvas, int width, int height) {
+ drawFrame(canvas, mFramePressed.getPaddings(), mFramePressed, 0, 0, width, height);
+ }
+
+ protected void drawSelectedFrame(GLCanvas canvas, int width, int height) {
+ drawFrame(canvas, mFrameSelected.getPaddings(), mFrameSelected, 0, 0, width, height);
+ }
+
+ protected static void drawFrame(GLCanvas canvas, Rect padding, Texture frame,
+ int x, int y, int width, int height) {
+ frame.draw(canvas, x - padding.left, y - padding.top, width + padding.left + padding.right,
+ height + padding.top + padding.bottom);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
new file mode 100644
index 000000000..6b4f10312
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -0,0 +1,501 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.os.Handler;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ShareActionProvider;
+import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.MenuExecutor.ProgressListener;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+
+public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "ActionModeHandler";
+
+ private static final int MAX_SELECTED_ITEMS_FOR_SHARE_INTENT = 300;
+ private static final int MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT = 10;
+
+ private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE
+ | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE
+ | MediaObject.SUPPORT_CACHE;
+
+ public interface ActionModeListener {
+ public boolean onActionItemClicked(MenuItem item);
+ }
+
+ private final AbstractGalleryActivity mActivity;
+ private final MenuExecutor mMenuExecutor;
+ private final SelectionManager mSelectionManager;
+ private final NfcAdapter mNfcAdapter;
+ private Menu mMenu;
+ private MenuItem mSharePanoramaMenuItem;
+ private MenuItem mShareMenuItem;
+ private ShareActionProvider mSharePanoramaActionProvider;
+ private ShareActionProvider mShareActionProvider;
+ private SelectionMenu mSelectionMenu;
+ private ActionModeListener mListener;
+ private Future<?> mMenuTask;
+ private final Handler mMainHandler;
+ private ActionMode mActionMode;
+
+ private static class GetAllPanoramaSupports implements PanoramaSupportCallback {
+ private int mNumInfoRequired;
+ private JobContext mJobContext;
+ public boolean mAllPanoramas = true;
+ public boolean mAllPanorama360 = true;
+ public boolean mHasPanorama360 = false;
+ private Object mLock = new Object();
+
+ public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) {
+ mJobContext = jc;
+ mNumInfoRequired = mediaObjects.size();
+ for (MediaObject mediaObject : mediaObjects) {
+ mediaObject.getPanoramaSupport(this);
+ }
+ }
+
+ @Override
+ public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360) {
+ synchronized (mLock) {
+ mNumInfoRequired--;
+ mAllPanoramas = isPanorama && mAllPanoramas;
+ mAllPanorama360 = isPanorama360 && mAllPanorama360;
+ mHasPanorama360 = mHasPanorama360 || isPanorama360;
+ if (mNumInfoRequired == 0 || mJobContext.isCancelled()) {
+ mLock.notifyAll();
+ }
+ }
+ }
+
+ public void waitForPanoramaSupport() {
+ synchronized (mLock) {
+ while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ // May be a cancelled job context
+ }
+ }
+ }
+ }
+ }
+
+ public ActionModeHandler(
+ AbstractGalleryActivity activity, SelectionManager selectionManager) {
+ mActivity = Utils.checkNotNull(activity);
+ mSelectionManager = Utils.checkNotNull(selectionManager);
+ mMenuExecutor = new MenuExecutor(activity, selectionManager);
+ mMainHandler = new Handler(activity.getMainLooper());
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
+ }
+
+ public void startActionMode() {
+ Activity a = mActivity;
+ mActionMode = a.startActionMode(this);
+ View customView = LayoutInflater.from(a).inflate(
+ R.layout.action_mode, null);
+ mActionMode.setCustomView(customView);
+ mSelectionMenu = new SelectionMenu(a,
+ (Button) customView.findViewById(R.id.selection_menu), this);
+ updateSelectionMenu();
+ }
+
+ public void finishActionMode() {
+ mActionMode.finish();
+ }
+
+ public void setTitle(String title) {
+ mSelectionMenu.setTitle(title);
+ }
+
+ public void setActionModeListener(ActionModeListener listener) {
+ mListener = listener;
+ }
+
+ private WakeLockHoldingProgressListener mDeleteProgressListener;
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ boolean result;
+ // Give listener a chance to process this command before it's routed to
+ // ActionModeHandler, which handles command only based on the action id.
+ // Sometimes the listener may have more background information to handle
+ // an action command.
+ if (mListener != null) {
+ result = mListener.onActionItemClicked(item);
+ if (result) {
+ mSelectionManager.leaveSelectionMode();
+ return result;
+ }
+ }
+ ProgressListener listener = null;
+ String confirmMsg = null;
+ int action = item.getItemId();
+ if (action == R.id.action_delete) {
+ confirmMsg = mActivity.getResources().getQuantityString(
+ R.plurals.delete_selection, mSelectionManager.getSelectedCount());
+ if (mDeleteProgressListener == null) {
+ mDeleteProgressListener = new WakeLockHoldingProgressListener(mActivity,
+ "Gallery Delete Progress Listener");
+ }
+ listener = mDeleteProgressListener;
+ }
+ mMenuExecutor.onMenuClicked(item, confirmMsg, listener);
+ } finally {
+ root.unlockRenderThread();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPopupItemClick(int itemId) {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ if (itemId == R.id.action_select_all) {
+ updateSupportedOperation();
+ mMenuExecutor.onMenuClicked(itemId, null, false, true);
+ }
+ return true;
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private void updateSelectionMenu() {
+ // update title
+ int count = mSelectionManager.getSelectedCount();
+ String format = mActivity.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count);
+ setTitle(String.format(format, count));
+
+ // For clients who call SelectionManager.selectAll() directly, we need to ensure the
+ // menu status is consistent with selection manager.
+ mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode());
+ }
+
+ private final OnShareTargetSelectedListener mShareTargetSelectedListener =
+ new OnShareTargetSelectedListener() {
+ @Override
+ public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
+ mSelectionManager.leaveSelectionMode();
+ return false;
+ }
+ };
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.getMenuInflater().inflate(R.menu.operation, menu);
+
+ mMenu = menu;
+ mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama);
+ if (mSharePanoramaMenuItem != null) {
+ mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem
+ .getActionProvider();
+ mSharePanoramaActionProvider.setOnShareTargetSelectedListener(
+ mShareTargetSelectedListener);
+ mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml");
+ }
+ mShareMenuItem = menu.findItem(R.id.action_share);
+ if (mShareMenuItem != null) {
+ mShareActionProvider = (ShareActionProvider) mShareMenuItem
+ .getActionProvider();
+ mShareActionProvider.setOnShareTargetSelectedListener(
+ mShareTargetSelectedListener);
+ mShareActionProvider.setShareHistoryFileName("share_history.xml");
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mSelectionManager.leaveSelectionMode();
+ }
+
+ private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) {
+ ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false);
+ if (unexpandedPaths.isEmpty()) {
+ // This happens when starting selection mode from overflow menu
+ // (instead of long press a media object)
+ return null;
+ }
+ ArrayList<MediaObject> selected = new ArrayList<MediaObject>();
+ DataManager manager = mActivity.getDataManager();
+ for (Path path : unexpandedPaths) {
+ if (jc.isCancelled()) {
+ return null;
+ }
+ selected.add(manager.getMediaObject(path));
+ }
+
+ return selected;
+ }
+ // Menu options are determined by selection set itself.
+ // We cannot expand it because MenuExecuter executes it based on
+ // the selection set instead of the expanded result.
+ // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
+ private int computeMenuOptions(ArrayList<MediaObject> selected) {
+ int operation = MediaObject.SUPPORT_ALL;
+ int type = 0;
+ for (MediaObject mediaObject: selected) {
+ int support = mediaObject.getSupportedOperations();
+ type |= mediaObject.getMediaType();
+ operation &= support;
+ }
+
+ switch (selected.size()) {
+ case 1:
+ final String mimeType = MenuExecutor.getMimeType(type);
+ if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) {
+ operation &= ~MediaObject.SUPPORT_EDIT;
+ }
+ break;
+ default:
+ operation &= SUPPORT_MULTIPLE_MASK;
+ }
+
+ return operation;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void setNfcBeamPushUris(Uri[] uris) {
+ if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
+ mNfcAdapter.setBeamPushUrisCallback(null, mActivity);
+ mNfcAdapter.setBeamPushUris(uris, mActivity);
+ }
+ }
+
+ // Share intent needs to expand the selection set so we can get URI of
+ // each media item
+ private Intent computePanoramaSharingIntent(JobContext jc, int maxItems) {
+ ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems);
+ if (expandedPaths == null || expandedPaths.size() == 0) {
+ return new Intent();
+ }
+ final ArrayList<Uri> uris = new ArrayList<Uri>();
+ DataManager manager = mActivity.getDataManager();
+ final Intent intent = new Intent();
+ for (Path path : expandedPaths) {
+ if (jc.isCancelled()) return null;
+ uris.add(manager.getContentUri(path));
+ }
+
+ final int size = uris.size();
+ if (size > 0) {
+ if (size > 1) {
+ intent.setAction(Intent.ACTION_SEND_MULTIPLE);
+ intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ } else {
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
+ intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ }
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ return intent;
+ }
+
+ private Intent computeSharingIntent(JobContext jc, int maxItems) {
+ ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems);
+ if (expandedPaths == null || expandedPaths.size() == 0) {
+ setNfcBeamPushUris(null);
+ return new Intent();
+ }
+ final ArrayList<Uri> uris = new ArrayList<Uri>();
+ DataManager manager = mActivity.getDataManager();
+ int type = 0;
+ final Intent intent = new Intent();
+ for (Path path : expandedPaths) {
+ if (jc.isCancelled()) return null;
+ int support = manager.getSupportedOperations(path);
+ type |= manager.getMediaType(path);
+
+ if ((support & MediaObject.SUPPORT_SHARE) != 0) {
+ uris.add(manager.getContentUri(path));
+ }
+ }
+
+ final int size = uris.size();
+ if (size > 0) {
+ final String mimeType = MenuExecutor.getMimeType(type);
+ if (size > 1) {
+ intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ } else {
+ intent.setAction(Intent.ACTION_SEND).setType(mimeType);
+ intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ }
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ setNfcBeamPushUris(uris.toArray(new Uri[uris.size()]));
+ } else {
+ setNfcBeamPushUris(null);
+ }
+
+ return intent;
+ }
+
+ public void updateSupportedOperation(Path path, boolean selected) {
+ // TODO: We need to improve the performance
+ updateSupportedOperation();
+ }
+
+ public void updateSupportedOperation() {
+ // Interrupt previous unfinished task, mMenuTask is only accessed in main thread
+ if (mMenuTask != null) mMenuTask.cancel();
+
+ updateSelectionMenu();
+
+ // Disable share actions until share intent is in good shape
+ if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false);
+ if (mShareMenuItem != null) mShareMenuItem.setEnabled(false);
+
+ // Generate sharing intent and update supported operations in the background
+ // The task can take a long time and be canceled in the mean time.
+ mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
+ @Override
+ public Void run(final JobContext jc) {
+ // Pass1: Deal with unexpanded media object list for menu operation.
+ ArrayList<MediaObject> selected = getSelectedMediaObjects(jc);
+ if (selected == null) {
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mMenuTask = null;
+ if (jc.isCancelled()) return;
+ // Disable all the operations when no item is selected
+ MenuExecutor.updateMenuOperation(mMenu, 0);
+ }
+ });
+ return null;
+ }
+ final int operation = computeMenuOptions(selected);
+ if (jc.isCancelled()) {
+ return null;
+ }
+ int numSelected = selected.size();
+ final boolean canSharePanoramas =
+ numSelected < MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT;
+ final boolean canShare =
+ numSelected < MAX_SELECTED_ITEMS_FOR_SHARE_INTENT;
+
+ final GetAllPanoramaSupports supportCallback = canSharePanoramas ?
+ new GetAllPanoramaSupports(selected, jc)
+ : null;
+
+ // Pass2: Deal with expanded media object list for sharing operation.
+ final Intent share_panorama_intent = canSharePanoramas ?
+ computePanoramaSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT)
+ : new Intent();
+ final Intent share_intent = canShare ?
+ computeSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT)
+ : new Intent();
+
+ if (canSharePanoramas) {
+ supportCallback.waitForPanoramaSupport();
+ }
+ if (jc.isCancelled()) {
+ return null;
+ }
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mMenuTask = null;
+ if (jc.isCancelled()) return;
+ MenuExecutor.updateMenuOperation(mMenu, operation);
+ MenuExecutor.updateMenuForPanorama(mMenu,
+ canSharePanoramas && supportCallback.mAllPanorama360,
+ canSharePanoramas && supportCallback.mHasPanorama360);
+ if (mSharePanoramaMenuItem != null) {
+ mSharePanoramaMenuItem.setEnabled(true);
+ if (canSharePanoramas && supportCallback.mAllPanorama360) {
+ mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ mShareMenuItem.setTitle(
+ mActivity.getResources().getString(R.string.share_as_photo));
+ } else {
+ mSharePanoramaMenuItem.setVisible(false);
+ mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ mShareMenuItem.setTitle(
+ mActivity.getResources().getString(R.string.share));
+ }
+ mSharePanoramaActionProvider.setShareIntent(share_panorama_intent);
+ }
+ if (mShareMenuItem != null) {
+ mShareMenuItem.setEnabled(canShare);
+ mShareActionProvider.setShareIntent(share_intent);
+ }
+ }
+ });
+ return null;
+ }
+ });
+ }
+
+ public void pause() {
+ if (mMenuTask != null) {
+ mMenuTask.cancel();
+ mMenuTask = null;
+ }
+ mMenuExecutor.pause();
+ }
+
+ public void destroy() {
+ mMenuExecutor.destroy();
+ }
+
+ public void resume() {
+ if (mSelectionManager.inSelectionMode()) updateSupportedOperation();
+ mMenuExecutor.resume();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumLabelMaker.java b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
new file mode 100644
index 000000000..da1cac0bd
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class AlbumLabelMaker {
+ private static final int BORDER_SIZE = 0;
+
+ private final AlbumSetSlotRenderer.LabelSpec mSpec;
+ private final TextPaint mTitlePaint;
+ private final TextPaint mCountPaint;
+ private final Context mContext;
+
+ private int mLabelWidth;
+ private int mBitmapWidth;
+ private int mBitmapHeight;
+
+ private final LazyLoadedBitmap mLocalSetIcon;
+ private final LazyLoadedBitmap mPicasaIcon;
+ private final LazyLoadedBitmap mCameraIcon;
+
+ public AlbumLabelMaker(Context context, AlbumSetSlotRenderer.LabelSpec spec) {
+ mContext = context;
+ mSpec = spec;
+ mTitlePaint = getTextPaint(spec.titleFontSize, spec.titleColor, false);
+ mCountPaint = getTextPaint(spec.countFontSize, spec.countColor, false);
+
+ mLocalSetIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_folder);
+ mPicasaIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_picasa);
+ mCameraIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_camera);
+ }
+
+ public static int getBorderSize() {
+ return BORDER_SIZE;
+ }
+
+ private Bitmap getOverlayAlbumIcon(int sourceType) {
+ switch (sourceType) {
+ case DataSourceType.TYPE_CAMERA:
+ return mCameraIcon.get();
+ case DataSourceType.TYPE_LOCAL:
+ return mLocalSetIcon.get();
+ case DataSourceType.TYPE_PICASA:
+ return mPicasaIcon.get();
+ }
+ return null;
+ }
+
+ private static TextPaint getTextPaint(int textSize, int color, boolean isBold) {
+ TextPaint paint = new TextPaint();
+ paint.setTextSize(textSize);
+ paint.setAntiAlias(true);
+ paint.setColor(color);
+ //paint.setShadowLayer(2f, 0f, 0f, Color.LTGRAY);
+ if (isBold) {
+ paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+ }
+ return paint;
+ }
+
+ private class LazyLoadedBitmap {
+ private Bitmap mBitmap;
+ private int mResId;
+
+ public LazyLoadedBitmap(int resId) {
+ mResId = resId;
+ }
+
+ public synchronized Bitmap get() {
+ if (mBitmap == null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ mBitmap = BitmapFactory.decodeResource(
+ mContext.getResources(), mResId, options);
+ }
+ return mBitmap;
+ }
+ }
+
+ public synchronized void setLabelWidth(int width) {
+ if (mLabelWidth == width) return;
+ mLabelWidth = width;
+ int borders = 2 * BORDER_SIZE;
+ mBitmapWidth = width + borders;
+ mBitmapHeight = mSpec.labelBackgroundHeight + borders;
+ }
+
+ public ThreadPool.Job<Bitmap> requestLabel(
+ String title, String count, int sourceType) {
+ return new AlbumLabelJob(title, count, sourceType);
+ }
+
+ static void drawText(Canvas canvas,
+ int x, int y, String text, int lengthLimit, TextPaint p) {
+ // The TextPaint cannot be used concurrently
+ synchronized (p) {
+ text = TextUtils.ellipsize(
+ text, p, lengthLimit, TextUtils.TruncateAt.END).toString();
+ canvas.drawText(text, x, y - p.getFontMetricsInt().ascent, p);
+ }
+ }
+
+ private class AlbumLabelJob implements ThreadPool.Job<Bitmap> {
+ private final String mTitle;
+ private final String mCount;
+ private final int mSourceType;
+
+ public AlbumLabelJob(String title, String count, int sourceType) {
+ mTitle = title;
+ mCount = count;
+ mSourceType = sourceType;
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ AlbumSetSlotRenderer.LabelSpec s = mSpec;
+
+ String title = mTitle;
+ String count = mCount;
+ Bitmap icon = getOverlayAlbumIcon(mSourceType);
+
+ Bitmap bitmap;
+ int labelWidth;
+
+ synchronized (this) {
+ labelWidth = mLabelWidth;
+ bitmap = GalleryBitmapPool.getInstance().get(mBitmapWidth, mBitmapHeight);
+ }
+
+ if (bitmap == null) {
+ int borders = 2 * BORDER_SIZE;
+ bitmap = Bitmap.createBitmap(labelWidth + borders,
+ s.labelBackgroundHeight + borders, Config.ARGB_8888);
+ }
+
+ Canvas canvas = new Canvas(bitmap);
+ canvas.clipRect(BORDER_SIZE, BORDER_SIZE,
+ bitmap.getWidth() - BORDER_SIZE,
+ bitmap.getHeight() - BORDER_SIZE);
+ canvas.drawColor(mSpec.backgroundColor, PorterDuff.Mode.SRC);
+
+ canvas.translate(BORDER_SIZE, BORDER_SIZE);
+
+ // draw title
+ if (jc.isCancelled()) return null;
+ int x = s.leftMargin + s.iconSize;
+ // TODO: is the offset relevant in new reskin?
+ // int y = s.titleOffset;
+ int y = (s.labelBackgroundHeight - s.titleFontSize) / 2;
+ drawText(canvas, x, y, title, labelWidth - s.leftMargin - x -
+ s.titleRightMargin, mTitlePaint);
+
+ // draw count
+ if (jc.isCancelled()) return null;
+ x = labelWidth - s.titleRightMargin;
+ y = (s.labelBackgroundHeight - s.countFontSize) / 2;
+ drawText(canvas, x, y, count,
+ labelWidth - x , mCountPaint);
+
+ // draw the icon
+ if (icon != null) {
+ if (jc.isCancelled()) return null;
+ float scale = (float) s.iconSize / icon.getWidth();
+ canvas.translate(s.leftMargin, (s.labelBackgroundHeight -
+ Math.round(scale * icon.getHeight()))/2f);
+ canvas.scale(scale, scale);
+ canvas.drawBitmap(icon, 0, 0, null);
+ }
+
+ return bitmap;
+ }
+ }
+
+ public void recycleLabel(Bitmap label) {
+ GalleryBitmapPool.getInstance().put(label);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
new file mode 100644
index 000000000..8149df4b3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -0,0 +1,549 @@
+/*T
+ * 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.graphics.Bitmap;
+import android.os.Message;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumSetDataLoader;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TextureUploader;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
+public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener {
+ private static final String TAG = "AlbumSetSlidingWindow";
+ private static final int MSG_UPDATE_ALBUM_ENTRY = 1;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentChanged();
+ }
+
+ private final AlbumSetDataLoader mSource;
+ private int mSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+
+ private final AlbumSetEntry mData[];
+ private final SynchronizedHandler mHandler;
+ private final ThreadPool mThreadPool;
+ private final AlbumLabelMaker mLabelMaker;
+ private final String mLoadingText;
+
+ private final TiledTexture.Uploader mContentUploader;
+ private final TextureUploader mLabelUploader;
+
+ private int mActiveRequestCount = 0;
+ private boolean mIsActive = false;
+ private BitmapTexture mLoadingLabel;
+
+ private int mSlotWidth;
+
+ public static class AlbumSetEntry {
+ public MediaSet album;
+ public MediaItem coverItem;
+ public Texture content;
+ public BitmapTexture labelTexture;
+ public TiledTexture bitmapTexture;
+ public Path setPath;
+ public String title;
+ public int totalCount;
+ public int sourceType;
+ public int cacheFlag;
+ public int cacheStatus;
+ public int rotation;
+ public boolean isWaitLoadingDisplayed;
+ public long setDataVersion;
+ public long coverDataVersion;
+ private BitmapLoader labelLoader;
+ private BitmapLoader coverLoader;
+ }
+
+ public AlbumSetSlidingWindow(AbstractGalleryActivity activity,
+ AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) {
+ source.setModelListener(this);
+ mSource = source;
+ mData = new AlbumSetEntry[cacheSize];
+ mSize = source.size();
+ mThreadPool = activity.getThreadPool();
+
+ mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec);
+ mLoadingText = activity.getAndroidContext().getString(R.string.loading);
+ mContentUploader = new TiledTexture.Uploader(activity.getGLRoot());
+ mLabelUploader = new TextureUploader(activity.getGLRoot());
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY);
+ ((EntryUpdater) message.obj).updateEntry();
+ }
+ };
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public AlbumSetEntry get(int slotIndex) {
+ if (!isActiveSlot(slotIndex)) {
+ Utils.fail("invalid slot: %s outsides (%s, %s)",
+ slotIndex, mActiveStart, mActiveEnd);
+ }
+ return mData[slotIndex % mData.length];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean isActiveSlot(int slotIndex) {
+ return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+ if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ } else {
+ for (int i = mContentStart; i < contentStart; ++i) {
+ freeSlotContent(i);
+ }
+ for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart, n = mContentStart; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ for (int i = mContentEnd; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ }
+
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+
+ public void setActiveWindow(int start, int end) {
+ if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+ Utils.fail("start = %s, end = %s, length = %s, size = %s",
+ start, end, mData.length, mSize);
+ }
+
+ AlbumSetEntry data[] = mData;
+ mActiveStart = start;
+ mActiveEnd = end;
+ int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+ 0, Math.max(0, mSize - data.length));
+ int contentEnd = Math.min(contentStart + data.length, mSize);
+ setContentWindow(contentStart, contentEnd);
+
+ if (mIsActive) {
+ updateTextureUploadQueue();
+ updateAllImageRequests();
+ }
+ }
+
+ // We would like to request non active slots in the following order:
+ // Order: 8 6 4 2 1 3 5 7
+ // |---------|---------------|---------|
+ // |<- active ->|
+ // |<-------- cached range ----------->|
+ private void requestNonactiveImages() {
+ int range = Math.max(
+ mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+ for (int i = 0 ;i < range; ++i) {
+ requestImagesInSlot(mActiveEnd + i);
+ requestImagesInSlot(mActiveStart - 1 - i);
+ }
+ }
+
+ private void cancelNonactiveImages() {
+ int range = Math.max(
+ mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+ for (int i = 0 ;i < range; ++i) {
+ cancelImagesInSlot(mActiveEnd + i);
+ cancelImagesInSlot(mActiveStart - 1 - i);
+ }
+ }
+
+ private void requestImagesInSlot(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumSetEntry entry = mData[slotIndex % mData.length];
+ if (entry.coverLoader != null) entry.coverLoader.startLoad();
+ if (entry.labelLoader != null) entry.labelLoader.startLoad();
+ }
+
+ private void cancelImagesInSlot(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumSetEntry entry = mData[slotIndex % mData.length];
+ if (entry.coverLoader != null) entry.coverLoader.cancelLoad();
+ if (entry.labelLoader != null) entry.labelLoader.cancelLoad();
+ }
+
+ private static long getDataVersion(MediaObject object) {
+ return object == null
+ ? MediaSet.INVALID_DATA_VERSION
+ : object.getDataVersion();
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumSetEntry entry = mData[slotIndex % mData.length];
+ if (entry.coverLoader != null) entry.coverLoader.recycle();
+ if (entry.labelLoader != null) entry.labelLoader.recycle();
+ if (entry.labelTexture != null) entry.labelTexture.recycle();
+ if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+ mData[slotIndex % mData.length] = null;
+ }
+
+ private boolean isLabelChanged(
+ AlbumSetEntry entry, String title, int totalCount, int sourceType) {
+ return !Utils.equals(entry.title, title)
+ || entry.totalCount != totalCount
+ || entry.sourceType != sourceType;
+ }
+
+ private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) {
+ MediaSet album = mSource.getMediaSet(slotIndex);
+ MediaItem cover = mSource.getCoverItem(slotIndex);
+ int totalCount = mSource.getTotalCount(slotIndex);
+
+ entry.album = album;
+ entry.setDataVersion = getDataVersion(album);
+ entry.cacheFlag = identifyCacheFlag(album);
+ entry.cacheStatus = identifyCacheStatus(album);
+ entry.setPath = (album == null) ? null : album.getPath();
+
+ String title = (album == null) ? "" : Utils.ensureNotNull(album.getName());
+ int sourceType = DataSourceType.identifySourceType(album);
+ if (isLabelChanged(entry, title, totalCount, sourceType)) {
+ entry.title = title;
+ entry.totalCount = totalCount;
+ entry.sourceType = sourceType;
+ if (entry.labelLoader != null) {
+ entry.labelLoader.recycle();
+ entry.labelLoader = null;
+ entry.labelTexture = null;
+ }
+ if (album != null) {
+ entry.labelLoader = new AlbumLabelLoader(
+ slotIndex, title, totalCount, sourceType);
+ }
+ }
+
+ entry.coverItem = cover;
+ if (getDataVersion(cover) != entry.coverDataVersion) {
+ entry.coverDataVersion = getDataVersion(cover);
+ entry.rotation = (cover == null) ? 0 : cover.getRotation();
+ if (entry.coverLoader != null) {
+ entry.coverLoader.recycle();
+ entry.coverLoader = null;
+ entry.bitmapTexture = null;
+ entry.content = null;
+ }
+ if (cover != null) {
+ entry.coverLoader = new AlbumCoverLoader(slotIndex, cover);
+ }
+ }
+ }
+
+ private void prepareSlotContent(int slotIndex) {
+ AlbumSetEntry entry = new AlbumSetEntry();
+ updateAlbumSetEntry(entry, slotIndex);
+ mData[slotIndex % mData.length] = entry;
+ }
+
+ private static boolean startLoadBitmap(BitmapLoader loader) {
+ if (loader == null) return false;
+ loader.startLoad();
+ return loader.isRequestInProgress();
+ }
+
+ private void uploadBackgroundTextureInSlot(int index) {
+ if (index < mContentStart || index >= mContentEnd) return;
+ AlbumSetEntry entry = mData[index % mData.length];
+ if (entry.bitmapTexture != null) {
+ mContentUploader.addTexture(entry.bitmapTexture);
+ }
+ if (entry.labelTexture != null) {
+ mLabelUploader.addBgTexture(entry.labelTexture);
+ }
+ }
+
+ private void updateTextureUploadQueue() {
+ if (!mIsActive) return;
+ mContentUploader.clear();
+ mLabelUploader.clear();
+
+ // Upload foreground texture
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumSetEntry entry = mData[i % mData.length];
+ if (entry.bitmapTexture != null) {
+ mContentUploader.addTexture(entry.bitmapTexture);
+ }
+ if (entry.labelTexture != null) {
+ mLabelUploader.addFgTexture(entry.labelTexture);
+ }
+ }
+
+ // add background textures
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0; i < range; ++i) {
+ uploadBackgroundTextureInSlot(mActiveEnd + i);
+ uploadBackgroundTextureInSlot(mActiveStart - i - 1);
+ }
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumSetEntry entry = mData[i % mData.length];
+ if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount;
+ if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount;
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ @Override
+ public void onSizeChanged(int size) {
+ if (mIsActive && mSize != size) {
+ mSize = size;
+ if (mListener != null) mListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+ }
+
+ @Override
+ public void onContentChanged(int index) {
+ if (!mIsActive) {
+ // paused, ignore slot changed event
+ return;
+ }
+
+ // If the updated content is not cached, ignore it
+ if (index < mContentStart || index >= mContentEnd) {
+ Log.w(TAG, String.format(
+ "invalid update: %s is outside (%s, %s)",
+ index, mContentStart, mContentEnd) );
+ return;
+ }
+
+ AlbumSetEntry entry = mData[index % mData.length];
+ updateAlbumSetEntry(entry, index);
+ updateAllImageRequests();
+ updateTextureUploadQueue();
+ if (mListener != null && isActiveSlot(index)) {
+ mListener.onContentChanged();
+ }
+ }
+
+ public BitmapTexture getLoadingTexture() {
+ if (mLoadingLabel == null) {
+ Bitmap bitmap = mLabelMaker.requestLabel(
+ mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED)
+ .run(ThreadPool.JOB_CONTEXT_STUB);
+ mLoadingLabel = new BitmapTexture(bitmap);
+ mLoadingLabel.setOpaque(false);
+ }
+ return mLoadingLabel;
+ }
+
+ public void pause() {
+ mIsActive = false;
+ mLabelUploader.clear();
+ mContentUploader.clear();
+ TiledTexture.freeResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ TiledTexture.prepareResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+
+ private static interface EntryUpdater {
+ public void updateEntry();
+ }
+
+ private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater {
+ private MediaItem mMediaItem;
+ private final int mSlotIndex;
+
+ public AlbumCoverLoader(int slotIndex, MediaItem item) {
+ mSlotIndex = slotIndex;
+ mMediaItem = item;
+ }
+
+ @Override
+ protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+ return mThreadPool.submit(mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL), l);
+ }
+
+ @Override
+ protected void onLoadComplete(Bitmap bitmap) {
+ mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+ }
+
+ @Override
+ public void updateEntry() {
+ Bitmap bitmap = getBitmap();
+ if (bitmap == null) return; // error or recycled
+
+ AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+ TiledTexture texture = new TiledTexture(bitmap);
+ entry.bitmapTexture = texture;
+ entry.content = texture;
+
+ if (isActiveSlot(mSlotIndex)) {
+ mContentUploader.addTexture(texture);
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ if (mListener != null) mListener.onContentChanged();
+ } else {
+ mContentUploader.addTexture(texture);
+ }
+ }
+ }
+
+ private static int identifyCacheFlag(MediaSet set) {
+ if (set == null || (set.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ return MediaSet.CACHE_FLAG_NO;
+ }
+
+ return set.getCacheFlag();
+ }
+
+ private static int identifyCacheStatus(MediaSet set) {
+ if (set == null || (set.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ return MediaSet.CACHE_STATUS_NOT_CACHED;
+ }
+
+ return set.getCacheStatus();
+ }
+
+ private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater {
+ private final int mSlotIndex;
+ private final String mTitle;
+ private final int mTotalCount;
+ private final int mSourceType;
+
+ public AlbumLabelLoader(
+ int slotIndex, String title, int totalCount, int sourceType) {
+ mSlotIndex = slotIndex;
+ mTitle = title;
+ mTotalCount = totalCount;
+ mSourceType = sourceType;
+ }
+
+ @Override
+ protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+ return mThreadPool.submit(mLabelMaker.requestLabel(
+ mTitle, String.valueOf(mTotalCount), mSourceType), l);
+ }
+
+ @Override
+ protected void onLoadComplete(Bitmap bitmap) {
+ mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+ }
+
+ @Override
+ public void updateEntry() {
+ Bitmap bitmap = getBitmap();
+ if (bitmap == null) return; // Error or recycled
+
+ AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ texture.setOpaque(false);
+ entry.labelTexture = texture;
+
+ if (isActiveSlot(mSlotIndex)) {
+ mLabelUploader.addFgTexture(texture);
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ if (mListener != null) mListener.onContentChanged();
+ } else {
+ mLabelUploader.addBgTexture(texture);
+ }
+ }
+ }
+
+ public void onSlotSizeChanged(int width, int height) {
+ if (mSlotWidth == width) return;
+
+ mSlotWidth = width;
+ mLoadingLabel = null;
+ mLabelMaker.setLabelWidth(mSlotWidth);
+
+ if (!mIsActive) return;
+
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ AlbumSetEntry entry = mData[i % mData.length];
+ if (entry.labelLoader != null) {
+ entry.labelLoader.recycle();
+ entry.labelLoader = null;
+ entry.labelTexture = null;
+ }
+ if (entry.album != null) {
+ entry.labelLoader = new AlbumLabelLoader(i,
+ entry.title, entry.totalCount, entry.sourceType);
+ }
+ }
+ updateAllImageRequests();
+ updateTextureUploadQueue();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
new file mode 100644
index 000000000..5332ef89a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
@@ -0,0 +1,242 @@
+/*
+ * 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.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumSetDataLoader;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.ColorTexture;
+import com.android.gallery3d.glrenderer.FadeInTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class AlbumSetSlotRenderer extends AbstractSlotRenderer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetView";
+ private static final int CACHE_SIZE = 96;
+ private final int mPlaceholderColor;
+
+ private final ColorTexture mWaitLoadingTexture;
+ private final ResourceTexture mCameraOverlay;
+ private final AbstractGalleryActivity mActivity;
+ private final SelectionManager mSelectionManager;
+ protected final LabelSpec mLabelSpec;
+
+ protected AlbumSetSlidingWindow mDataWindow;
+ private SlotView mSlotView;
+
+ private int mPressedIndex = -1;
+ private boolean mAnimatePressedUp;
+ private Path mHighlightItemPath = null;
+ private boolean mInSelectionMode;
+
+ public static class LabelSpec {
+ public int labelBackgroundHeight;
+ public int titleOffset;
+ public int countOffset;
+ public int titleFontSize;
+ public int countFontSize;
+ public int leftMargin;
+ public int iconSize;
+ public int titleRightMargin;
+ public int backgroundColor;
+ public int titleColor;
+ public int countColor;
+ public int borderSize;
+ }
+
+ public AlbumSetSlotRenderer(AbstractGalleryActivity activity,
+ SelectionManager selectionManager,
+ SlotView slotView, LabelSpec labelSpec, int placeholderColor) {
+ super (activity);
+ mActivity = activity;
+ mSelectionManager = selectionManager;
+ mSlotView = slotView;
+ mLabelSpec = labelSpec;
+ mPlaceholderColor = placeholderColor;
+
+ mWaitLoadingTexture = new ColorTexture(mPlaceholderColor);
+ mWaitLoadingTexture.setSize(1, 1);
+ mCameraOverlay = new ResourceTexture(activity,
+ R.drawable.ic_cameraalbum_overlay);
+ }
+
+ public void setPressedIndex(int index) {
+ if (mPressedIndex == index) return;
+ mPressedIndex = index;
+ mSlotView.invalidate();
+ }
+
+ public void setPressedUp() {
+ if (mPressedIndex == -1) return;
+ mAnimatePressedUp = true;
+ mSlotView.invalidate();
+ }
+
+ public void setHighlightItemPath(Path path) {
+ if (mHighlightItemPath == path) return;
+ mHighlightItemPath = path;
+ mSlotView.invalidate();
+ }
+
+ public void setModel(AlbumSetDataLoader model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ mDataWindow = null;
+ mSlotView.setSlotCount(0);
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSetSlidingWindow(
+ mActivity, model, mLabelSpec, CACHE_SIZE);
+ mDataWindow.setListener(new MyCacheListener());
+ mSlotView.setSlotCount(mDataWindow.size());
+ }
+ }
+
+ private static Texture checkLabelTexture(Texture texture) {
+ return ((texture instanceof UploadedTexture)
+ && ((UploadedTexture) texture).isUploading())
+ ? null
+ : texture;
+ }
+
+ private static Texture checkContentTexture(Texture texture) {
+ return ((texture instanceof TiledTexture)
+ && !((TiledTexture) texture).isReady())
+ ? null
+ : texture;
+ }
+
+ @Override
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+ AlbumSetEntry entry = mDataWindow.get(index);
+ int renderRequestFlags = 0;
+ renderRequestFlags |= renderContent(canvas, entry, width, height);
+ renderRequestFlags |= renderLabel(canvas, entry, width, height);
+ renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+ return renderRequestFlags;
+ }
+
+ protected int renderOverlay(
+ GLCanvas canvas, int index, AlbumSetEntry entry, int width, int height) {
+ int renderRequestFlags = 0;
+ if (entry.album != null && entry.album.isCameraRoll()) {
+ int uncoveredHeight = height - mLabelSpec.labelBackgroundHeight;
+ int dim = uncoveredHeight / 2;
+ mCameraOverlay.draw(canvas, (width - dim) / 2,
+ (uncoveredHeight - dim) / 2, dim, dim);
+ }
+ if (mPressedIndex == index) {
+ if (mAnimatePressedUp) {
+ drawPressedUpFrame(canvas, width, height);
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ if (isPressedUpFrameFinished()) {
+ mAnimatePressedUp = false;
+ mPressedIndex = -1;
+ }
+ } else {
+ drawPressedFrame(canvas, width, height);
+ }
+ } else if ((mHighlightItemPath != null) && (mHighlightItemPath == entry.setPath)) {
+ drawSelectedFrame(canvas, width, height);
+ } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.setPath)) {
+ drawSelectedFrame(canvas, width, height);
+ }
+ return renderRequestFlags;
+ }
+
+ protected int renderContent(
+ GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+ int renderRequestFlags = 0;
+
+ Texture content = checkContentTexture(entry.content);
+ if (content == null) {
+ content = mWaitLoadingTexture;
+ entry.isWaitLoadingDisplayed = true;
+ } else if (entry.isWaitLoadingDisplayed) {
+ entry.isWaitLoadingDisplayed = false;
+ content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture);
+ entry.content = content;
+ }
+ drawContent(canvas, content, width, height, entry.rotation);
+ if ((content instanceof FadeInTexture) &&
+ ((FadeInTexture) content).isAnimating()) {
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ }
+
+ return renderRequestFlags;
+ }
+
+ protected int renderLabel(
+ GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+ Texture content = checkLabelTexture(entry.labelTexture);
+ if (content == null) {
+ content = mWaitLoadingTexture;
+ }
+ int b = AlbumLabelMaker.getBorderSize();
+ int h = mLabelSpec.labelBackgroundHeight;
+ content.draw(canvas, -b, height - h + b, width + b + b, h);
+
+ return 0;
+ }
+
+ @Override
+ public void prepareDrawing() {
+ mInSelectionMode = mSelectionManager.inSelectionMode();
+ }
+
+ private class MyCacheListener implements AlbumSetSlidingWindow.Listener {
+
+ @Override
+ public void onSizeChanged(int size) {
+ mSlotView.setSlotCount(size);
+ }
+
+ @Override
+ public void onContentChanged() {
+ mSlotView.invalidate();
+ }
+ }
+
+ public void pause() {
+ mDataWindow.pause();
+ }
+
+ public void resume() {
+ mDataWindow.resume();
+ }
+
+ @Override
+ public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+ if (mDataWindow != null) {
+ mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+ }
+ }
+
+ @Override
+ public void onSlotSizeChanged(int width, int height) {
+ if (mDataWindow != null) {
+ mDataWindow.onSlotSizeChanged(width, height);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
new file mode 100644
index 000000000..fec7d1e92
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -0,0 +1,365 @@
+/*
+ * 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.graphics.Bitmap;
+import android.os.Message;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumDataLoader;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.JobLimiter;
+
+public class AlbumSlidingWindow implements AlbumDataLoader.DataListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSlidingWindow";
+
+ private static final int MSG_UPDATE_ENTRY = 0;
+ private static final int JOB_LIMIT = 2;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentChanged();
+ }
+
+ public static class AlbumEntry {
+ public MediaItem item;
+ public Path path;
+ public boolean isPanorama;
+ public int rotation;
+ public int mediaType;
+ public boolean isWaitDisplayed;
+ public TiledTexture bitmapTexture;
+ public Texture content;
+ private BitmapLoader contentLoader;
+ private PanoSupportListener mPanoSupportListener;
+ }
+
+ private final AlbumDataLoader mSource;
+ private final AlbumEntry mData[];
+ private final SynchronizedHandler mHandler;
+ private final JobLimiter mThreadPool;
+ private final TiledTexture.Uploader mTileUploader;
+
+ private int mSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+
+ private int mActiveRequestCount = 0;
+ private boolean mIsActive = false;
+
+ private class PanoSupportListener implements PanoramaSupportCallback {
+ public final AlbumEntry mEntry;
+ public PanoSupportListener (AlbumEntry entry) {
+ mEntry = entry;
+ }
+ @Override
+ public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360) {
+ if (mEntry != null) mEntry.isPanorama = isPanorama;
+ }
+ }
+
+ public AlbumSlidingWindow(AbstractGalleryActivity activity,
+ AlbumDataLoader source, int cacheSize) {
+ source.setDataListener(this);
+ mSource = source;
+ mData = new AlbumEntry[cacheSize];
+ mSize = source.size();
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_ENTRY);
+ ((ThumbnailLoader) message.obj).updateEntry();
+ }
+ };
+
+ mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT);
+ mTileUploader = new TiledTexture.Uploader(activity.getGLRoot());
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public AlbumEntry get(int slotIndex) {
+ if (!isActiveSlot(slotIndex)) {
+ Utils.fail("invalid slot: %s outsides (%s, %s)",
+ slotIndex, mActiveStart, mActiveEnd);
+ }
+ return mData[slotIndex % mData.length];
+ }
+
+ public boolean isActiveSlot(int slotIndex) {
+ return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+ if (!mIsActive) {
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ mSource.setActiveWindow(contentStart, contentEnd);
+ return;
+ }
+
+ if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ } else {
+ for (int i = mContentStart; i < contentStart; ++i) {
+ freeSlotContent(i);
+ }
+ for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart, n = mContentStart; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ for (int i = mContentEnd; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ }
+
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+
+ public void setActiveWindow(int start, int end) {
+ if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+ Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize);
+ }
+ AlbumEntry data[] = mData;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+ 0, Math.max(0, mSize - data.length));
+ int contentEnd = Math.min(contentStart + data.length, mSize);
+ setContentWindow(contentStart, contentEnd);
+ updateTextureUploadQueue();
+ if (mIsActive) updateAllImageRequests();
+ }
+
+ private void uploadBgTextureInSlot(int index) {
+ if (index < mContentEnd && index >= mContentStart) {
+ AlbumEntry entry = mData[index % mData.length];
+ if (entry.bitmapTexture != null) {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ }
+ }
+ }
+
+ private void updateTextureUploadQueue() {
+ if (!mIsActive) return;
+ mTileUploader.clear();
+
+ // add foreground textures
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumEntry entry = mData[i % mData.length];
+ if (entry.bitmapTexture != null) {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ }
+ }
+
+ // add background textures
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0; i < range; ++i) {
+ uploadBgTextureInSlot(mActiveEnd + i);
+ uploadBgTextureInSlot(mActiveStart - i - 1);
+ }
+ }
+
+ // We would like to request non active slots in the following order:
+ // Order: 8 6 4 2 1 3 5 7
+ // |---------|---------------|---------|
+ // |<- active ->|
+ // |<-------- cached range ----------->|
+ private void requestNonactiveImages() {
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0 ;i < range; ++i) {
+ requestSlotImage(mActiveEnd + i);
+ requestSlotImage(mActiveStart - 1 - i);
+ }
+ }
+
+ // return whether the request is in progress or not
+ private boolean requestSlotImage(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false;
+ AlbumEntry entry = mData[slotIndex % mData.length];
+ if (entry.content != null || entry.item == null) return false;
+
+ // Set up the panorama callback
+ entry.mPanoSupportListener = new PanoSupportListener(entry);
+ entry.item.getPanoramaSupport(entry.mPanoSupportListener);
+
+ entry.contentLoader.startLoad();
+ return entry.contentLoader.isRequestInProgress();
+ }
+
+ private void cancelNonactiveImages() {
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0 ;i < range; ++i) {
+ cancelSlotImage(mActiveEnd + i);
+ cancelSlotImage(mActiveStart - 1 - i);
+ }
+ }
+
+ private void cancelSlotImage(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumEntry item = mData[slotIndex % mData.length];
+ if (item.contentLoader != null) item.contentLoader.cancelLoad();
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumEntry data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumEntry entry = data[index];
+ if (entry.contentLoader != null) entry.contentLoader.recycle();
+ if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+ data[index] = null;
+ }
+
+ private void prepareSlotContent(int slotIndex) {
+ AlbumEntry entry = new AlbumEntry();
+ MediaItem item = mSource.get(slotIndex); // item could be null;
+ entry.item = item;
+ entry.mediaType = (item == null)
+ ? MediaItem.MEDIA_TYPE_UNKNOWN
+ : entry.item.getMediaType();
+ entry.path = (item == null) ? null : item.getPath();
+ entry.rotation = (item == null) ? 0 : item.getRotation();
+ entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item);
+ mData[slotIndex % mData.length] = entry;
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ if (requestSlotImage(i)) ++mActiveRequestCount;
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ private class ThumbnailLoader extends BitmapLoader {
+ private final int mSlotIndex;
+ private final MediaItem mItem;
+
+ public ThumbnailLoader(int slotIndex, MediaItem item) {
+ mSlotIndex = slotIndex;
+ mItem = item;
+ }
+
+ @Override
+ protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+ return mThreadPool.submit(
+ mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this);
+ }
+
+ @Override
+ protected void onLoadComplete(Bitmap bitmap) {
+ mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget();
+ }
+
+ public void updateEntry() {
+ Bitmap bitmap = getBitmap();
+ if (bitmap == null) return; // error or recycled
+ AlbumEntry entry = mData[mSlotIndex % mData.length];
+ entry.bitmapTexture = new TiledTexture(bitmap);
+ entry.content = entry.bitmapTexture;
+
+ if (isActiveSlot(mSlotIndex)) {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ if (mListener != null) mListener.onContentChanged();
+ } else {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ }
+ }
+ }
+
+ @Override
+ public void onSizeChanged(int size) {
+ if (mSize != size) {
+ mSize = size;
+ if (mListener != null) mListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+ }
+
+ @Override
+ public void onContentChanged(int index) {
+ if (index >= mContentStart && index < mContentEnd && mIsActive) {
+ freeSlotContent(index);
+ prepareSlotContent(index);
+ updateAllImageRequests();
+ if (mListener != null && isActiveSlot(index)) {
+ mListener.onContentChanged();
+ }
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ TiledTexture.prepareResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+
+ public void pause() {
+ mIsActive = false;
+ mTileUploader.clear();
+ TiledTexture.freeResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
new file mode 100644
index 000000000..dc6c89b0e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
@@ -0,0 +1,201 @@
+/*
+ * 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.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumDataLoader;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.ColorTexture;
+import com.android.gallery3d.glrenderer.FadeInTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+
+public class AlbumSlotRenderer extends AbstractSlotRenderer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumView";
+
+ public interface SlotFilter {
+ public boolean acceptSlot(int index);
+ }
+
+ private final int mPlaceholderColor;
+ private static final int CACHE_SIZE = 96;
+
+ private AlbumSlidingWindow mDataWindow;
+ private final AbstractGalleryActivity mActivity;
+ private final ColorTexture mWaitLoadingTexture;
+ private final SlotView mSlotView;
+ private final SelectionManager mSelectionManager;
+
+ private int mPressedIndex = -1;
+ private boolean mAnimatePressedUp;
+ private Path mHighlightItemPath = null;
+ private boolean mInSelectionMode;
+
+ private SlotFilter mSlotFilter;
+
+ public AlbumSlotRenderer(AbstractGalleryActivity activity, SlotView slotView,
+ SelectionManager selectionManager, int placeholderColor) {
+ super(activity);
+ mActivity = activity;
+ mSlotView = slotView;
+ mSelectionManager = selectionManager;
+ mPlaceholderColor = placeholderColor;
+
+ mWaitLoadingTexture = new ColorTexture(mPlaceholderColor);
+ mWaitLoadingTexture.setSize(1, 1);
+ }
+
+ public void setPressedIndex(int index) {
+ if (mPressedIndex == index) return;
+ mPressedIndex = index;
+ mSlotView.invalidate();
+ }
+
+ public void setPressedUp() {
+ if (mPressedIndex == -1) return;
+ mAnimatePressedUp = true;
+ mSlotView.invalidate();
+ }
+
+ public void setHighlightItemPath(Path path) {
+ if (mHighlightItemPath == path) return;
+ mHighlightItemPath = path;
+ mSlotView.invalidate();
+ }
+
+ public void setModel(AlbumDataLoader model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ mSlotView.setSlotCount(0);
+ mDataWindow = null;
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSlidingWindow(mActivity, model, CACHE_SIZE);
+ mDataWindow.setListener(new MyDataModelListener());
+ mSlotView.setSlotCount(model.size());
+ }
+ }
+
+ private static Texture checkTexture(Texture texture) {
+ return (texture instanceof TiledTexture)
+ && !((TiledTexture) texture).isReady()
+ ? null
+ : texture;
+ }
+
+ @Override
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+ if (mSlotFilter != null && !mSlotFilter.acceptSlot(index)) return 0;
+
+ AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index);
+
+ int renderRequestFlags = 0;
+
+ Texture content = checkTexture(entry.content);
+ if (content == null) {
+ content = mWaitLoadingTexture;
+ entry.isWaitDisplayed = true;
+ } else if (entry.isWaitDisplayed) {
+ entry.isWaitDisplayed = false;
+ content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture);
+ entry.content = content;
+ }
+ drawContent(canvas, content, width, height, entry.rotation);
+ if ((content instanceof FadeInTexture) &&
+ ((FadeInTexture) content).isAnimating()) {
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ }
+
+ if (entry.mediaType == MediaObject.MEDIA_TYPE_VIDEO) {
+ drawVideoOverlay(canvas, width, height);
+ }
+
+ if (entry.isPanorama) {
+ drawPanoramaIcon(canvas, width, height);
+ }
+
+ renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+
+ return renderRequestFlags;
+ }
+
+ private int renderOverlay(GLCanvas canvas, int index,
+ AlbumSlidingWindow.AlbumEntry entry, int width, int height) {
+ int renderRequestFlags = 0;
+ if (mPressedIndex == index) {
+ if (mAnimatePressedUp) {
+ drawPressedUpFrame(canvas, width, height);
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ if (isPressedUpFrameFinished()) {
+ mAnimatePressedUp = false;
+ mPressedIndex = -1;
+ }
+ } else {
+ drawPressedFrame(canvas, width, height);
+ }
+ } else if ((entry.path != null) && (mHighlightItemPath == entry.path)) {
+ drawSelectedFrame(canvas, width, height);
+ } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.path)) {
+ drawSelectedFrame(canvas, width, height);
+ }
+ return renderRequestFlags;
+ }
+
+ private class MyDataModelListener implements AlbumSlidingWindow.Listener {
+ @Override
+ public void onContentChanged() {
+ mSlotView.invalidate();
+ }
+
+ @Override
+ public void onSizeChanged(int size) {
+ mSlotView.setSlotCount(size);
+ }
+ }
+
+ public void resume() {
+ mDataWindow.resume();
+ }
+
+ public void pause() {
+ mDataWindow.pause();
+ }
+
+ @Override
+ public void prepareDrawing() {
+ mInSelectionMode = mSelectionManager.inSelectionMode();
+ }
+
+ @Override
+ public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+ if (mDataWindow != null) {
+ mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+ }
+ }
+
+ @Override
+ public void onSlotSizeChanged(int width, int height) {
+ // Do nothing
+ }
+
+ public void setSlotFilter(SlotFilter slotFilter) {
+ mSlotFilter = slotFilter;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AnimationTime.java b/src/com/android/gallery3d/ui/AnimationTime.java
new file mode 100644
index 000000000..063677423
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AnimationTime.java
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) 2012 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.os.SystemClock;
+
+//
+// The animation time should ideally be the vsync time the frame will be
+// displayed, but that is an unknown time in the future. So we use the system
+// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called)
+// as a approximation.
+//
+public class AnimationTime {
+ private static volatile long sTime;
+
+ // Sets current time as the animation time.
+ public static void update() {
+ sTime = SystemClock.uptimeMillis();
+ }
+
+ // Returns the animation time.
+ public static long get() {
+ return sTime;
+ }
+
+ public static long startTime() {
+ sTime = SystemClock.uptimeMillis();
+ return sTime;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapLoader.java b/src/com/android/gallery3d/ui/BitmapLoader.java
new file mode 100644
index 000000000..a708a90f3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapLoader.java
@@ -0,0 +1,108 @@
+/*
+ * 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.graphics.Bitmap;
+
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+// We use this class to
+// 1.) load bitmaps in background.
+// 2.) as a place holder for the loaded bitmap
+public abstract class BitmapLoader implements FutureListener<Bitmap> {
+ @SuppressWarnings("unused")
+ private static final String TAG = "BitmapLoader";
+
+ /* Transition Map:
+ * INIT -> REQUESTED, RECYCLED
+ * REQUESTED -> INIT (cancel), LOADED, ERROR, RECYCLED
+ * LOADED, ERROR -> RECYCLED
+ */
+ private static final int STATE_INIT = 0;
+ private static final int STATE_REQUESTED = 1;
+ private static final int STATE_LOADED = 2;
+ private static final int STATE_ERROR = 3;
+ private static final int STATE_RECYCLED = 4;
+
+ private int mState = STATE_INIT;
+ // mTask is not null only when a task is on the way
+ private Future<Bitmap> mTask;
+ private Bitmap mBitmap;
+
+ @Override
+ public void onFutureDone(Future<Bitmap> future) {
+ synchronized (this) {
+ mTask = null;
+ mBitmap = future.get();
+ if (mState == STATE_RECYCLED) {
+ if (mBitmap != null) {
+ GalleryBitmapPool.getInstance().put(mBitmap);
+ mBitmap = null;
+ }
+ return; // don't call callback
+ }
+ if (future.isCancelled() && mBitmap == null) {
+ if (mState == STATE_REQUESTED) mTask = submitBitmapTask(this);
+ return; // don't call callback
+ } else {
+ mState = mBitmap == null ? STATE_ERROR : STATE_LOADED;
+ }
+ }
+ onLoadComplete(mBitmap);
+ }
+
+ public synchronized void startLoad() {
+ if (mState == STATE_INIT) {
+ mState = STATE_REQUESTED;
+ if (mTask == null) mTask = submitBitmapTask(this);
+ }
+ }
+
+ public synchronized void cancelLoad() {
+ if (mState == STATE_REQUESTED) {
+ mState = STATE_INIT;
+ if (mTask != null) mTask.cancel();
+ }
+ }
+
+ // Recycle the loader and the bitmap
+ public synchronized void recycle() {
+ mState = STATE_RECYCLED;
+ if (mBitmap != null) {
+ GalleryBitmapPool.getInstance().put(mBitmap);
+ mBitmap = null;
+ }
+ if (mTask != null) mTask.cancel();
+ }
+
+ public synchronized boolean isRequestInProgress() {
+ return mState == STATE_REQUESTED;
+ }
+
+ public synchronized boolean isRecycled() {
+ return mState == STATE_RECYCLED;
+ }
+
+ public synchronized Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ abstract protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l);
+ abstract protected void onLoadComplete(Bitmap bitmap);
+}
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
new file mode 100644
index 000000000..a3d403946
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012 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.graphics.Bitmap;
+import android.graphics.RectF;
+
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public class BitmapScreenNail implements ScreenNail {
+ private final BitmapTexture mBitmapTexture;
+
+ public BitmapScreenNail(Bitmap bitmap) {
+ mBitmapTexture = new BitmapTexture(bitmap);
+ }
+
+ @Override
+ public int getWidth() {
+ return mBitmapTexture.getWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return mBitmapTexture.getHeight();
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ mBitmapTexture.draw(canvas, x, y, width, height);
+ }
+
+ @Override
+ public void noDraw() {
+ // do nothing
+ }
+
+ @Override
+ public void recycle() {
+ mBitmapTexture.recycle();
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, RectF source, RectF dest) {
+ canvas.drawTexture(mBitmapTexture, source, dest);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
new file mode 100644
index 000000000..e1a8b7644
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -0,0 +1,103 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.photos.data.GalleryBitmapPool;
+
+import java.util.ArrayList;
+
+public class BitmapTileProvider implements TileImageView.TileSource {
+ private final ScreenNail mScreenNail;
+ private final Bitmap[] mMipmaps;
+ private final Config mConfig;
+ private final int mImageWidth;
+ private final int mImageHeight;
+
+ private boolean mRecycled = false;
+
+ public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) {
+ mImageWidth = bitmap.getWidth();
+ mImageHeight = bitmap.getHeight();
+ ArrayList<Bitmap> list = new ArrayList<Bitmap>();
+ list.add(bitmap);
+ while (bitmap.getWidth() > maxBackupSize
+ || bitmap.getHeight() > maxBackupSize) {
+ bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false);
+ list.add(bitmap);
+ }
+
+ mScreenNail = new BitmapScreenNail(list.remove(list.size() - 1));
+ mMipmaps = list.toArray(new Bitmap[list.size()]);
+ mConfig = Config.ARGB_8888;
+ }
+
+ @Override
+ public ScreenNail getScreenNail() {
+ return mScreenNail;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mMipmaps.length;
+ }
+
+ @Override
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ x >>= level;
+ y >>= level;
+
+ Bitmap result = GalleryBitmapPool.getInstance().get(tileSize, tileSize);
+ if (result == null) {
+ result = Bitmap.createBitmap(tileSize, tileSize, mConfig);
+ } else {
+ result.eraseColor(0);
+ }
+
+ Bitmap mipmap = mMipmaps[level];
+ Canvas canvas = new Canvas(result);
+ int offsetX = -x;
+ int offsetY = -y;
+ canvas.drawBitmap(mipmap, offsetX, offsetY, null);
+ return result;
+ }
+
+ public void recycle() {
+ if (mRecycled) return;
+ mRecycled = true;
+ for (Bitmap bitmap : mMipmaps) {
+ BitmapUtils.recycleSilently(bitmap);
+ }
+ if (mScreenNail != null) {
+ mScreenNail.recycle();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
new file mode 100644
index 000000000..46f7a2433
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
@@ -0,0 +1,90 @@
+/*
+ * 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.os.StatFs;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+
+public class CacheStorageUsageInfo {
+ @SuppressWarnings("unused")
+ private static final String TAG = "CacheStorageUsageInfo";
+
+ // number of bytes the storage has.
+ private long mTotalBytes;
+
+ // number of bytes already used.
+ private long mUsedBytes;
+
+ // number of bytes used for the cache (should be less then usedBytes).
+ private long mUsedCacheBytes;
+
+ // number of bytes used for the cache if all pending downloads (and removals) are completed.
+ private long mTargetCacheBytes;
+
+ private AbstractGalleryActivity mActivity;
+ private Context mContext;
+ private long mUserChangeDelta;
+
+ public CacheStorageUsageInfo(AbstractGalleryActivity activity) {
+ mActivity = activity;
+ mContext = activity.getAndroidContext();
+ }
+
+ public void increaseTargetCacheSize(long delta) {
+ mUserChangeDelta += delta;
+ }
+
+ public void loadStorageInfo(JobContext jc) {
+ File cacheDir = mContext.getExternalCacheDir();
+ if (cacheDir == null) {
+ cacheDir = mContext.getCacheDir();
+ }
+
+ String path = cacheDir.getAbsolutePath();
+ StatFs stat = new StatFs(path);
+ long blockSize = stat.getBlockSize();
+ long availableBlocks = stat.getAvailableBlocks();
+ long totalBlocks = stat.getBlockCount();
+
+ mTotalBytes = blockSize * totalBlocks;
+ mUsedBytes = blockSize * (totalBlocks - availableBlocks);
+ mUsedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize();
+ mTargetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize();
+ }
+
+ public long getTotalBytes() {
+ return mTotalBytes;
+ }
+
+ public long getExpectedUsedBytes() {
+ return mUsedBytes - mUsedCacheBytes + mTargetCacheBytes + mUserChangeDelta;
+ }
+
+ public long getUsedBytes() {
+ // Should it be usedBytes - usedCacheBytes + targetCacheBytes ?
+ return mUsedBytes;
+ }
+
+ public long getFreeBytes() {
+ return mTotalBytes - mUsedBytes;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CaptureAnimation.java b/src/com/android/gallery3d/ui/CaptureAnimation.java
new file mode 100644
index 000000000..87c054ab3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CaptureAnimation.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+public class CaptureAnimation {
+ // The amount of change for zooming out.
+ private static final float ZOOM_DELTA = 0.2f;
+ // Pre-calculated value for convenience.
+ private static final float ZOOM_IN_BEGIN = 1f - ZOOM_DELTA;
+
+ private static final Interpolator sZoomOutInterpolator =
+ new DecelerateInterpolator();
+ private static final Interpolator sZoomInInterpolator =
+ new AccelerateInterpolator();
+ private static final Interpolator sSlideInterpolator =
+ new AccelerateDecelerateInterpolator();
+
+ // Calculate the slide factor based on the give time fraction.
+ public static float calculateSlide(float fraction) {
+ return sSlideInterpolator.getInterpolation(fraction);
+ }
+
+ // Calculate the scale factor based on the given time fraction.
+ public static float calculateScale(float fraction) {
+ float value;
+ if (fraction <= 0.5f) {
+ // Zoom in for the beginning.
+ value = 1f - ZOOM_DELTA *
+ sZoomOutInterpolator.getInterpolation(fraction * 2);
+ } else {
+ // Zoom out for the ending.
+ value = ZOOM_IN_BEGIN + ZOOM_DELTA *
+ sZoomInInterpolator.getInterpolation((fraction - 0.5f) * 2f);
+ }
+ return value;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsAddressResolver.java b/src/com/android/gallery3d/ui/DetailsAddressResolver.java
new file mode 100644
index 000000000..8de667745
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsAddressResolver.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2011 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.location.Address;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ReverseGeocoder;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class DetailsAddressResolver {
+ private AddressResolvingListener mListener;
+ private final AbstractGalleryActivity mContext;
+ private Future<Address> mAddressLookupJob;
+ private final Handler mHandler;
+
+ private class AddressLookupJob implements Job<Address> {
+ private double[] mLatlng;
+
+ protected AddressLookupJob(double[] latlng) {
+ mLatlng = latlng;
+ }
+
+ @Override
+ public Address run(JobContext jc) {
+ ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext());
+ return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true);
+ }
+ }
+
+ public interface AddressResolvingListener {
+ public void onAddressAvailable(String address);
+ }
+
+ public DetailsAddressResolver(AbstractGalleryActivity context) {
+ mContext = context;
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ public String resolveAddress(double[] latlng, AddressResolvingListener listener) {
+ mListener = listener;
+ mAddressLookupJob = mContext.getThreadPool().submit(
+ new AddressLookupJob(latlng),
+ new FutureListener<Address>() {
+ @Override
+ public void onFutureDone(final Future<Address> future) {
+ mAddressLookupJob = null;
+ if (!future.isCancelled()) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateLocation(future.get());
+ }
+ });
+ }
+ }
+ });
+ return GalleryUtils.formatLatitudeLongitude("(%f,%f)", latlng[0], latlng[1]);
+ }
+
+ private void updateLocation(Address address) {
+ if (address != null) {
+ Context context = mContext.getAndroidContext();
+ String parts[] = {
+ address.getAdminArea(),
+ address.getSubAdminArea(),
+ address.getLocality(),
+ address.getSubLocality(),
+ address.getThoroughfare(),
+ address.getSubThoroughfare(),
+ address.getPremises(),
+ address.getPostalCode(),
+ address.getCountryName()
+ };
+
+ String addressText = "";
+ for (int i = 0; i < parts.length; i++) {
+ if (parts[i] == null || parts[i].isEmpty()) continue;
+ if (!addressText.isEmpty()) {
+ addressText += ", ";
+ }
+ addressText += parts[i];
+ }
+ String text = String.format("%s : %s", DetailsHelper.getDetailsName(
+ context, MediaDetails.INDEX_LOCATION), addressText);
+ mListener.onAddressAvailable(text);
+ }
+ }
+
+ public void cancel() {
+ if (mAddressLookupJob != null) {
+ mAddressLookupJob.cancel();
+ mAddressLookupJob = null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsHelper.java b/src/com/android/gallery3d/ui/DetailsHelper.java
new file mode 100644
index 000000000..47296f655
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsHelper.java
@@ -0,0 +1,148 @@
+/*
+ * 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.Bitmap;
+import android.graphics.BitmapFactory;
+import android.view.View.MeasureSpec;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener;
+
+public class DetailsHelper {
+ private static DetailsAddressResolver sAddressResolver;
+ private DetailsViewContainer mContainer;
+
+ public interface DetailsSource {
+ public int size();
+ public int setIndex();
+ public MediaDetails getDetails();
+ }
+
+ public interface CloseListener {
+ public void onClose();
+ }
+
+ public interface DetailsViewContainer {
+ public void reloadDetails();
+ public void setCloseListener(CloseListener listener);
+ public void show();
+ public void hide();
+ }
+
+ public interface ResolutionResolvingListener {
+ public void onResolutionAvailable(int width, int height);
+ }
+
+ public DetailsHelper(AbstractGalleryActivity activity, GLView rootPane, DetailsSource source) {
+ mContainer = new DialogDetailsView(activity, source);
+ }
+
+ public void layout(int left, int top, int right, int bottom) {
+ if (mContainer instanceof GLView) {
+ GLView view = (GLView) mContainer;
+ view.measure(MeasureSpec.UNSPECIFIED,
+ MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.AT_MOST));
+ view.layout(0, top, view.getMeasuredWidth(), top + view.getMeasuredHeight());
+ }
+ }
+
+ public void reloadDetails() {
+ mContainer.reloadDetails();
+ }
+
+ public void setCloseListener(CloseListener listener) {
+ mContainer.setCloseListener(listener);
+ }
+
+ public static String resolveAddress(AbstractGalleryActivity activity, double[] latlng,
+ AddressResolvingListener listener) {
+ if (sAddressResolver == null) {
+ sAddressResolver = new DetailsAddressResolver(activity);
+ } else {
+ sAddressResolver.cancel();
+ }
+ return sAddressResolver.resolveAddress(latlng, listener);
+ }
+
+ public static void resolveResolution(String path, ResolutionResolvingListener listener) {
+ Bitmap bitmap = BitmapFactory.decodeFile(path);
+ if (bitmap == null) return;
+ listener.onResolutionAvailable(bitmap.getWidth(), bitmap.getHeight());
+ }
+
+ public static void pause() {
+ if (sAddressResolver != null) sAddressResolver.cancel();
+ }
+
+ public void show() {
+ mContainer.show();
+ }
+
+ public void hide() {
+ mContainer.hide();
+ }
+
+ public static String getDetailsName(Context context, int key) {
+ switch (key) {
+ case MediaDetails.INDEX_TITLE:
+ return context.getString(R.string.title);
+ case MediaDetails.INDEX_DESCRIPTION:
+ return context.getString(R.string.description);
+ case MediaDetails.INDEX_DATETIME:
+ return context.getString(R.string.time);
+ case MediaDetails.INDEX_LOCATION:
+ return context.getString(R.string.location);
+ case MediaDetails.INDEX_PATH:
+ return context.getString(R.string.path);
+ case MediaDetails.INDEX_WIDTH:
+ return context.getString(R.string.width);
+ case MediaDetails.INDEX_HEIGHT:
+ return context.getString(R.string.height);
+ case MediaDetails.INDEX_ORIENTATION:
+ return context.getString(R.string.orientation);
+ case MediaDetails.INDEX_DURATION:
+ return context.getString(R.string.duration);
+ case MediaDetails.INDEX_MIMETYPE:
+ return context.getString(R.string.mimetype);
+ case MediaDetails.INDEX_SIZE:
+ return context.getString(R.string.file_size);
+ case MediaDetails.INDEX_MAKE:
+ return context.getString(R.string.maker);
+ case MediaDetails.INDEX_MODEL:
+ return context.getString(R.string.model);
+ case MediaDetails.INDEX_FLASH:
+ return context.getString(R.string.flash);
+ case MediaDetails.INDEX_APERTURE:
+ return context.getString(R.string.aperture);
+ case MediaDetails.INDEX_FOCAL_LENGTH:
+ return context.getString(R.string.focal_length);
+ case MediaDetails.INDEX_WHITE_BALANCE:
+ return context.getString(R.string.white_balance);
+ case MediaDetails.INDEX_EXPOSURE_TIME:
+ return context.getString(R.string.exposure_time);
+ case MediaDetails.INDEX_ISO:
+ return context.getString(R.string.iso);
+ default:
+ return "Unknown key" + key;
+ }
+ }
+}
+
+
diff --git a/src/com/android/gallery3d/ui/DialogDetailsView.java b/src/com/android/gallery3d/ui/DialogDetailsView.java
new file mode 100644
index 000000000..058c03654
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DialogDetailsView.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2011 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.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
+import com.android.gallery3d.ui.DetailsHelper.DetailsViewContainer;
+import com.android.gallery3d.ui.DetailsHelper.ResolutionResolvingListener;
+
+import java.util.ArrayList;
+import java.util.Map.Entry;
+
+public class DialogDetailsView implements DetailsViewContainer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "DialogDetailsView";
+
+ private final AbstractGalleryActivity mActivity;
+ private DetailsAdapter mAdapter;
+ private MediaDetails mDetails;
+ private final DetailsSource mSource;
+ private int mIndex;
+ private Dialog mDialog;
+ private CloseListener mListener;
+
+ public DialogDetailsView(AbstractGalleryActivity activity, DetailsSource source) {
+ mActivity = activity;
+ mSource = source;
+ }
+
+ @Override
+ public void show() {
+ reloadDetails();
+ mDialog.show();
+ }
+
+ @Override
+ public void hide() {
+ mDialog.hide();
+ }
+
+ @Override
+ public void reloadDetails() {
+ int index = mSource.setIndex();
+ if (index == -1) return;
+ MediaDetails details = mSource.getDetails();
+ if (details != null) {
+ if (mIndex == index && mDetails == details) return;
+ mIndex = index;
+ mDetails = details;
+ setDetails(details);
+ }
+ }
+
+ private void setDetails(MediaDetails details) {
+ mAdapter = new DetailsAdapter(details);
+ String title = String.format(
+ mActivity.getAndroidContext().getString(R.string.details_title),
+ mIndex + 1, mSource.size());
+ ListView detailsList = (ListView) LayoutInflater.from(mActivity.getAndroidContext()).inflate(
+ R.layout.details_list, null, false);
+ detailsList.setAdapter(mAdapter);
+ mDialog = new AlertDialog.Builder(mActivity)
+ .setView(detailsList)
+ .setTitle(title)
+ .setPositiveButton(R.string.close, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mDialog.dismiss();
+ }
+ })
+ .create();
+
+ mDialog.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (mListener != null) {
+ mListener.onClose();
+ }
+ }
+ });
+ }
+
+
+ private class DetailsAdapter extends BaseAdapter
+ implements AddressResolvingListener, ResolutionResolvingListener {
+ private final ArrayList<String> mItems;
+ private int mLocationIndex;
+ private int mWidthIndex = -1;
+ private int mHeightIndex = -1;
+
+ public DetailsAdapter(MediaDetails details) {
+ Context context = mActivity.getAndroidContext();
+ mItems = new ArrayList<String>(details.size());
+ mLocationIndex = -1;
+ setDetails(context, details);
+ }
+
+ private void setDetails(Context context, MediaDetails details) {
+ boolean resolutionIsValid = true;
+ String path = null;
+ for (Entry<Integer, Object> detail : details) {
+ String value;
+ switch (detail.getKey()) {
+ case MediaDetails.INDEX_LOCATION: {
+ double[] latlng = (double[]) detail.getValue();
+ mLocationIndex = mItems.size();
+ value = DetailsHelper.resolveAddress(mActivity, latlng, this);
+ break;
+ }
+ case MediaDetails.INDEX_SIZE: {
+ value = Formatter.formatFileSize(
+ context, (Long) detail.getValue());
+ break;
+ }
+ case MediaDetails.INDEX_WHITE_BALANCE: {
+ value = "1".equals(detail.getValue())
+ ? context.getString(R.string.manual)
+ : context.getString(R.string.auto);
+ break;
+ }
+ case MediaDetails.INDEX_FLASH: {
+ MediaDetails.FlashState flash =
+ (MediaDetails.FlashState) detail.getValue();
+ // TODO: camera doesn't fill in the complete values, show more information
+ // when it is fixed.
+ if (flash.isFlashFired()) {
+ value = context.getString(R.string.flash_on);
+ } else {
+ value = context.getString(R.string.flash_off);
+ }
+ break;
+ }
+ case MediaDetails.INDEX_EXPOSURE_TIME: {
+ value = (String) detail.getValue();
+ double time = Double.valueOf(value);
+ if (time < 1.0f) {
+ value = String.format("1/%d", (int) (0.5f + 1 / time));
+ } else {
+ int integer = (int) time;
+ time -= integer;
+ value = String.valueOf(integer) + "''";
+ if (time > 0.0001) {
+ value += String.format(" 1/%d", (int) (0.5f + 1 / time));
+ }
+ }
+ break;
+ }
+ case MediaDetails.INDEX_WIDTH:
+ mWidthIndex = mItems.size();
+ value = detail.getValue().toString();
+ if (value.equalsIgnoreCase("0")) {
+ value = context.getString(R.string.unknown);
+ resolutionIsValid = false;
+ }
+ break;
+ case MediaDetails.INDEX_HEIGHT: {
+ mHeightIndex = mItems.size();
+ value = detail.getValue().toString();
+ if (value.equalsIgnoreCase("0")) {
+ value = context.getString(R.string.unknown);
+ resolutionIsValid = false;
+ }
+ break;
+ }
+ case MediaDetails.INDEX_PATH:
+ // Get the path and then fall through to the default case
+ path = detail.getValue().toString();
+ default: {
+ Object valueObj = detail.getValue();
+ // This shouldn't happen, log its key to help us diagnose the problem.
+ if (valueObj == null) {
+ Utils.fail("%s's value is Null",
+ DetailsHelper.getDetailsName(context, detail.getKey()));
+ }
+ value = valueObj.toString();
+ }
+ }
+ int key = detail.getKey();
+ if (details.hasUnit(key)) {
+ value = String.format("%s: %s %s", DetailsHelper.getDetailsName(
+ context, key), value, context.getString(details.getUnit(key)));
+ } else {
+ value = String.format("%s: %s", DetailsHelper.getDetailsName(
+ context, key), value);
+ }
+ mItems.add(value);
+ if (!resolutionIsValid) {
+ DetailsHelper.resolveResolution(path, this);
+ }
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mDetails.getDetail(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView tv;
+ if (convertView == null) {
+ tv = (TextView) LayoutInflater.from(mActivity.getAndroidContext()).inflate(
+ R.layout.details, parent, false);
+ } else {
+ tv = (TextView) convertView;
+ }
+ tv.setText(mItems.get(position));
+ return tv;
+ }
+
+ @Override
+ public void onAddressAvailable(String address) {
+ mItems.set(mLocationIndex, address);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onResolutionAvailable(int width, int height) {
+ if (width == 0 || height == 0) return;
+ // Update the resolution with the new width and height
+ Context context = mActivity.getAndroidContext();
+ String widthString = String.format("%s: %d", DetailsHelper.getDetailsName(
+ context, MediaDetails.INDEX_WIDTH), width);
+ String heightString = String.format("%s: %d", DetailsHelper.getDetailsName(
+ context, MediaDetails.INDEX_HEIGHT), height);
+ mItems.set(mWidthIndex, String.valueOf(widthString));
+ mItems.set(mHeightIndex, String.valueOf(heightString));
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void setCloseListener(CloseListener listener) {
+ mListener = listener;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java
new file mode 100644
index 000000000..19db77262
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DownUpDetector.java
@@ -0,0 +1,61 @@
+/*
+ * 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.view.MotionEvent;
+
+public class DownUpDetector {
+ public interface DownUpListener {
+ void onDown(MotionEvent e);
+ void onUp(MotionEvent e);
+ }
+
+ private boolean mStillDown;
+ private DownUpListener mListener;
+
+ public DownUpDetector(DownUpListener listener) {
+ mListener = listener;
+ }
+
+ private void setState(boolean down, MotionEvent e) {
+ if (down == mStillDown) return;
+ mStillDown = down;
+ if (down) {
+ mListener.onDown(e);
+ } else {
+ mListener.onUp(e);
+ }
+ }
+
+ public void onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ setState(true, ev);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort.
+ setState(false, ev);
+ break;
+ }
+ }
+
+ public boolean isDown() {
+ return mStillDown;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java
new file mode 100644
index 000000000..87ff0c5d3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeEffect.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2011 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.Canvas;
+import android.graphics.Rect;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+
+// This is copied from android.widget.EdgeEffect with some small modifications:
+// (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
+// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
+// (3) Use a private Drawable class (which inherits from ResourceTexture)
+// instead of android.graphics.drawable.Drawable to hold the images.
+// The private Drawable class is used to translate original Canvas calls to
+// corresponding GLCanvas calls.
+
+/**
+ * This class performs the graphical effect used at the edges of scrollable widgets
+ * when the user scrolls beyond the content bounds in 2D space.
+ *
+ * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
+ * instance for each edge that should show the effect, feed it input data using
+ * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
+ * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
+ * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
+ * false after drawing, the edge effect's animation is not yet complete and the widget
+ * should schedule another drawing pass to continue the animation.</p>
+ *
+ * <p>When drawing, widgets should draw their main content and child views first,
+ * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
+ * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
+ * The edge effect may then be drawn on top of the view's content using the
+ * {@link #draw(Canvas)} method.</p>
+ */
+public class EdgeEffect {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeEffect";
+
+ // Time it will take the effect to fully recede in ms
+ private static final int RECEDE_TIME = 1000;
+
+ // Time it will take before a pulled glow begins receding in ms
+ private static final int PULL_TIME = 167;
+
+ // Time it will take in ms for a pulled glow to decay to partial strength before release
+ private static final int PULL_DECAY_TIME = 1000;
+
+ private static final float MAX_ALPHA = 0.8f;
+ private static final float HELD_EDGE_ALPHA = 0.7f;
+ private static final float HELD_EDGE_SCALE_Y = 0.5f;
+ private static final float HELD_GLOW_ALPHA = 0.5f;
+ private static final float HELD_GLOW_SCALE_Y = 0.5f;
+
+ private static final float MAX_GLOW_HEIGHT = 4.f;
+
+ private static final float PULL_GLOW_BEGIN = 1.f;
+ private static final float PULL_EDGE_BEGIN = 0.6f;
+
+ // Minimum velocity that will be absorbed
+ private static final int MIN_VELOCITY = 100;
+
+ private static final float EPSILON = 0.001f;
+
+ private final Drawable mEdge;
+ private final Drawable mGlow;
+ private int mWidth;
+ private int mHeight;
+ private final int MIN_WIDTH = 300;
+ private final int mMinWidth;
+
+ private float mEdgeAlpha;
+ private float mEdgeScaleY;
+ private float mGlowAlpha;
+ private float mGlowScaleY;
+
+ private float mEdgeAlphaStart;
+ private float mEdgeAlphaFinish;
+ private float mEdgeScaleYStart;
+ private float mEdgeScaleYFinish;
+ private float mGlowAlphaStart;
+ private float mGlowAlphaFinish;
+ private float mGlowScaleYStart;
+ private float mGlowScaleYFinish;
+
+ private long mStartTime;
+ private float mDuration;
+
+ private final Interpolator mInterpolator;
+
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PULL = 1;
+ private static final int STATE_ABSORB = 2;
+ private static final int STATE_RECEDE = 3;
+ private static final int STATE_PULL_DECAY = 4;
+
+ // How much dragging should effect the height of the edge image.
+ // Number determined by user testing.
+ private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
+
+ // How much dragging should effect the height of the glow image.
+ // Number determined by user testing.
+ private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
+ private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
+
+ private static final int VELOCITY_EDGE_FACTOR = 8;
+ private static final int VELOCITY_GLOW_FACTOR = 16;
+
+ private int mState = STATE_IDLE;
+
+ private float mPullDistance;
+
+ /**
+ * Construct a new EdgeEffect with a theme appropriate for the provided context.
+ * @param context Context used to provide theming and resource information for the EdgeEffect
+ */
+ public EdgeEffect(Context context) {
+ mEdge = new Drawable(context, R.drawable.overscroll_edge);
+ mGlow = new Drawable(context, R.drawable.overscroll_glow);
+
+ mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f);
+ mInterpolator = new DecelerateInterpolator();
+ }
+
+ /**
+ * Set the size of this edge effect in pixels.
+ *
+ * @param width Effect width in pixels
+ * @param height Effect height in pixels
+ */
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Reports if this EdgeEffect's animation is finished. If this method returns false
+ * after a call to {@link #draw(Canvas)} the host widget should schedule another
+ * drawing pass to continue the animation.
+ *
+ * @return true if animation is finished, false if drawing should continue on the next frame.
+ */
+ public boolean isFinished() {
+ return mState == STATE_IDLE;
+ }
+
+ /**
+ * Immediately finish the current animation.
+ * After this call {@link #isFinished()} will return true.
+ */
+ public void finish() {
+ mState = STATE_IDLE;
+ }
+
+ /**
+ * A view should call this when content is pulled away from an edge by the user.
+ * This will update the state of the current visual effect and its associated animation.
+ * The host view should always {@link android.view.View#invalidate()} after this
+ * and draw the results accordingly.
+ *
+ * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+ * 1.f (full length of the view) or negative values to express change
+ * back toward the edge reached to initiate the effect.
+ */
+ public void onPull(float deltaDistance) {
+ final long now = AnimationTime.get();
+ if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
+ return;
+ }
+ if (mState != STATE_PULL) {
+ mGlowScaleY = PULL_GLOW_BEGIN;
+ }
+ mState = STATE_PULL;
+
+ mStartTime = now;
+ mDuration = PULL_TIME;
+
+ mPullDistance += deltaDistance;
+ float distance = Math.abs(mPullDistance);
+
+ mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
+ mEdgeScaleY = mEdgeScaleYStart = Math.max(
+ HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
+
+ mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
+ mGlowAlpha +
+ (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
+
+ float glowChange = Math.abs(deltaDistance);
+ if (deltaDistance > 0 && mPullDistance < 0) {
+ glowChange = -glowChange;
+ }
+ if (mPullDistance == 0) {
+ mGlowScaleY = 0;
+ }
+
+ // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
+ mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
+ 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
+
+ mEdgeAlphaFinish = mEdgeAlpha;
+ mEdgeScaleYFinish = mEdgeScaleY;
+ mGlowAlphaFinish = mGlowAlpha;
+ mGlowScaleYFinish = mGlowScaleY;
+ }
+
+ /**
+ * Call when the object is released after being pulled.
+ * This will begin the "decay" phase of the effect. After calling this method
+ * the host view should {@link android.view.View#invalidate()} and thereby
+ * draw the results accordingly.
+ */
+ public void onRelease() {
+ mPullDistance = 0;
+
+ if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
+ return;
+ }
+
+ mState = STATE_RECEDE;
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+
+ mStartTime = AnimationTime.get();
+ mDuration = RECEDE_TIME;
+ }
+
+ /**
+ * Call when the effect absorbs an impact at the given velocity.
+ * Used when a fling reaches the scroll boundary.
+ *
+ * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+ * the method <code>getCurrVelocity</code> will provide a reasonable approximation
+ * to use here.</p>
+ *
+ * @param velocity Velocity at impact in pixels per second.
+ */
+ public void onAbsorb(int velocity) {
+ mState = STATE_ABSORB;
+ velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
+
+ mStartTime = AnimationTime.get();
+ mDuration = 0.1f + (velocity * 0.03f);
+
+ // The edge should always be at least partially visible, regardless
+ // of velocity.
+ mEdgeAlphaStart = 0.f;
+ mEdgeScaleY = mEdgeScaleYStart = 0.f;
+ // The glow depends more on the velocity, and therefore starts out
+ // nearly invisible.
+ mGlowAlphaStart = 0.5f;
+ mGlowScaleYStart = 0.f;
+
+ // Factor the velocity by 8. Testing on device shows this works best to
+ // reflect the strength of the user's scrolling.
+ mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
+ // Edge should never get larger than the size of its asset.
+ mEdgeScaleYFinish = Math.max(
+ HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
+
+ // Growth for the size of the glow should be quadratic to properly
+ // respond
+ // to a user's scrolling speed. The faster the scrolling speed, the more
+ // intense the effect should be for both the size and the saturation.
+ mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
+ // Alpha should change for the glow as well as size.
+ mGlowAlphaFinish = Math.max(
+ mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
+ }
+
+
+ /**
+ * Draw into the provided canvas. Assumes that the canvas has been rotated
+ * accordingly and the size has been set. The effect will be drawn the full
+ * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+ * 1.f of height.
+ *
+ * @param canvas Canvas to draw into
+ * @return true if drawing should continue beyond this frame to continue the
+ * animation
+ */
+ public boolean draw(GLCanvas canvas) {
+ update();
+
+ final int edgeHeight = mEdge.getIntrinsicHeight();
+ final int edgeWidth = mEdge.getIntrinsicWidth();
+ final int glowHeight = mGlow.getIntrinsicHeight();
+ final int glowWidth = mGlow.getIntrinsicWidth();
+
+ mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
+
+ int glowBottom = (int) Math.min(
+ glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f,
+ glowHeight * MAX_GLOW_HEIGHT);
+ if (mWidth < mMinWidth) {
+ // Center the glow and clip it.
+ int glowLeft = (mWidth - mMinWidth)/2;
+ mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
+ } else {
+ // Stretch the glow to fit.
+ mGlow.setBounds(0, 0, mWidth, glowBottom);
+ }
+
+ mGlow.draw(canvas);
+
+ mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
+
+ int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
+ if (mWidth < mMinWidth) {
+ // Center the edge and clip it.
+ int edgeLeft = (mWidth - mMinWidth)/2;
+ mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
+ } else {
+ // Stretch the edge to fit.
+ mEdge.setBounds(0, 0, mWidth, edgeBottom);
+ }
+ mEdge.draw(canvas);
+
+ return mState != STATE_IDLE;
+ }
+
+ private void update() {
+ final long time = AnimationTime.get();
+ final float t = Math.min((time - mStartTime) / mDuration, 1.f);
+
+ final float interp = mInterpolator.getInterpolation(t);
+
+ mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
+ mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
+ mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
+ mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
+
+ if (t >= 1.f - EPSILON) {
+ switch (mState) {
+ case STATE_ABSORB:
+ mState = STATE_RECEDE;
+ mStartTime = AnimationTime.get();
+ mDuration = RECEDE_TIME;
+
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After absorb, the glow and edge should fade to nothing.
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL:
+ mState = STATE_PULL_DECAY;
+ mStartTime = AnimationTime.get();
+ mDuration = PULL_DECAY_TIME;
+
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After pull, the glow and edge should fade to nothing.
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL_DECAY:
+ // When receding, we want edge to decrease more slowly
+ // than the glow.
+ float factor = mGlowScaleYFinish != 0 ? 1
+ / (mGlowScaleYFinish * mGlowScaleYFinish)
+ : Float.MAX_VALUE;
+ mEdgeScaleY = mEdgeScaleYStart +
+ (mEdgeScaleYFinish - mEdgeScaleYStart) *
+ interp * factor;
+ mState = STATE_RECEDE;
+ break;
+ case STATE_RECEDE:
+ mState = STATE_IDLE;
+ break;
+ }
+ }
+ }
+
+ private static class Drawable extends ResourceTexture {
+ private Rect mBounds = new Rect();
+ private int mAlpha = 255;
+
+ public Drawable(Context context, int resId) {
+ super(context, resId);
+ }
+
+ public int getIntrinsicWidth() {
+ return getWidth();
+ }
+
+ public int getIntrinsicHeight() {
+ return getHeight();
+ }
+
+ public void setBounds(int left, int top, int right, int bottom) {
+ mBounds.set(left, top, right, bottom);
+ }
+
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ }
+
+ public void draw(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(mAlpha / 255.0f);
+ Rect b = mBounds;
+ draw(canvas, b.left, b.top, b.width(), b.height());
+ canvas.restore();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java
new file mode 100644
index 000000000..051de18fa
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeView.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 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.opengl.Matrix;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+// EdgeView draws EdgeEffect (blue glow) at four sides of the view.
+public class EdgeView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeView";
+
+ public static final int INVALID_DIRECTION = -1;
+ public static final int TOP = 0;
+ public static final int LEFT = 1;
+ public static final int BOTTOM = 2;
+ public static final int RIGHT = 3;
+
+ // Each edge effect has a transform matrix, and each matrix has 16 elements.
+ // We put all the elements in one array. These constants specify the
+ // starting index of each matrix.
+ private static final int TOP_M = TOP * 16;
+ private static final int LEFT_M = LEFT * 16;
+ private static final int BOTTOM_M = BOTTOM * 16;
+ private static final int RIGHT_M = RIGHT * 16;
+
+ private EdgeEffect[] mEffect = new EdgeEffect[4];
+ private float[] mMatrix = new float[4 * 16];
+
+ public EdgeView(Context context) {
+ for (int i = 0; i < 4; i++) {
+ mEffect[i] = new EdgeEffect(context);
+ }
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ if (!changeSize) return;
+
+ int w = right - left;
+ int h = bottom - top;
+ for (int i = 0; i < 4; i++) {
+ if ((i & 1) == 0) { // top or bottom
+ mEffect[i].setSize(w, h);
+ } else { // left or right
+ mEffect[i].setSize(h, w);
+ }
+ }
+
+ // Set up transforms for the four edges. Without transforms an
+ // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y
+ // is some factor < 1. For other edges we need to move, rotate, and
+ // flip the effects into proper places.
+ Matrix.setIdentityM(mMatrix, TOP_M);
+ Matrix.setIdentityM(mMatrix, LEFT_M);
+ Matrix.setIdentityM(mMatrix, BOTTOM_M);
+ Matrix.setIdentityM(mMatrix, RIGHT_M);
+
+ Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1);
+ Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1);
+
+ Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0);
+ Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1);
+
+ Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0);
+ Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ boolean more = false;
+ for (int i = 0; i < 4; i++) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, i * 16);
+ more |= mEffect[i].draw(canvas);
+ canvas.restore();
+ }
+ if (more) {
+ invalidate();
+ }
+ }
+
+ // Called when the content is pulled away from the edge.
+ // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+ public void onPull(int offset, int direction) {
+ int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight();
+ mEffect[direction].onPull((float)offset / fullLength);
+ if (!mEffect[direction].isFinished()) {
+ invalidate();
+ }
+ }
+
+ // Call when the object is released after being pulled.
+ public void onRelease() {
+ boolean more = false;
+ for (int i = 0; i < 4; i++) {
+ mEffect[i].onRelease();
+ more |= !mEffect[i].isFinished();
+ }
+ if (more) {
+ invalidate();
+ }
+ }
+
+ // Call when the effect absorbs an impact at the given velocity.
+ // Used when a fling reaches the scroll boundary. velocity is in pixels
+ // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+ public void onAbsorb(int velocity, int direction) {
+ mEffect[direction].onAbsorb(velocity);
+ if (!mEffect[direction].isFinished()) {
+ invalidate();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java
new file mode 100644
index 000000000..6f98c64f9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FlingScroller.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2011 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;
+
+
+// This is a customized version of Scroller, with a interface similar to
+// android.widget.Scroller. It does fling only, not scroll.
+//
+// The differences between the this Scroller and the system one are:
+//
+// (1) The velocity does not change because of min/max limit.
+// (2) The duration is different.
+// (3) The deceleration curve is different.
+class FlingScroller {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FlingController";
+
+ // The fling duration (in milliseconds) when velocity is 1 pixel/second
+ private static final float FLING_DURATION_PARAM = 50f;
+ private static final int DECELERATED_FACTOR = 4;
+
+ private int mStartX, mStartY;
+ private int mMinX, mMinY, mMaxX, mMaxY;
+ private double mSinAngle;
+ private double mCosAngle;
+ private int mDuration;
+ private int mDistance;
+ private int mFinalX, mFinalY;
+
+ private int mCurrX, mCurrY;
+ private double mCurrV;
+
+ public int getFinalX() {
+ return mFinalX;
+ }
+
+ public int getFinalY() {
+ return mFinalY;
+ }
+
+ public int getDuration() {
+ return mDuration;
+ }
+
+ public int getCurrX() {
+ return mCurrX;
+
+ }
+
+ public int getCurrY() {
+ return mCurrY;
+ }
+
+ public int getCurrVelocityX() {
+ return (int)Math.round(mCurrV * mCosAngle);
+ }
+
+ public int getCurrVelocityY() {
+ return (int)Math.round(mCurrV * mSinAngle);
+ }
+
+ public void fling(int startX, int startY, int velocityX, int velocityY,
+ int minX, int maxX, int minY, int maxY) {
+ mStartX = startX;
+ mStartY = startY;
+ mMinX = minX;
+ mMinY = minY;
+ mMaxX = maxX;
+ mMaxY = maxY;
+
+ double velocity = Math.hypot(velocityX, velocityY);
+ mSinAngle = velocityY / velocity;
+ mCosAngle = velocityX / velocity;
+ //
+ // The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d)
+ // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+ // Thus,
+ // v0 = d * (e - s) / T => (e - s) = v0 * T / d
+ //
+
+ // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second;
+ mDuration = (int)Math.round(FLING_DURATION_PARAM
+ * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1)));
+
+ // (e - s) = v0 * T / d
+ mDistance = (int)Math.round(
+ velocity * mDuration / DECELERATED_FACTOR / 1000);
+
+ mFinalX = getX(1.0f);
+ mFinalY = getY(1.0f);
+ }
+
+ public void computeScrollOffset(float progress) {
+ progress = Math.min(progress, 1);
+ float f = 1 - progress;
+ f = 1 - (float) Math.pow(f, DECELERATED_FACTOR);
+ mCurrX = getX(f);
+ mCurrY = getY(f);
+ mCurrV = getV(progress);
+ }
+
+ private int getX(float f) {
+ int r = (int) Math.round(mStartX + f * mDistance * mCosAngle);
+ if (mCosAngle > 0 && mStartX <= mMaxX) {
+ r = Math.min(r, mMaxX);
+ } else if (mCosAngle < 0 && mStartX >= mMinX) {
+ r = Math.max(r, mMinX);
+ }
+ return r;
+ }
+
+ private int getY(float f) {
+ int r = (int) Math.round(mStartY + f * mDistance * mSinAngle);
+ if (mSinAngle > 0 && mStartY <= mMaxY) {
+ r = Math.min(r, mMaxY);
+ } else if (mSinAngle < 0 && mStartY >= mMinY) {
+ r = Math.max(r, mMinY);
+ }
+ return r;
+ }
+
+ private double getV(float progress) {
+ // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+ return DECELERATED_FACTOR * mDistance * 1000 *
+ Math.pow(1 - progress, DECELERATED_FACTOR - 1) / mDuration;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java
new file mode 100644
index 000000000..33a82eaf7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRoot.java
@@ -0,0 +1,53 @@
+/*
+ * 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.Matrix;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public interface GLRoot {
+
+ // Listener will be called when GL is idle AND before each frame.
+ // Mainly used for uploading textures.
+ public static interface OnGLIdleListener {
+ public boolean onGLIdle(
+ GLCanvas canvas, boolean renderRequested);
+ }
+
+ public void addOnGLIdleListener(OnGLIdleListener listener);
+ public void registerLaunchedAnimation(CanvasAnimation animation);
+ public void requestRenderForced();
+ public void requestRender();
+ public void requestLayoutContentPane();
+
+ public void lockRenderThread();
+ public void unlockRenderThread();
+
+ public void setContentPane(GLView content);
+ public void setOrientationSource(OrientationSource source);
+ public int getDisplayRotation();
+ public int getCompensation();
+ public Matrix getCompensationMatrix();
+ public void freeze();
+ public void unfreeze();
+ public void setLightsOutMode(boolean enabled);
+
+ public Context getContext();
+}
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
new file mode 100644
index 000000000..dc898d83d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -0,0 +1,630 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.GLES11Canvas;
+import com.android.gallery3d.glrenderer.GLES20Canvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MotionEventHelper;
+import com.android.gallery3d.util.Profile;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+// The root component of all <code>GLView</code>s. The rendering is done in GL
+// thread while the event handling is done in the main thread. To synchronize
+// the two threads, the entry points of this package need to synchronize on the
+// <code>GLRootView</code> instance unless it can be proved that the rendering
+// thread won't access the same thing as the method. The entry points include:
+// (1) The public methods of HeadUpDisplay
+// (2) The public methods of CameraHeadUpDisplay
+// (3) The overridden methods in GLRootView.
+public class GLRootView extends GLSurfaceView
+ implements GLSurfaceView.Renderer, GLRoot {
+ private static final String TAG = "GLRootView";
+
+ private static final boolean DEBUG_FPS = false;
+ private int mFrameCount = 0;
+ private long mFrameCountingStart = 0;
+
+ private static final boolean DEBUG_INVALIDATE = false;
+ private int mInvalidateColor = 0;
+
+ private static final boolean DEBUG_DRAWING_STAT = false;
+
+ private static final boolean DEBUG_PROFILE = false;
+ private static final boolean DEBUG_PROFILE_SLOW_ONLY = false;
+
+ private static final int FLAG_INITIALIZED = 1;
+ private static final int FLAG_NEED_LAYOUT = 2;
+
+ private GL11 mGL;
+ private GLCanvas mCanvas;
+ private GLView mContentView;
+
+ private OrientationSource mOrientationSource;
+ // mCompensation is the difference between the UI orientation on GLCanvas
+ // and the framework orientation. See OrientationManager for details.
+ private int mCompensation;
+ // mCompensationMatrix maps the coordinates of touch events. It is kept sync
+ // with mCompensation.
+ private Matrix mCompensationMatrix = new Matrix();
+ private int mDisplayRotation;
+
+ private int mFlags = FLAG_NEED_LAYOUT;
+ private volatile boolean mRenderRequested = false;
+
+ private final ArrayList<CanvasAnimation> mAnimations =
+ new ArrayList<CanvasAnimation>();
+
+ private final ArrayDeque<OnGLIdleListener> mIdleListeners =
+ new ArrayDeque<OnGLIdleListener>();
+
+ private final IdleRunner mIdleRunner = new IdleRunner();
+
+ private final ReentrantLock mRenderLock = new ReentrantLock();
+ private final Condition mFreezeCondition =
+ mRenderLock.newCondition();
+ private boolean mFreeze;
+
+ private long mLastDrawFinishTime;
+ private boolean mInDownState = false;
+ private boolean mFirstDraw = true;
+
+ public GLRootView(Context context) {
+ this(context, null);
+ }
+
+ public GLRootView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mFlags |= FLAG_INITIALIZED;
+ setBackgroundDrawable(null);
+ setEGLContextClientVersion(ApiHelper.HAS_GLES20_REQUIRED ? 2 : 1);
+ if (ApiHelper.USE_888_PIXEL_FORMAT) {
+ setEGLConfigChooser(8, 8, 8, 0, 0, 0);
+ } else {
+ setEGLConfigChooser(5, 6, 5, 0, 0, 0);
+ }
+ setRenderer(this);
+ if (ApiHelper.USE_888_PIXEL_FORMAT) {
+ getHolder().setFormat(PixelFormat.RGB_888);
+ } else {
+ getHolder().setFormat(PixelFormat.RGB_565);
+ }
+
+ // Uncomment this to enable gl error check.
+ // setDebugFlags(DEBUG_CHECK_GL_ERROR);
+ }
+
+ @Override
+ public void registerLaunchedAnimation(CanvasAnimation animation) {
+ // Register the newly launched animation so that we can set the start
+ // time more precisely. (Usually, it takes much longer for first
+ // rendering, so we set the animation start time as the time we
+ // complete rendering)
+ mAnimations.add(animation);
+ }
+
+ @Override
+ public void addOnGLIdleListener(OnGLIdleListener listener) {
+ synchronized (mIdleListeners) {
+ mIdleListeners.addLast(listener);
+ mIdleRunner.enable();
+ }
+ }
+
+ @Override
+ public void setContentPane(GLView content) {
+ if (mContentView == content) return;
+ if (mContentView != null) {
+ if (mInDownState) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mContentView.dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ mInDownState = false;
+ }
+ mContentView.detachFromRoot();
+ BasicTexture.yieldAllTextures();
+ }
+ mContentView = content;
+ if (content != null) {
+ content.attachToRoot(this);
+ requestLayoutContentPane();
+ }
+ }
+
+ @Override
+ public void requestRenderForced() {
+ superRequestRender();
+ }
+
+ @Override
+ public void requestRender() {
+ if (DEBUG_INVALIDATE) {
+ StackTraceElement e = Thread.currentThread().getStackTrace()[4];
+ String caller = e.getFileName() + ":" + e.getLineNumber() + " ";
+ Log.d(TAG, "invalidate: " + caller);
+ }
+ if (mRenderRequested) return;
+ mRenderRequested = true;
+ if (ApiHelper.HAS_POST_ON_ANIMATION) {
+ postOnAnimation(mRequestRenderOnAnimationFrame);
+ } else {
+ super.requestRender();
+ }
+ }
+
+ private Runnable mRequestRenderOnAnimationFrame = new Runnable() {
+ @Override
+ public void run() {
+ superRequestRender();
+ }
+ };
+
+ private void superRequestRender() {
+ super.requestRender();
+ }
+
+ @Override
+ public void requestLayoutContentPane() {
+ mRenderLock.lock();
+ try {
+ if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return;
+
+ // "View" system will invoke onLayout() for initialization(bug ?), we
+ // have to ignore it since the GLThread is not ready yet.
+ if ((mFlags & FLAG_INITIALIZED) == 0) return;
+
+ mFlags |= FLAG_NEED_LAYOUT;
+ requestRender();
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ private void layoutContentPane() {
+ mFlags &= ~FLAG_NEED_LAYOUT;
+
+ int w = getWidth();
+ int h = getHeight();
+ int displayRotation = 0;
+ int compensation = 0;
+
+ // Get the new orientation values
+ if (mOrientationSource != null) {
+ displayRotation = mOrientationSource.getDisplayRotation();
+ compensation = mOrientationSource.getCompensation();
+ } else {
+ displayRotation = 0;
+ compensation = 0;
+ }
+
+ if (mCompensation != compensation) {
+ mCompensation = compensation;
+ if (mCompensation % 180 != 0) {
+ mCompensationMatrix.setRotate(mCompensation);
+ // move center to origin before rotation
+ mCompensationMatrix.preTranslate(-w / 2, -h / 2);
+ // align with the new origin after rotation
+ mCompensationMatrix.postTranslate(h / 2, w / 2);
+ } else {
+ mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2);
+ }
+ }
+ mDisplayRotation = displayRotation;
+
+ // Do the actual layout.
+ if (mCompensation % 180 != 0) {
+ int tmp = w;
+ w = h;
+ h = tmp;
+ }
+ Log.i(TAG, "layout content pane " + w + "x" + h
+ + " (compensation " + mCompensation + ")");
+ if (mContentView != null && w != 0 && h != 0) {
+ mContentView.layout(0, 0, w, h);
+ }
+ // Uncomment this to dump the view hierarchy.
+ //mContentView.dumpTree("");
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (changed) requestLayoutContentPane();
+ }
+
+ /**
+ * Called when the context is created, possibly after automatic destruction.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
+ GL11 gl = (GL11) gl1;
+ if (mGL != null) {
+ // The GL Object has changed
+ Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
+ }
+ mRenderLock.lock();
+ try {
+ mGL = gl;
+ mCanvas = ApiHelper.HAS_GLES20_REQUIRED ? new GLES20Canvas() : new GLES11Canvas(gl);
+ BasicTexture.invalidateAllTextures();
+ } finally {
+ mRenderLock.unlock();
+ }
+
+ if (DEBUG_FPS || DEBUG_PROFILE) {
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ } else {
+ setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ }
+ }
+
+ /**
+ * Called when the OpenGL surface is recreated without destroying the
+ * context.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceChanged(GL10 gl1, int width, int height) {
+ Log.i(TAG, "onSurfaceChanged: " + width + "x" + height
+ + ", gl10: " + gl1.toString());
+ Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
+ GalleryUtils.setRenderThread();
+ if (DEBUG_PROFILE) {
+ Log.d(TAG, "Start profiling");
+ Profile.enable(20); // take a sample every 20ms
+ }
+ GL11 gl = (GL11) gl1;
+ Utils.assertTrue(mGL == gl);
+
+ mCanvas.setSize(width, height);
+ }
+
+ private void outputFps() {
+ long now = System.nanoTime();
+ if (mFrameCountingStart == 0) {
+ mFrameCountingStart = now;
+ } else if ((now - mFrameCountingStart) > 1000000000) {
+ Log.d(TAG, "fps: " + (double) mFrameCount
+ * 1000000000 / (now - mFrameCountingStart));
+ mFrameCountingStart = now;
+ mFrameCount = 0;
+ }
+ ++mFrameCount;
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ AnimationTime.update();
+ long t0;
+ if (DEBUG_PROFILE_SLOW_ONLY) {
+ Profile.hold();
+ t0 = System.nanoTime();
+ }
+ mRenderLock.lock();
+
+ while (mFreeze) {
+ mFreezeCondition.awaitUninterruptibly();
+ }
+
+ try {
+ onDrawFrameLocked(gl);
+ } finally {
+ mRenderLock.unlock();
+ }
+
+ // We put a black cover View in front of the SurfaceView and hide it
+ // after the first draw. This prevents the SurfaceView being transparent
+ // before the first draw.
+ if (mFirstDraw) {
+ mFirstDraw = false;
+ post(new Runnable() {
+ @Override
+ public void run() {
+ View root = getRootView();
+ View cover = root.findViewById(R.id.gl_root_cover);
+ cover.setVisibility(GONE);
+ }
+ });
+ }
+
+ if (DEBUG_PROFILE_SLOW_ONLY) {
+ long t = System.nanoTime();
+ long durationInMs = (t - mLastDrawFinishTime) / 1000000;
+ long durationDrawInMs = (t - t0) / 1000000;
+ mLastDrawFinishTime = t;
+
+ if (durationInMs > 34) { // 34ms -> we skipped at least 2 frames
+ Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" +
+ durationInMs + ") -----");
+ Profile.commit();
+ } else {
+ Profile.drop();
+ }
+ }
+ }
+
+ private void onDrawFrameLocked(GL10 gl) {
+ if (DEBUG_FPS) outputFps();
+
+ // release the unbound textures and deleted buffers.
+ mCanvas.deleteRecycledResources();
+
+ // reset texture upload limit
+ UploadedTexture.resetUploadLimit();
+
+ mRenderRequested = false;
+
+ if ((mOrientationSource != null
+ && mDisplayRotation != mOrientationSource.getDisplayRotation())
+ || (mFlags & FLAG_NEED_LAYOUT) != 0) {
+ layoutContentPane();
+ }
+
+ mCanvas.save(GLCanvas.SAVE_FLAG_ALL);
+ rotateCanvas(-mCompensation);
+ if (mContentView != null) {
+ mContentView.render(mCanvas);
+ } else {
+ // Make sure we always draw something to prevent displaying garbage
+ mCanvas.clearBuffer();
+ }
+ mCanvas.restore();
+
+ if (!mAnimations.isEmpty()) {
+ long now = AnimationTime.get();
+ for (int i = 0, n = mAnimations.size(); i < n; i++) {
+ mAnimations.get(i).setStartTime(now);
+ }
+ mAnimations.clear();
+ }
+
+ if (UploadedTexture.uploadLimitReached()) {
+ requestRender();
+ }
+
+ synchronized (mIdleListeners) {
+ if (!mIdleListeners.isEmpty()) mIdleRunner.enable();
+ }
+
+ if (DEBUG_INVALIDATE) {
+ mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);
+ mInvalidateColor = ~mInvalidateColor;
+ }
+
+ if (DEBUG_DRAWING_STAT) {
+ mCanvas.dumpStatisticsAndClear();
+ }
+ }
+
+ private void rotateCanvas(int degrees) {
+ if (degrees == 0) return;
+ int w = getWidth();
+ int h = getHeight();
+ int cx = w / 2;
+ int cy = h / 2;
+ mCanvas.translate(cx, cy);
+ mCanvas.rotate(degrees, 0, 0, 1);
+ if (degrees % 180 != 0) {
+ mCanvas.translate(-cy, -cx);
+ } else {
+ mCanvas.translate(-cx, -cy);
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (!isEnabled()) return false;
+
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mInDownState = false;
+ } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+
+ if (mCompensation != 0) {
+ event = MotionEventHelper.transformEvent(event, mCompensationMatrix);
+ }
+
+ mRenderLock.lock();
+ try {
+ // If this has been detached from root, we don't need to handle event
+ boolean handled = mContentView != null
+ && mContentView.dispatchTouchEvent(event);
+ if (action == MotionEvent.ACTION_DOWN && handled) {
+ mInDownState = true;
+ }
+ return handled;
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ private class IdleRunner implements Runnable {
+ // true if the idle runner is in the queue
+ private boolean mActive = false;
+
+ @Override
+ public void run() {
+ OnGLIdleListener listener;
+ synchronized (mIdleListeners) {
+ mActive = false;
+ if (mIdleListeners.isEmpty()) return;
+ listener = mIdleListeners.removeFirst();
+ }
+ mRenderLock.lock();
+ boolean keepInQueue;
+ try {
+ keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested);
+ } finally {
+ mRenderLock.unlock();
+ }
+ synchronized (mIdleListeners) {
+ if (keepInQueue) mIdleListeners.addLast(listener);
+ if (!mRenderRequested && !mIdleListeners.isEmpty()) enable();
+ }
+ }
+
+ public void enable() {
+ // Who gets the flag can add it to the queue
+ if (mActive) return;
+ mActive = true;
+ queueEvent(this);
+ }
+ }
+
+ @Override
+ public void lockRenderThread() {
+ mRenderLock.lock();
+ }
+
+ @Override
+ public void unlockRenderThread() {
+ mRenderLock.unlock();
+ }
+
+ @Override
+ public void onPause() {
+ unfreeze();
+ super.onPause();
+ if (DEBUG_PROFILE) {
+ Log.d(TAG, "Stop profiling");
+ Profile.disableAll();
+ Profile.dumpToFile("/sdcard/gallery.prof");
+ Profile.reset();
+ }
+ }
+
+ @Override
+ public void setOrientationSource(OrientationSource source) {
+ mOrientationSource = source;
+ }
+
+ @Override
+ public int getDisplayRotation() {
+ return mDisplayRotation;
+ }
+
+ @Override
+ public int getCompensation() {
+ return mCompensation;
+ }
+
+ @Override
+ public Matrix getCompensationMatrix() {
+ return mCompensationMatrix;
+ }
+
+ @Override
+ public void freeze() {
+ mRenderLock.lock();
+ mFreeze = true;
+ mRenderLock.unlock();
+ }
+
+ @Override
+ public void unfreeze() {
+ mRenderLock.lock();
+ mFreeze = false;
+ mFreezeCondition.signalAll();
+ mRenderLock.unlock();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public void setLightsOutMode(boolean enabled) {
+ if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return;
+
+ int flags = 0;
+ if (enabled) {
+ flags = STATUS_BAR_HIDDEN;
+ if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
+ flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+ setSystemUiVisibility(flags);
+ }
+
+ // We need to unfreeze in the following methods and in onPause().
+ // These methods will wait on GLThread. If we have freezed the GLRootView,
+ // the GLThread will wait on main thread to call unfreeze and cause dead
+ // lock.
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ unfreeze();
+ super.surfaceChanged(holder, format, w, h);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ unfreeze();
+ super.surfaceCreated(holder);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ unfreeze();
+ super.surfaceDestroyed(holder);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ unfreeze();
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ unfreeze();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
new file mode 100644
index 000000000..83de19fe4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -0,0 +1,465 @@
+/*
+ * 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.graphics.Rect;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+import java.util.ArrayList;
+
+// GLView is a UI component. It can render to a GLCanvas and accept touch
+// events. A GLView may have zero or more child GLView and they form a tree
+// structure. The rendering and event handling will pass through the tree
+// structure.
+//
+// A GLView tree should be attached to a GLRoot before event dispatching and
+// rendering happens. GLView asks GLRoot to re-render or re-layout the
+// GLView hierarchy using requestRender() and requestLayoutContentPane().
+//
+// The render() method is called in a separate thread. Before calling
+// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the
+// rendering thread running at the same time. If there are other entry points
+// from main thread (like a Handler) in your GLView, you need to call
+// lockRendering() if the rendering thread should not run at the same time.
+//
+public class GLView {
+ private static final String TAG = "GLView";
+
+ public static final int VISIBLE = 0;
+ public static final int INVISIBLE = 1;
+
+ private static final int FLAG_INVISIBLE = 1;
+ private static final int FLAG_SET_MEASURED_SIZE = 2;
+ private static final int FLAG_LAYOUT_REQUESTED = 4;
+
+ public interface OnClickListener {
+ void onClick(GLView v);
+ }
+
+ protected final Rect mBounds = new Rect();
+ protected final Rect mPaddings = new Rect();
+
+ private GLRoot mRoot;
+ protected GLView mParent;
+ private ArrayList<GLView> mComponents;
+ private GLView mMotionTarget;
+
+ private CanvasAnimation mAnimation;
+
+ private int mViewFlags = 0;
+
+ protected int mMeasuredWidth = 0;
+ protected int mMeasuredHeight = 0;
+
+ private int mLastWidthSpec = -1;
+ private int mLastHeightSpec = -1;
+
+ protected int mScrollY = 0;
+ protected int mScrollX = 0;
+ protected int mScrollHeight = 0;
+ protected int mScrollWidth = 0;
+
+ private float [] mBackgroundColor;
+ private StateTransitionAnimation mTransition;
+
+ public void startAnimation(CanvasAnimation animation) {
+ GLRoot root = getGLRoot();
+ if (root == null) throw new IllegalStateException();
+ mAnimation = animation;
+ if (mAnimation != null) {
+ mAnimation.start();
+ root.registerLaunchedAnimation(mAnimation);
+ }
+ invalidate();
+ }
+
+ // Sets the visiblity of this GLView (either GLView.VISIBLE or
+ // GLView.INVISIBLE).
+ public void setVisibility(int visibility) {
+ if (visibility == getVisibility()) return;
+ if (visibility == VISIBLE) {
+ mViewFlags &= ~FLAG_INVISIBLE;
+ } else {
+ mViewFlags |= FLAG_INVISIBLE;
+ }
+ onVisibilityChanged(visibility);
+ invalidate();
+ }
+
+ // Returns GLView.VISIBLE or GLView.INVISIBLE
+ public int getVisibility() {
+ return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void attachToRoot(GLRoot root) {
+ Utils.assertTrue(mParent == null && mRoot == null);
+ onAttachToRoot(root);
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void detachFromRoot() {
+ Utils.assertTrue(mParent == null && mRoot != null);
+ onDetachFromRoot();
+ }
+
+ // Returns the number of children of the GLView.
+ public int getComponentCount() {
+ return mComponents == null ? 0 : mComponents.size();
+ }
+
+ // Returns the children for the given index.
+ public GLView getComponent(int index) {
+ if (mComponents == null) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mComponents.get(index);
+ }
+
+ // Adds a child to this GLView.
+ public void addComponent(GLView component) {
+ // Make sure the component doesn't have a parent currently.
+ if (component.mParent != null) throw new IllegalStateException();
+
+ // Build parent-child links
+ if (mComponents == null) {
+ mComponents = new ArrayList<GLView>();
+ }
+ mComponents.add(component);
+ component.mParent = this;
+
+ // If this is added after we have a root, tell the component.
+ if (mRoot != null) {
+ component.onAttachToRoot(mRoot);
+ }
+ }
+
+ // Removes a child from this GLView.
+ public boolean removeComponent(GLView component) {
+ if (mComponents == null) return false;
+ if (mComponents.remove(component)) {
+ removeOneComponent(component);
+ return true;
+ }
+ return false;
+ }
+
+ // Removes all children of this GLView.
+ public void removeAllComponents() {
+ for (int i = 0, n = mComponents.size(); i < n; ++i) {
+ removeOneComponent(mComponents.get(i));
+ }
+ mComponents.clear();
+ }
+
+ private void removeOneComponent(GLView component) {
+ if (mMotionTarget == component) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+ component.onDetachFromRoot();
+ component.mParent = null;
+ }
+
+ public Rect bounds() {
+ return mBounds;
+ }
+
+ public int getWidth() {
+ return mBounds.right - mBounds.left;
+ }
+
+ public int getHeight() {
+ return mBounds.bottom - mBounds.top;
+ }
+
+ public GLRoot getGLRoot() {
+ return mRoot;
+ }
+
+ // Request re-rendering of the view hierarchy.
+ // This is used for animation or when the contents changed.
+ public void invalidate() {
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestRender();
+ }
+
+ // Request re-layout of the view hierarchy.
+ public void requestLayout() {
+ mViewFlags |= FLAG_LAYOUT_REQUESTED;
+ mLastHeightSpec = -1;
+ mLastWidthSpec = -1;
+ if (mParent != null) {
+ mParent.requestLayout();
+ } else {
+ // Is this a content pane ?
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestLayoutContentPane();
+ }
+ }
+
+ protected void render(GLCanvas canvas) {
+ boolean transitionActive = false;
+ if (mTransition != null && mTransition.calculate(AnimationTime.get())) {
+ invalidate();
+ transitionActive = mTransition.isActive();
+ }
+ renderBackground(canvas);
+ canvas.save();
+ if (transitionActive) {
+ mTransition.applyContentTransform(this, canvas);
+ }
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ renderChild(canvas, getComponent(i));
+ }
+ canvas.restore();
+ if (transitionActive) {
+ mTransition.applyOverlay(this, canvas);
+ }
+ }
+
+ public void setIntroAnimation(StateTransitionAnimation intro) {
+ mTransition = intro;
+ if (mTransition != null) mTransition.start();
+ }
+
+ public float [] getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ public void setBackgroundColor(float [] color) {
+ mBackgroundColor = color;
+ }
+
+ protected void renderBackground(GLCanvas view) {
+ if (mBackgroundColor != null) {
+ view.clearBuffer(mBackgroundColor);
+ }
+ if (mTransition != null && mTransition.isActive()) {
+ mTransition.applyBackground(this, view);
+ return;
+ }
+ }
+
+ protected void renderChild(GLCanvas canvas, GLView component) {
+ if (component.getVisibility() != GLView.VISIBLE
+ && component.mAnimation == null) return;
+
+ int xoffset = component.mBounds.left - mScrollX;
+ int yoffset = component.mBounds.top - mScrollY;
+
+ canvas.translate(xoffset, yoffset);
+
+ CanvasAnimation anim = component.mAnimation;
+ if (anim != null) {
+ canvas.save(anim.getCanvasSaveFlags());
+ if (anim.calculate(AnimationTime.get())) {
+ invalidate();
+ } else {
+ component.mAnimation = null;
+ }
+ anim.apply(canvas);
+ }
+ component.render(canvas);
+ if (anim != null) canvas.restore();
+ canvas.translate(-xoffset, -yoffset);
+ }
+
+ protected boolean onTouch(MotionEvent event) {
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event,
+ int x, int y, GLView component, boolean checkBounds) {
+ Rect rect = component.mBounds;
+ int left = rect.left;
+ int top = rect.top;
+ if (!checkBounds || rect.contains(x, y)) {
+ event.offsetLocation(-left, -top);
+ if (component.dispatchTouchEvent(event)) {
+ event.offsetLocation(left, top);
+ return true;
+ }
+ event.offsetLocation(left, top);
+ }
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ int action = event.getAction();
+ if (mMotionTarget != null) {
+ if (action == MotionEvent.ACTION_DOWN) {
+ MotionEvent cancel = MotionEvent.obtain(event);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
+ dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
+ mMotionTarget = null;
+ } else {
+ dispatchTouchEvent(event, x, y, mMotionTarget, false);
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mMotionTarget = null;
+ }
+ return true;
+ }
+ }
+ if (action == MotionEvent.ACTION_DOWN) {
+ // in the reverse rendering order
+ for (int i = getComponentCount() - 1; i >= 0; --i) {
+ GLView component = getComponent(i);
+ if (component.getVisibility() != GLView.VISIBLE) continue;
+ if (dispatchTouchEvent(event, x, y, component, true)) {
+ mMotionTarget = component;
+ return true;
+ }
+ }
+ }
+ return onTouch(event);
+ }
+
+ public Rect getPaddings() {
+ return mPaddings;
+ }
+
+ public void layout(int left, int top, int right, int bottom) {
+ boolean sizeChanged = setBounds(left, top, right, bottom);
+ mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+ // We call onLayout no matter sizeChanged is true or not because the
+ // orientation may change without changing the size of the View (for
+ // example, rotate the device by 180 degrees), and we want to handle
+ // orientation change in onLayout.
+ onLayout(sizeChanged, left, top, right, bottom);
+ }
+
+ private boolean setBounds(int left, int top, int right, int bottom) {
+ boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left)
+ || (bottom - top) != (mBounds.bottom - mBounds.top);
+ mBounds.set(left, top, right, bottom);
+ return sizeChanged;
+ }
+
+ public void measure(int widthSpec, int heightSpec) {
+ if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec
+ && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) {
+ return;
+ }
+
+ mLastWidthSpec = widthSpec;
+ mLastHeightSpec = heightSpec;
+
+ mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
+ onMeasure(widthSpec, heightSpec);
+ if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
+ throw new IllegalStateException(getClass().getName()
+ + " should call setMeasuredSize() in onMeasure()");
+ }
+ }
+
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ }
+
+ protected void setMeasuredSize(int width, int height) {
+ mViewFlags |= FLAG_SET_MEASURED_SIZE;
+ mMeasuredWidth = width;
+ mMeasuredHeight = height;
+ }
+
+ public int getMeasuredWidth() {
+ return mMeasuredWidth;
+ }
+
+ public int getMeasuredHeight() {
+ return mMeasuredHeight;
+ }
+
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ }
+
+ /**
+ * Gets the bounds of the given descendant that relative to this view.
+ */
+ public boolean getBoundsOf(GLView descendant, Rect out) {
+ int xoffset = 0;
+ int yoffset = 0;
+ GLView view = descendant;
+ while (view != this) {
+ if (view == null) return false;
+ Rect bounds = view.mBounds;
+ xoffset += bounds.left;
+ yoffset += bounds.top;
+ view = view.mParent;
+ }
+ out.set(xoffset, yoffset, xoffset + descendant.getWidth(),
+ yoffset + descendant.getHeight());
+ return true;
+ }
+
+ protected void onVisibilityChanged(int visibility) {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ GLView child = getComponent(i);
+ if (child.getVisibility() == GLView.VISIBLE) {
+ child.onVisibilityChanged(visibility);
+ }
+ }
+ }
+
+ protected void onAttachToRoot(GLRoot root) {
+ mRoot = root;
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onAttachToRoot(root);
+ }
+ }
+
+ protected void onDetachFromRoot() {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onDetachFromRoot();
+ }
+ mRoot = null;
+ }
+
+ public void lockRendering() {
+ if (mRoot != null) {
+ mRoot.lockRenderThread();
+ }
+ }
+
+ public void unlockRendering() {
+ if (mRoot != null) {
+ mRoot.unlockRenderThread();
+ }
+ }
+
+ // This is for debugging only.
+ // Dump the view hierarchy into log.
+ void dumpTree(String prefix) {
+ Log.d(TAG, prefix + getClass().getSimpleName());
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).dumpTree(prefix + "....");
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java
new file mode 100644
index 000000000..1e5250b9b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GestureRecognizer.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 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.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector, and DownUpDetector.
+public class GestureRecognizer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "GestureRecognizer";
+
+ public interface Listener {
+ boolean onSingleTapUp(float x, float y);
+ boolean onDoubleTap(float x, float y);
+ boolean onScroll(float dx, float dy, float totalX, float totalY);
+ boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
+ boolean onScaleBegin(float focusX, float focusY);
+ boolean onScale(float focusX, float focusY, float scale);
+ void onScaleEnd();
+ void onDown(float x, float y);
+ void onUp();
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScaleGestureDetector mScaleDetector;
+ private final DownUpDetector mDownUpDetector;
+ private final Listener mListener;
+
+ public GestureRecognizer(Context context, Listener listener) {
+ mListener = listener;
+ mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+ null, true /* ignoreMultitouch */);
+ mScaleDetector = new ScaleGestureDetector(
+ context, new MyScaleListener());
+ mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ mScaleDetector.onTouchEvent(event);
+ mDownUpDetector.onTouchEvent(event);
+ }
+
+ public boolean isDown() {
+ return mDownUpDetector.isDown();
+ }
+
+ public void cancelScale() {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mScaleDetector.onTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return mListener.onSingleTapUp(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ return mListener.onDoubleTap(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onScroll(
+ MotionEvent e1, MotionEvent e2, float dx, float dy) {
+ return mListener.onScroll(
+ dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY());
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return mListener.onFling(e1, e2, velocityX, velocityY);
+ }
+ }
+
+ private class MyScaleListener
+ extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return mListener.onScaleBegin(
+ detector.getFocusX(), detector.getFocusY());
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return mListener.onScale(detector.getFocusX(),
+ detector.getFocusY(), detector.getScaleFactor());
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mListener.onScaleEnd();
+ }
+ }
+
+ private class MyDownUpListener implements DownUpDetector.DownUpListener {
+ @Override
+ public void onDown(MotionEvent e) {
+ mListener.onDown(e.getX(), e.getY());
+ }
+
+ @Override
+ public void onUp(MotionEvent e) {
+ mListener.onUp();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java
new file mode 100644
index 000000000..5570763bb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Log.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+// TODO: Delete this
+public class Log {
+ public static int v(String tag, String msg) {
+ return android.util.Log.v(tag, msg);
+ }
+ public static int v(String tag, String msg, Throwable tr) {
+ return android.util.Log.v(tag, msg, tr);
+ }
+ public static int d(String tag, String msg) {
+ return android.util.Log.d(tag, msg);
+ }
+ public static int d(String tag, String msg, Throwable tr) {
+ return android.util.Log.d(tag, msg, tr);
+ }
+ public static int i(String tag, String msg) {
+ return android.util.Log.i(tag, msg);
+ }
+ public static int i(String tag, String msg, Throwable tr) {
+ return android.util.Log.i(tag, msg, tr);
+ }
+ public static int w(String tag, String msg) {
+ return android.util.Log.w(tag, msg);
+ }
+ public static int w(String tag, String msg, Throwable tr) {
+ return android.util.Log.w(tag, msg, tr);
+ }
+ public static int w(String tag, Throwable tr) {
+ return android.util.Log.w(tag, tr);
+ }
+ public static int e(String tag, String msg) {
+ return android.util.Log.e(tag, msg);
+ }
+ public static int e(String tag, String msg, Throwable tr) {
+ return android.util.Log.e(tag, msg, tr);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
new file mode 100644
index 000000000..d210bd1f1
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
@@ -0,0 +1,116 @@
+/*
+ * 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 com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class ManageCacheDrawer extends AlbumSetSlotRenderer {
+ private final ResourceTexture mCheckedItem;
+ private final ResourceTexture mUnCheckedItem;
+ private final SelectionManager mSelectionManager;
+
+ private final ResourceTexture mLocalAlbumIcon;
+ private final StringTexture mCachingText;
+
+ private final int mCachePinSize;
+ private final int mCachePinMargin;
+
+ public ManageCacheDrawer(AbstractGalleryActivity activity, SelectionManager selectionManager,
+ SlotView slotView, LabelSpec labelSpec, int cachePinSize, int cachePinMargin) {
+ super(activity, selectionManager, slotView, labelSpec,
+ activity.getResources().getColor(R.color.cache_placeholder));
+ Context context = activity;
+ mCheckedItem = new ResourceTexture(
+ context, R.drawable.btn_make_offline_normal_on_holo_dark);
+ mUnCheckedItem = new ResourceTexture(
+ context, R.drawable.btn_make_offline_normal_off_holo_dark);
+ mLocalAlbumIcon = new ResourceTexture(
+ context, R.drawable.btn_make_offline_disabled_on_holo_dark);
+ String cachingLabel = context.getString(R.string.caching_label);
+ mCachingText = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
+ mSelectionManager = selectionManager;
+ mCachePinSize = cachePinSize;
+ mCachePinMargin = cachePinMargin;
+ }
+
+ private static boolean isLocal(int dataSourceType) {
+ return dataSourceType != DataSourceType.TYPE_PICASA;
+ }
+
+ @Override
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+ AlbumSetEntry entry = mDataWindow.get(index);
+
+ boolean wantCache = entry.cacheFlag == MediaSet.CACHE_FLAG_FULL;
+ boolean isCaching = wantCache && (
+ entry.cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL);
+ boolean selected = mSelectionManager.isItemSelected(entry.setPath);
+ boolean chooseToCache = wantCache ^ selected;
+ boolean available = isLocal(entry.sourceType) || chooseToCache;
+
+ int renderRequestFlags = 0;
+
+ if (!available) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(0.6f);
+ }
+ renderRequestFlags |= renderContent(canvas, entry, width, height);
+ if (!available) canvas.restore();
+
+ renderRequestFlags |= renderLabel(canvas, entry, width, height);
+
+ drawCachingPin(canvas, entry.setPath,
+ entry.sourceType, isCaching, chooseToCache, width, height);
+
+ renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+ return renderRequestFlags;
+ }
+
+ private void drawCachingPin(GLCanvas canvas, Path path, int dataSourceType,
+ boolean isCaching, boolean chooseToCache, int width, int height) {
+ ResourceTexture icon;
+ if (isLocal(dataSourceType)) {
+ icon = mLocalAlbumIcon;
+ } else if (chooseToCache) {
+ icon = mCheckedItem;
+ } else {
+ icon = mUnCheckedItem;
+ }
+
+ // show the icon in right bottom
+ int s = mCachePinSize;
+ int m = mCachePinMargin;
+ icon.draw(canvas, width - m - s, height - s, s, s);
+
+ if (isCaching) {
+ int w = mCachingText.getWidth();
+ int h = mCachingText.getHeight();
+ // Show the caching text in bottom center
+ mCachingText.draw(canvas, (width - w) / 2, height - h);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java
new file mode 100644
index 000000000..f65dc10b3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MeasureHelper.java
@@ -0,0 +1,65 @@
+/*
+ * 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.graphics.Rect;
+import android.view.View.MeasureSpec;
+
+class MeasureHelper {
+
+ private static MeasureHelper sInstance = new MeasureHelper(null);
+
+ private GLView mComponent;
+ private int mPreferredWidth;
+ private int mPreferredHeight;
+
+ private MeasureHelper(GLView component) {
+ mComponent = component;
+ }
+
+ public static MeasureHelper getInstance(GLView component) {
+ sInstance.mComponent = component;
+ return sInstance;
+ }
+
+ public MeasureHelper setPreferredContentSize(int width, int height) {
+ mPreferredWidth = width;
+ mPreferredHeight = height;
+ return this;
+ }
+
+ public void measure(int widthSpec, int heightSpec) {
+ Rect p = mComponent.getPaddings();
+ setMeasuredSize(
+ getLength(widthSpec, mPreferredWidth + p.left + p.right),
+ getLength(heightSpec, mPreferredHeight + p.top + p.bottom));
+ }
+
+ private static int getLength(int measureSpec, int prefered) {
+ int specLength = MeasureSpec.getSize(measureSpec);
+ switch(MeasureSpec.getMode(measureSpec)) {
+ case MeasureSpec.EXACTLY: return specLength;
+ case MeasureSpec.AT_MOST: return Math.min(prefered, specLength);
+ default: return prefered;
+ }
+ }
+
+ protected void setMeasuredSize(int width, int height) {
+ mComponent.setMeasuredSize(width, height);
+ }
+
+}
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
new file mode 100644
index 000000000..29def0527
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -0,0 +1,448 @@
+/*
+ * 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.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+
+public class MenuExecutor {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MenuExecutor";
+
+ private static final int MSG_TASK_COMPLETE = 1;
+ private static final int MSG_TASK_UPDATE = 2;
+ private static final int MSG_TASK_START = 3;
+ private static final int MSG_DO_SHARE = 4;
+
+ public static final int EXECUTION_RESULT_SUCCESS = 1;
+ public static final int EXECUTION_RESULT_FAIL = 2;
+ public static final int EXECUTION_RESULT_CANCEL = 3;
+
+ private ProgressDialog mDialog;
+ private Future<?> mTask;
+ // wait the operation to finish when we want to stop it.
+ private boolean mWaitOnStop;
+ private boolean mPaused;
+
+ private final AbstractGalleryActivity mActivity;
+ private final SelectionManager mSelectionManager;
+ private final Handler mHandler;
+
+ private static ProgressDialog createProgressDialog(
+ Context context, int titleId, int progressMax) {
+ ProgressDialog dialog = new ProgressDialog(context);
+ dialog.setTitle(titleId);
+ dialog.setMax(progressMax);
+ dialog.setCancelable(false);
+ dialog.setIndeterminate(false);
+ if (progressMax > 1) {
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ }
+ return dialog;
+ }
+
+ public interface ProgressListener {
+ public void onConfirmDialogShown();
+ public void onConfirmDialogDismissed(boolean confirmed);
+ public void onProgressStart();
+ public void onProgressUpdate(int index);
+ public void onProgressComplete(int result);
+ }
+
+ public MenuExecutor(
+ AbstractGalleryActivity activity, SelectionManager selectionManager) {
+ mActivity = Utils.checkNotNull(activity);
+ mSelectionManager = Utils.checkNotNull(selectionManager);
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_TASK_START: {
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressStart();
+ }
+ break;
+ }
+ case MSG_TASK_COMPLETE: {
+ stopTaskAndDismissDialog();
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressComplete(message.arg1);
+ }
+ mSelectionManager.leaveSelectionMode();
+ break;
+ }
+ case MSG_TASK_UPDATE: {
+ if (mDialog != null && !mPaused) mDialog.setProgress(message.arg1);
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressUpdate(message.arg1);
+ }
+ break;
+ }
+ case MSG_DO_SHARE: {
+ ((Activity) mActivity).startActivity((Intent) message.obj);
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ private void stopTaskAndDismissDialog() {
+ if (mTask != null) {
+ if (!mWaitOnStop) mTask.cancel();
+ if (mDialog != null && mDialog.isShowing()) mDialog.dismiss();
+ mDialog = null;
+ mTask = null;
+ }
+ }
+
+ public void resume() {
+ mPaused = false;
+ if (mDialog != null) mDialog.show();
+ }
+
+ public void pause() {
+ mPaused = true;
+ if (mDialog != null && mDialog.isShowing()) mDialog.hide();
+ }
+
+ public void destroy() {
+ stopTaskAndDismissDialog();
+ }
+
+ private void onProgressUpdate(int index, ProgressListener listener) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener));
+ }
+
+ private void onProgressStart(ProgressListener listener) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_START, listener));
+ }
+
+ private void onProgressComplete(int result, ProgressListener listener) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener));
+ }
+
+ public static void updateMenuOperation(Menu menu, int supported) {
+ boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0;
+ boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0;
+ boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0;
+ boolean supportTrim = (supported & MediaObject.SUPPORT_TRIM) != 0;
+ boolean supportMute = (supported & MediaObject.SUPPORT_MUTE) != 0;
+ boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0;
+ boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0;
+ boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0;
+ boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0;
+ boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0;
+ boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0;
+
+ setMenuItemVisible(menu, R.id.action_delete, supportDelete);
+ setMenuItemVisible(menu, R.id.action_rotate_ccw, supportRotate);
+ setMenuItemVisible(menu, R.id.action_rotate_cw, supportRotate);
+ setMenuItemVisible(menu, R.id.action_crop, supportCrop);
+ setMenuItemVisible(menu, R.id.action_trim, supportTrim);
+ setMenuItemVisible(menu, R.id.action_mute, supportMute);
+ // Hide panorama until call to updateMenuForPanorama corrects it
+ setMenuItemVisible(menu, R.id.action_share_panorama, false);
+ setMenuItemVisible(menu, R.id.action_share, supportShare);
+ setMenuItemVisible(menu, R.id.action_setas, supportSetAs);
+ setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap);
+ setMenuItemVisible(menu, R.id.action_edit, supportEdit);
+ setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit);
+ setMenuItemVisible(menu, R.id.action_details, supportInfo);
+ }
+
+ public static void updateMenuForPanorama(Menu menu, boolean shareAsPanorama360,
+ boolean disablePanorama360Options) {
+ setMenuItemVisible(menu, R.id.action_share_panorama, shareAsPanorama360);
+ if (disablePanorama360Options) {
+ setMenuItemVisible(menu, R.id.action_rotate_ccw, false);
+ setMenuItemVisible(menu, R.id.action_rotate_cw, false);
+ }
+ }
+
+ private static void setMenuItemVisible(Menu menu, int itemId, boolean visible) {
+ MenuItem item = menu.findItem(itemId);
+ if (item != null) item.setVisible(visible);
+ }
+
+ private Path getSingleSelectedPath() {
+ ArrayList<Path> ids = mSelectionManager.getSelected(true);
+ Utils.assertTrue(ids.size() == 1);
+ return ids.get(0);
+ }
+
+ private Intent getIntentBySingleSelectedPath(String action) {
+ DataManager manager = mActivity.getDataManager();
+ Path path = getSingleSelectedPath();
+ String mimeType = getMimeType(manager.getMediaType(path));
+ return new Intent(action).setDataAndType(manager.getContentUri(path), mimeType);
+ }
+
+ private void onMenuClicked(int action, ProgressListener listener) {
+ onMenuClicked(action, listener, false, true);
+ }
+
+ public void onMenuClicked(int action, ProgressListener listener,
+ boolean waitOnStop, boolean showDialog) {
+ int title;
+ switch (action) {
+ case R.id.action_select_all:
+ if (mSelectionManager.inSelectAllMode()) {
+ mSelectionManager.deSelectAll();
+ } else {
+ mSelectionManager.selectAll();
+ }
+ return;
+ case R.id.action_crop: {
+ Intent intent = getIntentBySingleSelectedPath(CropActivity.CROP_ACTION);
+ ((Activity) mActivity).startActivity(intent);
+ return;
+ }
+ case R.id.action_edit: {
+ Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_EDIT)
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ ((Activity) mActivity).startActivity(Intent.createChooser(intent, null));
+ return;
+ }
+ case R.id.action_setas: {
+ Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_ATTACH_DATA)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.putExtra("mimeType", intent.getType());
+ Activity activity = mActivity;
+ activity.startActivity(Intent.createChooser(
+ intent, activity.getString(R.string.set_as)));
+ return;
+ }
+ case R.id.action_delete:
+ title = R.string.delete;
+ break;
+ case R.id.action_rotate_cw:
+ title = R.string.rotate_right;
+ break;
+ case R.id.action_rotate_ccw:
+ title = R.string.rotate_left;
+ break;
+ case R.id.action_show_on_map:
+ title = R.string.show_on_map;
+ break;
+ default:
+ return;
+ }
+ startAction(action, title, listener, waitOnStop, showDialog);
+ }
+
+ private class ConfirmDialogListener implements OnClickListener, OnCancelListener {
+ private final int mActionId;
+ private final ProgressListener mListener;
+
+ public ConfirmDialogListener(int actionId, ProgressListener listener) {
+ mActionId = actionId;
+ mListener = listener;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (mListener != null) {
+ mListener.onConfirmDialogDismissed(true);
+ }
+ onMenuClicked(mActionId, mListener);
+ } else {
+ if (mListener != null) {
+ mListener.onConfirmDialogDismissed(false);
+ }
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (mListener != null) {
+ mListener.onConfirmDialogDismissed(false);
+ }
+ }
+ }
+
+ public void onMenuClicked(MenuItem menuItem, String confirmMsg,
+ final ProgressListener listener) {
+ final int action = menuItem.getItemId();
+
+ if (confirmMsg != null) {
+ if (listener != null) listener.onConfirmDialogShown();
+ ConfirmDialogListener cdl = new ConfirmDialogListener(action, listener);
+ new AlertDialog.Builder(mActivity.getAndroidContext())
+ .setMessage(confirmMsg)
+ .setOnCancelListener(cdl)
+ .setPositiveButton(R.string.ok, cdl)
+ .setNegativeButton(R.string.cancel, cdl)
+ .create().show();
+ } else {
+ onMenuClicked(action, listener);
+ }
+ }
+
+ public void startAction(int action, int title, ProgressListener listener) {
+ startAction(action, title, listener, false, true);
+ }
+
+ public void startAction(int action, int title, ProgressListener listener,
+ boolean waitOnStop, boolean showDialog) {
+ ArrayList<Path> ids = mSelectionManager.getSelected(false);
+ stopTaskAndDismissDialog();
+
+ Activity activity = mActivity;
+ if (showDialog) {
+ mDialog = createProgressDialog(activity, title, ids.size());
+ mDialog.show();
+ } else {
+ mDialog = null;
+ }
+ MediaOperation operation = new MediaOperation(action, ids, listener);
+ mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null);
+ mWaitOnStop = waitOnStop;
+ }
+
+ public void startSingleItemAction(int action, Path targetPath) {
+ ArrayList<Path> ids = new ArrayList<Path>(1);
+ ids.add(targetPath);
+ mDialog = null;
+ MediaOperation operation = new MediaOperation(action, ids, null);
+ mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null);
+ mWaitOnStop = false;
+ }
+
+ public static String getMimeType(int type) {
+ switch (type) {
+ case MediaObject.MEDIA_TYPE_IMAGE :
+ return GalleryUtils.MIME_TYPE_IMAGE;
+ case MediaObject.MEDIA_TYPE_VIDEO :
+ return GalleryUtils.MIME_TYPE_VIDEO;
+ default: return GalleryUtils.MIME_TYPE_ALL;
+ }
+ }
+
+ private boolean execute(
+ DataManager manager, JobContext jc, int cmd, Path path) {
+ boolean result = true;
+ Log.v(TAG, "Execute cmd: " + cmd + " for " + path);
+ long startTime = System.currentTimeMillis();
+
+ switch (cmd) {
+ case R.id.action_delete:
+ manager.delete(path);
+ break;
+ case R.id.action_rotate_cw:
+ manager.rotate(path, 90);
+ break;
+ case R.id.action_rotate_ccw:
+ manager.rotate(path, -90);
+ break;
+ case R.id.action_toggle_full_caching: {
+ MediaObject obj = manager.getMediaObject(path);
+ int cacheFlag = obj.getCacheFlag();
+ if (cacheFlag == MediaObject.CACHE_FLAG_FULL) {
+ cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL;
+ } else {
+ cacheFlag = MediaObject.CACHE_FLAG_FULL;
+ }
+ obj.cache(cacheFlag);
+ break;
+ }
+ case R.id.action_show_on_map: {
+ MediaItem item = (MediaItem) manager.getMediaObject(path);
+ double latlng[] = new double[2];
+ item.getLatLong(latlng);
+ if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) {
+ GalleryUtils.showOnMap(mActivity, latlng[0], latlng[1]);
+ }
+ break;
+ }
+ default:
+ throw new AssertionError();
+ }
+ Log.v(TAG, "It takes " + (System.currentTimeMillis() - startTime) +
+ " ms to execute cmd for " + path);
+ return result;
+ }
+
+ private class MediaOperation implements Job<Void> {
+ private final ArrayList<Path> mItems;
+ private final int mOperation;
+ private final ProgressListener mListener;
+
+ public MediaOperation(int operation, ArrayList<Path> items,
+ ProgressListener listener) {
+ mOperation = operation;
+ mItems = items;
+ mListener = listener;
+ }
+
+ @Override
+ public Void run(JobContext jc) {
+ int index = 0;
+ DataManager manager = mActivity.getDataManager();
+ int result = EXECUTION_RESULT_SUCCESS;
+ try {
+ onProgressStart(mListener);
+ for (Path id : mItems) {
+ if (jc.isCancelled()) {
+ result = EXECUTION_RESULT_CANCEL;
+ break;
+ }
+ if (!execute(manager, jc, mOperation, id)) {
+ result = EXECUTION_RESULT_FAIL;
+ }
+ onProgressUpdate(index++, mListener);
+ }
+ } catch (Throwable th) {
+ Log.e(TAG, "failed to execute operation " + mOperation
+ + " : " + th);
+ } finally {
+ onProgressComplete(result, mListener);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/OrientationSource.java b/src/com/android/gallery3d/ui/OrientationSource.java
new file mode 100644
index 000000000..e13ce1cec
--- /dev/null
+++ b/src/com/android/gallery3d/ui/OrientationSource.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2012 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;
+
+public interface OrientationSource {
+ public int getDisplayRotation();
+ public int getCompensation();
+}
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
new file mode 100644
index 000000000..b36f5c3a2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -0,0 +1,183 @@
+/*
+ * 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.graphics.Rect;
+import android.opengl.Matrix;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.common.Utils;
+
+// This class does the overscroll effect.
+class Paper {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Paper";
+ private static final int ROTATE_FACTOR = 4;
+ private EdgeAnimation mAnimationLeft = new EdgeAnimation();
+ private EdgeAnimation mAnimationRight = new EdgeAnimation();
+ private int mWidth;
+ private float[] mMatrix = new float[16];
+
+ public void overScroll(float distance) {
+ distance /= mWidth; // make it relative to width
+ if (distance < 0) {
+ mAnimationLeft.onPull(-distance);
+ } else {
+ mAnimationRight.onPull(distance);
+ }
+ }
+
+ public void edgeReached(float velocity) {
+ velocity /= mWidth; // make it relative to width
+ if (velocity < 0) {
+ mAnimationRight.onAbsorb(-velocity);
+ } else {
+ mAnimationLeft.onAbsorb(velocity);
+ }
+ }
+
+ public void onRelease() {
+ mAnimationLeft.onRelease();
+ mAnimationRight.onRelease();
+ }
+
+ public boolean advanceAnimation() {
+ // Note that we use "|" because we want both animations get updated.
+ return mAnimationLeft.update() | mAnimationRight.update();
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ }
+
+ public float[] getTransform(Rect rect, float scrollX) {
+ float left = mAnimationLeft.getValue();
+ float right = mAnimationRight.getValue();
+ float screenX = rect.centerX() - scrollX;
+ // We linearly interpolate the value [left, right] for the screenX
+ // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside
+ // the screen, we still get some transform.
+ float x = screenX + mWidth / 4;
+ int range = 3 * mWidth / 2;
+ float t = ((range - x) * left - x * right) / range;
+ // compress t to the range (-1, 1) by the function
+ // f(t) = (1 / (1 + e^-t) - 0.5) * 2
+ // then multiply by 90 to make the range (-45, 45)
+ float degrees =
+ (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45;
+ Matrix.setIdentityM(mMatrix, 0);
+ Matrix.translateM(mMatrix, 0, mMatrix, 0, rect.centerX(), rect.centerY(), 0);
+ Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0);
+ Matrix.translateM(mMatrix, 0, mMatrix, 0, -rect.width() / 2, -rect.height() / 2, 0);
+ return mMatrix;
+ }
+}
+
+// This class follows the structure of frameworks's EdgeEffect class.
+class EdgeAnimation {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeAnimation";
+
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PULL = 1;
+ private static final int STATE_ABSORB = 2;
+ private static final int STATE_RELEASE = 3;
+
+ // Time it will take the effect to fully done in ms
+ private static final int ABSORB_TIME = 200;
+ private static final int RELEASE_TIME = 500;
+
+ private static final float VELOCITY_FACTOR = 0.1f;
+
+ private final Interpolator mInterpolator;
+
+ private int mState;
+ private float mValue;
+
+ private float mValueStart;
+ private float mValueFinish;
+ private long mStartTime;
+ private long mDuration;
+
+ public EdgeAnimation() {
+ mInterpolator = new DecelerateInterpolator();
+ mState = STATE_IDLE;
+ }
+
+ private void startAnimation(float start, float finish, long duration,
+ int newState) {
+ mValueStart = start;
+ mValueFinish = finish;
+ mDuration = duration;
+ mStartTime = now();
+ mState = newState;
+ }
+
+ // The deltaDistance's magnitude is in the range of -1 (no change) to 1.
+ // The value 1 is the full length of the view. Negative values means the
+ // movement is in the opposite direction.
+ public void onPull(float deltaDistance) {
+ if (mState == STATE_ABSORB) return;
+ mValue = Utils.clamp(mValue + deltaDistance, -1.0f, 1.0f);
+ mState = STATE_PULL;
+ }
+
+ public void onRelease() {
+ if (mState == STATE_IDLE || mState == STATE_ABSORB) return;
+ startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+ }
+
+ public void onAbsorb(float velocity) {
+ float finish = Utils.clamp(mValue + velocity * VELOCITY_FACTOR,
+ -1.0f, 1.0f);
+ startAnimation(mValue, finish, ABSORB_TIME, STATE_ABSORB);
+ }
+
+ public boolean update() {
+ if (mState == STATE_IDLE) return false;
+ if (mState == STATE_PULL) return true;
+
+ float t = Utils.clamp((float)(now() - mStartTime) / mDuration, 0.0f, 1.0f);
+ /* Use linear interpolation for absorb, quadratic for others */
+ float interp = (mState == STATE_ABSORB)
+ ? t : mInterpolator.getInterpolation(t);
+
+ mValue = mValueStart + (mValueFinish - mValueStart) * interp;
+
+ if (t >= 1.0f) {
+ switch (mState) {
+ case STATE_ABSORB:
+ startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+ break;
+ case STATE_RELEASE:
+ mState = STATE_IDLE;
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ public float getValue() {
+ return mValue;
+ }
+
+ private long now() {
+ return AnimationTime.get();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoFallbackEffect.java b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
new file mode 100644
index 000000000..4603285a4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
@@ -0,0 +1,179 @@
+/*
+ * 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.graphics.Rect;
+import android.graphics.RectF;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.AlbumSlotRenderer.SlotFilter;
+
+import java.util.ArrayList;
+
+public class PhotoFallbackEffect extends Animation implements SlotFilter {
+
+ private static final int ANIM_DURATION = 300;
+ private static final Interpolator ANIM_INTERPOLATE = new DecelerateInterpolator(1.5f);
+
+ public static class Entry {
+ public int index;
+ public Path path;
+ public Rect source;
+ public Rect dest;
+ public RawTexture texture;
+
+ public Entry(Path path, Rect source, RawTexture texture) {
+ this.path = path;
+ this.source = source;
+ this.texture = texture;
+ }
+ }
+
+ public interface PositionProvider {
+ public Rect getPosition(int index);
+ public int getItemIndex(Path path);
+ }
+
+ private RectF mSource = new RectF();
+ private RectF mTarget = new RectF();
+ private float mProgress;
+ private PositionProvider mPositionProvider;
+
+ private ArrayList<Entry> mList = new ArrayList<Entry>();
+
+ public PhotoFallbackEffect() {
+ setDuration(ANIM_DURATION);
+ setInterpolator(ANIM_INTERPOLATE);
+ }
+
+ public void addEntry(Path path, Rect rect, RawTexture texture) {
+ mList.add(new Entry(path, rect, texture));
+ }
+
+ public Entry getEntry(Path path) {
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ if (entry.path == path) return entry;
+ }
+ return null;
+ }
+
+ public boolean draw(GLCanvas canvas) {
+ boolean more = calculate(AnimationTime.get());
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ if (entry.index < 0) continue;
+ entry.dest = mPositionProvider.getPosition(entry.index);
+ drawEntry(canvas, entry);
+ }
+ return more;
+ }
+
+ private void drawEntry(GLCanvas canvas, Entry entry) {
+ if (!entry.texture.isLoaded()) return;
+
+ int w = entry.texture.getWidth();
+ int h = entry.texture.getHeight();
+
+ Rect s = entry.source;
+ Rect d = entry.dest;
+
+ // the following calculation is based on d.width() == d.height()
+
+ float p = mProgress;
+
+ float fullScale = (float) d.height() / Math.min(s.width(), s.height());
+ float scale = fullScale * p + 1 * (1 - p);
+
+ float cx = d.centerX() * p + s.centerX() * (1 - p);
+ float cy = d.centerY() * p + s.centerY() * (1 - p);
+
+ float ch = s.height() * scale;
+ float cw = s.width() * scale;
+
+ if (w > h) {
+ // draw the center part
+ mTarget.set(cx - ch / 2, cy - ch / 2, cx + ch / 2, cy + ch / 2);
+ mSource.set((w - h) / 2, 0, (w + h) / 2, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(1 - p);
+
+ // draw the left part
+ mTarget.set(cx - cw / 2, cy - ch / 2, cx - ch / 2, cy + ch / 2);
+ mSource.set(0, 0, (w - h) / 2, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ // draw the right part
+ mTarget.set(cx + ch / 2, cy - ch / 2, cx + cw / 2, cy + ch / 2);
+ mSource.set((w + h) / 2, 0, w, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.restore();
+ } else {
+ // draw the center part
+ mTarget.set(cx - cw / 2, cy - cw / 2, cx + cw / 2, cy + cw / 2);
+ mSource.set(0, (h - w) / 2, w, (h + w) / 2);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(1 - p);
+
+ // draw the upper part
+ mTarget.set(cx - cw / 2, cy - ch / 2, cx + cw / 2, cy - cw / 2);
+ mSource.set(0, 0, w, (h - w) / 2);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ // draw the bottom part
+ mTarget.set(cx - cw / 2, cy + cw / 2, cx + cw / 2, cy + ch / 2);
+ mSource.set(0, (w + h) / 2, w, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.restore();
+ }
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mProgress = progress;
+ }
+
+ public void setPositionProvider(PositionProvider provider) {
+ mPositionProvider = provider;
+ if (mPositionProvider != null) {
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ entry.index = mPositionProvider.getItemIndex(entry.path);
+ }
+ }
+ }
+
+ @Override
+ public boolean acceptSlot(int index) {
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ if (entry.index == index) return false;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
new file mode 100644
index 000000000..7afa20348
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -0,0 +1,1858 @@
+/*
+ * 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.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Message;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+import android.view.animation.AccelerateInterpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.UsageStatistics;
+
+public class PhotoView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "PhotoView";
+ private final int mPlaceholderColor;
+
+ 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.TileSource {
+ 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 static image that represents camera
+ // preview.
+ public boolean isStaticCamera(int offset);
+
+ // Returns true if the item is a Video.
+ public boolean isVideo(int offset);
+
+ // Returns true if the item can be deleted.
+ public boolean isDeletable(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);
+
+ // When data change happens, we need to decide which MediaItem to focus
+ // on.
+ //
+ // 1. If focus hint path != null, we try to focus on it if we can find
+ // it. This is used for undo a deletion, so we can focus on the
+ // undeleted item.
+ //
+ // 2. Otherwise try to focus on the MediaItem that is currently focused,
+ // if we can find it.
+ //
+ // 3. Otherwise try to focus on the previous MediaItem or the next
+ // MediaItem, depending on the value of focus hint direction.
+ public static final int FOCUS_HINT_NEXT = 0;
+ public static final int FOCUS_HINT_PREVIOUS = 1;
+ public void setFocusHintDirection(int direction);
+ public void setFocusHintPath(Path path);
+ }
+
+ public interface Listener {
+ public void onSingleTapUp(int x, int y);
+ public void onFullScreenChanged(boolean full);
+ public void onActionBarAllowed(boolean allowed);
+ public void onActionBarWanted();
+ public void onCurrentImageUpdated();
+ public void onDeleteImage(Path path, int offset);
+ public void onUndoDeleteImage();
+ public void onCommitDeleteImage();
+ public void onFilmModeChanged(boolean enabled);
+ public void onPictureCenter(boolean isCamera);
+ public void onUndoBarVisibilityChanged(boolean visible);
+ }
+
+ // The rules about orientation locking:
+ //
+ // (1) We need to lock the orientation if we are in page mode camera
+ // preview, so there is no (unwanted) rotation animation when the user
+ // rotates the device.
+ //
+ // (2) We need to unlock the orientation if we want to show the action bar
+ // because the action bar follows the system orientation.
+ //
+ // The rules about action bar:
+ //
+ // (1) If we are in film mode, we don't show action bar.
+ //
+ // (2) If we go from camera to gallery with capture animation, we show
+ // action bar.
+ 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 MSG_DELETE_ANIMATION_DONE = 5;
+ private static final int MSG_DELETE_DONE = 6;
+ private static final int MSG_UNDO_BAR_TIMEOUT = 7;
+ private static final int MSG_UNDO_BAR_FULL_CAMERA = 8;
+
+ 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;
+
+ // whether we want to apply offset effect in film mode.
+ private static final boolean OFFSET_EFFECT = true;
+
+ // Used to calculate the scaling factor for the card deck effect.
+ 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;
+
+ // These are constants for the delete gesture.
+ private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec
+ private static final int MAX_DISMISS_VELOCITY = 2500; // dp/sec
+ private static final int SWIPE_ESCAPE_DISTANCE = 150; // dp
+
+ // 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 Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1];
+
+ private final MyGestureListener mGestureListener;
+ private final GestureRecognizer mGestureRecognizer;
+ private final PositionController mPositionController;
+
+ private Listener mListener;
+ private Model mModel;
+ private StringTexture mNoThumbnailText;
+ private TileImageView mTileView;
+ private EdgeView mEdgeView;
+ private UndoBarView mUndoBar;
+ private Texture mVideoPlayIcon;
+
+ private SynchronizedHandler mHandler;
+
+ private boolean mCancelExtraScalingPending;
+ private boolean mFilmMode = false;
+ private boolean mWantPictureCenterCallbacks = false;
+ private int mDisplayRotation = 0;
+ private int mCompensation = 0;
+ private boolean mFullScreenCamera;
+ private Rect mCameraRelativeFrame = new Rect();
+ private Rect mCameraRect = new Rect();
+ private boolean mFirst = true;
+
+ // [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;
+ private static final int HOLD_DELETE = 4;
+
+ // mTouchBoxIndex is the index of the box that is touched by the down
+ // gesture in film mode. The value Integer.MAX_VALUE means no box was
+ // touched.
+ private int mTouchBoxIndex = Integer.MAX_VALUE;
+ // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful
+ // if mTouchBoxIndex is not Integer.MAX_VALUE.
+ private boolean mTouchBoxDeletable;
+ // This is the index of the last deleted item. This is only used as a hint
+ // to hide the undo button when we are too far away from the deleted
+ // item. The value Integer.MAX_VALUE means there is no such hint.
+ private int mUndoIndexHint = Integer.MAX_VALUE;
+
+ private Context mContext;
+
+ public PhotoView(AbstractGalleryActivity activity) {
+ mTileView = new TileImageView(activity);
+ addComponent(mTileView);
+ mContext = activity.getAndroidContext();
+ mPlaceholderColor = mContext.getResources().getColor(
+ R.color.photo_placeholder);
+ mEdgeView = new EdgeView(mContext);
+ addComponent(mEdgeView);
+ mUndoBar = new UndoBarView(mContext);
+ addComponent(mUndoBar);
+ mUndoBar.setVisibility(GLView.INVISIBLE);
+ mUndoBar.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(GLView v) {
+ mListener.onUndoDeleteImage();
+ hideUndoBar();
+ }
+ });
+ mNoThumbnailText = StringTexture.newInstance(
+ mContext.getString(R.string.no_thumbnail),
+ DEFAULT_TEXT_SIZE, Color.WHITE);
+
+ mHandler = new MyHandler(activity.getGLRoot());
+
+ mGestureListener = new MyGestureListener();
+ mGestureRecognizer = new GestureRecognizer(mContext, mGestureListener);
+
+ mPositionController = new PositionController(mContext,
+ new PositionController.Listener() {
+
+ @Override
+ public void invalidate() {
+ PhotoView.this.invalidate();
+ }
+
+ @Override
+ public boolean isHoldingDown() {
+ return (mHolding & HOLD_TOUCH_DOWN) != 0;
+ }
+
+ @Override
+ public boolean isHoldingDelete() {
+ return (mHolding & HOLD_DELETE) != 0;
+ }
+
+ @Override
+ public void onPull(int offset, int direction) {
+ mEdgeView.onPull(offset, direction);
+ }
+
+ @Override
+ public void onRelease() {
+ mEdgeView.onRelease();
+ }
+
+ @Override
+ public void onAbsorb(int velocity, int direction) {
+ mEdgeView.onAbsorb(velocity, direction);
+ }
+ });
+ mVideoPlayIcon = new ResourceTexture(mContext, 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 stopScrolling() {
+ mPositionController.stopScrolling();
+ }
+
+ 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;
+ }
+ case MSG_DELETE_ANIMATION_DONE: {
+ // message.obj is the Path of the MediaItem which should be
+ // deleted. message.arg1 is the offset of the image.
+ mListener.onDeleteImage((Path) message.obj, message.arg1);
+ // Normally a box which finishes delete animation will hold
+ // position until the underlying MediaItem is actually
+ // deleted, and HOLD_DELETE will be cancelled that time. In
+ // case the MediaItem didn't actually get deleted in 2
+ // seconds, we will cancel HOLD_DELETE and make it bounce
+ // back.
+
+ // We make sure there is at most one MSG_DELETE_DONE
+ // in the handler.
+ mHandler.removeMessages(MSG_DELETE_DONE);
+ Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+ mHandler.sendMessageDelayed(m, 2000);
+
+ int numberOfPictures = mNextBound - mPrevBound + 1;
+ if (numberOfPictures == 2) {
+ if (mModel.isCamera(mNextBound)
+ || mModel.isCamera(mPrevBound)) {
+ numberOfPictures--;
+ }
+ }
+ showUndoBar(numberOfPictures <= 1);
+ break;
+ }
+ case MSG_DELETE_DONE: {
+ if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) {
+ mHolding &= ~HOLD_DELETE;
+ snapback();
+ }
+ break;
+ }
+ case MSG_UNDO_BAR_TIMEOUT: {
+ checkHideUndoBar(UNDO_BAR_TIMEOUT);
+ break;
+ }
+ case MSG_UNDO_BAR_FULL_CAMERA: {
+ checkHideUndoBar(UNDO_BAR_FULL_CAMERA);
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ }
+
+ public void setWantPictureCenterCallbacks(boolean wanted) {
+ mWantPictureCenterCallbacks = wanted;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Data/Image change notifications
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
+ mPrevBound = prevBound;
+ mNextBound = nextBound;
+
+ // Update mTouchBoxIndex
+ if (mTouchBoxIndex != Integer.MAX_VALUE) {
+ int k = mTouchBoxIndex;
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) {
+ if (fromIndex[i] == k) {
+ mTouchBoxIndex = i - SCREEN_NAIL_MAX;
+ break;
+ }
+ }
+ }
+
+ // Hide undo button if we are too far away
+ if (mUndoIndexHint != Integer.MAX_VALUE) {
+ if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) {
+ hideUndoBar();
+ }
+ }
+
+ // Update the ScreenNails.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ Picture p = mPictures.get(i);
+ p.reload();
+ mSizes[i + SCREEN_NAIL_MAX] = p.getSize();
+ }
+
+ boolean wasDeleting = mPositionController.hasDeletingBox();
+
+ // Move the boxes
+ mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
+ mModel.isCamera(0), mSizes);
+
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ setPictureSize(i);
+ }
+
+ boolean isDeleting = mPositionController.hasDeletingBox();
+
+ // If the deletion is done, make HOLD_DELETE persist for only the time
+ // needed for a snapback animation.
+ if (wasDeleting && !isDeleting) {
+ mHandler.removeMessages(MSG_DELETE_DONE);
+ Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+ mHandler.sendMessageDelayed(
+ m, PositionController.SNAPBACK_ANIMATION_TIME);
+ }
+
+ invalidate();
+ }
+
+ public boolean isDeleting() {
+ return (mHolding & HOLD_DELETE) != 0
+ && mPositionController.hasDeletingBox();
+ }
+
+ public void notifyImageChange(int index) {
+ if (index == 0) {
+ mListener.onCurrentImageUpdated();
+ }
+ mPictures.get(index).reload();
+ setPictureSize(index);
+ invalidate();
+ }
+
+ private void setPictureSize(int index) {
+ Picture p = mPictures.get(index);
+ mPositionController.setImageSize(index, p.getSize(),
+ index == 0 && p.isCamera() ? mCameraRect : null);
+ }
+
+ @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);
+ mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), 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.forceSize();
+ }
+ }
+ }
+
+ updateCameraRect();
+ mPositionController.setConstrainedFrame(mCameraRect);
+ if (changeSize) {
+ mPositionController.setViewSize(getWidth(), getHeight());
+ }
+ }
+
+ // Update the camera rectangle due to layout change or camera relative frame
+ // change.
+ private void updateCameraRect() {
+ // 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);
+ }
+
+ public void setCameraRelativeFrame(Rect frame) {
+ mCameraRelativeFrame.set(frame);
+ updateCameraRect();
+ // Originally we do
+ // mPositionController.setConstrainedFrame(mCameraRect);
+ // here, but it is moved to a parameter of the setImageSize() call, so
+ // it can be updated atomically with the CameraScreenNail's size change.
+ }
+
+ // 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() {
+ // This function is magic
+ // The issue here is that Pano makes bad assumptions about rotation and
+ // orientation. The first is it assumes only two rotations are possible,
+ // 0 and 90. Thus, if display rotation is >= 180, we invert the output.
+ // The second is that it assumes landscape is a 90 rotation from portrait,
+ // however on landscape devices this is not true. Thus, if we are in portrait
+ // on a landscape device, we need to invert the output
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ boolean invertPortrait = (orientation == Configuration.ORIENTATION_PORTRAIT
+ && (mDisplayRotation == 90 || mDisplayRotation == 270));
+ boolean invert = (mDisplayRotation >= 180);
+ if (invert != invertPortrait) {
+ return (mCompensation + 180) % 360;
+ }
+ 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
+ boolean isDeletable(); // whether the picture can be deleted
+ void forceSize(); // called when mCompensation changes
+ Size getSize();
+ }
+
+ class FullPicture implements Picture {
+ private int mRotation;
+ private boolean mIsCamera;
+ private boolean mIsPanorama;
+ private boolean mIsStaticCamera;
+ private boolean mIsVideo;
+ private boolean mIsDeletable;
+ private int mLoadingState = Model.LOADING_INIT;
+ private Size mSize = new Size();
+
+ @Override
+ public void reload() {
+ // mImageWidth and mImageHeight will get updated
+ mTileView.notifyModelInvalidated();
+
+ mIsCamera = mModel.isCamera(0);
+ mIsPanorama = mModel.isPanorama(0);
+ mIsStaticCamera = mModel.isStaticCamera(0);
+ mIsVideo = mModel.isVideo(0);
+ mIsDeletable = mModel.isDeletable(0);
+ mLoadingState = mModel.getLoadingState(0);
+ setScreenNail(mModel.getScreenNail(0));
+ updateSize();
+ }
+
+ @Override
+ public Size getSize() {
+ return mSize;
+ }
+
+ @Override
+ public void forceSize() {
+ updateSize();
+ mPositionController.forceImageSize(0, mSize);
+ }
+
+ private void updateSize() {
+ if (mIsPanorama) {
+ mRotation = getPanoramaRotation();
+ } else if (mIsCamera && !mIsStaticCamera) {
+ mRotation = getCameraRotation();
+ } else {
+ mRotation = mModel.getImageRotation(0);
+ }
+
+ int w = mTileView.mImageWidth;
+ int h = mTileView.mImageHeight;
+ mSize.width = getRotated(mRotation, w, h);
+ mSize.height = getRotated(mRotation, h, w);
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Rect r) {
+ drawTileView(canvas, r);
+
+ // 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;
+
+ if (mWantPictureCenterCallbacks && mPositionController.isCenter()) {
+ mListener.onPictureCenter(mIsCamera);
+ }
+ }
+
+ @Override
+ public void setScreenNail(ScreenNail s) {
+ mTileView.setScreenNail(s);
+ }
+
+ @Override
+ public boolean isCamera() {
+ return mIsCamera;
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return mIsDeletable;
+ }
+
+ 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();
+ boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+ && filmRatio == 1f && r.centerY() != viewH / 2;
+ 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);
+ }
+ } else if (wantsOffsetEffect) {
+ float offset = (float) (r.centerY() - viewH / 2) / viewH;
+ float alpha = getOffsetAlpha(offset);
+ canvas.multiplyAlpha(alpha);
+ }
+
+ // Draw the tile view.
+ setTileViewPosition(cx, cy, viewW, viewH, imageScale);
+ renderChild(canvas, mTileView);
+
+ // 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 boolean mIsCamera;
+ private boolean mIsPanorama;
+ private boolean mIsStaticCamera;
+ private boolean mIsVideo;
+ private boolean mIsDeletable;
+ private int mLoadingState = Model.LOADING_INIT;
+ private Size mSize = new Size();
+
+ public ScreenNailPicture(int index) {
+ mIndex = index;
+ }
+
+ @Override
+ public void reload() {
+ mIsCamera = mModel.isCamera(mIndex);
+ mIsPanorama = mModel.isPanorama(mIndex);
+ mIsStaticCamera = mModel.isStaticCamera(mIndex);
+ mIsVideo = mModel.isVideo(mIndex);
+ mIsDeletable = mModel.isDeletable(mIndex);
+ mLoadingState = mModel.getLoadingState(mIndex);
+ setScreenNail(mModel.getScreenNail(mIndex));
+ updateSize();
+ }
+
+ @Override
+ public Size getSize() {
+ return mSize;
+ }
+
+ @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;
+ }
+ int w = getWidth();
+ int h = getHeight();
+ if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) {
+ mScreenNail.noDraw();
+ return;
+ }
+
+ float filmRatio = mPositionController.getFilmRatio();
+ boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
+ && filmRatio != 1f && !mPictures.get(0).isCamera();
+ boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+ && filmRatio == 1f && r.centerY() != h / 2;
+ 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);
+ } else if (wantsOffsetEffect) {
+ float offset = (float) (r.centerY() - h / 2) / h;
+ float alpha = getOffsetAlpha(offset);
+ canvas.multiplyAlpha(alpha);
+ }
+ 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 TiledScreenNail)
+ && ((TiledScreenNail) mScreenNail).isAnimating();
+ }
+
+ @Override
+ public void setScreenNail(ScreenNail s) {
+ mScreenNail = s;
+ }
+
+ @Override
+ public void forceSize() {
+ updateSize();
+ mPositionController.forceImageSize(mIndex, mSize);
+ }
+
+ private void updateSize() {
+ if (mIsPanorama) {
+ mRotation = getPanoramaRotation();
+ } else if (mIsCamera && !mIsStaticCamera) {
+ mRotation = getCameraRotation();
+ } else {
+ mRotation = mModel.getImageRotation(mIndex);
+ }
+
+ if (mScreenNail != null) {
+ mSize.width = mScreenNail.getWidth();
+ mSize.height = mScreenNail.getHeight();
+ } else {
+ // If we don't have ScreenNail available, we can still try to
+ // get the size information of it.
+ mModel.getImageSize(mIndex, mSize);
+ }
+
+ int w = mSize.width;
+ int h = mSize.height;
+ mSize.width = getRotated(mRotation, w, h);
+ mSize.height = getRotated(mRotation, h, w);
+ }
+
+ @Override
+ public boolean isCamera() {
+ return mIsCamera;
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return mIsDeletable;
+ }
+ }
+
+ // 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(), mPlaceholderColor);
+ }
+
+ // 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;
+ // If a scrolling has happened after a down gesture.
+ private boolean mScrolledAfterDown;
+ // If the first scrolling move is in X direction. In the film mode, X
+ // direction scrolling is normal scrolling. but Y direction scrolling is
+ // a delete gesture.
+ private boolean mFirstScrollX;
+ // The accumulated Y delta that has been sent to mPositionController.
+ private int mDeltaY;
+ // The accumulated scaling change from a scaling gesture.
+ private float mAccScale;
+ // If an onFling happened after the last onDown
+ private boolean mHadFling;
+
+ @Override
+ public boolean onSingleTapUp(float x, float y) {
+ // On crespo running Android 2.3.6 (gingerbread), a pinch out gesture results in the
+ // following call sequence: onDown(), onUp() and then onSingleTapUp(). The correct
+ // sequence for a single-tap-up gesture should be: onDown(), onSingleTapUp() and onUp().
+ // The call sequence for a pinch out gesture in JB is: onDown(), then onUp() and there's
+ // no onSingleTapUp(). Base on these observations, the following condition is added to
+ // filter out the false alarm where onSingleTapUp() is called within a pinch out
+ // gesture. The framework fix went into ICS. Refer to b/4588114.
+ if (Build.VERSION.SDK_INT < ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ if ((mHolding & HOLD_TOUCH_DOWN) == 0) {
+ return true;
+ }
+ }
+
+ // 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));
+
+ // If this is a lock screen photo, let the listener handle the
+ // event. Tapping on lock screen photo should take the user
+ // directly to the lock screen.
+ MediaItem item = mModel.getMediaItem(0);
+ int supported = 0;
+ if (item != null) supported = item.getSupportedOperations();
+ if ((supported & MediaItem.SUPPORT_ACTION) == 0) {
+ 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 <= .75f || controller.isAtMinimalScale()) {
+ controller.zoomIn(x, y, Math.max(1.0f, scale * 1.5f));
+ } else {
+ controller.resetToFullView();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(float dx, float dy, float totalX, float totalY) {
+ if (mIgnoreSwipingGesture) return true;
+ if (!mScrolledAfterDown) {
+ mScrolledAfterDown = true;
+ mFirstScrollX = (Math.abs(dx) > Math.abs(dy));
+ }
+
+ int dxi = (int) (-dx + 0.5f);
+ int dyi = (int) (-dy + 0.5f);
+ if (mFilmMode) {
+ if (mFirstScrollX) {
+ mPositionController.scrollFilmX(dxi);
+ } else {
+ if (mTouchBoxIndex == Integer.MAX_VALUE) return true;
+ int newDeltaY = calculateDeltaY(totalY);
+ int d = newDeltaY - mDeltaY;
+ if (d != 0) {
+ mPositionController.scrollFilmY(mTouchBoxIndex, d);
+ mDeltaY = newDeltaY;
+ }
+ }
+ } else {
+ mPositionController.scrollPage(dxi, dyi);
+ }
+ return true;
+ }
+
+ private int calculateDeltaY(float delta) {
+ if (mTouchBoxDeletable) return (int) (delta + 0.5f);
+
+ // don't let items that can't be deleted be dragged more than
+ // maxScrollDistance, and make it harder and harder to drag.
+ int size = getHeight();
+ float maxScrollDistance = 0.15f * size;
+ if (Math.abs(delta) >= size) {
+ delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
+ } else {
+ delta = maxScrollDistance *
+ FloatMath.sin((delta / size) * (float) (Math.PI / 2));
+ }
+ return (int) (delta + 0.5f);
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (mIgnoreSwipingGesture) return true;
+ if (mModeChanged) return true;
+ if (swipeImages(velocityX, velocityY)) {
+ mIgnoreUpEvent = true;
+ } else {
+ flingImages(velocityX, velocityY, Math.abs(e2.getY() - e1.getY()));
+ }
+ mHadFling = true;
+ return true;
+ }
+
+ private boolean flingImages(float velocityX, float velocityY, float dY) {
+ int vx = (int) (velocityX + 0.5f);
+ int vy = (int) (velocityY + 0.5f);
+ if (!mFilmMode) {
+ return mPositionController.flingPage(vx, vy);
+ }
+ if (Math.abs(velocityX) > Math.abs(velocityY)) {
+ return mPositionController.flingFilmX(vx);
+ }
+ // If we scrolled in Y direction fast enough, treat it as a delete
+ // gesture.
+ if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE
+ || !mTouchBoxDeletable) {
+ return false;
+ }
+ int maxVelocity = GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY);
+ int escapeVelocity = GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY);
+ int escapeDistance = GalleryUtils.dpToPixel(SWIPE_ESCAPE_DISTANCE);
+ int centerY = mPositionController.getPosition(mTouchBoxIndex)
+ .centerY();
+ boolean fastEnough = (Math.abs(vy) > escapeVelocity)
+ && (Math.abs(vy) > Math.abs(vx))
+ && ((vy > 0) == (centerY > getHeight() / 2))
+ && dY >= escapeDistance;
+ if (fastEnough) {
+ vy = Math.min(vy, maxVelocity);
+ int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy);
+ if (duration >= 0) {
+ mPositionController.setPopFromTop(vy < 0);
+ deleteAfterAnimation(duration);
+ // We reset mTouchBoxIndex, so up() won't check if Y
+ // scrolled far enough to be a delete gesture.
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void deleteAfterAnimation(int duration) {
+ MediaItem item = mModel.getMediaItem(mTouchBoxIndex);
+ if (item == null) return;
+ mListener.onCommitDeleteImage();
+ mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex;
+ mHolding |= HOLD_DELETE;
+ Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE);
+ m.obj = item.getPath();
+ m.arg1 = mTouchBoxIndex;
+ mHandler.sendMessageDelayed(m, duration);
+ }
+
+ @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();
+ mAccScale = 1f;
+ 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;
+
+ int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
+
+ // We wait for a large enough scale change before changing mode.
+ // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out
+ // or vice versa.
+ mAccScale *= scale;
+ boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f);
+
+ // If mode changes, we treat this scaling gesture has ended.
+ if (mCanChangeMode && largeEnough) {
+ 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;
+ if (mFilmMode) {
+ UsageStatistics.setPendingTransitionCause(
+ UsageStatistics.TRANSITION_PINCH_OUT);
+ } else {
+ UsageStatistics.setPendingTransitionCause(
+ UsageStatistics.TRANSITION_PINCH_IN);
+ }
+ 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(float x, float y) {
+ checkHideUndoBar(UNDO_BAR_TOUCHED);
+
+ mDeltaY = 0;
+ mModeChanged = false;
+
+ if (mIgnoreSwipingGesture) return;
+
+ mHolding |= HOLD_TOUCH_DOWN;
+
+ if (mFilmMode && mPositionController.isScrolling()) {
+ mDownInScrolling = true;
+ mPositionController.stopScrolling();
+ } else {
+ mDownInScrolling = false;
+ }
+ mHadFling = false;
+ mScrolledAfterDown = false;
+ if (mFilmMode) {
+ int xi = (int) (x + 0.5f);
+ int yi = (int) (y + 0.5f);
+ // We only care about being within the x bounds, necessary for
+ // handling very wide images which are otherwise very hard to fling
+ mTouchBoxIndex = mPositionController.hitTest(xi, getHeight() / 2);
+
+ if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) {
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ } else {
+ mTouchBoxDeletable =
+ mPictures.get(mTouchBoxIndex).isDeletable();
+ }
+ } else {
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public void onUp() {
+ if (mIgnoreSwipingGesture) return;
+
+ mHolding &= ~HOLD_TOUCH_DOWN;
+ mEdgeView.onRelease();
+
+ // If we scrolled in Y direction far enough, treat it as a delete
+ // gesture.
+ if (mFilmMode && mScrolledAfterDown && !mFirstScrollX
+ && mTouchBoxIndex != Integer.MAX_VALUE) {
+ Rect r = mPositionController.getPosition(mTouchBoxIndex);
+ int h = getHeight();
+ if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) {
+ int duration = mPositionController
+ .flingFilmY(mTouchBoxIndex, 0);
+ if (duration >= 0) {
+ mPositionController.setPopFromTop(r.centerY() < h * 0.5f);
+ deleteAfterAnimation(duration);
+ }
+ }
+ }
+
+ if (mIgnoreUpEvent) {
+ mIgnoreUpEvent = false;
+ return;
+ }
+
+ if (!(mFilmMode && !mHadFling && mFirstScrollX
+ && snapToNeighborImage())) {
+ snapback();
+ }
+ }
+
+ public void setSwipingEnabled(boolean enabled) {
+ mIgnoreSwipingGesture = !enabled;
+ }
+ }
+
+ public void setSwipingEnabled(boolean enabled) {
+ mGestureListener.setSwipingEnabled(enabled);
+ }
+
+ private void updateActionBar() {
+ boolean isCamera = mPictures.get(0).isCamera();
+ if (isCamera && !mFilmMode) {
+ // Move into camera in page mode, lock
+ mListener.onActionBarAllowed(false);
+ } else {
+ mListener.onActionBarAllowed(true);
+ if (mFilmMode) mListener.onActionBarWanted();
+ }
+ }
+
+ public void setFilmMode(boolean enabled) {
+ if (mFilmMode == enabled) return;
+ mFilmMode = enabled;
+ mPositionController.setFilmMode(mFilmMode);
+ mModel.setNeedFullImage(!enabled);
+ mModel.setFocusHintDirection(
+ mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT);
+ updateActionBar();
+ mListener.onFilmModeChanged(enabled);
+ }
+
+ 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);
+ }
+ hideUndoBar();
+ }
+
+ public void resume() {
+ mTileView.prepareTextures();
+ mPositionController.skipToFinalPosition();
+ }
+
+ // move to the camera preview and show controls after resume
+ public void resetToFirstPicture() {
+ mModel.moveTo(0);
+ setFilmMode(false);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Undo Bar
+ ////////////////////////////////////////////////////////////////////////////
+
+ private int mUndoBarState;
+ private static final int UNDO_BAR_SHOW = 1;
+ private static final int UNDO_BAR_TIMEOUT = 2;
+ private static final int UNDO_BAR_TOUCHED = 4;
+ private static final int UNDO_BAR_FULL_CAMERA = 8;
+ private static final int UNDO_BAR_DELETE_LAST = 16;
+
+ // "deleteLast" means if the deletion is on the last remaining picture in
+ // the album.
+ private void showUndoBar(boolean deleteLast) {
+ mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
+ mUndoBarState = UNDO_BAR_SHOW;
+ if(deleteLast) mUndoBarState |= UNDO_BAR_DELETE_LAST;
+ mUndoBar.animateVisibility(GLView.VISIBLE);
+ mHandler.sendEmptyMessageDelayed(MSG_UNDO_BAR_TIMEOUT, 3000);
+ if (mListener != null) mListener.onUndoBarVisibilityChanged(true);
+ }
+
+ private void hideUndoBar() {
+ mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
+ mListener.onCommitDeleteImage();
+ mUndoBar.animateVisibility(GLView.INVISIBLE);
+ mUndoBarState = 0;
+ mUndoIndexHint = Integer.MAX_VALUE;
+ mListener.onUndoBarVisibilityChanged(false);
+ }
+
+ // Check if the one of the conditions for hiding the undo bar has been
+ // met. The conditions are:
+ //
+ // 1. It has been three seconds since last showing, and (a) the user has
+ // touched, or (b) the deleted picture is the last remaining picture in the
+ // album.
+ //
+ // 2. The camera is shown in full screen.
+ private void checkHideUndoBar(int addition) {
+ mUndoBarState |= addition;
+ if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return;
+ boolean timeout = (mUndoBarState & UNDO_BAR_TIMEOUT) != 0;
+ boolean touched = (mUndoBarState & UNDO_BAR_TOUCHED) != 0;
+ boolean fullCamera = (mUndoBarState & UNDO_BAR_FULL_CAMERA) != 0;
+ boolean deleteLast = (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0;
+ if ((timeout && deleteLast) || fullCamera || touched) {
+ hideUndoBar();
+ }
+ }
+
+ public boolean canUndo() {
+ return (mUndoBarState & UNDO_BAR_SHOW) != 0;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Rendering
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ if (mFirst) {
+ // Make sure the fields are properly initialized before checking
+ // whether isCamera()
+ mPictures.get(0).reload();
+ }
+ // Check if the camera preview occupies the full screen.
+ boolean full = !mFilmMode && mPictures.get(0).isCamera()
+ && mPositionController.isCenter()
+ && mPositionController.isAtMinimalScale();
+ if (mFirst || full != mFullScreenCamera) {
+ mFullScreenCamera = full;
+ mFirst = false;
+ mListener.onFullScreenChanged(full);
+ if (full) mHandler.sendEmptyMessage(MSG_UNDO_BAR_FULL_CAMERA);
+ }
+
+ // Determine how many photos we need to draw in addition to the center
+ // one.
+ int neighbors;
+ if (mFullScreenCamera) {
+ neighbors = 0;
+ } else {
+ // 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);
+ if (inPageMode && !inCaptureAnimation) {
+ neighbors = 1;
+ } else {
+ neighbors = 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);
+ }
+
+ renderChild(canvas, mEdgeView);
+ renderChild(canvas, mUndoBar);
+
+ 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 & ~HOLD_DELETE) != 0) return;
+ if (mFilmMode || !snapToNeighborImage()) {
+ mPositionController.snapback();
+ }
+ }
+
+ private boolean snapToNeighborImage() {
+ Rect r = mPositionController.getPosition(0);
+ int viewW = getWidth();
+ // Setting the move threshold proportional to the width of the view
+ int moveThreshold = viewW / 5 ;
+ int threshold = moveThreshold + 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
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void switchToImage(int index) {
+ mModel.moveTo(index);
+ }
+
+ 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();
+ if(root == null) return false;
+ 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.skipToFinalPosition();
+ 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 && !mFilmMode) {
+ // Now the capture animation is done, enable the action bar.
+ mListener.onActionBarAllowed(true);
+ mListener.onActionBarWanted();
+ }
+ 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;
+ }
+
+ // Returns the alpha factor in film mode if a picture is not in the center.
+ // The 0.03 lower bound is to make the item always visible a bit.
+ private float getOffsetAlpha(float offset) {
+ offset /= 0.5f;
+ float alpha = (offset > 0) ? (1 - offset) : (1 + offset);
+ return Utils.clamp(alpha, 0.03f, 1f);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // 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 TiledScreenNail)
+ || ((TiledScreenNail) 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);
+
+ int width = sc.getWidth();
+ int height = sc.getHeight();
+
+ int rotation = mModel.getImageRotation(i);
+ RawTexture texture;
+ if ((rotation % 180) == 0) {
+ texture = new RawTexture(width, height, true);
+ canvas.beginRenderTarget(texture);
+ canvas.translate(width / 2f, height / 2f);
+ } else {
+ texture = new RawTexture(height, width, true);
+ canvas.beginRenderTarget(texture);
+ canvas.translate(height / 2f, width / 2f);
+ }
+
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-width / 2f, -height / 2f);
+ sc.draw(canvas, 0, 0, width, height);
+ canvas.endRenderTarget();
+ effect.addEntry(item.getPath(), rect, texture);
+ }
+ return effect;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PopupList.java b/src/com/android/gallery3d/ui/PopupList.java
new file mode 100644
index 000000000..248f50b25
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PopupList.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class PopupList {
+
+ public static interface OnPopupItemClickListener {
+ public boolean onPopupItemClick(int itemId);
+ }
+
+ public static class Item {
+ public final int id;
+ public String title;
+
+ public Item(int id, String title) {
+ this.id = id;
+ this.title = title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+ }
+
+ private final Context mContext;
+ private final View mAnchorView;
+ private final ArrayList<Item> mItems = new ArrayList<Item>();
+ private PopupWindow mPopupWindow;
+ private ListView mContentList;
+ private OnPopupItemClickListener mOnPopupItemClickListener;
+ private int mPopupOffsetX;
+ private int mPopupOffsetY;
+ private int mPopupWidth;
+ private int mPopupHeight;
+
+ public PopupList(Context context, View anchorView) {
+ mContext = context;
+ mAnchorView = anchorView;
+ }
+
+ public void setOnPopupItemClickListener(OnPopupItemClickListener listener) {
+ mOnPopupItemClickListener = listener;
+ }
+
+ public void addItem(int id, String title) {
+ mItems.add(new Item(id, title));
+ }
+
+ public void clearItems() {
+ mItems.clear();
+ }
+
+ private final PopupWindow.OnDismissListener mOnDismissListener =
+ new PopupWindow.OnDismissListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onDismiss() {
+ if (mPopupWindow == null) return;
+ mPopupWindow = null;
+ ViewTreeObserver observer = mAnchorView.getViewTreeObserver();
+ if (observer.isAlive()) {
+ // We used the deprecated function for backward compatibility
+ // The new "removeOnGlobalLayoutListener" is introduced in API level 16
+ observer.removeGlobalOnLayoutListener(mOnGLobalLayoutListener);
+ }
+ }
+ };
+
+ private final OnItemClickListener mOnItemClickListener =
+ new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (mPopupWindow == null) return;
+ mPopupWindow.dismiss();
+ if (mOnPopupItemClickListener != null) {
+ mOnPopupItemClickListener.onPopupItemClick((int) id);
+ }
+ }
+ };
+
+ private final OnGlobalLayoutListener mOnGLobalLayoutListener =
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (mPopupWindow == null) return;
+ updatePopupLayoutParams();
+ // Need to update the position of the popup window
+ mPopupWindow.update(mAnchorView,
+ mPopupOffsetX, mPopupOffsetY, mPopupWidth, mPopupHeight);
+ }
+ };
+
+ public void show() {
+ if (mPopupWindow != null) return;
+ mAnchorView.getViewTreeObserver()
+ .addOnGlobalLayoutListener(mOnGLobalLayoutListener);
+ mPopupWindow = createPopupWindow();
+ updatePopupLayoutParams();
+ mPopupWindow.setWidth(mPopupWidth);
+ mPopupWindow.setHeight(mPopupHeight);
+ mPopupWindow.showAsDropDown(mAnchorView, mPopupOffsetX, mPopupOffsetY);
+ }
+
+ private void updatePopupLayoutParams() {
+ ListView content = mContentList;
+ PopupWindow popup = mPopupWindow;
+
+ Rect p = new Rect();
+ popup.getBackground().getPadding(p);
+
+ int maxHeight = mPopupWindow.getMaxAvailableHeight(mAnchorView) - p.top - p.bottom;
+ mContentList.measure(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST));
+ mPopupWidth = content.getMeasuredWidth() + p.top + p.bottom;
+ mPopupHeight = Math.min(maxHeight, content.getMeasuredHeight() + p.left + p.right);
+ mPopupOffsetX = -p.left;
+ mPopupOffsetY = -p.top;
+ }
+
+ private PopupWindow createPopupWindow() {
+ PopupWindow popup = new PopupWindow(mContext);
+ popup.setOnDismissListener(mOnDismissListener);
+
+ popup.setBackgroundDrawable(mContext.getResources().getDrawable(
+ R.drawable.menu_dropdown_panel_holo_dark));
+
+ mContentList = new ListView(mContext, null,
+ android.R.attr.dropDownListViewStyle);
+ mContentList.setAdapter(new ItemDataAdapter());
+ mContentList.setOnItemClickListener(mOnItemClickListener);
+ popup.setContentView(mContentList);
+ popup.setFocusable(true);
+ popup.setOutsideTouchable(true);
+
+ return popup;
+ }
+
+ public Item findItem(int id) {
+ for (Item item : mItems) {
+ if (item.id == id) return item;
+ }
+ return null;
+ }
+
+ private class ItemDataAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mItems.get(position).id;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(mContext)
+ .inflate(R.layout.popup_list_item, null);
+ }
+ TextView text = (TextView) convertView.findViewById(android.R.id.text1);
+ text.setText(mItems.get(position).title);
+ return convertView;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
new file mode 100644
index 000000000..6a4bcea87
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -0,0 +1,1821 @@
+/*
+ * Copyright (C) 2011 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.Rect;
+import android.util.Log;
+import android.widget.Scroller;
+
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.PhotoView.Size;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.RangeIntArray;
+
+class PositionController {
+ private static final String TAG = "PositionController";
+
+ public static final int IMAGE_AT_LEFT_EDGE = 1;
+ public static final int IMAGE_AT_RIGHT_EDGE = 2;
+ public static final int IMAGE_AT_TOP_EDGE = 4;
+ public static final int IMAGE_AT_BOTTOM_EDGE = 8;
+
+ public static final int CAPTURE_ANIMATION_TIME = 700;
+ public static final int SNAPBACK_ANIMATION_TIME = 600;
+
+ // Special values for animation time.
+ private static final long NO_ANIMATION = -1;
+ private static final long LAST_ANIMATION = -2;
+
+ private static final int ANIM_KIND_NONE = -1;
+ private static final int ANIM_KIND_SCROLL = 0;
+ private static final int ANIM_KIND_SCALE = 1;
+ private static final int ANIM_KIND_SNAPBACK = 2;
+ private static final int ANIM_KIND_SLIDE = 3;
+ private static final int ANIM_KIND_ZOOM = 4;
+ private static final int ANIM_KIND_OPENING = 5;
+ private static final int ANIM_KIND_FLING = 6;
+ private static final int ANIM_KIND_FLING_X = 7;
+ private static final int ANIM_KIND_DELETE = 8;
+ private static final int ANIM_KIND_CAPTURE = 9;
+
+ // Animation time in milliseconds. The order must match ANIM_KIND_* above.
+ //
+ // The values for ANIM_KIND_FLING_X does't matter because we use
+ // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's
+ // faster for Animatable.advanceAnimation() to calculate the progress
+ // (always 1).
+ private static final int ANIM_TIME[] = {
+ 0, // ANIM_KIND_SCROLL
+ 0, // ANIM_KIND_SCALE
+ SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK
+ 400, // ANIM_KIND_SLIDE
+ 300, // ANIM_KIND_ZOOM
+ 300, // ANIM_KIND_OPENING
+ 0, // ANIM_KIND_FLING (the duration is calculated dynamically)
+ 0, // ANIM_KIND_FLING_X (see the comment above)
+ 0, // ANIM_KIND_DELETE (the duration is calculated dynamically)
+ CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE
+ };
+
+ // 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;
+
+ // For user's gestures, we give a temporary extra scaling range which goes
+ // above or below the usual scaling limits.
+ private static final float SCALE_MIN_EXTRA = 0.7f;
+ private static final float SCALE_MAX_EXTRA = 1.4f;
+
+ // Setting this true makes the extra scaling range permanent (until this is
+ // set to false again).
+ private boolean mExtraScalingRange = false;
+
+ // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
+ private boolean mFilmMode = false;
+
+ // These are the limits for width / height of the picture in film mode.
+ private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
+ private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
+ private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
+ private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
+
+ // In addition to the focused box (index == 0). We also keep information
+ // about this many boxes on each side.
+ private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
+ private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1];
+
+ private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
+ private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
+
+ // These are constants for the delete gesture.
+ private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms
+ private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms
+
+ private Listener mListener;
+ private volatile Rect mOpenAnimationRect;
+
+ // Use a large enough value, so we won't see the gray shadow in the beginning.
+ private int mViewW = 1200;
+ private int mViewH = 1200;
+
+ // A scaling gesture is in progress.
+ private boolean mInScale;
+ // The focus point of the scaling gesture, relative to the center of the
+ // picture in bitmap pixels.
+ private float mFocusX, mFocusY;
+
+ // whether there is a previous/next picture.
+ private boolean mHasPrev, mHasNext;
+
+ // This is used by the fling animation (page mode).
+ private FlingScroller mPageScroller;
+
+ // This is used by the fling animation (film mode).
+ private Scroller mFilmScroller;
+
+ // The bound of the stable region that the focused box can stay, see the
+ // comments above calculateStableBound() for details.
+ private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
+
+ // Constrained frame is a rectangle that the focused box should fit into if
+ // it is constrained. It has two effects:
+ //
+ // (1) In page mode, if the focused box is constrained, scaling for the
+ // focused box is adjusted to fit into the constrained frame, instead of the
+ // whole view.
+ //
+ // (2) In page mode, if the focused box is constrained, the mPlatform's
+ // default center (mDefaultX/Y) is moved to the center of the constrained
+ // frame, instead of the view center.
+ //
+ private Rect mConstrainedFrame = new Rect();
+
+ // Whether the focused box is constrained.
+ //
+ // Our current program's first call to moveBox() sets constrained = true, so
+ // we set the initial value of this variable to true, and we will not see
+ // see unwanted transition animation.
+ private boolean mConstrained = true;
+
+ //
+ // ___________________________________________________________
+ // | _____ _____ _____ _____ _____ |
+ // | | | | | | | | | | | |
+ // | | Box | | Box | | Box*| | Box | | Box | |
+ // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| |
+ // | Gap Gap Gap Gap |
+ // |___________________________________________________________|
+ //
+ // <-- Platform -->
+ //
+ // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY)
+
+ private Platform mPlatform = new Platform();
+ private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+ // The gap at the right of a Box i is at index i. The gap at the left of a
+ // Box i is at index i - 1.
+ private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+ private FilmRatio mFilmRatio = new FilmRatio();
+
+ // These are only used during moveBox().
+ private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+ private RangeArray<Gap> mTempGaps =
+ new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+
+ // The output of the PositionController. Available through getPosition().
+ private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
+
+ // The direction of a new picture should appear. New pictures pop from top
+ // if this value is true, or from bottom if this value is false.
+ boolean mPopFromTop;
+
+ public interface Listener {
+ void invalidate();
+ boolean isHoldingDown();
+ boolean isHoldingDelete();
+
+ // EdgeView
+ void onPull(int offset, int direction);
+ void onRelease();
+ void onAbsorb(int velocity, int direction);
+ }
+
+ static {
+ // Initialize the CENTER_OUT_INDEX array.
+ // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX
+ // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX
+ for (int i = 0; i < CENTER_OUT_INDEX.length; i++) {
+ int j = (i + 1) / 2;
+ if ((i & 1) == 0) j = -j;
+ CENTER_OUT_INDEX[i] = j;
+ }
+ }
+
+ public PositionController(Context context, Listener listener) {
+ mListener = listener;
+ mPageScroller = new FlingScroller();
+ mFilmScroller = new Scroller(context, null, false);
+
+ // Initialize the areas.
+ initPlatform();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.put(i, new Box());
+ initBox(i);
+ mRects.put(i, new Rect());
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.put(i, new Gap());
+ initGap(i);
+ }
+ }
+
+ public void setOpenAnimationRect(Rect r) {
+ mOpenAnimationRect = r;
+ }
+
+ public void setViewSize(int viewW, int viewH) {
+ if (viewW == mViewW && viewH == mViewH) return;
+
+ boolean wasMinimal = isAtMinimalScale();
+
+ mViewW = viewW;
+ mViewH = viewH;
+ initPlatform();
+
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ setBoxSize(i, viewW, viewH, true);
+ }
+
+ updateScaleAndGapLimit();
+
+ // If the focused box was at minimal scale, we try to make it the
+ // minimal scale under the new view size.
+ if (wasMinimal) {
+ Box b = mBoxes.get(0);
+ b.mCurrentScale = b.mScaleMin;
+ }
+
+ // If we have the opening animation, do it. Otherwise go directly to the
+ // right position.
+ if (!startOpeningAnimationIfNeeded()) {
+ skipToFinalPosition();
+ }
+ }
+
+ public void setConstrainedFrame(Rect cFrame) {
+ if (mConstrainedFrame.equals(cFrame)) return;
+ mConstrainedFrame.set(cFrame);
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ snapAndRedraw();
+ }
+
+ public void forceImageSize(int index, Size s) {
+ if (s.width == 0 || s.height == 0) return;
+ Box b = mBoxes.get(index);
+ b.mImageW = s.width;
+ b.mImageH = s.height;
+ return;
+ }
+
+ public void setImageSize(int index, Size s, Rect cFrame) {
+ if (s.width == 0 || s.height == 0) return;
+
+ boolean needUpdate = false;
+ if (cFrame != null && !mConstrainedFrame.equals(cFrame)) {
+ mConstrainedFrame.set(cFrame);
+ mPlatform.updateDefaultXY();
+ needUpdate = true;
+ }
+ needUpdate |= setBoxSize(index, s.width, s.height, false);
+
+ if (!needUpdate) return;
+ updateScaleAndGapLimit();
+ snapAndRedraw();
+ }
+
+ // Returns false if the box size doesn't change.
+ private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
+ Box b = mBoxes.get(i);
+ boolean wasViewSize = b.mUseViewSize;
+
+ // If we already have an image size, we don't want to use the view size.
+ if (!wasViewSize && isViewSize) return false;
+
+ b.mUseViewSize = isViewSize;
+
+ if (width == b.mImageW && height == b.mImageH) {
+ return false;
+ }
+
+ // The ratio of the old size and the new size.
+ //
+ // If the aspect ratio changes, we don't know if it is because one side
+ // grows or the other side shrinks. Currently we just assume the view
+ // angle of the longer side doesn't change (so the aspect ratio change
+ // is because the view angle of the shorter side changes). This matches
+ // what camera preview does.
+ float ratio = (width > height)
+ ? (float) b.mImageW / width
+ : (float) b.mImageH / height;
+
+ b.mImageW = width;
+ b.mImageH = height;
+
+ // If this is the first time we receive an image size or we are in fullscreen,
+ // we change the scale directly. Otherwise adjust the scales by a ratio,
+ // and snapback will animate the scale into the min/max bounds if necessary.
+ if ((wasViewSize && !isViewSize) || !mFilmMode) {
+ b.mCurrentScale = getMinimalScale(b);
+ b.mAnimationStartTime = NO_ANIMATION;
+ } else {
+ b.mCurrentScale *= ratio;
+ b.mFromScale *= ratio;
+ b.mToScale *= ratio;
+ }
+
+ if (i == 0) {
+ mFocusX /= ratio;
+ mFocusY /= ratio;
+ }
+
+ return true;
+ }
+
+ private boolean startOpeningAnimationIfNeeded() {
+ if (mOpenAnimationRect == null) return false;
+ Box b = mBoxes.get(0);
+ if (b.mUseViewSize) return false;
+
+ // Start animation from the saved rectangle if we have one.
+ Rect r = mOpenAnimationRect;
+ mOpenAnimationRect = null;
+
+ mPlatform.mCurrentX = r.centerX() - mViewW / 2;
+ b.mCurrentY = r.centerY() - mViewH / 2;
+ b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
+ r.height() / (float) b.mImageH);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin,
+ ANIM_KIND_OPENING);
+
+ // Animate from large gaps for neighbor boxes to avoid them
+ // shown on the screen during opening animation.
+ for (int i = -1; i < 1; i++) {
+ Gap g = mGaps.get(i);
+ g.mCurrentGap = mViewW;
+ g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING);
+ }
+
+ return true;
+ }
+
+ public void setFilmMode(boolean enabled) {
+ if (enabled == mFilmMode) return;
+ mFilmMode = enabled;
+
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ stopAnimation();
+ snapAndRedraw();
+ }
+
+ public void setExtraScalingRange(boolean enabled) {
+ if (mExtraScalingRange == enabled) return;
+ mExtraScalingRange = enabled;
+ if (!enabled) {
+ snapAndRedraw();
+ }
+ }
+
+ // This should be called whenever the scale range of boxes or the default
+ // gap size may change. Currently this can happen due to change of view
+ // size, image size, mFilmMode, mConstrained, and mConstrainedFrame.
+ private void updateScaleAndGapLimit() {
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ }
+
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Gap g = mGaps.get(i);
+ g.mDefaultSize = getDefaultGapSize(i);
+ }
+ }
+
+ // Returns the default gap size according the the size of the boxes around
+ // the gap and the current mode.
+ private int getDefaultGapSize(int i) {
+ if (mFilmMode) return IMAGE_GAP;
+ Box a = mBoxes.get(i);
+ Box b = mBoxes.get(i + 1);
+ return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
+ }
+
+ // Here is how we layout the boxes in the page mode.
+ //
+ // previous current next
+ // ___________ ________________ __________
+ // | _______ | | __________ | | ______ |
+ // | | | | | | right->| | | | | |
+ // | | |<-------->|<--left | | | | | |
+ // | |_______| | | | |__________| | | |______| |
+ // |___________| | |________________| |__________|
+ // | <--> gapToSide()
+ // |
+ // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
+ private int gapToSide(Box b) {
+ return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
+ }
+
+ // Stop all animations at where they are now.
+ public void stopAnimation() {
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
+ }
+ }
+
+ public void skipAnimation() {
+ if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
+ mPlatform.mCurrentX = mPlatform.mToX;
+ mPlatform.mCurrentY = mPlatform.mToY;
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ if (b.mAnimationStartTime == NO_ANIMATION) continue;
+ b.mCurrentY = b.mToY;
+ b.mCurrentScale = b.mToScale;
+ b.mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Gap g = mGaps.get(i);
+ if (g.mAnimationStartTime == NO_ANIMATION) continue;
+ g.mCurrentGap = g.mToGap;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+ redraw();
+ }
+
+ public void snapback() {
+ snapAndRedraw();
+ }
+
+ public void skipToFinalPosition() {
+ stopAnimation();
+ snapAndRedraw();
+ skipAnimation();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Start an animations for the focused box
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void zoomIn(float tapX, float tapY, float targetScale) {
+ tapX -= mViewW / 2;
+ tapY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+
+ // Convert the tap position to distance to center in bitmap coordinates
+ float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
+ float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
+
+ int x = (int) (-tempX * targetScale + 0.5f);
+ int y = (int) (-tempY * targetScale + 0.5f);
+
+ calculateStableBound(targetScale);
+ int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
+ int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
+ targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
+
+ startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
+ }
+
+ public void resetToFullView() {
+ Box b = mBoxes.get(0);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM);
+ }
+
+ public void beginScale(float focusX, float focusY) {
+ focusX -= mViewW / 2;
+ focusY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+ mInScale = true;
+ mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
+ mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
+ }
+
+ // Scales the image by the given factor.
+ // Returns an out-of-range indicator:
+ // 1 if the intended scale is too large for the stable range.
+ // 0 if the intended scale is in the stable range.
+ // -1 if the intended scale is too small for the stable range.
+ public int scaleBy(float s, float focusX, float focusY) {
+ focusX -= mViewW / 2;
+ focusY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // We want to keep the focus point (on the bitmap) the same as when we
+ // begin the scale gesture, that is,
+ //
+ // (focusX' - currentX') / scale' = (focusX - currentX) / scale
+ //
+ s = b.clampScale(s * getTargetScale(b));
+ int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
+ int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
+ startAnimation(x, y, s, ANIM_KIND_SCALE);
+ if (s < b.mScaleMin) return -1;
+ if (s > b.mScaleMax) return 1;
+ return 0;
+ }
+
+ public void endScale() {
+ mInScale = false;
+ snapAndRedraw();
+ }
+
+ // Slide the focused box to the center of the view.
+ public void startHorizontalSlide() {
+ Box b = mBoxes.get(0);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE);
+ }
+
+ // Slide the focused box to the center of the view with the capture
+ // animation. In addition to the sliding, the animation will also scale the
+ // the focused box, the specified neighbor box, and the gap between the
+ // two. The specified offset should be 1 or -1.
+ public void startCaptureAnimationSlide(int offset) {
+ Box b = mBoxes.get(0);
+ Box n = mBoxes.get(offset); // the neighbor box
+ Gap g = mGaps.get(offset); // the gap between the two boxes
+
+ mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY,
+ ANIM_KIND_CAPTURE);
+ b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE);
+ n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE);
+ g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
+ redraw();
+ }
+
+ // Only allow scrolling when we are not currently in an animation or we
+ // are in some animation with can be interrupted.
+ private boolean canScroll() {
+ Box b = mBoxes.get(0);
+ if (b.mAnimationStartTime == NO_ANIMATION) return true;
+ switch (b.mAnimationKind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ return true;
+ }
+ return false;
+ }
+
+ public void scrollPage(int dx, int dy) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ calculateStableBound(b.mCurrentScale);
+
+ int x = p.mCurrentX + dx;
+ int y = b.mCurrentY + dy;
+
+ // Vertical direction: If we have space to move in the vertical
+ // direction, we show the edge effect when scrolling reaches the edge.
+ if (mBoundTop != mBoundBottom) {
+ if (y < mBoundTop) {
+ mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
+ } else if (y > mBoundBottom) {
+ mListener.onPull(y - mBoundBottom, EdgeView.TOP);
+ }
+ }
+
+ y = Utils.clamp(y, mBoundTop, mBoundBottom);
+
+ // Horizontal direction: we show the edge effect when the scrolling
+ // tries to go left of the first image or go right of the last image.
+ if (!mHasPrev && x > mBoundRight) {
+ int pixels = x - mBoundRight;
+ mListener.onPull(pixels, EdgeView.LEFT);
+ x = mBoundRight;
+ } else if (!mHasNext && x < mBoundLeft) {
+ int pixels = mBoundLeft - x;
+ mListener.onPull(pixels, EdgeView.RIGHT);
+ x = mBoundLeft;
+ }
+
+ startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
+ }
+
+ public void scrollFilmX(int dx) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // Only allow scrolling when we are not currently in an animation or we
+ // are in some animation with can be interrupted.
+ if (b.mAnimationStartTime != NO_ANIMATION) {
+ switch (b.mAnimationKind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ break;
+ default:
+ return;
+ }
+ }
+
+ int x = p.mCurrentX + dx;
+
+ // Horizontal direction: we show the edge effect when the scrolling
+ // tries to go left of the first image or go right of the last image.
+ x -= mPlatform.mDefaultX;
+ if (!mHasPrev && x > 0) {
+ mListener.onPull(x, EdgeView.LEFT);
+ x = 0;
+ } else if (!mHasNext && x < 0) {
+ mListener.onPull(-x, EdgeView.RIGHT);
+ x = 0;
+ }
+ x += mPlatform.mDefaultX;
+ startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL);
+ }
+
+ public void scrollFilmY(int boxIndex, int dy) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(boxIndex);
+ int y = b.mCurrentY + dy;
+ b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL);
+ redraw();
+ }
+
+ public boolean flingPage(int velocityX, int velocityY) {
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // We only want to do fling when the picture is zoomed-in.
+ if (viewWiderThanScaledImage(b.mCurrentScale) &&
+ viewTallerThanScaledImage(b.mCurrentScale)) {
+ return false;
+ }
+
+ // We only allow flinging in the directions where it won't go over the
+ // picture.
+ int edges = getImageAtEdges();
+ if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
+ (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
+ velocityX = 0;
+ }
+ if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
+ (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
+ velocityY = 0;
+ }
+
+ if (velocityX == 0 && velocityY == 0) return false;
+
+ mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
+ mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
+ int targetX = mPageScroller.getFinalX();
+ int targetY = mPageScroller.getFinalY();
+ ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
+ return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
+ }
+
+ public boolean flingFilmX(int velocityX) {
+ if (velocityX == 0) return false;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // If we are already at the edge, don't start the fling.
+ int defaultX = p.mDefaultX;
+ if ((!mHasPrev && p.mCurrentX >= defaultX)
+ || (!mHasNext && p.mCurrentX <= defaultX)) {
+ return false;
+ }
+
+ mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+ int targetX = mFilmScroller.getFinalX();
+ return startAnimation(
+ targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X);
+ }
+
+ // Moves the specified box out of screen. If velocityY is 0, a default
+ // velocity is used. Returns the time for the duration, or -1 if we cannot
+ // not do the animation.
+ public int flingFilmY(int boxIndex, int velocityY) {
+ Box b = mBoxes.get(boxIndex);
+
+ // Calculate targetY
+ int h = heightOf(b);
+ int targetY;
+ int FUZZY = 3; // TODO: figure out why this is needed.
+ if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) {
+ targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY;
+ } else {
+ targetY = (mViewH + 1) / 2 + h / 2 + FUZZY;
+ }
+
+ // Calculate duration
+ int duration;
+ if (velocityY != 0) {
+ duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f
+ / Math.abs(velocityY));
+ duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration);
+ } else {
+ duration = DEFAULT_DELETE_ANIMATION_DURATION;
+ }
+
+ // Start animation
+ ANIM_TIME[ANIM_KIND_DELETE] = duration;
+ if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) {
+ redraw();
+ return duration;
+ }
+ return -1;
+ }
+
+ // Returns the index of the box which contains the given point (x, y)
+ // Returns Integer.MAX_VALUE if there is no hit. There may be more than
+ // one box contains the given point, and we want to give priority to the
+ // one closer to the focused index (0).
+ public int hitTest(int x, int y) {
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ int j = CENTER_OUT_INDEX[i];
+ Rect r = mRects.get(j);
+ if (r.contains(x, y)) {
+ return j;
+ }
+ }
+
+ return Integer.MAX_VALUE;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Redraw
+ //
+ // If a method changes box positions directly, redraw()
+ // should be called.
+ //
+ // If a method may also cause a snapback to happen, snapAndRedraw() should
+ // be called.
+ //
+ // If a method starts an animation to change the position of focused box,
+ // startAnimation() should be called.
+ //
+ // If time advances to change the box position, advanceAnimation() should
+ // be called.
+ ////////////////////////////////////////////////////////////////////////////
+ private void redraw() {
+ layoutAndSetPosition();
+ mListener.invalidate();
+ }
+
+ private void snapAndRedraw() {
+ mPlatform.startSnapback();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.get(i).startSnapback();
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.get(i).startSnapback();
+ }
+ mFilmRatio.startSnapback();
+ redraw();
+ }
+
+ private boolean startAnimation(int targetX, int targetY, float targetScale,
+ int kind) {
+ boolean changed = false;
+ changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
+ changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
+ if (changed) redraw();
+ return changed;
+ }
+
+ public void advanceAnimation() {
+ boolean changed = false;
+ changed |= mPlatform.advanceAnimation();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ changed |= mBoxes.get(i).advanceAnimation();
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ changed |= mGaps.get(i).advanceAnimation();
+ }
+ changed |= mFilmRatio.advanceAnimation();
+ if (changed) redraw();
+ }
+
+ public boolean inOpeningAnimation() {
+ return (mPlatform.mAnimationKind == ANIM_KIND_OPENING &&
+ mPlatform.mAnimationStartTime != NO_ANIMATION) ||
+ (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING &&
+ mBoxes.get(0).mAnimationStartTime != NO_ANIMATION);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Layout
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Returns the display width of this box.
+ private int widthOf(Box b) {
+ return (int) (b.mImageW * b.mCurrentScale + 0.5f);
+ }
+
+ // Returns the display height of this box.
+ private int heightOf(Box b) {
+ return (int) (b.mImageH * b.mCurrentScale + 0.5f);
+ }
+
+ // Returns the display width of this box, using the given scale.
+ private int widthOf(Box b, float scale) {
+ return (int) (b.mImageW * scale + 0.5f);
+ }
+
+ // Returns the display height of this box, using the given scale.
+ private int heightOf(Box b, float scale) {
+ return (int) (b.mImageH * scale + 0.5f);
+ }
+
+ // Convert the information in mPlatform and mBoxes to mRects, so the user
+ // can get the position of each box by getPosition().
+ //
+ // Note we go from center-out because each box's X coordinate
+ // is relative to its anchor box (except the focused box).
+ private void layoutAndSetPosition() {
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ convertBoxToRect(CENTER_OUT_INDEX[i]);
+ }
+ //dumpState();
+ }
+
+ @SuppressWarnings("unused")
+ private void dumpState() {
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
+ }
+
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ dumpRect(CENTER_OUT_INDEX[i]);
+ }
+
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ for (int j = i + 1; j <= BOX_MAX; j++) {
+ if (Rect.intersects(mRects.get(i), mRects.get(j))) {
+ Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
+ }
+ }
+ }
+ }
+
+ private void dumpRect(int i) {
+ StringBuilder sb = new StringBuilder();
+ Rect r = mRects.get(i);
+ sb.append("Rect " + i + ":");
+ sb.append("(");
+ sb.append(r.centerX());
+ sb.append(",");
+ sb.append(r.centerY());
+ sb.append(") [");
+ sb.append(r.width());
+ sb.append("x");
+ sb.append(r.height());
+ sb.append("]");
+ Log.d(TAG, sb.toString());
+ }
+
+ private void convertBoxToRect(int i) {
+ Box b = mBoxes.get(i);
+ Rect r = mRects.get(i);
+ int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
+ int w = widthOf(b);
+ int h = heightOf(b);
+ if (i == 0) {
+ int x = mPlatform.mCurrentX + mViewW / 2;
+ r.left = x - w / 2;
+ r.right = r.left + w;
+ } else if (i > 0) {
+ Rect a = mRects.get(i - 1);
+ Gap g = mGaps.get(i - 1);
+ r.left = a.right + g.mCurrentGap;
+ r.right = r.left + w;
+ } else { // i < 0
+ Rect a = mRects.get(i + 1);
+ Gap g = mGaps.get(i);
+ r.right = a.left - g.mCurrentGap;
+ r.left = r.right - w;
+ }
+ r.top = y - h / 2;
+ r.bottom = r.top + h;
+ }
+
+ // Returns the position of a box.
+ public Rect getPosition(int index) {
+ return mRects.get(index);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Box management
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Initialize the platform to be at the view center.
+ private void initPlatform() {
+ mPlatform.updateDefaultXY();
+ mPlatform.mCurrentX = mPlatform.mDefaultX;
+ mPlatform.mCurrentY = mPlatform.mDefaultY;
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ // Initialize a box to have the size of the view.
+ private void initBox(int index) {
+ Box b = mBoxes.get(index);
+ b.mImageW = mViewW;
+ b.mImageH = mViewH;
+ b.mUseViewSize = true;
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ b.mCurrentY = 0;
+ b.mCurrentScale = b.mScaleMin;
+ b.mAnimationStartTime = NO_ANIMATION;
+ b.mAnimationKind = ANIM_KIND_NONE;
+ }
+
+ // Initialize a box to a given size.
+ private void initBox(int index, Size size) {
+ if (size.width == 0 || size.height == 0) {
+ initBox(index);
+ return;
+ }
+ Box b = mBoxes.get(index);
+ b.mImageW = size.width;
+ b.mImageH = size.height;
+ b.mUseViewSize = false;
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ b.mCurrentY = 0;
+ b.mCurrentScale = b.mScaleMin;
+ b.mAnimationStartTime = NO_ANIMATION;
+ b.mAnimationKind = ANIM_KIND_NONE;
+ }
+
+ // Initialize a gap. This can only be called after the boxes around the gap
+ // has been initialized.
+ private void initGap(int index) {
+ Gap g = mGaps.get(index);
+ g.mDefaultSize = getDefaultGapSize(index);
+ g.mCurrentGap = g.mDefaultSize;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ private void initGap(int index, int size) {
+ Gap g = mGaps.get(index);
+ g.mDefaultSize = getDefaultGapSize(index);
+ g.mCurrentGap = size;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ @SuppressWarnings("unused")
+ private void debugMoveBox(int fromIndex[]) {
+ StringBuilder s = new StringBuilder("moveBox:");
+ for (int i = 0; i < fromIndex.length; i++) {
+ int j = fromIndex[i];
+ if (j == Integer.MAX_VALUE) {
+ s.append(" N");
+ } else {
+ s.append(" ");
+ s.append(fromIndex[i]);
+ }
+ }
+ Log.d(TAG, s.toString());
+ }
+
+ // Move the boxes: it may indicate focus change, box deleted, box appearing,
+ // box reordered, etc.
+ //
+ // Each element in the fromIndex array indicates where each box was in the
+ // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
+ // means the box is new.
+ //
+ // For example:
+ // N N N N N N N -- all new boxes
+ // -3 -2 -1 0 1 2 3 -- nothing changed
+ // -2 -1 0 1 2 3 N -- focus goes to the next box
+ // N -3 -2 -1 0 1 2 -- focus goes to the previous box
+ // -3 -2 -1 1 2 3 N -- the focused box was deleted.
+ //
+ // hasPrev/hasNext indicates if there are previous/next boxes for the
+ // focused box. constrained indicates whether the focused box should be put
+ // into the constrained frame.
+ public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
+ boolean constrained, Size[] sizes) {
+ //debugMoveBox(fromIndex);
+ mHasPrev = hasPrev;
+ mHasNext = hasNext;
+
+ RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
+
+ // 1. Get the absolute X coordinates for the boxes.
+ layoutAndSetPosition();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ Rect r = mRects.get(i);
+ b.mAbsoluteX = r.centerX() - mViewW / 2;
+ }
+
+ // 2. copy boxes and gaps to temporary storage.
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mTempBoxes.put(i, mBoxes.get(i));
+ mBoxes.put(i, null);
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mTempGaps.put(i, mGaps.get(i));
+ mGaps.put(i, null);
+ }
+
+ // 3. move back boxes that are used in the new array.
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ int j = from.get(i);
+ if (j == Integer.MAX_VALUE) continue;
+ mBoxes.put(i, mTempBoxes.get(j));
+ mTempBoxes.put(j, null);
+ }
+
+ // 4. move back gaps if both boxes around it are kept together.
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ int j = from.get(i);
+ if (j == Integer.MAX_VALUE) continue;
+ int k = from.get(i + 1);
+ if (k == Integer.MAX_VALUE) continue;
+ if (j + 1 == k) {
+ mGaps.put(i, mTempGaps.get(j));
+ mTempGaps.put(j, null);
+ }
+ }
+
+ // 5. recycle the boxes that are not used in the new array.
+ int k = -BOX_MAX;
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ if (mBoxes.get(i) != null) continue;
+ while (mTempBoxes.get(k) == null) {
+ k++;
+ }
+ mBoxes.put(i, mTempBoxes.get(k++));
+ initBox(i, sizes[i + BOX_MAX]);
+ }
+
+ // 6. Now give the recycled box a reasonable absolute X position.
+ //
+ // First try to find the first and the last box which the absolute X
+ // position is known.
+ int first, last;
+ for (first = -BOX_MAX; first <= BOX_MAX; first++) {
+ if (from.get(first) != Integer.MAX_VALUE) break;
+ }
+ for (last = BOX_MAX; last >= -BOX_MAX; last--) {
+ if (from.get(last) != Integer.MAX_VALUE) break;
+ }
+ // If there is no box has known X position at all, make the focused one
+ // as known.
+ if (first > BOX_MAX) {
+ mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
+ first = last = 0;
+ }
+ // Now for those boxes between first and last, assign their position to
+ // align to the previous box or the next box with known position. For
+ // the boxes before first or after last, we will use a new default gap
+ // size below.
+
+ // Align to the previous box
+ for (int i = Math.max(0, first + 1); i < last; i++) {
+ if (from.get(i) != Integer.MAX_VALUE) continue;
+ Box a = mBoxes.get(i - 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
+ + getDefaultGapSize(i);
+ if (mPopFromTop) {
+ b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+ } else {
+ b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+ }
+ }
+
+ // Align to the next box
+ for (int i = Math.min(-1, last - 1); i > first; i--) {
+ if (from.get(i) != Integer.MAX_VALUE) continue;
+ Box a = mBoxes.get(i + 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
+ - getDefaultGapSize(i);
+ if (mPopFromTop) {
+ b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+ } else {
+ b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+ }
+ }
+
+ // 7. recycle the gaps that are not used in the new array.
+ k = -BOX_MAX;
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ if (mGaps.get(i) != null) continue;
+ while (mTempGaps.get(k) == null) {
+ k++;
+ }
+ mGaps.put(i, mTempGaps.get(k++));
+ Box a = mBoxes.get(i);
+ Box b = mBoxes.get(i + 1);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ if (i >= first && i < last) {
+ int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
+ initGap(i, g);
+ } else {
+ initGap(i);
+ }
+ }
+
+ // 8. calculate the new absolute X coordinates for those box before
+ // first or after last.
+ for (int i = first - 1; i >= -BOX_MAX; i--) {
+ Box a = mBoxes.get(i + 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ Gap g = mGaps.get(i);
+ b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
+ }
+
+ for (int i = last + 1; i <= BOX_MAX; i++) {
+ Box a = mBoxes.get(i - 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ Gap g = mGaps.get(i - 1);
+ b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
+ }
+
+ // 9. offset the Platform position
+ int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
+ mPlatform.mCurrentX += dx;
+ mPlatform.mFromX += dx;
+ mPlatform.mToX += dx;
+ mPlatform.mFlingOffset += dx;
+
+ if (mConstrained != constrained) {
+ mConstrained = constrained;
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ }
+
+ snapAndRedraw();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Public utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ public boolean isAtMinimalScale() {
+ Box b = mBoxes.get(0);
+ return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
+ }
+
+ public boolean isCenter() {
+ Box b = mBoxes.get(0);
+ return mPlatform.mCurrentX == mPlatform.mDefaultX
+ && b.mCurrentY == 0;
+ }
+
+ public int getImageWidth() {
+ Box b = mBoxes.get(0);
+ return b.mImageW;
+ }
+
+ public int getImageHeight() {
+ Box b = mBoxes.get(0);
+ return b.mImageH;
+ }
+
+ public float getImageScale() {
+ Box b = mBoxes.get(0);
+ return b.mCurrentScale;
+ }
+
+ public int getImageAtEdges() {
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+ calculateStableBound(b.mCurrentScale);
+ int edges = 0;
+ if (p.mCurrentX <= mBoundLeft) {
+ edges |= IMAGE_AT_RIGHT_EDGE;
+ }
+ if (p.mCurrentX >= mBoundRight) {
+ edges |= IMAGE_AT_LEFT_EDGE;
+ }
+ if (b.mCurrentY <= mBoundTop) {
+ edges |= IMAGE_AT_BOTTOM_EDGE;
+ }
+ if (b.mCurrentY >= mBoundBottom) {
+ edges |= IMAGE_AT_TOP_EDGE;
+ }
+ return edges;
+ }
+
+ public boolean isScrolling() {
+ return mPlatform.mAnimationStartTime != NO_ANIMATION
+ && mPlatform.mCurrentX != mPlatform.mToX;
+ }
+
+ public void stopScrolling() {
+ if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
+ if (mFilmMode) mFilmScroller.forceFinished(true);
+ mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
+ }
+
+ public float getFilmRatio() {
+ return mFilmRatio.mCurrentRatio;
+ }
+
+ public void setPopFromTop(boolean top) {
+ mPopFromTop = top;
+ }
+
+ public boolean hasDeletingBox() {
+ for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Private utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ private float getMinimalScale(Box b) {
+ float wFactor = 1.0f;
+ float hFactor = 1.0f;
+ int viewW, viewH;
+
+ if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
+ && b == mBoxes.get(0)) {
+ viewW = mConstrainedFrame.width();
+ viewH = mConstrainedFrame.height();
+ } else {
+ viewW = mViewW;
+ viewH = mViewH;
+ }
+
+ if (mFilmMode) {
+ if (mViewH > mViewW) { // portrait
+ wFactor = FILM_MODE_PORTRAIT_WIDTH;
+ hFactor = FILM_MODE_PORTRAIT_HEIGHT;
+ } else { // landscape
+ wFactor = FILM_MODE_LANDSCAPE_WIDTH;
+ hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
+ }
+ }
+
+ float s = Math.min(wFactor * viewW / b.mImageW,
+ hFactor * viewH / b.mImageH);
+ return Math.min(SCALE_LIMIT, s);
+ }
+
+ private float getMaximalScale(Box b) {
+ if (mFilmMode) return getMinimalScale(b);
+ if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
+ return SCALE_LIMIT;
+ }
+
+ private static boolean isAlmostEqual(float a, float b) {
+ float diff = a - b;
+ return (diff < 0 ? -diff : diff) < 0.02f;
+ }
+
+ // Calculates the stable region of mPlatform.mCurrentX and
+ // mBoxes.get(0).mCurrentY, where "stable" means
+ //
+ // (1) If the dimension of scaled image >= view dimension, we will not
+ // see black region outside the image (at that dimension).
+ // (2) If the dimension of scaled image < view dimension, we will center
+ // the scaled image.
+ //
+ // We might temporarily go out of this stable during user interaction,
+ // but will "snap back" after user stops interaction.
+ //
+ // The results are stored in mBound{Left/Right/Top/Bottom}.
+ //
+ // An extra parameter "horizontalSlack" (which has the value of 0 usually)
+ // is used to extend the stable region by some pixels on each side
+ // horizontally.
+ private void calculateStableBound(float scale, int horizontalSlack) {
+ Box b = mBoxes.get(0);
+
+ // The width and height of the box in number of view pixels
+ int w = widthOf(b, scale);
+ int h = heightOf(b, scale);
+
+ // When the edge of the view is aligned with the edge of the box
+ mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
+ mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
+ mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
+ mBoundBottom = h / 2 - mViewH / 2;
+
+ // If the scaled height is smaller than the view height,
+ // force it to be in the center.
+ if (viewTallerThanScaledImage(scale)) {
+ mBoundTop = mBoundBottom = 0;
+ }
+
+ // Same for width
+ if (viewWiderThanScaledImage(scale)) {
+ mBoundLeft = mBoundRight = mPlatform.mDefaultX;
+ }
+ }
+
+ private void calculateStableBound(float scale) {
+ calculateStableBound(scale, 0);
+ }
+
+ private boolean viewTallerThanScaledImage(float scale) {
+ return mViewH >= heightOf(mBoxes.get(0), scale);
+ }
+
+ private boolean viewWiderThanScaledImage(float scale) {
+ return mViewW >= widthOf(mBoxes.get(0), scale);
+ }
+
+ private float getTargetScale(Box b) {
+ return b.mAnimationStartTime == NO_ANIMATION
+ ? b.mCurrentScale : b.mToScale;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Animatable: an thing which can do animation.
+ ////////////////////////////////////////////////////////////////////////////
+ private abstract static class Animatable {
+ public long mAnimationStartTime;
+ public int mAnimationKind;
+ public int mAnimationDuration;
+
+ // This should be overridden in subclass to change the animation values
+ // give the progress value in [0, 1].
+ protected abstract boolean interpolate(float progress);
+ public abstract boolean startSnapback();
+
+ // Returns true if the animation values changes, so things need to be
+ // redrawn.
+ public boolean advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) {
+ return false;
+ }
+ if (mAnimationStartTime == LAST_ANIMATION) {
+ mAnimationStartTime = NO_ANIMATION;
+ return startSnapback();
+ }
+
+ float progress;
+ if (mAnimationDuration == 0) {
+ progress = 1;
+ } else {
+ long now = AnimationTime.get();
+ progress =
+ (float) (now - mAnimationStartTime) / mAnimationDuration;
+ }
+
+ if (progress >= 1) {
+ progress = 1;
+ } else {
+ progress = applyInterpolationCurve(mAnimationKind, progress);
+ }
+
+ boolean done = interpolate(progress);
+
+ if (done) {
+ mAnimationStartTime = LAST_ANIMATION;
+ }
+
+ return true;
+ }
+
+ private static float applyInterpolationCurve(int kind, float progress) {
+ float f = 1 - progress;
+ switch (kind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ case ANIM_KIND_DELETE:
+ case ANIM_KIND_CAPTURE:
+ progress = 1 - f; // linear
+ break;
+ case ANIM_KIND_OPENING:
+ case ANIM_KIND_SCALE:
+ progress = 1 - f * f; // quadratic
+ break;
+ case ANIM_KIND_SNAPBACK:
+ case ANIM_KIND_ZOOM:
+ case ANIM_KIND_SLIDE:
+ progress = 1 - f * f * f * f * f; // x^5
+ break;
+ }
+ return progress;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Platform: captures the global X/Y movement.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Platform extends Animatable {
+ public int mCurrentX, mFromX, mToX, mDefaultX;
+ public int mCurrentY, mFromY, mToY, mDefaultY;
+ public int mFlingOffset;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL
+ && mListener.isHoldingDown()) return false;
+ if (mInScale) return false;
+
+ Box b = mBoxes.get(0);
+ float scaleMin = mExtraScalingRange ?
+ b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
+ float scaleMax = mExtraScalingRange ?
+ b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
+ float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
+ int x = mCurrentX;
+ int y = mDefaultY;
+ if (mFilmMode) {
+ x = mDefaultX;
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ // If the picture is zoomed-in, we want to keep the focus point
+ // stay in the same position on screen, so we need to adjust
+ // target mCurrentX (which is the center of the focused
+ // box). The position of the focus point on screen (relative the
+ // the center of the view) is:
+ //
+ // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
+ // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
+ //
+ if (!viewWiderThanScaledImage(scale)) {
+ float scaleDiff = b.mCurrentScale - scale;
+ x += (int) (mFocusX * scaleDiff + 0.5f);
+ }
+ x = Utils.clamp(x, mBoundLeft, mBoundRight);
+ }
+ if (mCurrentX != x || mCurrentY != y) {
+ return doAnimation(x, y, ANIM_KIND_SNAPBACK);
+ }
+ return false;
+ }
+
+ // The updateDefaultXY() should be called whenever these variables
+ // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
+ // mFilmMode
+ public void updateDefaultXY() {
+ // We don't check mFilmMode and return 0 for mDefaultX. Because
+ // otherwise if we decide to leave film mode because we are
+ // centered, we will immediately back into film mode because we find
+ // we are not centered.
+ if (mConstrained && !mConstrainedFrame.isEmpty()) {
+ mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
+ mDefaultY = mFilmMode ? 0 :
+ mConstrainedFrame.centerY() - mViewH / 2;
+ } else {
+ mDefaultX = 0;
+ mDefaultY = 0;
+ }
+ }
+
+ // Starts an animation for the platform.
+ private boolean doAnimation(int targetX, int targetY, int kind) {
+ if (mCurrentX == targetX && mCurrentY == targetY) return false;
+ mAnimationKind = kind;
+ mFromX = mCurrentX;
+ mFromY = mCurrentY;
+ mToX = targetX;
+ mToY = targetY;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[kind];
+ mFlingOffset = 0;
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ return interpolateFlingPage(progress);
+ } else if (mAnimationKind == ANIM_KIND_FLING_X) {
+ return interpolateFlingFilm(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
+ }
+
+ private boolean interpolateFlingFilm(float progress) {
+ mFilmScroller.computeScrollOffset();
+ mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
+
+ int dir = EdgeView.INVALID_DIRECTION;
+ if (mCurrentX < mDefaultX) {
+ if (!mHasNext) {
+ dir = EdgeView.RIGHT;
+ }
+ } else if (mCurrentX > mDefaultX) {
+ if (!mHasPrev) {
+ dir = EdgeView.LEFT;
+ }
+ }
+ if (dir != EdgeView.INVALID_DIRECTION) {
+ // TODO: restore this onAbsorb call
+ //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
+ //mListener.onAbsorb(v, dir);
+ mFilmScroller.forceFinished(true);
+ mCurrentX = mDefaultX;
+ }
+ return mFilmScroller.isFinished();
+ }
+
+ private boolean interpolateFlingPage(float progress) {
+ mPageScroller.computeScrollOffset(progress);
+ Box b = mBoxes.get(0);
+ calculateStableBound(b.mCurrentScale);
+
+ int oldX = mCurrentX;
+ mCurrentX = mPageScroller.getCurrX();
+
+ // Check if we hit the edges; show edge effects if we do.
+ if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
+ int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.RIGHT);
+ } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
+ int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.LEFT);
+ }
+
+ return progress >= 1;
+ }
+
+ private boolean interpolateLinear(float progress) {
+ // Other animations
+ if (progress >= 1) {
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ return true;
+ } else {
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ progress = CaptureAnimation.calculateSlide(progress);
+ }
+ mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+ mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ return false;
+ } else {
+ return (mCurrentX == mToX && mCurrentY == mToY);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Box: represents a rectangular area which shows a picture.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Box extends Animatable {
+ // Size of the bitmap
+ public int mImageW, mImageH;
+
+ // This is true if we assume the image size is the same as view size
+ // until we know the actual size of image. This is also used to
+ // determine if there is an image ready to show.
+ public boolean mUseViewSize;
+
+ // The minimum and maximum scale we allow for this box.
+ public float mScaleMin, mScaleMax;
+
+ // The X/Y value indicates where the center of the box is on the view
+ // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
+ // actual values used currently. Note that the X values are implicitly
+ // defined by Platform and Gaps.
+ public int mCurrentY, mFromY, mToY;
+ public float mCurrentScale, mFromScale, mToScale;
+
+ // The absolute X coordinate of the center of the box. This is only used
+ // during moveBox().
+ public int mAbsoluteX;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL
+ && mListener.isHoldingDown()) return false;
+ if (mAnimationKind == ANIM_KIND_DELETE
+ && mListener.isHoldingDelete()) return false;
+ if (mInScale && this == mBoxes.get(0)) return false;
+
+ int y = mCurrentY;
+ float scale;
+
+ if (this == mBoxes.get(0)) {
+ float scaleMin = mExtraScalingRange ?
+ mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
+ float scaleMax = mExtraScalingRange ?
+ mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
+ scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
+ if (mFilmMode) {
+ y = 0;
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ // If the picture is zoomed-in, we want to keep the focus
+ // point stay in the same position on screen. See the
+ // comment in Platform.startSnapback for details.
+ if (!viewTallerThanScaledImage(scale)) {
+ float scaleDiff = mCurrentScale - scale;
+ y += (int) (mFocusY * scaleDiff + 0.5f);
+ }
+ y = Utils.clamp(y, mBoundTop, mBoundBottom);
+ }
+ } else {
+ y = 0;
+ scale = mScaleMin;
+ }
+
+ if (mCurrentY != y || mCurrentScale != scale) {
+ return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
+ }
+ return false;
+ }
+
+ private boolean doAnimation(int targetY, float targetScale, int kind) {
+ targetScale = clampScale(targetScale);
+
+ if (mCurrentY == targetY && mCurrentScale == targetScale
+ && kind != ANIM_KIND_CAPTURE) {
+ return false;
+ }
+
+ // Now starts an animation for the box.
+ mAnimationKind = kind;
+ mFromY = mCurrentY;
+ mFromScale = mCurrentScale;
+ mToY = targetY;
+ mToScale = targetScale;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[kind];
+ advanceAnimation();
+ return true;
+ }
+
+ // Clamps the input scale to the range that doAnimation() can reach.
+ public float clampScale(float s) {
+ return Utils.clamp(s,
+ SCALE_MIN_EXTRA * mScaleMin,
+ SCALE_MAX_EXTRA * mScaleMax);
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ return interpolateFlingPage(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
+ }
+
+ private boolean interpolateFlingPage(float progress) {
+ mPageScroller.computeScrollOffset(progress);
+ calculateStableBound(mCurrentScale);
+
+ int oldY = mCurrentY;
+ mCurrentY = mPageScroller.getCurrY();
+
+ // Check if we hit the edges; show edge effects if we do.
+ if (oldY > mBoundTop && mCurrentY == mBoundTop) {
+ int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.BOTTOM);
+ } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
+ int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.TOP);
+ }
+
+ return progress >= 1;
+ }
+
+ private boolean interpolateLinear(float progress) {
+ if (progress >= 1) {
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ return true;
+ } else {
+ mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+ mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ float f = CaptureAnimation.calculateScale(progress);
+ mCurrentScale *= f;
+ return false;
+ } else {
+ return (mCurrentY == mToY && mCurrentScale == mToScale);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Gap: represents a rectangular area which is between two boxes.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Gap extends Animatable {
+ // The default gap size between two boxes. The value may vary for
+ // different image size of the boxes and for different modes (page or
+ // film).
+ public int mDefaultSize;
+
+ // The gap size between the two boxes.
+ public int mCurrentGap, mFromGap, mToGap;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
+ }
+
+ // Starts an animation for a gap.
+ public boolean doAnimation(int targetSize, int kind) {
+ if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
+ return false;
+ }
+ mAnimationKind = kind;
+ mFromGap = mCurrentGap;
+ mToGap = targetSize;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[mAnimationKind];
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (progress >= 1) {
+ mCurrentGap = mToGap;
+ return true;
+ } else {
+ mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ float f = CaptureAnimation.calculateScale(progress);
+ mCurrentGap = (int) (mCurrentGap * f);
+ return false;
+ } else {
+ return (mCurrentGap == mToGap);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // FilmRatio: represents the progress of film mode change.
+ ////////////////////////////////////////////////////////////////////////////
+ private class FilmRatio extends Animatable {
+ // The film ratio: 1 means switching to film mode is complete, 0 means
+ // switching to page mode is complete.
+ public float mCurrentRatio, mFromRatio, mToRatio;
+
+ @Override
+ public boolean startSnapback() {
+ float target = mFilmMode ? 1f : 0f;
+ if (target == mToRatio) return false;
+ return doAnimation(target, ANIM_KIND_SNAPBACK);
+ }
+
+ // Starts an animation for the film ratio.
+ private boolean doAnimation(float targetRatio, int kind) {
+ mAnimationKind = kind;
+ mFromRatio = mCurrentRatio;
+ mToRatio = targetRatio;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[mAnimationKind];
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (progress >= 1) {
+ mCurrentRatio = mToRatio;
+ return true;
+ } else {
+ mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
+ return (mCurrentRatio == mToRatio);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
new file mode 100644
index 000000000..ce672f211
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
@@ -0,0 +1,85 @@
+package com.android.gallery3d.ui;
+
+import android.os.ConditionVariable;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+public class PreparePageFadeoutTexture implements OnGLIdleListener {
+ private static final long TIMEOUT = 200;
+ public static final String KEY_FADE_TEXTURE = "fade_texture";
+
+ private RawTexture mTexture;
+ private ConditionVariable mResultReady = new ConditionVariable(false);
+ private boolean mCancelled = false;
+ private GLView mRootPane;
+
+ public PreparePageFadeoutTexture(GLView rootPane) {
+ if (rootPane == null) {
+ mCancelled = true;
+ return;
+ }
+ int w = rootPane.getWidth();
+ int h = rootPane.getHeight();
+ if (w == 0 || h == 0) {
+ mCancelled = true;
+ return;
+ }
+ mTexture = new RawTexture(w, h, true);
+ mRootPane = rootPane;
+ }
+
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+
+ public synchronized RawTexture get() {
+ if (mCancelled) {
+ return null;
+ } else if (mResultReady.block(TIMEOUT)) {
+ return mTexture;
+ } else {
+ mCancelled = true;
+ return null;
+ }
+ }
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ if (!mCancelled) {
+ try {
+ canvas.beginRenderTarget(mTexture);
+ mRootPane.render(canvas);
+ canvas.endRenderTarget();
+ } catch (RuntimeException e) {
+ mTexture = null;
+ }
+ } else {
+ mTexture = null;
+ }
+ mResultReady.open();
+ return false;
+ }
+
+ public static void prepareFadeOutTexture(AbstractGalleryActivity activity,
+ GLView rootPane) {
+ PreparePageFadeoutTexture task = new PreparePageFadeoutTexture(rootPane);
+ if (task.isCancelled()) return;
+ GLRoot root = activity.getGLRoot();
+ RawTexture texture = null;
+ root.unlockRenderThread();
+ try {
+ root.addOnGLIdleListener(task);
+ texture = task.get();
+ } finally {
+ root.lockRenderThread();
+ }
+
+ if (texture == null) {
+ return;
+ }
+ activity.getTransitionStore().put(KEY_FADE_TEXTURE, texture);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java
new file mode 100644
index 000000000..1b31af278
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ProgressSpinner.java
@@ -0,0 +1,80 @@
+/*
+ * 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 com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+
+public class ProgressSpinner {
+ private static float ROTATE_SPEED_OUTER = 1080f / 3500f;
+ private static float ROTATE_SPEED_INNER = -720f / 3500f;
+ private final ResourceTexture mOuter;
+ private final ResourceTexture mInner;
+ private final int mWidth;
+ private final int mHeight;
+
+ private float mInnerDegree = 0f;
+ private float mOuterDegree = 0f;
+ private long mAnimationTimestamp = -1;
+
+ public ProgressSpinner(Context context) {
+ mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo);
+ mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo);
+
+ mWidth = Math.max(mOuter.getWidth(), mInner.getWidth());
+ mHeight = Math.max(mOuter.getHeight(), mInner.getHeight());
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public void startAnimation() {
+ mAnimationTimestamp = -1;
+ mOuterDegree = 0;
+ mInnerDegree = 0;
+ }
+
+ public void draw(GLCanvas canvas, int x, int y) {
+ long now = AnimationTime.get();
+ if (mAnimationTimestamp == -1) mAnimationTimestamp = now;
+ mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER;
+ mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER;
+
+ mAnimationTimestamp = now;
+
+ // just preventing overflow
+ if (mOuterDegree > 360) mOuterDegree -= 360f;
+ if (mInnerDegree < 0) mInnerDegree += 360f;
+
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+ canvas.translate(x + mWidth / 2, y + mHeight / 2);
+ canvas.rotate(mInnerDegree, 0, 0, 1);
+ mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2);
+ canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1);
+ mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2);
+ canvas.restore();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/RelativePosition.java b/src/com/android/gallery3d/ui/RelativePosition.java
new file mode 100644
index 000000000..0f2bfd812
--- /dev/null
+++ b/src/com/android/gallery3d/ui/RelativePosition.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+public class RelativePosition {
+ private float mAbsoluteX;
+ private float mAbsoluteY;
+ private float mReferenceX;
+ private float mReferenceY;
+
+ public void setAbsolutePosition(int absoluteX, int absoluteY) {
+ mAbsoluteX = absoluteX;
+ mAbsoluteY = absoluteY;
+ }
+
+ public void setReferencePosition(int x, int y) {
+ mReferenceX = x;
+ mReferenceY = y;
+ }
+
+ public float getX() {
+ return mAbsoluteX - mReferenceX;
+ }
+
+ public float getY() {
+ return mAbsoluteY - mReferenceY;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java
new file mode 100644
index 000000000..965bf0b54
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScreenNail.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 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.graphics.RectF;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public interface ScreenNail {
+ public int getWidth();
+ public int getHeight();
+ public void draw(GLCanvas canvas, int x, int y, int width, int height);
+
+ // We do not need to draw this ScreenNail in this frame.
+ public void noDraw();
+
+ // This ScreenNail will not be used anymore. Release related resources.
+ public void recycle();
+
+ // This is only used by TileImageView to back up the tiles not yet loaded.
+ public void draw(GLCanvas canvas, RectF source, RectF dest);
+}
diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java
new file mode 100644
index 000000000..34fbcef7a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollBarView.java
@@ -0,0 +1,97 @@
+/*
+ * 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.Rect;
+import android.util.TypedValue;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+
+public class ScrollBarView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ScrollBarView";
+
+ private int mBarHeight;
+
+ private int mGripHeight;
+ private int mGripPosition; // left side of the grip
+ private int mGripWidth; // zero if the grip is disabled
+ private int mGivenGripWidth;
+
+ private int mContentPosition;
+ private int mContentTotal;
+
+ private NinePatchTexture mScrollBarTexture;
+
+ public ScrollBarView(Context context, int gripHeight, int gripWidth) {
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(
+ android.R.attr.scrollbarThumbHorizontal, outValue, true);
+ mScrollBarTexture = new NinePatchTexture(
+ context, outValue.resourceId);
+ mGripPosition = 0;
+ mGripWidth = 0;
+ mGivenGripWidth = gripWidth;
+ mGripHeight = gripHeight;
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (!changed) return;
+ mBarHeight = bottom - top;
+ }
+
+ // The content position is between 0 to "total". The current position is
+ // in "position".
+ public void setContentPosition(int position, int total) {
+ if (position == mContentPosition && total == mContentTotal) {
+ return;
+ }
+
+ invalidate();
+
+ mContentPosition = position;
+ mContentTotal = total;
+
+ // If the grip cannot move, don't draw it.
+ if (mContentTotal <= 0) {
+ mGripPosition = 0;
+ mGripWidth = 0;
+ return;
+ }
+
+ // Map from the content range to scroll bar range.
+ //
+ // mContentTotal --> getWidth() - mGripWidth
+ // mContentPosition --> mGripPosition
+ mGripWidth = mGivenGripWidth;
+ float r = (getWidth() - mGripWidth) / (float) mContentTotal;
+ mGripPosition = Math.round(r * mContentPosition);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ if (mGripWidth == 0) return;
+ Rect b = bounds();
+ int y = (mBarHeight - mGripHeight) / 2;
+ mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
new file mode 100644
index 000000000..aa68d19d9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -0,0 +1,97 @@
+/*
+ * 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.view.ViewConfiguration;
+
+import com.android.gallery3d.common.OverScroller;
+import com.android.gallery3d.common.Utils;
+
+public class ScrollerHelper {
+ private OverScroller mScroller;
+ private int mOverflingDistance;
+ private boolean mOverflingEnabled;
+
+ public ScrollerHelper(Context context) {
+ mScroller = new OverScroller(context);
+ ViewConfiguration configuration = ViewConfiguration.get(context);
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+ }
+
+ public void setOverfling(boolean enabled) {
+ mOverflingEnabled = enabled;
+ }
+
+ /**
+ * Call this when you want to know the new location. The position will be
+ * updated and can be obtained by getPosition(). Returns true if the
+ * animation is not yet finished.
+ */
+ public boolean advanceAnimation(long currentTimeMillis) {
+ return mScroller.computeScrollOffset();
+ }
+
+ public boolean isFinished() {
+ return mScroller.isFinished();
+ }
+
+ public void forceFinished() {
+ mScroller.forceFinished(true);
+ }
+
+ public int getPosition() {
+ return mScroller.getCurrX();
+ }
+
+ public float getCurrVelocity() {
+ return mScroller.getCurrVelocity();
+ }
+
+ public void setPosition(int position) {
+ mScroller.startScroll(
+ position, 0, // startX, startY
+ 0, 0, 0); // dx, dy, duration
+
+ // This forces the scroller to reach the final position.
+ mScroller.abortAnimation();
+ }
+
+ public void fling(int velocity, int min, int max) {
+ int currX = getPosition();
+ mScroller.fling(
+ currX, 0, // startX, startY
+ velocity, 0, // velocityX, velocityY
+ min, max, // minX, maxX
+ 0, 0, // minY, maxY
+ mOverflingEnabled ? mOverflingDistance : 0, 0);
+ }
+
+ // Returns the distance that over the scroll limit.
+ public int startScroll(int distance, int min, int max) {
+ int currPosition = mScroller.getCurrX();
+ int finalPosition = mScroller.isFinished() ? currPosition :
+ mScroller.getFinalX();
+ int newPosition = Utils.clamp(finalPosition + distance, min, max);
+ if (newPosition != currPosition) {
+ mScroller.startScroll(
+ currPosition, 0, // startX, startY
+ newPosition - currPosition, 0, 0); // dx, dy, duration
+ }
+ return finalPosition + distance - newPosition;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java
new file mode 100644
index 000000000..be6811bc1
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionManager.java
@@ -0,0 +1,251 @@
+/*
+ * 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.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SelectionManager {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SelectionManager";
+
+ public static final int ENTER_SELECTION_MODE = 1;
+ public static final int LEAVE_SELECTION_MODE = 2;
+ public static final int SELECT_ALL_MODE = 3;
+
+ private Set<Path> mClickedSet;
+ private MediaSet mSourceMediaSet;
+ private SelectionListener mListener;
+ private DataManager mDataManager;
+ private boolean mInverseSelection;
+ private boolean mIsAlbumSet;
+ private boolean mInSelectionMode;
+ private boolean mAutoLeave = true;
+ private int mTotal;
+
+ public interface SelectionListener {
+ public void onSelectionModeChange(int mode);
+ public void onSelectionChange(Path path, boolean selected);
+ }
+
+ public SelectionManager(AbstractGalleryActivity activity, boolean isAlbumSet) {
+ mDataManager = activity.getDataManager();
+ mClickedSet = new HashSet<Path>();
+ mIsAlbumSet = isAlbumSet;
+ mTotal = -1;
+ }
+
+ // Whether we will leave selection mode automatically once the number of
+ // selected items is down to zero.
+ public void setAutoLeaveSelectionMode(boolean enable) {
+ mAutoLeave = enable;
+ }
+
+ public void setSelectionListener(SelectionListener listener) {
+ mListener = listener;
+ }
+
+ public void selectAll() {
+ mInverseSelection = true;
+ mClickedSet.clear();
+ enterSelectionMode();
+ if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE);
+ }
+
+ public void deSelectAll() {
+ leaveSelectionMode();
+ mInverseSelection = false;
+ mClickedSet.clear();
+ }
+
+ public boolean inSelectAllMode() {
+ return mInverseSelection;
+ }
+
+ public boolean inSelectionMode() {
+ return mInSelectionMode;
+ }
+
+ public void enterSelectionMode() {
+ if (mInSelectionMode) return;
+
+ mInSelectionMode = true;
+ if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE);
+ }
+
+ public void leaveSelectionMode() {
+ if (!mInSelectionMode) return;
+
+ mInSelectionMode = false;
+ mInverseSelection = false;
+ mClickedSet.clear();
+ if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE);
+ }
+
+ public boolean isItemSelected(Path itemId) {
+ return mInverseSelection ^ mClickedSet.contains(itemId);
+ }
+
+ private int getTotalCount() {
+ if (mSourceMediaSet == null) return -1;
+
+ if (mTotal < 0) {
+ mTotal = mIsAlbumSet
+ ? mSourceMediaSet.getSubMediaSetCount()
+ : mSourceMediaSet.getMediaItemCount();
+ }
+ return mTotal;
+ }
+
+ public int getSelectedCount() {
+ int count = mClickedSet.size();
+ if (mInverseSelection) {
+ count = getTotalCount() - count;
+ }
+ return count;
+ }
+
+ public void toggle(Path path) {
+ if (mClickedSet.contains(path)) {
+ mClickedSet.remove(path);
+ } else {
+ enterSelectionMode();
+ mClickedSet.add(path);
+ }
+
+ // Convert to inverse selection mode if everything is selected.
+ int count = getSelectedCount();
+ if (count == getTotalCount()) {
+ selectAll();
+ }
+
+ if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path));
+ if (count == 0 && mAutoLeave) {
+ leaveSelectionMode();
+ }
+ }
+
+ private static boolean expandMediaSet(ArrayList<Path> items, MediaSet set, int maxSelection) {
+ int subCount = set.getSubMediaSetCount();
+ for (int i = 0; i < subCount; i++) {
+ if (!expandMediaSet(items, set.getSubMediaSet(i), maxSelection)) {
+ return false;
+ }
+ }
+ int total = set.getMediaItemCount();
+ int batch = 50;
+ int index = 0;
+
+ while (index < total) {
+ int count = index + batch < total
+ ? batch
+ : total - index;
+ ArrayList<MediaItem> list = set.getMediaItem(index, count);
+ if (list != null
+ && list.size() > (maxSelection - items.size())) {
+ return false;
+ }
+ for (MediaItem item : list) {
+ items.add(item.getPath());
+ }
+ index += batch;
+ }
+ return true;
+ }
+
+ public ArrayList<Path> getSelected(boolean expandSet) {
+ return getSelected(expandSet, Integer.MAX_VALUE);
+ }
+
+ public ArrayList<Path> getSelected(boolean expandSet, int maxSelection) {
+ ArrayList<Path> selected = new ArrayList<Path>();
+ if (mIsAlbumSet) {
+ if (mInverseSelection) {
+ int total = getTotalCount();
+ for (int i = 0; i < total; i++) {
+ MediaSet set = mSourceMediaSet.getSubMediaSet(i);
+ Path id = set.getPath();
+ if (!mClickedSet.contains(id)) {
+ if (expandSet) {
+ if (!expandMediaSet(selected, set, maxSelection)) {
+ return null;
+ }
+ } else {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ if (expandSet) {
+ if (!expandMediaSet(selected, mDataManager.getMediaSet(id),
+ maxSelection)) {
+ return null;
+ }
+ } else {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ }
+ } else {
+ if (mInverseSelection) {
+ int total = getTotalCount();
+ int index = 0;
+ while (index < total) {
+ int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT);
+ ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count);
+ for (MediaItem item : list) {
+ Path id = item.getPath();
+ if (!mClickedSet.contains(id)) {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ index += count;
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ }
+ return selected;
+ }
+
+ public void setSourceMediaSet(MediaSet set) {
+ mSourceMediaSet = set;
+ mTotal = -1;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionMenu.java b/src/com/android/gallery3d/ui/SelectionMenu.java
new file mode 100644
index 000000000..5b0828328
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionMenu.java
@@ -0,0 +1,61 @@
+/*
+ * 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.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.PopupList.OnPopupItemClickListener;
+
+public class SelectionMenu implements OnClickListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SelectionMenu";
+
+ private final Context mContext;
+ private final Button mButton;
+ private final PopupList mPopupList;
+
+ public SelectionMenu(Context context, Button button, OnPopupItemClickListener listener) {
+ mContext = context;
+ mButton = button;
+ mPopupList = new PopupList(context, mButton);
+ mPopupList.addItem(R.id.action_select_all,
+ context.getString(R.string.select_all));
+ mPopupList.setOnPopupItemClickListener(listener);
+ mButton.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ mPopupList.show();
+ }
+
+ public void updateSelectAllMode(boolean inSelectAllMode) {
+ PopupList.Item item = mPopupList.findItem(R.id.action_select_all);
+ if (item != null) {
+ item.setTitle(mContext.getString(
+ inSelectAllMode ? R.string.deselect_all : R.string.select_all));
+ }
+ }
+
+ public void setTitle(CharSequence title) {
+ mButton.setText(title);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java
new file mode 100644
index 000000000..43784232d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlideshowView.java
@@ -0,0 +1,163 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.PointF;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.FloatAnimation;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+import java.util.Random;
+
+public class SlideshowView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SlideshowView";
+
+ private static final int SLIDESHOW_DURATION = 3500;
+ private static final int TRANSITION_DURATION = 1000;
+
+ private static final float SCALE_SPEED = 0.20f ;
+ private static final float MOVE_SPEED = SCALE_SPEED;
+
+ private int mCurrentRotation;
+ private BitmapTexture mCurrentTexture;
+ private SlideshowAnimation mCurrentAnimation;
+
+ private int mPrevRotation;
+ private BitmapTexture mPrevTexture;
+ private SlideshowAnimation mPrevAnimation;
+
+ private final FloatAnimation mTransitionAnimation =
+ new FloatAnimation(0, 1, TRANSITION_DURATION);
+
+ private Random mRandom = new Random();
+
+ public void next(Bitmap bitmap, int rotation) {
+
+ mTransitionAnimation.start();
+
+ if (mPrevTexture != null) {
+ mPrevTexture.getBitmap().recycle();
+ mPrevTexture.recycle();
+ }
+
+ mPrevTexture = mCurrentTexture;
+ mPrevAnimation = mCurrentAnimation;
+ mPrevRotation = mCurrentRotation;
+
+ mCurrentRotation = rotation;
+ mCurrentTexture = new BitmapTexture(bitmap);
+ if (((rotation / 90) & 0x01) == 0) {
+ mCurrentAnimation = new SlideshowAnimation(
+ mCurrentTexture.getWidth(), mCurrentTexture.getHeight(),
+ mRandom);
+ } else {
+ mCurrentAnimation = new SlideshowAnimation(
+ mCurrentTexture.getHeight(), mCurrentTexture.getWidth(),
+ mRandom);
+ }
+ mCurrentAnimation.start();
+
+ invalidate();
+ }
+
+ public void release() {
+ if (mPrevTexture != null) {
+ mPrevTexture.recycle();
+ mPrevTexture = null;
+ }
+ if (mCurrentTexture != null) {
+ mCurrentTexture.recycle();
+ mCurrentTexture = null;
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ long animTime = AnimationTime.get();
+ boolean requestRender = mTransitionAnimation.calculate(animTime);
+ float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get();
+
+ if (mPrevTexture != null && alpha != 1f) {
+ requestRender |= mPrevAnimation.calculate(animTime);
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.setAlpha(1f - alpha);
+ mPrevAnimation.apply(canvas);
+ canvas.rotate(mPrevRotation, 0, 0, 1);
+ mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2,
+ -mPrevTexture.getHeight() / 2);
+ canvas.restore();
+ }
+ if (mCurrentTexture != null) {
+ requestRender |= mCurrentAnimation.calculate(animTime);
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.setAlpha(alpha);
+ mCurrentAnimation.apply(canvas);
+ canvas.rotate(mCurrentRotation, 0, 0, 1);
+ mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2,
+ -mCurrentTexture.getHeight() / 2);
+ canvas.restore();
+ }
+ if (requestRender) invalidate();
+ }
+
+ private class SlideshowAnimation extends CanvasAnimation {
+ private final int mWidth;
+ private final int mHeight;
+
+ private final PointF mMovingVector;
+ private float mProgress;
+
+ public SlideshowAnimation(int width, int height, Random random) {
+ mWidth = width;
+ mHeight = height;
+ mMovingVector = new PointF(
+ MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f),
+ MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f));
+ setDuration(SLIDESHOW_DURATION);
+ }
+
+ @Override
+ public void apply(GLCanvas canvas) {
+ int viewWidth = getWidth();
+ int viewHeight = getHeight();
+
+ float initScale = Math.min((float)
+ viewWidth / mWidth, (float) viewHeight / mHeight);
+ float scale = initScale * (1 + SCALE_SPEED * mProgress);
+
+ float centerX = viewWidth / 2 + mMovingVector.x * mProgress;
+ float centerY = viewHeight / 2 + mMovingVector.y * mProgress;
+
+ canvas.translate(centerX, centerY);
+ canvas.scale(scale, scale, 0);
+ }
+
+ @Override
+ public int getCanvasSaveFlags() {
+ return GLCanvas.SAVE_FLAG_MATRIX;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mProgress = progress;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
new file mode 100644
index 000000000..bd0ffdc15
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -0,0 +1,788 @@
+/*
+ * 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.graphics.Rect;
+import android.os.Handler;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public class SlotView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SlotView";
+
+ private static final boolean WIDE = true;
+ private static final int INDEX_NONE = -1;
+
+ public static final int RENDER_MORE_PASS = 1;
+ public static final int RENDER_MORE_FRAME = 2;
+
+ public interface Listener {
+ public void onDown(int index);
+ public void onUp(boolean followedByLongPress);
+ public void onSingleTapUp(int index);
+ public void onLongTap(int index);
+ public void onScrollPositionChanged(int position, int total);
+ }
+
+ public static class SimpleListener implements Listener {
+ @Override public void onDown(int index) {}
+ @Override public void onUp(boolean followedByLongPress) {}
+ @Override public void onSingleTapUp(int index) {}
+ @Override public void onLongTap(int index) {}
+ @Override public void onScrollPositionChanged(int position, int total) {}
+ }
+
+ public static interface SlotRenderer {
+ public void prepareDrawing();
+ public void onVisibleRangeChanged(int visibleStart, int visibleEnd);
+ public void onSlotSizeChanged(int width, int height);
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height);
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScrollerHelper mScroller;
+ private final Paper mPaper = new Paper();
+
+ private Listener mListener;
+ private UserInteractionListener mUIListener;
+
+ private boolean mMoreAnimation = false;
+ private SlotAnimation mAnimation = null;
+ private final Layout mLayout = new Layout();
+ private int mStartIndex = INDEX_NONE;
+
+ // whether the down action happened while the view is scrolling.
+ private boolean mDownInScrolling;
+ private int mOverscrollEffect = OVERSCROLL_3D;
+ private final Handler mHandler;
+
+ private SlotRenderer mRenderer;
+
+ private int[] mRequestRenderSlots = new int[16];
+
+ public static final int OVERSCROLL_3D = 0;
+ public static final int OVERSCROLL_SYSTEM = 1;
+ public static final int OVERSCROLL_NONE = 2;
+
+ // to prevent allocating memory
+ private final Rect mTempRect = new Rect();
+
+ public SlotView(AbstractGalleryActivity activity, Spec spec) {
+ mGestureDetector = new GestureDetector(activity, new MyGestureListener());
+ mScroller = new ScrollerHelper(activity);
+ mHandler = new SynchronizedHandler(activity.getGLRoot());
+ setSlotSpec(spec);
+ }
+
+ public void setSlotRenderer(SlotRenderer slotDrawer) {
+ mRenderer = slotDrawer;
+ if (mRenderer != null) {
+ mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight);
+ mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd());
+ }
+ }
+
+ public void setCenterIndex(int index) {
+ int slotCount = mLayout.mSlotCount;
+ if (index < 0 || index >= slotCount) {
+ return;
+ }
+ Rect rect = mLayout.getSlotRect(index, mTempRect);
+ int position = WIDE
+ ? (rect.left + rect.right - getWidth()) / 2
+ : (rect.top + rect.bottom - getHeight()) / 2;
+ setScrollPosition(position);
+ }
+
+ public void makeSlotVisible(int index) {
+ Rect rect = mLayout.getSlotRect(index, mTempRect);
+ int visibleBegin = WIDE ? mScrollX : mScrollY;
+ int visibleLength = WIDE ? getWidth() : getHeight();
+ int visibleEnd = visibleBegin + visibleLength;
+ int slotBegin = WIDE ? rect.left : rect.top;
+ int slotEnd = WIDE ? rect.right : rect.bottom;
+
+ int position = visibleBegin;
+ if (visibleLength < slotEnd - slotBegin) {
+ position = visibleBegin;
+ } else if (slotBegin < visibleBegin) {
+ position = slotBegin;
+ } else if (slotEnd > visibleEnd) {
+ position = slotEnd - visibleLength;
+ }
+
+ setScrollPosition(position);
+ }
+
+ public void setScrollPosition(int position) {
+ position = Utils.clamp(position, 0, mLayout.getScrollLimit());
+ mScroller.setPosition(position);
+ updateScrollPosition(position, false);
+ }
+
+ public void setSlotSpec(Spec spec) {
+ mLayout.setSlotSpec(spec);
+ }
+
+ @Override
+ public void addComponent(GLView view) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
+ if (!changeSize) return;
+
+ // Make sure we are still at a resonable scroll position after the size
+ // is changed (like orientation change). We choose to keep the center
+ // visible slot still visible. This is arbitrary but reasonable.
+ int visibleIndex =
+ (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
+ mLayout.setSize(r - l, b - t);
+ makeSlotVisible(visibleIndex);
+ if (mOverscrollEffect == OVERSCROLL_3D) {
+ mPaper.setSize(r - l, b - t);
+ }
+ }
+
+ public void startScatteringAnimation(RelativePosition position) {
+ mAnimation = new ScatteringAnimation(position);
+ mAnimation.start();
+ if (mLayout.mSlotCount != 0) invalidate();
+ }
+
+ public void startRisingAnimation() {
+ mAnimation = new RisingAnimation();
+ mAnimation.start();
+ if (mLayout.mSlotCount != 0) invalidate();
+ }
+
+ private void updateScrollPosition(int position, boolean force) {
+ if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return;
+ if (WIDE) {
+ mScrollX = position;
+ } else {
+ mScrollY = position;
+ }
+ mLayout.setScrollPosition(position);
+ onScrollPositionChanged(position);
+ }
+
+ protected void onScrollPositionChanged(int newPosition) {
+ int limit = mLayout.getScrollLimit();
+ mListener.onScrollPositionChanged(newPosition, limit);
+ }
+
+ public Rect getSlotRect(int slotIndex) {
+ return mLayout.getSlotRect(slotIndex, new Rect());
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ if (mUIListener != null) mUIListener.onUserInteraction();
+ mGestureDetector.onTouchEvent(event);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownInScrolling = !mScroller.isFinished();
+ mScroller.forceFinished();
+ break;
+ case MotionEvent.ACTION_UP:
+ mPaper.onRelease();
+ invalidate();
+ break;
+ }
+ return true;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setUserInteractionListener(UserInteractionListener listener) {
+ mUIListener = listener;
+ }
+
+ public void setOverscrollEffect(int kind) {
+ mOverscrollEffect = kind;
+ mScroller.setOverfling(kind == OVERSCROLL_SYSTEM);
+ }
+
+ private static int[] expandIntArray(int array[], int capacity) {
+ while (array.length < capacity) {
+ array = new int[array.length * 2];
+ }
+ return array;
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+
+ if (mRenderer == null) return;
+ mRenderer.prepareDrawing();
+
+ long animTime = AnimationTime.get();
+ boolean more = mScroller.advanceAnimation(animTime);
+ more |= mLayout.advanceAnimation(animTime);
+ int oldX = mScrollX;
+ updateScrollPosition(mScroller.getPosition(), false);
+
+ boolean paperActive = false;
+ if (mOverscrollEffect == OVERSCROLL_3D) {
+ // Check if an edge is reached and notify mPaper if so.
+ int newX = mScrollX;
+ int limit = mLayout.getScrollLimit();
+ if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
+ float v = mScroller.getCurrVelocity();
+ if (newX == limit) v = -v;
+
+ // I don't know why, but getCurrVelocity() can return NaN.
+ if (!Float.isNaN(v)) {
+ mPaper.edgeReached(v);
+ }
+ }
+ paperActive = mPaper.advanceAnimation();
+ }
+
+ more |= paperActive;
+
+ if (mAnimation != null) {
+ more |= mAnimation.calculate(animTime);
+ }
+
+ canvas.translate(-mScrollX, -mScrollY);
+
+ int requestCount = 0;
+ int requestedSlot[] = expandIntArray(mRequestRenderSlots,
+ mLayout.mVisibleEnd - mLayout.mVisibleStart);
+
+ for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) {
+ int r = renderItem(canvas, i, 0, paperActive);
+ if ((r & RENDER_MORE_FRAME) != 0) more = true;
+ if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i;
+ }
+
+ for (int pass = 1; requestCount != 0; ++pass) {
+ int newCount = 0;
+ for (int i = 0; i < requestCount; ++i) {
+ int r = renderItem(canvas,
+ requestedSlot[i], pass, paperActive);
+ if ((r & RENDER_MORE_FRAME) != 0) more = true;
+ if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i;
+ }
+ requestCount = newCount;
+ }
+
+ canvas.translate(mScrollX, mScrollY);
+
+ if (more) invalidate();
+
+ final UserInteractionListener listener = mUIListener;
+ if (mMoreAnimation && !more && listener != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onUserInteractionEnd();
+ }
+ });
+ }
+ mMoreAnimation = more;
+ }
+
+ private int renderItem(
+ GLCanvas canvas, int index, int pass, boolean paperActive) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ Rect rect = mLayout.getSlotRect(index, mTempRect);
+ if (paperActive) {
+ canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0);
+ } else {
+ canvas.translate(rect.left, rect.top, 0);
+ }
+ if (mAnimation != null && mAnimation.isActive()) {
+ mAnimation.apply(canvas, index, rect);
+ }
+ int result = mRenderer.renderSlot(
+ canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top);
+ canvas.restore();
+ return result;
+ }
+
+ public static abstract class SlotAnimation extends Animation {
+ protected float mProgress = 0;
+
+ public SlotAnimation() {
+ setInterpolator(new DecelerateInterpolator(4));
+ setDuration(1500);
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mProgress = progress;
+ }
+
+ abstract public void apply(GLCanvas canvas, int slotIndex, Rect target);
+ }
+
+ public static class RisingAnimation extends SlotAnimation {
+ private static final int RISING_DISTANCE = 128;
+
+ @Override
+ public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+ canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress));
+ }
+ }
+
+ public static class ScatteringAnimation extends SlotAnimation {
+ private int PHOTO_DISTANCE = 1000;
+ private RelativePosition mCenter;
+
+ public ScatteringAnimation(RelativePosition center) {
+ mCenter = center;
+ }
+
+ @Override
+ public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+ canvas.translate(
+ (mCenter.getX() - target.centerX()) * (1 - mProgress),
+ (mCenter.getY() - target.centerY()) * (1 - mProgress),
+ slotIndex * PHOTO_DISTANCE * (1 - mProgress));
+ canvas.setAlpha(mProgress);
+ }
+ }
+
+ // This Spec class is used to specify the size of each slot in the SlotView.
+ // There are two ways to do it:
+ //
+ // (1) Specify slotWidth and slotHeight: they specify the width and height
+ // of each slot. The number of rows and the gap between slots will be
+ // determined automatically.
+ // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number
+ // of rows in landscape/portrait mode and the gap between slots. The
+ // width and height of each slot is determined automatically.
+ //
+ // The initial value of -1 means they are not specified.
+ public static class Spec {
+ public int slotWidth = -1;
+ public int slotHeight = -1;
+ public int slotHeightAdditional = 0;
+
+ public int rowsLand = -1;
+ public int rowsPort = -1;
+ public int slotGap = -1;
+ }
+
+ public class Layout {
+
+ private int mVisibleStart;
+ private int mVisibleEnd;
+
+ private int mSlotCount;
+ private int mSlotWidth;
+ private int mSlotHeight;
+ private int mSlotGap;
+
+ private Spec mSpec;
+
+ private int mWidth;
+ private int mHeight;
+
+ private int mUnitCount;
+ private int mContentLength;
+ private int mScrollPosition;
+
+ private IntegerAnimation mVerticalPadding = new IntegerAnimation();
+ private IntegerAnimation mHorizontalPadding = new IntegerAnimation();
+
+ public void setSlotSpec(Spec spec) {
+ mSpec = spec;
+ }
+
+ public boolean setSlotCount(int slotCount) {
+ if (slotCount == mSlotCount) return false;
+ if (mSlotCount != 0) {
+ mHorizontalPadding.setEnabled(true);
+ mVerticalPadding.setEnabled(true);
+ }
+ mSlotCount = slotCount;
+ int hPadding = mHorizontalPadding.getTarget();
+ int vPadding = mVerticalPadding.getTarget();
+ initLayoutParameters();
+ return vPadding != mVerticalPadding.getTarget()
+ || hPadding != mHorizontalPadding.getTarget();
+ }
+
+ public Rect getSlotRect(int index, Rect rect) {
+ int col, row;
+ if (WIDE) {
+ col = index / mUnitCount;
+ row = index - col * mUnitCount;
+ } else {
+ row = index / mUnitCount;
+ col = index - row * mUnitCount;
+ }
+
+ int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap);
+ int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap);
+ rect.set(x, y, x + mSlotWidth, y + mSlotHeight);
+ return rect;
+ }
+
+ public int getSlotWidth() {
+ return mSlotWidth;
+ }
+
+ public int getSlotHeight() {
+ return mSlotHeight;
+ }
+
+ // Calculate
+ // (1) mUnitCount: the number of slots we can fit into one column (or row).
+ // (2) mContentLength: the width (or height) we need to display all the
+ // columns (rows).
+ // (3) padding[]: the vertical and horizontal padding we need in order
+ // to put the slots towards to the center of the display.
+ //
+ // The "major" direction is the direction the user can scroll. The other
+ // direction is the "minor" direction.
+ //
+ // The comments inside this method are the description when the major
+ // directon is horizontal (X), and the minor directon is vertical (Y).
+ private void initLayoutParameters(
+ int majorLength, int minorLength, /* The view width and height */
+ int majorUnitSize, int minorUnitSize, /* The slot width and height */
+ int[] padding) {
+ int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap);
+ if (unitCount == 0) unitCount = 1;
+ mUnitCount = unitCount;
+
+ // We put extra padding above and below the column.
+ int availableUnits = Math.min(mUnitCount, mSlotCount);
+ int usedMinorLength = availableUnits * minorUnitSize +
+ (availableUnits - 1) * mSlotGap;
+ padding[0] = (minorLength - usedMinorLength) / 2;
+
+ // Then calculate how many columns we need for all slots.
+ int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
+ mContentLength = count * majorUnitSize + (count - 1) * mSlotGap;
+
+ // If the content length is less then the screen width, put
+ // extra padding in left and right.
+ padding[1] = Math.max(0, (majorLength - mContentLength) / 2);
+ }
+
+ private void initLayoutParameters() {
+ // Initialize mSlotWidth and mSlotHeight from mSpec
+ if (mSpec.slotWidth != -1) {
+ mSlotGap = 0;
+ mSlotWidth = mSpec.slotWidth;
+ mSlotHeight = mSpec.slotHeight;
+ } else {
+ int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort;
+ mSlotGap = mSpec.slotGap;
+ mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows);
+ mSlotWidth = mSlotHeight - mSpec.slotHeightAdditional;
+ }
+
+ if (mRenderer != null) {
+ mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight);
+ }
+
+ int[] padding = new int[2];
+ if (WIDE) {
+ initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
+ mVerticalPadding.startAnimateTo(padding[0]);
+ mHorizontalPadding.startAnimateTo(padding[1]);
+ } else {
+ initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
+ mVerticalPadding.startAnimateTo(padding[1]);
+ mHorizontalPadding.startAnimateTo(padding[0]);
+ }
+ updateVisibleSlotRange();
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ initLayoutParameters();
+ }
+
+ private void updateVisibleSlotRange() {
+ int position = mScrollPosition;
+
+ if (WIDE) {
+ int startCol = position / (mSlotWidth + mSlotGap);
+ int start = Math.max(0, mUnitCount * startCol);
+ int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) /
+ (mSlotWidth + mSlotGap);
+ int end = Math.min(mSlotCount, mUnitCount * endCol);
+ setVisibleRange(start, end);
+ } else {
+ int startRow = position / (mSlotHeight + mSlotGap);
+ int start = Math.max(0, mUnitCount * startRow);
+ int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) /
+ (mSlotHeight + mSlotGap);
+ int end = Math.min(mSlotCount, mUnitCount * endRow);
+ setVisibleRange(start, end);
+ }
+ }
+
+ public void setScrollPosition(int position) {
+ if (mScrollPosition == position) return;
+ mScrollPosition = position;
+ updateVisibleSlotRange();
+ }
+
+ private void setVisibleRange(int start, int end) {
+ if (start == mVisibleStart && end == mVisibleEnd) return;
+ if (start < end) {
+ mVisibleStart = start;
+ mVisibleEnd = end;
+ } else {
+ mVisibleStart = mVisibleEnd = 0;
+ }
+ if (mRenderer != null) {
+ mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd);
+ }
+ }
+
+ public int getVisibleStart() {
+ return mVisibleStart;
+ }
+
+ public int getVisibleEnd() {
+ return mVisibleEnd;
+ }
+
+ public int getSlotIndexByPosition(float x, float y) {
+ int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0);
+ int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition);
+
+ absoluteX -= mHorizontalPadding.get();
+ absoluteY -= mVerticalPadding.get();
+
+ if (absoluteX < 0 || absoluteY < 0) {
+ return INDEX_NONE;
+ }
+
+ int columnIdx = absoluteX / (mSlotWidth + mSlotGap);
+ int rowIdx = absoluteY / (mSlotHeight + mSlotGap);
+
+ if (!WIDE && columnIdx >= mUnitCount) {
+ return INDEX_NONE;
+ }
+
+ if (WIDE && rowIdx >= mUnitCount) {
+ return INDEX_NONE;
+ }
+
+ if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) {
+ return INDEX_NONE;
+ }
+
+ if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) {
+ return INDEX_NONE;
+ }
+
+ int index = WIDE
+ ? (columnIdx * mUnitCount + rowIdx)
+ : (rowIdx * mUnitCount + columnIdx);
+
+ return index >= mSlotCount ? INDEX_NONE : index;
+ }
+
+ public int getScrollLimit() {
+ int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight;
+ return limit <= 0 ? 0 : limit;
+ }
+
+ public boolean advanceAnimation(long animTime) {
+ // use '|' to make sure both sides will be executed
+ return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime);
+ }
+ }
+
+ private class MyGestureListener implements GestureDetector.OnGestureListener {
+ private boolean isDown;
+
+ // We call the listener's onDown() when our onShowPress() is called and
+ // call the listener's onUp() when we receive any further event.
+ @Override
+ public void onShowPress(MotionEvent e) {
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ if (isDown) return;
+ int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+ if (index != INDEX_NONE) {
+ isDown = true;
+ mListener.onDown(index);
+ }
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private void cancelDown(boolean byLongPress) {
+ if (!isDown) return;
+ isDown = false;
+ mListener.onUp(byLongPress);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1,
+ MotionEvent e2, float velocityX, float velocityY) {
+ cancelDown(false);
+ int scrollLimit = mLayout.getScrollLimit();
+ if (scrollLimit == 0) return false;
+ float velocity = WIDE ? velocityX : velocityY;
+ mScroller.fling((int) -velocity, 0, scrollLimit);
+ if (mUIListener != null) mUIListener.onUserInteractionBegin();
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1,
+ MotionEvent e2, float distanceX, float distanceY) {
+ cancelDown(false);
+ float distance = WIDE ? distanceX : distanceY;
+ int overDistance = mScroller.startScroll(
+ Math.round(distance), 0, mLayout.getScrollLimit());
+ if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) {
+ mPaper.overScroll(overDistance);
+ }
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ cancelDown(false);
+ if (mDownInScrolling) return true;
+ int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+ if (index != INDEX_NONE) mListener.onSingleTapUp(index);
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ cancelDown(true);
+ if (mDownInScrolling) return;
+ lockRendering();
+ try {
+ int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+ if (index != INDEX_NONE) mListener.onLongTap(index);
+ } finally {
+ unlockRendering();
+ }
+ }
+ }
+
+ public void setStartIndex(int index) {
+ mStartIndex = index;
+ }
+
+ // Return true if the layout parameters have been changed
+ public boolean setSlotCount(int slotCount) {
+ boolean changed = mLayout.setSlotCount(slotCount);
+
+ // mStartIndex is applied the first time setSlotCount is called.
+ if (mStartIndex != INDEX_NONE) {
+ setCenterIndex(mStartIndex);
+ mStartIndex = INDEX_NONE;
+ }
+ // Reset the scroll position to avoid scrolling over the updated limit.
+ setScrollPosition(WIDE ? mScrollX : mScrollY);
+ return changed;
+ }
+
+ public int getVisibleStart() {
+ return mLayout.getVisibleStart();
+ }
+
+ public int getVisibleEnd() {
+ return mLayout.getVisibleEnd();
+ }
+
+ public int getScrollX() {
+ return mScrollX;
+ }
+
+ public int getScrollY() {
+ return mScrollY;
+ }
+
+ public Rect getSlotRect(int slotIndex, GLView rootPane) {
+ // Get slot rectangle relative to this root pane.
+ Rect offset = new Rect();
+ rootPane.getBoundsOf(this, offset);
+ Rect r = getSlotRect(slotIndex);
+ r.offset(offset.left - getScrollX(),
+ offset.top - getScrollY());
+ return r;
+ }
+
+ private static class IntegerAnimation extends Animation {
+ private int mTarget;
+ private int mCurrent = 0;
+ private int mFrom = 0;
+ private boolean mEnabled = false;
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public void startAnimateTo(int target) {
+ if (!mEnabled) {
+ mTarget = mCurrent = target;
+ return;
+ }
+ if (target == mTarget) return;
+
+ mFrom = mCurrent;
+ mTarget = target;
+ setDuration(180);
+ start();
+ }
+
+ public int get() {
+ return mCurrent;
+ }
+
+ public int getTarget() {
+ return mTarget;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrent = Math.round(mFrom + progress * (mTarget - mFrom));
+ if (progress == 1f) mEnabled = false;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
new file mode 100644
index 000000000..18121e63b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.glrenderer.ExtTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+public abstract class SurfaceTextureScreenNail implements ScreenNail,
+ SurfaceTexture.OnFrameAvailableListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SurfaceTextureScreenNail";
+ // This constant is not available in API level before 15, but it was just an
+ // oversight.
+ private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
+
+ protected ExtTexture mExtTexture;
+ private SurfaceTexture mSurfaceTexture;
+ private int mWidth, mHeight;
+ private float[] mTransform = new float[16];
+ private boolean mHasTexture = false;
+
+ public SurfaceTextureScreenNail() {
+ }
+
+ public void acquireSurfaceTexture(GLCanvas canvas) {
+ mExtTexture = new ExtTexture(canvas, GL_TEXTURE_EXTERNAL_OES);
+ mExtTexture.setSize(mWidth, mHeight);
+ mSurfaceTexture = new SurfaceTexture(mExtTexture.getId());
+ setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight);
+ mSurfaceTexture.setOnFrameAvailableListener(this);
+ synchronized (this) {
+ mHasTexture = true;
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+ private static void setDefaultBufferSize(SurfaceTexture st, int width, int height) {
+ if (ApiHelper.HAS_SET_DEFALT_BUFFER_SIZE) {
+ st.setDefaultBufferSize(width, height);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private static void releaseSurfaceTexture(SurfaceTexture st) {
+ st.setOnFrameAvailableListener(null);
+ if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) {
+ st.release();
+ }
+ }
+
+ public SurfaceTexture getSurfaceTexture() {
+ return mSurfaceTexture;
+ }
+
+ public void releaseSurfaceTexture() {
+ synchronized (this) {
+ mHasTexture = false;
+ }
+ mExtTexture.recycle();
+ mExtTexture = null;
+ releaseSurfaceTexture(mSurfaceTexture);
+ mSurfaceTexture = null;
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ public void resizeTexture() {
+ if (mExtTexture != null) {
+ mExtTexture.setSize(mWidth, mHeight);
+ setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight);
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ synchronized (this) {
+ if (!mHasTexture) return;
+ mSurfaceTexture.updateTexImage();
+ mSurfaceTexture.getTransformMatrix(mTransform);
+
+ // Flip vertically.
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ int cx = x + width / 2;
+ int cy = y + height / 2;
+ canvas.translate(cx, cy);
+ canvas.scale(1, -1, 1);
+ canvas.translate(-cx, -cy);
+ updateTransformMatrix(mTransform);
+ canvas.drawTexture(mExtTexture, mTransform, x, y, width, height);
+ canvas.restore();
+ }
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, RectF source, RectF dest) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected void updateTransformMatrix(float[] matrix) {}
+
+ @Override
+ abstract public void noDraw();
+
+ @Override
+ abstract public void recycle();
+
+ @Override
+ abstract public void onFrameAvailable(SurfaceTexture surfaceTexture);
+}
diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java
new file mode 100644
index 000000000..ba1035747
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java
@@ -0,0 +1,41 @@
+/*
+ * 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.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.Utils;
+
+public class SynchronizedHandler extends Handler {
+
+ private final GLRoot mRoot;
+
+ public SynchronizedHandler(GLRoot root) {
+ mRoot = Utils.checkNotNull(root);
+ }
+
+ @Override
+ public void dispatchMessage(Message message) {
+ mRoot.lockRenderThread();
+ try {
+ super.dispatchMessage(message);
+ } finally {
+ mRoot.unlockRenderThread();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
new file mode 100644
index 000000000..3185c7598
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -0,0 +1,786 @@
+/*
+ * 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.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.v4.util.LongSparseArray;
+import android.util.DisplayMetrics;
+import android.util.FloatMath;
+import android.view.WindowManager;
+
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DecodeUtils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class TileImageView extends GLView {
+ public static final int SIZE_UNKNOWN = -1;
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "TileImageView";
+ private static final int UPLOAD_LIMIT = 1;
+
+ // TILE_SIZE must be 2^N
+ private static int sTileSize;
+
+ /*
+ * This is the tile state in the CPU side.
+ * Life of a Tile:
+ * ACTIVATED (initial state)
+ * --> IN_QUEUE - by queueForDecode()
+ * --> RECYCLED - by recycleTile()
+ * IN_QUEUE --> DECODING - by decodeTile()
+ * --> RECYCLED - by recycleTile)
+ * DECODING --> RECYCLING - by recycleTile()
+ * --> DECODED - by decodeTile()
+ * --> DECODE_FAIL - by decodeTile()
+ * RECYCLING --> RECYCLED - by decodeTile()
+ * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
+ * DECODED --> RECYCLED - by recycleTile()
+ * DECODE_FAIL -> RECYCLED - by recycleTile()
+ * RECYCLED --> ACTIVATED - by obtainTile()
+ */
+ private static final int STATE_ACTIVATED = 0x01;
+ private static final int STATE_IN_QUEUE = 0x02;
+ private static final int STATE_DECODING = 0x04;
+ private static final int STATE_DECODED = 0x08;
+ private static final int STATE_DECODE_FAIL = 0x10;
+ private static final int STATE_RECYCLING = 0x20;
+ private static final int STATE_RECYCLED = 0x40;
+
+ private TileSource mModel;
+ private ScreenNail mScreenNail;
+ protected int mLevelCount; // cache the value of mScaledBitmaps.length
+
+ // The mLevel variable indicates which level of bitmap we should use.
+ // Level 0 means the original full-sized bitmap, and a larger value means
+ // a smaller scaled bitmap (The width and height of each scaled bitmap is
+ // half size of the previous one). If the value is in [0, mLevelCount), we
+ // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
+ // is mLevelCount, and that means we use mScreenNail for display.
+ private int mLevel = 0;
+
+ // The offsets of the (left, top) of the upper-left tile to the (left, top)
+ // of the view.
+ private int mOffsetX;
+ private int mOffsetY;
+
+ private int mUploadQuota;
+ private boolean mRenderComplete;
+
+ private final RectF mSourceRect = new RectF();
+ private final RectF mTargetRect = new RectF();
+
+ private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
+
+ // The following three queue is guarded by TileImageView.this
+ private final TileQueue mRecycledQueue = new TileQueue();
+ private final TileQueue mUploadQueue = new TileQueue();
+ private final TileQueue mDecodeQueue = new TileQueue();
+
+ // The width and height of the full-sized bitmap
+ protected int mImageWidth = SIZE_UNKNOWN;
+ protected int mImageHeight = SIZE_UNKNOWN;
+
+ protected int mCenterX;
+ protected int mCenterY;
+ protected float mScale;
+ protected int mRotation;
+
+ // Temp variables to avoid memory allocation
+ private final Rect mTileRange = new Rect();
+ private final Rect mActiveRange[] = {new Rect(), new Rect()};
+
+ private final TileUploader mTileUploader = new TileUploader();
+ private boolean mIsTextureFreed;
+ private Future<Void> mTileDecoder;
+ private final ThreadPool mThreadPool;
+ private boolean mBackgroundTileUploaded;
+
+ public static interface TileSource {
+ public int getLevelCount();
+ public ScreenNail getScreenNail();
+ public int getImageWidth();
+ public int getImageHeight();
+
+ // The tile returned by this method can be specified this way: Assuming
+ // the image size is (width, height), first take the intersection of (0,
+ // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
+ // in extending the region, we found some part of the region are outside
+ // the image, those pixels are filled with black.
+ //
+ // If level > 0, it does the same operation on a down-scaled version of
+ // the original image (down-scaled by a factor of 2^level), but (x, y)
+ // still refers to the coordinate on the original image.
+ //
+ // The method would be called in another thread.
+ public Bitmap getTile(int level, int x, int y, int tileSize);
+ }
+
+ public static boolean isHighResolution(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ return metrics.heightPixels > 2048 || metrics.widthPixels > 2048;
+ }
+
+ public TileImageView(GalleryContext context) {
+ mThreadPool = context.getThreadPool();
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ if (sTileSize == 0) {
+ if (isHighResolution(context.getAndroidContext())) {
+ sTileSize = 512 ;
+ } else {
+ sTileSize = 256;
+ }
+ }
+ }
+
+ public void setModel(TileSource model) {
+ mModel = model;
+ if (model != null) notifyModelInvalidated();
+ }
+
+ public void setScreenNail(ScreenNail s) {
+ mScreenNail = s;
+ }
+
+ public void notifyModelInvalidated() {
+ invalidateTiles();
+ if (mModel == null) {
+ mScreenNail = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ } else {
+ setScreenNail(mModel.getScreenNail());
+ mImageWidth = mModel.getImageWidth();
+ mImageHeight = mModel.getImageHeight();
+ mLevelCount = mModel.getLevelCount();
+ }
+ layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ invalidate();
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ super.onLayout(changeSize, left, top, right, bottom);
+ if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ }
+
+ // Prepare the tiles we want to use for display.
+ //
+ // 1. Decide the tile level we want to use for display.
+ // 2. Decide the tile levels we want to keep as texture (in addition to
+ // the one we use for display).
+ // 3. Recycle unused tiles.
+ // 4. Activate the tiles we want.
+ private void layoutTiles(int centerX, int centerY, float scale, int rotation) {
+ // The width and height of this view.
+ int width = getWidth();
+ int height = getHeight();
+
+ // The tile levels we want to keep as texture is in the range
+ // [fromLevel, endLevel).
+ int fromLevel;
+ int endLevel;
+
+ // We want to use a texture larger than or equal to the display size.
+ mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount);
+
+ // We want to keep one more tile level as texture in addition to what
+ // we use for display. So it can be faster when the scale moves to the
+ // next level. We choose a level closer to the current scale.
+ if (mLevel != mLevelCount) {
+ Rect range = mTileRange;
+ getRange(range, centerX, centerY, mLevel, scale, rotation);
+ mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale);
+ mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale);
+ fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
+ } else {
+ // Activate the tiles of the smallest two levels.
+ fromLevel = mLevel - 2;
+ mOffsetX = Math.round(width / 2f - centerX * scale);
+ mOffsetY = Math.round(height / 2f - centerY * scale);
+ }
+
+ fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
+ endLevel = Math.min(fromLevel + 2, mLevelCount);
+
+ Rect range[] = mActiveRange;
+ for (int i = fromLevel; i < endLevel; ++i) {
+ getRange(range[i - fromLevel], centerX, centerY, i, rotation);
+ }
+
+ // If rotation is transient, don't update the tile.
+ if (rotation % 90 != 0) return;
+
+ synchronized (this) {
+ mDecodeQueue.clean();
+ mUploadQueue.clean();
+ mBackgroundTileUploaded = false;
+
+ // Recycle unused tiles: if the level of the active tile is outside the
+ // range [fromLevel, endLevel) or not in the visible range.
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ int level = tile.mTileLevel;
+ if (level < fromLevel || level >= endLevel
+ || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
+ mActiveTiles.removeAt(i);
+ i--;
+ n--;
+ recycleTile(tile);
+ }
+ }
+ }
+
+ for (int i = fromLevel; i < endLevel; ++i) {
+ int size = sTileSize << i;
+ Rect r = range[i - fromLevel];
+ for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
+ for (int x = r.left, right = r.right; x < right; x += size) {
+ activateTile(x, y, i);
+ }
+ }
+ }
+ invalidate();
+ }
+
+ protected synchronized void invalidateTiles() {
+ mDecodeQueue.clean();
+ mUploadQueue.clean();
+
+ // TODO disable decoder
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ recycleTile(tile);
+ }
+ mActiveTiles.clear();
+ }
+
+ private void getRange(Rect out, int cX, int cY, int level, int rotation) {
+ getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
+ }
+
+ // If the bitmap is scaled by the given factor "scale", return the
+ // rectangle containing visible range. The left-top coordinate returned is
+ // aligned to the tile boundary.
+ //
+ // (cX, cY) is the point on the original bitmap which will be put in the
+ // center of the ImageViewer.
+ private void getRange(Rect out,
+ int cX, int cY, int level, float scale, int rotation) {
+
+ double radians = Math.toRadians(-rotation);
+ double w = getWidth();
+ double h = getHeight();
+
+ double cos = Math.cos(radians);
+ double sin = Math.sin(radians);
+ int width = (int) Math.ceil(Math.max(
+ Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
+ int height = (int) Math.ceil(Math.max(
+ Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
+
+ int left = (int) FloatMath.floor(cX - width / (2f * scale));
+ int top = (int) FloatMath.floor(cY - height / (2f * scale));
+ int right = (int) FloatMath.ceil(left + width / scale);
+ int bottom = (int) FloatMath.ceil(top + height / scale);
+
+ // align the rectangle to tile boundary
+ int size = sTileSize << level;
+ left = Math.max(0, size * (left / size));
+ top = Math.max(0, size * (top / size));
+ right = Math.min(mImageWidth, right);
+ bottom = Math.min(mImageHeight, bottom);
+
+ out.set(left, top, right, bottom);
+ }
+
+ // Calculate where the center of the image is, in the view coordinates.
+ public void getImageCenter(Point center) {
+ // The width and height of this view.
+ int viewW = getWidth();
+ int viewH = getHeight();
+
+ // The distance between the center of the view to the center of the
+ // bitmap, in bitmap units. (mCenterX and mCenterY are the bitmap
+ // coordinates correspond to the center of view)
+ int distW, distH;
+ if (mRotation % 180 == 0) {
+ distW = mImageWidth / 2 - mCenterX;
+ distH = mImageHeight / 2 - mCenterY;
+ } else {
+ distW = mImageHeight / 2 - mCenterY;
+ distH = mImageWidth / 2 - mCenterX;
+ }
+
+ // Convert to view coordinates. mScale translates from bitmap units to
+ // view units.
+ center.x = Math.round(viewW / 2f + distW * mScale);
+ center.y = Math.round(viewH / 2f + distH * mScale);
+ }
+
+ public boolean setPosition(int centerX, int centerY, float scale, int rotation) {
+ if (mCenterX == centerX && mCenterY == centerY
+ && mScale == scale && mRotation == rotation) return false;
+ mCenterX = centerX;
+ mCenterY = centerY;
+ mScale = scale;
+ mRotation = rotation;
+ layoutTiles(centerX, centerY, scale, rotation);
+ invalidate();
+ return true;
+ }
+
+ public void freeTextures() {
+ mIsTextureFreed = true;
+
+ if (mTileDecoder != null) {
+ mTileDecoder.cancel();
+ mTileDecoder.get();
+ mTileDecoder = null;
+ }
+
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile texture = mActiveTiles.valueAt(i);
+ texture.recycle();
+ }
+ mActiveTiles.clear();
+ mTileRange.set(0, 0, 0, 0);
+
+ synchronized (this) {
+ mUploadQueue.clean();
+ mDecodeQueue.clean();
+ Tile tile = mRecycledQueue.pop();
+ while (tile != null) {
+ tile.recycle();
+ tile = mRecycledQueue.pop();
+ }
+ }
+ setScreenNail(null);
+ }
+
+ public void prepareTextures() {
+ if (mTileDecoder == null) {
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ }
+ if (mIsTextureFreed) {
+ layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ mIsTextureFreed = false;
+ setScreenNail(mModel == null ? null : mModel.getScreenNail());
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mUploadQuota = UPLOAD_LIMIT;
+ mRenderComplete = true;
+
+ int level = mLevel;
+ int rotation = mRotation;
+ int flags = 0;
+ if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
+
+ if (flags != 0) {
+ canvas.save(flags);
+ if (rotation != 0) {
+ int centerX = getWidth() / 2, centerY = getHeight() / 2;
+ canvas.translate(centerX, centerY);
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-centerX, -centerY);
+ }
+ }
+ try {
+ if (level != mLevelCount && !isScreenNailAnimating()) {
+ if (mScreenNail != null) {
+ mScreenNail.noDraw();
+ }
+
+ int size = (sTileSize << level);
+ float length = size * mScale;
+ Rect r = mTileRange;
+
+ for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
+ float y = mOffsetY + i * length;
+ for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
+ float x = mOffsetX + j * length;
+ drawTile(canvas, tx, ty, level, x, y, length);
+ }
+ }
+ } else if (mScreenNail != null) {
+ mScreenNail.draw(canvas, mOffsetX, mOffsetY,
+ Math.round(mImageWidth * mScale),
+ Math.round(mImageHeight * mScale));
+ if (isScreenNailAnimating()) {
+ invalidate();
+ }
+ }
+ } finally {
+ if (flags != 0) canvas.restore();
+ }
+
+ if (mRenderComplete) {
+ if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas);
+ } else {
+ invalidate();
+ }
+ }
+
+ private boolean isScreenNailAnimating() {
+ return (mScreenNail instanceof TiledScreenNail)
+ && ((TiledScreenNail) mScreenNail).isAnimating();
+ }
+
+ private void uploadBackgroundTiles(GLCanvas canvas) {
+ mBackgroundTileUploaded = true;
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ if (!tile.isContentValid()) queueForDecode(tile);
+ }
+ }
+
+ void queueForUpload(Tile tile) {
+ synchronized (this) {
+ mUploadQueue.push(tile);
+ }
+ if (mTileUploader.mActive.compareAndSet(false, true)) {
+ getGLRoot().addOnGLIdleListener(mTileUploader);
+ }
+ }
+
+ synchronized void queueForDecode(Tile tile) {
+ if (tile.mTileState == STATE_ACTIVATED) {
+ tile.mTileState = STATE_IN_QUEUE;
+ if (mDecodeQueue.push(tile)) notifyAll();
+ }
+ }
+
+ boolean decodeTile(Tile tile) {
+ synchronized (this) {
+ if (tile.mTileState != STATE_IN_QUEUE) return false;
+ tile.mTileState = STATE_DECODING;
+ }
+ boolean decodeComplete = tile.decode();
+ synchronized (this) {
+ if (tile.mTileState == STATE_RECYCLING) {
+ tile.mTileState = STATE_RECYCLED;
+ if (tile.mDecodedTile != null) {
+ GalleryBitmapPool.getInstance().put(tile.mDecodedTile);
+ tile.mDecodedTile = null;
+ }
+ mRecycledQueue.push(tile);
+ return false;
+ }
+ tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
+ return decodeComplete;
+ }
+ }
+
+ private synchronized Tile obtainTile(int x, int y, int level) {
+ Tile tile = mRecycledQueue.pop();
+ if (tile != null) {
+ tile.mTileState = STATE_ACTIVATED;
+ tile.update(x, y, level);
+ return tile;
+ }
+ return new Tile(x, y, level);
+ }
+
+ synchronized void recycleTile(Tile tile) {
+ if (tile.mTileState == STATE_DECODING) {
+ tile.mTileState = STATE_RECYCLING;
+ return;
+ }
+ tile.mTileState = STATE_RECYCLED;
+ if (tile.mDecodedTile != null) {
+ GalleryBitmapPool.getInstance().put(tile.mDecodedTile);
+ tile.mDecodedTile = null;
+ }
+ mRecycledQueue.push(tile);
+ }
+
+ private void activateTile(int x, int y, int level) {
+ long key = makeTileKey(x, y, level);
+ Tile tile = mActiveTiles.get(key);
+ if (tile != null) {
+ if (tile.mTileState == STATE_IN_QUEUE) {
+ tile.mTileState = STATE_ACTIVATED;
+ }
+ return;
+ }
+ tile = obtainTile(x, y, level);
+ mActiveTiles.put(key, tile);
+ }
+
+ private Tile getTile(int x, int y, int level) {
+ return mActiveTiles.get(makeTileKey(x, y, level));
+ }
+
+ private static long makeTileKey(int x, int y, int level) {
+ long result = x;
+ result = (result << 16) | y;
+ result = (result << 16) | level;
+ return result;
+ }
+
+ private class TileUploader implements GLRoot.OnGLIdleListener {
+ AtomicBoolean mActive = new AtomicBoolean(false);
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ // Skips uploading if there is a pending rendering request.
+ // Returns true to keep uploading in next rendering loop.
+ if (renderRequested) return true;
+ int quota = UPLOAD_LIMIT;
+ Tile tile = null;
+ while (quota > 0) {
+ synchronized (TileImageView.this) {
+ tile = mUploadQueue.pop();
+ }
+ if (tile == null) break;
+ if (!tile.isContentValid()) {
+ boolean hasBeenLoaded = tile.isLoaded();
+ Utils.assertTrue(tile.mTileState == STATE_DECODED);
+ tile.updateContent(canvas);
+ if (!hasBeenLoaded) tile.draw(canvas, 0, 0);
+ --quota;
+ }
+ }
+ if (tile == null) mActive.set(false);
+ return tile != null;
+ }
+ }
+
+ // Draw the tile to a square at canvas that locates at (x, y) and
+ // has a side length of length.
+ public void drawTile(GLCanvas canvas,
+ int tx, int ty, int level, float x, float y, float length) {
+ RectF source = mSourceRect;
+ RectF target = mTargetRect;
+ target.set(x, y, x + length, y + length);
+ source.set(0, 0, sTileSize, sTileSize);
+
+ Tile tile = getTile(tx, ty, level);
+ if (tile != null) {
+ if (!tile.isContentValid()) {
+ if (tile.mTileState == STATE_DECODED) {
+ if (mUploadQuota > 0) {
+ --mUploadQuota;
+ tile.updateContent(canvas);
+ } else {
+ mRenderComplete = false;
+ }
+ } else if (tile.mTileState != STATE_DECODE_FAIL){
+ mRenderComplete = false;
+ queueForDecode(tile);
+ }
+ }
+ if (drawTile(tile, canvas, source, target)) return;
+ }
+ if (mScreenNail != null) {
+ int size = sTileSize << level;
+ float scaleX = (float) mScreenNail.getWidth() / mImageWidth;
+ float scaleY = (float) mScreenNail.getHeight() / mImageHeight;
+ source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+ (ty + size) * scaleY);
+ mScreenNail.draw(canvas, source, target);
+ }
+ }
+
+ static boolean drawTile(
+ Tile tile, GLCanvas canvas, RectF source, RectF target) {
+ while (true) {
+ if (tile.isContentValid()) {
+ canvas.drawTexture(tile, source, target);
+ return true;
+ }
+
+ // Parent can be divided to four quads and tile is one of the four.
+ Tile parent = tile.getParentTile();
+ if (parent == null) return false;
+ if (tile.mX == parent.mX) {
+ source.left /= 2f;
+ source.right /= 2f;
+ } else {
+ source.left = (sTileSize + source.left) / 2f;
+ source.right = (sTileSize + source.right) / 2f;
+ }
+ if (tile.mY == parent.mY) {
+ source.top /= 2f;
+ source.bottom /= 2f;
+ } else {
+ source.top = (sTileSize + source.top) / 2f;
+ source.bottom = (sTileSize + source.bottom) / 2f;
+ }
+ tile = parent;
+ }
+ }
+
+ private class Tile extends UploadedTexture {
+ public int mX;
+ public int mY;
+ public int mTileLevel;
+ public Tile mNext;
+ public Bitmap mDecodedTile;
+ public volatile int mTileState = STATE_ACTIVATED;
+
+ public Tile(int x, int y, int level) {
+ mX = x;
+ mY = y;
+ mTileLevel = level;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ GalleryBitmapPool.getInstance().put(bitmap);
+ }
+
+ boolean decode() {
+ // Get a tile from the original image. The tile is down-scaled
+ // by (1 << mTilelevel) from a region in the original image.
+ try {
+ mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile(
+ mTileLevel, mX, mY, sTileSize));
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode tile", t);
+ }
+ return mDecodedTile != null;
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ Utils.assertTrue(mTileState == STATE_DECODED);
+
+ // We need to override the width and height, so that we won't
+ // draw beyond the boundaries.
+ int rightEdge = ((mImageWidth - mX) >> mTileLevel);
+ int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
+ setSize(Math.min(sTileSize, rightEdge), Math.min(sTileSize, bottomEdge));
+
+ Bitmap bitmap = mDecodedTile;
+ mDecodedTile = null;
+ mTileState = STATE_ACTIVATED;
+ return bitmap;
+ }
+
+ // We override getTextureWidth() and getTextureHeight() here, so the
+ // texture can be re-used for different tiles regardless of the actual
+ // size of the tile (which may be small because it is a tile at the
+ // boundary).
+ @Override
+ public int getTextureWidth() {
+ return sTileSize;
+ }
+
+ @Override
+ public int getTextureHeight() {
+ return sTileSize;
+ }
+
+ public void update(int x, int y, int level) {
+ mX = x;
+ mY = y;
+ mTileLevel = level;
+ invalidateContent();
+ }
+
+ public Tile getParentTile() {
+ if (mTileLevel + 1 == mLevelCount) return null;
+ int size = sTileSize << (mTileLevel + 1);
+ int x = size * (mX / size);
+ int y = size * (mY / size);
+ return getTile(x, y, mTileLevel + 1);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tile(%s, %s, %s / %s)",
+ mX / sTileSize, mY / sTileSize, mLevel, mLevelCount);
+ }
+ }
+
+ private static class TileQueue {
+ private Tile mHead;
+
+ public Tile pop() {
+ Tile tile = mHead;
+ if (tile != null) mHead = tile.mNext;
+ return tile;
+ }
+
+ public boolean push(Tile tile) {
+ boolean wasEmpty = mHead == null;
+ tile.mNext = mHead;
+ mHead = tile;
+ return wasEmpty;
+ }
+
+ public void clean() {
+ mHead = null;
+ }
+ }
+
+ private class TileDecoder implements ThreadPool.Job<Void> {
+
+ private CancelListener mNotifier = new CancelListener() {
+ @Override
+ public void onCancel() {
+ synchronized (TileImageView.this) {
+ TileImageView.this.notifyAll();
+ }
+ }
+ };
+
+ @Override
+ public Void run(JobContext jc) {
+ jc.setMode(ThreadPool.MODE_NONE);
+ jc.setCancelListener(mNotifier);
+ while (!jc.isCancelled()) {
+ Tile tile = null;
+ synchronized(TileImageView.this) {
+ tile = mDecodeQueue.pop();
+ if (tile == null && !jc.isCancelled()) {
+ Utils.waitWithoutInterrupt(TileImageView.this);
+ }
+ }
+ if (tile == null) continue;
+ if (decodeTile(tile)) queueForUpload(tile);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
new file mode 100644
index 000000000..0c1f66d0c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -0,0 +1,200 @@
+/*
+ * 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.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class TileImageViewAdapter implements TileImageView.TileSource {
+ private static final String TAG = "TileImageViewAdapter";
+ protected ScreenNail mScreenNail;
+ protected boolean mOwnScreenNail;
+ protected BitmapRegionDecoder mRegionDecoder;
+ protected int mImageWidth;
+ protected int mImageHeight;
+ protected int mLevelCount;
+
+ public TileImageViewAdapter() {
+ }
+
+ public synchronized void clear() {
+ mScreenNail = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ mRegionDecoder = null;
+ }
+
+ // Caller is responsible to recycle the ScreenNail
+ public synchronized void setScreenNail(
+ ScreenNail screenNail, int width, int height) {
+ Utils.checkNotNull(screenNail);
+ mScreenNail = screenNail;
+ mImageWidth = width;
+ mImageHeight = height;
+ mRegionDecoder = null;
+ mLevelCount = 0;
+ }
+
+ public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
+ mRegionDecoder = Utils.checkNotNull(decoder);
+ mImageWidth = decoder.getWidth();
+ mImageHeight = decoder.getHeight();
+ mLevelCount = calculateLevelCount();
+ }
+
+ private int calculateLevelCount() {
+ return Math.max(0, Utils.ceilLog2(
+ (float) mImageWidth / mScreenNail.getWidth()));
+ }
+
+ // Gets a sub image on a rectangle of the current photo. For example,
+ // getTile(1, 50, 50, 100, 3, pool) means to get the region located
+ // at (50, 50) with sample level 1 (ie, down sampled by 2^1) and the
+ // target tile size (after sampling) 100 with border 3.
+ //
+ // From this spec, we can infer the actual tile size to be
+ // 100 + 3x2 = 106, and the size of the region to be extracted from the
+ // photo to be 200 with border 6.
+ //
+ // As a result, we should decode region (50-6, 50-6, 250+6, 250+6) or
+ // (44, 44, 256, 256) from the original photo and down sample it to 106.
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ @Override
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ if (!ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER) {
+ return getTileWithoutReusingBitmap(level, x, y, tileSize);
+ }
+
+ int t = tileSize << level;
+
+ Rect wantRegion = new Rect(x, y, x + t, y + t);
+
+ boolean needClear;
+ BitmapRegionDecoder regionDecoder = null;
+
+ synchronized (this) {
+ regionDecoder = mRegionDecoder;
+ if (regionDecoder == null) return null;
+
+ // We need to clear a reused bitmap, if wantRegion is not fully
+ // within the image.
+ needClear = !new Rect(0, 0, mImageWidth, mImageHeight)
+ .contains(wantRegion);
+ }
+
+ Bitmap bitmap = GalleryBitmapPool.getInstance().get(tileSize, tileSize);
+ if (bitmap != null) {
+ if (needClear) bitmap.eraseColor(0);
+ } else {
+ bitmap = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inPreferQualityOverSpeed = true;
+ options.inSampleSize = (1 << level);
+ options.inBitmap = bitmap;
+
+ try {
+ // In CropImage, we may call the decodeRegion() concurrently.
+ synchronized (regionDecoder) {
+ bitmap = regionDecoder.decodeRegion(wantRegion, options);
+ }
+ } finally {
+ if (options.inBitmap != bitmap && options.inBitmap != null) {
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ }
+ }
+
+ if (bitmap == null) {
+ Log.w(TAG, "fail in decoding region");
+ }
+ return bitmap;
+ }
+
+ private Bitmap getTileWithoutReusingBitmap(
+ int level, int x, int y, int tileSize) {
+ int t = tileSize << level;
+ Rect wantRegion = new Rect(x, y, x + t, y + t);
+
+ BitmapRegionDecoder regionDecoder;
+ Rect overlapRegion;
+
+ synchronized (this) {
+ regionDecoder = mRegionDecoder;
+ if (regionDecoder == null) return null;
+ overlapRegion = new Rect(0, 0, mImageWidth, mImageHeight);
+ Utils.assertTrue(overlapRegion.intersect(wantRegion));
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inPreferQualityOverSpeed = true;
+ options.inSampleSize = (1 << level);
+ Bitmap bitmap = null;
+
+ // In CropImage, we may call the decodeRegion() concurrently.
+ synchronized (regionDecoder) {
+ bitmap = regionDecoder.decodeRegion(overlapRegion, options);
+ }
+
+ if (bitmap == null) {
+ Log.w(TAG, "fail in decoding region");
+ }
+
+ if (wantRegion.equals(overlapRegion)) return bitmap;
+
+ Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ canvas.drawBitmap(bitmap,
+ (overlapRegion.left - wantRegion.left) >> level,
+ (overlapRegion.top - wantRegion.top) >> level, null);
+ return result;
+ }
+
+
+ @Override
+ public ScreenNail getScreenNail() {
+ return mScreenNail;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mLevelCount;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TiledScreenNail.java b/src/com/android/gallery3d/ui/TiledScreenNail.java
new file mode 100644
index 000000000..860e230bb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TiledScreenNail.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2012 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.graphics.Bitmap;
+import android.graphics.RectF;
+
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.TiledTexture;
+
+// This is a ScreenNail wraps a Bitmap. There are some extra functions:
+//
+// - If we need to draw before the bitmap is available, we draw a rectange of
+// placeholder color (gray).
+//
+// - When the the bitmap is available, and we have drawn the placeholder color
+// before, we will do a fade-in animation.
+public class TiledScreenNail implements ScreenNail {
+ @SuppressWarnings("unused")
+ private static final String TAG = "TiledScreenNail";
+
+ // The duration of the fading animation in milliseconds
+ private static final int DURATION = 180;
+
+ private static int sMaxSide = 640;
+
+ // These are special values for mAnimationStartTime
+ private static final long ANIMATION_NOT_NEEDED = -1;
+ private static final long ANIMATION_NEEDED = -2;
+ private static final long ANIMATION_DONE = -3;
+
+ private int mWidth;
+ private int mHeight;
+ private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
+
+ private Bitmap mBitmap;
+ private TiledTexture mTexture;
+
+ public TiledScreenNail(Bitmap bitmap) {
+ mWidth = bitmap.getWidth();
+ mHeight = bitmap.getHeight();
+ mBitmap = bitmap;
+ mTexture = new TiledTexture(bitmap);
+ }
+
+ public TiledScreenNail(int width, int height) {
+ setSize(width, height);
+ }
+
+ // This gets overridden by bitmap_screennail_placeholder
+ // in GalleryUtils.initialize
+ private static int mPlaceholderColor = 0xFF222222;
+ private static boolean mDrawPlaceholder = true;
+
+ public static void setPlaceholderColor(int color) {
+ mPlaceholderColor = color;
+ }
+
+ private void setSize(int width, int height) {
+ if (width == 0 || height == 0) {
+ width = sMaxSide;
+ height = sMaxSide * 3 / 4;
+ }
+ float scale = Math.min(1, (float) sMaxSide / Math.max(width, height));
+ mWidth = Math.round(scale * width);
+ mHeight = Math.round(scale * height);
+ }
+
+ // Combines the two ScreenNails.
+ // Returns the used one and recycle the unused one.
+ public ScreenNail combine(ScreenNail other) {
+ if (other == null) {
+ return this;
+ }
+
+ if (!(other instanceof TiledScreenNail)) {
+ recycle();
+ return other;
+ }
+
+ // Now both are TiledScreenNail. Move over the information about width,
+ // height, and Bitmap, then recycle the other.
+ TiledScreenNail newer = (TiledScreenNail) other;
+ mWidth = newer.mWidth;
+ mHeight = newer.mHeight;
+ if (newer.mTexture != null) {
+ if (mBitmap != null) GalleryBitmapPool.getInstance().put(mBitmap);
+ if (mTexture != null) mTexture.recycle();
+ mBitmap = newer.mBitmap;
+ mTexture = newer.mTexture;
+ newer.mBitmap = null;
+ newer.mTexture = null;
+ }
+ newer.recycle();
+ return this;
+ }
+
+ public void updatePlaceholderSize(int width, int height) {
+ if (mBitmap != null) return;
+ if (width == 0 || height == 0) return;
+ setSize(width, height);
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void noDraw() {
+ }
+
+ @Override
+ public void recycle() {
+ if (mTexture != null) {
+ mTexture.recycle();
+ mTexture = null;
+ }
+ if (mBitmap != null) {
+ GalleryBitmapPool.getInstance().put(mBitmap);
+ mBitmap = null;
+ }
+ }
+
+ public static void disableDrawPlaceholder() {
+ mDrawPlaceholder = false;
+ }
+
+ public static void enableDrawPlaceholder() {
+ mDrawPlaceholder = true;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ if (mTexture == null || !mTexture.isReady()) {
+ if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
+ mAnimationStartTime = ANIMATION_NEEDED;
+ }
+ if(mDrawPlaceholder) {
+ canvas.fillRect(x, y, width, height, mPlaceholderColor);
+ }
+ return;
+ }
+
+ if (mAnimationStartTime == ANIMATION_NEEDED) {
+ mAnimationStartTime = AnimationTime.get();
+ }
+
+ if (isAnimating()) {
+ mTexture.drawMixed(canvas, mPlaceholderColor, getRatio(), x, y,
+ width, height);
+ } else {
+ mTexture.draw(canvas, x, y, width, height);
+ }
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, RectF source, RectF dest) {
+ if (mTexture == null || !mTexture.isReady()) {
+ canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
+ mPlaceholderColor);
+ return;
+ }
+
+ mTexture.draw(canvas, source, dest);
+ }
+
+ public boolean isAnimating() {
+ // The TiledTexture may not be uploaded completely yet.
+ // In that case, we count it as animating state and we will draw
+ // the placeholder in TileImageView.
+ if (mTexture == null || !mTexture.isReady()) return true;
+ if (mAnimationStartTime < 0) return false;
+ if (AnimationTime.get() - mAnimationStartTime >= DURATION) {
+ mAnimationStartTime = ANIMATION_DONE;
+ return false;
+ }
+ return true;
+ }
+
+ private float getRatio() {
+ float r = (float) (AnimationTime.get() - mAnimationStartTime) / DURATION;
+ return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+ }
+
+ public boolean isShowingPlaceholder() {
+ return (mBitmap == null) || isAnimating();
+ }
+
+ public TiledTexture getTexture() {
+ return mTexture;
+ }
+
+ public static void setMaxSide(int size) {
+ sMaxSide = size;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/UndoBarView.java b/src/com/android/gallery3d/ui/UndoBarView.java
new file mode 100644
index 000000000..42f12ae72
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UndoBarView.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012 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.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.util.GalleryUtils;
+
+public class UndoBarView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "UndoBarView";
+
+ private static final int WHITE = 0xFFFFFFFF;
+ private static final int GRAY = 0xFFAAAAAA;
+
+ private final NinePatchTexture mPanel;
+ private final StringTexture mUndoText;
+ private final StringTexture mDeletedText;
+ private final ResourceTexture mUndoIcon;
+ private final int mBarHeight;
+ private final int mBarMargin;
+ private final int mUndoTextMargin;
+ private final int mIconSize;
+ private final int mIconMargin;
+ private final int mSeparatorTopMargin;
+ private final int mSeparatorBottomMargin;
+ private final int mSeparatorRightMargin;
+ private final int mSeparatorWidth;
+ private final int mDeletedTextMargin;
+ private final int mClickRegion;
+
+ private OnClickListener mOnClickListener;
+ private boolean mDownOnButton;
+
+ // This is the layout of UndoBarView. The unit is dp.
+ //
+ // +-+----+----------------+-+--+----+-+------+--+-+
+ // 48 | | | Deleted | | | <- | | UNDO | | |
+ // +-+----+----------------+-+--+----+-+------+--+-+
+ // 4 16 1 12 32 8 16 4
+ public UndoBarView(Context context) {
+ mBarHeight = GalleryUtils.dpToPixel(48);
+ mBarMargin = GalleryUtils.dpToPixel(4);
+ mUndoTextMargin = GalleryUtils.dpToPixel(16);
+ mIconMargin = GalleryUtils.dpToPixel(8);
+ mIconSize = GalleryUtils.dpToPixel(32);
+ mSeparatorRightMargin = GalleryUtils.dpToPixel(12);
+ mSeparatorTopMargin = GalleryUtils.dpToPixel(10);
+ mSeparatorBottomMargin = GalleryUtils.dpToPixel(10);
+ mSeparatorWidth = GalleryUtils.dpToPixel(1);
+ mDeletedTextMargin = GalleryUtils.dpToPixel(16);
+
+ mPanel = new NinePatchTexture(context, R.drawable.panel_undo_holo);
+ mUndoText = StringTexture.newInstance(context.getString(R.string.undo),
+ GalleryUtils.dpToPixel(12), GRAY, 0, true);
+ mDeletedText = StringTexture.newInstance(
+ context.getString(R.string.deleted),
+ GalleryUtils.dpToPixel(16), WHITE);
+ mUndoIcon = new ResourceTexture(
+ context, R.drawable.ic_menu_revert_holo_dark);
+ mClickRegion = mBarMargin + mUndoTextMargin + mUndoText.getWidth()
+ + mIconMargin + mIconSize + mSeparatorRightMargin;
+ }
+
+ public void setOnClickListener(OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ setMeasuredSize(0 /* unused */, mBarHeight);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ advanceAnimation();
+
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(mAlpha);
+
+ int w = getWidth();
+ int h = getHeight();
+ mPanel.draw(canvas, mBarMargin, 0, w - mBarMargin * 2, mBarHeight);
+
+ int x = w - mBarMargin;
+ int y;
+
+ x -= mUndoTextMargin + mUndoText.getWidth();
+ y = (mBarHeight - mUndoText.getHeight()) / 2;
+ mUndoText.draw(canvas, x, y);
+
+ x -= mIconMargin + mIconSize;
+ y = (mBarHeight - mIconSize) / 2;
+ mUndoIcon.draw(canvas, x, y, mIconSize, mIconSize);
+
+ x -= mSeparatorRightMargin + mSeparatorWidth;
+ y = mSeparatorTopMargin;
+ canvas.fillRect(x, y, mSeparatorWidth,
+ mBarHeight - mSeparatorTopMargin - mSeparatorBottomMargin, GRAY);
+
+ x = mBarMargin + mDeletedTextMargin;
+ y = (mBarHeight - mDeletedText.getHeight()) / 2;
+ mDeletedText.draw(canvas, x, y);
+
+ canvas.restore();
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownOnButton = inUndoButton(event);
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mDownOnButton) {
+ if (mOnClickListener != null && inUndoButton(event)) {
+ mOnClickListener.onClick(this);
+ }
+ mDownOnButton = false;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mDownOnButton = false;
+ break;
+ }
+ return true;
+ }
+
+ // Check if the event is on the right of the separator
+ private boolean inUndoButton(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ int w = getWidth();
+ int h = getHeight();
+ return (x >= w - mClickRegion && x < w && y >= 0 && y < h);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Alpha Animation
+ ////////////////////////////////////////////////////////////////////////////
+
+ private static final long NO_ANIMATION = -1;
+ private static long ANIM_TIME = 200;
+ private long mAnimationStartTime = NO_ANIMATION;
+ private float mFromAlpha, mToAlpha;
+ private float mAlpha;
+
+ private static float getTargetAlpha(int visibility) {
+ return (visibility == VISIBLE) ? 1f : 0f;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mAlpha = getTargetAlpha(visibility);
+ mAnimationStartTime = NO_ANIMATION;
+ super.setVisibility(visibility);
+ invalidate();
+ }
+
+ public void animateVisibility(int visibility) {
+ float target = getTargetAlpha(visibility);
+ if (mAnimationStartTime == NO_ANIMATION && mAlpha == target) return;
+ if (mAnimationStartTime != NO_ANIMATION && mToAlpha == target) return;
+
+ mFromAlpha = mAlpha;
+ mToAlpha = target;
+ mAnimationStartTime = AnimationTime.startTime();
+
+ super.setVisibility(VISIBLE);
+ invalidate();
+ }
+
+ private void advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) return;
+
+ float delta = (float) (AnimationTime.get() - mAnimationStartTime) /
+ ANIM_TIME;
+ mAlpha = mFromAlpha + ((mToAlpha > mFromAlpha) ? delta : -delta);
+ mAlpha = Utils.clamp(mAlpha, 0f, 1f);
+
+ if (mAlpha == mToAlpha) {
+ mAnimationStartTime = NO_ANIMATION;
+ if (mAlpha == 0) {
+ super.setVisibility(INVISIBLE);
+ }
+ }
+ invalidate();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java
new file mode 100644
index 000000000..bc4a71800
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UserInteractionListener.java
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+public interface UserInteractionListener {
+ // Called when a user interaction begins (for example, fling).
+ public void onUserInteractionBegin();
+ // Called when the user interaction ends.
+ public void onUserInteractionEnd();
+ // Other one-shot user interactions.
+ public void onUserInteraction();
+}
diff --git a/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java
new file mode 100644
index 000000000..ee61d8edb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 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.app.Activity;
+import android.content.Context;
+import android.os.PowerManager;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+
+public class WakeLockHoldingProgressListener implements MenuExecutor.ProgressListener {
+ static private final String DEFAULT_WAKE_LOCK_LABEL = "Gallery Progress Listener";
+ private AbstractGalleryActivity mActivity;
+ private PowerManager.WakeLock mWakeLock;
+
+ public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity) {
+ this(galleryActivity, DEFAULT_WAKE_LOCK_LABEL);
+ }
+
+ public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity, String label) {
+ mActivity = galleryActivity;
+ PowerManager pm =
+ (PowerManager) ((Activity) mActivity).getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, label);
+ }
+
+ @Override
+ public void onProgressComplete(int result) {
+ mWakeLock.release();
+ }
+
+ @Override
+ public void onProgressStart() {
+ mWakeLock.acquire();
+ }
+
+ protected AbstractGalleryActivity getActivity() {
+ return mActivity;
+ }
+
+ @Override
+ public void onProgressUpdate(int index) {
+ }
+
+ @Override
+ public void onConfirmDialogDismissed(boolean confirmed) {
+ }
+
+ @Override
+ public void onConfirmDialogShown() {
+ }
+}