diff options
Diffstat (limited to 'src/com/android/gallery3d/ui')
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(); +} |