diff options
| author | Ravi Banuri <ravibanuri@codeaurora.org> | 2015-10-19 16:03:03 +0530 |
|---|---|---|
| committer | Gerrit - the friendly Code Review server <code-review@localhost> | 2015-11-08 22:55:05 -0800 |
| commit | 7a41c376e299d9944a956004c5176b31a5ef0a4e (patch) | |
| tree | d11d9e09ca81fed337fdf5d45ac658b80ef1ee43 /src | |
| parent | ec09287bd140f2ec076b62f99f2e5b6ddf464215 (diff) | |
| download | android_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.java | 402 | ||||
| -rw-r--r-- | src/com/android/gallery3d/app/TimeLinePage.java | 853 | ||||
| -rw-r--r-- | src/com/android/gallery3d/data/TimeLineTitleMediaItem.java | 385 | ||||
| -rw-r--r-- | src/com/android/gallery3d/ui/TimeLineSlidingWindow.java | 365 | ||||
| -rw-r--r-- | src/com/android/gallery3d/ui/TimeLineSlotRenderer.java | 205 | ||||
| -rw-r--r-- | src/com/android/gallery3d/ui/TimeLineSlotView.java | 835 | ||||
| -rw-r--r-- | src/com/android/gallery3d/ui/TimeLineTitleMaker.java | 239 |
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); + } +} |
