summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRavi Banuri <ravibanuri@codeaurora.org>2015-10-19 16:03:03 +0530
committerGerrit - the friendly Code Review server <code-review@localhost>2015-11-08 22:55:05 -0800
commit7a41c376e299d9944a956004c5176b31a5ef0a4e (patch)
treed11d9e09ca81fed337fdf5d45ac658b80ef1ee43 /src
parentec09287bd140f2ec076b62f99f2e5b6ddf464215 (diff)
downloadandroid_packages_apps_Gallery2-7a41c376e299d9944a956004c5176b31a5ef0a4e.tar.gz
android_packages_apps_Gallery2-7a41c376e299d9944a956004c5176b31a5ef0a4e.tar.bz2
android_packages_apps_Gallery2-7a41c376e299d9944a956004c5176b31a5ef0a4e.zip
Galley2-FR: Create new copy of AOSP files to enhance the functionality.
New files created from Opensource files & taken from LA.BR.1.2.5 tip with commit id: 7574509c9d3b73e77917333661e0a2fbab777129 from res/layout/filtershow_editor_panel.xml to res/layout/filtershow_actionbar_effects.xml with commit id: c12b8b10bc4c033ba6855a400f14c3aae52ec143 from src/com/android/gallery3d/app/AlbumPage.java to src/com/android/gallery3d/app/TimeLinePage.java with commit id: ed2687a7d84b0d5ab9bf6eb1b20be2ec745444c6 from src/com/android/gallery3d/app/AlbumDataLoader.java to src/com/android/gallery3d/app/TimeLineDataLoader.java with commit id: be49d8c1ec074ef27c5c49f1dd7b51e236f96bdb from src/com/android/gallery3d/data/LocalImage.java to src/com/android/gallery3d/data/TimeLineTitleMediaItem.java with commit id: ed2687a7d84b0d5ab9bf6eb1b20be2ec745444c6 from src/com/android/gallery3d/ui/AlbumLabelMaker.java to src/com/android/gallery3d/ui/TimeLineTitleMaker.java with commit id: c12b8b10bc4c033ba6855a400f14c3aae52ec143 from src/com/android/gallery3d/ui/AlbumSlidingWindow.java to src/com/android/gallery3d/ui/TimeLineSlidingWindow.java with commit id: cf002f22904821fb603e6bb625c50159ddc5d8bb from src/com/android/gallery3d/ui/AlbumSlotRenderer.java to src/com/android/gallery3d/ui/TimeLineSlotRenderer.java with commit id: ed2687a7d84b0d5ab9bf6eb1b20be2ec745444c6 from src/com/android/gallery3d/ui/SlotView.java to src/com/android/gallery3d/ui/TimeLineSlotView.java Change-Id: I37c8b0e92e7a23069186f08ddeef3b766d5c8c05 CRs-Fixed: 926603
Diffstat (limited to 'src')
-rw-r--r--src/com/android/gallery3d/app/TimeLineDataLoader.java402
-rw-r--r--src/com/android/gallery3d/app/TimeLinePage.java853
-rw-r--r--src/com/android/gallery3d/data/TimeLineTitleMediaItem.java385
-rw-r--r--src/com/android/gallery3d/ui/TimeLineSlidingWindow.java365
-rw-r--r--src/com/android/gallery3d/ui/TimeLineSlotRenderer.java205
-rw-r--r--src/com/android/gallery3d/ui/TimeLineSlotView.java835
-rw-r--r--src/com/android/gallery3d/ui/TimeLineTitleMaker.java239
7 files changed, 3284 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/app/TimeLineDataLoader.java b/src/com/android/gallery3d/app/TimeLineDataLoader.java
new file mode 100644
index 000000000..b56304e39
--- /dev/null
+++ b/src/com/android/gallery3d/app/TimeLineDataLoader.java
@@ -0,0 +1,402 @@
+/*
+ * 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.app;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.text.TextUtils;
+import android.view.View;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+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.ui.SynchronizedHandler;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.Locale;
+
+public class AlbumDataLoader {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumDataAdapter";
+ private static final int DATA_CACHE_SIZE = 1000;
+
+ private static final int MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+
+ private static final int MIN_LOAD_COUNT = 32;
+ private static final int MAX_LOAD_COUNT = 64;
+
+ private final MediaItem[] mData;
+ private final long[] mItemVersion;
+ private final long[] mSetVersion;
+
+ public static interface DataListener {
+ public void onContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private final MediaSet mSource;
+ private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;
+
+ private final Handler mMainHandler;
+ private int mSize = 0;
+
+ private DataListener mDataListener;
+ private MySourceListener mSourceListener = new MySourceListener();
+ private LoadingListener mLoadingListener;
+
+ private ReloadTask mReloadTask;
+ // the data version on which last loading failed
+ private long mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+
+ public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) {
+ mSource = mediaSet;
+
+ mData = new MediaItem[DATA_CACHE_SIZE];
+ mItemVersion = new long[DATA_CACHE_SIZE];
+ mSetVersion = new long[DATA_CACHE_SIZE];
+ Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
+ Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);
+
+ mMainHandler = new SynchronizedHandler(context.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_RUN_OBJECT:
+ ((Runnable) message.obj).run();
+ return;
+ case MSG_LOAD_START:
+ if (mLoadingListener != null) mLoadingListener.onLoadingStarted();
+ return;
+ case MSG_LOAD_FINISH:
+ if (mLoadingListener != null) {
+ boolean loadingFailed =
+ (mFailedVersion != MediaObject.INVALID_DATA_VERSION);
+ mLoadingListener.onLoadingFinished(loadingFailed);
+ }
+ return;
+ }
+ }
+ };
+ }
+
+ public void resume() {
+ mSource.addContentListener(mSourceListener);
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+ }
+
+ public void pause() {
+ mReloadTask.terminate();
+ mReloadTask = null;
+ mSource.removeContentListener(mSourceListener);
+ }
+
+ public MediaItem get(int index) {
+ if (!isActive(index)
+ && View.LAYOUT_DIRECTION_LTR == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {
+ return mSource.getMediaItem(index, 1).get(0);
+ }
+ return mData[index % mData.length];
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // Returns the index of the MediaItem with the given path or
+ // -1 if the path is not cached
+ public int findItem(Path id) {
+ for (int i = mContentStart; i < mContentEnd; i++) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ if (item != null && id == item.getPath()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void clearSlot(int slotIndex) {
+ mData[slotIndex] = null;
+ mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+ int end = mContentEnd;
+ int start = mContentStart;
+
+ // We need change the content window before calling reloadData(...)
+ synchronized (this) {
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+ long[] itemVersion = mItemVersion;
+ long[] setVersion = mSetVersion;
+ if (contentStart >= end || start >= contentEnd) {
+ for (int i = start, n = end; i < n; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ } else {
+ for (int i = start; i < contentStart; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ for (int i = contentEnd, n = end; i < n; ++i) {
+ clearSlot(i % DATA_CACHE_SIZE);
+ }
+ }
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+
+ public void setActiveWindow(int start, int end) {
+ if (start == mActiveStart && end == mActiveEnd) return;
+
+ Utils.assertTrue(start <= end
+ && end - start <= mData.length && end <= mSize);
+
+ int length = mData.length;
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - length / 2,
+ 0, Math.max(0, mSize - length));
+ int contentEnd = Math.min(contentStart + length, mSize);
+ if (mContentStart > start || mContentEnd < end
+ || Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
+ setContentWindow(contentStart, contentEnd);
+ }
+ }
+
+ private class MySourceListener implements ContentListener {
+ @Override
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(callable);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, task));
+ try {
+ return task.get();
+ } catch (InterruptedException e) {
+ return null;
+ } catch (ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public int reloadStart;
+ public int reloadCount;
+
+ public int size;
+ public ArrayList<MediaItem> items;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+ private final long mVersion;
+
+ public GetUpdateInfo(long version) {
+ mVersion = version;
+ }
+
+ @Override
+ public UpdateInfo call() throws Exception {
+ if (mFailedVersion == mVersion) {
+ // previous loading failed, return null to pause loading
+ return null;
+ }
+ UpdateInfo info = new UpdateInfo();
+ long version = mVersion;
+ info.version = mSourceVersion;
+ info.size = mSize;
+ long setVersion[] = mSetVersion;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ int index = i % DATA_CACHE_SIZE;
+ if (setVersion[index] != version) {
+ info.reloadStart = i;
+ info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
+ return info;
+ }
+ }
+ return mSourceVersion == mVersion ? null : info;
+ }
+ }
+
+ private class UpdateContent implements Callable<Void> {
+
+ private UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo info) {
+ mUpdateInfo = info;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+ if (mSize != info.size) {
+ mSize = info.size;
+ if (mDataListener != null) mDataListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+
+ ArrayList<MediaItem> items = info.items;
+
+ mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+ if ((items == null) || items.isEmpty()) {
+ if (info.reloadCount > 0) {
+ mFailedVersion = info.version;
+ Log.d(TAG, "loading failed: " + mFailedVersion);
+ }
+ return null;
+ }
+ int start = Math.max(info.reloadStart, mContentStart);
+ int end = Math.min(info.reloadStart + items.size(), mContentEnd);
+
+ for (int i = start; i < end; ++i) {
+ int index = i % DATA_CACHE_SIZE;
+ mSetVersion[index] = info.version;
+ MediaItem updateItem = items.get(i - info.reloadStart);
+ long itemVersion = updateItem.getDataVersion();
+ if (mItemVersion[index] != itemVersion) {
+ mItemVersion[index] = itemVersion;
+ mData[index] = updateItem;
+ if (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
+ mDataListener.onContentChanged(i);
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ /*
+ * The thread model of ReloadTask
+ * *
+ * [Reload Task] [Main Thread]
+ * | |
+ * getUpdateInfo() --> | (synchronous call)
+ * (wait) <---- getUpdateInfo()
+ * | |
+ * Load Data |
+ * | |
+ * updateContent() --> | (synchronous call)
+ * (wait) updateContent()
+ * | |
+ * | |
+ */
+ private class ReloadTask extends Thread {
+
+ private volatile boolean mActive = true;
+ private volatile boolean mDirty = true;
+ private boolean mIsLoading = false;
+
+ private void updateLoading(boolean loading) {
+ if (mIsLoading == loading) return;
+ mIsLoading = loading;
+ mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START : MSG_LOAD_FINISH);
+ }
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ updateLoading(false);
+ if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
+ Log.d(TAG, "reload pause");
+ }
+ Utils.waitWithoutInterrupt(this);
+ if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
+ Log.d(TAG, "reload resume");
+ }
+ continue;
+ }
+ mDirty = false;
+ }
+ updateLoading(true);
+ long version = mSource.reload();
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+ if (info.version != version) {
+ info.size = mSource.getMediaItemCount();
+ info.version = version;
+ }
+ if (info.reloadCount > 0) {
+ info.items = mSource.getMediaItem(info.reloadStart, info.reloadCount);
+ }
+ executeAndWait(new UpdateContent(info));
+ }
+ updateLoading(false);
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/TimeLinePage.java b/src/com/android/gallery3d/app/TimeLinePage.java
new file mode 100644
index 000000000..932865a06
--- /dev/null
+++ b/src/com/android/gallery3d/app/TimeLinePage.java
@@ -0,0 +1,853 @@
+/*
+ * 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.app;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.drm.DrmHelper;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaDetails;
+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.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.glrenderer.FadeTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumSlotRenderer;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
+import com.android.gallery3d.ui.RelativePosition;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import java.util.Locale;
+
+public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
+ SelectionManager.SelectionListener, MediaSet.SyncListener, GalleryActionBar.OnAlbumModeSelectedListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumPage";
+
+ public static final String KEY_MEDIA_PATH = "media-path";
+ public static final String KEY_PARENT_MEDIA_PATH = "parent-media-path";
+ public static final String KEY_SET_CENTER = "set-center";
+ public static final String KEY_AUTO_SELECT_ALL = "auto-select-all";
+ public static final String KEY_SHOW_CLUSTER_MENU = "cluster-menu";
+ public static final String KEY_EMPTY_ALBUM = "empty-album";
+ public static final String KEY_RESUME_ANIMATION = "resume_animation";
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ public static final int REQUEST_PHOTO = 2;
+ private static final int REQUEST_DO_ANIMATION = 3;
+
+ private static final int BIT_LOADING_RELOAD = 1;
+ private static final int BIT_LOADING_SYNC = 2;
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+
+ // Data cache size, equal to AlbumDataLoader.DATA_CACHE_SIZE
+ private static final int DATA_CACHE_SIZE = 256;
+
+ private boolean mIsActive = false;
+ private AlbumSlotRenderer mAlbumView;
+ private Path mMediaSetPath;
+ private String mParentMediaSetString;
+ private SlotView mSlotView;
+
+ private AlbumDataLoader mAlbumDataAdapter;
+
+ protected SelectionManager mSelectionManager;
+
+ private boolean mGetContent;
+ private boolean mShowClusterMenu;
+
+ private ActionModeHandler mActionModeHandler;
+ private int mFocusIndex = 0;
+ private DetailsHelper mDetailsHelper;
+ private MyDetailsSource mDetailsSource;
+ private MediaSet mMediaSet;
+ private boolean mShowDetails;
+ private float mUserDistance; // in pixel
+ private Future<Integer> mSyncTask = null;
+ private boolean mLaunchedFromPhotoPage;
+ private boolean mInCameraApp;
+ private boolean mInCameraAndWantQuitOnPause;
+
+ private int mLoadingBits = 0;
+ private boolean mInitialSynced = false;
+ private int mSyncResult;
+ private boolean mLoadingFailed;
+ private RelativePosition mOpenCenter = new RelativePosition();
+
+ private Handler mHandler;
+ private static final int MSG_PICK_PHOTO = 0;
+
+ private PhotoFallbackEffect mResumeEffect;
+ private PhotoFallbackEffect.PositionProvider mPositionProvider =
+ new PhotoFallbackEffect.PositionProvider() {
+ @Override
+ public Rect getPosition(int index) {
+ Rect rect = mSlotView.getSlotRect(index);
+ Rect bounds = mSlotView.bounds();
+ rect.offset(bounds.left - mSlotView.getScrollX(),
+ bounds.top - mSlotView.getScrollY());
+ return rect;
+ }
+
+ @Override
+ public int getItemIndex(Path path) {
+ int start = mSlotView.getVisibleStart();
+ int end = mSlotView.getVisibleEnd();
+ for (int i = start; i < end; ++i) {
+ MediaItem item = mAlbumDataAdapter.get(i);
+ if (item != null && item.getPath() == path) return i;
+ }
+ return -1;
+ }
+ };
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.album_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ private final float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+
+ int slotViewTop = mActivity.getGalleryActionBar().getHeight();
+ int slotViewBottom = bottom - top;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsHelper.layout(left, slotViewTop, right, bottom);
+ } else {
+ mAlbumView.setHighlightItemPath(null);
+ }
+
+ // Set the mSlotView as a reference point to the open animation
+ mOpenCenter.setReferencePosition(0, slotViewTop);
+ mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ (right - left) / 2, (bottom - top) / 2, -mUserDistance);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+
+ if (mResumeEffect != null) {
+ boolean more = mResumeEffect.draw(canvas);
+ if (!more) {
+ mResumeEffect = null;
+ mAlbumView.setSlotFilter(null);
+ }
+ // We want to render one more time even when no more effect
+ // required. So that the animated thumbnails could be draw
+ // with declarations in super.render().
+ invalidate();
+ }
+ canvas.restore();
+ }
+ };
+
+ // This are the transitions we want:
+ //
+ // +--------+ +------------+ +-------+ +----------+
+ // | Camera |---------->| Fullscreen |--->| Album |--->| AlbumSet |
+ // | View | thumbnail | Photo | up | Page | up | Page |
+ // +--------+ +------------+ +-------+ +----------+
+ // ^ | | ^ |
+ // | | | | | close
+ // +----------back--------+ +----back----+ +--back-> app
+ //
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ } else {
+ if(mLaunchedFromPhotoPage) {
+ mActivity.getTransitionStore().putIfNotPresent(
+ PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_RESUMED);
+ }
+ // TODO: fix this regression
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ if (mInCameraApp) {
+ super.onBackPressed();
+ } else {
+ onUpPressed();
+ }
+ }
+ }
+
+ private void onUpPressed() {
+ if (mInCameraApp) {
+ GalleryUtils.startGalleryActivity(mActivity);
+ } else if (mActivity.getStateManager().getStateCount() > 1) {
+ super.onBackPressed();
+ } else if (mParentMediaSetString != null) {
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, mParentMediaSetString);
+ mActivity.getStateManager().switchState(
+ this, AlbumSetPage.class, data);
+ }
+ }
+
+ private void onDown(int index) {
+ mAlbumView.setPressedIndex(index);
+ }
+
+ private void onUp(boolean followedByLongPress) {
+ if (followedByLongPress) {
+ // Avoid showing press-up animations for long-press.
+ mAlbumView.setPressedIndex(-1);
+ } else {
+ mAlbumView.setPressedUp();
+ }
+ }
+
+ private void onSingleTapUp(int slotIndex) {
+ if (!mIsActive) return;
+
+ if (mSelectionManager.inSelectionMode()) {
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return; // Item not ready yet, ignore the click
+ mSelectionManager.toggle(item.getPath());
+ mSlotView.invalidate();
+ } else {
+ // Render transition in pressed state
+ mAlbumView.setPressedIndex(slotIndex);
+ mAlbumView.setPressedUp();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_PHOTO, slotIndex, 0),
+ FadeTexture.DURATION);
+ }
+ }
+
+ private void pickPhoto(int slotIndex) {
+ if ((View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault()))
+ && !mGetContent) {
+ // Fetch corresponding slotIndex from another side, (RTL)
+ if (slotIndex > DATA_CACHE_SIZE / 2
+ && slotIndex < mAlbumDataAdapter.size() - DATA_CACHE_SIZE / 2) {
+ slotIndex = mAlbumDataAdapter.size() - slotIndex - 2;
+ } else {
+ slotIndex = mAlbumDataAdapter.size() - slotIndex - 1;
+ }
+ }
+ pickPhoto(slotIndex, false);
+ }
+
+ private void pickPhoto(int slotIndex, boolean startInFilmstrip) {
+ if (!mIsActive) return;
+
+ if (!startInFilmstrip) {
+ // Launch photos in lights out mode
+ mActivity.getGLRoot().setLightsOutMode(true);
+ }
+
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+
+ // Checking it is RTL or not
+ boolean isLayoutRtl = (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) ? true : false;
+
+ // When not RTL, return directly to ignore the click
+ if (!isLayoutRtl && item == null) {
+ return;
+ }
+
+ if (mGetContent) {
+ if (isLayoutRtl && item == null) {
+ return; // Item not ready yet, ignore the click
+ }
+ if (DrmHelper.isDrmFile(DrmHelper.getFilePath(
+ mActivity.getAndroidContext(), item.getContentUri()))) {
+ Toast.makeText(mActivity, R.string.no_permission_for_drm,
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ onGetContent(item);
+ } else if (mLaunchedFromPhotoPage) {
+ if (isLayoutRtl && item == null) {
+ return; // Item not ready yet, ignore the click
+ }
+ TransitionStore transitions = mActivity.getTransitionStore();
+ transitions.put(
+ PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_PICKED);
+ transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ onBackPressed();
+ } else {
+ // Get into the PhotoPage.
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ Bundle data = new Bundle();
+ data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ mMediaSetPath.toString());
+
+ // Item not ready yet, don't pass the photo path to bundle
+ if (!isLayoutRtl && item != null) {
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+ item.getPath().toString());
+ }
+ data.putInt(PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_STARTED);
+ data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP,
+ startInFilmstrip);
+ data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, mMediaSet.isCameraRoll());
+ if (startInFilmstrip) {
+ mActivity.getStateManager().switchState(this, FilmstripPage.class, data);
+ } else {
+ mActivity.getStateManager().startStateForResult(
+ SinglePhotoPage.class, REQUEST_PHOTO, data);
+ }
+ }
+ }
+
+ private void onGetContent(final MediaItem item) {
+ DataManager dm = mActivity.getDataManager();
+ Activity activity = mActivity;
+ if (mData.getString(GalleryActivity.EXTRA_CROP) != null) {
+ Uri uri = dm.getContentUri(item.getPath());
+ Intent intent = new Intent(CropActivity.CROP_ACTION, uri)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtras(getData());
+ if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) {
+ intent.putExtra(CropExtras.KEY_RETURN_DATA, true);
+ }
+ activity.startActivity(intent);
+ activity.finish();
+ } else {
+ Intent intent = new Intent(null, item.getContentUri())
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ activity.setResult(Activity.RESULT_OK, intent);
+ activity.finish();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent) return;
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(item.getPath());
+ mSlotView.invalidate();
+ }
+
+ @Override
+ public void doCluster(int clusterType) {
+ String basePath = mMediaSet.getPath().toString();
+ String newPath = FilterUtils.newClusterPath(basePath, clusterType);
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, newPath);
+ if (mShowClusterMenu) {
+ Context context = mActivity.getAndroidContext();
+ data.putString(AlbumSetPage.KEY_SET_TITLE, mMediaSet.getName());
+ data.putString(AlbumSetPage.KEY_SET_SUBTITLE,
+ GalleryActionBar.getClusterByTypeString(context, clusterType));
+ }
+
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ mActivity.getStateManager().startStateForResult(
+ AlbumSetPage.class, REQUEST_DO_ANIMATION, data);
+ }
+
+ @Override
+ protected void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mUserDistance = GalleryUtils.meterToPixel(USER_DISTANCE_METER);
+ initializeViews();
+ initializeData(data);
+ mGetContent = data.getBoolean(GalleryActivity.KEY_GET_CONTENT, false);
+ mShowClusterMenu = data.getBoolean(KEY_SHOW_CLUSTER_MENU, false);
+ mDetailsSource = new MyDetailsSource();
+ Context context = mActivity.getAndroidContext();
+
+ if (data.getBoolean(KEY_AUTO_SELECT_ALL)) {
+ mSelectionManager.selectAll();
+ }
+
+ mLaunchedFromPhotoPage =
+ mActivity.getStateManager().hasStateClass(FilmstripPage.class);
+ mInCameraApp = data.getBoolean(PhotoPage.KEY_APP_BRIDGE, false);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_PICK_PHOTO: {
+ pickPhoto(message.arg1);
+ break;
+ }
+ default:
+ throw new AssertionError(message.what);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsActive = true;
+
+ mResumeEffect = mActivity.getTransitionStore().get(KEY_RESUME_ANIMATION);
+ if (mResumeEffect != null) {
+ mAlbumView.setSlotFilter(mResumeEffect);
+ mResumeEffect.setPositionProvider(mPositionProvider);
+ mResumeEffect.start();
+ }
+
+ setContentPane(mRootPane);
+
+ boolean enableHomeButton = (mActivity.getStateManager().getStateCount() > 1) |
+ mParentMediaSetString != null;
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ actionBar.setDisplayOptions(enableHomeButton, false);
+ if (!mGetContent) {
+ actionBar.enableAlbumModeMenu(GalleryActionBar.ALBUM_GRID_MODE_SELECTED, this);
+ }
+
+ // Set the reload bit here to prevent it exit this page in clearLoadingBit().
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = false;
+ mAlbumDataAdapter.resume();
+
+ mAlbumView.resume();
+ mAlbumView.setPressedIndex(-1);
+ mActionModeHandler.resume();
+ if (!mInitialSynced) {
+ setLoadingBit(BIT_LOADING_SYNC);
+ mSyncTask = mMediaSet.requestSync(this);
+ }
+ mInCameraAndWantQuitOnPause = mInCameraApp;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsActive = false;
+
+ if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ }
+ mAlbumView.setSlotFilter(null);
+ mActionModeHandler.pause();
+ mAlbumDataAdapter.pause();
+ mAlbumView.pause();
+ DetailsHelper.pause();
+ if (!mGetContent) {
+ mActivity.getGalleryActionBar().disableAlbumModeMenu(true);
+ }
+
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ clearLoadingBit(BIT_LOADING_SYNC);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mAlbumDataAdapter != null) {
+ mAlbumDataAdapter.setLoadingListener(null);
+ }
+ mActionModeHandler.destroy();
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mSelectionManager.setSelectionListener(this);
+ Config.AlbumPage config = Config.AlbumPage.get(mActivity);
+ mSlotView = new SlotView(mActivity, config.slotViewSpec);
+ mAlbumView = new AlbumSlotRenderer(mActivity, mSlotView,
+ mSelectionManager, config.placeholderColor);
+ mSlotView.setSlotRenderer(mAlbumView);
+ mRootPane.addComponent(mSlotView);
+ mSlotView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onDown(int index) {
+ AlbumPage.this.onDown(index);
+ }
+
+ @Override
+ public void onUp(boolean followedByLongPress) {
+ AlbumPage.this.onUp(followedByLongPress);
+ }
+
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ AlbumPage.this.onSingleTapUp(slotIndex);
+ }
+
+ @Override
+ public void onLongTap(int slotIndex) {
+ AlbumPage.this.onLongTap(slotIndex);
+ }
+ });
+ mActionModeHandler = new ActionModeHandler(mActivity, mSelectionManager);
+ mActionModeHandler.setActionModeListener(new ActionModeListener() {
+ @Override
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ }
+
+ private void initializeData(Bundle data) {
+ mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH));
+ mParentMediaSetString = data.getString(KEY_PARENT_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath);
+ if (mMediaSet == null) {
+ Utils.fail("MediaSet is null. Path = %s", mMediaSetPath);
+ }
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumDataAdapter = new AlbumDataLoader(mActivity, mMediaSet);
+ mAlbumDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumView.setModel(mAlbumDataAdapter);
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsHelper == null) {
+ mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
+ mDetailsHelper.setCloseListener(new CloseListener() {
+ @Override
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ }
+ mDetailsHelper.show();
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsHelper.hide();
+ mAlbumView.setHighlightItemPath(null);
+ mSlotView.invalidate();
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ MenuInflater inflator = getSupportMenuInflater();
+ if (mGetContent) {
+ inflator.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(GalleryActivity.KEY_TYPE_BITS,
+ DataManager.INCLUDE_IMAGE);
+ actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ } else {
+ inflator.inflate(R.menu.album, menu);
+ actionBar.setTitle(mMediaSet.getName());
+
+ FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true);
+
+ menu.findItem(R.id.action_group_by).setVisible(mShowClusterMenu);
+ menu.findItem(R.id.action_camera).setVisible(
+ MediaSetUtils.isCameraSource(mMediaSetPath)
+ && GalleryUtils.isCameraAvailable(mActivity));
+
+ }
+ actionBar.setSubtitle(null);
+ return true;
+ }
+
+ private void prepareAnimationBackToFilmstrip(int slotIndex) {
+ if (mAlbumDataAdapter == null || !mAlbumDataAdapter.isActive(slotIndex)) return;
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ TransitionStore transitions = mActivity.getTransitionStore();
+ transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ transitions.put(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ }
+
+ private void switchToFilmstrip() {
+ // Invalid album, return back directly.
+ if (mAlbumDataAdapter.size() < 1) {
+ return;
+ }
+
+ int targetPhoto = mSlotView.getVisibleStart();
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {
+ // Fetch corresponding index from another side, only in RTL
+ targetPhoto = mAlbumDataAdapter.size() - targetPhoto - 1;
+ }
+ prepareAnimationBackToFilmstrip(targetPhoto);
+ if(mLaunchedFromPhotoPage) {
+ onBackPressed();
+ } else {
+ pickPhoto(targetPhoto, true);
+ }
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home: {
+ onUpPressed();
+ return true;
+ }
+ case R.id.action_cancel:
+ mActivity.getStateManager().finishState(this);
+ return true;
+ case R.id.action_select:
+ mSelectionManager.setAutoLeaveSelectionMode(false);
+ mSelectionManager.enterSelectionMode();
+ return true;
+ case R.id.action_group_by: {
+ mActivity.getGalleryActionBar().showClusterDialog(this);
+ return true;
+ }
+ case R.id.action_slideshow: {
+ mInCameraAndWantQuitOnPause = false;
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH,
+ mMediaSetPath.toString());
+ data.putBoolean(SlideshowPage.KEY_REPEAT, true);
+ mActivity.getStateManager().startStateForResult(
+ SlideshowPage.class, REQUEST_SLIDESHOW, data);
+ return true;
+ }
+ case R.id.action_details: {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails();
+ }
+ return true;
+ }
+ case R.id.action_camera: {
+ GalleryUtils.startCameraActivity(mActivity);
+ return true;
+ }
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onStateResult(int request, int result, Intent data) {
+ switch (request) {
+ case REQUEST_SLIDESHOW: {
+ // data could be null, if there is no images in the album
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(SlideshowPage.KEY_PHOTO_INDEX, 0);
+ mSlotView.setCenterIndex(mFocusIndex);
+ break;
+ }
+ case REQUEST_PHOTO: {
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(PhotoPage.KEY_RETURN_INDEX_HINT, 0);
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {
+ // Fetch corresponding index from another side, only in RTL
+ mFocusIndex = mAlbumDataAdapter.size() - mFocusIndex - 1;
+ // Prepare to jump to mFocusIndex position, only enabled in RTL
+ mSlotView.setIsFromPhotoPage(true);
+ }
+
+ // Let picture of mFocusIndex visible
+ mSlotView.makeSlotVisible(mFocusIndex);
+
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {
+ // Reset variable
+ mSlotView.setIsFromPhotoPage(false);
+ }
+ break;
+ }
+ case REQUEST_DO_ANIMATION: {
+ mSlotView.startRisingAnimation();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionModeChange(int mode) {
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActionModeHandler.startActionMode();
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionModeHandler.finishActionMode();
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ mActionModeHandler.updateSupportedOperation();
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionChange(Path path, boolean selected) {
+ int count = mSelectionManager.getSelectedCount();
+ String format = mActivity.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count);
+ mActionModeHandler.setTitle(String.format(format, count));
+ mActionModeHandler.updateSupportedOperation(path, selected);
+ }
+
+ @Override
+ public void onSyncDone(final MediaSet mediaSet, final int resultCode) {
+ Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result="
+ + resultCode);
+ ((Activity) mActivity).runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ mSyncResult = resultCode;
+ try {
+ if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+ mInitialSynced = true;
+ }
+ clearLoadingBit(BIT_LOADING_SYNC);
+ showSyncErrorIfNecessary(mLoadingFailed);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+ });
+ }
+
+ // Show sync error toast when all the following conditions are met:
+ // (1) both loading and sync are done,
+ // (2) sync result is error,
+ // (3) the page is still active, and
+ // (4) no photo is shown or loading fails.
+ private void showSyncErrorIfNecessary(boolean loadingFailed) {
+ if ((mLoadingBits == 0) && (mSyncResult == MediaSet.SYNC_RESULT_ERROR) && mIsActive
+ && (loadingFailed || (mAlbumDataAdapter.size() == 0))) {
+ Toast.makeText(mActivity, R.string.sync_album_error,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void setLoadingBit(int loadTaskBit) {
+ mLoadingBits |= loadTaskBit;
+ }
+
+ private void clearLoadingBit(int loadTaskBit) {
+ mLoadingBits &= ~loadTaskBit;
+ if (mLoadingBits == 0 && mIsActive) {
+ if (mAlbumDataAdapter.size() == 0) {
+ Intent result = new Intent();
+ result.putExtra(KEY_EMPTY_ALBUM, true);
+ setStateResult(Activity.RESULT_OK, result);
+ mActivity.getStateManager().finishState(this);
+ }
+ }
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ @Override
+ public void onLoadingStarted() {
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = false;
+ }
+
+ @Override
+ public void onLoadingFinished(boolean loadingFailed) {
+ clearLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = loadingFailed;
+ showSyncErrorIfNecessary(loadingFailed);
+ }
+ }
+
+ private class MyDetailsSource implements DetailsHelper.DetailsSource {
+ private int mIndex;
+
+ @Override
+ public int size() {
+ return mAlbumDataAdapter.size();
+ }
+
+ @Override
+ public int setIndex() {
+ Path id = mSelectionManager.getSelected(false).get(0);
+ mIndex = mAlbumDataAdapter.findItem(id);
+ return mIndex;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ // this relies on setIndex() being called beforehand
+ MediaObject item = mAlbumDataAdapter.get(mIndex);
+ if (item != null) {
+ mAlbumView.setHighlightItemPath(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void onAlbumModeSelected(int mode) {
+ if (mode == GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED) {
+ switchToFilmstrip();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/TimeLineTitleMediaItem.java b/src/com/android/gallery3d/data/TimeLineTitleMediaItem.java
new file mode 100644
index 000000000..1b0384548
--- /dev/null
+++ b/src/com/android/gallery3d/data/TimeLineTitleMediaItem.java
@@ -0,0 +1,385 @@
+/*
+ * 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.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.drm.DrmHelper;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+// LocalImage represents an image in the local storage.
+public class LocalImage extends LocalMediaItem {
+ private static final String TAG = "LocalImage";
+
+ static final Path ITEM_PATH = Path.fromString("/local/image/item");
+
+ // Must preserve order between these indices and the order of the terms in
+ // the following PROJECTION array.
+ private static final int INDEX_ID = 0;
+ private static final int INDEX_CAPTION = 1;
+ private static final int INDEX_MIME_TYPE = 2;
+ private static final int INDEX_LATITUDE = 3;
+ private static final int INDEX_LONGITUDE = 4;
+ private static final int INDEX_DATE_TAKEN = 5;
+ private static final int INDEX_DATE_ADDED = 6;
+ private static final int INDEX_DATE_MODIFIED = 7;
+ private static final int INDEX_DATA = 8;
+ private static final int INDEX_ORIENTATION = 9;
+ private static final int INDEX_BUCKET_ID = 10;
+ private static final int INDEX_SIZE = 11;
+ private static final int INDEX_WIDTH = 12;
+ private static final int INDEX_HEIGHT = 13;
+
+ static final String[] PROJECTION = {
+ ImageColumns._ID, // 0
+ ImageColumns.TITLE, // 1
+ ImageColumns.MIME_TYPE, // 2
+ ImageColumns.LATITUDE, // 3
+ ImageColumns.LONGITUDE, // 4
+ ImageColumns.DATE_TAKEN, // 5
+ ImageColumns.DATE_ADDED, // 6
+ ImageColumns.DATE_MODIFIED, // 7
+ ImageColumns.DATA, // 8
+ ImageColumns.ORIENTATION, // 9
+ ImageColumns.BUCKET_ID, // 10
+ ImageColumns.SIZE, // 11
+ "0", // 12
+ "0" // 13
+ };
+
+ static {
+ updateWidthAndHeightProjection();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private static void updateWidthAndHeightProjection() {
+ if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
+ PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH;
+ PROJECTION[INDEX_HEIGHT] = MediaColumns.HEIGHT;
+ }
+ }
+
+ private final GalleryApp mApplication;
+
+ public int rotation;
+
+ private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
+
+ public LocalImage(Path path, GalleryApp application, Cursor cursor) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ loadFromCursor(cursor);
+ }
+
+ public LocalImage(Path path, GalleryApp application, int id) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ ContentResolver resolver = mApplication.getContentResolver();
+ Uri uri = Images.Media.EXTERNAL_CONTENT_URI;
+ Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id);
+ if (cursor == null) {
+ throw new RuntimeException("cannot get cursor for: " + path);
+ }
+ try {
+ if (cursor.moveToNext()) {
+ loadFromCursor(cursor);
+ } else {
+ throw new RuntimeException("cannot find data for: " + path);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void loadFromCursor(Cursor cursor) {
+ id = cursor.getInt(INDEX_ID);
+ caption = cursor.getString(INDEX_CAPTION);
+ mimeType = cursor.getString(INDEX_MIME_TYPE);
+ latitude = cursor.getDouble(INDEX_LATITUDE);
+ longitude = cursor.getDouble(INDEX_LONGITUDE);
+ dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN);
+ dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+ dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+ filePath = cursor.getString(INDEX_DATA);
+ rotation = cursor.getInt(INDEX_ORIENTATION);
+ bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ fileSize = cursor.getLong(INDEX_SIZE);
+ width = cursor.getInt(INDEX_WIDTH);
+ height = cursor.getInt(INDEX_HEIGHT);
+ }
+
+ @Override
+ protected boolean updateFromCursor(Cursor cursor) {
+ UpdateHelper uh = new UpdateHelper();
+ id = uh.update(id, cursor.getInt(INDEX_ID));
+ caption = uh.update(caption, cursor.getString(INDEX_CAPTION));
+ mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE));
+ latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE));
+ longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE));
+ dateTakenInMs = uh.update(
+ dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN));
+ dateAddedInSec = uh.update(
+ dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED));
+ dateModifiedInSec = uh.update(
+ dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED));
+ filePath = uh.update(filePath, cursor.getString(INDEX_DATA));
+ rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION));
+ bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID));
+ fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE));
+ width = uh.update(width, cursor.getInt(INDEX_WIDTH));
+ height = uh.update(height, cursor.getInt(INDEX_HEIGHT));
+ return uh.isUpdated();
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new LocalImageRequest(mApplication, mPath, dateModifiedInSec,
+ type, filePath, mimeType);
+ }
+
+ public static class LocalImageRequest extends ImageCacheRequest {
+ private String mLocalFilePath;
+
+ LocalImageRequest(GalleryApp application, Path path, long timeModified,
+ int type, String localFilePath) {
+ super(application, path, timeModified, type,
+ MediaItem.getTargetSize(type));
+ mLocalFilePath = localFilePath;
+ }
+
+ LocalImageRequest(GalleryApp application, Path path, long timeModified,
+ int type, String localFilePath, String mimeType) {
+ super(application, path, timeModified, type,
+ MediaItem.getTargetSize(type),localFilePath, mimeType);
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public Bitmap onDecodeOriginal(JobContext jc, final int type) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ if (DrmHelper.isDrmFile(mLocalFilePath)) {
+ return DecodeUtils.ensureGLCompatibleBitmap(DrmHelper
+ .getBitmap(mLocalFilePath, options));
+ }
+
+ int targetSize = MediaItem.getTargetSize(type);
+
+ // try to decode from JPEG EXIF
+ if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+ ExifInterface exif = new ExifInterface();
+ byte[] thumbData = null;
+ try {
+ exif.readExif(mLocalFilePath);
+ thumbData = exif.getThumbnail();
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath);
+ } catch (IOException e) {
+ Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath);
+ }
+ if (thumbData != null) {
+ Bitmap bitmap = DecodeUtils.decodeIfBigEnough(
+ jc, thumbData, options, targetSize);
+ if (bitmap != null) return bitmap;
+ }
+ }
+
+ return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type);
+ }
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return new LocalLargeImageRequest(filePath);
+ }
+
+ public static class LocalLargeImageRequest
+ implements Job<BitmapRegionDecoder> {
+ String mLocalFilePath;
+
+ public LocalLargeImageRequest(String localFilePath) {
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ if (DrmHelper.isDrmFile(mLocalFilePath)) {
+ return DrmHelper.createBitmapRegionDecoder(mLocalFilePath,
+ false);
+ }
+
+ return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false);
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ int operation = SUPPORT_DELETE | SUPPORT_INFO;
+ if (DrmHelper.isDrmFile(getFilePath())) {
+ if (DrmHelper.isDrmFLBlocking(mApplication.getAndroidContext(),
+ getFilePath())) {
+ operation |= SUPPORT_SETAS;
+ }
+ operation |= SUPPORT_DRM_INFO | SUPPORT_FULL_IMAGE;
+ if (DrmHelper.isShareableDrmFile(getFilePath())) {
+ operation |= SUPPORT_SHARE;
+ }
+ } else {
+ operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP
+ | SUPPORT_SETAS | SUPPORT_PRINT | SUPPORT_INFO;
+ if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) {
+ operation |= SUPPORT_FULL_IMAGE | SUPPORT_EDIT;
+ }
+
+ if (BitmapUtils.isRotationSupported(mimeType)) {
+ operation |= SUPPORT_ROTATE;
+ }
+
+ if (GalleryUtils.isValidLocation(latitude, longitude)) {
+ operation |= SUPPORT_SHOW_ON_MAP;
+ }
+ }
+ return operation;
+ }
+
+ @Override
+ public void getPanoramaSupport(PanoramaSupportCallback callback) {
+ mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
+ }
+
+ @Override
+ public void clearCachedPanoramaSupport() {
+ mPanoramaMetadata.clearCachedValues();
+ }
+
+ @Override
+ public void delete() {
+ GalleryUtils.assertNotInRenderThread();
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ ContentResolver contentResolver = mApplication.getContentResolver();
+ SaveImage.deleteAuxFiles(contentResolver, getContentUri());
+ contentResolver.delete(baseUri, "_id=?",
+ new String[]{String.valueOf(id)});
+ }
+
+ @Override
+ public void rotate(int degrees) {
+ GalleryUtils.assertNotInRenderThread();
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ ContentValues values = new ContentValues();
+ int rotation = (this.rotation + degrees) % 360;
+ if (rotation < 0) rotation += 360;
+
+ if (mimeType.equalsIgnoreCase("image/jpeg")) {
+ ExifInterface exifInterface = new ExifInterface();
+ ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+ ExifInterface.getOrientationValueForRotation(rotation));
+ if(tag != null) {
+ exifInterface.setTag(tag);
+ try {
+ exifInterface.forceRewriteExif(filePath);
+ fileSize = new File(filePath).length();
+ values.put(Images.Media.SIZE, fileSize);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "cannot find file to set exif: " + filePath);
+ } catch (IOException e) {
+ Log.w(TAG, "cannot set exif data: " + filePath);
+ }
+ } else {
+ Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION);
+ }
+ }
+
+ values.put(Images.Media.ORIENTATION, rotation);
+ mApplication.getContentResolver().update(baseUri, values, "_id=?",
+ new String[]{String.valueOf(id)});
+ }
+
+ @Override
+ public Uri getContentUri() {
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
+ }
+
+ @Override
+ public int getMediaType() {
+ if (DrmHelper.isDrmFile(getFilePath())) {
+ return MEDIA_TYPE_DRM_IMAGE;
+ }
+
+ return MEDIA_TYPE_IMAGE;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaDetails details = super.getDetails();
+ details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation));
+ if (MIME_TYPE_JPEG.equals(mimeType)) {
+ // ExifInterface returns incorrect values for photos in other format.
+ // For example, the width and height of an webp images is always '0'.
+ MediaDetails.extractExifInfo(details, filePath);
+ }
+ return details;
+ }
+
+ @Override
+ public int getRotation() {
+ return rotation;
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TimeLineSlidingWindow.java b/src/com/android/gallery3d/ui/TimeLineSlidingWindow.java
new file mode 100644
index 000000000..fec7d1e92
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TimeLineSlidingWindow.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/TimeLineSlotRenderer.java b/src/com/android/gallery3d/ui/TimeLineSlotRenderer.java
new file mode 100644
index 000000000..5c1441ee9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TimeLineSlotRenderer.java
@@ -0,0 +1,205 @@
+/*
+ * 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);
+ } else if ((entry.mediaType == MediaObject.MEDIA_TYPE_DRM_VIDEO)
+ || (entry.mediaType == MediaObject.MEDIA_TYPE_DRM_IMAGE)) {
+ drawDrmOverlay(canvas, width, height, entry.mediaType);
+ }
+
+ 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);
+ mSlotView.invalidate();
+ }
+ }
+
+ 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/TimeLineSlotView.java b/src/com/android/gallery3d/ui/TimeLineSlotView.java
new file mode 100644
index 000000000..eaf23f553
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TimeLineSlotView.java
@@ -0,0 +1,835 @@
+/*
+ * 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.text.TextUtils;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+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;
+
+import java.util.Locale;
+
+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();
+
+ // Flag to check whether it is come from Photo Page.
+ private boolean isFromPhotoPage = false;
+
+ 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);
+ }
+
+ /**
+ * Set the flag which used for check whether it is come from Photo Page.
+ */
+ public void setIsFromPhotoPage(boolean flag) {
+ isFromPhotoPage = flag;
+ }
+
+ public void setScrollPosition(int position) {
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())
+ && position == 0 && !isFromPhotoPage) {
+ // If RTL and not from Photo Page, set position to max.
+ position = mLayout.getScrollLimit();
+ }
+ 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) {
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {
+ // If RTL, recalculate the columns and rows.
+ int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
+ col = count - index / mUnitCount - 1;
+ row = index % mUnitCount;
+ } else {
+ 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) {
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {
+ // If RTL, recalculate the position.
+ position = mContentLength > mWidth ? (mContentLength - position - mWidth)
+ : position;
+ position = Math.max(0, position);
+ }
+ 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);
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {
+ // If RTL, recalculate the absoluteX.
+ absoluteX = mContentLength > mWidth ? (mContentLength - absoluteX) : mWidth
+ - absoluteX;
+ }
+ 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;
+ }
+ }
+
+ /**
+ * Get the SlotView's max scroll value.
+ */
+ public int getScrollLimit() {
+ return mLayout.getScrollLimit();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TimeLineTitleMaker.java b/src/com/android/gallery3d/ui/TimeLineTitleMaker.java
new file mode 100644
index 000000000..3ac3bb7fe
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TimeLineTitleMaker.java
@@ -0,0 +1,239 @@
+/*
+ * 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 android.view.View;
+
+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;
+
+import java.util.Locale;
+
+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);
+
+ if (View.LAYOUT_DIRECTION_RTL == TextUtils
+ .getLayoutDirectionFromLocale(Locale.getDefault())) {// RTL
+ // draw title
+ if (jc.isCancelled()) return null;
+ int strLength = (int) mTitlePaint.measureText(title);
+ int x = labelWidth - (s.leftMargin + s.iconSize) - strLength;
+ // 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 = s.leftMargin + 10;// plus 10 to get a much bigger margin
+ 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(labelWidth - s.leftMargin - s.iconSize,
+ (s.labelBackgroundHeight -
+ Math.round(scale * icon.getHeight())) / 2f);
+ canvas.scale(scale, scale);
+ canvas.drawBitmap(icon, 0, 0, null);
+ }
+ } else { // LTR
+ // 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);
+ }
+}