summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/ui')
-rw-r--r--src/com/android/gallery3d/ui/AbstractDisplayItem.java114
-rw-r--r--src/com/android/gallery3d/ui/ActionModeHandler.java246
-rw-r--r--src/com/android/gallery3d/ui/AdaptiveBackground.java128
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java543
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetView.java240
-rw-r--r--src/com/android/gallery3d/ui/AlbumSlidingWindow.java433
-rw-r--r--src/com/android/gallery3d/ui/AlbumView.java197
-rw-r--r--src/com/android/gallery3d/ui/BasicTexture.java164
-rw-r--r--src/com/android/gallery3d/ui/BitmapTexture.java49
-rw-r--r--src/com/android/gallery3d/ui/BitmapTileProvider.java91
-rw-r--r--src/com/android/gallery3d/ui/BoxBlurFilter.java100
-rw-r--r--src/com/android/gallery3d/ui/CacheBarView.java270
-rw-r--r--src/com/android/gallery3d/ui/CanvasTexture.java52
-rw-r--r--src/com/android/gallery3d/ui/ColorTexture.java58
-rw-r--r--src/com/android/gallery3d/ui/Config.java31
-rw-r--r--src/com/android/gallery3d/ui/CropView.java801
-rw-r--r--src/com/android/gallery3d/ui/CustomMenu.java126
-rw-r--r--src/com/android/gallery3d/ui/DetailsWindow.java451
-rw-r--r--src/com/android/gallery3d/ui/DisplayItem.java45
-rw-r--r--src/com/android/gallery3d/ui/DownUpDetector.java61
-rw-r--r--src/com/android/gallery3d/ui/DrawableTexture.java38
-rw-r--r--src/com/android/gallery3d/ui/FilmStripView.java261
-rw-r--r--src/com/android/gallery3d/ui/GLCanvas.java138
-rw-r--r--src/com/android/gallery3d/ui/GLCanvasImpl.java913
-rw-r--r--src/com/android/gallery3d/ui/GLPaint.java65
-rw-r--r--src/com/android/gallery3d/ui/GLRoot.java37
-rw-r--r--src/com/android/gallery3d/ui/GLRootView.java414
-rw-r--r--src/com/android/gallery3d/ui/GLView.java431
-rw-r--r--src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java126
-rw-r--r--src/com/android/gallery3d/ui/GridDrawer.java109
-rw-r--r--src/com/android/gallery3d/ui/HighlightDrawer.java73
-rw-r--r--src/com/android/gallery3d/ui/Icon.java59
-rw-r--r--src/com/android/gallery3d/ui/IconDrawer.java112
-rw-r--r--src/com/android/gallery3d/ui/ImportCompleteListener.java57
-rw-r--r--src/com/android/gallery3d/ui/Label.java84
-rw-r--r--src/com/android/gallery3d/ui/Log.java53
-rw-r--r--src/com/android/gallery3d/ui/ManageCacheDrawer.java126
-rw-r--r--src/com/android/gallery3d/ui/MeasureHelper.java65
-rw-r--r--src/com/android/gallery3d/ui/MenuExecutor.java398
-rw-r--r--src/com/android/gallery3d/ui/MultiLineTexture.java50
-rw-r--r--src/com/android/gallery3d/ui/NinePatchChunk.java82
-rw-r--r--src/com/android/gallery3d/ui/NinePatchTexture.java401
-rw-r--r--src/com/android/gallery3d/ui/OnSelectedListener.java21
-rw-r--r--src/com/android/gallery3d/ui/Paper.java112
-rw-r--r--src/com/android/gallery3d/ui/PhotoView.java1191
-rw-r--r--src/com/android/gallery3d/ui/PositionProvider.java23
-rw-r--r--src/com/android/gallery3d/ui/PositionRepository.java139
-rw-r--r--src/com/android/gallery3d/ui/ProgressBar.java65
-rw-r--r--src/com/android/gallery3d/ui/ProgressSpinner.java78
-rw-r--r--src/com/android/gallery3d/ui/RawTexture.java54
-rw-r--r--src/com/android/gallery3d/ui/ResourceTexture.java52
-rw-r--r--src/com/android/gallery3d/ui/ScrollBarView.java135
-rw-r--r--src/com/android/gallery3d/ui/ScrollView.java99
-rw-r--r--src/com/android/gallery3d/ui/ScrollerHelper.java93
-rw-r--r--src/com/android/gallery3d/ui/SelectionDrawer.java89
-rw-r--r--src/com/android/gallery3d/ui/SelectionManager.java221
-rw-r--r--src/com/android/gallery3d/ui/SlideshowView.java165
-rw-r--r--src/com/android/gallery3d/ui/SlotView.java607
-rw-r--r--src/com/android/gallery3d/ui/StaticBackground.java62
-rw-r--r--src/com/android/gallery3d/ui/StringTexture.java92
-rw-r--r--src/com/android/gallery3d/ui/StripDrawer.java57
-rw-r--r--src/com/android/gallery3d/ui/SynchronizedHandler.java41
-rw-r--r--src/com/android/gallery3d/ui/TextButton.java91
-rw-r--r--src/com/android/gallery3d/ui/Texture.java44
-rw-r--r--src/com/android/gallery3d/ui/TileImageView.java693
-rw-r--r--src/com/android/gallery3d/ui/TileImageViewAdapter.java144
-rw-r--r--src/com/android/gallery3d/ui/UploadedTexture.java285
-rw-r--r--src/com/android/gallery3d/ui/UserInteractionListener.java26
68 files changed, 12971 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/ui/AbstractDisplayItem.java b/src/com/android/gallery3d/ui/AbstractDisplayItem.java
new file mode 100644
index 000000000..aad3919b5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AbstractDisplayItem.java
@@ -0,0 +1,114 @@
+/*
+ * 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.data.MediaItem;
+
+import android.graphics.Bitmap;
+
+public abstract class AbstractDisplayItem extends DisplayItem {
+
+ private static final String TAG = "AbstractDisplayItem";
+
+ private static final int STATE_INVALID = 0x01;
+ private static final int STATE_VALID = 0x02;
+ private static final int STATE_UPDATING = 0x04;
+ private static final int STATE_CANCELING = 0x08;
+ private static final int STATE_ERROR = 0x10;
+
+ private int mState = STATE_INVALID;
+ private boolean mImageRequested = false;
+ private boolean mRecycling = false;
+ private Bitmap mBitmap;
+
+ protected final MediaItem mMediaItem;
+ private int mRotation;
+
+ public AbstractDisplayItem(MediaItem item) {
+ mMediaItem = item;
+ if (item == null) mState = STATE_ERROR;
+ if (item != null) mRotation = mMediaItem.getRotation();
+ }
+
+ protected void updateImage(Bitmap bitmap, boolean isCancelled) {
+ if (mRecycling) {
+ return;
+ }
+
+ if (isCancelled && bitmap == null) {
+ mState = STATE_INVALID;
+ if (mImageRequested) {
+ // request image again.
+ requestImage();
+ }
+ return;
+ }
+
+ mBitmap = bitmap;
+ mState = bitmap == null ? STATE_ERROR : STATE_VALID ;
+ onBitmapAvailable(mBitmap);
+ }
+
+ @Override
+ public int getRotation() {
+ return mRotation;
+ }
+
+ @Override
+ public long getIdentity() {
+ return mMediaItem != null
+ ? System.identityHashCode(mMediaItem.getPath())
+ : System.identityHashCode(this);
+ }
+
+ public void requestImage() {
+ mImageRequested = true;
+ if (mState == STATE_INVALID) {
+ mState = STATE_UPDATING;
+ startLoadBitmap();
+ }
+ }
+
+ public void cancelImageRequest() {
+ mImageRequested = false;
+ if (mState == STATE_UPDATING) {
+ mState = STATE_CANCELING;
+ cancelLoadBitmap();
+ }
+ }
+
+ private boolean inState(int states) {
+ return (mState & states) != 0;
+ }
+
+ public void recycle() {
+ if (!inState(STATE_UPDATING | STATE_CANCELING)) {
+ if (mBitmap != null) mBitmap = null;
+ } else {
+ mRecycling = true;
+ cancelImageRequest();
+ }
+ }
+
+ public boolean isRequestInProgress() {
+ return mImageRequested && inState(STATE_UPDATING | STATE_CANCELING);
+ }
+
+ abstract protected void startLoadBitmap();
+ abstract protected void cancelLoadBitmap();
+ abstract protected void onBitmapAvailable(Bitmap bitmap);
+}
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
new file mode 100644
index 000000000..6c81a3f6a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActionBar;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.CustomMenu.DropDownMenu;
+import com.android.gallery3d.ui.MenuExecutor.ProgressListener;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.ShareActionProvider;
+
+import java.util.ArrayList;
+
+public class ActionModeHandler implements ActionMode.Callback {
+ private static final String TAG = "ActionModeHandler";
+ private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE
+ | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE
+ | MediaObject.SUPPORT_CACHE | MediaObject.SUPPORT_IMPORT;
+
+ public interface ActionModeListener {
+ public boolean onActionItemClicked(MenuItem item);
+ }
+
+ private final GalleryActivity mActivity;
+ private final MenuExecutor mMenuExecutor;
+ private final SelectionManager mSelectionManager;
+ private Menu mMenu;
+ private DropDownMenu mSelectionMenu;
+ private ActionModeListener mListener;
+ private Future<?> mMenuTask;
+ private Handler mMainHandler;
+ private ShareActionProvider mShareActionProvider;
+
+ public ActionModeHandler(
+ GalleryActivity activity, SelectionManager selectionManager) {
+ mActivity = Utils.checkNotNull(activity);
+ mSelectionManager = Utils.checkNotNull(selectionManager);
+ mMenuExecutor = new MenuExecutor(activity, selectionManager);
+ mMainHandler = new Handler(activity.getMainLooper());
+ }
+
+ public ActionMode startActionMode() {
+ Activity a = (Activity) mActivity;
+ final ActionMode actionMode = a.startActionMode(this);
+ CustomMenu customMenu = new CustomMenu(a);
+ View customView = LayoutInflater.from(a).inflate(
+ R.layout.action_mode, null);
+ actionMode.setCustomView(customView);
+ mSelectionMenu = customMenu.addDropDownMenu(
+ (Button) customView.findViewById(R.id.selection_menu),
+ R.menu.selection);
+ customMenu.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ return onActionItemClicked(actionMode, item);
+ }
+ });
+ return actionMode;
+ }
+
+ public void setTitle(String title) {
+ mSelectionMenu.setTitle(title);
+ }
+
+ public void setActionModeListener(ActionModeListener listener) {
+ mListener = listener;
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ boolean result;
+ if (mListener != null) {
+ result = mListener.onActionItemClicked(item);
+ if (result) {
+ mSelectionManager.leaveSelectionMode();
+ return result;
+ }
+ }
+ ProgressListener listener = null;
+ if (item.getItemId() == R.id.action_import) {
+ listener = new ImportCompleteListener(mActivity);
+ }
+ result = mMenuExecutor.onMenuClicked(item, listener);
+ if (item.getItemId() == R.id.action_select_all) {
+ updateSupportedOperation();
+
+ // For clients who call SelectionManager.selectAll() directly, we need to ensure the
+ // menu status is consistent with selection manager.
+ item = mSelectionMenu.findItem(R.id.action_select_all);
+ if (item != null) {
+ if (mSelectionManager.inSelectAllMode()) {
+ item.setChecked(true);
+ item.setTitle(R.string.deselect_all);
+ } else {
+ item.setChecked(false);
+ item.setTitle(R.string.select_all);
+ }
+ }
+ }
+ return result;
+ }
+
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.operation, menu);
+
+ mShareActionProvider = GalleryActionBar.initializeShareActionProvider(menu);
+
+ mMenu = menu;
+ return true;
+ }
+
+ public void onDestroyActionMode(ActionMode mode) {
+ mSelectionManager.leaveSelectionMode();
+ }
+
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return true;
+ }
+
+ private void updateMenuOptionsAndSharingIntent(JobContext jc) {
+ ArrayList<Path> paths = mSelectionManager.getSelected(true);
+ if (paths.size() == 0) return;
+
+ int operation = MediaObject.SUPPORT_ALL;
+ DataManager manager = mActivity.getDataManager();
+ final ArrayList<Uri> uris = new ArrayList<Uri>();
+ int type = 0;
+ for (Path path : paths) {
+ if (jc.isCancelled()) return;
+ int support = manager.getSupportedOperations(path);
+ type |= manager.getMediaType(path);
+ operation &= support;
+ if ((support & MediaObject.SUPPORT_SHARE) != 0) {
+ uris.add(manager.getContentUri(path));
+ }
+ }
+ final Intent intent = new Intent();
+ final String mimeType = MenuExecutor.getMimeType(type);
+
+ if (paths.size() == 1) {
+ if (!GalleryUtils.isEditorAvailable((Context) mActivity, mimeType)) {
+ operation &= ~MediaObject.SUPPORT_EDIT;
+ }
+ } else {
+ operation &= SUPPORT_MULTIPLE_MASK;
+ }
+
+
+ Log.v(TAG, "Sharing intent MIME type=" + mimeType + ", uri size = "+ uris.size());
+ if (uris.size() > 1) {
+ intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ } else {
+ intent.setAction(Intent.ACTION_SEND).setType(mimeType);
+ intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ }
+ intent.setType(mimeType);
+
+ final int supportedOperation = operation;
+
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mMenuTask = null;
+ MenuExecutor.updateMenuOperation(mMenu, supportedOperation);
+
+ if (mShareActionProvider != null) {
+ Log.v(TAG, "Sharing intent is ready: action = " + intent.getAction());
+ mShareActionProvider.setShareIntent(intent);
+ }
+ }
+ });
+ }
+
+ public void updateSupportedOperation(Path path, boolean selected) {
+ // TODO: We need to improve the performance
+ updateSupportedOperation();
+ }
+
+ public void updateSupportedOperation() {
+ if (mMenuTask != null) {
+ mMenuTask.cancel();
+ }
+
+ // Disable share action until share intent is in good shape
+ if (mShareActionProvider != null) {
+ Log.v(TAG, "Disable sharing until intent is ready");
+ mShareActionProvider.setShareIntent(null);
+ }
+
+ // Generate sharing intent and update supported operations in the background
+ mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
+ public Void run(JobContext jc) {
+ updateMenuOptionsAndSharingIntent(jc);
+ return null;
+ }
+ });
+ }
+
+ public void pause() {
+ if (mMenuTask != null) {
+ mMenuTask.cancel();
+ mMenuTask = null;
+ }
+ mMenuExecutor.pause();
+ }
+
+ public void resume() {
+ updateSupportedOperation();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AdaptiveBackground.java b/src/com/android/gallery3d/ui/AdaptiveBackground.java
new file mode 100644
index 000000000..42cb2ccdb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AdaptiveBackground.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.LightingColorFilter;
+import android.graphics.Paint;
+
+import com.android.gallery3d.anim.FloatAnimation;
+
+public class AdaptiveBackground extends GLView {
+
+ private static final int BACKGROUND_WIDTH = 128;
+ private static final int BACKGROUND_HEIGHT = 64;
+ private static final int FILTERED_COLOR = 0xffaaaaaa;
+ private static final int ANIMATION_DURATION = 500;
+
+ private BasicTexture mOldBackground;
+ private BasicTexture mBackground;
+
+ private final Paint mPaint;
+ private Bitmap mPendingBitmap;
+ private final FloatAnimation mAnimation =
+ new FloatAnimation(0, 1, ANIMATION_DURATION);
+
+ public AdaptiveBackground() {
+ Paint paint = new Paint();
+ paint.setFilterBitmap(true);
+ paint.setColorFilter(new LightingColorFilter(FILTERED_COLOR, 0));
+ mPaint = paint;
+ }
+
+ public Bitmap getAdaptiveBitmap(Bitmap bitmap) {
+ Bitmap target = Bitmap.createBitmap(
+ BACKGROUND_WIDTH, BACKGROUND_HEIGHT, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(target);
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int left = 0;
+ int top = 0;
+ if (width * BACKGROUND_HEIGHT > height * BACKGROUND_WIDTH) {
+ float scale = (float) BACKGROUND_HEIGHT / height;
+ canvas.scale(scale, scale);
+ left = (BACKGROUND_WIDTH - (int) (width * scale + 0.5)) / 2;
+ } else {
+ float scale = (float) BACKGROUND_WIDTH / width;
+ canvas.scale(scale, scale);
+ top = (BACKGROUND_HEIGHT - (int) (height * scale + 0.5)) / 2;
+ }
+ canvas.drawBitmap(bitmap, left, top, mPaint);
+ BoxBlurFilter.apply(target,
+ BoxBlurFilter.MODE_REPEAT, BoxBlurFilter.MODE_CLAMP);
+ return target;
+ }
+
+ private void startTransition(Bitmap bitmap) {
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ if (mBackground == null) {
+ mBackground = texture;
+ } else {
+ if (mOldBackground != null) mOldBackground.recycle();
+ mOldBackground = mBackground;
+ mBackground = texture;
+ mAnimation.start();
+ }
+ invalidate();
+ }
+
+ public void setImage(Bitmap bitmap) {
+ if (mAnimation.isActive()) {
+ mPendingBitmap = bitmap;
+ } else {
+ startTransition(bitmap);
+ }
+ }
+
+ public void setScrollPosition(int position) {
+ if (mScrollX == position) return;
+ mScrollX = position;
+ invalidate();
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ if (mBackground == null) return;
+
+ int height = getHeight();
+ float scale = (float) height / BACKGROUND_HEIGHT;
+ int width = (int) (BACKGROUND_WIDTH * scale + 0.5f);
+ int scroll = mScrollX;
+ int start = (scroll / width) * width;
+
+ if (mOldBackground == null) {
+ for (int i = start, n = scroll + getWidth(); i < n; i += width) {
+ mBackground.draw(canvas, i - scroll, 0, width, height);
+ }
+ } else {
+ boolean moreAnimation =
+ mAnimation.calculate(canvas.currentAnimationTimeMillis());
+ float ratio = mAnimation.get();
+ for (int i = start, n = scroll + getWidth(); i < n; i += width) {
+ canvas.drawMixed(mOldBackground,
+ mBackground, ratio, i - scroll, 0, width, height);
+ }
+ if (moreAnimation) {
+ invalidate();
+ } else if (mPendingBitmap != null) {
+ startTransition(mPendingBitmap);
+ mPendingBitmap = null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
new file mode 100644
index 000000000..92d8b4156
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.AlbumSetView.AlbumSetItem;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.MediaSetUtils;
+import com.android.gallery3d.util.ThreadPool;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Message;
+
+public class AlbumSetSlidingWindow implements AlbumSetView.ModelListener {
+ private static final String TAG = "GallerySlidingWindow";
+ private static final int MSG_LOAD_BITMAP_DONE = 0;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentInvalidated();
+ public void onWindowContentChanged(
+ int slot, AlbumSetItem old, AlbumSetItem update);
+ }
+
+ private final AlbumSetView.Model mSource;
+ private int mSize;
+ private int mLabelWidth;
+ private int mDisplayItemSize;
+ private int mLabelFontSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+
+ private final MyAlbumSetItem mData[];
+ private SelectionDrawer mSelectionDrawer;
+ private final ColorTexture mWaitLoadingTexture;
+
+ private SynchronizedHandler mHandler;
+ private ThreadPool mThreadPool;
+
+ private int mActiveRequestCount = 0;
+ private String mLoadingLabel;
+ private boolean mIsActive = false;
+
+ private static class MyAlbumSetItem extends AlbumSetItem {
+ public Path setPath;
+ public int sourceType;
+ public int cacheFlag;
+ public int cacheStatus;
+ }
+
+ public AlbumSetSlidingWindow(GalleryActivity activity, int labelWidth,
+ int displayItemSize, int labelFontSize, SelectionDrawer drawer,
+ AlbumSetView.Model source, int cacheSize) {
+ source.setModelListener(this);
+ mLabelWidth = labelWidth;
+ mDisplayItemSize = displayItemSize;
+ mLabelFontSize = labelFontSize;
+ mLoadingLabel = activity.getAndroidContext().getString(R.string.loading);
+ mSource = source;
+ mSelectionDrawer = drawer;
+ mData = new MyAlbumSetItem[cacheSize];
+ mSize = source.size();
+
+ mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT);
+ mWaitLoadingTexture.setSize(1, 1);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_LOAD_BITMAP_DONE);
+ ((GalleryDisplayItem) message.obj).onLoadBitmapDone();
+ }
+ };
+
+ mThreadPool = activity.getThreadPool();
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public AlbumSetItem get(int slotIndex) {
+ Utils.assertTrue(isActiveSlot(slotIndex),
+ "invalid slot: %s outsides (%s, %s)",
+ slotIndex, mActiveStart, mActiveEnd);
+ return mData[slotIndex % mData.length];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean isActiveSlot(int slotIndex) {
+ return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+ if (contentStart >= mContentEnd || mContentStart >= contentEnd) {
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ } else {
+ for (int i = mContentStart; i < contentStart; ++i) {
+ freeSlotContent(i);
+ }
+ for (int i = contentEnd, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mSource.setActiveWindow(contentStart, contentEnd);
+ for (int i = contentStart, n = mContentStart; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ for (int i = mContentEnd; i < contentEnd; ++i) {
+ prepareSlotContent(i);
+ }
+ }
+
+ mContentStart = contentStart;
+ mContentEnd = contentEnd;
+ }
+
+ public void setActiveWindow(int start, int end) {
+ Utils.assertTrue(
+ start <= end && end - start <= mData.length && end <= mSize,
+ "start = %s, end = %s, length = %s, size = %s",
+ start, end, mData.length, mSize);
+
+ AlbumSetItem data[] = mData;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+ 0, Math.max(0, mSize - data.length));
+ int contentEnd = Math.min(contentStart + data.length, mSize);
+ setContentWindow(contentStart, contentEnd);
+ if (mIsActive) updateAllImageRequests();
+ }
+
+ // We would like to request non active slots in the following order:
+ // Order: 8 6 4 2 1 3 5 7
+ // |---------|---------------|---------|
+ // |<- active ->|
+ // |<-------- cached range ----------->|
+ private void requestNonactiveImages() {
+ int range = Math.max(
+ mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+ for (int i = 0 ;i < range; ++i) {
+ requestImagesInSlot(mActiveEnd + i);
+ requestImagesInSlot(mActiveStart - 1 - i);
+ }
+ }
+
+ private void cancelNonactiveImages() {
+ int range = Math.max(
+ mContentEnd - mActiveEnd, mActiveStart - mContentStart);
+ for (int i = 0 ;i < range; ++i) {
+ cancelImagesInSlot(mActiveEnd + i);
+ cancelImagesInSlot(mActiveStart - 1 - i);
+ }
+ }
+
+ private void requestImagesInSlot(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumSetItem items = mData[slotIndex % mData.length];
+ for (DisplayItem item : items.covers) {
+ ((GalleryDisplayItem) item).requestImage();
+ }
+ }
+
+ private void cancelImagesInSlot(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumSetItem items = mData[slotIndex % mData.length];
+ for (DisplayItem item : items.covers) {
+ ((GalleryDisplayItem) item).cancelImageRequest();
+ }
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumSetItem data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumSetItem original = data[index];
+ if (original != null) {
+ data[index] = null;
+ for (DisplayItem item : original.covers) {
+ ((GalleryDisplayItem) item).recycle();
+ }
+ }
+ }
+
+ private long getMediaSetDataVersion(MediaSet set) {
+ return set == null
+ ? MediaSet.INVALID_DATA_VERSION
+ : set.getDataVersion();
+ }
+
+ private void prepareSlotContent(int slotIndex) {
+ MediaSet set = mSource.getMediaSet(slotIndex);
+
+ MyAlbumSetItem item = new MyAlbumSetItem();
+ MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
+ item.covers = new GalleryDisplayItem[coverItems.length];
+ item.sourceType = identifySourceType(set);
+ item.cacheFlag = identifyCacheFlag(set);
+ item.cacheStatus = identifyCacheStatus(set);
+ item.setPath = set == null ? null : set.getPath();
+
+ for (int i = 0; i < coverItems.length; ++i) {
+ item.covers[i] = new GalleryDisplayItem(slotIndex, i, coverItems[i]);
+ }
+ item.labelItem = new LabelDisplayItem(slotIndex);
+ item.setDataVersion = getMediaSetDataVersion(set);
+ mData[slotIndex % mData.length] = item;
+ }
+
+ private boolean isCoverItemsChanged(int slotIndex) {
+ AlbumSetItem original = mData[slotIndex % mData.length];
+ if (original == null) return true;
+ MediaItem[] coverItems = mSource.getCoverItems(slotIndex);
+
+ if (original.covers.length != coverItems.length) return true;
+ for (int i = 0, n = coverItems.length; i < n; ++i) {
+ GalleryDisplayItem g = (GalleryDisplayItem) original.covers[i];
+ if (g.mDataVersion != coverItems[i].getDataVersion()) return true;
+ }
+ return false;
+ }
+
+ private void updateSlotContent(final int slotIndex) {
+
+ MyAlbumSetItem data[] = mData;
+ int pos = slotIndex % data.length;
+ MyAlbumSetItem original = data[pos];
+
+ if (!isCoverItemsChanged(slotIndex)) {
+ MediaSet set = mSource.getMediaSet(slotIndex);
+ original.sourceType = identifySourceType(set);
+ original.cacheFlag = identifyCacheFlag(set);
+ original.cacheStatus = identifyCacheStatus(set);
+ original.setPath = set == null ? null : set.getPath();
+ ((LabelDisplayItem) original.labelItem).updateContent();
+ if (mListener != null) mListener.onContentInvalidated();
+ return;
+ }
+
+ prepareSlotContent(slotIndex);
+ AlbumSetItem update = data[pos];
+
+ if (mListener != null && isActiveSlot(slotIndex)) {
+ mListener.onWindowContentChanged(slotIndex, original, update);
+ }
+ if (original != null) {
+ for (DisplayItem item : original.covers) {
+ ((GalleryDisplayItem) item).recycle();
+ }
+ }
+ }
+
+ private void notifySlotChanged(int slotIndex) {
+ // If the updated content is not cached, ignore it
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) {
+ Log.w(TAG, String.format(
+ "invalid update: %s is outside (%s, %s)",
+ slotIndex, mContentStart, mContentEnd) );
+ return;
+ }
+ updateSlotContent(slotIndex);
+ boolean isActiveSlot = isActiveSlot(slotIndex);
+ if (mActiveRequestCount == 0 || isActiveSlot) {
+ for (DisplayItem item : mData[slotIndex % mData.length].covers) {
+ GalleryDisplayItem galleryItem = (GalleryDisplayItem) item;
+ galleryItem.requestImage();
+ if (isActiveSlot && galleryItem.isRequestInProgress()) {
+ ++mActiveRequestCount;
+ }
+ }
+ }
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ for (DisplayItem item : mData[i % mData.length].covers) {
+ GalleryDisplayItem coverItem = (GalleryDisplayItem) item;
+ coverItem.requestImage();
+ if (coverItem.isRequestInProgress()) ++mActiveRequestCount;
+ }
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ private class GalleryDisplayItem extends AbstractDisplayItem
+ implements FutureListener<Bitmap> {
+ private Future<Bitmap> mFuture;
+ private final int mSlotIndex;
+ private final int mCoverIndex;
+ private final int mMediaType;
+ private Texture mContent;
+ private final long mDataVersion;
+
+ public GalleryDisplayItem(int slotIndex, int coverIndex, MediaItem item) {
+ super(item);
+ mSlotIndex = slotIndex;
+ mCoverIndex = coverIndex;
+ mMediaType = item.getMediaType();
+ mDataVersion = item.getDataVersion();
+ updateContent(mWaitLoadingTexture);
+ }
+
+ @Override
+ protected void onBitmapAvailable(Bitmap bitmap) {
+ if (isActiveSlot(mSlotIndex)) {
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ }
+ if (bitmap != null) {
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ texture.setThrottled(true);
+ updateContent(texture);
+ if (mListener != null) mListener.onContentInvalidated();
+ }
+ }
+
+ private void updateContent(Texture content) {
+ mContent = content;
+
+ int width = content.getWidth();
+ int height = content.getHeight();
+
+ float scale = (float) mDisplayItemSize / Math.max(width, height);
+
+ width = (int) Math.floor(width * scale);
+ height = (int) Math.floor(height * scale);
+
+ setSize(width, height);
+ }
+
+ @Override
+ public boolean render(GLCanvas canvas, int pass) {
+ int sourceType = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
+ int cacheFlag = MediaSet.CACHE_FLAG_NO;
+ int cacheStatus = MediaSet.CACHE_STATUS_NOT_CACHED;
+ MyAlbumSetItem set = mData[mSlotIndex % mData.length];
+ Path path = set.setPath;
+ if (mCoverIndex == 0) {
+ sourceType = set.sourceType;
+ cacheFlag = set.cacheFlag;
+ cacheStatus = set.cacheStatus;
+ }
+
+ mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight,
+ getRotation(), path, mCoverIndex, sourceType, mMediaType,
+ cacheFlag == MediaSet.CACHE_FLAG_FULL,
+ (cacheFlag == MediaSet.CACHE_FLAG_FULL)
+ && (cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL));
+ return false;
+ }
+
+ @Override
+ public void startLoadBitmap() {
+ mFuture = mThreadPool.submit(mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL), this);
+ }
+
+ @Override
+ public void cancelLoadBitmap() {
+ mFuture.cancel();
+ }
+
+ @Override
+ public void onFutureDone(Future<Bitmap> future) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
+ }
+
+ private void onLoadBitmapDone() {
+ Future<Bitmap> future = mFuture;
+ mFuture = null;
+ updateImage(future.get(), future.isCancelled());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("GalleryDisplayItem(%s, %s)", mSlotIndex, mCoverIndex);
+ }
+ }
+
+ private static int identifySourceType(MediaSet set) {
+ if (set == null) {
+ return SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
+ }
+
+ Path path = set.getPath();
+ if (MediaSetUtils.isCameraSource(path)) {
+ return SelectionDrawer.DATASOURCE_TYPE_CAMERA;
+ }
+
+ int type = SelectionDrawer.DATASOURCE_TYPE_NOT_CATEGORIZED;
+ String prefix = path.getPrefix();
+
+ if (prefix.equals("picasa")) {
+ type = SelectionDrawer.DATASOURCE_TYPE_PICASA;
+ } else if (prefix.equals("local") || prefix.equals("merge")) {
+ type = SelectionDrawer.DATASOURCE_TYPE_LOCAL;
+ } else if (prefix.equals("mtp")) {
+ type = SelectionDrawer.DATASOURCE_TYPE_MTP;
+ }
+
+ return type;
+ }
+
+ private static int identifyCacheFlag(MediaSet set) {
+ if (set == null || (set.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ return MediaSet.CACHE_FLAG_NO;
+ }
+
+ return set.getCacheFlag();
+ }
+
+ private static int identifyCacheStatus(MediaSet set) {
+ if (set == null || (set.getSupportedOperations()
+ & MediaSet.SUPPORT_CACHE) == 0) {
+ return MediaSet.CACHE_STATUS_NOT_CACHED;
+ }
+
+ return set.getCacheStatus();
+ }
+
+ private class LabelDisplayItem extends DisplayItem {
+ private static final int FONT_COLOR = Color.WHITE;
+
+ private StringTexture mTexture;
+ private String mLabel;
+ private String mPostfix;
+ private final int mSlotIndex;
+
+ public LabelDisplayItem(int slotIndex) {
+ mSlotIndex = slotIndex;
+ updateContent();
+ }
+
+ public boolean updateContent() {
+ String label = mLoadingLabel;
+ String postfix = null;
+ MediaSet set = mSource.getMediaSet(mSlotIndex);
+ if (set != null) {
+ label = Utils.ensureNotNull(set.getName());
+ postfix = " (" + set.getTotalMediaItemCount() + ")";
+ }
+ if (Utils.equals(label, mLabel)
+ && Utils.equals(postfix, mPostfix)) return false;
+ mTexture = StringTexture.newInstance(
+ label, postfix, mLabelFontSize, FONT_COLOR, mLabelWidth, true);
+ setSize(mTexture.getWidth(), mTexture.getHeight());
+ return true;
+ }
+
+ @Override
+ public boolean render(GLCanvas canvas, int pass) {
+ mTexture.draw(canvas, -mWidth / 2, -mHeight / 2);
+ return false;
+ }
+
+ @Override
+ public long getIdentity() {
+ return System.identityHashCode(this);
+ }
+ }
+
+ public void onSizeChanged(int size) {
+ if (mSize != size) {
+ mSize = size;
+ if (mListener != null && mIsActive) mListener.onSizeChanged(mSize);
+ }
+ }
+
+ public void onWindowContentChanged(int index) {
+ if (!mIsActive) {
+ // paused, ignore slot changed event
+ return;
+ }
+ notifySlotChanged(index);
+ }
+
+ public void pause() {
+ mIsActive = false;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetView.java b/src/com/android/gallery3d/ui/AlbumSetView.java
new file mode 100644
index 000000000..ef066b34c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetView.java
@@ -0,0 +1,240 @@
+/*
+ * 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.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+import android.graphics.Rect;
+
+import java.util.Random;
+
+public class AlbumSetView extends SlotView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetView";
+ private static final int CACHE_SIZE = 32;
+ private static final float PHOTO_DISTANCE = 35f;
+
+ private int mVisibleStart;
+ private int mVisibleEnd;
+
+ private Random mRandom = new Random();
+ private long mSeed = mRandom.nextLong();
+
+ private AlbumSetSlidingWindow mDataWindow;
+ private final GalleryActivity mActivity;
+ private final int mSlotWidth;
+ private final int mDisplayItemSize;
+ private final int mLabelFontSize;
+ private final int mLabelOffsetY;
+ private final int mLabelMargin;
+
+ private SelectionDrawer mSelectionDrawer;
+
+ public static interface Model {
+ public MediaItem[] getCoverItems(int index);
+ public MediaSet getMediaSet(int index);
+ public int size();
+ public void setActiveWindow(int start, int end);
+ public void setModelListener(ModelListener listener);
+ }
+
+ public static interface ModelListener {
+ public void onWindowContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ public static class AlbumSetItem {
+ public DisplayItem[] covers;
+ public DisplayItem labelItem;
+ public long setDataVersion;
+ }
+
+ public AlbumSetView(GalleryActivity activity, SelectionDrawer drawer,
+ int slotWidth, int slotHeight, int displayItemSize,
+ int labelFontSize, int labelOffsetY, int labelMargin) {
+ super(activity.getAndroidContext());
+ mActivity = activity;
+ setSelectionDrawer(drawer);
+ setSlotSize(slotWidth, slotHeight);
+ mSlotWidth = slotWidth;
+ mDisplayItemSize = displayItemSize;
+ mLabelFontSize = labelFontSize;
+ mLabelOffsetY = labelOffsetY;
+ mLabelMargin = labelMargin;
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ if (mDataWindow != null) {
+ mDataWindow.setSelectionDrawer(drawer);
+ }
+ }
+
+ public void setModel(AlbumSetView.Model model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ setSlotCount(0);
+ mDataWindow = null;
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSetSlidingWindow(mActivity,
+ mSlotWidth - mLabelMargin * 2, mDisplayItemSize, mLabelFontSize,
+ mSelectionDrawer, model, CACHE_SIZE);
+ mDataWindow.setListener(new MyCacheListener());
+ setSlotCount(mDataWindow.size());
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+ }
+
+ private void putSlotContent(int slotIndex, AlbumSetItem entry) {
+ // Get displayItems from mItemsetMap or create them from MediaSet.
+ Utils.assertTrue(entry != null);
+ Rect rect = getSlotRect(slotIndex);
+
+ DisplayItem[] items = entry.covers;
+ mRandom.setSeed(slotIndex ^ mSeed);
+
+ int x = (rect.left + rect.right) / 2;
+ int y = (rect.top + rect.bottom) / 2;
+
+ Position basePosition = new Position(x, y, 0);
+
+ // Put the cover items in reverse order, so that the first item is on
+ // top of the rest.
+ int labelY = y + mLabelOffsetY - entry.labelItem.getHeight() / 2;
+ Position position = new Position(x, labelY, 0f);
+ putDisplayItem(position, position, entry.labelItem);
+
+ for (int i = 0, n = items.length; i < n; ++i) {
+ DisplayItem item = items[i];
+ float dx = 0;
+ float dy = 0;
+ float dz = 0f;
+ float theta = 0;
+ if (i != 0) {
+ dz = i * PHOTO_DISTANCE;
+ }
+ position = new Position(x + dx, y + dy, dz);
+ position.theta = theta;
+ putDisplayItem(position, basePosition, item);
+ }
+
+ }
+
+ private void freeSlotContent(int index, AlbumSetItem entry) {
+ if (entry == null) return;
+ for (DisplayItem item : entry.covers) {
+ removeDisplayItem(item);
+ }
+ removeDisplayItem(entry.labelItem);
+ }
+
+ public int size() {
+ return mDataWindow.size();
+ }
+
+ @Override
+ public void onLayoutChanged(int width, int height) {
+ updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ @Override
+ public void onScrollPositionChanged(int position) {
+ super.onScrollPositionChanged(position);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ private void updateVisibleRange(int start, int end) {
+ if (start == mVisibleStart && end == mVisibleEnd) {
+ // we need to set the mDataWindow active range in any case.
+ mDataWindow.setActiveWindow(start, end);
+ return;
+ }
+ if (start >= mVisibleEnd || mVisibleStart >= end) {
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ } else {
+ for (int i = mVisibleStart; i < start; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ for (int i = end, n = mVisibleEnd; i < n; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start, n = mVisibleStart; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ for (int i = mVisibleEnd; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+ mVisibleStart = start;
+ mVisibleEnd = end;
+
+ invalidate();
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mSelectionDrawer.prepareDrawing();
+ super.render(canvas);
+ }
+
+ private class MyCacheListener implements AlbumSetSlidingWindow.Listener {
+
+ public void onSizeChanged(int size) {
+ // If the layout parameters are changed, we need reput all items.
+ if (setSlotCount(size)) updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ invalidate();
+ }
+
+ public void onWindowContentChanged(int slot, AlbumSetItem old, AlbumSetItem update) {
+ freeSlotContent(slot, old);
+ putSlotContent(slot, update);
+ invalidate();
+ }
+
+ public void onContentInvalidated() {
+ invalidate();
+ }
+ }
+
+ public void pause() {
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ freeSlotContent(i, mDataWindow.get(i));
+ }
+ mDataWindow.pause();
+ }
+
+ public void resume() {
+ mDataWindow.resume();
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
new file mode 100644
index 000000000..9e44bd1d2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -0,0 +1,433 @@
+/*
+ * 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.GalleryActivity;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.LruCache;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Message;
+
+public class AlbumSlidingWindow implements AlbumView.ModelListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSlidingWindow";
+
+ private static final int MSG_LOAD_BITMAP_DONE = 0;
+ private static final int MSG_UPDATE_SLOT = 1;
+ private static final int MIN_THUMB_SIZE = 100;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentInvalidated();
+ public void onWindowContentChanged(
+ int slot, DisplayItem old, DisplayItem update);
+ }
+
+ private final AlbumView.Model mSource;
+ private int mSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+ private int mFocusIndex = -1;
+
+ private final AlbumDisplayItem mData[];
+ private final ColorTexture mWaitLoadingTexture;
+ private SelectionDrawer mSelectionDrawer;
+
+ private SynchronizedHandler mHandler;
+ private ThreadPool mThreadPool;
+ private int mSlotWidth, mSlotHeight;
+
+ private int mActiveRequestCount = 0;
+ private boolean mIsActive = false;
+
+ private int mDisplayItemSize; // 0: disabled
+ private LruCache<Path, Bitmap> mImageCache = new LruCache<Path, Bitmap>(1000);
+
+ public AlbumSlidingWindow(GalleryActivity activity,
+ AlbumView.Model source, int cacheSize,
+ int slotWidth, int slotHeight, int displayItemSize) {
+ source.setModelListener(this);
+ mSource = source;
+ mData = new AlbumDisplayItem[cacheSize];
+ mSize = source.size();
+ mSlotWidth = slotWidth;
+ mSlotHeight = slotHeight;
+ mDisplayItemSize = displayItemSize;
+
+ mWaitLoadingTexture = new ColorTexture(Color.TRANSPARENT);
+ mWaitLoadingTexture.setSize(1, 1);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_LOAD_BITMAP_DONE: {
+ ((AlbumDisplayItem) message.obj).onLoadBitmapDone();
+ break;
+ }
+ case MSG_UPDATE_SLOT: {
+ updateSlotContent(message.arg1);
+ break;
+ }
+ }
+ }
+ };
+
+ mThreadPool = activity.getThreadPool();
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setFocusIndex(int slotIndex) {
+ mFocusIndex = slotIndex;
+ }
+
+ public DisplayItem get(int slotIndex) {
+ Utils.assertTrue(isActiveSlot(slotIndex),
+ "invalid slot: %s outsides (%s, %s)",
+ slotIndex, mActiveStart, mActiveEnd);
+ return mData[slotIndex % mData.length];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean isActiveSlot(int slotIndex) {
+ return slotIndex >= mActiveStart && slotIndex < mActiveEnd;
+ }
+
+ private void setContentWindow(int contentStart, int contentEnd) {
+ if (contentStart == mContentStart && contentEnd == mContentEnd) return;
+
+ if (!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) {
+ Utils.assertTrue(start <= end
+ && end - start <= mData.length && end <= mSize,
+ "%s, %s, %s, %s", start, end, mData.length, mSize);
+ DisplayItem data[] = mData;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ // If no data is visible, keep the cache content
+ if (start == end) return;
+
+ int contentStart = Utils.clamp((start + end) / 2 - data.length / 2,
+ 0, Math.max(0, mSize - data.length));
+ int contentEnd = Math.min(contentStart + data.length, mSize);
+ setContentWindow(contentStart, contentEnd);
+ if (mIsActive) updateAllImageRequests();
+ }
+
+ // We would like to request non active slots in the following order:
+ // Order: 8 6 4 2 1 3 5 7
+ // |---------|---------------|---------|
+ // |<- active ->|
+ // |<-------- cached range ----------->|
+ private void requestNonactiveImages() {
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0 ;i < range; ++i) {
+ requestSlotImage(mActiveEnd + i, false);
+ requestSlotImage(mActiveStart - 1 - i, false);
+ }
+ }
+
+ private void requestSlotImage(int slotIndex, boolean isActive) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumDisplayItem item = mData[slotIndex % mData.length];
+ item.requestImage();
+ }
+
+ private void cancelNonactiveImages() {
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0 ;i < range; ++i) {
+ cancelSlotImage(mActiveEnd + i, false);
+ cancelSlotImage(mActiveStart - 1 - i, false);
+ }
+ }
+
+ private void cancelSlotImage(int slotIndex, boolean isActive) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumDisplayItem item = mData[slotIndex % mData.length];
+ item.cancelImageRequest();
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumDisplayItem data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumDisplayItem original = data[index];
+ if (original != null) {
+ original.recycle();
+ data[index] = null;
+ }
+ }
+
+ private void prepareSlotContent(final int slotIndex) {
+ mData[slotIndex % mData.length] = new AlbumDisplayItem(
+ slotIndex, mSource.get(slotIndex));
+ }
+
+ private void updateSlotContent(final int slotIndex) {
+ MediaItem item = mSource.get(slotIndex);
+ AlbumDisplayItem data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumDisplayItem original = data[index];
+ AlbumDisplayItem update = new AlbumDisplayItem(slotIndex, item);
+ data[index] = update;
+ boolean isActive = isActiveSlot(slotIndex);
+ if (mListener != null && isActive) {
+ mListener.onWindowContentChanged(slotIndex, original, update);
+ }
+ if (original != null) {
+ if (isActive && original.isRequestInProgress()) {
+ --mActiveRequestCount;
+ }
+ original.recycle();
+ }
+ if (isActive) {
+ if (mActiveRequestCount == 0) cancelNonactiveImages();
+ ++mActiveRequestCount;
+ update.requestImage();
+ } else {
+ if (mActiveRequestCount == 0) update.requestImage();
+ }
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ AlbumDisplayItem data[] = mData;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumDisplayItem item = data[i % data.length];
+ item.requestImage();
+ if (item.isRequestInProgress()) ++mActiveRequestCount;
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ private class AlbumDisplayItem extends AbstractDisplayItem
+ implements FutureListener<Bitmap>, Job<Bitmap> {
+ private Future<Bitmap> mFuture;
+ private final int mSlotIndex;
+ private final int mMediaType;
+ private Texture mContent;
+
+ public AlbumDisplayItem(int slotIndex, MediaItem item) {
+ super(item);
+ mMediaType = (item == null)
+ ? MediaItem.MEDIA_TYPE_UNKNOWN
+ : item.getMediaType();
+ mSlotIndex = slotIndex;
+ updateContent(mWaitLoadingTexture);
+ }
+
+ @Override
+ protected void onBitmapAvailable(Bitmap bitmap) {
+ boolean isActiveSlot = isActiveSlot(mSlotIndex);
+ if (isActiveSlot) {
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ }
+ if (bitmap != null) {
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ texture.setThrottled(true);
+ updateContent(texture);
+ if (mListener != null && isActiveSlot) {
+ mListener.onContentInvalidated();
+ }
+ }
+ }
+
+ private void updateContent(Texture content) {
+ mContent = content;
+
+ int width = mContent.getWidth();
+ int height = mContent.getHeight();
+
+ float scalex = mDisplayItemSize / (float) width;
+ float scaley = mDisplayItemSize / (float) height;
+ float scale = Math.min(scalex, scaley);
+
+ width = (int) Math.floor(width * scale);
+ height = (int) Math.floor(height * scale);
+
+ setSize(width, height);
+ }
+
+ @Override
+ public boolean render(GLCanvas canvas, int pass) {
+ if (pass == 0) {
+ Path path = null;
+ if (mMediaItem != null) path = mMediaItem.getPath();
+ mSelectionDrawer.draw(canvas, mContent, mWidth, mHeight,
+ getRotation(), path, mMediaType);
+ return (mFocusIndex == mSlotIndex);
+ } else if (pass == 1) {
+ mSelectionDrawer.drawFocus(canvas, mWidth, mHeight);
+ }
+ return false;
+ }
+
+ @Override
+ public void startLoadBitmap() {
+ if (mDisplayItemSize < MIN_THUMB_SIZE) {
+ Path path = mMediaItem.getPath();
+ if (mImageCache.containsKey(path)) {
+ Bitmap bitmap = mImageCache.get(path);
+ updateImage(bitmap, false);
+ return;
+ }
+ mFuture = mThreadPool.submit(this, this);
+ } else {
+ mFuture = mThreadPool.submit(mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL), this);
+ }
+ }
+
+ // This gets the bitmap and scale it down.
+ public Bitmap run(JobContext jc) {
+ Job<Bitmap> job = mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL);
+ Bitmap bitmap = job.run(jc);
+ if (bitmap != null) {
+ bitmap = BitmapUtils.resizeDownBySideLength(
+ bitmap, mDisplayItemSize, true);
+ }
+ return bitmap;
+ }
+
+ @Override
+ public void cancelLoadBitmap() {
+ if (mFuture != null) {
+ mFuture.cancel();
+ }
+ }
+
+ @Override
+ public void onFutureDone(Future<Bitmap> bitmap) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOAD_BITMAP_DONE, this));
+ }
+
+ private void onLoadBitmapDone() {
+ Future<Bitmap> future = mFuture;
+ mFuture = null;
+ Bitmap bitmap = future.get();
+ boolean isCancelled = future.isCancelled();
+ if (mDisplayItemSize < MIN_THUMB_SIZE && (bitmap != null || !isCancelled)) {
+ Path path = mMediaItem.getPath();
+ mImageCache.put(path, bitmap);
+ }
+ updateImage(bitmap, isCancelled);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AlbumDisplayItem[%s]", mSlotIndex);
+ }
+ }
+
+ public void onSizeChanged(int size) {
+ if (mSize != size) {
+ mSize = size;
+ if (mListener != null) mListener.onSizeChanged(mSize);
+ }
+ }
+
+ public void onWindowContentChanged(int index) {
+ if (index >= mContentStart && index < mContentEnd && mIsActive) {
+ updateSlotContent(index);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+
+ public void pause() {
+ mIsActive = false;
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ mImageCache.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumView.java b/src/com/android/gallery3d/ui/AlbumView.java
new file mode 100644
index 000000000..417611a69
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumView.java
@@ -0,0 +1,197 @@
+/*
+ * 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.GalleryActivity;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+import android.graphics.Rect;
+
+public class AlbumView extends SlotView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumView";
+ private static final int CACHE_SIZE = 64;
+
+ private int mVisibleStart = 0;
+ private int mVisibleEnd = 0;
+
+ private AlbumSlidingWindow mDataWindow;
+ private final GalleryActivity mActivity;
+ private SelectionDrawer mSelectionDrawer;
+ private int mSlotWidth, mSlotHeight;
+ private int mDisplayItemSize;
+
+ private boolean mIsActive = false;
+
+ public static interface Model {
+ public int size();
+ public MediaItem get(int index);
+ public void setActiveWindow(int start, int end);
+ public void setModelListener(ModelListener listener);
+ }
+
+ public static interface ModelListener {
+ public void onWindowContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ public AlbumView(GalleryActivity activity,
+ int slotWidth, int slotHeight, int displayItemSize) {
+ super(activity.getAndroidContext());
+ mSlotWidth = slotWidth;
+ mSlotHeight = slotHeight;
+ mDisplayItemSize = displayItemSize;
+ setSlotSize(slotWidth, slotHeight);
+ mActivity = activity;
+ }
+
+ public void setSelectionDrawer(SelectionDrawer drawer) {
+ mSelectionDrawer = drawer;
+ if (mDataWindow != null) mDataWindow.setSelectionDrawer(drawer);
+ }
+
+ public void setModel(Model model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ setSlotCount(0);
+ mDataWindow = null;
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSlidingWindow(
+ mActivity, model, CACHE_SIZE,
+ mSlotWidth, mSlotHeight, mDisplayItemSize);
+ mDataWindow.setSelectionDrawer(mSelectionDrawer);
+ mDataWindow.setListener(new MyDataModelListener());
+ setSlotCount(model.size());
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+ }
+
+ public void setFocusIndex(int slotIndex) {
+ if (mDataWindow != null) {
+ mDataWindow.setFocusIndex(slotIndex);
+ }
+ }
+
+ private void putSlotContent(int slotIndex, DisplayItem item) {
+ Rect rect = getSlotRect(slotIndex);
+ Position position = new Position(
+ (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2, 0);
+ putDisplayItem(position, position, item);
+ }
+
+ private void updateVisibleRange(int start, int end) {
+ if (start == mVisibleStart && end == mVisibleEnd) {
+ // we need to set the mDataWindow active range in any case.
+ mDataWindow.setActiveWindow(start, end);
+ return;
+ }
+
+ if (!mIsActive) {
+ mVisibleStart = start;
+ mVisibleEnd = end;
+ mDataWindow.setActiveWindow(start, end);
+ return;
+ }
+
+ if (start >= mVisibleEnd || mVisibleStart >= end) {
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ DisplayItem item = mDataWindow.get(i);
+ if (item != null) removeDisplayItem(item);
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ } else {
+ for (int i = mVisibleStart; i < start; ++i) {
+ DisplayItem item = mDataWindow.get(i);
+ if (item != null) removeDisplayItem(item);
+ }
+ for (int i = end, n = mVisibleEnd; i < n; ++i) {
+ DisplayItem item = mDataWindow.get(i);
+ if (item != null) removeDisplayItem(item);
+ }
+ mDataWindow.setActiveWindow(start, end);
+ for (int i = start, n = mVisibleStart; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ for (int i = mVisibleEnd; i < end; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+
+ mVisibleStart = start;
+ mVisibleEnd = end;
+ }
+
+ @Override
+ protected void onLayoutChanged(int width, int height) {
+ // Reput all the items
+ updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ @Override
+ protected void onScrollPositionChanged(int position) {
+ super.onScrollPositionChanged(position);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mSelectionDrawer.prepareDrawing();
+ super.render(canvas);
+ }
+
+ private class MyDataModelListener implements AlbumSlidingWindow.Listener {
+
+ public void onContentInvalidated() {
+ invalidate();
+ }
+
+ public void onSizeChanged(int size) {
+ // If the layout parameters are changed, we need reput all items.
+ if (setSlotCount(size)) updateVisibleRange(0, 0);
+ updateVisibleRange(getVisibleStart(), getVisibleEnd());
+ invalidate();
+ }
+
+ public void onWindowContentChanged(
+ int slotIndex, DisplayItem old, DisplayItem update) {
+ removeDisplayItem(old);
+ putSlotContent(slotIndex, update);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ mDataWindow.resume();
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ putSlotContent(i, mDataWindow.get(i));
+ }
+ }
+
+ public void pause() {
+ mIsActive = false;
+ for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
+ removeDisplayItem(mDataWindow.get(i));
+ }
+ mDataWindow.pause();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BasicTexture.java b/src/com/android/gallery3d/ui/BasicTexture.java
new file mode 100644
index 000000000..e93006326
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BasicTexture.java
@@ -0,0 +1,164 @@
+/*
+ * 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.common.Utils;
+
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+// BasicTexture is a Texture corresponds to a real GL texture.
+// The state of a BasicTexture indicates whether its data is loaded to GL memory.
+// If a BasicTexture is loaded into GL memory, it has a GL texture id.
+abstract class BasicTexture implements Texture {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "BasicTexture";
+ protected static final int UNSPECIFIED = -1;
+
+ protected static final int STATE_UNLOADED = 0;
+ protected static final int STATE_LOADED = 1;
+ protected static final int STATE_ERROR = -1;
+
+ protected int mId;
+ protected int mState;
+
+ protected int mWidth = UNSPECIFIED;
+ protected int mHeight = UNSPECIFIED;
+
+ private int mTextureWidth;
+ private int mTextureHeight;
+
+ protected WeakReference<GLCanvas> mCanvasRef = null;
+ private static WeakHashMap<BasicTexture, Object> sAllTextures
+ = new WeakHashMap<BasicTexture, Object>();
+ private static ThreadLocal sInFinalizer = new ThreadLocal();
+
+ protected BasicTexture(GLCanvas canvas, int id, int state) {
+ setAssociatedCanvas(canvas);
+ mId = id;
+ mState = state;
+ synchronized (sAllTextures) {
+ sAllTextures.put(this, null);
+ }
+ }
+
+ protected BasicTexture() {
+ this(null, 0, STATE_UNLOADED);
+ }
+
+ protected void setAssociatedCanvas(GLCanvas canvas) {
+ mCanvasRef = canvas == null
+ ? null
+ : new WeakReference<GLCanvas>(canvas);
+ }
+
+ /**
+ * Sets the content size of this texture. In OpenGL, the actual texture
+ * size must be of power of 2, the size of the content may be smaller.
+ */
+ protected void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ mTextureWidth = Utils.nextPowerOf2(width);
+ mTextureHeight = Utils.nextPowerOf2(height);
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ // Returns the width rounded to the next power of 2.
+ public int getTextureWidth() {
+ return mTextureWidth;
+ }
+
+ // Returns the height rounded to the next power of 2.
+ public int getTextureHeight() {
+ return mTextureHeight;
+ }
+
+ public void draw(GLCanvas canvas, int x, int y) {
+ canvas.drawTexture(this, x, y, getWidth(), getHeight());
+ }
+
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ canvas.drawTexture(this, x, y, w, h);
+ }
+
+ // onBind is called before GLCanvas binds this texture.
+ // It should make sure the data is uploaded to GL memory.
+ abstract protected boolean onBind(GLCanvas canvas);
+
+ public boolean isLoaded(GLCanvas canvas) {
+ return mState == STATE_LOADED && mCanvasRef.get() == canvas;
+ }
+
+ // recycle() is called when the texture will never be used again,
+ // so it can free all resources.
+ public void recycle() {
+ freeResource();
+ }
+
+ // yield() is called when the texture will not be used temporarily,
+ // so it can free some resources.
+ // The default implementation unloads the texture from GL memory, so
+ // the subclass should make sure it can reload the texture to GL memory
+ // later, or it will have to override this method.
+ public void yield() {
+ freeResource();
+ }
+
+ private void freeResource() {
+ GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get();
+ if (canvas != null && isLoaded(canvas)) {
+ canvas.unloadTexture(this);
+ }
+ mState = BasicTexture.STATE_UNLOADED;
+ setAssociatedCanvas(null);
+ }
+
+ @Override
+ protected void finalize() {
+ sInFinalizer.set(BasicTexture.class);
+ recycle();
+ sInFinalizer.set(null);
+ }
+
+ // This is for deciding if we can call Bitmap's recycle().
+ // We cannot call Bitmap's recycle() in finalizer because at that point
+ // the finalizer of Bitmap may already be called so recycle() will crash.
+ public static boolean inFinalizer() {
+ return sInFinalizer.get() != null;
+ }
+
+ public static void yieldAllTextures() {
+ synchronized (sAllTextures) {
+ for (BasicTexture t : sAllTextures.keySet()) {
+ t.yield();
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTexture.java b/src/com/android/gallery3d/ui/BitmapTexture.java
new file mode 100644
index 000000000..046bda94c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapTexture.java
@@ -0,0 +1,49 @@
+/*
+ * 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.common.Utils;
+
+import android.graphics.Bitmap;
+
+// BitmapTexture is a texture whose content is specified by a fixed Bitmap.
+//
+// The texture does not own the Bitmap. The user should make sure the Bitmap
+// is valid during the texture's lifetime. When the texture is recycled, it
+// does not free the Bitmap.
+public class BitmapTexture extends UploadedTexture {
+ protected Bitmap mContentBitmap;
+
+ public BitmapTexture(Bitmap bitmap) {
+ Utils.assertTrue(bitmap != null && !bitmap.isRecycled());
+ mContentBitmap = bitmap;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ // Do nothing.
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ return mContentBitmap;
+ }
+
+ public Bitmap getBitmap() {
+ return mContentBitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
new file mode 100644
index 000000000..a47337fa2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java
@@ -0,0 +1,91 @@
+/*
+ * 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.common.BitmapUtils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Bitmap.Config;
+
+import java.util.ArrayList;
+
+public class BitmapTileProvider implements TileImageView.Model {
+ private final Bitmap mBackup;
+ private final Bitmap[] mMipmaps;
+ private final Config mConfig;
+ private final int mImageWidth;
+ private final int mImageHeight;
+
+ private boolean mRecycled = false;
+
+ public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) {
+ mImageWidth = bitmap.getWidth();
+ mImageHeight = bitmap.getHeight();
+ ArrayList<Bitmap> list = new ArrayList<Bitmap>();
+ list.add(bitmap);
+ while (bitmap.getWidth() > maxBackupSize
+ || bitmap.getHeight() > maxBackupSize) {
+ bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false);
+ list.add(bitmap);
+ }
+
+ mBackup = list.remove(list.size() - 1);
+ mMipmaps = list.toArray(new Bitmap[list.size()]);
+ mConfig = Config.ARGB_8888;
+ }
+
+ public Bitmap getBackupImage() {
+ return mBackup;
+ }
+
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ public int getLevelCount() {
+ return mMipmaps.length;
+ }
+
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ Bitmap result = Bitmap.createBitmap(tileSize, tileSize, mConfig);
+ Canvas canvas = new Canvas(result);
+ canvas.drawBitmap(mMipmaps[level], -(x >> level), -(y >> level), null);
+ return result;
+ }
+
+ public void recycle() {
+ if (mRecycled) return;
+ mRecycled = true;
+ for (Bitmap bitmap : mMipmaps) {
+ BitmapUtils.recycleSilently(bitmap);
+ }
+ BitmapUtils.recycleSilently(mBackup);
+ }
+
+ public int getRotation() {
+ return 0;
+ }
+
+ public boolean isFailedToLoad() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BoxBlurFilter.java b/src/com/android/gallery3d/ui/BoxBlurFilter.java
new file mode 100644
index 000000000..0497a61fa
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BoxBlurFilter.java
@@ -0,0 +1,100 @@
+/*
+ * 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;
+
+
+public class BoxBlurFilter {
+ private static final int RED_MASK = 0xff0000;
+ private static final int RED_MASK_SHIFT = 16;
+ private static final int GREEN_MASK = 0x00ff00;
+ private static final int GREEN_MASK_SHIFT = 8;
+ private static final int BLUE_MASK = 0x0000ff;
+ private static final int RADIUS = 4;
+ private static final int KERNEL_SIZE = RADIUS * 2 + 1;
+ private static final int NUM_COLORS = 256;
+ private static final int[] KERNEL_NORM = new int[KERNEL_SIZE * NUM_COLORS];
+
+ public static final int MODE_REPEAT = 1;
+ public static final int MODE_CLAMP = 2;
+
+ static {
+ int index = 0;
+ // Build a lookup table from summed to normalized kernel values.
+ // The formula: KERNAL_NORM[value] = value / KERNEL_SIZE
+ for (int i = 0; i < NUM_COLORS; ++i) {
+ for (int j = 0; j < KERNEL_SIZE; ++j) {
+ KERNEL_NORM[index++] = i;
+ }
+ }
+ }
+
+ private BoxBlurFilter() {
+ }
+
+ private static int sample(int x, int width, int mode) {
+ if (x >= 0 && x < width) return x;
+ return mode == MODE_REPEAT
+ ? x < 0 ? x + width : x - width
+ : x < 0 ? 0 : width - 1;
+ }
+
+ public static void apply(
+ Bitmap bitmap, int horizontalMode, int verticalMode) {
+
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int data[] = new int[width * height];
+ bitmap.getPixels(data, 0, width, 0, 0, width, height);
+ int temp[] = new int[width * height];
+ applyOneDimension(data, temp, width, height, horizontalMode);
+ applyOneDimension(temp, data, height, width, verticalMode);
+ bitmap.setPixels(data, 0, width, 0, 0, width, height);
+ }
+
+ private static void applyOneDimension(
+ int[] in, int[] out, int width, int height, int mode) {
+ for (int y = 0, read = 0; y < height; ++y, read += width) {
+ // Evaluate the kernel for the first pixel in the row.
+ int red = 0;
+ int green = 0;
+ int blue = 0;
+ for (int i = -RADIUS; i <= RADIUS; ++i) {
+ int argb = in[read + sample(i, width, mode)];
+ red += (argb & RED_MASK) >> RED_MASK_SHIFT;
+ green += (argb & GREEN_MASK) >> GREEN_MASK_SHIFT;
+ blue += argb & BLUE_MASK;
+ }
+ for (int x = 0, write = y; x < width; ++x, write += height) {
+ // Output the current pixel.
+ out[write] = 0xFF000000
+ | (KERNEL_NORM[red] << RED_MASK_SHIFT)
+ | (KERNEL_NORM[green] << GREEN_MASK_SHIFT)
+ | KERNEL_NORM[blue];
+
+ // Slide to the next pixel, adding the new rightmost pixel and
+ // subtracting the former leftmost.
+ int prev = in[read + sample(x - RADIUS, width, mode)];
+ int next = in[read + sample(x + RADIUS + 1, width, mode)];
+ red += ((next & RED_MASK) - (prev & RED_MASK)) >> RED_MASK_SHIFT;
+ green += ((next & GREEN_MASK) - (prev & GREEN_MASK)) >> GREEN_MASK_SHIFT;
+ blue += (next & BLUE_MASK) - (prev & BLUE_MASK);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CacheBarView.java b/src/com/android/gallery3d/ui/CacheBarView.java
new file mode 100644
index 000000000..40f84d8f9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CacheBarView.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import android.os.StatFs;
+import android.text.format.Formatter;
+import android.view.View.MeasureSpec;
+
+import java.io.File;
+
+public class CacheBarView extends GLView implements TextButton.OnClickedListener {
+ private static final String TAG = "CacheBarView";
+ private static final int FONT_COLOR = 0xffffffff;
+ private static final int MSG_REFRESH_STORAGE = 1;
+ private static final int PIN_SIZE = 36;
+
+ public interface Listener {
+ void onDoneClicked();
+ }
+
+ private GalleryActivity mActivity;
+ private Context mContext;
+
+ private StorageInfo mStorageInfo;
+ private long mUserChangeDelta;
+ private Future<StorageInfo> mStorageInfoFuture;
+ private Handler mHandler;
+
+ private int mTotalHeight;
+ private int mPinLeftMargin;
+ private int mPinRightMargin;
+ private int mButtonRightMargin;
+
+ private NinePatchTexture mBackground;
+ private GLView mLeftPin; // The pin icon.
+ private GLView mLeftLabel; // "Make available offline"
+ private ProgressBar mStorageBar;
+ private Label mStorageLabel; // "27.26 GB free"
+ private TextButton mDoneButton; // "Done"
+
+ private Listener mListener;
+
+ public CacheBarView(GalleryActivity activity, int resBackground, int height,
+ int pinLeftMargin, int pinRightMargin, int buttonRightMargin,
+ int fontSize) {
+ mActivity = activity;
+ mContext = activity.getAndroidContext();
+
+ mPinLeftMargin = pinLeftMargin;
+ mPinRightMargin = pinRightMargin;
+ mButtonRightMargin = buttonRightMargin;
+
+ mBackground = new NinePatchTexture(mContext, resBackground);
+ Rect paddings = mBackground.getPaddings();
+
+ // The total height of the strip that includes the bar containing Pin,
+ // Label, DoneButton, ..., ect. and the extended fading bar.
+ mTotalHeight = height + paddings.top;
+
+ mLeftPin = new Icon(mContext, R.drawable.ic_manage_pin, PIN_SIZE, PIN_SIZE);
+ mLeftLabel = new Label(mContext, R.string.make_available_offline,
+ fontSize, FONT_COLOR);
+ addComponent(mLeftPin);
+ addComponent(mLeftLabel);
+
+ mDoneButton = new TextButton(mContext, R.string.done);
+ mDoneButton.setOnClickListener(this);
+ NinePatchTexture normal = new NinePatchTexture(
+ mContext, R.drawable.btn_default_normal_holo_dark);
+ NinePatchTexture pressed = new NinePatchTexture(
+ mContext, R.drawable.btn_default_pressed_holo_dark);
+ mDoneButton.setNormalBackground(normal);
+ mDoneButton.setPressedBackground(pressed);
+ addComponent(mDoneButton);
+
+ // Initially the progress bar and label are invisible.
+ // It will be made visible after we have the storage information.
+ mStorageBar = new ProgressBar(mContext,
+ R.drawable.progress_primary_holo_dark,
+ R.drawable.progress_secondary_holo_dark,
+ R.drawable.progress_bg_holo_dark);
+ mStorageLabel = new Label(mContext, "", 14, Color.WHITE);
+ addComponent(mStorageBar);
+ addComponent(mStorageLabel);
+ mStorageBar.setVisibility(GLView.INVISIBLE);
+ mStorageLabel.setVisibility(GLView.INVISIBLE);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_REFRESH_STORAGE:
+ mStorageInfo = (StorageInfo) msg.obj;
+ refreshStorageInfo();
+ break;
+ }
+ }
+ };
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ // Called by mDoneButton
+ public void onClicked(GLView source) {
+ if (mListener != null) {
+ mListener.onDoneClicked();
+ }
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ // The size of mStorageLabel can change, so we need to layout
+ // even if the size of CacheBarView does not change.
+ int w = right - left;
+ int h = bottom - top;
+
+ mLeftPin.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int pinH = mLeftPin.getMeasuredHeight();
+ int pinW = mLeftPin.getMeasuredWidth();
+ int pinT = (h - pinH) / 2;
+ int pinL = mPinLeftMargin;
+ mLeftPin.layout(pinL, pinT, pinL + pinW, pinT + pinH);
+
+ mLeftLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int labelH = mLeftLabel.getMeasuredHeight();
+ int labelW = mLeftLabel.getMeasuredWidth();
+ int labelT = (h - labelH) / 2;
+ int labelL = pinL + pinW + mPinRightMargin;
+ mLeftLabel.layout(labelL, labelT, labelL + labelW, labelT + labelH);
+
+ mDoneButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int doneH = mDoneButton.getMeasuredHeight();
+ int doneW = mDoneButton.getMeasuredWidth();
+ int doneT = (h - doneH) / 2;
+ int doneR = w - mButtonRightMargin;
+ mDoneButton.layout(doneR - doneW, doneT, doneR, doneT + doneH);
+
+ int centerX = w / 2;
+ int centerY = h / 2;
+
+ int capBarH = 20;
+ int capBarW = 200;
+ int capBarT = centerY - capBarH / 2;
+ int capBarL = centerX - capBarW / 2;
+ mStorageBar.layout(capBarL, capBarT, capBarL + capBarW,
+ capBarT + capBarH);
+
+ mStorageLabel.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int capLabelH = mStorageLabel.getMeasuredHeight();
+ int capLabelW = mStorageLabel.getMeasuredWidth();
+ int capLabelT = centerY - capLabelH / 2;
+ int capLabelL = centerX + capBarW / 2 + 8;
+ mStorageLabel.layout(capLabelL , capLabelT, capLabelL + capLabelW,
+ capLabelT + capLabelH);
+ }
+
+ public void refreshStorageInfo() {
+ long used = mStorageInfo.usedBytes;
+ long total = mStorageInfo.totalBytes;
+ long cached = mStorageInfo.usedCacheBytes;
+ long target = mStorageInfo.targetCacheBytes;
+
+ double primary = (double) used / total;
+ double secondary =
+ (double) (used - cached + target + mUserChangeDelta) / total;
+
+ mStorageBar.setProgress((int) (primary * 10000));
+ mStorageBar.setSecondaryProgress((int) (secondary * 10000));
+
+ long freeBytes = mStorageInfo.totalBytes - mStorageInfo.usedBytes;
+ String sizeString = Formatter.formatFileSize(mContext, freeBytes);
+ String label = mContext.getString(R.string.free_space_format, sizeString);
+ mStorageLabel.setText(label);
+ mStorageBar.setVisibility(GLView.VISIBLE);
+ mStorageLabel.setVisibility(GLView.VISIBLE);
+ requestLayout(); // because the size of the label may have changed.
+ }
+
+ public void increaseTargetCacheSize(long delta) {
+ mUserChangeDelta += delta;
+ refreshStorageInfo();
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ Rect paddings = mBackground.getPaddings();
+ mBackground.draw(canvas, 0, -paddings.top, getWidth(), mTotalHeight);
+ }
+
+ public void resume() {
+ mStorageInfoFuture = mActivity.getThreadPool().submit(
+ new StorageInfoJob(),
+ new FutureListener<StorageInfo>() {
+ public void onFutureDone(Future<StorageInfo> future) {
+ mStorageInfoFuture = null;
+ if (!future.isCancelled()) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_REFRESH_STORAGE, future.get()));
+ }
+ }
+ });
+ }
+
+ public void pause() {
+ if (mStorageInfoFuture != null) {
+ mStorageInfoFuture.cancel();
+ mStorageInfoFuture = null;
+ }
+ mStorageBar.setVisibility(GLView.INVISIBLE);
+ mStorageLabel.setVisibility(GLView.INVISIBLE);
+ }
+
+ public static class StorageInfo {
+ long totalBytes; // number of bytes the storage has.
+ long usedBytes; // number of bytes already used.
+ long usedCacheBytes; // number of bytes used for the cache (should be less
+ // then usedBytes).
+ long targetCacheBytes;// number of bytes used for the cache
+ // if all pending downloads (and removals) are completed.
+ }
+
+ private class StorageInfoJob implements Job<StorageInfo> {
+ public StorageInfo run(JobContext jc) {
+ File cacheDir = mContext.getExternalCacheDir();
+ if (cacheDir == null) {
+ cacheDir = mContext.getCacheDir();
+ }
+ String path = cacheDir.getAbsolutePath();
+ StatFs stat = new StatFs(path);
+ long blockSize = stat.getBlockSize();
+ long availableBlocks = stat.getAvailableBlocks();
+ long totalBlocks = stat.getBlockCount();
+ StorageInfo si = new StorageInfo();
+ si.totalBytes = blockSize * totalBlocks;
+ si.usedBytes = blockSize * (totalBlocks - availableBlocks);
+ si.usedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize();
+ si.targetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize();
+ return si;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CanvasTexture.java b/src/com/android/gallery3d/ui/CanvasTexture.java
new file mode 100644
index 000000000..679a4bcdc
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CanvasTexture.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Bitmap.Config;
+
+// CanvasTexture is a texture whose content is the drawing on a Canvas.
+// The subclasses should override onDraw() to draw on the bitmap.
+// By default CanvasTexture is not opaque.
+abstract class CanvasTexture extends UploadedTexture {
+ protected Canvas mCanvas;
+ private final Config mConfig;
+
+ public CanvasTexture(int width, int height) {
+ mConfig = Config.ARGB_8888;
+ setSize(width, height);
+ setOpaque(false);
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig);
+ mCanvas = new Canvas(bitmap);
+ onDraw(mCanvas, bitmap);
+ return bitmap;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ if (!inFinalizer()) {
+ bitmap.recycle();
+ }
+ }
+
+ abstract protected void onDraw(Canvas canvas, Bitmap backing);
+}
diff --git a/src/com/android/gallery3d/ui/ColorTexture.java b/src/com/android/gallery3d/ui/ColorTexture.java
new file mode 100644
index 000000000..24e8914b5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ColorTexture.java
@@ -0,0 +1,58 @@
+/*
+ * 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.common.Utils;
+
+// ColorTexture is a texture which fills the rectangle with the specified color.
+public class ColorTexture implements Texture {
+
+ private final int mColor;
+ private int mWidth;
+ private int mHeight;
+
+ public ColorTexture(int color) {
+ mColor = color;
+ mWidth = 1;
+ mHeight = 1;
+ }
+
+ public void draw(GLCanvas canvas, int x, int y) {
+ draw(canvas, x, y, mWidth, mHeight);
+ }
+
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ canvas.fillRect(x, y, w, h, mColor);
+ }
+
+ public boolean isOpaque() {
+ return Utils.isOpaque(mColor);
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Config.java b/src/com/android/gallery3d/ui/Config.java
new file mode 100644
index 000000000..5c5b6210a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Config.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+interface DetailsWindowConfig {
+ public static final int FONT_SIZE = 18;
+ public static final int PREFERRED_WIDTH = 400;
+ public static final int LEFT_RIGHT_EXTRA_PADDING = 9;
+ public static final int TOP_BOTTOM_EXTRA_PADDING = 9;
+ public static final int LINE_SPACING = 5;
+ public static final int FIRST_LINE_SPACING = 18;
+}
+
+interface TextButtonConfig {
+ public static final int HORIZONTAL_PADDINGS = 16;
+ public static final int VERTICAL_PADDINGS = 5;
+}
diff --git a/src/com/android/gallery3d/ui/CropView.java b/src/com/android/gallery3d/ui/CropView.java
new file mode 100644
index 000000000..9c59c9a84
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CropView.java
@@ -0,0 +1,801 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.media.FaceDetector;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import javax.microedition.khronos.opengles.GL11;
+
+/**
+ * The activity can crop specific region of interest from an image.
+ */
+public class CropView extends GLView {
+ private static final String TAG = "CropView";
+
+ private static final int FACE_PIXEL_COUNT = 120000; // around 400x300
+
+ private static final int COLOR_OUTLINE = 0xFF008AFF;
+ private static final int COLOR_FACE_OUTLINE = 0xFF000000;
+
+ private static final float OUTLINE_WIDTH = 3f;
+
+ private static final int SIZE_UNKNOWN = -1;
+ private static final int TOUCH_TOLERANCE = 30;
+
+ private static final float MIN_SELECTION_LENGTH = 16f;
+ public static final float UNSPECIFIED = -1f;
+
+ private static final int MAX_FACE_COUNT = 3;
+ private static final float FACE_EYE_RATIO = 2f;
+
+ private static final int ANIMATION_DURATION = 1250;
+
+ private static final int MOVE_LEFT = 1;
+ private static final int MOVE_TOP = 2;
+ private static final int MOVE_RIGHT = 4;
+ private static final int MOVE_BOTTOM = 8;
+ private static final int MOVE_BLOCK = 16;
+
+ private static final float MAX_SELECTION_RATIO = 0.8f;
+ private static final float MIN_SELECTION_RATIO = 0.4f;
+ private static final float SELECTION_RATIO = 0.60f;
+ private static final int ANIMATION_TRIGGER = 64;
+
+ private static final int MSG_UPDATE_FACES = 1;
+
+ private float mAspectRatio = UNSPECIFIED;
+ private float mSpotlightRatioX = 0;
+ private float mSpotlightRatioY = 0;
+
+ private Handler mMainHandler;
+
+ private FaceHighlightView mFaceDetectionView;
+ private HighlightRectangle mHighlightRectangle;
+ private TileImageView mImageView;
+ private AnimationController mAnimation = new AnimationController();
+
+ private int mImageWidth = SIZE_UNKNOWN;
+ private int mImageHeight = SIZE_UNKNOWN;
+
+ private GalleryActivity mActivity;
+
+ private GLPaint mPaint = new GLPaint();
+ private GLPaint mFacePaint = new GLPaint();
+
+ private int mImageRotation;
+
+ public CropView(GalleryActivity activity) {
+ mActivity = activity;
+ mImageView = new TileImageView(activity);
+ mFaceDetectionView = new FaceHighlightView();
+ mHighlightRectangle = new HighlightRectangle();
+
+ addComponent(mImageView);
+ addComponent(mFaceDetectionView);
+ addComponent(mHighlightRectangle);
+
+ mHighlightRectangle.setVisibility(GLView.INVISIBLE);
+
+ mPaint.setColor(COLOR_OUTLINE);
+ mPaint.setLineWidth(OUTLINE_WIDTH);
+
+ mFacePaint.setColor(COLOR_FACE_OUTLINE);
+ mFacePaint.setLineWidth(OUTLINE_WIDTH);
+
+ mMainHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_FACES);
+ ((DetectFaceTask) message.obj).updateFaces();
+ }
+ };
+ }
+
+ public void setAspectRatio(float ratio) {
+ mAspectRatio = ratio;
+ }
+
+ public void setSpotlightRatio(float ratioX, float ratioY) {
+ mSpotlightRatioX = ratioX;
+ mSpotlightRatioY = ratioY;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ int width = r - l;
+ int height = b - t;
+
+ mFaceDetectionView.layout(0, 0, width, height);
+ mHighlightRectangle.layout(0, 0, width, height);
+ mImageView.layout(0, 0, width, height);
+ if (mImageHeight != SIZE_UNKNOWN) {
+ mAnimation.initialize();
+ if (mHighlightRectangle.getVisibility() == GLView.VISIBLE) {
+ mAnimation.parkNow(
+ mHighlightRectangle.mHighlightRect);
+ }
+ }
+ }
+
+ private boolean setImageViewPosition(int centerX, int centerY, float scale) {
+ int inverseX = mImageWidth - centerX;
+ int inverseY = mImageHeight - centerY;
+ TileImageView t = mImageView;
+ int rotation = mImageRotation;
+ switch (rotation) {
+ case 0: return t.setPosition(centerX, centerY, scale, 0);
+ case 90: return t.setPosition(centerY, inverseX, scale, 90);
+ case 180: return t.setPosition(inverseX, inverseY, scale, 180);
+ case 270: return t.setPosition(inverseY, centerX, scale, 270);
+ default: throw new IllegalArgumentException(String.valueOf(rotation));
+ }
+ }
+
+ @Override
+ public void render(GLCanvas canvas) {
+ AnimationController a = mAnimation;
+ if (a.calculate(canvas.currentAnimationTimeMillis())) invalidate();
+ setImageViewPosition(a.getCenterX(), a.getCenterY(), a.getScale());
+ super.render(canvas);
+ }
+
+ @Override
+ public void renderBackground(GLCanvas canvas) {
+ canvas.clearBuffer();
+ }
+
+ public RectF getCropRectangle() {
+ if (mHighlightRectangle.getVisibility() == GLView.INVISIBLE) return null;
+ RectF rect = mHighlightRectangle.mHighlightRect;
+ RectF result = new RectF(rect.left * mImageWidth, rect.top * mImageHeight,
+ rect.right * mImageWidth, rect.bottom * mImageHeight);
+ return result;
+ }
+
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ private class FaceHighlightView extends GLView {
+ private static final int INDEX_NONE = -1;
+ private ArrayList<RectF> mFaces = new ArrayList<RectF>();
+ private RectF mRect = new RectF();
+ private int mPressedFaceIndex = INDEX_NONE;
+
+ public void addFace(RectF faceRect) {
+ mFaces.add(faceRect);
+ invalidate();
+ }
+
+ private void renderFace(GLCanvas canvas, RectF face, boolean pressed) {
+ GL11 gl = canvas.getGLInstance();
+ if (pressed) {
+ gl.glEnable(GL11.GL_STENCIL_TEST);
+ gl.glClear(GL11.GL_STENCIL_BUFFER_BIT);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE);
+ gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1);
+ }
+
+ RectF r = mAnimation.mapRect(face, mRect);
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT);
+ canvas.drawRect(r.left, r.top, r.width(), r.height(), mFacePaint);
+
+ if (pressed) {
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
+ }
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ ArrayList<RectF> faces = mFaces;
+ for (int i = 0, n = faces.size(); i < n; ++i) {
+ renderFace(canvas, faces.get(i), i == mPressedFaceIndex);
+ }
+
+ GL11 gl = canvas.getGLInstance();
+ if (mPressedFaceIndex != INDEX_NONE) {
+ gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1);
+ canvas.fillRect(0, 0, getWidth(), getHeight(), 0x66000000);
+ gl.glDisable(GL11.GL_STENCIL_TEST);
+ }
+ }
+
+ private void setPressedFace(int index) {
+ if (mPressedFaceIndex == index) return;
+ mPressedFaceIndex = index;
+ invalidate();
+ }
+
+ private int getFaceIndexByPosition(float x, float y) {
+ ArrayList<RectF> faces = mFaces;
+ for (int i = 0, n = faces.size(); i < n; ++i) {
+ RectF r = mAnimation.mapRect(faces.get(i), mRect);
+ if (r.contains(x, y)) return i;
+ }
+ return INDEX_NONE;
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE: {
+ setPressedFace(getFaceIndexByPosition(x, y));
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ int index = mPressedFaceIndex;
+ setPressedFace(INDEX_NONE);
+ if (index != INDEX_NONE) {
+ mHighlightRectangle.setRectangle(mFaces.get(index));
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ setVisibility(GLView.INVISIBLE);
+ }
+ }
+ }
+ return true;
+ }
+ }
+
+ private class AnimationController extends Animation {
+ private int mCurrentX;
+ private int mCurrentY;
+ private float mCurrentScale;
+ private int mStartX;
+ private int mStartY;
+ private float mStartScale;
+ private int mTargetX;
+ private int mTargetY;
+ private float mTargetScale;
+
+ public AnimationController() {
+ setDuration(ANIMATION_DURATION);
+ setInterpolator(new DecelerateInterpolator(4));
+ }
+
+ public void initialize() {
+ mCurrentX = mImageWidth / 2;
+ mCurrentY = mImageHeight / 2;
+ mCurrentScale = Math.min(2, Math.min(
+ (float) getWidth() / mImageWidth,
+ (float) getHeight() / mImageHeight));
+ }
+
+ public void startParkingAnimation(RectF highlight) {
+ RectF r = mAnimation.mapRect(highlight, new RectF());
+ int width = getWidth();
+ int height = getHeight();
+
+ float wr = r.width() / width;
+ float hr = r.height() / height;
+ final int d = ANIMATION_TRIGGER;
+ if (wr >= MIN_SELECTION_RATIO && wr < MAX_SELECTION_RATIO
+ && hr >= MIN_SELECTION_RATIO && hr < MAX_SELECTION_RATIO
+ && r.left >= d && r.right < width - d
+ && r.top >= d && r.bottom < height - d) return;
+
+ mStartX = mCurrentX;
+ mStartY = mCurrentY;
+ mStartScale = mCurrentScale;
+ calculateTarget(highlight);
+ start();
+ }
+
+ public void parkNow(RectF highlight) {
+ calculateTarget(highlight);
+ forceStop();
+ mStartX = mCurrentX = mTargetX;
+ mStartY = mCurrentY = mTargetY;
+ mStartScale = mCurrentScale = mTargetScale;
+ }
+
+ public void inverseMapPoint(PointF point) {
+ float s = mCurrentScale;
+ point.x = Utils.clamp(((point.x - getWidth() * 0.5f) / s
+ + mCurrentX) / mImageWidth, 0, 1);
+ point.y = Utils.clamp(((point.y - getHeight() * 0.5f) / s
+ + mCurrentY) / mImageHeight, 0, 1);
+ }
+
+ public RectF mapRect(RectF input, RectF output) {
+ float offsetX = getWidth() * 0.5f;
+ float offsetY = getHeight() * 0.5f;
+ int x = mCurrentX;
+ int y = mCurrentY;
+ float s = mCurrentScale;
+ output.set(
+ offsetX + (input.left * mImageWidth - x) * s,
+ offsetY + (input.top * mImageHeight - y) * s,
+ offsetX + (input.right * mImageWidth - x) * s,
+ offsetY + (input.bottom * mImageHeight - y) * s);
+ return output;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrentX = Math.round(mStartX + (mTargetX - mStartX) * progress);
+ mCurrentY = Math.round(mStartY + (mTargetY - mStartY) * progress);
+ mCurrentScale = mStartScale + (mTargetScale - mStartScale) * progress;
+
+ if (mCurrentX == mTargetX && mCurrentY == mTargetY
+ && mCurrentScale == mTargetScale) forceStop();
+ }
+
+ public int getCenterX() {
+ return mCurrentX;
+ }
+
+ public int getCenterY() {
+ return mCurrentY;
+ }
+
+ public float getScale() {
+ return mCurrentScale;
+ }
+
+ private void calculateTarget(RectF highlight) {
+ float width = getWidth();
+ float height = getHeight();
+
+ if (mImageWidth != SIZE_UNKNOWN) {
+ float minScale = Math.min(width / mImageWidth, height / mImageHeight);
+ float scale = Utils.clamp(SELECTION_RATIO * Math.min(
+ width / (highlight.width() * mImageWidth),
+ height / (highlight.height() * mImageHeight)), minScale, 2f);
+ int centerX = Math.round(
+ mImageWidth * (highlight.left + highlight.right) * 0.5f);
+ int centerY = Math.round(
+ mImageHeight * (highlight.top + highlight.bottom) * 0.5f);
+
+ if (Math.round(mImageWidth * scale) > width) {
+ int limitX = Math.round(width * 0.5f / scale);
+ centerX = Math.round(
+ (highlight.left + highlight.right) * mImageWidth / 2);
+ centerX = Utils.clamp(centerX, limitX, mImageWidth - limitX);
+ } else {
+ centerX = mImageWidth / 2;
+ }
+ if (Math.round(mImageHeight * scale) > height) {
+ int limitY = Math.round(height * 0.5f / scale);
+ centerY = Math.round(
+ (highlight.top + highlight.bottom) * mImageHeight / 2);
+ centerY = Utils.clamp(centerY, limitY, mImageHeight - limitY);
+ } else {
+ centerY = mImageHeight / 2;
+ }
+ mTargetX = centerX;
+ mTargetY = centerY;
+ mTargetScale = scale;
+ }
+ }
+
+ }
+
+ private class HighlightRectangle extends GLView {
+ private RectF mHighlightRect = new RectF(0.25f, 0.25f, 0.75f, 0.75f);
+ private RectF mTempRect = new RectF();
+ private PointF mTempPoint = new PointF();
+
+ private ResourceTexture mArrowX;
+ private ResourceTexture mArrowY;
+
+ private int mMovingEdges = 0;
+ private float mReferenceX;
+ private float mReferenceY;
+
+ public HighlightRectangle() {
+ mArrowX = new ResourceTexture(mActivity.getAndroidContext(),
+ R.drawable.camera_crop_width_holo);
+ mArrowY = new ResourceTexture(mActivity.getAndroidContext(),
+ R.drawable.camera_crop_height_holo);
+ }
+
+ public void setInitRectangle() {
+ float targetRatio = mAspectRatio == UNSPECIFIED
+ ? 1f
+ : mAspectRatio * mImageHeight / mImageWidth;
+ float w = SELECTION_RATIO / 2f;
+ float h = SELECTION_RATIO / 2f;
+ if (targetRatio > 1) {
+ h = w / targetRatio;
+ } else {
+ w = h * targetRatio;
+ }
+ mHighlightRect.set(0.5f - w, 0.5f - h, 0.5f + w, 0.5f + h);
+ }
+
+ public void setRectangle(RectF faceRect) {
+ mHighlightRect.set(faceRect);
+ mAnimation.startParkingAnimation(faceRect);
+ invalidate();
+ }
+
+ private void moveEdges(MotionEvent event) {
+ float scale = mAnimation.getScale();
+ float dx = (event.getX() - mReferenceX) / scale / mImageWidth;
+ float dy = (event.getY() - mReferenceY) / scale / mImageHeight;
+ mReferenceX = event.getX();
+ mReferenceY = event.getY();
+ RectF r = mHighlightRect;
+
+ if ((mMovingEdges & MOVE_BLOCK) != 0) {
+ dx = Utils.clamp(dx, -r.left, 1 - r.right);
+ dy = Utils.clamp(dy, -r.top , 1 - r.bottom);
+ r.top += dy;
+ r.bottom += dy;
+ r.left += dx;
+ r.right += dx;
+ } else {
+ PointF point = mTempPoint;
+ point.set(mReferenceX, mReferenceY);
+ mAnimation.inverseMapPoint(point);
+ float left = r.left + MIN_SELECTION_LENGTH / mImageWidth;
+ float right = r.right - MIN_SELECTION_LENGTH / mImageWidth;
+ float top = r.top + MIN_SELECTION_LENGTH / mImageHeight;
+ float bottom = r.bottom - MIN_SELECTION_LENGTH / mImageHeight;
+ if ((mMovingEdges & MOVE_RIGHT) != 0) {
+ r.right = Utils.clamp(point.x, left, 1f);
+ }
+ if ((mMovingEdges & MOVE_LEFT) != 0) {
+ r.left = Utils.clamp(point.x, 0, right);
+ }
+ if ((mMovingEdges & MOVE_TOP) != 0) {
+ r.top = Utils.clamp(point.y, 0, bottom);
+ }
+ if ((mMovingEdges & MOVE_BOTTOM) != 0) {
+ r.bottom = Utils.clamp(point.y, top, 1f);
+ }
+ if (mAspectRatio != UNSPECIFIED) {
+ float targetRatio = mAspectRatio * mImageHeight / mImageWidth;
+ if (r.width() / r.height() > targetRatio) {
+ float height = r.width() / targetRatio;
+ if ((mMovingEdges & MOVE_BOTTOM) != 0) {
+ r.bottom = Utils.clamp(r.top + height, top, 1f);
+ } else {
+ r.top = Utils.clamp(r.bottom - height, 0, bottom);
+ }
+ } else {
+ float width = r.height() * targetRatio;
+ if ((mMovingEdges & MOVE_LEFT) != 0) {
+ r.left = Utils.clamp(r.right - width, 0, right);
+ } else {
+ r.right = Utils.clamp(r.left + width, left, 1f);
+ }
+ }
+ if (r.width() / r.height() > targetRatio) {
+ float width = r.height() * targetRatio;
+ if ((mMovingEdges & MOVE_LEFT) != 0) {
+ r.left = Utils.clamp(r.right - width, 0, right);
+ } else {
+ r.right = Utils.clamp(r.left + width, left, 1f);
+ }
+ } else {
+ float height = r.width() / targetRatio;
+ if ((mMovingEdges & MOVE_BOTTOM) != 0) {
+ r.bottom = Utils.clamp(r.top + height, top, 1f);
+ } else {
+ r.top = Utils.clamp(r.bottom - height, 0, bottom);
+ }
+ }
+ }
+ }
+ invalidate();
+ }
+
+ private void setMovingEdges(MotionEvent event) {
+ RectF r = mAnimation.mapRect(mHighlightRect, mTempRect);
+ float x = event.getX();
+ float y = event.getY();
+
+ if (x > r.left + TOUCH_TOLERANCE && x < r.right - TOUCH_TOLERANCE
+ && y > r.top + TOUCH_TOLERANCE && y < r.bottom - TOUCH_TOLERANCE) {
+ mMovingEdges = MOVE_BLOCK;
+ return;
+ }
+
+ boolean inVerticalRange = (r.top - TOUCH_TOLERANCE) <= y
+ && y <= (r.bottom + TOUCH_TOLERANCE);
+ boolean inHorizontalRange = (r.left - TOUCH_TOLERANCE) <= x
+ && x <= (r.right + TOUCH_TOLERANCE);
+
+ if (inVerticalRange) {
+ boolean left = Math.abs(x - r.left) <= TOUCH_TOLERANCE;
+ boolean right = Math.abs(x - r.right) <= TOUCH_TOLERANCE;
+ if (left && right) {
+ left = Math.abs(x - r.left) < Math.abs(x - r.right);
+ right = !left;
+ }
+ if (left) mMovingEdges |= MOVE_LEFT;
+ if (right) mMovingEdges |= MOVE_RIGHT;
+ if (mAspectRatio != UNSPECIFIED && inHorizontalRange) {
+ mMovingEdges |= (y >
+ (r.top + r.bottom) / 2) ? MOVE_BOTTOM : MOVE_TOP;
+ }
+ }
+ if (inHorizontalRange) {
+ boolean top = Math.abs(y - r.top) <= TOUCH_TOLERANCE;
+ boolean bottom = Math.abs(y - r.bottom) <= TOUCH_TOLERANCE;
+ if (top && bottom) {
+ top = Math.abs(y - r.top) < Math.abs(y - r.bottom);
+ bottom = !top;
+ }
+ if (top) mMovingEdges |= MOVE_TOP;
+ if (bottom) mMovingEdges |= MOVE_BOTTOM;
+ if (mAspectRatio != UNSPECIFIED && inVerticalRange) {
+ mMovingEdges |= (x >
+ (r.left + r.right) / 2) ? MOVE_RIGHT : MOVE_LEFT;
+ }
+ }
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ mReferenceX = event.getX();
+ mReferenceY = event.getY();
+ setMovingEdges(event);
+ invalidate();
+ return true;
+ }
+ case MotionEvent.ACTION_MOVE:
+ moveEdges(event);
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ mMovingEdges = 0;
+ mAnimation.startParkingAnimation(mHighlightRect);
+ invalidate();
+ return true;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ RectF r = mAnimation.mapRect(mHighlightRect, mTempRect);
+ drawHighlightRectangle(canvas, r);
+
+ float centerY = (r.top + r.bottom) / 2;
+ float centerX = (r.left + r.right) / 2;
+ if ((mMovingEdges & (MOVE_RIGHT | MOVE_BLOCK)) != 0) {
+ mArrowX.draw(canvas,
+ Math.round(r.right - mArrowX.getWidth() / 2),
+ Math.round(centerY - mArrowX.getHeight() / 2));
+ }
+ if ((mMovingEdges & (MOVE_LEFT | MOVE_BLOCK)) != 0) {
+ mArrowX.draw(canvas,
+ Math.round(r.left - mArrowX.getWidth() / 2),
+ Math.round(centerY - mArrowX.getHeight() / 2));
+ }
+ if ((mMovingEdges & (MOVE_TOP | MOVE_BLOCK)) != 0) {
+ mArrowY.draw(canvas,
+ Math.round(centerX - mArrowY.getWidth() / 2),
+ Math.round(r.top - mArrowY.getHeight() / 2));
+ }
+ if ((mMovingEdges & (MOVE_BOTTOM | MOVE_BLOCK)) != 0) {
+ mArrowY.draw(canvas,
+ Math.round(centerX - mArrowY.getWidth() / 2),
+ Math.round(r.bottom - mArrowY.getHeight() / 2));
+ }
+ }
+
+ private void drawHighlightRectangle(GLCanvas canvas, RectF r) {
+ GL11 gl = canvas.getGLInstance();
+ gl.glLineWidth(3.0f);
+ gl.glEnable(GL11.GL_LINE_SMOOTH);
+
+ gl.glEnable(GL11.GL_STENCIL_TEST);
+ gl.glClear(GL11.GL_STENCIL_BUFFER_BIT);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE);
+ gl.glStencilFunc(GL11.GL_ALWAYS, 1, 1);
+
+ if (mSpotlightRatioX == 0 || mSpotlightRatioY == 0) {
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), Color.TRANSPARENT);
+ canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint);
+ } else {
+ float sx = r.width() * mSpotlightRatioX;
+ float sy = r.height() * mSpotlightRatioY;
+ float cx = r.centerX();
+ float cy = r.centerY();
+
+ canvas.fillRect(cx - sx / 2, cy - sy / 2, sx, sy, Color.TRANSPARENT);
+ canvas.drawRect(cx - sx / 2, cy - sy / 2, sx, sy, mPaint);
+ canvas.drawRect(r.left, r.top, r.width(), r.height(), mPaint);
+
+ gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_REPLACE);
+
+ canvas.drawRect(cx - sy / 2, cy - sx / 2, sy, sx, mPaint);
+ canvas.fillRect(cx - sy / 2, cy - sx / 2, sy, sx, Color.TRANSPARENT);
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), 0x80000000);
+ }
+
+ gl.glStencilFunc(GL11.GL_NOTEQUAL, 1, 1);
+ gl.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
+
+ canvas.fillRect(0, 0, getWidth(), getHeight(), 0xA0000000);
+
+ gl.glDisable(GL11.GL_STENCIL_TEST);
+ }
+ }
+
+ private class DetectFaceTask extends Thread {
+ private final FaceDetector.Face[] mFaces = new FaceDetector.Face[MAX_FACE_COUNT];
+ private final Bitmap mFaceBitmap;
+ private int mFaceCount;
+
+ public DetectFaceTask(Bitmap bitmap) {
+ mFaceBitmap = bitmap;
+ setName("face-detect");
+ }
+
+ @Override
+ public void run() {
+ Bitmap bitmap = mFaceBitmap;
+ FaceDetector detector = new FaceDetector(
+ bitmap.getWidth(), bitmap.getHeight(), MAX_FACE_COUNT);
+ mFaceCount = detector.findFaces(bitmap, mFaces);
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_UPDATE_FACES, this));
+ }
+
+ private RectF getFaceRect(FaceDetector.Face face) {
+ PointF point = new PointF();
+ face.getMidPoint(point);
+
+ int width = mFaceBitmap.getWidth();
+ int height = mFaceBitmap.getHeight();
+ float rx = face.eyesDistance() * FACE_EYE_RATIO;
+ float ry = rx;
+ float aspect = mAspectRatio;
+ if (aspect != UNSPECIFIED) {
+ if (aspect > 1) {
+ rx = ry * aspect;
+ } else {
+ ry = rx / aspect;
+ }
+ }
+
+ RectF r = new RectF(
+ point.x - rx, point.y - ry, point.x + rx, point.y + ry);
+ r.intersect(0, 0, width, height);
+
+ if (aspect != UNSPECIFIED) {
+ if (r.width() / r.height() > aspect) {
+ float w = r.height() * aspect;
+ r.left = (r.left + r.right - w) * 0.5f;
+ r.right = r.left + w;
+ } else {
+ float h = r.width() / aspect;
+ r.top = (r.top + r.bottom - h) * 0.5f;
+ r.bottom = r.top + h;
+ }
+ }
+
+ r.left /= width;
+ r.right /= width;
+ r.top /= height;
+ r.bottom /= height;
+ return r;
+ }
+
+ public void updateFaces() {
+ if (mFaceCount > 1) {
+ for (int i = 0, n = mFaceCount; i < n; ++i) {
+ mFaceDetectionView.addFace(getFaceRect(mFaces[i]));
+ }
+ mFaceDetectionView.setVisibility(GLView.VISIBLE);
+ Toast.makeText(mActivity.getAndroidContext(),
+ R.string.multiface_crop_help, Toast.LENGTH_SHORT).show();
+ } else if (mFaceCount == 1) {
+ mFaceDetectionView.setVisibility(GLView.INVISIBLE);
+ mHighlightRectangle.setRectangle(getFaceRect(mFaces[0]));
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ } else /*mFaceCount == 0*/ {
+ mHighlightRectangle.setInitRectangle();
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ }
+ }
+ }
+
+ public void setDataModel(TileImageView.Model dataModel, int rotation) {
+ if (((rotation / 90) & 0x01) != 0) {
+ mImageWidth = dataModel.getImageHeight();
+ mImageHeight = dataModel.getImageWidth();
+ } else {
+ mImageWidth = dataModel.getImageWidth();
+ mImageHeight = dataModel.getImageHeight();
+ }
+
+ mImageRotation = rotation;
+
+ mImageView.setModel(dataModel);
+ mAnimation.initialize();
+ }
+
+ public void detectFaces(Bitmap bitmap) {
+ int rotation = mImageRotation;
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ float scale = (float) Math.sqrt(
+ (double) FACE_PIXEL_COUNT / (width * height));
+
+ // faceBitmap is a correctly rotated bitmap, as viewed by a user.
+ Bitmap faceBitmap;
+ if (((rotation / 90) & 1) == 0) {
+ int w = (Math.round(width * scale) & ~1); // must be even
+ int h = Math.round(height * scale);
+ faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565);
+ Canvas canvas = new Canvas(faceBitmap);
+ canvas.rotate(rotation, w / 2, h / 2);
+ canvas.scale((float) w / width, (float) h / height);
+ canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
+ } else {
+ int w = (Math.round(height * scale) & ~1); // must be even
+ int h = Math.round(width * scale);
+ faceBitmap = Bitmap.createBitmap(w, h, Config.RGB_565);
+ Canvas canvas = new Canvas(faceBitmap);
+ canvas.translate(w / 2, h / 2);
+ canvas.rotate(rotation);
+ canvas.translate(-h / 2, -w / 2);
+ canvas.scale((float) w / height, (float) h / width);
+ canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
+ }
+ new DetectFaceTask(faceBitmap).start();
+ }
+
+ public void initializeHighlightRectangle() {
+ mHighlightRectangle.setInitRectangle();
+ mHighlightRectangle.setVisibility(GLView.VISIBLE);
+ }
+
+ public void resume() {
+ mImageView.prepareTextures();
+ }
+
+ public void pause() {
+ mImageView.freeTextures();
+ }
+}
+
diff --git a/src/com/android/gallery3d/ui/CustomMenu.java b/src/com/android/gallery3d/ui/CustomMenu.java
new file mode 100644
index 000000000..de2367e60
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CustomMenu.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+
+import java.util.ArrayList;
+
+public class CustomMenu implements OnMenuItemClickListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterMenu";
+
+ public static class DropDownMenu {
+ private Button mButton;
+ private PopupMenu mPopupMenu;
+ private Menu mMenu;
+
+ public DropDownMenu(Context context, Button button, int menuId,
+ OnMenuItemClickListener listener) {
+ mButton = button;
+ mButton.setBackgroundDrawable(context.getResources().getDrawable(
+ R.drawable.dropdown_normal_holo_dark));
+ mPopupMenu = new PopupMenu(context, mButton);
+ mMenu = mPopupMenu.getMenu();
+ mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
+ mPopupMenu.setOnMenuItemClickListener(listener);
+ mButton.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mPopupMenu.show();
+ }
+ });
+ }
+
+ public MenuItem findItem(int id) {
+ return mMenu.findItem(id);
+ }
+
+ public void setTitle(CharSequence title) {
+ mButton.setText(title);
+ }
+ }
+
+
+
+ private Context mContext;
+ private ArrayList<DropDownMenu> mMenus;
+ private OnMenuItemClickListener mListener;
+
+ public CustomMenu(Context context) {
+ mContext = context;
+ mMenus = new ArrayList<DropDownMenu>();
+ }
+
+ public DropDownMenu addDropDownMenu(Button button, int menuId) {
+ DropDownMenu menu = new DropDownMenu(mContext, button, menuId, this);
+ mMenus.add(menu);
+ return menu;
+ }
+
+ public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+ mListener = listener;
+ }
+
+ public MenuItem findMenuItem(int id) {
+ MenuItem item = null;
+ for (DropDownMenu menu : mMenus) {
+ item = menu.findItem(id);
+ if (item != null) return item;
+ }
+ return item;
+ }
+
+ public void setMenuItemAppliedEnabled(int id, boolean applied, boolean enabled,
+ boolean updateTitle) {
+ MenuItem item = null;
+ for (DropDownMenu menu : mMenus) {
+ item = menu.findItem(id);
+ if (item != null) {
+ item.setCheckable(true);
+ item.setChecked(applied);
+ item.setEnabled(enabled);
+ if (updateTitle) {
+ menu.setTitle(item.getTitle());
+ }
+ }
+ }
+ }
+
+ public void setMenuItemVisibility(int id, boolean visibility) {
+ MenuItem item = findMenuItem(id);
+ if (item != null) {
+ item.setVisible(visibility);
+ }
+ }
+
+ public boolean onMenuItemClick(MenuItem item) {
+ if (mListener != null) {
+ return mListener.onMenuItemClick(item);
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsWindow.java b/src/com/android/gallery3d/ui/DetailsWindow.java
new file mode 100644
index 000000000..03e216922
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsWindow.java
@@ -0,0 +1,451 @@
+/*
+ * 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 static com.android.gallery3d.ui.DetailsWindowConfig.FONT_SIZE;
+import static com.android.gallery3d.ui.DetailsWindowConfig.LEFT_RIGHT_EXTRA_PADDING;
+import static com.android.gallery3d.ui.DetailsWindowConfig.LINE_SPACING;
+import static com.android.gallery3d.ui.DetailsWindowConfig.PREFERRED_WIDTH;
+import static com.android.gallery3d.ui.DetailsWindowConfig.TOP_BOTTOM_EXTRA_PADDING;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ReverseGeocoder;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.location.Address;
+import android.os.Handler;
+import android.os.Message;
+import android.text.format.Formatter;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+
+import java.util.ArrayList;
+import java.util.Map.Entry;
+
+// TODO: Add scroll bar to this window.
+public class DetailsWindow extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "DetailsWindow";
+ private static final int MSG_REFRESH_LOCATION = 1;
+ private static final int FONT_COLOR = Color.WHITE;
+ private static final int CLOSE_BUTTON_SIZE = 32;
+
+ private GalleryActivity mContext;
+ protected Texture mBackground;
+ private StringTexture mTitle;
+ private MyDataModel mModel;
+ private MediaDetails mDetails;
+ private DetailsSource mSource;
+ private int mIndex;
+ private int mLocationIndex;
+ private Future<Address> mAddressLookupJob;
+ private Handler mHandler;
+ private Icon mCloseButton;
+ private int mMaxDetailLength;
+ private CloseListener mListener;
+
+ private ScrollView mScrollView;
+ private DetailsPanel mDetailPanel = new DetailsPanel();
+
+ public interface DetailsSource {
+ public int size();
+ public int findIndex(int indexHint);
+ public MediaDetails getDetails();
+ }
+
+ public interface CloseListener {
+ public void onClose();
+ }
+
+ public DetailsWindow(GalleryActivity activity, DetailsSource source) {
+ mContext = activity;
+ mSource = source;
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_REFRESH_LOCATION:
+ mModel.updateLocation((Address) msg.obj);
+ invalidate();
+ break;
+ }
+ }
+ };
+ Context context = activity.getAndroidContext();
+ ResourceTexture icon = new ResourceTexture(context, R.drawable.ic_menu_cancel_holo_light);
+ setBackground(new NinePatchTexture(context, R.drawable.popup_full_dark));
+
+ mCloseButton = new Icon(context, icon, CLOSE_BUTTON_SIZE, CLOSE_BUTTON_SIZE) {
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_UP:
+ if (mListener != null) mListener.onClose();
+ }
+ return true;
+ }
+ };
+ mScrollView = new ScrollView(context);
+ mScrollView.addComponent(mDetailPanel);
+
+ super.addComponent(mScrollView);
+ super.addComponent(mCloseButton);
+
+ reloadDetails(0);
+ }
+
+ public void setCloseListener(CloseListener listener) {
+ mListener = listener;
+ }
+
+ public void setBackground(Texture background) {
+ if (background == mBackground) return;
+ mBackground = background;
+ if (background != null && background instanceof NinePatchTexture) {
+ Rect p = ((NinePatchTexture) mBackground).getPaddings();
+ p.left += LEFT_RIGHT_EXTRA_PADDING;
+ p.right += LEFT_RIGHT_EXTRA_PADDING;
+ p.top += TOP_BOTTOM_EXTRA_PADDING;
+ p.bottom += TOP_BOTTOM_EXTRA_PADDING;
+ setPaddings(p);
+ } else {
+ setPaddings(0, 0, 0, 0);
+ }
+ Rect p = getPaddings();
+ mMaxDetailLength = PREFERRED_WIDTH - p.left - p.right;
+ invalidate();
+ }
+
+ public void setTitle(String title) {
+ mTitle = StringTexture.newInstance(title, FONT_SIZE, FONT_COLOR);
+ }
+
+ @Override
+ protected void renderBackground(GLCanvas canvas) {
+ if (mBackground == null) return;
+ int width = getWidth();
+ int height = getHeight();
+
+ //TODO: change alpha in the background image.
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.setAlpha(0.7f);
+ mBackground.draw(canvas, 0, 0, width, height);
+ canvas.restore();
+
+ Rect p = getPaddings();
+ if (mTitle != null) mTitle.draw(canvas, p.left, p.top);
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int height = MeasureSpec.getSize(heightSpec);
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(PREFERRED_WIDTH, height)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean sizeChange, int l, int t, int r, int b) {
+ mCloseButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ int bWidth = mCloseButton.getMeasuredWidth();
+ int bHeight = mCloseButton.getMeasuredHeight();
+ int width = getWidth();
+ int height = getHeight();
+
+ Rect p = getPaddings();
+ mCloseButton.layout(width - p.right - bWidth, p.top,
+ width - p.right, p.top + bHeight);
+ mScrollView.layout(p.left, p.top + bHeight, width - p.right,
+ height - p.bottom);
+ }
+
+ public void show() {
+ setVisibility(GLView.VISIBLE);
+ requestLayout();
+ }
+
+ public void hide() {
+ setVisibility(GLView.INVISIBLE);
+ requestLayout();
+ }
+
+ public void pause() {
+ Future<Address> lookupJob = mAddressLookupJob;
+ if (lookupJob != null) {
+ lookupJob.cancel();
+ lookupJob.waitDone();
+ }
+ }
+
+ public void reloadDetails(int indexHint) {
+ int index = mSource.findIndex(indexHint);
+ if (index == -1) return;
+ MediaDetails details = mSource.getDetails();
+ if (details != null) {
+ if (mIndex == index && mDetails == details) return;
+ mIndex = index;
+ mDetails = details;
+ setDetails(details);
+ }
+ mDetailPanel.requestLayout();
+ }
+
+ private void setDetails(MediaDetails details) {
+ mModel = new MyDataModel(details);
+ invalidate();
+ }
+
+ private class AddressLookupJob implements Job<Address> {
+ double[] mLatlng;
+ protected AddressLookupJob(double[] latlng) {
+ mLatlng = latlng;
+ }
+
+ public Address run(JobContext jc) {
+ ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext());
+ return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true);
+ }
+ }
+
+ private class MyDataModel {
+ ArrayList<Texture> mItems;
+
+ public MyDataModel(MediaDetails details) {
+ Context context = mContext.getAndroidContext();
+ mLocationIndex = -1;
+ mItems = new ArrayList<Texture>(details.size());
+ setTitle(String.format(context.getString(R.string.sequence_in_set),
+ mIndex + 1, mSource.size()));
+ setDetails(context, details);
+ }
+
+ private void setDetails(Context context, MediaDetails details) {
+ for (Entry<Integer, Object> detail : details) {
+ String value;
+ switch (detail.getKey()) {
+ case MediaDetails.INDEX_LOCATION: {
+ value = getLocationText((double[]) detail.getValue());
+ break;
+ }
+ case MediaDetails.INDEX_SIZE: {
+ value = Formatter.formatFileSize(
+ context, (Long) detail.getValue());
+ break;
+ }
+ case MediaDetails.INDEX_WHITE_BALANCE: {
+ value = "1".equals(detail.getValue())
+ ? context.getString(R.string.manual)
+ : context.getString(R.string.auto);
+ break;
+ }
+ case MediaDetails.INDEX_FLASH: {
+ MediaDetails.FlashState flash =
+ (MediaDetails.FlashState) detail.getValue();
+ // TODO: camera doesn't fill in the complete values, show more information
+ // when it is fixed.
+ if (flash.isFlashFired()) {
+ value = context.getString(R.string.flash_on);
+ } else {
+ value = context.getString(R.string.flash_off);
+ }
+ break;
+ }
+ case MediaDetails.INDEX_EXPOSURE_TIME: {
+ value = (String) detail.getValue();
+ double time = Double.valueOf(value);
+ if (time < 1.0f) {
+ value = String.format("1/%d", (int) (0.5f + 1 / time));
+ } else {
+ int integer = (int) time;
+ time -= integer;
+ value = String.valueOf(integer) + "''";
+ if (time > 0.0001) {
+ value += String.format(" 1/%d", (int) (0.5f + 1 / time));
+ }
+ }
+ break;
+ }
+ default: {
+ Object valueObj = detail.getValue();
+ // This shouldn't happen, log its key to help us diagnose the problem.
+ Utils.assertTrue(valueObj != null, "%s's value is Null",
+ getName(context, detail.getKey()));
+ value = valueObj.toString();
+ }
+ }
+ int key = detail.getKey();
+ if (details.hasUnit(key)) {
+ value = String.format("%s : %s %s", getName(context, key), value,
+ context.getString(details.getUnit(key)));
+ } else {
+ value = String.format("%s : %s", getName(context, key), value);
+ }
+ Texture label = MultiLineTexture.newInstance(
+ value, mMaxDetailLength, FONT_SIZE, FONT_COLOR);
+ mItems.add(label);
+ }
+ }
+
+ private String getLocationText(double[] latlng) {
+ String text = String.format("(%f, %f)", latlng[0], latlng[1]);
+ mAddressLookupJob = mContext.getThreadPool().submit(
+ new AddressLookupJob(latlng),
+ new FutureListener<Address>() {
+ public void onFutureDone(Future<Address> future) {
+ mAddressLookupJob = null;
+ if (!future.isCancelled()) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_REFRESH_LOCATION, future.get()));
+ }
+ }
+ });
+ mLocationIndex = mItems.size();
+ return text;
+ }
+
+ public void updateLocation(Address address) {
+ int index = mLocationIndex;
+ if (address != null && index >=0 && index < mItems.size()) {
+ Context context = mContext.getAndroidContext();
+ String parts[] = {
+ address.getAdminArea(),
+ address.getSubAdminArea(),
+ address.getLocality(),
+ address.getSubLocality(),
+ address.getThoroughfare(),
+ address.getSubThoroughfare(),
+ address.getPremises(),
+ address.getPostalCode(),
+ address.getCountryName()
+ };
+
+ String addressText = "";
+ for (int i = 0; i < parts.length; i++) {
+ if (parts[i] == null || parts[i].isEmpty()) continue;
+ if (!addressText.isEmpty()) {
+ addressText += ", ";
+ }
+ addressText += parts[i];
+ }
+ String text = String.format("%s : %s", getName(context,
+ MediaDetails.INDEX_LOCATION), addressText);
+ mItems.set(index, MultiLineTexture.newInstance(
+ text, mMaxDetailLength, FONT_SIZE, FONT_COLOR));
+ }
+ }
+
+ public Texture getView(int index) {
+ return mItems.get(index);
+ }
+
+ public int size() {
+ return mItems.size();
+ }
+ }
+
+ private static String getName(Context context, int key) {
+ switch (key) {
+ case MediaDetails.INDEX_TITLE:
+ return context.getString(R.string.title);
+ case MediaDetails.INDEX_DESCRIPTION:
+ return context.getString(R.string.description);
+ case MediaDetails.INDEX_DATETIME:
+ return context.getString(R.string.time);
+ case MediaDetails.INDEX_LOCATION:
+ return context.getString(R.string.location);
+ case MediaDetails.INDEX_PATH:
+ return context.getString(R.string.path);
+ case MediaDetails.INDEX_WIDTH:
+ return context.getString(R.string.width);
+ case MediaDetails.INDEX_HEIGHT:
+ return context.getString(R.string.height);
+ case MediaDetails.INDEX_ORIENTATION:
+ return context.getString(R.string.orientation);
+ case MediaDetails.INDEX_DURATION:
+ return context.getString(R.string.duration);
+ case MediaDetails.INDEX_MIMETYPE:
+ return context.getString(R.string.mimetype);
+ case MediaDetails.INDEX_SIZE:
+ return context.getString(R.string.file_size);
+ case MediaDetails.INDEX_MAKE:
+ return context.getString(R.string.maker);
+ case MediaDetails.INDEX_MODEL:
+ return context.getString(R.string.model);
+ case MediaDetails.INDEX_FLASH:
+ return context.getString(R.string.flash);
+ case MediaDetails.INDEX_APERTURE:
+ return context.getString(R.string.aperture);
+ case MediaDetails.INDEX_FOCAL_LENGTH:
+ return context.getString(R.string.focal_length);
+ case MediaDetails.INDEX_WHITE_BALANCE:
+ return context.getString(R.string.white_balance);
+ case MediaDetails.INDEX_EXPOSURE_TIME:
+ return context.getString(R.string.exposure_time);
+ case MediaDetails.INDEX_ISO:
+ return context.getString(R.string.iso);
+ default:
+ return "Unknown key" + key;
+ }
+ }
+
+ private class DetailsPanel extends GLView {
+
+ @Override
+ public void onMeasure(int widthSpec, int heightSpec) {
+ if (mTitle == null || mModel == null) {
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(PREFERRED_WIDTH, 0)
+ .measure(widthSpec, heightSpec);
+ return;
+ }
+
+ int h = getPaddings().top + LINE_SPACING;
+ for (int i = 0, n = mModel.size(); i < n; ++i) {
+ h += mModel.getView(i).getHeight() + LINE_SPACING;
+ }
+
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(PREFERRED_WIDTH, h)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+
+ if (mTitle == null || mModel == null) {
+ return;
+ }
+ Rect p = getPaddings();
+ int x = p.left, y = p.top + LINE_SPACING;
+ for (int i = 0, n = mModel.size(); i < n ; i++) {
+ Texture t = mModel.getView(i);
+ t.draw(canvas, x, y);
+ y += t.getHeight() + LINE_SPACING;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DisplayItem.java b/src/com/android/gallery3d/ui/DisplayItem.java
new file mode 100644
index 000000000..3038232f6
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DisplayItem.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+public abstract class DisplayItem {
+
+ protected int mWidth;
+ protected int mHeight;
+
+ protected void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ // returns true if more pass is needed
+ public abstract boolean render(GLCanvas canvas, int pass);
+
+ public abstract long getIdentity();
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getRotation() {
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java
new file mode 100644
index 000000000..19db77262
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DownUpDetector.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.view.MotionEvent;
+
+public class DownUpDetector {
+ public interface DownUpListener {
+ void onDown(MotionEvent e);
+ void onUp(MotionEvent e);
+ }
+
+ private boolean mStillDown;
+ private DownUpListener mListener;
+
+ public DownUpDetector(DownUpListener listener) {
+ mListener = listener;
+ }
+
+ private void setState(boolean down, MotionEvent e) {
+ if (down == mStillDown) return;
+ mStillDown = down;
+ if (down) {
+ mListener.onDown(e);
+ } else {
+ mListener.onUp(e);
+ }
+ }
+
+ public void onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction() & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN:
+ setState(true, ev);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort.
+ setState(false, ev);
+ break;
+ }
+ }
+
+ public boolean isDown() {
+ return mStillDown;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DrawableTexture.java b/src/com/android/gallery3d/ui/DrawableTexture.java
new file mode 100644
index 000000000..5c3964d5c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DrawableTexture.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+
+// DrawableTexture is a texture whose content is from a Drawable.
+public class DrawableTexture extends CanvasTexture {
+
+ private final Drawable mDrawable;
+
+ public DrawableTexture(Drawable drawable, int width, int height) {
+ super(width, height);
+ mDrawable = drawable;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas, Bitmap backing) {
+ mDrawable.setBounds(0, 0, mWidth, mHeight);
+ mDrawable.draw(canvas);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/FilmStripView.java b/src/com/android/gallery3d/ui/FilmStripView.java
new file mode 100644
index 000000000..8d28f2c7b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FilmStripView.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.AlphaAnimation;
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.app.AlbumDataAdapter;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.MediaSet;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+
+public class FilmStripView extends GLView implements SlotView.Listener,
+ ScrollBarView.Listener, UserInteractionListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilmStripView";
+
+ private static final int HIDE_ANIMATION_DURATION = 300; // 0.3 sec
+
+ public interface Listener {
+ void onSlotSelected(int slotIndex);
+ }
+
+ private int mTopMargin, mMidMargin, mBottomMargin;
+ private int mContentSize, mBarSize, mGripSize;
+ private AlbumView mAlbumView;
+ private ScrollBarView mScrollBarView;
+ private AlbumDataAdapter mAlbumDataAdapter;
+ private StripDrawer mStripDrawer;
+ private Listener mListener;
+ private UserInteractionListener mUIListener;
+ private boolean mFilmStripVisible;
+ private CanvasAnimation mFilmStripAnimation;
+ private NinePatchTexture mBackgroundTexture;
+
+ // The layout of FileStripView is
+ // topMargin
+ // ----+----+
+ // / +----+--\
+ // contentSize | | thumbSize
+ // \ +----+--/
+ // ----+----+
+ // midMargin
+ // ----+----+
+ // / +----+--\
+ // barSize | | gripSize
+ // \ +----+--/
+ // ----+----+
+ // bottomMargin
+ public FilmStripView(GalleryActivity activity, MediaSet mediaSet,
+ int topMargin, int midMargin, int bottomMargin, int contentSize,
+ int thumbSize, int barSize, int gripSize, int gripWidth) {
+ mTopMargin = topMargin;
+ mMidMargin = midMargin;
+ mBottomMargin = bottomMargin;
+ mContentSize = contentSize;
+ mBarSize = barSize;
+ mGripSize = gripSize;
+
+ mStripDrawer = new StripDrawer((Context) activity);
+ mAlbumView = new AlbumView(activity, thumbSize, thumbSize, thumbSize);
+ mAlbumView.setOverscrollEffect(SlotView.OVERSCROLL_SYSTEM);
+ mAlbumView.setSelectionDrawer(mStripDrawer);
+ mAlbumView.setListener(this);
+ mAlbumView.setUserInteractionListener(this);
+ mAlbumDataAdapter = new AlbumDataAdapter(activity, mediaSet);
+ addComponent(mAlbumView);
+ mScrollBarView = new ScrollBarView(activity.getAndroidContext(),
+ mGripSize, gripWidth);
+ mScrollBarView.setListener(this);
+ addComponent(mScrollBarView);
+
+ mAlbumView.setModel(mAlbumDataAdapter);
+ mBackgroundTexture = new NinePatchTexture(activity.getAndroidContext(),
+ R.drawable.navstrip_translucent);
+ mFilmStripVisible = true;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void setUserInteractionListener(UserInteractionListener listener) {
+ mUIListener = listener;
+ }
+
+ private void setFilmStripVisible(boolean visible) {
+ if (mFilmStripVisible == visible) return;
+ mFilmStripVisible = visible;
+ if (!visible) {
+ mFilmStripAnimation = new AlphaAnimation(1, 0);
+ mFilmStripAnimation.setDuration(HIDE_ANIMATION_DURATION);
+ mFilmStripAnimation.start();
+ } else {
+ mFilmStripAnimation = null;
+ }
+ invalidate();
+ }
+
+ public void show() {
+ setFilmStripVisible(true);
+ }
+
+ public void hide() {
+ setFilmStripVisible(false);
+ }
+
+ @Override
+ protected void onVisibilityChanged(int visibility) {
+ super.onVisibilityChanged(visibility);
+ if (visibility == GLView.VISIBLE) {
+ onUserInteraction();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int height = mTopMargin + mContentSize + mMidMargin + mBarSize + mBottomMargin;
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(MeasureSpec.getSize(widthSpec), height)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (!changed) return;
+ mAlbumView.layout(0, mTopMargin, right - left, mTopMargin + mContentSize);
+ int barStart = mTopMargin + mContentSize + mMidMargin;
+ mScrollBarView.layout(0, barStart, right - left, barStart + mBarSize);
+ int width = right - left;
+ int height = bottom - top;
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ // consume all touch events on the "gray area", so they don't go to
+ // the photo view below. (otherwise you can scroll the picture through
+ // it).
+ return true;
+ }
+
+ @Override
+ protected boolean dispatchTouchEvent(MotionEvent event) {
+ if (!mFilmStripVisible && mFilmStripAnimation == null) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_MOVE:
+ onUserInteractionBegin();
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ onUserInteractionEnd();
+ break;
+ }
+
+ return super.dispatchTouchEvent(event);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ CanvasAnimation anim = mFilmStripAnimation;
+ if (anim == null && !mFilmStripVisible) return;
+
+ boolean needRestore = false;
+ if (anim != null) {
+ needRestore = true;
+ canvas.save(anim.getCanvasSaveFlags());
+ long now = canvas.currentAnimationTimeMillis();
+ boolean more = anim.calculate(now);
+ anim.apply(canvas);
+ if (more) {
+ invalidate();
+ } else {
+ mFilmStripAnimation = null;
+ }
+ }
+
+ mBackgroundTexture.draw(canvas, 0, 0, getWidth(), getHeight());
+ super.render(canvas);
+
+ if (needRestore) {
+ canvas.restore();
+ }
+ }
+
+ // Called by AlbumView
+ public void onSingleTapUp(int slotIndex) {
+ mAlbumView.setFocusIndex(slotIndex);
+ mListener.onSlotSelected(slotIndex);
+ }
+
+ // Called by AlbumView
+ public void onLongTap(int slotIndex) {
+ onSingleTapUp(slotIndex);
+ }
+
+ // Called by AlbumView
+ public void onUserInteractionBegin() {
+ mUIListener.onUserInteractionBegin();
+ }
+
+ // Called by AlbumView
+ public void onUserInteractionEnd() {
+ mUIListener.onUserInteractionEnd();
+ }
+
+ // Called by AlbumView
+ public void onUserInteraction() {
+ mUIListener.onUserInteraction();
+ }
+
+ // Called by AlbumView
+ public void onScrollPositionChanged(int position, int total) {
+ mScrollBarView.setContentPosition(position, total);
+ }
+
+ // Called by ScrollBarView
+ public void onScrollBarPositionChanged(int position) {
+ mAlbumView.setScrollPosition(position);
+ }
+
+ public void setFocusIndex(int slotIndex) {
+ mAlbumView.setFocusIndex(slotIndex);
+ mAlbumView.makeSlotVisible(slotIndex);
+ }
+
+ public void setStartIndex(int slotIndex) {
+ mAlbumView.setStartIndex(slotIndex);
+ }
+
+ public void pause() {
+ mAlbumView.pause();
+ mAlbumDataAdapter.pause();
+ }
+
+ public void resume() {
+ mAlbumView.resume();
+ mAlbumDataAdapter.resume();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLCanvas.java b/src/com/android/gallery3d/ui/GLCanvas.java
new file mode 100644
index 000000000..88c02f3b5
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLCanvas.java
@@ -0,0 +1,138 @@
+/*
+ * 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.RectF;
+
+import javax.microedition.khronos.opengles.GL11;
+
+//
+// GLCanvas gives a convenient interface to draw using OpenGL.
+//
+// When a rectangle is specified in this interface, it means the region
+// [x, x+width) * [y, y+height)
+//
+public interface GLCanvas {
+ // Tells GLCanvas the size of the underlying GL surface. This should be
+ // called before first drawing and when the size of GL surface is changed.
+ // This is called by GLRoot and should not be called by the clients
+ // who only want to draw on the GLCanvas. Both width and height must be
+ // nonnegative.
+ public void setSize(int width, int height);
+
+ // Clear the drawing buffers. This should only be used by GLRoot.
+ public void clearBuffer();
+
+ // This is the time value used to calculate the animation in the current
+ // frame. The "set" function should only called by GLRoot, and the
+ // "time" parameter must be nonnegative.
+ public void setCurrentAnimationTimeMillis(long time);
+ public long currentAnimationTimeMillis();
+
+ public void setBlendEnabled(boolean enabled);
+
+ // Sets and gets the current alpha, alpha must be in [0, 1].
+ public void setAlpha(float alpha);
+ public float getAlpha();
+
+ // (current alpha) = (current alpha) * alpha
+ public void multiplyAlpha(float alpha);
+
+ // Change the current transform matrix.
+ public void translate(float x, float y, float z);
+ public void scale(float sx, float sy, float sz);
+ public void rotate(float angle, float x, float y, float z);
+ public void multiplyMatrix(float[] mMatrix, int offset);
+
+ // Modifies the current clip with the specified rectangle.
+ // (current clip) = (current clip) intersect (specified rectangle).
+ // Returns true if the result clip is non-empty.
+ public boolean clipRect(int left, int top, int right, int bottom);
+
+ // Pushes the configuration state (matrix, alpha, and clip) onto
+ // a private stack.
+ public int save();
+
+ // Same as save(), but only save those specified in saveFlags.
+ public int save(int saveFlags);
+
+ public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
+ public static final int SAVE_FLAG_CLIP = 0x01;
+ public static final int SAVE_FLAG_ALPHA = 0x02;
+ public static final int SAVE_FLAG_MATRIX = 0x04;
+
+ // Pops from the top of the stack as current configuration state (matrix,
+ // alpha, and clip). This call balances a previous call to save(), and is
+ // used to remove all modifications to the configuration state since the
+ // last save call.
+ public void restore();
+
+ // Draws a line using the specified paint from (x1, y1) to (x2, y2).
+ // (Both end points are included).
+ public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint);
+
+ // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2).
+ // (Both end points are included).
+ public void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);
+
+ // Fills the specified rectangle with the specified color.
+ public void fillRect(float x, float y, float width, float height, int color);
+
+ // Draws a texture to the specified rectangle.
+ public void drawTexture(
+ BasicTexture texture, int x, int y, int width, int height);
+ public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+ int uvBuffer, int indexBuffer, int indexCount);
+
+ // Draws a texture to the specified rectangle. The "alpha" parameter
+ // overrides the current drawing alpha value.
+ public void drawTexture(BasicTexture texture,
+ int x, int y, int width, int height, float alpha);
+
+ // Draws a the source rectangle part of the texture to the target rectangle.
+ public void drawTexture(BasicTexture texture, RectF source, RectF target);
+
+ // Draw two textures to the specified rectangle. The actual texture used is
+ // from * (1 - ratio) + to * ratio
+ // The two textures must have the same size.
+ public void drawMixed(BasicTexture from, BasicTexture to,
+ float ratio, int x, int y, int w, int h);
+
+ public void drawMixed(BasicTexture from, int toColor,
+ float ratio, int x, int y, int w, int h);
+
+ // Return a texture copied from the specified rectangle.
+ public BasicTexture copyTexture(int x, int y, int width, int height);
+
+ // Gets the underlying GL instance. This is used only when direct access to
+ // GL is needed.
+ public GL11 getGLInstance();
+
+ // Unloads the specified texture from the canvas. The resource allocated
+ // to draw the texture will be released. The specified texture will return
+ // to the unloaded state. This function should be called only from
+ // BasicTexture or its descendant
+ public boolean unloadTexture(BasicTexture texture);
+
+ // Delete the specified buffer object, similar to unloadTexture.
+ public void deleteBuffer(int bufferId);
+
+ // Delete the textures and buffers in GL side. This function should only be
+ // called in the GL thread.
+ public void deleteRecycledResources();
+
+}
diff --git a/src/com/android/gallery3d/ui/GLCanvasImpl.java b/src/com/android/gallery3d/ui/GLCanvasImpl.java
new file mode 100644
index 000000000..387743f5d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLCanvasImpl.java
@@ -0,0 +1,913 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.util.IntArray;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLU;
+import android.opengl.Matrix;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.Stack;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+public class GLCanvasImpl implements GLCanvas {
+ @SuppressWarnings("unused")
+ private static final String TAG = "GLCanvasImp";
+
+ private static final float OPAQUE_ALPHA = 0.95f;
+
+ private static final int OFFSET_FILL_RECT = 0;
+ private static final int OFFSET_DRAW_LINE = 4;
+ private static final int OFFSET_DRAW_RECT = 6;
+ private static final float[] BOX_COORDINATES = {
+ 0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle
+ 0, 0, 1, 1, // used for drawing a line
+ 0, 0, 0, 1, 1, 1, 1, 0}; // used for drawing the outline of a rectangle
+
+ private final GL11 mGL;
+
+ private final float mMatrixValues[] = new float[16];
+ private final float mTextureMatrixValues[] = new float[16];
+
+ // mapPoints needs 10 input and output numbers.
+ private final float mMapPointsBuffer[] = new float[10];
+
+ private final float mTextureColor[] = new float[4];
+
+ private int mBoxCoords;
+
+ private final GLState mGLState;
+
+ private long mAnimationTime;
+
+ private float mAlpha;
+ private final Rect mClipRect = new Rect();
+ private final Stack<ConfigState> mRestoreStack =
+ new Stack<ConfigState>();
+ private ConfigState mRecycledRestoreAction;
+
+ private final RectF mDrawTextureSourceRect = new RectF();
+ private final RectF mDrawTextureTargetRect = new RectF();
+ private final float[] mTempMatrix = new float[32];
+ private final IntArray mUnboundTextures = new IntArray();
+ private final IntArray mDeleteBuffers = new IntArray();
+ private int mHeight;
+ private boolean mBlendEnabled = true;
+
+ // Drawing statistics
+ int mCountDrawLine;
+ int mCountFillRect;
+ int mCountDrawMesh;
+ int mCountTextureRect;
+ int mCountTextureOES;
+
+ GLCanvasImpl(GL11 gl) {
+ mGL = gl;
+ mGLState = new GLState(gl);
+ initialize();
+ }
+
+ public void setSize(int width, int height) {
+ Utils.assertTrue(width >= 0 && height >= 0);
+ mHeight = height;
+
+ GL11 gl = mGL;
+ gl.glViewport(0, 0, width, height);
+ gl.glMatrixMode(GL11.GL_PROJECTION);
+ gl.glLoadIdentity();
+ GLU.gluOrtho2D(gl, 0, width, 0, height);
+
+ gl.glMatrixMode(GL11.GL_MODELVIEW);
+ gl.glLoadIdentity();
+ float matrix[] = mMatrixValues;
+
+ Matrix.setIdentityM(matrix, 0);
+ Matrix.translateM(matrix, 0, 0, mHeight, 0);
+ Matrix.scaleM(matrix, 0, 1, -1, 1);
+
+ mClipRect.set(0, 0, width, height);
+ gl.glScissor(0, 0, width, height);
+ }
+
+ public long currentAnimationTimeMillis() {
+ return mAnimationTime;
+ }
+
+ public void setAlpha(float alpha) {
+ Utils.assertTrue(alpha >= 0 && alpha <= 1);
+ mAlpha = alpha;
+ }
+
+ public void multiplyAlpha(float alpha) {
+ Utils.assertTrue(alpha >= 0 && alpha <= 1);
+ mAlpha *= alpha;
+ }
+
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+ return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ }
+
+ private void initialize() {
+ GL11 gl = mGL;
+
+ // First create an nio buffer, then create a VBO from it.
+ int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE;
+ FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);
+
+ int[] name = new int[1];
+ gl.glGenBuffers(1, name, 0);
+ mBoxCoords = name[0];
+
+ gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
+ gl.glBufferData(GL11.GL_ARRAY_BUFFER,
+ xyBuffer.capacity() * (Float.SIZE / Byte.SIZE),
+ xyBuffer, GL11.GL_STATIC_DRAW);
+
+ gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+ gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ // Enable the texture coordinate array for Texture 1
+ gl.glClientActiveTexture(GL11.GL_TEXTURE1);
+ gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+ gl.glClientActiveTexture(GL11.GL_TEXTURE0);
+ gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
+
+ // mMatrixValues will be initialized in setSize()
+ mAlpha = 1.0f;
+ }
+
+ public void drawRect(float x, float y, float width, float height, GLPaint paint) {
+ GL11 gl = mGL;
+
+ mGLState.setColorMode(paint.getColor(), mAlpha);
+ mGLState.setLineWidth(paint.getLineWidth());
+ mGLState.setLineSmooth(paint.getAntiAlias());
+
+ saveTransform();
+ translate(x, y, 0);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);
+
+ restoreTransform();
+ mCountDrawLine++;
+ }
+
+ public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
+ GL11 gl = mGL;
+
+ mGLState.setColorMode(paint.getColor(), mAlpha);
+ mGLState.setLineWidth(paint.getLineWidth());
+ mGLState.setLineSmooth(paint.getAntiAlias());
+
+ saveTransform();
+ translate(x1, y1, 0);
+ scale(x2 - x1, y2 - y1, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2);
+
+ restoreTransform();
+ mCountDrawLine++;
+ }
+
+ public void fillRect(float x, float y, float width, float height, int color) {
+ mGLState.setColorMode(color, mAlpha);
+ GL11 gl = mGL;
+
+ saveTransform();
+ translate(x, y, 0);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+ restoreTransform();
+ mCountFillRect++;
+ }
+
+ public void translate(float x, float y, float z) {
+ Matrix.translateM(mMatrixValues, 0, x, y, z);
+ }
+
+ public void scale(float sx, float sy, float sz) {
+ Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
+ }
+
+ public void rotate(float angle, float x, float y, float z) {
+ float[] temp = mTempMatrix;
+ Matrix.setRotateM(temp, 0, angle, x, y, z);
+ Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);
+ System.arraycopy(temp, 16, mMatrixValues, 0, 16);
+ }
+
+ public void multiplyMatrix(float matrix[], int offset) {
+ float[] temp = mTempMatrix;
+ Matrix.multiplyMM(temp, 0, mMatrixValues , 0, matrix, 0);
+ System.arraycopy(temp, 0, mMatrixValues, 0, 16);
+ }
+
+ private void textureRect(float x, float y, float width, float height) {
+ GL11 gl = mGL;
+
+ saveTransform();
+ translate(x, y, 0);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+ restoreTransform();
+ mCountTextureRect++;
+ }
+
+ public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+ int uvBuffer, int indexBuffer, int indexCount) {
+ float alpha = mAlpha;
+ if (!bindTexture(tex)) return;
+
+ mGLState.setBlendEnabled(mBlendEnabled
+ && (!tex.isOpaque() || alpha < OPAQUE_ALPHA));
+ mGLState.setTextureAlpha(alpha);
+
+ // Reset the texture matrix. We will set our own texture coordinates
+ // below.
+ setTextureCoords(0, 0, 1, 1);
+
+ saveTransform();
+ translate(x, y, 0);
+
+ mGL.glLoadMatrixf(mMatrixValues, 0);
+
+ mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer);
+ mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer);
+ mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+ mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP,
+ indexCount, GL11.GL_UNSIGNED_BYTE, 0);
+
+ mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
+ mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);
+ mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);
+
+ restoreTransform();
+ mCountDrawMesh++;
+ }
+
+ private float[] mapPoints(float matrix[], int x1, int y1, int x2, int y2) {
+ float[] point = mMapPointsBuffer;
+ int srcOffset = 6;
+ point[srcOffset] = x1;
+ point[srcOffset + 1] = y1;
+ point[srcOffset + 2] = 0;
+ point[srcOffset + 3] = 1;
+
+ int resultOffset = 0;
+ Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset);
+ point[resultOffset] /= point[resultOffset + 3];
+ point[resultOffset + 1] /= point[resultOffset + 3];
+
+ // map the second point
+ point[srcOffset] = x2;
+ point[srcOffset + 1] = y2;
+ resultOffset = 2;
+ Matrix.multiplyMV(point, resultOffset, matrix, 0, point, srcOffset);
+ point[resultOffset] /= point[resultOffset + 3];
+ point[resultOffset + 1] /= point[resultOffset + 3];
+
+ return point;
+ }
+
+ public boolean clipRect(int left, int top, int right, int bottom) {
+ float point[] = mapPoints(mMatrixValues, left, top, right, bottom);
+
+ // mMatrix could be a rotation matrix. In this case, we need to find
+ // the boundaries after rotation. (only handle 90 * n degrees)
+ if (point[0] > point[2]) {
+ left = (int) point[2];
+ right = (int) point[0];
+ } else {
+ left = (int) point[0];
+ right = (int) point[2];
+ }
+ if (point[1] > point[3]) {
+ top = (int) point[3];
+ bottom = (int) point[1];
+ } else {
+ top = (int) point[1];
+ bottom = (int) point[3];
+ }
+ Rect clip = mClipRect;
+
+ boolean intersect = clip.intersect(left, top, right, bottom);
+ if (!intersect) clip.set(0, 0, 0, 0);
+ mGL.glScissor(clip.left, clip.top, clip.width(), clip.height());
+ return intersect;
+ }
+
+ private void drawBoundTexture(
+ BasicTexture texture, int x, int y, int width, int height) {
+ // Test whether it has been rotated or flipped, if so, glDrawTexiOES
+ // won't work
+ if (isMatrixRotatedOrFlipped(mMatrixValues)) {
+ setTextureCoords(0, 0,
+ (float) texture.getWidth() / texture.getTextureWidth(),
+ (float) texture.getHeight() / texture.getTextureHeight());
+ textureRect(x, y, width, height);
+ } else {
+ // draw the rect from bottom-left to top-right
+ float points[] = mapPoints(
+ mMatrixValues, x, y + height, x + width, y);
+ x = Math.round(points[0]);
+ y = Math.round(points[1]);
+ width = Math.round(points[2]) - x;
+ height = Math.round(points[3]) - y;
+ if (width > 0 && height > 0) {
+ ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);
+ mCountTextureOES++;
+ }
+ }
+ }
+
+ public void drawTexture(
+ BasicTexture texture, int x, int y, int width, int height) {
+ drawTexture(texture, x, y, width, height, mAlpha);
+ }
+
+ public void setBlendEnabled(boolean enabled) {
+ mBlendEnabled = enabled;
+ }
+
+ public void drawTexture(BasicTexture texture,
+ int x, int y, int width, int height, float alpha) {
+ if (width <= 0 || height <= 0) return;
+
+ mGLState.setBlendEnabled(mBlendEnabled
+ && (!texture.isOpaque() || alpha < OPAQUE_ALPHA));
+ if (!bindTexture(texture)) return;
+ mGLState.setTextureAlpha(alpha);
+ drawBoundTexture(texture, x, y, width, height);
+ }
+
+ public void drawTexture(BasicTexture texture, RectF source, RectF target) {
+ if (target.width() <= 0 || target.height() <= 0) return;
+
+ // Copy the input to avoid changing it.
+ mDrawTextureSourceRect.set(source);
+ mDrawTextureTargetRect.set(target);
+ source = mDrawTextureSourceRect;
+ target = mDrawTextureTargetRect;
+
+ mGLState.setBlendEnabled(mBlendEnabled
+ && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
+ if (!bindTexture(texture)) return;
+ convertCoordinate(source, target, texture);
+ setTextureCoords(source);
+ mGLState.setTextureAlpha(mAlpha);
+ textureRect(target.left, target.top, target.width(), target.height());
+ }
+
+ // This function changes the source coordinate to the texture coordinates.
+ // It also clips the source and target coordinates if it is beyond the
+ // bound of the texture.
+ private void convertCoordinate(RectF source, RectF target,
+ BasicTexture texture) {
+
+ int width = texture.getWidth();
+ int height = texture.getHeight();
+ int texWidth = texture.getTextureWidth();
+ int texHeight = texture.getTextureHeight();
+ // Convert to texture coordinates
+ source.left /= texWidth;
+ source.right /= texWidth;
+ source.top /= texHeight;
+ source.bottom /= texHeight;
+
+ // Clip if the rendering range is beyond the bound of the texture.
+ float xBound = (float) width / texWidth;
+ if (source.right > xBound) {
+ target.right = target.left + target.width() *
+ (xBound - source.left) / source.width();
+ source.right = xBound;
+ }
+ float yBound = (float) height / texHeight;
+ if (source.bottom > yBound) {
+ target.bottom = target.top + target.height() *
+ (yBound - source.top) / source.height();
+ source.bottom = yBound;
+ }
+ }
+
+ public void drawMixed(BasicTexture from,
+ int toColor, float ratio, int x, int y, int w, int h) {
+ drawMixed(from, toColor, ratio, x, y, w, h, mAlpha);
+ }
+
+ public void drawMixed(BasicTexture from, BasicTexture to,
+ float ratio, int x, int y, int w, int h) {
+ drawMixed(from, to, ratio, x, y, w, h, mAlpha);
+ }
+
+ private boolean bindTexture(BasicTexture texture) {
+ if (!texture.onBind(this)) return false;
+ mGLState.setTexture2DEnabled(true);
+ mGL.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId());
+ return true;
+ }
+
+ private void setTextureColor(float r, float g, float b, float alpha) {
+ float[] color = mTextureColor;
+ color[0] = r;
+ color[1] = g;
+ color[2] = b;
+ color[3] = alpha;
+ }
+
+ private void drawMixed(BasicTexture from, int toColor,
+ float ratio, int x, int y, int width, int height, float alpha) {
+
+ if (ratio <= 0) {
+ drawTexture(from, x, y, width, height, alpha);
+ return;
+ } else if (ratio >= 1) {
+ fillRect(x, y, width, height, toColor);
+ return;
+ }
+
+ mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+ || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
+
+ final GL11 gl = mGL;
+ if (!bindTexture(from)) return;
+
+ //
+ // The formula we want:
+ // alpha * ((1 - ratio) * from + ratio * to)
+ // The formula that GL supports is in the form of:
+ // combo * (modulate * from) + (1 - combo) * to
+ //
+ // So, we have combo = 1 - alpha * ratio
+ // and modulate = alpha * (1f - ratio) / combo
+ //
+ float comboRatio = 1 - alpha * ratio;
+
+ // handle the case that (1 - comboRatio) == 0
+ if (alpha < OPAQUE_ALPHA) {
+ mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio);
+ } else {
+ mGLState.setTextureAlpha(1f);
+ }
+
+ // Interpolate the RGB and alpha values between both textures.
+ mGLState.setTexEnvMode(GL11.GL_COMBINE);
+ // Specify the interpolation factor via the alpha component of
+ // GL_TEXTURE_ENV_COLORs.
+ // RGB component are get from toColor and will used as SRC1
+ float colorAlpha = (float) (toColor >>> 24) / (0xff * 0xff);
+ setTextureColor(((toColor >>> 16) & 0xff) * colorAlpha,
+ ((toColor >>> 8) & 0xff) * colorAlpha,
+ (toColor & 0xff) * colorAlpha, comboRatio);
+ gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
+
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
+
+ // Wire up the interpolation factor for RGB.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+ // Wire up the interpolation factor for alpha.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+ drawBoundTexture(from, x, y, width, height);
+ mGLState.setTexEnvMode(GL11.GL_REPLACE);
+ }
+
+ private void drawMixed(BasicTexture from, BasicTexture to,
+ float ratio, int x, int y, int width, int height, float alpha) {
+
+ if (ratio <= 0) {
+ drawTexture(from, x, y, width, height, alpha);
+ return;
+ } else if (ratio >= 1) {
+ drawTexture(to, x, y, width, height, alpha);
+ return;
+ }
+
+ // In the current implementation the two textures must have the
+ // same size.
+ Utils.assertTrue(from.getWidth() == to.getWidth()
+ && from.getHeight() == to.getHeight());
+
+ mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+ || !to.isOpaque() || alpha < OPAQUE_ALPHA));
+
+ final GL11 gl = mGL;
+ if (!bindTexture(from)) return;
+
+ //
+ // The formula we want:
+ // alpha * ((1 - ratio) * from + ratio * to)
+ // The formula that GL supports is in the form of:
+ // combo * (modulate * from) + (1 - combo) * to
+ //
+ // So, we have combo = 1 - alpha * ratio
+ // and modulate = alpha * (1f - ratio) / combo
+ //
+ float comboRatio = 1 - alpha * ratio;
+
+ // handle the case that (1 - comboRatio) == 0
+ if (alpha < OPAQUE_ALPHA) {
+ mGLState.setTextureAlpha(alpha * (1f - ratio) / comboRatio);
+ } else {
+ mGLState.setTextureAlpha(1f);
+ }
+
+ gl.glActiveTexture(GL11.GL_TEXTURE1);
+ if (!bindTexture(to)) {
+ // Disable TEXTURE1.
+ gl.glDisable(GL11.GL_TEXTURE_2D);
+ // Switch back to the default texture unit.
+ gl.glActiveTexture(GL11.GL_TEXTURE0);
+ return;
+ }
+ gl.glEnable(GL11.GL_TEXTURE_2D);
+
+ // Interpolate the RGB and alpha values between both textures.
+ mGLState.setTexEnvMode(GL11.GL_COMBINE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);
+
+ // Specify the interpolation factor via the alpha component of
+ // GL_TEXTURE_ENV_COLORs.
+ // We don't use the RGB color, so just give them 0s.
+ setTextureColor(0, 0, 0, comboRatio);
+ gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);
+
+ // Wire up the interpolation factor for RGB.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
+
+ // Wire up the interpolation factor for alpha.
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
+
+ // Draw the combined texture.
+ drawBoundTexture(to, x, y, width, height);
+
+ // Disable TEXTURE1.
+ gl.glDisable(GL11.GL_TEXTURE_2D);
+ // Switch back to the default texture unit.
+ gl.glActiveTexture(GL11.GL_TEXTURE0);
+ }
+
+ // TODO: the code only work for 2D should get fixed for 3D or removed
+ private static final int MSKEW_X = 4;
+ private static final int MSKEW_Y = 1;
+ private static final int MSCALE_X = 0;
+ private static final int MSCALE_Y = 5;
+
+ private static boolean isMatrixRotatedOrFlipped(float matrix[]) {
+ final float eps = 1e-5f;
+ return Math.abs(matrix[MSKEW_X]) > eps
+ || Math.abs(matrix[MSKEW_Y]) > eps
+ || matrix[MSCALE_X] < -eps
+ || matrix[MSCALE_Y] > eps;
+ }
+
+ public BasicTexture copyTexture(int x, int y, int width, int height) {
+
+ if (isMatrixRotatedOrFlipped(mMatrixValues)) {
+ throw new IllegalArgumentException("cannot support rotated matrix");
+ }
+ float points[] = mapPoints(mMatrixValues, x, y + height, x + width, y);
+ x = (int) points[0];
+ y = (int) points[1];
+ width = (int) points[2] - x;
+ height = (int) points[3] - y;
+
+ GL11 gl = mGL;
+
+ RawTexture texture = RawTexture.newInstance(this);
+ gl.glBindTexture(GL11.GL_TEXTURE_2D, texture.getId());
+ texture.setSize(width, height);
+
+ int[] cropRect = {0, 0, width, height};
+ gl.glTexParameteriv(GL11.GL_TEXTURE_2D,
+ GL11Ext.GL_TEXTURE_CROP_RECT_OES, cropRect, 0);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+ gl.glCopyTexImage2D(GL11.GL_TEXTURE_2D, 0,
+ GL11.GL_RGB, x, y, texture.getTextureWidth(),
+ texture.getTextureHeight(), 0);
+
+ return texture;
+ }
+
+ private static class GLState {
+
+ private final GL11 mGL;
+
+ private int mTexEnvMode = GL11.GL_REPLACE;
+ private float mTextureAlpha = 1.0f;
+ private boolean mTexture2DEnabled = true;
+ private boolean mBlendEnabled = true;
+ private float mLineWidth = 1.0f;
+ private boolean mLineSmooth = false;
+
+ public GLState(GL11 gl) {
+ mGL = gl;
+
+ // Disable unused state
+ gl.glDisable(GL11.GL_LIGHTING);
+
+ // Enable used features
+ gl.glEnable(GL11.GL_DITHER);
+ gl.glEnable(GL11.GL_SCISSOR_TEST);
+
+ gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
+ gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
+ gl.glEnable(GL11.GL_TEXTURE_2D);
+
+ gl.glTexEnvf(GL11.GL_TEXTURE_ENV,
+ GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
+
+ // Set the background color
+ gl.glClearColor(0f, 0f, 0f, 0f);
+ gl.glClearStencil(0);
+
+ gl.glEnable(GL11.GL_BLEND);
+ gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+
+ // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel.
+ gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2);
+ }
+
+ public void setTexEnvMode(int mode) {
+ if (mTexEnvMode == mode) return;
+ mTexEnvMode = mode;
+ mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode);
+ }
+
+ public void setLineWidth(float width) {
+ if (mLineWidth == width) return;
+ mLineWidth = width;
+ mGL.glLineWidth(width);
+ }
+
+ public void setLineSmooth(boolean enabled) {
+ if (mLineSmooth == enabled) return;
+ mLineSmooth = enabled;
+ if (enabled) {
+ mGL.glEnable(GL11.GL_LINE_SMOOTH);
+ } else {
+ mGL.glDisable(GL11.GL_LINE_SMOOTH);
+ }
+ }
+
+ public void setTextureAlpha(float alpha) {
+ if (mTextureAlpha == alpha) return;
+ mTextureAlpha = alpha;
+ if (alpha >= OPAQUE_ALPHA) {
+ // The alpha is need for those texture without alpha channel
+ mGL.glColor4f(1, 1, 1, 1);
+ setTexEnvMode(GL11.GL_REPLACE);
+ } else {
+ mGL.glColor4f(alpha, alpha, alpha, alpha);
+ setTexEnvMode(GL11.GL_MODULATE);
+ }
+ }
+
+ public void setColorMode(int color, float alpha) {
+ setBlendEnabled(!Utils.isOpaque(color) || alpha < OPAQUE_ALPHA);
+
+ // Set mTextureAlpha to an invalid value, so that it will reset
+ // again in setTextureAlpha(float) later.
+ mTextureAlpha = -1.0f;
+
+ setTexture2DEnabled(false);
+
+ float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
+ mGL.glColor4x(
+ Math.round(((color >> 16) & 0xFF) * prealpha),
+ Math.round(((color >> 8) & 0xFF) * prealpha),
+ Math.round((color & 0xFF) * prealpha),
+ Math.round(255 * prealpha));
+ }
+
+ public void setTexture2DEnabled(boolean enabled) {
+ if (mTexture2DEnabled == enabled) return;
+ mTexture2DEnabled = enabled;
+ if (enabled) {
+ mGL.glEnable(GL11.GL_TEXTURE_2D);
+ } else {
+ mGL.glDisable(GL11.GL_TEXTURE_2D);
+ }
+ }
+
+ public void setBlendEnabled(boolean enabled) {
+ if (mBlendEnabled == enabled) return;
+ mBlendEnabled = enabled;
+ if (enabled) {
+ mGL.glEnable(GL11.GL_BLEND);
+ } else {
+ mGL.glDisable(GL11.GL_BLEND);
+ }
+ }
+ }
+
+ public GL11 getGLInstance() {
+ return mGL;
+ }
+
+ public void setCurrentAnimationTimeMillis(long time) {
+ Utils.assertTrue(time >= 0);
+ mAnimationTime = time;
+ }
+
+ public void clearBuffer() {
+ mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
+ }
+
+ private void setTextureCoords(RectF source) {
+ setTextureCoords(source.left, source.top, source.right, source.bottom);
+ }
+
+ private void setTextureCoords(float left, float top,
+ float right, float bottom) {
+ mGL.glMatrixMode(GL11.GL_TEXTURE);
+ mTextureMatrixValues[0] = right - left;
+ mTextureMatrixValues[5] = bottom - top;
+ mTextureMatrixValues[10] = 1;
+ mTextureMatrixValues[12] = left;
+ mTextureMatrixValues[13] = top;
+ mTextureMatrixValues[15] = 1;
+ mGL.glLoadMatrixf(mTextureMatrixValues, 0);
+ mGL.glMatrixMode(GL11.GL_MODELVIEW);
+ }
+
+ // unloadTexture and deleteBuffer can be called from the finalizer thread,
+ // so we synchronized on the mUnboundTextures object.
+ public boolean unloadTexture(BasicTexture t) {
+ synchronized (mUnboundTextures) {
+ if (!t.isLoaded(this)) return false;
+ mUnboundTextures.add(t.mId);
+ return true;
+ }
+ }
+
+ public void deleteBuffer(int bufferId) {
+ synchronized (mUnboundTextures) {
+ mDeleteBuffers.add(bufferId);
+ }
+ }
+
+ public void deleteRecycledResources() {
+ synchronized (mUnboundTextures) {
+ IntArray ids = mUnboundTextures;
+ if (ids.size() > 0) {
+ mGL.glDeleteTextures(ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+
+ ids = mDeleteBuffers;
+ if (ids.size() > 0) {
+ mGL.glDeleteBuffers(ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+ }
+ }
+
+ public int save() {
+ return save(SAVE_FLAG_ALL);
+ }
+
+ public int save(int saveFlags) {
+ ConfigState config = obtainRestoreConfig();
+
+ if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
+ config.mAlpha = mAlpha;
+ } else {
+ config.mAlpha = -1;
+ }
+
+ if ((saveFlags & SAVE_FLAG_CLIP) != 0) {
+ config.mRect.set(mClipRect);
+ } else {
+ config.mRect.left = Integer.MAX_VALUE;
+ }
+
+ if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
+ System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
+ } else {
+ config.mMatrix[0] = Float.NEGATIVE_INFINITY;
+ }
+
+ mRestoreStack.push(config);
+ return mRestoreStack.size() - 1;
+ }
+
+ public void restore() {
+ if (mRestoreStack.isEmpty()) throw new IllegalStateException();
+ ConfigState config = mRestoreStack.pop();
+ config.restore(this);
+ freeRestoreConfig(config);
+ }
+
+ private void freeRestoreConfig(ConfigState action) {
+ action.mNextFree = mRecycledRestoreAction;
+ mRecycledRestoreAction = action;
+ }
+
+ private ConfigState obtainRestoreConfig() {
+ if (mRecycledRestoreAction != null) {
+ ConfigState result = mRecycledRestoreAction;
+ mRecycledRestoreAction = result.mNextFree;
+ return result;
+ }
+ return new ConfigState();
+ }
+
+ private static class ConfigState {
+ float mAlpha;
+ Rect mRect = new Rect();
+ float mMatrix[] = new float[16];
+ ConfigState mNextFree;
+
+ public void restore(GLCanvasImpl canvas) {
+ if (mAlpha >= 0) canvas.setAlpha(mAlpha);
+ if (mRect.left != Integer.MAX_VALUE) {
+ Rect rect = mRect;
+ canvas.mClipRect.set(rect);
+ canvas.mGL.glScissor(
+ rect.left, rect.top, rect.width(), rect.height());
+ }
+ if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
+ System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
+ }
+ }
+ }
+
+ public void dumpStatisticsAndClear() {
+ String line = String.format(
+ "MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d",
+ mCountDrawMesh, mCountTextureRect, mCountTextureOES,
+ mCountFillRect, mCountDrawLine);
+ mCountDrawMesh = 0;
+ mCountTextureRect = 0;
+ mCountTextureOES = 0;
+ mCountFillRect = 0;
+ mCountDrawLine = 0;
+ Log.d(TAG, line);
+ }
+
+ private void saveTransform() {
+ System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16);
+ }
+
+ private void restoreTransform() {
+ System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLPaint.java b/src/com/android/gallery3d/ui/GLPaint.java
new file mode 100644
index 000000000..9f7b6f1f3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLPaint.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.common.Utils;
+
+
+public class GLPaint {
+ public static final int FLAG_ANTI_ALIAS = 0x01;
+
+ private int mFlags = 0;
+ private float mLineWidth = 1f;
+ private int mColor = 0;
+
+ public int getFlags() {
+ return mFlags;
+ }
+
+ public void setFlags(int flags) {
+ mFlags = flags;
+ }
+
+ public void setColor(int color) {
+ mColor = color;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public void setLineWidth(float width) {
+ Utils.assertTrue(width >= 0);
+ mLineWidth = width;
+ }
+
+ public float getLineWidth() {
+ return mLineWidth;
+ }
+
+ public void setAntiAlias(boolean enabled) {
+ if (enabled) {
+ mFlags |= FLAG_ANTI_ALIAS;
+ } else {
+ mFlags &= ~FLAG_ANTI_ALIAS;
+ }
+ }
+
+ public boolean getAntiAlias(){
+ return (mFlags & FLAG_ANTI_ALIAS) != 0;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java
new file mode 100644
index 000000000..24e5794b0
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRoot.java
@@ -0,0 +1,37 @@
+/*
+ * 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.anim.CanvasAnimation;
+
+public interface GLRoot {
+
+ public static interface OnGLIdleListener {
+ public boolean onGLIdle(GLRoot root, GLCanvas canvas);
+ }
+
+ public void addOnGLIdleListener(OnGLIdleListener listener);
+ public void registerLaunchedAnimation(CanvasAnimation animation);
+ public void requestRender();
+ public void requestLayoutContentPane();
+ public boolean hasStencil();
+
+ public void lockRenderThread();
+ public void unlockRenderThread();
+
+ public void setContentPane(GLView content);
+}
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
new file mode 100644
index 000000000..e03adf1c4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -0,0 +1,414 @@
+/*
+ * 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.anim.CanvasAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.opengl.GLSurfaceView;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.concurrent.locks.ReentrantLock;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+
+// The root component of all <code>GLView</code>s. The rendering is done in GL
+// thread while the event handling is done in the main thread. To synchronize
+// the two threads, the entry points of this package need to synchronize on the
+// <code>GLRootView</code> instance unless it can be proved that the rendering
+// thread won't access the same thing as the method. The entry points include:
+// (1) The public methods of HeadUpDisplay
+// (2) The public methods of CameraHeadUpDisplay
+// (3) The overridden methods in GLRootView.
+public class GLRootView extends GLSurfaceView
+ implements GLSurfaceView.Renderer, GLRoot {
+ private static final String TAG = "GLRootView";
+
+ private static final boolean DEBUG_FPS = false;
+ private int mFrameCount = 0;
+ private long mFrameCountingStart = 0;
+
+ private static final boolean DEBUG_INVALIDATE = false;
+ private int mInvalidateColor = 0;
+
+ private static final boolean DEBUG_DRAWING_STAT = false;
+
+ private static final int FLAG_INITIALIZED = 1;
+ private static final int FLAG_NEED_LAYOUT = 2;
+
+ private GL11 mGL;
+ private GLCanvasImpl mCanvas;
+
+ private GLView mContentView;
+ private DisplayMetrics mDisplayMetrics;
+
+ private int mFlags = FLAG_NEED_LAYOUT;
+ private volatile boolean mRenderRequested = false;
+
+ private Rect mClipRect = new Rect();
+ private int mClipRetryCount = 0;
+
+ private final GalleryEGLConfigChooser mEglConfigChooser =
+ new GalleryEGLConfigChooser();
+
+ private final ArrayList<CanvasAnimation> mAnimations =
+ new ArrayList<CanvasAnimation>();
+
+ private final LinkedList<OnGLIdleListener> mIdleListeners =
+ new LinkedList<OnGLIdleListener>();
+
+ private final IdleRunner mIdleRunner = new IdleRunner();
+
+ private final ReentrantLock mRenderLock = new ReentrantLock();
+
+ private static final int TARGET_FRAME_TIME = 33;
+ private long mLastDrawFinishTime;
+ private boolean mInDownState = false;
+
+ public GLRootView(Context context) {
+ this(context, null);
+ }
+
+ public GLRootView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mFlags |= FLAG_INITIALIZED;
+ setBackgroundDrawable(null);
+ setEGLConfigChooser(mEglConfigChooser);
+ setRenderer(this);
+ getHolder().setFormat(PixelFormat.RGB_565);
+
+ // Uncomment this to enable gl error check.
+ //setDebugFlags(DEBUG_CHECK_GL_ERROR);
+ }
+
+ public GalleryEGLConfigChooser getEGLConfigChooser() {
+ return mEglConfigChooser;
+ }
+
+ @Override
+ public boolean hasStencil() {
+ return getEGLConfigChooser().getStencilBits() > 0;
+ }
+
+ @Override
+ public void registerLaunchedAnimation(CanvasAnimation animation) {
+ // Register the newly launched animation so that we can set the start
+ // time more precisely. (Usually, it takes much longer for first
+ // rendering, so we set the animation start time as the time we
+ // complete rendering)
+ mAnimations.add(animation);
+ }
+
+ @Override
+ public void addOnGLIdleListener(OnGLIdleListener listener) {
+ synchronized (mIdleListeners) {
+ mIdleListeners.addLast(listener);
+ mIdleRunner.enable();
+ }
+ }
+
+ @Override
+ public void setContentPane(GLView content) {
+ if (mContentView == content) return;
+ if (mContentView != null) {
+ if (mInDownState) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mContentView.dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ mInDownState = false;
+ }
+ mContentView.detachFromRoot();
+ BasicTexture.yieldAllTextures();
+ }
+ mContentView = content;
+ if (content != null) {
+ content.attachToRoot(this);
+ requestLayoutContentPane();
+ }
+ }
+
+ public GLView getContentPane() {
+ return mContentView;
+ }
+
+ @Override
+ public void requestRender() {
+ if (DEBUG_INVALIDATE) {
+ StackTraceElement e = Thread.currentThread().getStackTrace()[4];
+ String caller = e.getFileName() + ":" + e.getLineNumber() + " ";
+ Log.d(TAG, "invalidate: " + caller);
+ }
+ if (mRenderRequested) return;
+ mRenderRequested = true;
+ super.requestRender();
+ }
+
+ @Override
+ public void requestLayoutContentPane() {
+ mRenderLock.lock();
+ try {
+ if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return;
+
+ // "View" system will invoke onLayout() for initialization(bug ?), we
+ // have to ignore it since the GLThread is not ready yet.
+ if ((mFlags & FLAG_INITIALIZED) == 0) return;
+
+ mFlags |= FLAG_NEED_LAYOUT;
+ requestRender();
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ private void layoutContentPane() {
+ mFlags &= ~FLAG_NEED_LAYOUT;
+ int width = getWidth();
+ int height = getHeight();
+ Log.i(TAG, "layout content pane " + width + "x" + height);
+ if (mContentView != null && width != 0 && height != 0) {
+ mContentView.layout(0, 0, width, height);
+ }
+ // Uncomment this to dump the view hierarchy.
+ //mContentView.dumpTree("");
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (changed) requestLayoutContentPane();
+ }
+
+ /**
+ * Called when the context is created, possibly after automatic destruction.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceCreated(GL10 gl1, EGLConfig config) {
+ GL11 gl = (GL11) gl1;
+ if (mGL != null) {
+ // The GL Object has changed
+ Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl);
+ }
+ mGL = gl;
+ mCanvas = new GLCanvasImpl(gl);
+ if (!DEBUG_FPS) {
+ setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ } else {
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ }
+ }
+
+ /**
+ * Called when the OpenGL surface is recreated without destroying the
+ * context.
+ */
+ // This is a GLSurfaceView.Renderer callback
+ @Override
+ public void onSurfaceChanged(GL10 gl1, int width, int height) {
+ Log.i(TAG, "onSurfaceChanged: " + width + "x" + height
+ + ", gl10: " + gl1.toString());
+ Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
+ GalleryUtils.setRenderThread();
+ GL11 gl = (GL11) gl1;
+ Utils.assertTrue(mGL == gl);
+
+ mCanvas.setSize(width, height);
+
+ mClipRect.set(0, 0, width, height);
+ mClipRetryCount = 2;
+ }
+
+ private void outputFps() {
+ long now = System.nanoTime();
+ if (mFrameCountingStart == 0) {
+ mFrameCountingStart = now;
+ } else if ((now - mFrameCountingStart) > 1000000000) {
+ Log.d(TAG, "fps: " + (double) mFrameCount
+ * 1000000000 / (now - mFrameCountingStart));
+ mFrameCountingStart = now;
+ mFrameCount = 0;
+ }
+ ++mFrameCount;
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ mRenderLock.lock();
+ try {
+ onDrawFrameLocked(gl);
+ } finally {
+ mRenderLock.unlock();
+ }
+ long end = SystemClock.uptimeMillis();
+
+ if (mLastDrawFinishTime != 0) {
+ long wait = mLastDrawFinishTime + TARGET_FRAME_TIME - end;
+ if (wait > 0) {
+ SystemClock.sleep(wait);
+ }
+ }
+ mLastDrawFinishTime = SystemClock.uptimeMillis();
+ }
+
+ private void onDrawFrameLocked(GL10 gl) {
+ if (DEBUG_FPS) outputFps();
+
+ // release the unbound textures and deleted buffers.
+ mCanvas.deleteRecycledResources();
+
+ // reset texture upload limit
+ UploadedTexture.resetUploadLimit();
+
+ mRenderRequested = false;
+
+ if ((mFlags & FLAG_NEED_LAYOUT) != 0) layoutContentPane();
+
+ // OpenGL seems having a bug causing us not being able to reset the
+ // scissor box in "onSurfaceChanged()". We have to do it in the second
+ // onDrawFrame().
+ if (mClipRetryCount > 0) {
+ --mClipRetryCount;
+ Rect clip = mClipRect;
+ gl.glScissor(clip.left, clip.top, clip.width(), clip.height());
+ }
+
+ mCanvas.setCurrentAnimationTimeMillis(SystemClock.uptimeMillis());
+ if (mContentView != null) {
+ mContentView.render(mCanvas);
+ }
+
+ if (!mAnimations.isEmpty()) {
+ long now = SystemClock.uptimeMillis();
+ for (int i = 0, n = mAnimations.size(); i < n; i++) {
+ mAnimations.get(i).setStartTime(now);
+ }
+ mAnimations.clear();
+ }
+
+ if (UploadedTexture.uploadLimitReached()) {
+ requestRender();
+ }
+
+ synchronized (mIdleListeners) {
+ if (!mRenderRequested && !mIdleListeners.isEmpty()) {
+ mIdleRunner.enable();
+ }
+ }
+
+ if (DEBUG_INVALIDATE) {
+ mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);
+ mInvalidateColor = ~mInvalidateColor;
+ }
+
+ if (DEBUG_DRAWING_STAT) {
+ mCanvas.dumpStatisticsAndClear();
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mInDownState = false;
+ } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ mRenderLock.lock();
+ try {
+ // If this has been detached from root, we don't need to handle event
+ boolean handled = mContentView != null
+ && mContentView.dispatchTouchEvent(event);
+ if (action == MotionEvent.ACTION_DOWN && handled) {
+ mInDownState = true;
+ }
+ return handled;
+ } finally {
+ mRenderLock.unlock();
+ }
+ }
+
+ public DisplayMetrics getDisplayMetrics() {
+ if (mDisplayMetrics == null) {
+ mDisplayMetrics = new DisplayMetrics();
+ ((Activity) getContext()).getWindowManager()
+ .getDefaultDisplay().getMetrics(mDisplayMetrics);
+ }
+ return mDisplayMetrics;
+ }
+
+ public GLCanvas getCanvas() {
+ return mCanvas;
+ }
+
+ private class IdleRunner implements Runnable {
+ // true if the idle runner is in the queue
+ private boolean mActive = false;
+
+ @Override
+ public void run() {
+ OnGLIdleListener listener;
+ synchronized (mIdleListeners) {
+ mActive = false;
+ if (mRenderRequested) return;
+ if (mIdleListeners.isEmpty()) return;
+ listener = mIdleListeners.removeFirst();
+ }
+ mRenderLock.lock();
+ try {
+ if (!listener.onGLIdle(GLRootView.this, mCanvas)) return;
+ } finally {
+ mRenderLock.unlock();
+ }
+ synchronized (mIdleListeners) {
+ mIdleListeners.addLast(listener);
+ enable();
+ }
+ }
+
+ public void enable() {
+ // Who gets the flag can add it to the queue
+ if (mActive) return;
+ mActive = true;
+ queueEvent(this);
+ }
+ }
+
+ @Override
+ public void lockRenderThread() {
+ mRenderLock.lock();
+ }
+
+ @Override
+ public void unlockRenderThread() {
+ mRenderLock.unlock();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
new file mode 100644
index 000000000..c59327831
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -0,0 +1,431 @@
+/*
+ * 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.anim.CanvasAnimation;
+import com.android.gallery3d.common.Utils;
+
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+
+import java.util.ArrayList;
+
+// GLView is a UI component. It can render to a GLCanvas and accept touch
+// events. A GLView may have zero or more child GLView and they form a tree
+// structure. The rendering and event handling will pass through the tree
+// structure.
+//
+// A GLView tree should be attached to a GLRoot before event dispatching and
+// rendering happens. GLView asks GLRoot to re-render or re-layout the
+// GLView hierarchy using requestRender() and requestLayoutContentPane().
+//
+// The render() method is called in a separate thread. Before calling
+// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the
+// rendering thread running at the same time. If there are other entry points
+// from main thread (like a Handler) in your GLView, you need to call
+// lockRendering() if the rendering thread should not run at the same time.
+//
+public class GLView {
+ private static final String TAG = "GLView";
+
+ public static final int VISIBLE = 0;
+ public static final int INVISIBLE = 1;
+
+ private static final int FLAG_INVISIBLE = 1;
+ private static final int FLAG_SET_MEASURED_SIZE = 2;
+ private static final int FLAG_LAYOUT_REQUESTED = 4;
+
+ protected final Rect mBounds = new Rect();
+ protected final Rect mPaddings = new Rect();
+
+ private GLRoot mRoot;
+ protected GLView mParent;
+ private ArrayList<GLView> mComponents;
+ private GLView mMotionTarget;
+
+ private CanvasAnimation mAnimation;
+
+ private int mViewFlags = 0;
+
+ protected int mMeasuredWidth = 0;
+ protected int mMeasuredHeight = 0;
+
+ private int mLastWidthSpec = -1;
+ private int mLastHeightSpec = -1;
+
+ protected int mScrollY = 0;
+ protected int mScrollX = 0;
+ protected int mScrollHeight = 0;
+ protected int mScrollWidth = 0;
+
+ public void startAnimation(CanvasAnimation animation) {
+ GLRoot root = getGLRoot();
+ if (root == null) throw new IllegalStateException();
+
+ mAnimation = animation;
+ mAnimation.start();
+ root.registerLaunchedAnimation(mAnimation);
+ invalidate();
+ }
+
+ // Sets the visiblity of this GLView (either GLView.VISIBLE or
+ // GLView.INVISIBLE).
+ public void setVisibility(int visibility) {
+ if (visibility == getVisibility()) return;
+ if (visibility == VISIBLE) {
+ mViewFlags &= ~FLAG_INVISIBLE;
+ } else {
+ mViewFlags |= FLAG_INVISIBLE;
+ }
+ onVisibilityChanged(visibility);
+ invalidate();
+ }
+
+ // Returns GLView.VISIBLE or GLView.INVISIBLE
+ public int getVisibility() {
+ return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE;
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void attachToRoot(GLRoot root) {
+ Utils.assertTrue(mParent == null && mRoot == null);
+ onAttachToRoot(root);
+ }
+
+ // This should only be called on the content pane (the topmost GLView).
+ public void detachFromRoot() {
+ Utils.assertTrue(mParent == null && mRoot != null);
+ onDetachFromRoot();
+ }
+
+ // Returns the number of children of the GLView.
+ public int getComponentCount() {
+ return mComponents == null ? 0 : mComponents.size();
+ }
+
+ // Returns the children for the given index.
+ public GLView getComponent(int index) {
+ if (mComponents == null) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mComponents.get(index);
+ }
+
+ // Adds a child to this GLView.
+ public void addComponent(GLView component) {
+ // Make sure the component doesn't have a parent currently.
+ if (component.mParent != null) throw new IllegalStateException();
+
+ // Build parent-child links
+ if (mComponents == null) {
+ mComponents = new ArrayList<GLView>();
+ }
+ mComponents.add(component);
+ component.mParent = this;
+
+ // If this is added after we have a root, tell the component.
+ if (mRoot != null) {
+ component.onAttachToRoot(mRoot);
+ }
+ }
+
+ // Removes a child from this GLView.
+ public boolean removeComponent(GLView component) {
+ if (mComponents == null) return false;
+ if (mComponents.remove(component)) {
+ removeOneComponent(component);
+ return true;
+ }
+ return false;
+ }
+
+ // Removes all children of this GLView.
+ public void removeAllComponents() {
+ for (int i = 0, n = mComponents.size(); i < n; ++i) {
+ removeOneComponent(mComponents.get(i));
+ }
+ mComponents.clear();
+ }
+
+ private void removeOneComponent(GLView component) {
+ if (mMotionTarget == component) {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ dispatchTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+ component.onDetachFromRoot();
+ component.mParent = null;
+ }
+
+ public Rect bounds() {
+ return mBounds;
+ }
+
+ public int getWidth() {
+ return mBounds.right - mBounds.left;
+ }
+
+ public int getHeight() {
+ return mBounds.bottom - mBounds.top;
+ }
+
+ public GLRoot getGLRoot() {
+ return mRoot;
+ }
+
+ // Request re-rendering of the view hierarchy.
+ // This is used for animation or when the contents changed.
+ public void invalidate() {
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestRender();
+ }
+
+ // Request re-layout of the view hierarchy.
+ public void requestLayout() {
+ mViewFlags |= FLAG_LAYOUT_REQUESTED;
+ mLastHeightSpec = -1;
+ mLastWidthSpec = -1;
+ if (mParent != null) {
+ mParent.requestLayout();
+ } else {
+ // Is this a content pane ?
+ GLRoot root = getGLRoot();
+ if (root != null) root.requestLayoutContentPane();
+ }
+ }
+
+ protected void render(GLCanvas canvas) {
+ renderBackground(canvas);
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ renderChild(canvas, getComponent(i));
+ }
+ }
+
+ protected void renderBackground(GLCanvas view) {
+ }
+
+ protected void renderChild(GLCanvas canvas, GLView component) {
+ if (component.getVisibility() != GLView.VISIBLE
+ && component.mAnimation == null) return;
+
+ int xoffset = component.mBounds.left - mScrollX;
+ int yoffset = component.mBounds.top - mScrollY;
+
+ canvas.translate(xoffset, yoffset, 0);
+
+ CanvasAnimation anim = component.mAnimation;
+ if (anim != null) {
+ canvas.save(anim.getCanvasSaveFlags());
+ if (anim.calculate(canvas.currentAnimationTimeMillis())) {
+ invalidate();
+ } else {
+ component.mAnimation = null;
+ }
+ anim.apply(canvas);
+ }
+ component.render(canvas);
+ if (anim != null) canvas.restore();
+ canvas.translate(-xoffset, -yoffset, 0);
+ }
+
+ protected boolean onTouch(MotionEvent event) {
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event,
+ int x, int y, GLView component, boolean checkBounds) {
+ Rect rect = component.mBounds;
+ int left = rect.left;
+ int top = rect.top;
+ if (!checkBounds || rect.contains(x, y)) {
+ event.offsetLocation(-left, -top);
+ if (component.dispatchTouchEvent(event)) {
+ event.offsetLocation(left, top);
+ return true;
+ }
+ event.offsetLocation(left, top);
+ }
+ return false;
+ }
+
+ protected boolean dispatchTouchEvent(MotionEvent event) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ int action = event.getAction();
+ if (mMotionTarget != null) {
+ if (action == MotionEvent.ACTION_DOWN) {
+ MotionEvent cancel = MotionEvent.obtain(event);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
+ dispatchTouchEvent(cancel, x, y, mMotionTarget, false);
+ mMotionTarget = null;
+ } else {
+ dispatchTouchEvent(event, x, y, mMotionTarget, false);
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mMotionTarget = null;
+ }
+ return true;
+ }
+ }
+ if (action == MotionEvent.ACTION_DOWN) {
+ // in the reverse rendering order
+ for (int i = getComponentCount() - 1; i >= 0; --i) {
+ GLView component = getComponent(i);
+ if (component.getVisibility() != GLView.VISIBLE) continue;
+ if (dispatchTouchEvent(event, x, y, component, true)) {
+ mMotionTarget = component;
+ return true;
+ }
+ }
+ }
+ return onTouch(event);
+ }
+
+ public Rect getPaddings() {
+ return mPaddings;
+ }
+
+ public void setPaddings(Rect paddings) {
+ mPaddings.set(paddings);
+ }
+
+ public void setPaddings(int left, int top, int right, int bottom) {
+ mPaddings.set(left, top, right, bottom);
+ }
+
+ public void layout(int left, int top, int right, int bottom) {
+ boolean sizeChanged = setBounds(left, top, right, bottom);
+ if (sizeChanged) {
+ mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+ onLayout(true, left, top, right, bottom);
+ } else if ((mViewFlags & FLAG_LAYOUT_REQUESTED)!= 0) {
+ mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+ onLayout(false, left, top, right, bottom);
+ }
+ }
+
+ private boolean setBounds(int left, int top, int right, int bottom) {
+ boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left)
+ || (bottom - top) != (mBounds.bottom - mBounds.top);
+ mBounds.set(left, top, right, bottom);
+ return sizeChanged;
+ }
+
+ public void measure(int widthSpec, int heightSpec) {
+ if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec
+ && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) {
+ return;
+ }
+
+ mLastWidthSpec = widthSpec;
+ mLastHeightSpec = heightSpec;
+
+ mViewFlags &= ~FLAG_SET_MEASURED_SIZE;
+ onMeasure(widthSpec, heightSpec);
+ if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {
+ throw new IllegalStateException(getClass().getName()
+ + " should call setMeasuredSize() in onMeasure()");
+ }
+ }
+
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ }
+
+ protected void setMeasuredSize(int width, int height) {
+ mViewFlags |= FLAG_SET_MEASURED_SIZE;
+ mMeasuredWidth = width;
+ mMeasuredHeight = height;
+ }
+
+ public int getMeasuredWidth() {
+ return mMeasuredWidth;
+ }
+
+ public int getMeasuredHeight() {
+ return mMeasuredHeight;
+ }
+
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ }
+
+ /**
+ * Gets the bounds of the given descendant that relative to this view.
+ */
+ public boolean getBoundsOf(GLView descendant, Rect out) {
+ int xoffset = 0;
+ int yoffset = 0;
+ GLView view = descendant;
+ while (view != this) {
+ if (view == null) return false;
+ Rect bounds = view.mBounds;
+ xoffset += bounds.left;
+ yoffset += bounds.top;
+ view = view.mParent;
+ }
+ out.set(xoffset, yoffset, xoffset + descendant.getWidth(),
+ yoffset + descendant.getHeight());
+ return true;
+ }
+
+ protected void onVisibilityChanged(int visibility) {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ GLView child = getComponent(i);
+ if (child.getVisibility() == GLView.VISIBLE) {
+ child.onVisibilityChanged(visibility);
+ }
+ }
+ }
+
+ protected void onAttachToRoot(GLRoot root) {
+ mRoot = root;
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onAttachToRoot(root);
+ }
+ }
+
+ protected void onDetachFromRoot() {
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).onDetachFromRoot();
+ }
+ mRoot = null;
+ }
+
+ public void lockRendering() {
+ if (mRoot != null) {
+ mRoot.lockRenderThread();
+ }
+ }
+
+ public void unlockRendering() {
+ if (mRoot != null) {
+ mRoot.unlockRenderThread();
+ }
+ }
+
+ // This is for debugging only.
+ // Dump the view hierarchy into log.
+ void dumpTree(String prefix) {
+ Log.d(TAG, prefix + getClass().getSimpleName());
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ getComponent(i).dumpTree(prefix + "....");
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java
new file mode 100644
index 000000000..1d50d43f7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GalleryEGLConfigChooser.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2009 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.opengl.GLSurfaceView.EGLConfigChooser;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLDisplay;
+
+/*
+ * The code is copied/adapted from
+ * <code>android.opengl.GLSurfaceView.BaseConfigChooser</code>. Here we try to
+ * choose a configuration that support RGBA_8888 format and if possible,
+ * with stencil buffer, but is not required.
+ */
+class GalleryEGLConfigChooser implements EGLConfigChooser {
+
+ private static final String TAG = "GalleryEGLConfigChooser";
+ private int mStencilBits;
+
+ private final int mConfigSpec[] = new int[] {
+ EGL10.EGL_RED_SIZE, 5,
+ EGL10.EGL_GREEN_SIZE, 6,
+ EGL10.EGL_BLUE_SIZE, 5,
+ EGL10.EGL_ALPHA_SIZE, 0,
+ EGL10.EGL_NONE
+ };
+
+ public int getStencilBits() {
+ return mStencilBits;
+ }
+
+ public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+ int[] numConfig = new int[1];
+ if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, numConfig)) {
+ throw new RuntimeException("eglChooseConfig failed");
+ }
+
+ if (numConfig[0] <= 0) {
+ throw new RuntimeException("No configs match configSpec");
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfig[0]];
+ if (!egl.eglChooseConfig(display,
+ mConfigSpec, configs, configs.length, numConfig)) {
+ throw new RuntimeException();
+ }
+
+ return chooseConfig(egl, display, configs);
+ }
+
+ private EGLConfig chooseConfig(
+ EGL10 egl, EGLDisplay display, EGLConfig configs[]) {
+
+ EGLConfig result = null;
+ int minStencil = Integer.MAX_VALUE;
+ int value[] = new int[1];
+
+ // Because we need only one bit of stencil, try to choose a config that
+ // has stencil support but with smallest number of stencil bits. If
+ // none is found, choose any one.
+ for (int i = 0, n = configs.length; i < n; ++i) {
+ if (egl.eglGetConfigAttrib(
+ display, configs[i], EGL10.EGL_RED_SIZE, value)) {
+ // Filter out ARGB 8888 configs.
+ if (value[0] == 8) continue;
+ }
+ if (egl.eglGetConfigAttrib(
+ display, configs[i], EGL10.EGL_STENCIL_SIZE, value)) {
+ if (value[0] == 0) continue;
+ if (value[0] < minStencil) {
+ minStencil = value[0];
+ result = configs[i];
+ }
+ } else {
+ throw new RuntimeException(
+ "eglGetConfigAttrib error: " + egl.eglGetError());
+ }
+ }
+ if (result == null) result = configs[0];
+ egl.eglGetConfigAttrib(
+ display, result, EGL10.EGL_STENCIL_SIZE, value);
+ mStencilBits = value[0];
+ logConfig(egl, display, result);
+ return result;
+ }
+
+ private static final int[] ATTR_ID = {
+ EGL10.EGL_RED_SIZE,
+ EGL10.EGL_GREEN_SIZE,
+ EGL10.EGL_BLUE_SIZE,
+ EGL10.EGL_ALPHA_SIZE,
+ EGL10.EGL_DEPTH_SIZE,
+ EGL10.EGL_STENCIL_SIZE,
+ EGL10.EGL_CONFIG_ID,
+ EGL10.EGL_CONFIG_CAVEAT
+ };
+
+ private static final String[] ATTR_NAME = {
+ "R", "G", "B", "A", "D", "S", "ID", "CAVEAT"
+ };
+
+ private void logConfig(EGL10 egl, EGLDisplay display, EGLConfig config) {
+ int value[] = new int[1];
+ StringBuilder sb = new StringBuilder();
+ for (int j = 0; j < ATTR_ID.length; j++) {
+ egl.eglGetConfigAttrib(display, config, ATTR_ID[j], value);
+ sb.append(ATTR_NAME[j] + value[0] + " ");
+ }
+ Log.i(TAG, "Config chosen: " + sb.toString());
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GridDrawer.java b/src/com/android/gallery3d/ui/GridDrawer.java
new file mode 100644
index 000000000..54b175cb4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GridDrawer.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+import android.graphics.Color;
+
+public class GridDrawer extends IconDrawer {
+ private final NinePatchTexture mFrame;
+ private final NinePatchTexture mFrameSelected;
+ private final NinePatchTexture mFrameSelectedTop;
+ private final NinePatchTexture mImportBackground;
+ private Texture mImportLabel;
+ private int mGridWidth;
+ private final SelectionManager mSelectionManager;
+ private final Context mContext;
+ private final int FONT_SIZE = 14;
+ private final int FONT_COLOR = Color.WHITE;
+ private final int IMPORT_LABEL_PADDING = 10;
+ private boolean mSelectionMode;
+
+ public GridDrawer(Context context, SelectionManager selectionManager) {
+ super(context);
+ mContext = context;
+ mFrame = new NinePatchTexture(context, R.drawable.album_frame);
+ mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+ mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top);
+ mImportBackground = new NinePatchTexture(context, R.drawable.import_translucent);
+ mSelectionManager = selectionManager;
+ }
+
+ @Override
+ public void prepareDrawing() {
+ mSelectionMode = mSelectionManager.inSelectionMode();
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotationAndGray(canvas, content, x, y, width, height, rotation,
+ topIndex);
+
+ if (((rotation / 90) & 0x01) == 1) {
+ int temp = width;
+ width = height;
+ height = temp;
+ x = -width / 2;
+ y = -height / 2;
+ }
+
+ drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
+
+ NinePatchTexture frame;
+ if (mSelectionMode && mSelectionManager.isItemSelected(path)) {
+ frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected;
+ } else {
+ frame = mFrame;
+ }
+
+ drawFrame(canvas, frame, x, y, width, height);
+
+ if (topIndex == 0) {
+ ResourceTexture icon = getIcon(dataSourceType);
+ if (icon != null) {
+ IconDimension id = getIconDimension(icon, width, height);
+ if (dataSourceType == DATASOURCE_TYPE_MTP) {
+ if (mImportLabel == null || mGridWidth != width) {
+ mGridWidth = width;
+ mImportLabel = MultiLineTexture.newInstance(
+ mContext.getString(R.string.click_import),
+ width - id.width - IMPORT_LABEL_PADDING, FONT_SIZE, FONT_COLOR);
+ }
+ int bgHeight = Math.max(id.height, mImportLabel.getHeight());
+ mImportBackground.setSize(width, bgHeight);
+ mImportBackground.draw(canvas, x, -y - bgHeight);
+ mImportLabel.draw(canvas, x + id.width + IMPORT_LABEL_PADDING,
+ -y - bgHeight + Math.abs(bgHeight - mImportLabel.getHeight()) / 2);
+ }
+ icon.draw(canvas, id.x, id.y, id.width, id.height);
+ }
+ }
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ }
+}
diff --git a/src/com/android/gallery3d/ui/HighlightDrawer.java b/src/com/android/gallery3d/ui/HighlightDrawer.java
new file mode 100644
index 000000000..9d5868bcb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/HighlightDrawer.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+
+public class HighlightDrawer extends IconDrawer {
+ private final NinePatchTexture mFrame;
+ private final NinePatchTexture mFrameSelected;
+ private final NinePatchTexture mFrameSelectedTop;
+ private SelectionManager mSelectionManager;
+ private Path mHighlightItem;
+
+ public HighlightDrawer(Context context) {
+ super(context);
+ mFrame = new NinePatchTexture(context, R.drawable.album_frame);
+ mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+ mFrameSelectedTop = new NinePatchTexture(context, R.drawable.grid_selected_top);
+ }
+
+ public void setHighlightItem(Path item) {
+ mHighlightItem = item;
+ }
+
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotationAndGray(canvas, content, x, y, width, height, rotation,
+ topIndex);
+
+ if (((rotation / 90) & 0x01) == 1) {
+ int temp = width;
+ width = height;
+ height = temp;
+ x = -width / 2;
+ y = -height / 2;
+ }
+
+ drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
+
+ NinePatchTexture frame;
+ if (path == mHighlightItem) {
+ frame = topIndex == 0 ? mFrameSelectedTop : mFrameSelected;
+ } else {
+ frame = mFrame;
+ }
+
+ drawFrame(canvas, frame, x, y, width, height);
+
+ if (topIndex == 0) {
+ drawIcon(canvas, width, height, dataSourceType);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Icon.java b/src/com/android/gallery3d/ui/Icon.java
new file mode 100644
index 000000000..c710859f8
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Icon.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class Icon extends GLView {
+ private final BasicTexture mIcon;
+
+ // The width and height requested by the user.
+ private int mReqWidth;
+ private int mReqHeight;
+
+ public Icon(Context context, int iconId, int width, int height) {
+ this(context, new ResourceTexture(context, iconId), width, height);
+ }
+
+ public Icon(Context context, BasicTexture icon, int width, int height) {
+ mIcon = icon;
+ mReqWidth = width;
+ mReqHeight = height;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(mReqWidth, mReqHeight)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Rect p = mPaddings;
+
+ int width = getWidth() - p.left - p.right;
+ int height = getHeight() - p.top - p.bottom;
+
+ // Draw the icon in the center of the space
+ int xoffset = p.left + (width - mReqWidth) / 2;
+ int yoffset = p.top + (height - mReqHeight) / 2;
+
+ mIcon.draw(canvas, xoffset, yoffset, mReqWidth, mReqHeight);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/IconDrawer.java b/src/com/android/gallery3d/ui/IconDrawer.java
new file mode 100644
index 000000000..91732d338
--- /dev/null
+++ b/src/com/android/gallery3d/ui/IconDrawer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaObject;
+
+import android.content.Context;
+
+public abstract class IconDrawer extends SelectionDrawer {
+ private final String TAG = "IconDrawer";
+ private final ResourceTexture mLocalSetIcon;
+ private final ResourceTexture mCameraIcon;
+ private final ResourceTexture mPicasaIcon;
+ private final ResourceTexture mMtpIcon;
+ private final Texture mVideoOverlay;
+ private final Texture mVideoPlayIcon;
+
+ public static class IconDimension {
+ int x;
+ int y;
+ int width;
+ int height;
+ }
+
+ public IconDrawer(Context context) {
+ mLocalSetIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_folder_holo);
+ mCameraIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_camera_holo);
+ mPicasaIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_picassa_holo);
+ mMtpIcon = new ResourceTexture(context, R.drawable.ic_album_overlay_ptp_holo);
+ mVideoOverlay = new ResourceTexture(context,
+ R.drawable.thumbnail_album_video_overlay_holo);
+ mVideoPlayIcon = new ResourceTexture(context,
+ R.drawable.videooverlay);
+ }
+
+ @Override
+ public void prepareDrawing() {
+ }
+
+ protected IconDimension drawIcon(GLCanvas canvas, int width, int height,
+ int dataSourceType) {
+ ResourceTexture icon = getIcon(dataSourceType);
+
+ if (icon != null) {
+ IconDimension id = getIconDimension(icon, width, height);
+ icon.draw(canvas, id.x, id.y, id.width, id.height);
+ return id;
+ }
+ return null;
+ }
+
+ protected ResourceTexture getIcon(int dataSourceType) {
+ ResourceTexture icon = null;
+ switch (dataSourceType) {
+ case DATASOURCE_TYPE_LOCAL:
+ icon = mLocalSetIcon;
+ break;
+ case DATASOURCE_TYPE_PICASA:
+ icon = mPicasaIcon;
+ break;
+ case DATASOURCE_TYPE_CAMERA:
+ icon = mCameraIcon;
+ break;
+ case DATASOURCE_TYPE_MTP:
+ icon = mMtpIcon;
+ break;
+ default:
+ break;
+ }
+
+ return icon;
+ }
+
+ protected IconDimension getIconDimension(ResourceTexture icon, int width,
+ int height) {
+ IconDimension id = new IconDimension();
+ float scale = 0.25f * width / icon.getWidth();
+ id.width = (int) (scale * icon.getWidth());
+ id.height = (int) (scale * icon.getHeight());
+ id.x = -width / 2;
+ id.y = height / 2 - id.height;
+ return id;
+ }
+
+ protected void drawVideoOverlay(GLCanvas canvas, int mediaType,
+ int x, int y, int width, int height, int topIndex) {
+ if (mediaType != MediaObject.MEDIA_TYPE_VIDEO) return;
+ mVideoOverlay.draw(canvas, x, y, width, height);
+ if (topIndex == 0) {
+ int side = Math.min(width, height) / 6;
+ mVideoPlayIcon.draw(canvas, -side / 2, -side / 2, side, side);
+ }
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ImportCompleteListener.java b/src/com/android/gallery3d/ui/ImportCompleteListener.java
new file mode 100644
index 000000000..5c52ea135
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ImportCompleteListener.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AlbumPage;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.widget.Toast;
+
+public class ImportCompleteListener implements MenuExecutor.ProgressListener {
+ private GalleryActivity mActivity;
+
+ public ImportCompleteListener(GalleryActivity galleryActivity) {
+ mActivity = galleryActivity;
+ }
+
+ public void onProgressComplete(int result) {
+ int message;
+ if (result == MenuExecutor.EXECUTION_RESULT_SUCCESS) {
+ message = R.string.import_complete;
+ goToImportedAlbum();
+ } else {
+ message = R.string.import_fail;
+ }
+ Toast.makeText(mActivity.getAndroidContext(), message, Toast.LENGTH_LONG).show();
+ }
+
+ public void onProgressUpdate(int index) {
+ }
+
+ private void goToImportedAlbum() {
+ String pathOfImportedAlbum = "/local/all/" + MediaSetUtils.IMPORTED_BUCKET_ID;
+ Bundle data = new Bundle();
+ data.putString(AlbumPage.KEY_MEDIA_PATH, pathOfImportedAlbum);
+ mActivity.getStateManager().startState(AlbumPage.class, data);
+ }
+
+}
diff --git a/src/com/android/gallery3d/ui/Label.java b/src/com/android/gallery3d/ui/Label.java
new file mode 100644
index 000000000..6a70a1895
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Label.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+
+public class Label extends GLView {
+ private static final String TAG = "Label";
+ public static final int NULL_ID = 0;
+
+ private static final int FONT_SIZE = 18;
+ private static final int FONT_COLOR = Color.WHITE;
+
+ private String mText;
+ private StringTexture mTexture;
+ private int mFontSize, mFontColor;
+
+ public Label(Context context, int stringId,
+ int fontSize, int fontColor) {
+ this(context, context.getString(stringId), fontSize, fontColor);
+ }
+
+ public Label(Context context, int stringId) {
+ this(context, stringId, FONT_SIZE, FONT_COLOR);
+ }
+
+ public Label(Context context, String text) {
+ this(context, text, FONT_SIZE, FONT_COLOR);
+ }
+
+ public Label(Context context, String text, int fontSize, int fontColor) {
+ //TODO: cut the text if it is too long
+ mText = text;
+ mTexture = StringTexture.newInstance(text, fontSize, fontColor);
+ mFontSize = fontSize;
+ mFontColor = fontColor;
+ }
+
+ public void setText(String text) {
+ if (!mText.equals(text)) {
+ mText = text;
+ mTexture = StringTexture.newInstance(text, mFontSize, mFontColor);
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int width = mTexture.getWidth();
+ int height = mTexture.getHeight();
+ MeasureHelper.getInstance(this)
+ .setPreferredContentSize(width, height)
+ .measure(widthSpec, heightSpec);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Rect p = mPaddings;
+
+ int width = getWidth() - p.left - p.right;
+ int height = getHeight() - p.top - p.bottom;
+
+ int xoffset = p.left + (width - mTexture.getWidth()) / 2;
+ int yoffset = p.top + (height - mTexture.getHeight()) / 2;
+
+ mTexture.draw(canvas, xoffset, yoffset);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java
new file mode 100644
index 000000000..32adc98eb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Log.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+public class Log {
+ public static int v(String tag, String msg) {
+ return android.util.Log.v(tag, msg);
+ }
+ public static int v(String tag, String msg, Throwable tr) {
+ return android.util.Log.v(tag, msg, tr);
+ }
+ public static int d(String tag, String msg) {
+ return android.util.Log.d(tag, msg);
+ }
+ public static int d(String tag, String msg, Throwable tr) {
+ return android.util.Log.d(tag, msg, tr);
+ }
+ public static int i(String tag, String msg) {
+ return android.util.Log.i(tag, msg);
+ }
+ public static int i(String tag, String msg, Throwable tr) {
+ return android.util.Log.i(tag, msg, tr);
+ }
+ public static int w(String tag, String msg) {
+ return android.util.Log.w(tag, msg);
+ }
+ public static int w(String tag, String msg, Throwable tr) {
+ return android.util.Log.w(tag, msg, tr);
+ }
+ public static int w(String tag, Throwable tr) {
+ return android.util.Log.w(tag, tr);
+ }
+ public static int e(String tag, String msg) {
+ return android.util.Log.e(tag, msg);
+ }
+ public static int e(String tag, String msg, Throwable tr) {
+ return android.util.Log.e(tag, msg, tr);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
new file mode 100644
index 000000000..cf1e39e24
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+
+public class ManageCacheDrawer extends IconDrawer {
+ private static final int COLOR_CACHING_BACKGROUND = 0x7F000000;
+ private static final int ICON_SIZE = 36;
+ private final NinePatchTexture mFrame;
+ private final ResourceTexture mCheckedItem;
+ private final ResourceTexture mUnCheckedItem;
+ private final SelectionManager mSelectionManager;
+
+ private final ResourceTexture mLocalAlbumIcon;
+ private final StringTexture mCaching;
+
+ public ManageCacheDrawer(Context context, SelectionManager selectionManager) {
+ super(context);
+ mFrame = new NinePatchTexture(context, R.drawable.manage_frame);
+ mCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_on_holo_dark);
+ mUnCheckedItem = new ResourceTexture(context, R.drawable.btn_make_offline_normal_off_holo_dark);
+ mLocalAlbumIcon = new ResourceTexture(context, R.drawable.btn_make_offline_disabled_on_holo_dark);
+ String cachingLabel = context.getString(R.string.caching_label);
+ mCaching = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
+ mSelectionManager = selectionManager;
+ }
+
+ @Override
+ public void prepareDrawing() {
+ }
+
+ private static boolean isLocal(int dataSourceType) {
+ return dataSourceType != DATASOURCE_TYPE_PICASA;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+
+ boolean selected = mSelectionManager.isItemSelected(path);
+ boolean chooseToCache = wantCache ^ selected;
+
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotationAndGray(canvas, content, x, y, width, height, rotation,
+ topIndex);
+
+ if (((rotation / 90) & 0x01) == 1) {
+ int temp = width;
+ width = height;
+ height = temp;
+ x = -width / 2;
+ y = -height / 2;
+ }
+
+ drawVideoOverlay(canvas, mediaType, x, y, width, height, topIndex);
+
+ drawFrame(canvas, mFrame, x, y, width, height);
+
+ if (topIndex == 0) {
+ drawIcon(canvas, width, height, dataSourceType);
+ }
+
+ if (topIndex == 0) {
+ ResourceTexture icon = null;
+ if (isLocal(dataSourceType)) {
+ icon = mLocalAlbumIcon;
+ } else if (chooseToCache) {
+ icon = mCheckedItem;
+ } else {
+ icon = mUnCheckedItem;
+ }
+
+ int w = ICON_SIZE;
+ int h = ICON_SIZE;
+ x = width / 2 - w / 2;
+ y = -height / 2 - h / 2;
+
+ icon.draw(canvas, x, y, w, h);
+
+ if (isCaching) {
+ int textWidth = mCaching.getWidth();
+ int textHeight = mCaching.getHeight();
+ x = -textWidth / 2;
+ y = height / 2 - textHeight;
+
+ // Leave a few pixels of margin in the background rect.
+ float sideMargin = Utils.clamp(textWidth * 0.1f, 2.0f,
+ 6.0f);
+ float clearance = Utils.clamp(textHeight * 0.1f, 2.0f,
+ 6.0f);
+
+ // Overlay the "Caching" wording at the bottom-center of the content.
+ canvas.fillRect(x - sideMargin, y - clearance,
+ textWidth + sideMargin * 2, textHeight + clearance,
+ COLOR_CACHING_BACKGROUND);
+ mCaching.draw(canvas, x, y);
+ }
+ }
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ }
+}
diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java
new file mode 100644
index 000000000..f65dc10b3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MeasureHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Rect;
+import android.view.View.MeasureSpec;
+
+class MeasureHelper {
+
+ private static MeasureHelper sInstance = new MeasureHelper(null);
+
+ private GLView mComponent;
+ private int mPreferredWidth;
+ private int mPreferredHeight;
+
+ private MeasureHelper(GLView component) {
+ mComponent = component;
+ }
+
+ public static MeasureHelper getInstance(GLView component) {
+ sInstance.mComponent = component;
+ return sInstance;
+ }
+
+ public MeasureHelper setPreferredContentSize(int width, int height) {
+ mPreferredWidth = width;
+ mPreferredHeight = height;
+ return this;
+ }
+
+ public void measure(int widthSpec, int heightSpec) {
+ Rect p = mComponent.getPaddings();
+ setMeasuredSize(
+ getLength(widthSpec, mPreferredWidth + p.left + p.right),
+ getLength(heightSpec, mPreferredHeight + p.top + p.bottom));
+ }
+
+ private static int getLength(int measureSpec, int prefered) {
+ int specLength = MeasureSpec.getSize(measureSpec);
+ switch(MeasureSpec.getMode(measureSpec)) {
+ case MeasureSpec.EXACTLY: return specLength;
+ case MeasureSpec.AT_MOST: return Math.min(prefered, specLength);
+ default: return prefered;
+ }
+ }
+
+ protected void setMeasuredSize(int width, int height) {
+ mComponent.setMeasuredSize(width, height);
+ }
+
+}
diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java
new file mode 100644
index 000000000..710ddc422
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.CropImage;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class MenuExecutor {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MenuExecutor";
+
+ private static final int MSG_TASK_COMPLETE = 1;
+ private static final int MSG_TASK_UPDATE = 2;
+ private static final int MSG_DO_SHARE = 3;
+
+ public static final int EXECUTION_RESULT_SUCCESS = 1;
+ public static final int EXECUTION_RESULT_FAIL = 2;
+ public static final int EXECUTION_RESULT_CANCEL = 3;
+
+ private ProgressDialog mDialog;
+ private Future<?> mTask;
+
+ private final GalleryActivity mActivity;
+ private final SelectionManager mSelectionManager;
+ private final Handler mHandler;
+
+ private static ProgressDialog showProgressDialog(
+ Context context, int titleId, int progressMax) {
+ ProgressDialog dialog = new ProgressDialog(context);
+ dialog.setTitle(titleId);
+ dialog.setMax(progressMax);
+ dialog.setCancelable(false);
+ dialog.setIndeterminate(false);
+ if (progressMax > 1) {
+ dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ }
+ dialog.show();
+ return dialog;
+ }
+
+ public interface ProgressListener {
+ public void onProgressUpdate(int index);
+ public void onProgressComplete(int result);
+ }
+
+ public MenuExecutor(
+ GalleryActivity activity, SelectionManager selectionManager) {
+ mActivity = Utils.checkNotNull(activity);
+ mSelectionManager = Utils.checkNotNull(selectionManager);
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_TASK_COMPLETE: {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ mDialog = null;
+ mTask = null;
+ }
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressComplete(message.arg1);
+ }
+ mSelectionManager.leaveSelectionMode();
+ break;
+ }
+ case MSG_TASK_UPDATE: {
+ if (mDialog != null) mDialog.setProgress(message.arg1);
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressUpdate(message.arg1);
+ }
+ break;
+ }
+ case MSG_DO_SHARE: {
+ ((Activity) mActivity).startActivity((Intent) message.obj);
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ public void pause() {
+ if (mTask != null) {
+ mTask.cancel();
+ mTask.waitDone();
+ mDialog.dismiss();
+ mDialog = null;
+ mTask = null;
+ }
+ }
+
+ private void onProgressUpdate(int index, ProgressListener listener) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener));
+ }
+
+ private void onProgressComplete(int result, ProgressListener listener) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener));
+ }
+
+ private int getShareType(SelectionManager selectionManager) {
+ ArrayList<Path> items = selectionManager.getSelected(false);
+ int type = 0;
+ DataManager dataManager = mActivity.getDataManager();
+ for (Path id : items) {
+ type |= dataManager.getMediaType(id);
+ }
+ return type;
+ }
+
+ private void onShareItemClicked(final SelectionManager selectionManager,
+ final String mimeType, final ComponentName component) {
+ Utils.assertTrue(mDialog == null);
+ final ArrayList<Path> items = selectionManager.getSelected(true);
+ mDialog = showProgressDialog((Activity) mActivity,
+ R.string.loading_image, items.size());
+
+ mTask = mActivity.getThreadPool().submit(new Job<Void>() {
+ @Override
+ public Void run(JobContext jc) {
+ DataManager manager = mActivity.getDataManager();
+ ArrayList<Uri> uris = new ArrayList<Uri>(items.size());
+ int index = 0;
+ for (Path path : items) {
+ if ((manager.getSupportedOperations(path)
+ & MediaObject.SUPPORT_SHARE) != 0) {
+ uris.add(manager.getContentUri(path));
+ }
+ onProgressUpdate(++index, null);
+ }
+ if (jc.isCancelled()) return null;
+ Intent intent = new Intent()
+ .setComponent(component).setType(mimeType);
+ if (uris.isEmpty()) {
+ return null;
+ } else if (uris.size() == 1) {
+ intent.setAction(Intent.ACTION_SEND);
+ intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ } else {
+ intent.setAction(Intent.ACTION_SEND_MULTIPLE);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ }
+ onProgressComplete(EXECUTION_RESULT_SUCCESS, null);
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_DO_SHARE, intent));
+ return null;
+ }
+ }, null);
+ }
+
+ private static void setMenuItemVisibility(
+ Menu menu, int id, boolean visibility) {
+ MenuItem item = menu.findItem(id);
+ if (item != null) item.setVisible(visibility);
+ }
+
+ public static void updateMenuOperation(Menu menu, int supported) {
+ boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0;
+ boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0;
+ boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0;
+ boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0;
+ boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0;
+ boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0;
+ boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0;
+ boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0;
+ boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0;
+ boolean supportImport = (supported & MediaObject.SUPPORT_IMPORT) != 0;
+
+ setMenuItemVisibility(menu, R.id.action_delete, supportDelete);
+ setMenuItemVisibility(menu, R.id.action_rotate_ccw, supportRotate);
+ setMenuItemVisibility(menu, R.id.action_rotate_cw, supportRotate);
+ setMenuItemVisibility(menu, R.id.action_crop, supportCrop);
+ setMenuItemVisibility(menu, R.id.action_share, supportShare);
+ setMenuItemVisibility(menu, R.id.action_setas, supportSetAs);
+ setMenuItemVisibility(menu, R.id.action_show_on_map, supportShowOnMap);
+ setMenuItemVisibility(menu, R.id.action_edit, supportEdit);
+ setMenuItemVisibility(menu, R.id.action_details, supportInfo);
+ setMenuItemVisibility(menu, R.id.action_import, supportImport);
+ }
+
+ private Path getSingleSelectedPath() {
+ ArrayList<Path> ids = mSelectionManager.getSelected(true);
+ Utils.assertTrue(ids.size() == 1);
+ return ids.get(0);
+ }
+
+ public boolean onMenuClicked(MenuItem menuItem, ProgressListener listener) {
+ int title;
+ DataManager manager = mActivity.getDataManager();
+ int action = menuItem.getItemId();
+ switch (action) {
+ case R.id.action_select_all:
+ if (mSelectionManager.inSelectAllMode()) {
+ mSelectionManager.deSelectAll();
+ } else {
+ mSelectionManager.selectAll();
+ }
+ return true;
+ case R.id.action_crop: {
+ Path path = getSingleSelectedPath();
+ String mimeType = getMimeType(manager.getMediaType(path));
+ Intent intent = new Intent(CropImage.ACTION_CROP)
+ .setDataAndType(manager.getContentUri(path), mimeType);
+ ((Activity) mActivity).startActivity(intent);
+ return true;
+ }
+ case R.id.action_setas: {
+ Path path = getSingleSelectedPath();
+ int type = manager.getMediaType(path);
+ Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
+ String mimeType = getMimeType(type);
+ intent.setDataAndType(manager.getContentUri(path), mimeType);
+ intent.putExtra("mimeType", mimeType);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ Activity activity = (Activity) mActivity;
+ activity.startActivity(Intent.createChooser(
+ intent, activity.getString(R.string.set_as)));
+ return true;
+ }
+ case R.id.action_confirm_delete:
+ title = R.string.delete;
+ break;
+ case R.id.action_rotate_cw:
+ title = R.string.rotate_right;
+ break;
+ case R.id.action_rotate_ccw:
+ title = R.string.rotate_left;
+ break;
+ case R.id.action_show_on_map:
+ title = R.string.show_on_map;
+ break;
+ case R.id.action_edit:
+ title = R.string.edit;
+ break;
+ case R.id.action_import:
+ title = R.string.Import;
+ break;
+ default:
+ return false;
+ }
+ startAction(action, title, listener);
+ return true;
+ }
+
+ public void startAction(int action, int title, ProgressListener listener) {
+ ArrayList<Path> ids = mSelectionManager.getSelected(false);
+ Utils.assertTrue(mDialog == null);
+
+ Activity activity = (Activity) mActivity;
+ mDialog = showProgressDialog(activity, title, ids.size());
+ MediaOperation operation = new MediaOperation(action, ids, listener);
+ mTask = mActivity.getThreadPool().submit(operation, null);
+ }
+
+ public static String getMimeType(int type) {
+ switch (type) {
+ case MediaObject.MEDIA_TYPE_IMAGE :
+ return "image/*";
+ case MediaObject.MEDIA_TYPE_VIDEO :
+ return "video/*";
+ default: return "*/*";
+ }
+ }
+
+ private boolean execute(
+ DataManager manager, JobContext jc, int cmd, Path path) {
+ boolean result = true;
+ switch (cmd) {
+ case R.id.action_confirm_delete:
+ manager.delete(path);
+ break;
+ case R.id.action_rotate_cw:
+ manager.rotate(path, 90);
+ break;
+ case R.id.action_rotate_ccw:
+ manager.rotate(path, -90);
+ break;
+ case R.id.action_toggle_full_caching: {
+ MediaObject obj = manager.getMediaObject(path);
+ int cacheFlag = obj.getCacheFlag();
+ if (cacheFlag == MediaObject.CACHE_FLAG_FULL) {
+ cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL;
+ } else {
+ cacheFlag = MediaObject.CACHE_FLAG_FULL;
+ }
+ obj.cache(cacheFlag);
+ break;
+ }
+ case R.id.action_show_on_map: {
+ MediaItem item = (MediaItem) manager.getMediaObject(path);
+ double latlng[] = new double[2];
+ item.getLatLong(latlng);
+ if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) {
+ GalleryUtils.showOnMap((Context) mActivity, latlng[0], latlng[1]);
+ }
+ break;
+ }
+ case R.id.action_import: {
+ MediaObject obj = manager.getMediaObject(path);
+ result = obj.Import();
+ break;
+ }
+ case R.id.action_edit: {
+ Activity activity = (Activity) mActivity;
+ MediaItem item = (MediaItem) manager.getMediaObject(path);
+ try {
+ activity.startActivity(Intent.createChooser(
+ new Intent(Intent.ACTION_EDIT)
+ .setData(item.getContentUri())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION),
+ null));
+ } catch (Throwable t) {
+ Log.w(TAG, "failed to start edit activity: ", t);
+ Toast.makeText(activity,
+ activity.getString(R.string.activity_not_found),
+ Toast.LENGTH_SHORT).show();
+ }
+ break;
+ }
+ default:
+ throw new AssertionError();
+ }
+ return result;
+ }
+
+ private class MediaOperation implements Job<Void> {
+ private final ArrayList<Path> mItems;
+ private final int mOperation;
+ private final ProgressListener mListener;
+
+ public MediaOperation(int operation, ArrayList<Path> items, ProgressListener listener) {
+ mOperation = operation;
+ mItems = items;
+ mListener = listener;
+ }
+
+ public Void run(JobContext jc) {
+ int index = 0;
+ DataManager manager = mActivity.getDataManager();
+ int result = EXECUTION_RESULT_SUCCESS;
+ for (Path id : mItems) {
+ if (jc.isCancelled()) {
+ result = EXECUTION_RESULT_CANCEL;
+ break;
+ }
+ try {
+ if (!execute(manager, jc, mOperation, id)) result = EXECUTION_RESULT_FAIL;
+ } catch (Throwable th) {
+ Log.e(TAG, "failed to execute operation " + mOperation
+ + " for " + id, th);
+ }
+ onProgressUpdate(index++, mListener);
+ }
+ onProgressComplete(result, mListener);
+ return null;
+ }
+ }
+}
+
diff --git a/src/com/android/gallery3d/ui/MultiLineTexture.java b/src/com/android/gallery3d/ui/MultiLineTexture.java
new file mode 100644
index 000000000..be62d59c0
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MultiLineTexture.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+
+// MultiLineTexture is a texture shows the content of a specified String.
+//
+// To create a MultiLineTexture, use the newInstance() method and specify
+// the String, the font size, and the color.
+class MultiLineTexture extends CanvasTexture {
+ private final Layout mLayout;
+
+ private MultiLineTexture(Layout layout) {
+ super(layout.getWidth(), layout.getHeight());
+ mLayout = layout;
+ }
+
+ public static MultiLineTexture newInstance(
+ String text, int maxWidth, float textSize, int color) {
+ TextPaint paint = StringTexture.getDefaultPaint(textSize, color);
+ Layout layout = new StaticLayout(text, 0, text.length(), paint,
+ maxWidth, Layout.Alignment.ALIGN_NORMAL, 1, 0, true, null, 0);
+
+ return new MultiLineTexture(layout);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas, Bitmap backing) {
+ mLayout.draw(canvas);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/NinePatchChunk.java b/src/com/android/gallery3d/ui/NinePatchChunk.java
new file mode 100644
index 000000000..61bf22c33
--- /dev/null
+++ b/src/com/android/gallery3d/ui/NinePatchChunk.java
@@ -0,0 +1,82 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+// See "frameworks/base/include/utils/ResourceTypes.h" for the format of
+// NinePatch chunk.
+class NinePatchChunk {
+
+ public static final int NO_COLOR = 0x00000001;
+ public static final int TRANSPARENT_COLOR = 0x00000000;
+
+ public Rect mPaddings = new Rect();
+
+ public int mDivX[];
+ public int mDivY[];
+ public int mColor[];
+
+ private static void readIntArray(int[] data, ByteBuffer buffer) {
+ for (int i = 0, n = data.length; i < n; ++i) {
+ data[i] = buffer.getInt();
+ }
+ }
+
+ private static void checkDivCount(int length) {
+ if (length == 0 || (length & 0x01) != 0) {
+ throw new RuntimeException("invalid nine-patch: " + length);
+ }
+ }
+
+ public static NinePatchChunk deserialize(byte[] data) {
+ ByteBuffer byteBuffer =
+ ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
+
+ byte wasSerialized = byteBuffer.get();
+ if (wasSerialized == 0) return null;
+
+ NinePatchChunk chunk = new NinePatchChunk();
+ chunk.mDivX = new int[byteBuffer.get()];
+ chunk.mDivY = new int[byteBuffer.get()];
+ chunk.mColor = new int[byteBuffer.get()];
+
+ checkDivCount(chunk.mDivX.length);
+ checkDivCount(chunk.mDivY.length);
+
+ // skip 8 bytes
+ byteBuffer.getInt();
+ byteBuffer.getInt();
+
+ chunk.mPaddings.left = byteBuffer.getInt();
+ chunk.mPaddings.right = byteBuffer.getInt();
+ chunk.mPaddings.top = byteBuffer.getInt();
+ chunk.mPaddings.bottom = byteBuffer.getInt();
+
+ // skip 4 bytes
+ byteBuffer.getInt();
+
+ readIntArray(chunk.mDivX, byteBuffer);
+ readIntArray(chunk.mDivY, byteBuffer);
+ readIntArray(chunk.mColor, byteBuffer);
+
+ return chunk;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/ui/NinePatchTexture.java b/src/com/android/gallery3d/ui/NinePatchTexture.java
new file mode 100644
index 000000000..15b057a92
--- /dev/null
+++ b/src/com/android/gallery3d/ui/NinePatchTexture.java
@@ -0,0 +1,401 @@
+/*
+ * 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.common.Utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.microedition.khronos.opengles.GL11;
+
+// NinePatchTexture is a texture backed by a NinePatch resource.
+//
+// getPaddings() returns paddings specified in the NinePatch.
+// getNinePatchChunk() returns the layout data specified in the NinePatch.
+//
+public class NinePatchTexture extends ResourceTexture {
+ @SuppressWarnings("unused")
+ private static final String TAG = "NinePatchTexture";
+ private NinePatchChunk mChunk;
+ private MyCacheMap<Long, NinePatchInstance> mInstanceCache =
+ new MyCacheMap<Long, NinePatchInstance>();
+
+ public NinePatchTexture(Context context, int resId) {
+ super(context, resId);
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ if (mBitmap != null) return mBitmap;
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap = BitmapFactory.decodeResource(
+ mContext.getResources(), mResId, options);
+ mBitmap = bitmap;
+ setSize(bitmap.getWidth(), bitmap.getHeight());
+ byte[] chunkData = bitmap.getNinePatchChunk();
+ mChunk = chunkData == null
+ ? null
+ : NinePatchChunk.deserialize(bitmap.getNinePatchChunk());
+ if (mChunk == null) {
+ throw new RuntimeException("invalid nine-patch image: " + mResId);
+ }
+ return bitmap;
+ }
+
+ public Rect getPaddings() {
+ // get the paddings from nine patch
+ if (mChunk == null) onGetBitmap();
+ return mChunk.mPaddings;
+ }
+
+ public NinePatchChunk getNinePatchChunk() {
+ if (mChunk == null) onGetBitmap();
+ return mChunk;
+ }
+
+ private static class MyCacheMap<K, V> extends LinkedHashMap<K, V> {
+ private int CACHE_SIZE = 16;
+ private V mJustRemoved;
+
+ public MyCacheMap() {
+ super(4, 0.75f, true);
+ }
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ if (size() > CACHE_SIZE) {
+ mJustRemoved = eldest.getValue();
+ return true;
+ }
+ return false;
+ }
+
+ public V getJustRemoved() {
+ V result = mJustRemoved;
+ mJustRemoved = null;
+ return result;
+ }
+ }
+
+ private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
+ long key = w;
+ key = (key << 32) | h;
+ NinePatchInstance instance = mInstanceCache.get(key);
+
+ if (instance == null) {
+ instance = new NinePatchInstance(this, w, h);
+ mInstanceCache.put(key, instance);
+ NinePatchInstance removed = mInstanceCache.getJustRemoved();
+ if (removed != null) {
+ removed.recycle(canvas);
+ }
+ }
+
+ return instance;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ if (!isLoaded(canvas)) {
+ mInstanceCache.clear();
+ }
+
+ if (w != 0 && h != 0) {
+ findInstance(canvas, w, h).draw(canvas, this, x, y);
+ }
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ GLCanvas canvas = mCanvasRef == null ? null : mCanvasRef.get();
+ if (canvas == null) return;
+ for (NinePatchInstance instance : mInstanceCache.values()) {
+ instance.recycle(canvas);
+ }
+ mInstanceCache.clear();
+ }
+}
+
+// This keeps data for a specialization of NinePatchTexture with the size
+// (width, height). We pre-compute the coordinates for efficiency.
+class NinePatchInstance {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "NinePatchInstance";
+
+ // We need 16 vertices for a normal nine-patch image (the 4x4 vertices)
+ private static final int VERTEX_BUFFER_SIZE = 16 * 2;
+
+ // We need 22 indices for a normal nine-patch image, plus 2 for each
+ // transparent region. Current there are at most 1 transparent region.
+ private static final int INDEX_BUFFER_SIZE = 22 + 2;
+
+ private FloatBuffer mXyBuffer;
+ private FloatBuffer mUvBuffer;
+ private ByteBuffer mIndexBuffer;
+
+ // Names for buffer names: xy, uv, index.
+ private int[] mBufferNames;
+
+ private int mIdxCount;
+
+ public NinePatchInstance(NinePatchTexture tex, int width, int height) {
+ NinePatchChunk chunk = tex.getNinePatchChunk();
+
+ if (width <= 0 || height <= 0) {
+ throw new RuntimeException("invalid dimension");
+ }
+
+ // The code should be easily extended to handle the general cases by
+ // allocating more space for buffers. But let's just handle the only
+ // use case.
+ if (chunk.mDivX.length != 2 || chunk.mDivY.length != 2) {
+ throw new RuntimeException("unsupported nine patch");
+ }
+
+ float divX[] = new float[4];
+ float divY[] = new float[4];
+ float divU[] = new float[4];
+ float divV[] = new float[4];
+
+ int nx = stretch(divX, divU, chunk.mDivX, tex.getWidth(), width);
+ int ny = stretch(divY, divV, chunk.mDivY, tex.getHeight(), height);
+
+ prepareVertexData(divX, divY, divU, divV, nx, ny, chunk.mColor);
+ }
+
+ /**
+ * Stretches the texture according to the nine-patch rules. It will
+ * linearly distribute the strechy parts defined in the nine-patch chunk to
+ * the target area.
+ *
+ * <pre>
+ * source
+ * /--------------^---------------\
+ * u0 u1 u2 u3 u4 u5
+ * div ---> |fffff|ssssssss|fff|ssssss|ffff| ---> u
+ * | div0 div1 div2 div3 |
+ * | | / / / /
+ * | | / / / /
+ * | | / / / /
+ * |fffff|ssss|fff|sss|ffff| ---> x
+ * x0 x1 x2 x3 x4 x5
+ * \----------v------------/
+ * target
+ *
+ * f: fixed segment
+ * s: stretchy segment
+ * </pre>
+ *
+ * @param div the stretch parts defined in nine-patch chunk
+ * @param source the length of the texture
+ * @param target the length on the drawing plan
+ * @param u output, the positions of these dividers in the texture
+ * coordinate
+ * @param x output, the corresponding position of these dividers on the
+ * drawing plan
+ * @return the number of these dividers.
+ */
+ private static int stretch(
+ float x[], float u[], int div[], int source, int target) {
+ int textureSize = Utils.nextPowerOf2(source);
+ float textureBound = (float) source / textureSize;
+
+ float stretch = 0;
+ for (int i = 0, n = div.length; i < n; i += 2) {
+ stretch += div[i + 1] - div[i];
+ }
+
+ float remaining = target - source + stretch;
+
+ float lastX = 0;
+ float lastU = 0;
+
+ x[0] = 0;
+ u[0] = 0;
+ for (int i = 0, n = div.length; i < n; i += 2) {
+ // Make the stretchy segment a little smaller to prevent sampling
+ // on neighboring fixed segments.
+ // fixed segment
+ x[i + 1] = lastX + (div[i] - lastU) + 0.5f;
+ u[i + 1] = Math.min((div[i] + 0.5f) / textureSize, textureBound);
+
+ // stretchy segment
+ float partU = div[i + 1] - div[i];
+ float partX = remaining * partU / stretch;
+ remaining -= partX;
+ stretch -= partU;
+
+ lastX = x[i + 1] + partX;
+ lastU = div[i + 1];
+ x[i + 2] = lastX - 0.5f;
+ u[i + 2] = Math.min((lastU - 0.5f)/ textureSize, textureBound);
+ }
+ // the last fixed segment
+ x[div.length + 1] = target;
+ u[div.length + 1] = textureBound;
+
+ // remove segments with length 0.
+ int last = 0;
+ for (int i = 1, n = div.length + 2; i < n; ++i) {
+ if ((x[i] - x[last]) < 1f) continue;
+ x[++last] = x[i];
+ u[last] = u[i];
+ }
+ return last + 1;
+ }
+
+ private void prepareVertexData(float x[], float y[], float u[], float v[],
+ int nx, int ny, int[] color) {
+ /*
+ * Given a 3x3 nine-patch image, the vertex order is defined as the
+ * following graph:
+ *
+ * (0) (1) (2) (3)
+ * | /| /| /|
+ * | / | / | / |
+ * (4) (5) (6) (7)
+ * | \ | \ | \ |
+ * | \| \| \|
+ * (8) (9) (A) (B)
+ * | /| /| /|
+ * | / | / | / |
+ * (C) (D) (E) (F)
+ *
+ * And we draw the triangle strip in the following index order:
+ *
+ * index: 04152637B6A5948C9DAEBF
+ */
+ int pntCount = 0;
+ float xy[] = new float[VERTEX_BUFFER_SIZE];
+ float uv[] = new float[VERTEX_BUFFER_SIZE];
+ for (int j = 0; j < ny; ++j) {
+ for (int i = 0; i < nx; ++i) {
+ int xIndex = (pntCount++) << 1;
+ int yIndex = xIndex + 1;
+ xy[xIndex] = x[i];
+ xy[yIndex] = y[j];
+ uv[xIndex] = u[i];
+ uv[yIndex] = v[j];
+ }
+ }
+
+ int idxCount = 1;
+ boolean isForward = false;
+ byte index[] = new byte[INDEX_BUFFER_SIZE];
+ for (int row = 0; row < ny - 1; row++) {
+ --idxCount;
+ isForward = !isForward;
+
+ int start, end, inc;
+ if (isForward) {
+ start = 0;
+ end = nx;
+ inc = 1;
+ } else {
+ start = nx - 1;
+ end = -1;
+ inc = -1;
+ }
+
+ for (int col = start; col != end; col += inc) {
+ int k = row * nx + col;
+ if (col != start) {
+ int colorIdx = row * (nx - 1) + col;
+ if (isForward) colorIdx--;
+ if (color[colorIdx] == NinePatchChunk.TRANSPARENT_COLOR) {
+ index[idxCount] = index[idxCount - 1];
+ ++idxCount;
+ index[idxCount++] = (byte) k;
+ }
+ }
+
+ index[idxCount++] = (byte) k;
+ index[idxCount++] = (byte) (k + nx);
+ }
+ }
+
+ mIdxCount = idxCount;
+
+ int size = (pntCount * 2) * (Float.SIZE / Byte.SIZE);
+ mXyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ mUvBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
+ mIndexBuffer = allocateDirectNativeOrderBuffer(mIdxCount);
+
+ mXyBuffer.put(xy, 0, pntCount * 2).position(0);
+ mUvBuffer.put(uv, 0, pntCount * 2).position(0);
+ mIndexBuffer.put(index, 0, idxCount).position(0);
+ }
+
+ private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+ return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ }
+
+ private void prepareBuffers(GLCanvas canvas) {
+ mBufferNames = new int[3];
+ GL11 gl = canvas.getGLInstance();
+ gl.glGenBuffers(3, mBufferNames, 0);
+
+ gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[0]);
+ gl.glBufferData(GL11.GL_ARRAY_BUFFER,
+ mXyBuffer.capacity() * (Float.SIZE / Byte.SIZE),
+ mXyBuffer, GL11.GL_STATIC_DRAW);
+
+ gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBufferNames[1]);
+ gl.glBufferData(GL11.GL_ARRAY_BUFFER,
+ mUvBuffer.capacity() * (Float.SIZE / Byte.SIZE),
+ mUvBuffer, GL11.GL_STATIC_DRAW);
+
+ gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, mBufferNames[2]);
+ gl.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER,
+ mIndexBuffer.capacity(),
+ mIndexBuffer, GL11.GL_STATIC_DRAW);
+
+ // These buffers are never used again.
+ mXyBuffer = null;
+ mUvBuffer = null;
+ mIndexBuffer = null;
+ }
+
+ public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) {
+ if (mBufferNames == null) {
+ prepareBuffers(canvas);
+ }
+ canvas.drawMesh(tex, x, y, mBufferNames[0], mBufferNames[1],
+ mBufferNames[2], mIdxCount);
+ }
+
+ public void recycle(GLCanvas canvas) {
+ if (mBufferNames != null) {
+ canvas.deleteBuffer(mBufferNames[0]);
+ canvas.deleteBuffer(mBufferNames[1]);
+ canvas.deleteBuffer(mBufferNames[2]);
+ mBufferNames = null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/OnSelectedListener.java b/src/com/android/gallery3d/ui/OnSelectedListener.java
new file mode 100644
index 000000000..2cc5809bf
--- /dev/null
+++ b/src/com/android/gallery3d/ui/OnSelectedListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+public interface OnSelectedListener {
+ public void onSelected(GLView source);
+}
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
new file mode 100644
index 000000000..641fc2c8e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -0,0 +1,112 @@
+/*
+ * 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.ui.PositionRepository.Position;
+import com.android.gallery3d.util.GalleryUtils;
+
+import android.opengl.Matrix;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+// This class does the overscroll effect.
+class Paper {
+ private static final String TAG = "Paper";
+ private static final int ROTATE_FACTOR = 4;
+ private OverscrollAnimation mAnimationLeft = new OverscrollAnimation();
+ private OverscrollAnimation mAnimationRight = new OverscrollAnimation();
+ private int mWidth, mHeight;
+ private float[] mMatrix = new float[16];
+
+ public void overScroll(float distance) {
+ if (distance < 0) {
+ mAnimationLeft.scroll(-distance);
+ } else {
+ mAnimationRight.scroll(distance);
+ }
+ }
+
+ public boolean advanceAnimation(long currentTimeMillis) {
+ return mAnimationLeft.advanceAnimation(currentTimeMillis)
+ | mAnimationRight.advanceAnimation(currentTimeMillis);
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ public float[] getTransform(Position target, Position base,
+ float scrollX, float scrollY) {
+ float left = mAnimationLeft.getValue();
+ float right = mAnimationRight.getValue();
+ float screenX = target.x - scrollX;
+ float t = ((mWidth - screenX) * left - screenX * right) / (mWidth * mWidth);
+ // compress t to the range (-1, 1) by the function
+ // f(t) = (1 / (1 + e^-t) - 0.5) * 2
+ // then multiply by 90 to make the range (-45, 45)
+ float degrees =
+ (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45;
+ Matrix.setIdentityM(mMatrix, 0);
+ Matrix.translateM(mMatrix, 0, mMatrix, 0, base.x, base.y, base.z);
+ Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0);
+ Matrix.translateM(mMatrix, 0, mMatrix, 0,
+ target.x - base.x, target.y - base.y, target.z - base.z);
+ return mMatrix;
+ }
+}
+
+class OverscrollAnimation {
+ private static final String TAG = "OverscrollAnimation";
+ private static final long START_ANIMATION = -1;
+ private static final long NO_ANIMATION = -2;
+ private static final long ANIMATION_DURATION = 500;
+
+ private long mAnimationStartTime = NO_ANIMATION;
+ private float mVelocity;
+ private float mCurrentValue;
+
+ public void scroll(float distance) {
+ mAnimationStartTime = START_ANIMATION;
+ mCurrentValue += distance;
+ }
+
+ public boolean advanceAnimation(long currentTimeMillis) {
+ if (mAnimationStartTime == NO_ANIMATION) return false;
+ if (mAnimationStartTime == START_ANIMATION) {
+ mAnimationStartTime = currentTimeMillis;
+ return true;
+ }
+
+ long deltaTime = currentTimeMillis - mAnimationStartTime;
+ float t = deltaTime / 100f;
+ mCurrentValue *= Math.pow(0.5f, t);
+ mAnimationStartTime = currentTimeMillis;
+
+ if (mCurrentValue < 1) {
+ mAnimationStartTime = NO_ANIMATION;
+ mCurrentValue = 0;
+ return false;
+ }
+ return true;
+ }
+
+ public float getValue() {
+ return mCurrentValue;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
new file mode 100644
index 000000000..aba572b00
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -0,0 +1,1191 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.ui.PositionRepository.Position;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.RectF;
+import android.os.Message;
+import android.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+public class PhotoView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "PhotoView";
+
+ public static final int INVALID_SIZE = -1;
+
+ private static final int MSG_TRANSITION_COMPLETE = 1;
+ private static final int MSG_SHOW_LOADING = 2;
+
+ private static final long DELAY_SHOW_LOADING = 250; // 250ms;
+
+ private static final int TRANS_NONE = 0;
+ private static final int TRANS_SWITCH_NEXT = 3;
+ private static final int TRANS_SWITCH_PREVIOUS = 4;
+
+ public static final int TRANS_SLIDE_IN_RIGHT = 1;
+ public static final int TRANS_SLIDE_IN_LEFT = 2;
+ public static final int TRANS_OPEN_ANIMATION = 5;
+
+ private static final int LOADING_INIT = 0;
+ private static final int LOADING_TIMEOUT = 1;
+ private static final int LOADING_COMPLETE = 2;
+ private static final int LOADING_FAIL = 3;
+
+ private static final int ENTRY_PREVIOUS = 0;
+ private static final int ENTRY_NEXT = 1;
+
+ private static final int IMAGE_GAP = 96;
+ private static final int SWITCH_THRESHOLD = 256;
+ private static final float SWIPE_THRESHOLD = 300f;
+
+ private static final float DEFAULT_TEXT_SIZE = 20;
+
+ // We try to scale up the image to fill the screen. But in order not to
+ // scale too much for small icons, we limit the max up-scaling factor here.
+ private static final float SCALE_LIMIT = 4;
+
+ public interface PhotoTapListener {
+ public void onSingleTapUp(int x, int y);
+ }
+
+ // the previous/next image entries
+ private final ScreenNailEntry mScreenNails[] = new ScreenNailEntry[2];
+
+ private final ScaleGestureDetector mScaleDetector;
+ private final GestureDetector mGestureDetector;
+ private final DownUpDetector mDownUpDetector;
+
+ private PhotoTapListener mPhotoTapListener;
+
+ private final PositionController mPositionController;
+
+ private Model mModel;
+ private StringTexture mLoadingText;
+ private StringTexture mNoThumbnailText;
+ private int mTransitionMode = TRANS_NONE;
+ private final TileImageView mTileView;
+ private Texture mVideoPlayIcon;
+
+ private boolean mShowVideoPlayIcon;
+ private ProgressSpinner mLoadingSpinner;
+
+ private SynchronizedHandler mHandler;
+
+ private int mLoadingState = LOADING_COMPLETE;
+
+ private RectF mTempRect = new RectF();
+ private float[] mTempPoints = new float[8];
+
+ private int mImageRotation;
+
+ private Path mOpenedItemPath;
+ private GalleryActivity mActivity;
+
+ public PhotoView(GalleryActivity activity) {
+ mActivity = activity;
+ mTileView = new TileImageView(activity);
+ addComponent(mTileView);
+ Context context = activity.getAndroidContext();
+ mLoadingSpinner = new ProgressSpinner(context);
+ mLoadingText = StringTexture.newInstance(
+ context.getString(R.string.loading),
+ DEFAULT_TEXT_SIZE, Color.WHITE);
+ mNoThumbnailText = StringTexture.newInstance(
+ context.getString(R.string.no_thumbnail),
+ DEFAULT_TEXT_SIZE, Color.WHITE);
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_TRANSITION_COMPLETE: {
+ onTransitionComplete();
+ break;
+ }
+ case MSG_SHOW_LOADING: {
+ if (mLoadingState == LOADING_INIT) {
+ // We don't need the opening animation
+ mOpenedItemPath = null;
+
+ mLoadingSpinner.startAnimation();
+ mLoadingState = LOADING_TIMEOUT;
+ invalidate();
+ }
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ };
+
+ mGestureDetector = new GestureDetector(context,
+ new MyGestureListener(), null, true /* ignoreMultitouch */);
+ mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener());
+ mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+
+ for (int i = 0, n = mScreenNails.length; i < n; ++i) {
+ mScreenNails[i] = new ScreenNailEntry();
+ }
+
+ mPositionController = new PositionController(this);
+ mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
+ }
+
+
+ public void setModel(Model model) {
+ if (mModel == model) return;
+ mModel = model;
+ mTileView.setModel(model);
+ if (model != null) notifyOnNewImage();
+ }
+
+ public void setPhotoTapListener(PhotoTapListener listener) {
+ mPhotoTapListener = listener;
+ }
+
+ private boolean setTileViewPosition(int centerX, int centerY, float scale) {
+ int inverseX = mPositionController.mImageW - centerX;
+ int inverseY = mPositionController.mImageH - centerY;
+ TileImageView t = mTileView;
+ int rotation = mImageRotation;
+ switch (rotation) {
+ case 0: return t.setPosition(centerX, centerY, scale, 0);
+ case 90: return t.setPosition(centerY, inverseX, scale, 90);
+ case 180: return t.setPosition(inverseX, inverseY, scale, 180);
+ case 270: return t.setPosition(inverseY, centerX, scale, 270);
+ default: throw new IllegalArgumentException(String.valueOf(rotation));
+ }
+ }
+
+ public void setPosition(int centerX, int centerY, float scale) {
+ if (setTileViewPosition(centerX, centerY, scale)) {
+ layoutScreenNails();
+ }
+ }
+
+ private void updateScreenNailEntry(int which, ImageData data) {
+ if (mTransitionMode == TRANS_SWITCH_NEXT
+ || mTransitionMode == TRANS_SWITCH_PREVIOUS) {
+ // ignore screen nail updating during switching
+ return;
+ }
+ ScreenNailEntry entry = mScreenNails[which];
+ if (data == null) {
+ entry.set(false, null, 0);
+ } else {
+ entry.set(true, data.bitmap, data.rotation);
+ }
+ }
+
+ // -1 previous, 0 current, 1 next
+ public void notifyImageInvalidated(int which) {
+ switch (which) {
+ case -1: {
+ updateScreenNailEntry(
+ ENTRY_PREVIOUS, mModel.getPreviousImage());
+ layoutScreenNails();
+ invalidate();
+ break;
+ }
+ case 1: {
+ updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
+ layoutScreenNails();
+ invalidate();
+ break;
+ }
+ case 0: {
+ // mImageWidth and mImageHeight will get updated
+ mTileView.notifyModelInvalidated();
+
+ mImageRotation = mModel.getImageRotation();
+ if (((mImageRotation / 90) & 1) == 0) {
+ mPositionController.setImageSize(
+ mTileView.mImageWidth, mTileView.mImageHeight);
+ } else {
+ mPositionController.setImageSize(
+ mTileView.mImageHeight, mTileView.mImageWidth);
+ }
+ updateLoadingState();
+ break;
+ }
+ }
+ }
+
+ private void updateLoadingState() {
+ // Possible transitions of mLoadingState:
+ // INIT --> TIMEOUT, COMPLETE, FAIL
+ // TIMEOUT --> COMPLETE, FAIL, INIT
+ // COMPLETE --> INIT
+ // FAIL --> INIT
+ if (mModel.getLevelCount() != 0 || mModel.getBackupImage() != null) {
+ mHandler.removeMessages(MSG_SHOW_LOADING);
+ mLoadingState = LOADING_COMPLETE;
+ } else if (mModel.isFailedToLoad()) {
+ mHandler.removeMessages(MSG_SHOW_LOADING);
+ mLoadingState = LOADING_FAIL;
+ } else if (mLoadingState != LOADING_INIT) {
+ mLoadingState = LOADING_INIT;
+ mHandler.removeMessages(MSG_SHOW_LOADING);
+ mHandler.sendEmptyMessageDelayed(
+ MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
+ }
+ }
+
+ public void notifyModelInvalidated() {
+ if (mModel == null) {
+ updateScreenNailEntry(ENTRY_PREVIOUS, null);
+ updateScreenNailEntry(ENTRY_NEXT, null);
+ } else {
+ updateScreenNailEntry(ENTRY_PREVIOUS, mModel.getPreviousImage());
+ updateScreenNailEntry(ENTRY_NEXT, mModel.getNextImage());
+ }
+ layoutScreenNails();
+
+ if (mModel == null) {
+ mTileView.notifyModelInvalidated();
+ mImageRotation = 0;
+ mPositionController.setImageSize(0, 0);
+ updateLoadingState();
+ } else {
+ notifyImageInvalidated(0);
+ }
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ mScaleDetector.onTouchEvent(event);
+ mDownUpDetector.onTouchEvent(event);
+ return true;
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ mTileView.layout(left, top, right, bottom);
+ if (changeSize) {
+ mPositionController.setViewSize(getWidth(), getHeight());
+ for (ScreenNailEntry entry : mScreenNails) {
+ entry.updateDrawingSize();
+ }
+ }
+ }
+
+ private static int gapToSide(int imageWidth, int viewWidth) {
+ return Math.max(0, (viewWidth - imageWidth) / 2);
+ }
+
+ private RectF getImageBounds() {
+ PositionController p = mPositionController;
+ float points[] = mTempPoints;
+
+ /*
+ * (p0,p1)----------(p2,p3)
+ * | |
+ * | |
+ * (p4,p5)----------(p6,p7)
+ */
+ points[0] = points[4] = -p.mCurrentX;
+ points[1] = points[3] = -p.mCurrentY;
+ points[2] = points[6] = p.mImageW - p.mCurrentX;
+ points[5] = points[7] = p.mImageH - p.mCurrentY;
+
+ RectF rect = mTempRect;
+ rect.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+ Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+
+ float scale = p.mCurrentScale;
+ float offsetX = p.mViewW / 2;
+ float offsetY = p.mViewH / 2;
+ for (int i = 0; i < 4; ++i) {
+ float x = points[i + i] * scale + offsetX;
+ float y = points[i + i + 1] * scale + offsetY;
+ if (x < rect.left) rect.left = x;
+ if (x > rect.right) rect.right = x;
+ if (y < rect.top) rect.top = y;
+ if (y > rect.bottom) rect.bottom = y;
+ }
+ return rect;
+ }
+
+
+ /*
+ * Here is how we layout the screen nails
+ *
+ * previous current next
+ * ___________ ________________ __________
+ * | _______ | | __________ | | ______ |
+ * | | | | | | right->| | | | | |
+ * | | |<-------->|<--left | | | | | |
+ * | |_______| | | | |__________| | | |______| |
+ * |___________| | |________________| |__________|
+ * | <--> gapToSide()
+ * |
+ * IMAGE_GAP + Max(previous.gapToSide(), current.gapToSide)
+ */
+ private void layoutScreenNails() {
+ int width = getWidth();
+ int height = getHeight();
+
+ // Use the image width in AC, since we may fake the size if the
+ // image is unavailable
+ RectF bounds = getImageBounds();
+ int left = Math.round(bounds.left);
+ int right = Math.round(bounds.right);
+ int gap = gapToSide(right - left, width);
+
+ // layout the previous image
+ ScreenNailEntry entry = mScreenNails[ENTRY_PREVIOUS];
+
+ if (entry.isEnabled()) {
+ entry.layoutRightEdgeAt(left - (
+ IMAGE_GAP + Math.max(gap, entry.gapToSide())));
+ }
+
+ // layout the next image
+ entry = mScreenNails[ENTRY_NEXT];
+ if (entry.isEnabled()) {
+ entry.layoutLeftEdgeAt(right + (
+ IMAGE_GAP + Math.max(gap, entry.gapToSide())));
+ }
+ }
+
+ private static class PositionController {
+ private long mAnimationStartTime = NO_ANIMATION;
+ private static final long NO_ANIMATION = -1;
+ private static final long LAST_ANIMATION = -2;
+
+ // Animation time in milliseconds.
+ private static final float ANIM_TIME_SCROLL = 0;
+ private static final float ANIM_TIME_SCALE = 50;
+ private static final float ANIM_TIME_SNAPBACK = 600;
+ private static final float ANIM_TIME_SLIDE = 400;
+ private static final float ANIM_TIME_ZOOM = 300;
+
+ private int mAnimationKind;
+ private final static int ANIM_KIND_SCROLL = 0;
+ private final static int ANIM_KIND_SCALE = 1;
+ private final static int ANIM_KIND_SNAPBACK = 2;
+ private final static int ANIM_KIND_SLIDE = 3;
+ private final static int ANIM_KIND_ZOOM = 4;
+
+ private PhotoView mViewer;
+ private int mImageW, mImageH;
+ private int mViewW, mViewH;
+
+ // The X, Y are the coordinate on bitmap which shows on the center of
+ // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual
+ // values used currently.
+ private int mCurrentX, mFromX, mToX;
+ private int mCurrentY, mFromY, mToY;
+ private float mCurrentScale, mFromScale, mToScale;
+
+ // The offsets from the center of the view to the user's focus point,
+ // converted to the bitmap domain.
+ private float mPrevOffsetX;
+ private float mPrevOffsetY;
+ private boolean mInScale;
+ private boolean mUseViewSize = true;
+
+ // The limits for position and scale.
+ private float mScaleMin, mScaleMax = 4f;
+
+ PositionController(PhotoView viewer) {
+ mViewer = viewer;
+ }
+
+ public void setImageSize(int width, int height) {
+
+ // If no image available, use view size.
+ if (width == 0 || height == 0) {
+ mUseViewSize = true;
+ mImageW = mViewW;
+ mImageH = mViewH;
+ mCurrentX = mImageW / 2;
+ mCurrentY = mImageH / 2;
+ mCurrentScale = 1;
+ mScaleMin = 1;
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ return;
+ }
+
+ mUseViewSize = false;
+
+ float ratio = Math.min(
+ (float) mImageW / width, (float) mImageH / height);
+
+ mCurrentX = translate(mCurrentX, mImageW, width, ratio);
+ mCurrentY = translate(mCurrentY, mImageH, height, ratio);
+ mCurrentScale = mCurrentScale * ratio;
+
+ mFromX = translate(mFromX, mImageW, width, ratio);
+ mFromY = translate(mFromY, mImageH, height, ratio);
+ mFromScale = mFromScale * ratio;
+
+ mToX = translate(mToX, mImageW, width, ratio);
+ mToY = translate(mToY, mImageH, height, ratio);
+ mToScale = mToScale * ratio;
+
+ mImageW = width;
+ mImageH = height;
+
+ mScaleMin = getMinimalScale(width, height, 0);
+
+ // Scale the new image to fit into the old one
+ if (mViewer.mOpenedItemPath != null) {
+ Position position = PositionRepository
+ .getInstance(mViewer.mActivity).get(Long.valueOf(
+ System.identityHashCode(mViewer.mOpenedItemPath)));
+ mViewer.mOpenedItemPath = null;
+ if (position != null) {
+ float scale = 240f / Math.min(width, height);
+ mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
+ mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
+ mCurrentScale = scale;
+ mViewer.mTransitionMode = TRANS_OPEN_ANIMATION;
+ startSnapback();
+ }
+ } else if (mAnimationStartTime == NO_ANIMATION) {
+ mCurrentScale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
+ }
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ }
+
+ public void zoomIn(float tapX, float tapY, float targetScale) {
+ if (targetScale > mScaleMax) targetScale = mScaleMax;
+ float scale = mCurrentScale;
+ float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX;
+ float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY;
+
+ // mCurrentX + (mViewW / 2) * (1 / targetScale) < mImageW
+ // mCurrentX - (mViewW / 2) * (1 / targetScale) > 0
+ float min = mViewW / 2.0f / targetScale;
+ float max = mImageW - mViewW / 2.0f / targetScale;
+ int targetX = (int) Utils.clamp(tempX, min, max);
+
+ min = mViewH / 2.0f / targetScale;
+ max = mImageH - mViewH / 2.0f / targetScale;
+ int targetY = (int) Utils.clamp(tempY, min, max);
+
+ // If the width of the image is less then the view, center the image
+ if (mImageW * targetScale < mViewW) targetX = mImageW / 2;
+ if (mImageH * targetScale < mViewH) targetY = mImageH / 2;
+
+ startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
+ }
+
+ public void resetToFullView() {
+ startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
+ }
+
+ private float getMinimalScale(int w, int h, int rotation) {
+ return Math.min(SCALE_LIMIT, ((rotation / 90) & 0x01) == 0
+ ? Math.min((float) mViewW / w, (float) mViewH / h)
+ : Math.min((float) mViewW / h, (float) mViewH / w));
+ }
+
+ private static int translate(int value, int size, int updateSize, float ratio) {
+ return Math.round(
+ (value + (updateSize * ratio - size) / 2f) / ratio);
+ }
+
+ public void setViewSize(int viewW, int viewH) {
+ boolean needLayout = mViewW == 0 || mViewH == 0;
+
+ mViewW = viewW;
+ mViewH = viewH;
+
+ if (mUseViewSize) {
+ mImageW = viewW;
+ mImageH = viewH;
+ mCurrentX = mImageW / 2;
+ mCurrentY = mImageH / 2;
+ mCurrentScale = 1;
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ } else {
+ boolean wasMinScale = (mCurrentScale == mScaleMin);
+ mScaleMin = Math.min(SCALE_LIMIT, Math.min(
+ (float) viewW / mImageW, (float) viewH / mImageH));
+ if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
+ mCurrentX = mImageW / 2;
+ mCurrentY = mImageH / 2;
+ mCurrentScale = mScaleMin;
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ }
+ }
+ }
+
+ public void stopAnimation() {
+ mAnimationStartTime = NO_ANIMATION;
+ }
+
+ public void skipAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) return;
+ mAnimationStartTime = NO_ANIMATION;
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ }
+
+ public void scrollBy(float dx, float dy, int type) {
+ startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
+ getTargetY() + Math.round(dy / mCurrentScale),
+ mCurrentScale, type);
+ }
+
+ public void beginScale(float focusX, float focusY) {
+ mInScale = true;
+ mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale;
+ mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale;
+ }
+
+ public void scaleBy(float s, float focusX, float focusY) {
+
+ // The focus point should keep this position on the ImageView.
+ // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX.
+ // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY.
+ float offsetX = (focusX - mViewW / 2f) / mCurrentScale;
+ float offsetY = (focusY - mViewH / 2f) / mCurrentScale;
+
+ startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX),
+ getTargetY() - Math.round(offsetY - mPrevOffsetY),
+ getTargetScale() * s, ANIM_KIND_SCALE);
+ mPrevOffsetX = offsetX;
+ mPrevOffsetY = offsetY;
+ }
+
+ public void endScale() {
+ mInScale = false;
+ startSnapbackIfNeeded();
+ }
+
+ public void up() {
+ startSnapback();
+ }
+
+ public void startSlideInAnimation(int fromX) {
+ mFromX = Math.round(fromX + (mImageW - mViewW) / 2f);
+ mFromY = Math.round(mImageH / 2f);
+ mCurrentX = mFromX;
+ mCurrentY = mFromY;
+ startAnimation(mImageW / 2, mImageH / 2, mCurrentScale,
+ ANIM_KIND_SLIDE);
+ }
+
+ public void startHorizontalSlide(int distance) {
+ scrollBy(distance, 0, ANIM_KIND_SLIDE);
+ }
+
+ private void startAnimation(
+ int centerX, int centerY, float scale, int kind) {
+ if (centerX == mCurrentX && centerY == mCurrentY
+ && scale == mCurrentScale) return;
+
+ mFromX = mCurrentX;
+ mFromY = mCurrentY;
+ mFromScale = mCurrentScale;
+
+ mToX = centerX;
+ mToY = centerY;
+ mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
+
+ // If the scaled dimension is smaller than the view,
+ // force it to be in the center.
+ if (Math.floor(mImageH * mToScale) <= mViewH) {
+ mToY = mImageH / 2;
+ }
+
+ mAnimationStartTime = SystemClock.uptimeMillis();
+ mAnimationKind = kind;
+ if (advanceAnimation()) mViewer.invalidate();
+ }
+
+ // Returns true if redraw is needed.
+ public boolean advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) {
+ return false;
+ } else if (mAnimationStartTime == LAST_ANIMATION) {
+ mAnimationStartTime = NO_ANIMATION;
+ if (mViewer.mTransitionMode != TRANS_NONE) {
+ mViewer.mHandler.sendEmptyMessage(MSG_TRANSITION_COMPLETE);
+ return false;
+ } else {
+ return startSnapbackIfNeeded();
+ }
+ }
+
+ float animationTime;
+ if (mAnimationKind == ANIM_KIND_SCROLL) {
+ animationTime = ANIM_TIME_SCROLL;
+ } else if (mAnimationKind == ANIM_KIND_SCALE) {
+ animationTime = ANIM_TIME_SCALE;
+ } else if (mAnimationKind == ANIM_KIND_SLIDE) {
+ animationTime = ANIM_TIME_SLIDE;
+ } else if (mAnimationKind == ANIM_KIND_ZOOM) {
+ animationTime = ANIM_TIME_ZOOM;
+ } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ {
+ animationTime = ANIM_TIME_SNAPBACK;
+ }
+
+ float progress;
+ if (animationTime == 0) {
+ progress = 1;
+ } else {
+ long now = SystemClock.uptimeMillis();
+ progress = (now - mAnimationStartTime) / animationTime;
+ }
+
+ if (progress >= 1) {
+ progress = 1;
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ mAnimationStartTime = LAST_ANIMATION;
+ } else {
+ float f = 1 - progress;
+ if (mAnimationKind == ANIM_KIND_SCROLL) {
+ progress = 1 - f; // linear
+ } else if (mAnimationKind == ANIM_KIND_SCALE) {
+ progress = 1 - f * f; // quadratic
+ } else /* if mAnimationKind is ANIM_KIND_SNAPBACK,
+ ANIM_KIND_ZOOM or ANIM_KIND_SLIDE */ {
+ progress = 1 - f * f * f * f * f; // x^5
+ }
+ linearInterpolate(progress);
+ }
+ mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+ return true;
+ }
+
+ private void linearInterpolate(float progress) {
+ // To linearly interpolate the position, we have to translate the
+ // coordinates. The meaning of the translated point (x, y) is the
+ // coordinates of the center of the bitmap on the view component.
+ float fromX = mViewW / 2f + (mImageW / 2f - mFromX) * mFromScale;
+ float toX = mViewW / 2f + (mImageW / 2f - mToX) * mToScale;
+ float currentX = fromX + progress * (toX - fromX);
+
+ float fromY = mViewH / 2f + (mImageH / 2f - mFromY) * mFromScale;
+ float toY = mViewH / 2f + (mImageH / 2f - mToY) * mToScale;
+ float currentY = fromY + progress * (toY - fromY);
+
+ mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
+ mCurrentX = Math.round(
+ mImageW / 2f + (mViewW / 2f - currentX) / mCurrentScale);
+ mCurrentY = Math.round(
+ mImageH / 2f + (mViewH / 2f - currentY) / mCurrentScale);
+ }
+
+ // Returns true if redraw is needed.
+ private boolean startSnapbackIfNeeded() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mInScale) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL && mViewer.isDown()) {
+ return false;
+ }
+ return startSnapback();
+ }
+
+ public boolean startSnapback() {
+ boolean needAnimation = false;
+ int x = mCurrentX;
+ int y = mCurrentY;
+ float scale = mCurrentScale;
+
+ if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
+ needAnimation = true;
+ scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
+ }
+
+ // The number of pixels when the edge is aligned.
+ int left = (int) Math.ceil(mViewW / (2 * scale));
+ int right = mImageW - left;
+ int top = (int) Math.ceil(mViewH / (2 * scale));
+ int bottom = mImageH - top;
+
+ if (mImageW * scale > mViewW) {
+ if (mCurrentX < left) {
+ needAnimation = true;
+ x = left;
+ } else if (mCurrentX > right) {
+ needAnimation = true;
+ x = right;
+ }
+ } else if (mCurrentX != mImageW / 2) {
+ needAnimation = true;
+ x = mImageW / 2;
+ }
+
+ if (mImageH * scale > mViewH) {
+ if (mCurrentY < top) {
+ needAnimation = true;
+ y = top;
+ } else if (mCurrentY > bottom) {
+ needAnimation = true;
+ y = bottom;
+ }
+ } else if (mCurrentY != mImageH / 2) {
+ needAnimation = true;
+ y = mImageH / 2;
+ }
+
+ if (needAnimation) {
+ startAnimation(x, y, scale, ANIM_KIND_SNAPBACK);
+ }
+
+ return needAnimation;
+ }
+
+ private float getTargetScale() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale;
+ return mToScale;
+ }
+
+ private int getTargetX() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX;
+ return mToX;
+ }
+
+ private int getTargetY() {
+ if (mAnimationStartTime == NO_ANIMATION
+ || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY;
+ return mToY;
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ PositionController p = mPositionController;
+
+ // Draw the current photo
+ if (mLoadingState == LOADING_COMPLETE) {
+ super.render(canvas);
+ }
+
+ // Draw the previous and the next photo
+ if (mTransitionMode != TRANS_SLIDE_IN_LEFT
+ && mTransitionMode != TRANS_SLIDE_IN_RIGHT
+ && mTransitionMode != TRANS_OPEN_ANIMATION) {
+ ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
+ ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
+
+ if (prevNail.mVisible) prevNail.draw(canvas);
+ if (nextNail.mVisible) nextNail.draw(canvas);
+ }
+
+ // Draw the progress spinner and the text below it
+ //
+ // (x, y) is where we put the center of the spinner.
+ // s is the size of the video play icon, and we use s to layout text
+ // because we want to keep the text at the same place when the video
+ // play icon is shown instead of the spinner.
+ int w = getWidth();
+ int h = getHeight();
+ int x = Math.round(getImageBounds().centerX());
+ int y = h / 2;
+ int s = Math.min(getWidth(), getHeight()) / 6;
+
+ if (mLoadingState == LOADING_TIMEOUT) {
+ StringTexture m = mLoadingText;
+ ProgressSpinner r = mLoadingSpinner;
+ r.draw(canvas, x - r.getWidth() / 2, y - r.getHeight() / 2);
+ m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ invalidate(); // we need to keep the spinner rotating
+ } else if (mLoadingState == LOADING_FAIL) {
+ StringTexture m = mNoThumbnailText;
+ m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+ }
+
+ // Draw the video play icon (in the place where the spinner was)
+ if (mShowVideoPlayIcon
+ && mLoadingState != LOADING_INIT
+ && mLoadingState != LOADING_TIMEOUT) {
+ mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
+ }
+
+ if (mPositionController.advanceAnimation()) invalidate();
+ }
+
+ private void stopCurrentSwipingIfNeeded() {
+ // Enable fast sweeping
+ if (mTransitionMode == TRANS_SWITCH_NEXT) {
+ mTransitionMode = TRANS_NONE;
+ mPositionController.stopAnimation();
+ switchToNextImage();
+ } else if (mTransitionMode == TRANS_SWITCH_PREVIOUS) {
+ mTransitionMode = TRANS_NONE;
+ mPositionController.stopAnimation();
+ switchToPreviousImage();
+ }
+ }
+
+ private static boolean isAlmostEquals(float a, float b) {
+ float diff = a - b;
+ return (diff < 0 ? -diff : diff) < 0.02f;
+ }
+
+ private boolean swipeImages(float velocity) {
+ if (mTransitionMode != TRANS_NONE
+ && mTransitionMode != TRANS_SWITCH_NEXT
+ && mTransitionMode != TRANS_SWITCH_PREVIOUS) return false;
+
+ ScreenNailEntry next = mScreenNails[ENTRY_NEXT];
+ ScreenNailEntry prev = mScreenNails[ENTRY_PREVIOUS];
+
+ int width = getWidth();
+
+ // If the edge of the current photo is visible and the sweeping velocity
+ // exceed the threshold, switch to next / previous image
+ PositionController controller = mPositionController;
+ if (isAlmostEquals(controller.mCurrentScale, controller.mScaleMin)) {
+ if (velocity < -SWIPE_THRESHOLD) {
+ stopCurrentSwipingIfNeeded();
+ if (next.isEnabled()) {
+ mTransitionMode = TRANS_SWITCH_NEXT;
+ controller.startHorizontalSlide(next.mOffsetX - width / 2);
+ return true;
+ }
+ return false;
+ }
+ if (velocity > SWIPE_THRESHOLD) {
+ stopCurrentSwipingIfNeeded();
+ if (prev.isEnabled()) {
+ mTransitionMode = TRANS_SWITCH_PREVIOUS;
+ controller.startHorizontalSlide(prev.mOffsetX - width / 2);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ if (mTransitionMode != TRANS_NONE) return false;
+
+ // Decide whether to swiping to the next/prev image in the zoom-in case
+ RectF bounds = getImageBounds();
+ int left = Math.round(bounds.left);
+ int right = Math.round(bounds.right);
+ int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width);
+
+ // If we have moved the picture a lot, switching.
+ if (next.isEnabled() && threshold < width - right) {
+ mTransitionMode = TRANS_SWITCH_NEXT;
+ controller.startHorizontalSlide(next.mOffsetX - width / 2);
+ return true;
+ }
+ if (prev.isEnabled() && threshold < left) {
+ mTransitionMode = TRANS_SWITCH_PREVIOUS;
+ controller.startHorizontalSlide(prev.mOffsetX - width / 2);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean mIgnoreUpEvent = false;
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onScroll(
+ MotionEvent e1, MotionEvent e2, float dx, float dy) {
+ if (mTransitionMode != TRANS_NONE) return true;
+ mPositionController.scrollBy(
+ dx, dy, PositionController.ANIM_KIND_SCROLL);
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (mPhotoTapListener != null) {
+ mPhotoTapListener.onSingleTapUp((int) e.getX(), (int) e.getY());
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ mIgnoreUpEvent = true;
+ if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) {
+ mPositionController.up();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (mTransitionMode != TRANS_NONE) return true;
+ PositionController controller = mPositionController;
+ float scale = controller.mCurrentScale;
+ // onDoubleTap happened on the second ACTION_DOWN.
+ // We need to ignore the next UP event.
+ mIgnoreUpEvent = true;
+ if (scale <= 1.0f || isAlmostEquals(scale, controller.mScaleMin)) {
+ controller.zoomIn(
+ e.getX(), e.getY(), Math.max(1.5f, scale * 1.5f));
+ } else {
+ controller.resetToFullView();
+ }
+ return true;
+ }
+ }
+
+ private class MyScaleListener
+ extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ float scale = detector.getScaleFactor();
+ if (Float.isNaN(scale) || Float.isInfinite(scale)
+ || mTransitionMode != TRANS_NONE) return true;
+ mPositionController.scaleBy(scale,
+ detector.getFocusX(), detector.getFocusY());
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ if (mTransitionMode != TRANS_NONE) return false;
+ mPositionController.beginScale(
+ detector.getFocusX(), detector.getFocusY());
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mPositionController.endScale();
+ swipeImages(0);
+ }
+ }
+
+ public void notifyOnNewImage() {
+ mPositionController.setImageSize(0, 0);
+ }
+
+ public void startSlideInAnimation(int direction) {
+ PositionController a = mPositionController;
+ a.stopAnimation();
+ switch (direction) {
+ case TRANS_SLIDE_IN_LEFT: {
+ mTransitionMode = TRANS_SLIDE_IN_LEFT;
+ a.startSlideInAnimation(a.mViewW);
+ break;
+ }
+ case TRANS_SLIDE_IN_RIGHT: {
+ mTransitionMode = TRANS_SLIDE_IN_RIGHT;
+ a.startSlideInAnimation(-a.mViewW);
+ break;
+ }
+ default: throw new IllegalArgumentException(String.valueOf(direction));
+ }
+ }
+
+ private class MyDownUpListener implements DownUpDetector.DownUpListener {
+ public void onDown(MotionEvent e) {
+ }
+
+ public void onUp(MotionEvent e) {
+ if (mIgnoreUpEvent) {
+ mIgnoreUpEvent = false;
+ return;
+ }
+ if (!swipeImages(0) && mTransitionMode == TRANS_NONE) {
+ mPositionController.up();
+ }
+ }
+ }
+
+ private void switchToNextImage() {
+ // We update the texture here directly to prevent texture uploading.
+ ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
+ ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
+ mTileView.invalidateTiles();
+ if (prevNail.mTexture != null) prevNail.mTexture.recycle();
+ prevNail.mTexture = mTileView.mBackupImage;
+ mTileView.mBackupImage = nextNail.mTexture;
+ nextNail.mTexture = null;
+ mModel.next();
+ }
+
+ private void switchToPreviousImage() {
+ // We update the texture here directly to prevent texture uploading.
+ ScreenNailEntry prevNail = mScreenNails[ENTRY_PREVIOUS];
+ ScreenNailEntry nextNail = mScreenNails[ENTRY_NEXT];
+ mTileView.invalidateTiles();
+ if (nextNail.mTexture != null) nextNail.mTexture.recycle();
+ nextNail.mTexture = mTileView.mBackupImage;
+ mTileView.mBackupImage = prevNail.mTexture;
+ nextNail.mTexture = null;
+ mModel.previous();
+ }
+
+ private void onTransitionComplete() {
+ int mode = mTransitionMode;
+ mTransitionMode = TRANS_NONE;
+
+ if (mModel == null) return;
+ if (mode == TRANS_SWITCH_NEXT) {
+ switchToNextImage();
+ } else if (mode == TRANS_SWITCH_PREVIOUS) {
+ switchToPreviousImage();
+ }
+ }
+
+ private boolean isDown() {
+ return mDownUpDetector.isDown();
+ }
+
+ public static interface Model extends TileImageView.Model {
+ public void next();
+ public void previous();
+ public int getImageRotation();
+
+ // Return null if the specified image is unavailable.
+ public ImageData getNextImage();
+ public ImageData getPreviousImage();
+ }
+
+ public static class ImageData {
+ public int rotation;
+ public Bitmap bitmap;
+
+ public ImageData(Bitmap bitmap, int rotation) {
+ this.bitmap = bitmap;
+ this.rotation = rotation;
+ }
+ }
+
+ private static int getRotated(int degree, int original, int theother) {
+ return ((degree / 90) & 1) == 0 ? original : theother;
+ }
+
+ private class ScreenNailEntry {
+ private boolean mVisible;
+ private boolean mEnabled;
+
+ private int mRotation;
+ private int mDrawWidth;
+ private int mDrawHeight;
+ private int mOffsetX;
+
+ private BitmapTexture mTexture;
+
+ public void set(boolean enabled, Bitmap bitmap, int rotation) {
+ mEnabled = enabled;
+ mRotation = rotation;
+ if (bitmap == null) {
+ if (mTexture != null) mTexture.recycle();
+ mTexture = null;
+ } else {
+ if (mTexture != null) {
+ if (mTexture.getBitmap() != bitmap) {
+ mTexture.recycle();
+ mTexture = new BitmapTexture(bitmap);
+ }
+ } else {
+ mTexture = new BitmapTexture(bitmap);
+ }
+ updateDrawingSize();
+ }
+ }
+
+ public void layoutRightEdgeAt(int x) {
+ mVisible = x > 0;
+ mOffsetX = x - getRotated(
+ mRotation, mDrawWidth, mDrawHeight) / 2;
+ }
+
+ public void layoutLeftEdgeAt(int x) {
+ mVisible = x < getWidth();
+ mOffsetX = x + getRotated(
+ mRotation, mDrawWidth, mDrawHeight) / 2;
+ }
+
+ public int gapToSide() {
+ return ((mRotation / 90) & 1) != 0
+ ? PhotoView.gapToSide(mDrawHeight, getWidth())
+ : PhotoView.gapToSide(mDrawWidth, getWidth());
+ }
+
+ public void updateDrawingSize() {
+ if (mTexture == null) return;
+
+ int width = mTexture.getWidth();
+ int height = mTexture.getHeight();
+ float s = mPositionController.getMinimalScale(width, height, mRotation);
+ mDrawWidth = Math.round(width * s);
+ mDrawHeight = Math.round(height * s);
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void draw(GLCanvas canvas) {
+ int x = mOffsetX;
+ int y = getHeight() / 2;
+
+ if (mTexture != null) {
+ if (mRotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.translate(x, y, 0);
+ canvas.rotate(mRotation, 0, 0, 1); //mRotation
+ canvas.translate(-x, -y, 0);
+ }
+ mTexture.draw(canvas, x - mDrawWidth / 2, y - mDrawHeight / 2,
+ mDrawWidth, mDrawHeight);
+ if (mRotation != 0) {
+ canvas.restore();
+ }
+ }
+ }
+ }
+
+ public void pause() {
+ mPositionController.skipAnimation();
+ mTransitionMode = TRANS_NONE;
+ mTileView.freeTextures();
+ for (ScreenNailEntry entry : mScreenNails) {
+ entry.set(false, null, 0);
+ }
+ }
+
+ public void resume() {
+ mTileView.prepareTextures();
+ }
+
+ public void setOpenedItem(Path itemPath) {
+ mOpenedItemPath = itemPath;
+ }
+
+ public void showVideoPlayIcon(boolean show) {
+ mShowVideoPlayIcon = show;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PositionProvider.java b/src/com/android/gallery3d/ui/PositionProvider.java
new file mode 100644
index 000000000..930c61ee9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionProvider.java
@@ -0,0 +1,23 @@
+/*
+ * 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.ui.PositionRepository.Position;
+
+public interface PositionProvider {
+ public Position getPosition(long identity, Position target);
+}
diff --git a/src/com/android/gallery3d/ui/PositionRepository.java b/src/com/android/gallery3d/ui/PositionRepository.java
new file mode 100644
index 000000000..0b829fa25
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionRepository.java
@@ -0,0 +1,139 @@
+/*
+ * 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.GalleryActivity;
+import com.android.gallery3d.common.Utils;
+
+import java.util.HashMap;
+import java.util.WeakHashMap;
+
+public class PositionRepository {
+ private static final WeakHashMap<GalleryActivity, PositionRepository>
+ sMap = new WeakHashMap<GalleryActivity, PositionRepository>();
+
+ public static class Position implements Cloneable {
+ public float x;
+ public float y;
+ public float z;
+ public float theta;
+ public float alpha;
+
+ public Position() {
+ }
+
+ public Position(float x, float y, float z) {
+ this(x, y, z, 0f, 1f);
+ }
+
+ public Position(float x, float y, float z, float ftheta, float alpha) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.theta = ftheta;
+ this.alpha = alpha;
+ }
+
+ @Override
+ public Position clone() {
+ try {
+ return (Position) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(); // we do support clone.
+ }
+ }
+
+ public void set(Position another) {
+ x = another.x;
+ y = another.y;
+ z = another.z;
+ theta = another.theta;
+ alpha = another.alpha;
+ }
+
+ public void set(float x, float y, float z, float ftheta, float alpha) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.theta = ftheta;
+ this.alpha = alpha;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof Position)) return false;
+ Position position = (Position) object;
+ return x == position.x && y == position.y && z == position.z
+ && theta == position.theta
+ && alpha == position.alpha;
+ }
+
+ public static void interpolate(
+ Position source, Position target, Position output, float progress) {
+ if (progress < 1f) {
+ output.set(
+ Utils.interpolateScale(source.x, target.x, progress),
+ Utils.interpolateScale(source.y, target.y, progress),
+ Utils.interpolateScale(source.z, target.z, progress),
+ Utils.interpolateAngle(source.theta, target.theta, progress),
+ Utils.interpolateScale(source.alpha, target.alpha, progress));
+ } else {
+ output.set(target);
+ }
+ }
+ }
+
+ public static PositionRepository getInstance(GalleryActivity activity) {
+ PositionRepository repository = sMap.get(activity);
+ if (repository == null) {
+ repository = new PositionRepository();
+ sMap.put(activity, repository);
+ }
+ return repository;
+ }
+
+ private HashMap<Long, Position> mData = new HashMap<Long, Position>();
+ private int mOffsetX;
+ private int mOffsetY;
+ private Position mTempPosition = new Position();
+
+ public Position get(Long identity) {
+ Position position = mData.get(identity);
+ if (position == null) return null;
+ mTempPosition.set(position);
+ position = mTempPosition;
+ position.x -= mOffsetX;
+ position.y -= mOffsetY;
+ return position;
+ }
+
+ public void setOffset(int offsetX, int offsetY) {
+ mOffsetX = offsetX;
+ mOffsetY = offsetY;
+ }
+
+ public void putPosition(Long identity, Position position) {
+ Position clone = position.clone();
+ clone.x += mOffsetX;
+ clone.y += mOffsetY;
+ mData.put(identity, clone);
+ }
+
+ public void clear() {
+ mData.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ProgressBar.java b/src/com/android/gallery3d/ui/ProgressBar.java
new file mode 100644
index 000000000..c62fa9a62
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ProgressBar.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class ProgressBar extends GLView {
+ private final int MAX_PROGRESS = 10000;
+ private int mProgress;
+ private int mSecondaryProgress;
+ private BasicTexture mProgressTexture;
+ private BasicTexture mSecondaryProgressTexture;
+ private BasicTexture mBackgrondTexture;
+
+
+ public ProgressBar(Context context, int resProgress,
+ int resSecondaryProgress, int resBackground) {
+ mProgressTexture = new NinePatchTexture(context, resProgress);
+ mSecondaryProgressTexture = new NinePatchTexture(
+ context, resSecondaryProgress);
+ mBackgrondTexture = new NinePatchTexture(context, resBackground);
+
+ }
+
+ // The progress value is between 0 (empty) and MAX_PROGRESS (full).
+ public void setProgress(int progress) {
+ mProgress = progress;
+ }
+
+ public void setSecondaryProgress(int progress) {
+ mSecondaryProgress = progress;
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Rect p = mPaddings;
+
+ int width = getWidth() - p.left - p.right;
+ int height = getHeight() - p.top - p.bottom;
+
+ int primary = width * mProgress / MAX_PROGRESS;
+ int secondary = width * mSecondaryProgress / MAX_PROGRESS;
+ int x = p.left;
+ int y = p.top;
+
+ canvas.drawTexture(mBackgrondTexture, x, y, width, height);
+ canvas.drawTexture(mProgressTexture, x, y, primary, height);
+ canvas.drawTexture(mSecondaryProgressTexture, x, y, secondary, height);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java
new file mode 100644
index 000000000..e4d60242b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ProgressSpinner.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+
+import android.content.Context;
+
+public class ProgressSpinner {
+ private static float ROTATE_SPEED_OUTER = 1080f / 3500f;
+ private static float ROTATE_SPEED_INNER = -720f / 3500f;
+ private final ResourceTexture mOuter;
+ private final ResourceTexture mInner;
+ private final int mWidth;
+ private final int mHeight;
+
+ private float mInnerDegree = 0f;
+ private float mOuterDegree = 0f;
+ private long mAnimationTimestamp = -1;
+
+ public ProgressSpinner(Context context) {
+ mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo);
+ mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo);
+
+ mWidth = Math.max(mOuter.getWidth(), mInner.getWidth());
+ mHeight = Math.max(mOuter.getHeight(), mInner.getHeight());
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public void startAnimation() {
+ mAnimationTimestamp = -1;
+ mOuterDegree = 0;
+ mInnerDegree = 0;
+ }
+
+ public void draw(GLCanvas canvas, int x, int y) {
+ long now = canvas.currentAnimationTimeMillis();
+ if (mAnimationTimestamp == -1) mAnimationTimestamp = now;
+ mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER;
+ mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER;
+
+ mAnimationTimestamp = now;
+
+ // just preventing overflow
+ if (mOuterDegree > 360) mOuterDegree -= 360f;
+ if (mInnerDegree < 0) mInnerDegree += 360f;
+
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+ canvas.translate(x + mWidth / 2, y + mHeight / 2, 0);
+ canvas.rotate(mInnerDegree, 0, 0, 1);
+ mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2);
+ canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1);
+ mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2);
+ canvas.restore();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/RawTexture.java b/src/com/android/gallery3d/ui/RawTexture.java
new file mode 100644
index 000000000..c1be435d1
--- /dev/null
+++ b/src/com/android/gallery3d/ui/RawTexture.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import javax.microedition.khronos.opengles.GL11;
+
+// RawTexture is used for texture created by glCopyTexImage2D.
+//
+// It will throw RuntimeException in onBind() if used with a different GL
+// context. It is only used internally by copyTexture() in GLCanvas.
+class RawTexture extends BasicTexture {
+
+ private RawTexture(GLCanvas canvas, int id) {
+ super(canvas, id, STATE_LOADED);
+ }
+
+ public static RawTexture newInstance(GLCanvas canvas) {
+ int[] textureId = new int[1];
+ GL11 gl = canvas.getGLInstance();
+ gl.glGenTextures(1, textureId, 0);
+ return new RawTexture(canvas, textureId[0]);
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ if (mCanvasRef.get() != canvas) {
+ throw new RuntimeException("cannot bind to different canvas");
+ }
+ return true;
+ }
+
+ public boolean isOpaque() {
+ return true;
+ }
+
+ @Override
+ public void yield() {
+ // we cannot free the texture because we have no backup.
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ResourceTexture.java b/src/com/android/gallery3d/ui/ResourceTexture.java
new file mode 100644
index 000000000..08fb89187
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ResourceTexture.java
@@ -0,0 +1,52 @@
+/*
+ * 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.common.Utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+// ResourceTexture is a texture whose Bitmap is decoded from a resource.
+// By default ResourceTexture is not opaque.
+public class ResourceTexture extends UploadedTexture {
+
+ protected final Context mContext;
+ protected final int mResId;
+
+ public ResourceTexture(Context context, int resId) {
+ mContext = Utils.checkNotNull(context);
+ mResId = resId;
+ setOpaque(false);
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ return BitmapFactory.decodeResource(
+ mContext.getResources(), mResId, options);
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ if (!inFinalizer()) {
+ bitmap.recycle();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java
new file mode 100644
index 000000000..7e375c9f7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollBarView.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class ScrollBarView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ScrollBarView";
+
+ public interface Listener {
+ void onScrollBarPositionChanged(int position);
+ }
+
+ private int mBarHeight;
+
+ private int mGripHeight;
+ private int mGripPosition; // left side of the grip
+ private int mGripWidth; // zero if the grip is disabled
+ private int mGivenGripWidth;
+
+ private int mContentPosition;
+ private int mContentTotal;
+
+ private Listener mListener;
+ private NinePatchTexture mScrollBarTexture;
+
+ public ScrollBarView(Context context, int gripHeight, int gripWidth) {
+ mScrollBarTexture = new NinePatchTexture(
+ context, R.drawable.scrollbar_handle_holo_dark);
+ mGripPosition = 0;
+ mGripWidth = 0;
+ mGivenGripWidth = gripWidth;
+ mGripHeight = gripHeight;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ if (!changed) return;
+ mBarHeight = bottom - top;
+ }
+
+ // The content position is between 0 to "total". The current position is
+ // in "position".
+ public void setContentPosition(int position, int total) {
+ if (position == mContentPosition && total == mContentTotal) {
+ return;
+ }
+
+ invalidate();
+
+ mContentPosition = position;
+ mContentTotal = total;
+
+ // If the grip cannot move, don't draw it.
+ if (mContentTotal <= 0) {
+ mGripPosition = 0;
+ mGripWidth = 0;
+ return;
+ }
+
+ // Map from the content range to scroll bar range.
+ //
+ // mContentTotal --> getWidth() - mGripWidth
+ // mContentPosition --> mGripPosition
+ mGripWidth = mGivenGripWidth;
+ float r = (getWidth() - mGripWidth) / (float) mContentTotal;
+ mGripPosition = Math.round(r * mContentPosition);
+ }
+
+ private void notifyContentPositionFromGrip() {
+ if (mContentTotal <= 0) return;
+ float r = (getWidth() - mGripWidth) / (float) mContentTotal;
+ int newContentPosition = Math.round(mGripPosition / r);
+ mListener.onScrollBarPositionChanged(newContentPosition);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ if (mGripWidth == 0) return;
+ Rect b = bounds();
+ int y = (mBarHeight - mGripHeight) / 2;
+ mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight);
+ }
+
+ // The onTouch() handler is disabled because now we don't want the user
+ // to drag the bar (it's an indicator only).
+ /*
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ int x = (int) event.getX();
+ return (x >= mGripPosition && x < mGripPosition + mGripWidth);
+ }
+ case MotionEvent.ACTION_MOVE: {
+ // Adjust x by mGripWidth / 2 so the center of the grip
+ // matches the touch position.
+ int x = (int) event.getX() - mGripWidth / 2;
+ x = Utils.clamp(x, 0, getWidth() - mGripWidth);
+ if (mGripPosition != x) {
+ mGripPosition = x;
+ notifyContentPositionFromGrip();
+ invalidate();
+ }
+ break;
+ }
+ }
+ return true;
+ }
+ */
+}
diff --git a/src/com/android/gallery3d/ui/ScrollView.java b/src/com/android/gallery3d/ui/ScrollView.java
new file mode 100644
index 000000000..f7628335c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollView.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+
+// The current implementation can only scroll vertically.
+public class ScrollView extends GLView {
+
+ private static final int MIN_SCROLLER_HEIGHT = 20;
+
+ private NinePatchTexture mScroller;
+ private int mScrollLimit = 0;
+ private int mScrollerHeight = MIN_SCROLLER_HEIGHT;
+ private GestureDetector mGestureDetector;
+
+ public ScrollView(Context context) {
+ mScroller = new NinePatchTexture(context, R.drawable.scrollbar_handle_holo_dark);
+ mGestureDetector = new GestureDetector(context, new MyGestureListener());
+ }
+
+ private GLView getContentView() {
+ return getComponentCount() == 0 ? null : getComponent(0);
+ }
+
+ @Override
+ public void onLayout(boolean sizeChange, int l, int t, int r, int b) {
+ GLView content = getContentView();
+ int width = getWidth();
+ int height = getHeight();
+ content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.UNSPECIFIED);
+ int contentHeight = content.getMeasuredHeight();
+ content.layout(0, 0, width, contentHeight);
+ if (height < contentHeight) {
+ mScrollLimit = contentHeight - height;
+ mScrollerHeight = Math.max(MIN_SCROLLER_HEIGHT,
+ height * height / contentHeight);
+ } else {
+ mScrollLimit = 0;
+ }
+ mScrollY = Utils.clamp(mScrollY, 0, mScrollLimit);
+ }
+
+ @Override
+ public void render(GLCanvas canvas) {
+ GLView content = getContentView();
+ if (content == null) return;
+ int width = getWidth();
+ int height = getHeight();
+
+ canvas.save(GLCanvas.SAVE_FLAG_CLIP);
+ canvas.clipRect(0, 0, width, height);
+ super.render(canvas);
+ if (mScrollLimit > 0) {
+ int x = getWidth() - mScroller.getWidth();
+ int y = (height - mScrollerHeight) * mScrollY / mScrollLimit;
+ mScroller.draw(canvas, x, y, mScroller.getWidth(), mScrollerHeight);
+ }
+ canvas.restore();
+ }
+
+ @Override
+ public boolean onTouch(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ return true;
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onScroll(MotionEvent e1,
+ MotionEvent e2, float distanceX, float distanceY) {
+ mScrollY = Utils.clamp(mScrollY + (int) distanceY, 0, mScrollLimit);
+ invalidate();
+ return true;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
new file mode 100644
index 000000000..9f19cec96
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -0,0 +1,93 @@
+/*
+ * 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.common.Utils;
+
+import android.content.Context;
+import android.view.ViewConfiguration;
+import android.widget.OverScroller;
+
+public class ScrollerHelper {
+ private OverScroller mScroller;
+ private int mOverflingDistance;
+ private boolean mOverflingEnabled;
+
+ public ScrollerHelper(Context context) {
+ mScroller = new OverScroller(context);
+ ViewConfiguration configuration = ViewConfiguration.get(context);
+ mOverflingDistance = configuration.getScaledOverflingDistance();
+ }
+
+ public void setOverfling(boolean enabled) {
+ mOverflingEnabled = enabled;
+ }
+
+ /**
+ * Call this when you want to know the new location. The position will be
+ * updated and can be obtained by getPosition(). Returns true if the
+ * animation is not yet finished.
+ */
+ public boolean advanceAnimation(long currentTimeMillis) {
+ return mScroller.computeScrollOffset();
+ }
+
+ public boolean isFinished() {
+ return mScroller.isFinished();
+ }
+
+ public void forceFinished() {
+ mScroller.forceFinished(true);
+ }
+
+ public int getPosition() {
+ return mScroller.getCurrX();
+ }
+
+ public void setPosition(int position) {
+ mScroller.startScroll(
+ position, 0, // startX, startY
+ 0, 0, 0); // dx, dy, duration
+
+ // This forces the scroller to reach the final position.
+ mScroller.abortAnimation();
+ }
+
+ public void fling(int velocity, int min, int max) {
+ int currX = getPosition();
+ mScroller.fling(
+ currX, 0, // startX, startY
+ velocity, 0, // velocityX, velocityY
+ min, max, // minX, maxX
+ 0, 0, // minY, maxY
+ mOverflingEnabled ? mOverflingDistance : 0, 0);
+ }
+
+ public boolean startScroll(int distance, int min, int max) {
+ int currPosition = mScroller.getCurrX();
+ int finalPosition = mScroller.getFinalX();
+ int newPosition = Utils.clamp(finalPosition + distance, min, max);
+ if (newPosition != currPosition) {
+ mScroller.startScroll(
+ currPosition, 0, // startX, startY
+ newPosition - currPosition, 0, 0); // dx, dy, duration
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionDrawer.java b/src/com/android/gallery3d/ui/SelectionDrawer.java
new file mode 100644
index 000000000..2655a221c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionDrawer.java
@@ -0,0 +1,89 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.data.Path;
+
+import android.graphics.Rect;
+
+/**
+ * Drawer class responsible for drawing selectable frame.
+ */
+public abstract class SelectionDrawer {
+ public static final int DATASOURCE_TYPE_NOT_CATEGORIZED = 0;
+ public static final int DATASOURCE_TYPE_LOCAL = 1;
+ public static final int DATASOURCE_TYPE_PICASA = 2;
+ public static final int DATASOURCE_TYPE_MTP = 3;
+ public static final int DATASOURCE_TYPE_CAMERA = 4;
+
+ public abstract void prepareDrawing();
+ public abstract void draw(GLCanvas canvas, Texture content,
+ int width, int height, int rotation, Path path,
+ int topIndex, int dataSourceType, int mediaType,
+ boolean wantCache, boolean isCaching);
+ public abstract void drawFocus(GLCanvas canvas, int width, int height);
+
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int mediaType) {
+ draw(canvas, content, width, height, rotation, path, 0,
+ DATASOURCE_TYPE_NOT_CATEGORIZED, mediaType,
+ false, false);
+ }
+
+ public static void drawWithRotation(GLCanvas canvas, Texture content,
+ int x, int y, int width, int height, int rotation) {
+ if (rotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.rotate(rotation, 0, 0, 1);
+ }
+
+ content.draw(canvas, x, y, width, height);
+
+ if (rotation != 0) {
+ canvas.restore();
+ }
+ }
+
+ public static void drawWithRotationAndGray(GLCanvas canvas, Texture content,
+ int x, int y, int width, int height, int rotation,
+ int topIndex) {
+ if (rotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.rotate(rotation, 0, 0, 1);
+ }
+
+ if (topIndex > 0 && (content instanceof BasicTexture)) {
+ float ratio = Utils.clamp(0.3f + 0.2f * topIndex, 0f, 1f);
+ canvas.drawMixed((BasicTexture) content, 0xFF222222, ratio,
+ x, y, width, height);
+ } else {
+ content.draw(canvas, x, y, width, height);
+ }
+
+ if (rotation != 0) {
+ canvas.restore();
+ }
+ }
+
+ public static void drawFrame(GLCanvas canvas, NinePatchTexture frame,
+ int x, int y, int width, int height) {
+ Rect p = frame.getPaddings();
+ frame.draw(canvas, x - p.left, y - p.top, width + p.left + p.right,
+ height + p.top + p.bottom);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java
new file mode 100644
index 000000000..b85ca7a41
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionManager.java
@@ -0,0 +1,221 @@
+/*
+ * 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.GalleryContext;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+import android.os.Vibrator;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+public class SelectionManager {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SelectionManager";
+
+ public static final int ENTER_SELECTION_MODE = 1;
+ public static final int LEAVE_SELECTION_MODE = 2;
+ public static final int SELECT_ALL_MODE = 3;
+
+ private Set<Path> mClickedSet;
+ private MediaSet mSourceMediaSet;
+ private final Vibrator mVibrator;
+ private SelectionListener mListener;
+ private DataManager mDataManager;
+ private boolean mInverseSelection;
+ private boolean mIsAlbumSet;
+ private boolean mInSelectionMode;
+ private boolean mAutoLeave = true;
+ private int mTotal;
+
+ public interface SelectionListener {
+ public void onSelectionModeChange(int mode);
+ public void onSelectionChange(Path path, boolean selected);
+ }
+
+ public SelectionManager(GalleryContext galleryContext, boolean isAlbumSet) {
+ Context context = galleryContext.getAndroidContext();
+ mDataManager = galleryContext.getDataManager();
+ mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ mClickedSet = new HashSet<Path>();
+ mIsAlbumSet = isAlbumSet;
+ mTotal = -1;
+ }
+
+ // Whether we will leave selection mode automatically once the number of
+ // selected items is down to zero.
+ public void setAutoLeaveSelectionMode(boolean enable) {
+ mAutoLeave = enable;
+ }
+
+ public void setSelectionListener(SelectionListener listener) {
+ mListener = listener;
+ }
+
+ public void selectAll() {
+ enterSelectionMode();
+ mInverseSelection = true;
+ mClickedSet.clear();
+ if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE);
+ }
+
+ public void deSelectAll() {
+ leaveSelectionMode();
+ mInverseSelection = false;
+ mClickedSet.clear();
+ }
+
+ public boolean inSelectAllMode() {
+ return mInverseSelection;
+ }
+
+ public boolean inSelectionMode() {
+ return mInSelectionMode;
+ }
+
+ public void enterSelectionMode() {
+ if (mInSelectionMode) return;
+
+ mInSelectionMode = true;
+ mVibrator.vibrate(100);
+ if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE);
+ }
+
+ public void leaveSelectionMode() {
+ if (!mInSelectionMode) return;
+
+ mInSelectionMode = false;
+ mInverseSelection = false;
+ mClickedSet.clear();
+ if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE);
+ }
+
+ public boolean isItemSelected(Path itemId) {
+ return mInverseSelection ^ mClickedSet.contains(itemId);
+ }
+
+ public int getSelectedCount() {
+ int count = mClickedSet.size();
+ if (mInverseSelection) {
+ if (mTotal < 0) {
+ mTotal = mIsAlbumSet
+ ? mSourceMediaSet.getSubMediaSetCount()
+ : mSourceMediaSet.getMediaItemCount();
+ }
+ count = mTotal - count;
+ }
+ return count;
+ }
+
+ public void toggle(Path path) {
+ if (mClickedSet.contains(path)) {
+ mClickedSet.remove(path);
+ } else {
+ enterSelectionMode();
+ mClickedSet.add(path);
+ }
+
+ if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path));
+ if (getSelectedCount() == 0 && mAutoLeave) {
+ leaveSelectionMode();
+ }
+ }
+
+ private static void expandMediaSet(ArrayList<Path> items, MediaSet set) {
+ int subCount = set.getSubMediaSetCount();
+ for (int i = 0; i < subCount; i++) {
+ expandMediaSet(items, set.getSubMediaSet(i));
+ }
+ int total = set.getMediaItemCount();
+ int batch = 50;
+ int index = 0;
+
+ while (index < total) {
+ int count = index + batch < total
+ ? batch
+ : total - index;
+ ArrayList<MediaItem> list = set.getMediaItem(index, count);
+ for (MediaItem item : list) {
+ items.add(item.getPath());
+ }
+ index += batch;
+ }
+ }
+
+ public ArrayList<Path> getSelected(boolean expandSet) {
+ ArrayList<Path> selected = new ArrayList<Path>();
+ if (mIsAlbumSet) {
+ if (mInverseSelection) {
+ int max = mSourceMediaSet.getSubMediaSetCount();
+ for (int i = 0; i < max; i++) {
+ MediaSet set = mSourceMediaSet.getSubMediaSet(i);
+ Path id = set.getPath();
+ if (!mClickedSet.contains(id)) {
+ if (expandSet) {
+ expandMediaSet(selected, set);
+ } else {
+ selected.add(id);
+ }
+ }
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ if (expandSet) {
+ expandMediaSet(selected, mDataManager.getMediaSet(id));
+ } else {
+ selected.add(id);
+ }
+ }
+ }
+ } else {
+ if (mInverseSelection) {
+
+ int total = mSourceMediaSet.getMediaItemCount();
+ int index = 0;
+ while (index < total) {
+ int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT);
+ ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count);
+ for (MediaItem item : list) {
+ Path id = item.getPath();
+ if (!mClickedSet.contains(id)) selected.add(id);
+ }
+ index += count;
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ selected.add(id);
+ }
+ }
+ }
+ return selected;
+ }
+
+ public void setSourceMediaSet(MediaSet set) {
+ mSourceMediaSet = set;
+ mTotal = -1;
+ }
+
+ public MediaSet getSourceMediaSet() {
+ return mSourceMediaSet;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java
new file mode 100644
index 000000000..79a6bf080
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlideshowView.java
@@ -0,0 +1,165 @@
+/*
+ * 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.anim.CanvasAnimation;
+import com.android.gallery3d.anim.FloatAnimation;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+
+import java.util.Random;
+import javax.microedition.khronos.opengles.GL11;
+
+public class SlideshowView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SlideshowView";
+
+ private static final int SLIDESHOW_DURATION = 3500;
+ private static final int TRANSITION_DURATION = 1000;
+
+ private static final float SCALE_SPEED = 0.20f ;
+ private static final float MOVE_SPEED = SCALE_SPEED;
+
+ private int mCurrentRotation;
+ private BitmapTexture mCurrentTexture;
+ private SlideshowAnimation mCurrentAnimation;
+
+ private int mPrevRotation;
+ private BitmapTexture mPrevTexture;
+ private SlideshowAnimation mPrevAnimation;
+
+ private final FloatAnimation mTransitionAnimation =
+ new FloatAnimation(0, 1, TRANSITION_DURATION);
+
+ private Random mRandom = new Random();
+
+ public void next(Bitmap bitmap, int rotation) {
+
+ mTransitionAnimation.start();
+
+ if (mPrevTexture != null) {
+ mPrevTexture.getBitmap().recycle();
+ mPrevTexture.recycle();
+ }
+
+ mPrevTexture = mCurrentTexture;
+ mPrevAnimation = mCurrentAnimation;
+ mPrevRotation = mCurrentRotation;
+
+ mCurrentRotation = rotation;
+ mCurrentTexture = new BitmapTexture(bitmap);
+ if (((rotation / 90) & 0x01) == 0) {
+ mCurrentAnimation = new SlideshowAnimation(
+ mCurrentTexture.getWidth(), mCurrentTexture.getHeight(),
+ mRandom);
+ } else {
+ mCurrentAnimation = new SlideshowAnimation(
+ mCurrentTexture.getHeight(), mCurrentTexture.getWidth(),
+ mRandom);
+ }
+ mCurrentAnimation.start();
+
+ invalidate();
+ }
+
+ public void release() {
+ if (mPrevTexture != null) {
+ mPrevTexture.recycle();
+ mPrevTexture = null;
+ }
+ if (mCurrentTexture != null) {
+ mCurrentTexture.recycle();
+ mCurrentTexture = null;
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ long currentTimeMillis = canvas.currentAnimationTimeMillis();
+ boolean requestRender = mTransitionAnimation.calculate(currentTimeMillis);
+ GL11 gl = canvas.getGLInstance();
+ gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE);
+ float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get();
+
+ if (mPrevTexture != null && alpha != 1f) {
+ requestRender |= mPrevAnimation.calculate(currentTimeMillis);
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.setAlpha(1f - alpha);
+ mPrevAnimation.apply(canvas);
+ canvas.rotate(mPrevRotation, 0, 0, 1);
+ mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2,
+ -mPrevTexture.getHeight() / 2);
+ canvas.restore();
+ }
+ if (mCurrentTexture != null) {
+ requestRender |= mCurrentAnimation.calculate(currentTimeMillis);
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.setAlpha(alpha);
+ mCurrentAnimation.apply(canvas);
+ canvas.rotate(mCurrentRotation, 0, 0, 1);
+ mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2,
+ -mCurrentTexture.getHeight() / 2);
+ canvas.restore();
+ }
+ if (requestRender) invalidate();
+ gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ }
+
+ private class SlideshowAnimation extends CanvasAnimation {
+ private final int mWidth;
+ private final int mHeight;
+
+ private final PointF mMovingVector;
+ private float mProgress;
+
+ public SlideshowAnimation(int width, int height, Random random) {
+ mWidth = width;
+ mHeight = height;
+ mMovingVector = new PointF(
+ MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f),
+ MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f));
+ setDuration(SLIDESHOW_DURATION);
+ }
+
+ @Override
+ public void apply(GLCanvas canvas) {
+ int viewWidth = getWidth();
+ int viewHeight = getHeight();
+
+ float initScale = Math.min(2f, Math.min((float)
+ viewWidth / mWidth, (float) viewHeight / mHeight));
+ float scale = initScale * (1 + SCALE_SPEED * mProgress);
+
+ float centerX = viewWidth / 2 + mMovingVector.x * mProgress;
+ float centerY = viewHeight / 2 + mMovingVector.y * mProgress;
+
+ canvas.translate(centerX, centerY, 0);
+ canvas.scale(scale, scale, 0);
+ }
+
+ @Override
+ public int getCanvasSaveFlags() {
+ return GLCanvas.SAVE_FLAG_MATRIX;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mProgress = progress;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
new file mode 100644
index 000000000..a8ca5f290
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -0,0 +1,607 @@
+/*
+ * 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.anim.Animation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.PositionRepository.Position;
+import com.android.gallery3d.util.LinkedNode;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+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 interface Listener {
+ public void onSingleTapUp(int index);
+ public void onLongTap(int index);
+ public void onScrollPositionChanged(int position, int total);
+ }
+
+ public static class SimpleListener implements Listener {
+ public void onSingleTapUp(int index) {}
+ public void onLongTap(int index) {}
+ public void onScrollPositionChanged(int position, int total) {}
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScrollerHelper mScroller;
+ private final Paper mPaper = new Paper();
+
+ private Listener mListener;
+ private UserInteractionListener mUIListener;
+
+ // Use linked hash map to keep the rendering order
+ private HashMap<DisplayItem, ItemEntry> mItems =
+ new HashMap<DisplayItem, ItemEntry>();
+
+ public LinkedNode.List<ItemEntry> mItemList = LinkedNode.newList();
+
+ // This is used for multipass rendering
+ private ArrayList<ItemEntry> mCurrentItems = new ArrayList<ItemEntry>();
+ private ArrayList<ItemEntry> mNextItems = new ArrayList<ItemEntry>();
+
+ private boolean mMoreAnimation = false;
+ private MyAnimation mAnimation = null;
+ private final Position mTempPosition = new Position();
+ private final Layout mLayout = new Layout();
+ private PositionProvider mPositions;
+ private int mStartIndex = INDEX_NONE;
+
+ // whether the down action happened while the view is scrolling.
+ private boolean mDownInScrolling;
+ private int mOverscrollEffect = OVERSCROLL_3D;
+
+ public static final int OVERSCROLL_3D = 0;
+ public static final int OVERSCROLL_SYSTEM = 1;
+ public static final int OVERSCROLL_NONE = 2;
+
+ public SlotView(Context context) {
+ mGestureDetector =
+ new GestureDetector(context, new MyGestureListener());
+ mScroller = new ScrollerHelper(context);
+ }
+
+ public void setCenterIndex(int index) {
+ int slotCount = mLayout.mSlotCount;
+ if (index < 0 || index >= slotCount) {
+ return;
+ }
+ Rect rect = mLayout.getSlotRect(index);
+ 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);
+ int visibleBegin = WIDE ? mScrollX : mScrollY;
+ int visibleLength = WIDE ? getWidth() : getHeight();
+ int visibleEnd = visibleBegin + visibleLength;
+ int slotBegin = WIDE ? rect.left : rect.top;
+ int slotEnd = WIDE ? rect.right : rect.bottom;
+
+ int position = visibleBegin;
+ if (visibleLength < slotEnd - slotBegin) {
+ position = visibleBegin;
+ } else if (slotBegin < visibleBegin) {
+ position = slotBegin;
+ } else if (slotEnd > visibleEnd) {
+ position = slotEnd - visibleLength;
+ }
+
+ setScrollPosition(position);
+ }
+
+ public void setScrollPosition(int position) {
+ position = Utils.clamp(position, 0, mLayout.getScrollLimit());
+ mScroller.setPosition(position);
+ updateScrollPosition(position, false);
+ }
+
+ public void setSlotSize(int slotWidth, int slotHeight) {
+ mLayout.setSlotSize(slotWidth, slotHeight);
+ }
+
+ @Override
+ public void addComponent(GLView view) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeComponent(GLView view) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
+ if (!changeSize) return;
+ mLayout.setSize(r - l, b - t);
+ onLayoutChanged(r - l, b - t);
+ if (mOverscrollEffect == OVERSCROLL_3D) {
+ mPaper.setSize(r - l, b - t);
+ }
+ }
+
+ protected void onLayoutChanged(int width, int height) {
+ }
+
+ public void startTransition(PositionProvider position) {
+ mPositions = position;
+ mAnimation = new MyAnimation();
+ mAnimation.start();
+ if (mItems.size() != 0) invalidate();
+ }
+
+ public void savePositions(PositionRepository repository) {
+ repository.clear();
+ LinkedNode.List<ItemEntry> list = mItemList;
+ ItemEntry entry = list.getFirst();
+ Position position = new Position();
+ while (entry != null) {
+ position.set(entry.target);
+ position.x -= mScrollX;
+ position.y -= mScrollY;
+ repository.putPosition(entry.item.getIdentity(), position);
+ entry = list.nextOf(entry);
+ }
+ }
+
+ 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 void putDisplayItem(Position target, Position base, DisplayItem item) {
+ ItemEntry entry = new ItemEntry(item, target, base);
+ mItemList.insertLast(entry);
+ mItems.put(item, entry);
+ }
+
+ public void removeDisplayItem(DisplayItem item) {
+ ItemEntry entry = mItems.remove(item);
+ if (entry != null) entry.remove();
+ }
+
+ public Rect getSlotRect(int slotIndex) {
+ return mLayout.getSlotRect(slotIndex);
+ }
+
+ @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;
+ }
+ 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);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_CLIP);
+ canvas.clipRect(0, 0, getWidth(), getHeight());
+ super.render(canvas);
+
+ long currentTimeMillis = canvas.currentAnimationTimeMillis();
+ boolean more = mScroller.advanceAnimation(currentTimeMillis);
+ boolean paperActive = (mOverscrollEffect == OVERSCROLL_3D)
+ && mPaper.advanceAnimation(currentTimeMillis);
+ updateScrollPosition(mScroller.getPosition(), false);
+ float interpolate = 1f;
+ if (mAnimation != null) {
+ more |= mAnimation.calculate(currentTimeMillis);
+ interpolate = mAnimation.value;
+ }
+
+ more |= paperActive;
+
+ if (WIDE) {
+ canvas.translate(-mScrollX, 0, 0);
+ } else {
+ canvas.translate(0, -mScrollY, 0);
+ }
+
+ LinkedNode.List<ItemEntry> list = mItemList;
+ for (ItemEntry entry = list.getLast(); entry != null;) {
+ if (renderItem(canvas, entry, interpolate, 0, paperActive)) {
+ mCurrentItems.add(entry);
+ }
+ entry = list.previousOf(entry);
+ }
+
+ int pass = 1;
+ while (!mCurrentItems.isEmpty()) {
+ for (int i = 0, n = mCurrentItems.size(); i < n; i++) {
+ ItemEntry entry = mCurrentItems.get(i);
+ if (renderItem(canvas, entry, interpolate, pass, paperActive)) {
+ mNextItems.add(entry);
+ }
+ }
+ mCurrentItems.clear();
+ // swap mNextItems with mCurrentItems
+ ArrayList<ItemEntry> tmp = mNextItems;
+ mNextItems = mCurrentItems;
+ mCurrentItems = tmp;
+ pass += 1;
+ }
+
+ if (WIDE) {
+ canvas.translate(mScrollX, 0, 0);
+ } else {
+ canvas.translate(0, mScrollY, 0);
+ }
+
+ if (more) invalidate();
+ if (mMoreAnimation && !more && mUIListener != null) {
+ mUIListener.onUserInteractionEnd();
+ }
+ mMoreAnimation = more;
+ canvas.restore();
+ }
+
+ private boolean renderItem(GLCanvas canvas, ItemEntry entry,
+ float interpolate, int pass, boolean paperActive) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ Position position = entry.target;
+ if (mPositions != null) {
+ position = mTempPosition;
+ position.set(entry.target);
+ position.x -= mScrollX;
+ position.y -= mScrollY;
+ Position source = mPositions
+ .getPosition(entry.item.getIdentity(), position);
+ source.x += mScrollX;
+ source.y += mScrollY;
+ position = mTempPosition;
+ Position.interpolate(
+ source, entry.target, position, interpolate);
+ }
+ canvas.multiplyAlpha(position.alpha);
+ if (paperActive) {
+ canvas.multiplyMatrix(mPaper.getTransform(
+ position, entry.base, mScrollX, mScrollY), 0);
+ } else {
+ canvas.translate(position.x, position.y, position.z);
+ }
+ canvas.rotate(position.theta, 0, 0, 1);
+ boolean more = entry.item.render(canvas, pass);
+ canvas.restore();
+ return more;
+ }
+
+ public static class MyAnimation extends Animation {
+ public float value;
+
+ public MyAnimation() {
+ setInterpolator(new DecelerateInterpolator(4));
+ setDuration(1500);
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ value = progress;
+ }
+ }
+
+ private static class ItemEntry extends LinkedNode {
+ public DisplayItem item;
+ public Position target;
+ public Position base;
+
+ public ItemEntry(DisplayItem item, Position target, Position base) {
+ this.item = item;
+ this.target = target;
+ this.base = base;
+ }
+ }
+
+ public static class Layout {
+
+ private int mVisibleStart;
+ private int mVisibleEnd;
+
+ private int mSlotCount;
+ private int mSlotWidth;
+ private int mSlotHeight;
+
+ private int mWidth;
+ private int mHeight;
+
+ private int mUnitCount;
+ private int mContentLength;
+ private int mScrollPosition;
+
+ private int mVerticalPadding;
+ private int mHorizontalPadding;
+
+ public void setSlotSize(int slotWidth, int slotHeight) {
+ mSlotWidth = slotWidth;
+ mSlotHeight = slotHeight;
+ }
+
+ public boolean setSlotCount(int slotCount) {
+ mSlotCount = slotCount;
+ int hPadding = mHorizontalPadding;
+ int vPadding = mVerticalPadding;
+ initLayoutParameters();
+ return vPadding != mVerticalPadding || hPadding != mHorizontalPadding;
+ }
+
+ public Rect getSlotRect(int index) {
+ int col, row;
+ if (WIDE) {
+ col = index / mUnitCount;
+ row = index - col * mUnitCount;
+ } else {
+ row = index / mUnitCount;
+ col = index - row * mUnitCount;
+ }
+
+ int x = mHorizontalPadding + col * mSlotWidth;
+ int y = mVerticalPadding + row * mSlotHeight;
+ return new Rect(x, y, x + mSlotWidth, y + mSlotHeight);
+ }
+
+ public int getContentLength() {
+ return mContentLength;
+ }
+
+ // 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 / minorUnitSize;
+ if (unitCount == 0) unitCount = 1;
+ mUnitCount = unitCount;
+
+ // We put extra padding above and below the column.
+ int availableUnits = Math.min(mUnitCount, mSlotCount);
+ padding[0] = (minorLength - availableUnits * minorUnitSize) / 2;
+
+ // Then calculate how many columns we need for all slots.
+ int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
+ mContentLength = count * majorUnitSize;
+
+ // 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() {
+ int[] padding = new int[2];
+ if (WIDE) {
+ initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
+ mVerticalPadding = padding[0];
+ mHorizontalPadding = padding[1];
+ } else {
+ initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
+ mVerticalPadding = padding[1];
+ mHorizontalPadding = padding[0];
+ }
+ updateVisibleSlotRange();
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ initLayoutParameters();
+ }
+
+ private void updateVisibleSlotRange() {
+ int position = mScrollPosition;
+
+ if (WIDE) {
+ int start = Math.max(0, (position / mSlotWidth) * mUnitCount);
+ int end = Math.min(mSlotCount, mUnitCount
+ * (position + mWidth + mSlotWidth - 1) / mSlotWidth);
+ setVisibleRange(start, end);
+ } else {
+ int start = Math.max(0, mUnitCount * (position / mSlotHeight));
+ int end = Math.min(mSlotCount, mUnitCount
+ * (position + mHeight + mSlotHeight - 1) / mSlotHeight);
+ 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;
+ }
+ }
+
+ public int getVisibleStart() {
+ return mVisibleStart;
+ }
+
+ public int getVisibleEnd() {
+ return mVisibleEnd;
+ }
+
+ public int getSlotIndexByPosition(float x, float y) {
+ float absoluteX = x + (WIDE ? mScrollPosition : 0);
+ absoluteX -= mHorizontalPadding;
+ int columnIdx = (int) (absoluteX + 0.5) / mSlotWidth;
+ if ((absoluteX - mSlotWidth * columnIdx) < 0
+ || (!WIDE && columnIdx >= mUnitCount)) {
+ return INDEX_NONE;
+ }
+
+ float absoluteY = y + (WIDE ? 0 : mScrollPosition);
+ absoluteY -= mVerticalPadding;
+ int rowIdx = (int) (absoluteY + 0.5) / mSlotHeight;
+ if (((absoluteY - mSlotHeight * rowIdx) < 0)
+ || (WIDE && rowIdx >= mUnitCount)) {
+ 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;
+ }
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+
+ @Override
+ public boolean onFling(MotionEvent e1,
+ MotionEvent e2, float velocityX, float velocityY) {
+ 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) {
+ float distance = WIDE ? distanceX : distanceY;
+ boolean canMove = mScroller.startScroll(
+ Math.round(distance), 0, mLayout.getScrollLimit());
+ if (mOverscrollEffect == OVERSCROLL_3D && !canMove) {
+ mPaper.overScroll(distance);
+ }
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ 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) {
+ 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;
+ }
+ updateScrollPosition(WIDE ? mScrollX : mScrollY, true);
+ return changed;
+ }
+
+ public int getVisibleStart() {
+ return mLayout.getVisibleStart();
+ }
+
+ public int getVisibleEnd() {
+ return mLayout.getVisibleEnd();
+ }
+
+ public int getScrollX() {
+ return mScrollX;
+ }
+
+ public int getScrollY() {
+ return mScrollY;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/StaticBackground.java b/src/com/android/gallery3d/ui/StaticBackground.java
new file mode 100644
index 000000000..08c55c378
--- /dev/null
+++ b/src/com/android/gallery3d/ui/StaticBackground.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.content.Context;
+
+public class StaticBackground extends GLView {
+
+ private Context mContext;
+ private int mLandscapeResource;
+ private int mPortraitResource;
+
+ private BasicTexture mBackground;
+ private boolean mIsLandscape = false;
+
+ public StaticBackground(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
+ setOrientation(getWidth() >= getHeight());
+ }
+
+ private void setOrientation(boolean isLandscape) {
+ if (mIsLandscape == isLandscape) return;
+ mIsLandscape = isLandscape;
+ if (mBackground != null) mBackground.recycle();
+ mBackground = new ResourceTexture(
+ mContext, mIsLandscape ? mLandscapeResource : mPortraitResource);
+ invalidate();
+ }
+
+ public void setImage(int landscapeId, int portraitId) {
+ mLandscapeResource = landscapeId;
+ mPortraitResource = portraitId;
+ if (mBackground != null) mBackground.recycle();
+ mBackground = new ResourceTexture(
+ mContext, mIsLandscape ? landscapeId : portraitId);
+ invalidate();
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ //mBackground.draw(canvas, 0, 0, getWidth(), getHeight());
+ canvas.fillRect(0, 0, getWidth(), getHeight(), 0xFF000000);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/StringTexture.java b/src/com/android/gallery3d/ui/StringTexture.java
new file mode 100644
index 000000000..71ab9b351
--- /dev/null
+++ b/src/com/android/gallery3d/ui/StringTexture.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+// StringTexture is a texture shows the content of a specified String.
+//
+// To create a StringTexture, use the newInstance() method and specify
+// the String, the font size, and the color.
+class StringTexture extends CanvasTexture {
+ private final String mText;
+ private final TextPaint mPaint;
+ private final FontMetricsInt mMetrics;
+
+ private StringTexture(String text, TextPaint paint,
+ FontMetricsInt metrics, int width, int height) {
+ super(width, height);
+ mText = text;
+ mPaint = paint;
+ mMetrics = metrics;
+ }
+
+ public static TextPaint getDefaultPaint(float textSize, int color) {
+ TextPaint paint = new TextPaint();
+ paint.setTextSize(textSize);
+ paint.setAntiAlias(true);
+ paint.setColor(color);
+ paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);
+ return paint;
+ }
+
+ public static StringTexture newInstance(
+ String text, float textSize, int color) {
+ return newInstance(text, getDefaultPaint(textSize, color));
+ }
+
+ public static StringTexture newInstance(
+ String text, String postfix, float textSize, int color,
+ float lengthLimit, boolean isBold) {
+ TextPaint paint = getDefaultPaint(textSize, color);
+ if (isBold) {
+ paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+ }
+ if (postfix != null) {
+ lengthLimit = Math.max(0,
+ lengthLimit - paint.measureText(postfix));
+ text = TextUtils.ellipsize(text, paint, lengthLimit,
+ TextUtils.TruncateAt.END).toString() + postfix;
+ } else {
+ text = TextUtils.ellipsize(
+ text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();
+ }
+ return newInstance(text, paint);
+ }
+
+ private static StringTexture newInstance(String text, TextPaint paint) {
+ FontMetricsInt metrics = paint.getFontMetricsInt();
+ int width = (int) Math.ceil(paint.measureText(text));
+ int height = metrics.bottom - metrics.top;
+ // The texture size needs to be at least 1x1.
+ if (width <= 0) width = 1;
+ if (height <= 0) height = 1;
+ return new StringTexture(text, paint, metrics, width, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas, Bitmap backing) {
+ canvas.translate(0, -mMetrics.ascent);
+ canvas.drawText(mText, 0, 0, mPaint);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/StripDrawer.java b/src/com/android/gallery3d/ui/StripDrawer.java
new file mode 100644
index 000000000..09106128f
--- /dev/null
+++ b/src/com/android/gallery3d/ui/StripDrawer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.Path;
+
+import android.content.Context;
+import android.graphics.Rect;
+
+public class StripDrawer extends SelectionDrawer {
+ private NinePatchTexture mFocusBox;
+ private Rect mFocusBoxPadding;
+
+ public StripDrawer(Context context) {
+ mFocusBox = new NinePatchTexture(context, R.drawable.focus_box);
+ mFocusBoxPadding = mFocusBox.getPaddings();
+ }
+
+ @Override
+ public void prepareDrawing() {
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Texture content, int width, int height,
+ int rotation, Path path, int topIndex, int dataSourceType,
+ int mediaType, boolean wantCache, boolean isCaching) {
+
+ int x = -width / 2;
+ int y = -height / 2;
+
+ drawWithRotation(canvas, content, x, y, width, height, rotation);
+ }
+
+ @Override
+ public void drawFocus(GLCanvas canvas, int width, int height) {
+ int x = -width / 2;
+ int y = -height / 2;
+ Rect p = mFocusBoxPadding;
+ mFocusBox.draw(canvas, x - p.left, y - p.top,
+ width + p.left + p.right, height + p.top + p.bottom);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java
new file mode 100644
index 000000000..bd494a331
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.common.Utils;
+
+import android.os.Handler;
+import android.os.Message;
+
+public class SynchronizedHandler extends Handler {
+
+ private final GLRoot mRoot;
+
+ public SynchronizedHandler(GLRoot root) {
+ mRoot = Utils.checkNotNull(root);
+ }
+
+ @Override
+ public void dispatchMessage(Message message) {
+ mRoot.lockRenderThread();
+ try {
+ super.dispatchMessage(message);
+ } finally {
+ mRoot.unlockRenderThread();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TextButton.java b/src/com/android/gallery3d/ui/TextButton.java
new file mode 100644
index 000000000..c6b85bf55
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TextButton.java
@@ -0,0 +1,91 @@
+/*
+ * 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 static com.android.gallery3d.ui.TextButtonConfig.*;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+public class TextButton extends Label {
+ private static final String TAG = "TextButton";
+ private boolean mPressed;
+ private Texture mPressedBackground;
+ private Texture mNormalBackground;
+ private OnClickedListener mOnClickListener;
+
+ public interface OnClickedListener {
+ public void onClicked(GLView source);
+ }
+
+ public TextButton(Context context, int label) {
+ super(context, label);
+ setPaddings(HORIZONTAL_PADDINGS, VERTICAL_PADDINGS,
+ HORIZONTAL_PADDINGS, VERTICAL_PADDINGS);
+ }
+
+ public void setOnClickListener(OnClickedListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void setPressedBackground(Texture texture) {
+ mPressedBackground = texture;
+ }
+
+ public void setNormalBackground(Texture texture) {
+ mNormalBackground = texture;
+ }
+
+ @SuppressWarnings("fallthrough")
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mPressed = true;
+ invalidate();
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mOnClickListener != null) {
+ mOnClickListener.onClicked(this);
+ }
+ // fall-through
+ case MotionEvent.ACTION_CANCEL:
+ mPressed = false;
+ invalidate();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ Texture bg = mPressed ? mPressedBackground : mNormalBackground;
+ if (bg != null) {
+ int width = getWidth();
+ int height = getHeight();
+ if (bg instanceof NinePatchTexture) {
+ Rect p = ((NinePatchTexture) bg).getPaddings();
+ bg.draw(canvas, -p.left, -p.top,
+ width + p.left + p.right, height + p.top + p.bottom);
+ } else {
+ bg.draw(canvas, 0, 0, width, height);
+ }
+ }
+ super.render(canvas);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Texture.java b/src/com/android/gallery3d/ui/Texture.java
new file mode 100644
index 000000000..feb7b0ab7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Texture.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+// Texture is a rectangular image which can be drawn on GLCanvas.
+// The isOpaque() function gives a hint about whether the texture is opaque,
+// so the drawing can be done faster.
+//
+// This is the current texture hierarchy:
+//
+// Texture
+// -- ColorTexture
+// -- BasicTexture
+// -- RawTexture
+// -- UploadedTexture
+// -- BitmapTexture
+// -- Tile
+// -- ResourceTexture
+// -- NinePatchTexture
+// -- CanvasTexture
+// -- DrawableTexture
+// -- StringTexture
+//
+public interface Texture {
+ public int getWidth();
+ public int getHeight();
+ public void draw(GLCanvas canvas, int x, int y);
+ public void draw(GLCanvas canvas, int x, int y, int w, int h);
+ public boolean isOpaque();
+}
diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
new file mode 100644
index 000000000..cf0685191
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -0,0 +1,693 @@
+/*
+ * 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.GalleryContext;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class TileImageView extends GLView {
+ public static final int SIZE_UNKNOWN = -1;
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "TileImageView";
+
+ // TILE_SIZE must be 2^N - 2. We put one pixel border in each side of the
+ // texture to avoid seams between tiles.
+ private static final int TILE_SIZE = 254;
+ private static final int TILE_BORDER = 1;
+ private static final int UPLOAD_LIMIT = 1;
+
+ /*
+ * This is the tile state in the CPU side.
+ * Life of a Tile:
+ * ACTIVATED (initial state)
+ * --> IN_QUEUE - by queueForDecode()
+ * --> RECYCLED - by recycleTile()
+ * IN_QUEUE --> DECODING - by decodeTile()
+ * --> RECYCLED - by recycleTile)
+ * DECODING --> RECYCLING - by recycleTile()
+ * --> DECODED - by decodeTile()
+ * RECYCLING --> RECYCLED - by decodeTile()
+ * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
+ * DECODED --> RECYCLED - by recycleTile()
+ * RECYCLED --> ACTIVATED - by obtainTile()
+ */
+ private static final int STATE_ACTIVATED = 0x01;
+ private static final int STATE_IN_QUEUE = 0x02;
+ private static final int STATE_DECODING = 0x04;
+ private static final int STATE_DECODED = 0x08;
+ private static final int STATE_RECYCLING = 0x10;
+ private static final int STATE_RECYCLED = 0x20;
+
+ private Model mModel;
+ protected BitmapTexture mBackupImage;
+ protected int mLevelCount; // cache the value of mScaledBitmaps.length
+
+ // The mLevel variable indicates which level of bitmap we should use.
+ // Level 0 means the original full-sized bitmap, and a larger value means
+ // a smaller scaled bitmap (The width and height of each scaled bitmap is
+ // half size of the previous one). If the value is in [0, mLevelCount), we
+ // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
+ // is mLevelCount, and that means we use mBackupTexture for display.
+ private int mLevel = 0;
+
+ // The offsets of the (left, top) of the upper-left tile to the (left, top)
+ // of the view.
+ private int mOffsetX;
+ private int mOffsetY;
+
+ private int mUploadQuota;
+ private boolean mRenderComplete;
+
+ private final RectF mSourceRect = new RectF();
+ private final RectF mTargetRect = new RectF();
+
+ private final HashMap<Long, Tile> mActiveTiles = new HashMap<Long, Tile>();
+
+ // The following three queue is guarded by TileImageView.this
+ private TileQueue mRecycledQueue = new TileQueue();
+ private TileQueue mUploadQueue = new TileQueue();
+ private TileQueue mDecodeQueue = new TileQueue();
+
+ // The width and height of the full-sized bitmap
+ protected int mImageWidth = SIZE_UNKNOWN;
+ protected int mImageHeight = SIZE_UNKNOWN;
+
+ protected int mCenterX;
+ protected int mCenterY;
+ protected float mScale;
+ protected int mRotation;
+
+ // Temp variables to avoid memory allocation
+ private final Rect mTileRange = new Rect();
+ private final Rect mActiveRange[] = {new Rect(), new Rect()};
+
+ private final TileUploader mTileUploader = new TileUploader();
+ private boolean mIsTextureFreed;
+ private Future<Void> mTileDecoder;
+ private ThreadPool mThreadPool;
+ private boolean mBackgroundTileUploaded;
+
+ public static interface Model {
+ public int getLevelCount();
+ public Bitmap getBackupImage();
+ public int getImageWidth();
+ public int getImageHeight();
+
+ // The method would be called in another thread
+ public Bitmap getTile(int level, int x, int y, int tileSize);
+ public boolean isFailedToLoad();
+ }
+
+ public TileImageView(GalleryContext context) {
+ mThreadPool = context.getThreadPool();
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ }
+
+ public void setModel(Model model) {
+ mModel = model;
+ if (model != null) notifyModelInvalidated();
+ }
+
+ private void updateBackupTexture(Bitmap backup) {
+ if (backup == null) {
+ if (mBackupImage != null) mBackupImage.recycle();
+ mBackupImage = null;
+ } else {
+ if (mBackupImage != null) {
+ if (mBackupImage.getBitmap() != backup) {
+ mBackupImage.recycle();
+ mBackupImage = new BitmapTexture(backup);
+ }
+ } else {
+ mBackupImage = new BitmapTexture(backup);
+ }
+ }
+ }
+
+ public void notifyModelInvalidated() {
+ invalidateTiles();
+ if (mModel == null) {
+ mBackupImage = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ } else {
+ updateBackupTexture(mModel.getBackupImage());
+ mImageWidth = mModel.getImageWidth();
+ mImageHeight = mModel.getImageHeight();
+ mLevelCount = mModel.getLevelCount();
+ }
+ layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ invalidate();
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ super.onLayout(changeSize, left, top, right, bottom);
+ if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ }
+
+ // Prepare the tiles we want to use for display.
+ //
+ // 1. Decide the tile level we want to use for display.
+ // 2. Decide the tile levels we want to keep as texture (in addition to
+ // the one we use for display).
+ // 3. Recycle unused tiles.
+ // 4. Activate the tiles we want.
+ private void layoutTiles(int centerX, int centerY, float scale, int rotation) {
+ // The width and height of this view.
+ int width = getWidth();
+ int height = getHeight();
+
+ // The tile levels we want to keep as texture is in the range
+ // [fromLevel, endLevel).
+ int fromLevel;
+ int endLevel;
+
+ // We want to use a texture larger than or equal to the display size.
+ mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount);
+
+ // We want to keep one more tile level as texture in addition to what
+ // we use for display. So it can be faster when the scale moves to the
+ // next level. We choose a level closer to the current scale.
+ if (mLevel != mLevelCount) {
+ Rect range = mTileRange;
+ getRange(range, centerX, centerY, mLevel, scale, rotation);
+ mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale);
+ mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale);
+ fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
+ } else {
+ // Activate the tiles of the smallest two levels.
+ fromLevel = mLevel - 2;
+ mOffsetX = Math.round(width / 2f - centerX * scale);
+ mOffsetY = Math.round(height / 2f - centerY * scale);
+ }
+
+ fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2));
+ endLevel = Math.min(fromLevel + 2, mLevelCount);
+
+ Rect range[] = mActiveRange;
+ for (int i = fromLevel; i < endLevel; ++i) {
+ getRange(range[i - fromLevel], centerX, centerY, i, rotation);
+ }
+
+ // If rotation is transient, don't update the tile.
+ if (rotation % 90 != 0) return;
+
+ synchronized (this) {
+ mDecodeQueue.clean();
+ mUploadQueue.clean();
+ mBackgroundTileUploaded = false;
+ }
+
+ // Recycle unused tiles: if the level of the active tile is outside the
+ // range [fromLevel, endLevel) or not in the visible range.
+ Iterator<Map.Entry<Long, Tile>>
+ iter = mActiveTiles.entrySet().iterator();
+ while (iter.hasNext()) {
+ Tile tile = iter.next().getValue();
+ int level = tile.mTileLevel;
+ if (level < fromLevel || level >= endLevel
+ || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
+ iter.remove();
+ recycleTile(tile);
+ }
+ }
+
+ for (int i = fromLevel; i < endLevel; ++i) {
+ int size = TILE_SIZE << i;
+ Rect r = range[i - fromLevel];
+ for (int y = r.top, bottom = r.bottom; y < bottom; y += size) {
+ for (int x = r.left, right = r.right; x < right; x += size) {
+ activateTile(x, y, i);
+ }
+ }
+ }
+ invalidate();
+ }
+
+ protected synchronized void invalidateTiles() {
+ mDecodeQueue.clean();
+ mUploadQueue.clean();
+ // TODO disable decoder
+ for (Tile tile : mActiveTiles.values()) {
+ recycleTile(tile);
+ }
+ mActiveTiles.clear();
+ }
+
+ private void getRange(Rect out, int cX, int cY, int level, int rotation) {
+ getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation);
+ }
+
+ // If the bitmap is scaled by the given factor "scale", return the
+ // rectangle containing visible range. The left-top coordinate returned is
+ // aligned to the tile boundary.
+ //
+ // (cX, cY) is the point on the original bitmap which will be put in the
+ // center of the ImageViewer.
+ private void getRange(Rect out,
+ int cX, int cY, int level, float scale, int rotation) {
+
+ double radians = Math.toRadians(-rotation);
+ double w = getWidth();
+ double h = getHeight();
+
+ double cos = Math.cos(radians);
+ double sin = Math.sin(radians);
+ int width = (int) Math.ceil(Math.max(
+ Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h)));
+ int height = (int) Math.ceil(Math.max(
+ Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h)));
+
+ int left = (int) Math.floor(cX - width / (2f * scale));
+ int top = (int) Math.floor(cY - height / (2f * scale));
+ int right = (int) Math.ceil(left + width / scale);
+ int bottom = (int) Math.ceil(top + height / scale);
+
+ // align the rectangle to tile boundary
+ int size = TILE_SIZE << level;
+ left = Math.max(0, size * (left / size));
+ top = Math.max(0, size * (top / size));
+ right = Math.min(mImageWidth, right);
+ bottom = Math.min(mImageHeight, bottom);
+
+ out.set(left, top, right, bottom);
+ }
+
+ public boolean setPosition(int centerX, int centerY, float scale, int rotation) {
+ if (mCenterX == centerX
+ && mCenterY == centerY && mScale == scale) return false;
+ mCenterX = centerX;
+ mCenterY = centerY;
+ mScale = scale;
+ mRotation = rotation;
+ layoutTiles(centerX, centerY, scale, rotation);
+ invalidate();
+ return true;
+ }
+
+ public void freeTextures() {
+ mIsTextureFreed = true;
+
+ if (mTileDecoder != null) {
+ mTileDecoder.cancel();
+ mTileDecoder.get();
+ mTileDecoder = null;
+ }
+
+ for (Tile texture : mActiveTiles.values()) {
+ texture.recycle();
+ }
+ mTileRange.set(0, 0, 0, 0);
+ mActiveTiles.clear();
+
+ synchronized (this) {
+ mUploadQueue.clean();
+ mDecodeQueue.clean();
+ Tile tile = mRecycledQueue.pop();
+ while (tile != null) {
+ tile.recycle();
+ tile = mRecycledQueue.pop();
+ }
+ }
+ updateBackupTexture(null);
+ }
+
+ public void prepareTextures() {
+ if (mTileDecoder == null) {
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ }
+ if (mIsTextureFreed) {
+ layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ mIsTextureFreed = false;
+ updateBackupTexture(mModel.getBackupImage());
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mUploadQuota = UPLOAD_LIMIT;
+ mRenderComplete = true;
+
+ int level = mLevel;
+ int rotation = mRotation;
+
+ if (rotation != 0) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ int centerX = getWidth() / 2, centerY = getHeight() / 2;
+ canvas.translate(centerX, centerY, 0);
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-centerX, -centerY, 0);
+ }
+ try {
+ if (level != mLevelCount) {
+ int size = (TILE_SIZE << level);
+ float length = size * mScale;
+ Rect r = mTileRange;
+
+ for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) {
+ float y = mOffsetY + i * length;
+ for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) {
+ float x = mOffsetX + j * length;
+ drawTile(canvas, tx, ty, level, x, y, length);
+ }
+ }
+ } else if (mBackupImage != null) {
+ mBackupImage.draw(canvas, mOffsetX, mOffsetY,
+ Math.round(mImageWidth * mScale),
+ Math.round(mImageHeight * mScale));
+ }
+ } finally {
+ if (rotation != 0) canvas.restore();
+ }
+
+ if (mRenderComplete) {
+ if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas);
+ } else {
+ invalidate();
+ }
+ }
+
+ private void uploadBackgroundTiles(GLCanvas canvas) {
+ mBackgroundTileUploaded = true;
+ for (Tile tile : mActiveTiles.values()) {
+ if (!tile.isContentValid(canvas)) queueForDecode(tile);
+ }
+ }
+
+ void queueForUpload(Tile tile) {
+ synchronized (this) {
+ mUploadQueue.push(tile);
+ }
+ if (mTileUploader.mActive.compareAndSet(false, true)) {
+ getGLRoot().addOnGLIdleListener(mTileUploader);
+ }
+ }
+
+ synchronized void queueForDecode(Tile tile) {
+ if (tile.mTileState == STATE_ACTIVATED) {
+ tile.mTileState = STATE_IN_QUEUE;
+ if (mDecodeQueue.push(tile)) notifyAll();
+ }
+ }
+
+ boolean decodeTile(Tile tile) {
+ synchronized (this) {
+ if (tile.mTileState != STATE_IN_QUEUE) return false;
+ tile.mTileState = STATE_DECODING;
+ }
+ boolean decodeComplete = tile.decode();
+ synchronized (this) {
+ if (tile.mTileState == STATE_RECYCLING) {
+ tile.mTileState = STATE_RECYCLED;
+ tile.mDecodedTile = null;
+ mRecycledQueue.push(tile);
+ return false;
+ }
+ tile.mTileState = STATE_DECODED;
+ return decodeComplete;
+ }
+ }
+
+ private synchronized Tile obtainTile(int x, int y, int level) {
+ Tile tile = mRecycledQueue.pop();
+ if (tile != null) {
+ tile.mTileState = STATE_ACTIVATED;
+ tile.update(x, y, level);
+ return tile;
+ }
+ return new Tile(x, y, level);
+ }
+
+ synchronized void recycleTile(Tile tile) {
+ if (tile.mTileState == STATE_DECODING) {
+ tile.mTileState = STATE_RECYCLING;
+ return;
+ }
+ tile.mTileState = STATE_RECYCLED;
+ tile.mDecodedTile = null;
+ mRecycledQueue.push(tile);
+ }
+
+ private void activateTile(int x, int y, int level) {
+ Long key = makeTileKey(x, y, level);
+ Tile tile = mActiveTiles.get(key);
+ if (tile != null) {
+ if (tile.mTileState == STATE_IN_QUEUE) {
+ tile.mTileState = STATE_ACTIVATED;
+ }
+ return;
+ }
+ tile = obtainTile(x, y, level);
+ mActiveTiles.put(key, tile);
+ }
+
+ private Tile getTile(int x, int y, int level) {
+ return mActiveTiles.get(makeTileKey(x, y, level));
+ }
+
+ private static Long makeTileKey(int x, int y, int level) {
+ long result = x;
+ result = (result << 16) | y;
+ result = (result << 16) | level;
+ return Long.valueOf(result);
+ }
+
+ private class TileUploader implements GLRoot.OnGLIdleListener {
+ AtomicBoolean mActive = new AtomicBoolean(false);
+
+ @Override
+ public boolean onGLIdle(GLRoot root, GLCanvas canvas) {
+ int quota = UPLOAD_LIMIT;
+ Tile tile;
+ while (true) {
+ synchronized (TileImageView.this) {
+ tile = mUploadQueue.pop();
+ }
+ if (tile == null || quota <= 0) break;
+ if (!tile.isContentValid(canvas)) {
+ Utils.assertTrue(tile.mTileState == STATE_DECODED);
+ tile.updateContent(canvas);
+ --quota;
+ }
+ }
+ mActive.set(tile != null);
+ return tile != null;
+ }
+ }
+
+ // Draw the tile to a square at canvas that locates at (x, y) and
+ // has a side length of length.
+ public void drawTile(GLCanvas canvas,
+ int tx, int ty, int level, float x, float y, float length) {
+ RectF source = mSourceRect;
+ RectF target = mTargetRect;
+ target.set(x, y, x + length, y + length);
+ source.set(0, 0, TILE_SIZE, TILE_SIZE);
+
+ Tile tile = getTile(tx, ty, level);
+ if (tile != null) {
+ if (!tile.isContentValid(canvas)) {
+ if (tile.mTileState == STATE_DECODED) {
+ if (mUploadQuota > 0) {
+ --mUploadQuota;
+ tile.updateContent(canvas);
+ } else {
+ mRenderComplete = false;
+ }
+ } else {
+ mRenderComplete = false;
+ queueForDecode(tile);
+ }
+ }
+ if (drawTile(tile, canvas, source, target)) return;
+ }
+ if (mBackupImage != null) {
+ BasicTexture backup = mBackupImage;
+ int size = TILE_SIZE << level;
+ float scaleX = (float) backup.getWidth() / mImageWidth;
+ float scaleY = (float) backup.getHeight() / mImageHeight;
+ source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+ (ty + size) * scaleY);
+ canvas.drawTexture(backup, source, target);
+ }
+ }
+
+ // TODO: avoid drawing the unused part of the textures.
+ static boolean drawTile(
+ Tile tile, GLCanvas canvas, RectF source, RectF target) {
+ while (true) {
+ if (tile.isContentValid(canvas)) {
+ // offset source rectangle for the texture border.
+ source.offset(TILE_BORDER, TILE_BORDER);
+ canvas.drawTexture(tile, source, target);
+ return true;
+ }
+
+ // Parent can be divided to four quads and tile is one of the four.
+ Tile parent = tile.getParentTile();
+ if (parent == null) return false;
+ if (tile.mX == parent.mX) {
+ source.left /= 2f;
+ source.right /= 2f;
+ } else {
+ source.left = (TILE_SIZE + source.left) / 2f;
+ source.right = (TILE_SIZE + source.right) / 2f;
+ }
+ if (tile.mY == parent.mY) {
+ source.top /= 2f;
+ source.bottom /= 2f;
+ } else {
+ source.top = (TILE_SIZE + source.top) / 2f;
+ source.bottom = (TILE_SIZE + source.bottom) / 2f;
+ }
+ tile = parent;
+ }
+ }
+
+ private class Tile extends UploadedTexture {
+ int mX;
+ int mY;
+ int mTileLevel;
+ Tile mNext;
+ Bitmap mDecodedTile;
+ volatile int mTileState = STATE_ACTIVATED;
+
+ public Tile(int x, int y, int level) {
+ mX = x;
+ mY = y;
+ mTileLevel = level;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ bitmap.recycle();
+ }
+
+ boolean decode() {
+ // Get a tile from the original image. The tile is down-scaled
+ // by (1 << mTilelevel) from a region in the original image.
+ int tileLength = (TILE_SIZE + 2 * TILE_BORDER);
+ int borderLength = TILE_BORDER << mTileLevel;
+ try {
+ mDecodedTile = mModel.getTile(
+ mTileLevel, mX - borderLength, mY - borderLength, tileLength);
+ return mDecodedTile != null;
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode tile", t);
+ return false;
+ }
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ Utils.assertTrue(mTileState == STATE_DECODED);
+ Bitmap bitmap = mDecodedTile;
+ mDecodedTile = null;
+ mTileState = STATE_ACTIVATED;
+ return bitmap;
+ }
+
+ public void update(int x, int y, int level) {
+ mX = x;
+ mY = y;
+ mTileLevel = level;
+ invalidateContent();
+ }
+
+ public Tile getParentTile() {
+ if (mTileLevel + 1 == mLevelCount) return null;
+ int size = TILE_SIZE << (mTileLevel + 1);
+ int x = size * (mX / size);
+ int y = size * (mY / size);
+ return getTile(x, y, mTileLevel + 1);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tile(%s, %s, %s / %s)",
+ mX / TILE_SIZE, mY / TILE_SIZE, mLevel, mLevelCount);
+ }
+ }
+
+ private static class TileQueue {
+ private Tile mHead;
+
+ public Tile pop() {
+ Tile tile = mHead;
+ if (tile != null) mHead = tile.mNext;
+ return tile;
+ }
+
+ public boolean push(Tile tile) {
+ boolean wasEmpty = mHead == null;
+ tile.mNext = mHead;
+ mHead = tile;
+ return wasEmpty;
+ }
+
+ public void clean() {
+ mHead = null;
+ }
+ }
+
+ private class TileDecoder implements ThreadPool.Job<Void> {
+
+ private CancelListener mNotifier = new CancelListener() {
+ @Override
+ public void onCancel() {
+ synchronized (TileImageView.this) {
+ TileImageView.this.notifyAll();
+ }
+ }
+ };
+
+ @Override
+ public Void run(JobContext jc) {
+ jc.setMode(ThreadPool.MODE_NONE);
+ jc.setCancelListener(mNotifier);
+ while (!jc.isCancelled()) {
+ Tile tile = null;
+ synchronized(TileImageView.this) {
+ tile = mDecodeQueue.pop();
+ if (tile == null && !jc.isCancelled()) {
+ Utils.waitWithoutInterrupt(TileImageView.this);
+ }
+ }
+ if (tile == null) continue;
+ if (decodeTile(tile)) queueForUpload(tile);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
new file mode 100644
index 000000000..65dea0eac
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -0,0 +1,144 @@
+/*
+ * 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.common.Utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+public class TileImageViewAdapter implements TileImageView.Model {
+ protected BitmapRegionDecoder mRegionDecoder;
+ protected int mImageWidth;
+ protected int mImageHeight;
+ protected Bitmap mBackupImage;
+ protected int mLevelCount;
+ protected boolean mFailedToLoad;
+
+ private final Rect mIntersectRect = new Rect();
+ private final Rect mRegionRect = new Rect();
+
+ public TileImageViewAdapter() {
+ }
+
+ public TileImageViewAdapter(Bitmap backup, BitmapRegionDecoder regionDecoder) {
+ mBackupImage = Utils.checkNotNull(backup);
+ mRegionDecoder = regionDecoder;
+ mImageWidth = regionDecoder.getWidth();
+ mImageHeight = regionDecoder.getHeight();
+ mLevelCount = calculateLevelCount();
+ }
+
+ public synchronized void clear() {
+ mBackupImage = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ mRegionDecoder = null;
+ mFailedToLoad = false;
+ }
+
+ public synchronized void setBackupImage(Bitmap backup, int width, int height) {
+ mBackupImage = Utils.checkNotNull(backup);
+ mImageWidth = width;
+ mImageHeight = height;
+ mRegionDecoder = null;
+ mLevelCount = 0;
+ mFailedToLoad = false;
+ }
+
+ public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
+ mRegionDecoder = Utils.checkNotNull(decoder);
+ mImageWidth = decoder.getWidth();
+ mImageHeight = decoder.getHeight();
+ mLevelCount = calculateLevelCount();
+ mFailedToLoad = false;
+ }
+
+ private int calculateLevelCount() {
+ return Math.max(0, Utils.ceilLog2(
+ (float) mImageWidth / mBackupImage.getWidth()));
+ }
+
+ @Override
+ public synchronized Bitmap getTile(int level, int x, int y, int length) {
+ Rect region = mRegionRect;
+ Rect intersectRect = mIntersectRect;
+ region.set(x, y, x + (length << level), y + (length << level));
+ intersectRect.set(0, 0, mImageWidth, mImageHeight);
+
+ // Get the intersected rect of the requested region and the image.
+ Utils.assertTrue(intersectRect.intersect(region));
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inPreferQualityOverSpeed = true;
+ options.inSampleSize = (1 << level);
+
+ Bitmap bitmap;
+
+ // In CropImage, we may call the decodeRegion() concurrently.
+ synchronized (mRegionDecoder) {
+ bitmap = mRegionDecoder.decodeRegion(intersectRect, options);
+ }
+
+ // The returned region may not match with the targetLength.
+ // If so, we fill black pixels on it.
+ if (intersectRect.equals(region)) return bitmap;
+
+ Bitmap tile = Bitmap.createBitmap(length, length, Config.ARGB_8888);
+ Canvas canvas = new Canvas(tile);
+ canvas.drawBitmap(bitmap,
+ (intersectRect.left - region.left) >> level,
+ (intersectRect.top - region.top) >> level, null);
+ bitmap.recycle();
+ return tile;
+ }
+
+ @Override
+ public Bitmap getBackupImage() {
+ return mBackupImage;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mLevelCount;
+ }
+
+ public void setFailedToLoad() {
+ mFailedToLoad = true;
+ }
+
+ @Override
+ public boolean isFailedToLoad() {
+ return mFailedToLoad;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/UploadedTexture.java b/src/com/android/gallery3d/ui/UploadedTexture.java
new file mode 100644
index 000000000..b063824d2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UploadedTexture.java
@@ -0,0 +1,285 @@
+/*
+ * 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.common.Utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.opengl.GLUtils;
+
+import java.util.HashMap;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+
+// UploadedTextures use a Bitmap for the content of the texture.
+//
+// Subclasses should implement onGetBitmap() to provide the Bitmap and
+// implement onFreeBitmap(mBitmap) which will be called when the Bitmap
+// is not needed anymore.
+//
+// isContentValid() is meaningful only when the isLoaded() returns true.
+// It means whether the content needs to be updated.
+//
+// The user of this class should call recycle() when the texture is not
+// needed anymore.
+//
+// By default an UploadedTexture is opaque (so it can be drawn faster without
+// blending). The user or subclass can override it using setOpaque().
+abstract class UploadedTexture extends BasicTexture {
+
+ // To prevent keeping allocation the borders, we store those used borders here.
+ // Since the length will be power of two, it won't use too much memory.
+ private static HashMap<BorderKey, Bitmap> sBorderLines =
+ new HashMap<BorderKey, Bitmap>();
+ private static BorderKey sBorderKey = new BorderKey();
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "Texture";
+ private boolean mContentValid = true;
+ private boolean mOpaque = true;
+ private boolean mThrottled = false;
+ private static int sUploadedCount;
+ private static final int UPLOAD_LIMIT = 100;
+
+ protected Bitmap mBitmap;
+
+ protected UploadedTexture() {
+ super(null, 0, STATE_UNLOADED);
+ }
+
+ private static class BorderKey implements Cloneable {
+ public boolean vertical;
+ public Config config;
+ public int length;
+
+ @Override
+ public int hashCode() {
+ int x = config.hashCode() ^ length;
+ return vertical ? x : -x;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof BorderKey)) return false;
+ BorderKey o = (BorderKey) object;
+ return vertical == o.vertical
+ && config == o.config && length == o.length;
+ }
+
+ @Override
+ public BorderKey clone() {
+ try {
+ return (BorderKey) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+
+ protected void setThrottled(boolean throttled) {
+ mThrottled = throttled;
+ }
+
+ private static Bitmap getBorderLine(
+ boolean vertical, Config config, int length) {
+ BorderKey key = sBorderKey;
+ key.vertical = vertical;
+ key.config = config;
+ key.length = length;
+ Bitmap bitmap = sBorderLines.get(key);
+ if (bitmap == null) {
+ bitmap = vertical
+ ? Bitmap.createBitmap(1, length, config)
+ : Bitmap.createBitmap(length, 1, config);
+ sBorderLines.put(key.clone(), bitmap);
+ }
+ return bitmap;
+ }
+
+ private Bitmap getBitmap() {
+ if (mBitmap == null) {
+ mBitmap = onGetBitmap();
+ if (mWidth == UNSPECIFIED) {
+ setSize(mBitmap.getWidth(), mBitmap.getHeight());
+ } else if (mWidth != mBitmap.getWidth()
+ || mHeight != mBitmap.getHeight()) {
+ throw new IllegalStateException(String.format(
+ "cannot change size: this = %s, orig = %sx%s, new = %sx%s",
+ toString(), mWidth, mHeight, mBitmap.getWidth(),
+ mBitmap.getHeight()));
+ }
+ }
+ return mBitmap;
+ }
+
+ private void freeBitmap() {
+ Utils.assertTrue(mBitmap != null);
+ onFreeBitmap(mBitmap);
+ mBitmap = null;
+ }
+
+ @Override
+ public int getWidth() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mHeight;
+ }
+
+ protected abstract Bitmap onGetBitmap();
+
+ protected abstract void onFreeBitmap(Bitmap bitmap);
+
+ protected void invalidateContent() {
+ if (mBitmap != null) freeBitmap();
+ mContentValid = false;
+ }
+
+ /**
+ * Whether the content on GPU is valid.
+ */
+ public boolean isContentValid(GLCanvas canvas) {
+ return isLoaded(canvas) && mContentValid;
+ }
+
+ /**
+ * Updates the content on GPU's memory.
+ * @param canvas
+ */
+ public void updateContent(GLCanvas canvas) {
+ if (!isLoaded(canvas)) {
+ if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {
+ return;
+ }
+ uploadToCanvas(canvas);
+ } else if (!mContentValid) {
+ Bitmap bitmap = getBitmap();
+ int format = GLUtils.getInternalFormat(bitmap);
+ int type = GLUtils.getType(bitmap);
+ canvas.getGLInstance().glBindTexture(GL11.GL_TEXTURE_2D, mId);
+ GLUtils.texSubImage2D(
+ GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap, format, type);
+ freeBitmap();
+ mContentValid = true;
+ }
+ }
+
+ public static void resetUploadLimit() {
+ sUploadedCount = 0;
+ }
+
+ public static boolean uploadLimitReached() {
+ return sUploadedCount > UPLOAD_LIMIT;
+ }
+
+ static int[] sTextureId = new int[1];
+ static float[] sCropRect = new float[4];
+
+ private void uploadToCanvas(GLCanvas canvas) {
+ GL11 gl = canvas.getGLInstance();
+
+ Bitmap bitmap = getBitmap();
+ if (bitmap != null) {
+ try {
+ // Define a vertically flipped crop rectangle for
+ // OES_draw_texture.
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ sCropRect[0] = 0;
+ sCropRect[1] = height;
+ sCropRect[2] = width;
+ sCropRect[3] = -height;
+
+ // Upload the bitmap to a new texture.
+ gl.glGenTextures(1, sTextureId, 0);
+ gl.glBindTexture(GL11.GL_TEXTURE_2D, sTextureId[0]);
+ gl.glTexParameterfv(GL11.GL_TEXTURE_2D,
+ GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameteri(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+ gl.glTexParameterf(GL11.GL_TEXTURE_2D,
+ GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+
+ if (width == getTextureWidth() && height == getTextureHeight()) {
+ GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bitmap, 0);
+ } else {
+ int format = GLUtils.getInternalFormat(bitmap);
+ int type = GLUtils.getType(bitmap);
+ Config config = bitmap.getConfig();
+
+ gl.glTexImage2D(GL11.GL_TEXTURE_2D, 0, format,
+ getTextureWidth(), getTextureHeight(),
+ 0, format, type, null);
+ GLUtils.texSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, bitmap,
+ format, type);
+
+ if (width != getTextureWidth()) {
+ Bitmap line = getBorderLine(true, config, getTextureHeight());
+ GLUtils.texSubImage2D(
+ GL11.GL_TEXTURE_2D, 0, width, 0, line, format, type);
+ }
+
+ if (height != getTextureHeight()) {
+ Bitmap line = getBorderLine(false, config, getTextureWidth());
+ GLUtils.texSubImage2D(
+ GL11.GL_TEXTURE_2D, 0, 0, height, line, format, type);
+ }
+
+ }
+ } finally {
+ freeBitmap();
+ }
+ // Update texture state.
+ setAssociatedCanvas(canvas);
+ mId = sTextureId[0];
+ mState = UploadedTexture.STATE_LOADED;
+ mContentValid = true;
+ } else {
+ mState = STATE_ERROR;
+ throw new RuntimeException("Texture load fail, no bitmap");
+ }
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ updateContent(canvas);
+ return isContentValid(canvas);
+ }
+
+ public void setOpaque(boolean isOpaque) {
+ mOpaque = isOpaque;
+ }
+
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ if (mBitmap != null) freeBitmap();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java
new file mode 100644
index 000000000..bc4a71800
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UserInteractionListener.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+public interface UserInteractionListener {
+ // Called when a user interaction begins (for example, fling).
+ public void onUserInteractionBegin();
+ // Called when the user interaction ends.
+ public void onUserInteractionEnd();
+ // Other one-shot user interactions.
+ public void onUserInteraction();
+}