diff options
Diffstat (limited to 'src/com/android/gallery3d/ui')
52 files changed, 13598 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/ui/AbstractSlotRenderer.java b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java new file mode 100644 index 000000000..729439dc3 --- /dev/null +++ b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Rect; + +import com.android.gallery3d.R; +import com.android.gallery3d.glrenderer.FadeOutTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.NinePatchTexture; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.Texture; + +public abstract class AbstractSlotRenderer implements SlotView.SlotRenderer { + + private final ResourceTexture mVideoOverlay; + private final ResourceTexture mVideoPlayIcon; + private final ResourceTexture mPanoramaIcon; + private final NinePatchTexture mFramePressed; + private final NinePatchTexture mFrameSelected; + private FadeOutTexture mFramePressedUp; + + protected AbstractSlotRenderer(Context context) { + mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb); + mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play); + mPanoramaIcon = new ResourceTexture(context, R.drawable.ic_360pano_holo_light); + mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed); + mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected); + } + + protected void drawContent(GLCanvas canvas, + Texture content, int width, int height, int rotation) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + + // The content is always rendered in to the largest square that fits + // inside the slot, aligned to the top of the slot. + width = height = Math.min(width, height); + if (rotation != 0) { + canvas.translate(width / 2, height / 2); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-width / 2, -height / 2); + } + + // Fit the content into the box + float scale = Math.min( + (float) width / content.getWidth(), + (float) height / content.getHeight()); + canvas.scale(scale, scale, 1); + content.draw(canvas, 0, 0); + + canvas.restore(); + } + + protected void drawVideoOverlay(GLCanvas canvas, int width, int height) { + // Scale the video overlay to the height of the thumbnail and put it + // on the left side. + ResourceTexture v = mVideoOverlay; + float scale = (float) height / v.getHeight(); + int w = Math.round(scale * v.getWidth()); + int h = Math.round(scale * v.getHeight()); + v.draw(canvas, 0, 0, w, h); + + int s = Math.min(width, height) / 6; + mVideoPlayIcon.draw(canvas, (width - s) / 2, (height - s) / 2, s, s); + } + + protected void drawPanoramaIcon(GLCanvas canvas, int width, int height) { + int iconSize = Math.min(width, height) / 6; + mPanoramaIcon.draw(canvas, (width - iconSize) / 2, (height - iconSize) / 2, + iconSize, iconSize); + } + + protected boolean isPressedUpFrameFinished() { + if (mFramePressedUp != null) { + if (mFramePressedUp.isAnimating()) { + return false; + } else { + mFramePressedUp = null; + } + } + return true; + } + + protected void drawPressedUpFrame(GLCanvas canvas, int width, int height) { + if (mFramePressedUp == null) { + mFramePressedUp = new FadeOutTexture(mFramePressed); + } + drawFrame(canvas, mFramePressed.getPaddings(), mFramePressedUp, 0, 0, width, height); + } + + protected void drawPressedFrame(GLCanvas canvas, int width, int height) { + drawFrame(canvas, mFramePressed.getPaddings(), mFramePressed, 0, 0, width, height); + } + + protected void drawSelectedFrame(GLCanvas canvas, int width, int height) { + drawFrame(canvas, mFrameSelected.getPaddings(), mFrameSelected, 0, 0, width, height); + } + + protected static void drawFrame(GLCanvas canvas, Rect padding, Texture frame, + int x, int y, int width, int height) { + frame.draw(canvas, x - padding.left, y - padding.top, width + padding.left + padding.right, + height + padding.top + padding.bottom); + } +} diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java new file mode 100644 index 000000000..6b4f10312 --- /dev/null +++ b/src/com/android/gallery3d/ui/ActionModeHandler.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.nfc.NfcAdapter; +import android.os.Handler; +import android.view.ActionMode; +import android.view.ActionMode.Callback; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ShareActionProvider; +import android.widget.ShareActionProvider.OnShareTargetSelectedListener; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.ui.MenuExecutor.ProgressListener; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; + +public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener { + + @SuppressWarnings("unused") + private static final String TAG = "ActionModeHandler"; + + private static final int MAX_SELECTED_ITEMS_FOR_SHARE_INTENT = 300; + private static final int MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT = 10; + + private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE + | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE + | MediaObject.SUPPORT_CACHE; + + public interface ActionModeListener { + public boolean onActionItemClicked(MenuItem item); + } + + private final AbstractGalleryActivity mActivity; + private final MenuExecutor mMenuExecutor; + private final SelectionManager mSelectionManager; + private final NfcAdapter mNfcAdapter; + private Menu mMenu; + private MenuItem mSharePanoramaMenuItem; + private MenuItem mShareMenuItem; + private ShareActionProvider mSharePanoramaActionProvider; + private ShareActionProvider mShareActionProvider; + private SelectionMenu mSelectionMenu; + private ActionModeListener mListener; + private Future<?> mMenuTask; + private final Handler mMainHandler; + private ActionMode mActionMode; + + private static class GetAllPanoramaSupports implements PanoramaSupportCallback { + private int mNumInfoRequired; + private JobContext mJobContext; + public boolean mAllPanoramas = true; + public boolean mAllPanorama360 = true; + public boolean mHasPanorama360 = false; + private Object mLock = new Object(); + + public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) { + mJobContext = jc; + mNumInfoRequired = mediaObjects.size(); + for (MediaObject mediaObject : mediaObjects) { + mediaObject.getPanoramaSupport(this); + } + } + + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + synchronized (mLock) { + mNumInfoRequired--; + mAllPanoramas = isPanorama && mAllPanoramas; + mAllPanorama360 = isPanorama360 && mAllPanorama360; + mHasPanorama360 = mHasPanorama360 || isPanorama360; + if (mNumInfoRequired == 0 || mJobContext.isCancelled()) { + mLock.notifyAll(); + } + } + } + + public void waitForPanoramaSupport() { + synchronized (mLock) { + while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) { + try { + mLock.wait(); + } catch (InterruptedException e) { + // May be a cancelled job context + } + } + } + } + } + + public ActionModeHandler( + AbstractGalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mMenuExecutor = new MenuExecutor(activity, selectionManager); + mMainHandler = new Handler(activity.getMainLooper()); + mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext()); + } + + public void startActionMode() { + Activity a = mActivity; + mActionMode = a.startActionMode(this); + View customView = LayoutInflater.from(a).inflate( + R.layout.action_mode, null); + mActionMode.setCustomView(customView); + mSelectionMenu = new SelectionMenu(a, + (Button) customView.findViewById(R.id.selection_menu), this); + updateSelectionMenu(); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void setTitle(String title) { + mSelectionMenu.setTitle(title); + } + + public void setActionModeListener(ActionModeListener listener) { + mListener = listener; + } + + private WakeLockHoldingProgressListener mDeleteProgressListener; + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + boolean result; + // Give listener a chance to process this command before it's routed to + // ActionModeHandler, which handles command only based on the action id. + // Sometimes the listener may have more background information to handle + // an action command. + if (mListener != null) { + result = mListener.onActionItemClicked(item); + if (result) { + mSelectionManager.leaveSelectionMode(); + return result; + } + } + ProgressListener listener = null; + String confirmMsg = null; + int action = item.getItemId(); + if (action == R.id.action_delete) { + confirmMsg = mActivity.getResources().getQuantityString( + R.plurals.delete_selection, mSelectionManager.getSelectedCount()); + if (mDeleteProgressListener == null) { + mDeleteProgressListener = new WakeLockHoldingProgressListener(mActivity, + "Gallery Delete Progress Listener"); + } + listener = mDeleteProgressListener; + } + mMenuExecutor.onMenuClicked(item, confirmMsg, listener); + } finally { + root.unlockRenderThread(); + } + return true; + } + + @Override + public boolean onPopupItemClick(int itemId) { + GLRoot root = mActivity.getGLRoot(); + root.lockRenderThread(); + try { + if (itemId == R.id.action_select_all) { + updateSupportedOperation(); + mMenuExecutor.onMenuClicked(itemId, null, false, true); + } + return true; + } finally { + root.unlockRenderThread(); + } + } + + private void updateSelectionMenu() { + // update title + int count = mSelectionManager.getSelectedCount(); + String format = mActivity.getResources().getQuantityString( + R.plurals.number_of_items_selected, count); + setTitle(String.format(format, count)); + + // For clients who call SelectionManager.selectAll() directly, we need to ensure the + // menu status is consistent with selection manager. + mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode()); + } + + private final OnShareTargetSelectedListener mShareTargetSelectedListener = + new OnShareTargetSelectedListener() { + @Override + public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) { + mSelectionManager.leaveSelectionMode(); + return false; + } + }; + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + mode.getMenuInflater().inflate(R.menu.operation, menu); + + mMenu = menu; + mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama); + if (mSharePanoramaMenuItem != null) { + mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem + .getActionProvider(); + mSharePanoramaActionProvider.setOnShareTargetSelectedListener( + mShareTargetSelectedListener); + mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml"); + } + mShareMenuItem = menu.findItem(R.id.action_share); + if (mShareMenuItem != null) { + mShareActionProvider = (ShareActionProvider) mShareMenuItem + .getActionProvider(); + mShareActionProvider.setOnShareTargetSelectedListener( + mShareTargetSelectedListener); + mShareActionProvider.setShareHistoryFileName("share_history.xml"); + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mSelectionManager.leaveSelectionMode(); + } + + private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) { + ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false); + if (unexpandedPaths.isEmpty()) { + // This happens when starting selection mode from overflow menu + // (instead of long press a media object) + return null; + } + ArrayList<MediaObject> selected = new ArrayList<MediaObject>(); + DataManager manager = mActivity.getDataManager(); + for (Path path : unexpandedPaths) { + if (jc.isCancelled()) { + return null; + } + selected.add(manager.getMediaObject(path)); + } + + return selected; + } + // Menu options are determined by selection set itself. + // We cannot expand it because MenuExecuter executes it based on + // the selection set instead of the expanded result. + // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't. + private int computeMenuOptions(ArrayList<MediaObject> selected) { + int operation = MediaObject.SUPPORT_ALL; + int type = 0; + for (MediaObject mediaObject: selected) { + int support = mediaObject.getSupportedOperations(); + type |= mediaObject.getMediaType(); + operation &= support; + } + + switch (selected.size()) { + case 1: + final String mimeType = MenuExecutor.getMimeType(type); + if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) { + operation &= ~MediaObject.SUPPORT_EDIT; + } + break; + default: + operation &= SUPPORT_MULTIPLE_MASK; + } + + return operation; + } + + @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN) + private void setNfcBeamPushUris(Uri[] uris) { + if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) { + mNfcAdapter.setBeamPushUrisCallback(null, mActivity); + mNfcAdapter.setBeamPushUris(uris, mActivity); + } + } + + // Share intent needs to expand the selection set so we can get URI of + // each media item + private Intent computePanoramaSharingIntent(JobContext jc, int maxItems) { + ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); + if (expandedPaths == null || expandedPaths.size() == 0) { + return new Intent(); + } + final ArrayList<Uri> uris = new ArrayList<Uri>(); + DataManager manager = mActivity.getDataManager(); + final Intent intent = new Intent(); + for (Path path : expandedPaths) { + if (jc.isCancelled()) return null; + uris.add(manager.getContentUri(path)); + } + + final int size = uris.size(); + if (size > 0) { + if (size > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE); + intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND); + intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + return intent; + } + + private Intent computeSharingIntent(JobContext jc, int maxItems) { + ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems); + if (expandedPaths == null || expandedPaths.size() == 0) { + setNfcBeamPushUris(null); + return new Intent(); + } + final ArrayList<Uri> uris = new ArrayList<Uri>(); + DataManager manager = mActivity.getDataManager(); + int type = 0; + final Intent intent = new Intent(); + for (Path path : expandedPaths) { + if (jc.isCancelled()) return null; + int support = manager.getSupportedOperations(path); + type |= manager.getMediaType(path); + + if ((support & MediaObject.SUPPORT_SHARE) != 0) { + uris.add(manager.getContentUri(path)); + } + } + + final int size = uris.size(); + if (size > 0) { + final String mimeType = MenuExecutor.getMimeType(type); + if (size > 1) { + intent.setAction(Intent.ACTION_SEND_MULTIPLE).setType(mimeType); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } else { + intent.setAction(Intent.ACTION_SEND).setType(mimeType); + intent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + setNfcBeamPushUris(uris.toArray(new Uri[uris.size()])); + } else { + setNfcBeamPushUris(null); + } + + return intent; + } + + public void updateSupportedOperation(Path path, boolean selected) { + // TODO: We need to improve the performance + updateSupportedOperation(); + } + + public void updateSupportedOperation() { + // Interrupt previous unfinished task, mMenuTask is only accessed in main thread + if (mMenuTask != null) mMenuTask.cancel(); + + updateSelectionMenu(); + + // Disable share actions until share intent is in good shape + if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false); + if (mShareMenuItem != null) mShareMenuItem.setEnabled(false); + + // Generate sharing intent and update supported operations in the background + // The task can take a long time and be canceled in the mean time. + mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() { + @Override + public Void run(final JobContext jc) { + // Pass1: Deal with unexpanded media object list for menu operation. + ArrayList<MediaObject> selected = getSelectedMediaObjects(jc); + if (selected == null) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + if (jc.isCancelled()) return; + // Disable all the operations when no item is selected + MenuExecutor.updateMenuOperation(mMenu, 0); + } + }); + return null; + } + final int operation = computeMenuOptions(selected); + if (jc.isCancelled()) { + return null; + } + int numSelected = selected.size(); + final boolean canSharePanoramas = + numSelected < MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT; + final boolean canShare = + numSelected < MAX_SELECTED_ITEMS_FOR_SHARE_INTENT; + + final GetAllPanoramaSupports supportCallback = canSharePanoramas ? + new GetAllPanoramaSupports(selected, jc) + : null; + + // Pass2: Deal with expanded media object list for sharing operation. + final Intent share_panorama_intent = canSharePanoramas ? + computePanoramaSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT) + : new Intent(); + final Intent share_intent = canShare ? + computeSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT) + : new Intent(); + + if (canSharePanoramas) { + supportCallback.waitForPanoramaSupport(); + } + if (jc.isCancelled()) { + return null; + } + mMainHandler.post(new Runnable() { + @Override + public void run() { + mMenuTask = null; + if (jc.isCancelled()) return; + MenuExecutor.updateMenuOperation(mMenu, operation); + MenuExecutor.updateMenuForPanorama(mMenu, + canSharePanoramas && supportCallback.mAllPanorama360, + canSharePanoramas && supportCallback.mHasPanorama360); + if (mSharePanoramaMenuItem != null) { + mSharePanoramaMenuItem.setEnabled(true); + if (canSharePanoramas && supportCallback.mAllPanorama360) { + mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); + mShareMenuItem.setTitle( + mActivity.getResources().getString(R.string.share_as_photo)); + } else { + mSharePanoramaMenuItem.setVisible(false); + mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + mShareMenuItem.setTitle( + mActivity.getResources().getString(R.string.share)); + } + mSharePanoramaActionProvider.setShareIntent(share_panorama_intent); + } + if (mShareMenuItem != null) { + mShareMenuItem.setEnabled(canShare); + mShareActionProvider.setShareIntent(share_intent); + } + } + }); + return null; + } + }); + } + + public void pause() { + if (mMenuTask != null) { + mMenuTask.cancel(); + mMenuTask = null; + } + mMenuExecutor.pause(); + } + + public void destroy() { + mMenuExecutor.destroy(); + } + + public void resume() { + if (mSelectionManager.inSelectionMode()) updateSupportedOperation(); + mMenuExecutor.resume(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumLabelMaker.java b/src/com/android/gallery3d/ui/AlbumLabelMaker.java new file mode 100644 index 000000000..da1cac0bd --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumLabelMaker.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.TextUtils; + +import com.android.gallery3d.R; +import com.android.gallery3d.data.DataSourceType; +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.JobContext; + +public class AlbumLabelMaker { + private static final int BORDER_SIZE = 0; + + private final AlbumSetSlotRenderer.LabelSpec mSpec; + private final TextPaint mTitlePaint; + private final TextPaint mCountPaint; + private final Context mContext; + + private int mLabelWidth; + private int mBitmapWidth; + private int mBitmapHeight; + + private final LazyLoadedBitmap mLocalSetIcon; + private final LazyLoadedBitmap mPicasaIcon; + private final LazyLoadedBitmap mCameraIcon; + + public AlbumLabelMaker(Context context, AlbumSetSlotRenderer.LabelSpec spec) { + mContext = context; + mSpec = spec; + mTitlePaint = getTextPaint(spec.titleFontSize, spec.titleColor, false); + mCountPaint = getTextPaint(spec.countFontSize, spec.countColor, false); + + mLocalSetIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_folder); + mPicasaIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_picasa); + mCameraIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_camera); + } + + public static int getBorderSize() { + return BORDER_SIZE; + } + + private Bitmap getOverlayAlbumIcon(int sourceType) { + switch (sourceType) { + case DataSourceType.TYPE_CAMERA: + return mCameraIcon.get(); + case DataSourceType.TYPE_LOCAL: + return mLocalSetIcon.get(); + case DataSourceType.TYPE_PICASA: + return mPicasaIcon.get(); + } + return null; + } + + private static TextPaint getTextPaint(int textSize, int color, boolean isBold) { + TextPaint paint = new TextPaint(); + paint.setTextSize(textSize); + paint.setAntiAlias(true); + paint.setColor(color); + //paint.setShadowLayer(2f, 0f, 0f, Color.LTGRAY); + if (isBold) { + paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); + } + return paint; + } + + private class LazyLoadedBitmap { + private Bitmap mBitmap; + private int mResId; + + public LazyLoadedBitmap(int resId) { + mResId = resId; + } + + public synchronized Bitmap get() { + if (mBitmap == null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Bitmap.Config.ARGB_8888; + mBitmap = BitmapFactory.decodeResource( + mContext.getResources(), mResId, options); + } + return mBitmap; + } + } + + public synchronized void setLabelWidth(int width) { + if (mLabelWidth == width) return; + mLabelWidth = width; + int borders = 2 * BORDER_SIZE; + mBitmapWidth = width + borders; + mBitmapHeight = mSpec.labelBackgroundHeight + borders; + } + + public ThreadPool.Job<Bitmap> requestLabel( + String title, String count, int sourceType) { + return new AlbumLabelJob(title, count, sourceType); + } + + static void drawText(Canvas canvas, + int x, int y, String text, int lengthLimit, TextPaint p) { + // The TextPaint cannot be used concurrently + synchronized (p) { + text = TextUtils.ellipsize( + text, p, lengthLimit, TextUtils.TruncateAt.END).toString(); + canvas.drawText(text, x, y - p.getFontMetricsInt().ascent, p); + } + } + + private class AlbumLabelJob implements ThreadPool.Job<Bitmap> { + private final String mTitle; + private final String mCount; + private final int mSourceType; + + public AlbumLabelJob(String title, String count, int sourceType) { + mTitle = title; + mCount = count; + mSourceType = sourceType; + } + + @Override + public Bitmap run(JobContext jc) { + AlbumSetSlotRenderer.LabelSpec s = mSpec; + + String title = mTitle; + String count = mCount; + Bitmap icon = getOverlayAlbumIcon(mSourceType); + + Bitmap bitmap; + int labelWidth; + + synchronized (this) { + labelWidth = mLabelWidth; + bitmap = GalleryBitmapPool.getInstance().get(mBitmapWidth, mBitmapHeight); + } + + if (bitmap == null) { + int borders = 2 * BORDER_SIZE; + bitmap = Bitmap.createBitmap(labelWidth + borders, + s.labelBackgroundHeight + borders, Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + canvas.clipRect(BORDER_SIZE, BORDER_SIZE, + bitmap.getWidth() - BORDER_SIZE, + bitmap.getHeight() - BORDER_SIZE); + canvas.drawColor(mSpec.backgroundColor, PorterDuff.Mode.SRC); + + canvas.translate(BORDER_SIZE, BORDER_SIZE); + + // draw title + if (jc.isCancelled()) return null; + int x = s.leftMargin + s.iconSize; + // TODO: is the offset relevant in new reskin? + // int y = s.titleOffset; + int y = (s.labelBackgroundHeight - s.titleFontSize) / 2; + drawText(canvas, x, y, title, labelWidth - s.leftMargin - x - + s.titleRightMargin, mTitlePaint); + + // draw count + if (jc.isCancelled()) return null; + x = labelWidth - s.titleRightMargin; + y = (s.labelBackgroundHeight - s.countFontSize) / 2; + drawText(canvas, x, y, count, + labelWidth - x , mCountPaint); + + // draw the icon + if (icon != null) { + if (jc.isCancelled()) return null; + float scale = (float) s.iconSize / icon.getWidth(); + canvas.translate(s.leftMargin, (s.labelBackgroundHeight - + Math.round(scale * icon.getHeight()))/2f); + canvas.scale(scale, scale); + canvas.drawBitmap(icon, 0, 0, null); + } + + return bitmap; + } + } + + public void recycleLabel(Bitmap label) { + GalleryBitmapPool.getInstance().put(label); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java new file mode 100644 index 000000000..8149df4b3 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java @@ -0,0 +1,549 @@ +/*T + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.os.Message; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumSetDataLoader; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataSourceType; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.BitmapTexture; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TextureUploader; +import com.android.gallery3d.glrenderer.TiledTexture; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.ThreadPool; + +public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener { + private static final String TAG = "AlbumSetSlidingWindow"; + private static final int MSG_UPDATE_ALBUM_ENTRY = 1; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentChanged(); + } + + private final AlbumSetDataLoader mSource; + private int mSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + + private final AlbumSetEntry mData[]; + private final SynchronizedHandler mHandler; + private final ThreadPool mThreadPool; + private final AlbumLabelMaker mLabelMaker; + private final String mLoadingText; + + private final TiledTexture.Uploader mContentUploader; + private final TextureUploader mLabelUploader; + + private int mActiveRequestCount = 0; + private boolean mIsActive = false; + private BitmapTexture mLoadingLabel; + + private int mSlotWidth; + + public static class AlbumSetEntry { + public MediaSet album; + public MediaItem coverItem; + public Texture content; + public BitmapTexture labelTexture; + public TiledTexture bitmapTexture; + public Path setPath; + public String title; + public int totalCount; + public int sourceType; + public int cacheFlag; + public int cacheStatus; + public int rotation; + public boolean isWaitLoadingDisplayed; + public long setDataVersion; + public long coverDataVersion; + private BitmapLoader labelLoader; + private BitmapLoader coverLoader; + } + + public AlbumSetSlidingWindow(AbstractGalleryActivity activity, + AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) { + source.setModelListener(this); + mSource = source; + mData = new AlbumSetEntry[cacheSize]; + mSize = source.size(); + mThreadPool = activity.getThreadPool(); + + mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec); + mLoadingText = activity.getAndroidContext().getString(R.string.loading); + mContentUploader = new TiledTexture.Uploader(activity.getGLRoot()); + mLabelUploader = new TextureUploader(activity.getGLRoot()); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY); + ((EntryUpdater) message.obj).updateEntry(); + } + }; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public AlbumSetEntry get(int slotIndex) { + if (!isActiveSlot(slotIndex)) { + Utils.fail("invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + } + return mData[slotIndex % mData.length]; + } + + public int size() { + return mSize; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + if (!(start <= end && end - start <= mData.length && end <= mSize)) { + Utils.fail("start = %s, end = %s, length = %s, size = %s", + start, end, mData.length, mSize); + } + + AlbumSetEntry data[] = mData; + mActiveStart = start; + mActiveEnd = end; + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + + if (mIsActive) { + updateTextureUploadQueue(); + updateAllImageRequests(); + } + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + requestImagesInSlot(mActiveEnd + i); + requestImagesInSlot(mActiveStart - 1 - i); + } + } + + private void cancelNonactiveImages() { + int range = Math.max( + mContentEnd - mActiveEnd, mActiveStart - mContentStart); + for (int i = 0 ;i < range; ++i) { + cancelImagesInSlot(mActiveEnd + i); + cancelImagesInSlot(mActiveStart - 1 - i); + } + } + + private void requestImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetEntry entry = mData[slotIndex % mData.length]; + if (entry.coverLoader != null) entry.coverLoader.startLoad(); + if (entry.labelLoader != null) entry.labelLoader.startLoad(); + } + + private void cancelImagesInSlot(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumSetEntry entry = mData[slotIndex % mData.length]; + if (entry.coverLoader != null) entry.coverLoader.cancelLoad(); + if (entry.labelLoader != null) entry.labelLoader.cancelLoad(); + } + + private static long getDataVersion(MediaObject object) { + return object == null + ? MediaSet.INVALID_DATA_VERSION + : object.getDataVersion(); + } + + private void freeSlotContent(int slotIndex) { + AlbumSetEntry entry = mData[slotIndex % mData.length]; + if (entry.coverLoader != null) entry.coverLoader.recycle(); + if (entry.labelLoader != null) entry.labelLoader.recycle(); + if (entry.labelTexture != null) entry.labelTexture.recycle(); + if (entry.bitmapTexture != null) entry.bitmapTexture.recycle(); + mData[slotIndex % mData.length] = null; + } + + private boolean isLabelChanged( + AlbumSetEntry entry, String title, int totalCount, int sourceType) { + return !Utils.equals(entry.title, title) + || entry.totalCount != totalCount + || entry.sourceType != sourceType; + } + + private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) { + MediaSet album = mSource.getMediaSet(slotIndex); + MediaItem cover = mSource.getCoverItem(slotIndex); + int totalCount = mSource.getTotalCount(slotIndex); + + entry.album = album; + entry.setDataVersion = getDataVersion(album); + entry.cacheFlag = identifyCacheFlag(album); + entry.cacheStatus = identifyCacheStatus(album); + entry.setPath = (album == null) ? null : album.getPath(); + + String title = (album == null) ? "" : Utils.ensureNotNull(album.getName()); + int sourceType = DataSourceType.identifySourceType(album); + if (isLabelChanged(entry, title, totalCount, sourceType)) { + entry.title = title; + entry.totalCount = totalCount; + entry.sourceType = sourceType; + if (entry.labelLoader != null) { + entry.labelLoader.recycle(); + entry.labelLoader = null; + entry.labelTexture = null; + } + if (album != null) { + entry.labelLoader = new AlbumLabelLoader( + slotIndex, title, totalCount, sourceType); + } + } + + entry.coverItem = cover; + if (getDataVersion(cover) != entry.coverDataVersion) { + entry.coverDataVersion = getDataVersion(cover); + entry.rotation = (cover == null) ? 0 : cover.getRotation(); + if (entry.coverLoader != null) { + entry.coverLoader.recycle(); + entry.coverLoader = null; + entry.bitmapTexture = null; + entry.content = null; + } + if (cover != null) { + entry.coverLoader = new AlbumCoverLoader(slotIndex, cover); + } + } + } + + private void prepareSlotContent(int slotIndex) { + AlbumSetEntry entry = new AlbumSetEntry(); + updateAlbumSetEntry(entry, slotIndex); + mData[slotIndex % mData.length] = entry; + } + + private static boolean startLoadBitmap(BitmapLoader loader) { + if (loader == null) return false; + loader.startLoad(); + return loader.isRequestInProgress(); + } + + private void uploadBackgroundTextureInSlot(int index) { + if (index < mContentStart || index >= mContentEnd) return; + AlbumSetEntry entry = mData[index % mData.length]; + if (entry.bitmapTexture != null) { + mContentUploader.addTexture(entry.bitmapTexture); + } + if (entry.labelTexture != null) { + mLabelUploader.addBgTexture(entry.labelTexture); + } + } + + private void updateTextureUploadQueue() { + if (!mIsActive) return; + mContentUploader.clear(); + mLabelUploader.clear(); + + // Upload foreground texture + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumSetEntry entry = mData[i % mData.length]; + if (entry.bitmapTexture != null) { + mContentUploader.addTexture(entry.bitmapTexture); + } + if (entry.labelTexture != null) { + mLabelUploader.addFgTexture(entry.labelTexture); + } + } + + // add background textures + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0; i < range; ++i) { + uploadBackgroundTextureInSlot(mActiveEnd + i); + uploadBackgroundTextureInSlot(mActiveStart - i - 1); + } + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumSetEntry entry = mData[i % mData.length]; + if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount; + if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount; + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + @Override + public void onSizeChanged(int size) { + if (mIsActive && mSize != size) { + mSize = size; + if (mListener != null) mListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + } + + @Override + public void onContentChanged(int index) { + if (!mIsActive) { + // paused, ignore slot changed event + return; + } + + // If the updated content is not cached, ignore it + if (index < mContentStart || index >= mContentEnd) { + Log.w(TAG, String.format( + "invalid update: %s is outside (%s, %s)", + index, mContentStart, mContentEnd) ); + return; + } + + AlbumSetEntry entry = mData[index % mData.length]; + updateAlbumSetEntry(entry, index); + updateAllImageRequests(); + updateTextureUploadQueue(); + if (mListener != null && isActiveSlot(index)) { + mListener.onContentChanged(); + } + } + + public BitmapTexture getLoadingTexture() { + if (mLoadingLabel == null) { + Bitmap bitmap = mLabelMaker.requestLabel( + mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED) + .run(ThreadPool.JOB_CONTEXT_STUB); + mLoadingLabel = new BitmapTexture(bitmap); + mLoadingLabel.setOpaque(false); + } + return mLoadingLabel; + } + + public void pause() { + mIsActive = false; + mLabelUploader.clear(); + mContentUploader.clear(); + TiledTexture.freeResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + } + + public void resume() { + mIsActive = true; + TiledTexture.prepareResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } + + private static interface EntryUpdater { + public void updateEntry(); + } + + private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater { + private MediaItem mMediaItem; + private final int mSlotIndex; + + public AlbumCoverLoader(int slotIndex, MediaItem item) { + mSlotIndex = slotIndex; + mMediaItem = item; + } + + @Override + protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { + return mThreadPool.submit(mMediaItem.requestImage( + MediaItem.TYPE_MICROTHUMBNAIL), l); + } + + @Override + protected void onLoadComplete(Bitmap bitmap) { + mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); + } + + @Override + public void updateEntry() { + Bitmap bitmap = getBitmap(); + if (bitmap == null) return; // error or recycled + + AlbumSetEntry entry = mData[mSlotIndex % mData.length]; + TiledTexture texture = new TiledTexture(bitmap); + entry.bitmapTexture = texture; + entry.content = texture; + + if (isActiveSlot(mSlotIndex)) { + mContentUploader.addTexture(texture); + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + if (mListener != null) mListener.onContentChanged(); + } else { + mContentUploader.addTexture(texture); + } + } + } + + private static int identifyCacheFlag(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_FLAG_NO; + } + + return set.getCacheFlag(); + } + + private static int identifyCacheStatus(MediaSet set) { + if (set == null || (set.getSupportedOperations() + & MediaSet.SUPPORT_CACHE) == 0) { + return MediaSet.CACHE_STATUS_NOT_CACHED; + } + + return set.getCacheStatus(); + } + + private class AlbumLabelLoader extends BitmapLoader implements EntryUpdater { + private final int mSlotIndex; + private final String mTitle; + private final int mTotalCount; + private final int mSourceType; + + public AlbumLabelLoader( + int slotIndex, String title, int totalCount, int sourceType) { + mSlotIndex = slotIndex; + mTitle = title; + mTotalCount = totalCount; + mSourceType = sourceType; + } + + @Override + protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { + return mThreadPool.submit(mLabelMaker.requestLabel( + mTitle, String.valueOf(mTotalCount), mSourceType), l); + } + + @Override + protected void onLoadComplete(Bitmap bitmap) { + mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget(); + } + + @Override + public void updateEntry() { + Bitmap bitmap = getBitmap(); + if (bitmap == null) return; // Error or recycled + + AlbumSetEntry entry = mData[mSlotIndex % mData.length]; + BitmapTexture texture = new BitmapTexture(bitmap); + texture.setOpaque(false); + entry.labelTexture = texture; + + if (isActiveSlot(mSlotIndex)) { + mLabelUploader.addFgTexture(texture); + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + if (mListener != null) mListener.onContentChanged(); + } else { + mLabelUploader.addBgTexture(texture); + } + } + } + + public void onSlotSizeChanged(int width, int height) { + if (mSlotWidth == width) return; + + mSlotWidth = width; + mLoadingLabel = null; + mLabelMaker.setLabelWidth(mSlotWidth); + + if (!mIsActive) return; + + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + AlbumSetEntry entry = mData[i % mData.length]; + if (entry.labelLoader != null) { + entry.labelLoader.recycle(); + entry.labelLoader = null; + entry.labelTexture = null; + } + if (entry.album != null) { + entry.labelLoader = new AlbumLabelLoader(i, + entry.title, entry.totalCount, entry.sourceType); + } + } + updateAllImageRequests(); + updateTextureUploadQueue(); + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java new file mode 100644 index 000000000..5332ef89a --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumSetDataLoader; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.ColorTexture; +import com.android.gallery3d.glrenderer.FadeInTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TiledTexture; +import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry; + +public class AlbumSetSlotRenderer extends AbstractSlotRenderer { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSetView"; + private static final int CACHE_SIZE = 96; + private final int mPlaceholderColor; + + private final ColorTexture mWaitLoadingTexture; + private final ResourceTexture mCameraOverlay; + private final AbstractGalleryActivity mActivity; + private final SelectionManager mSelectionManager; + protected final LabelSpec mLabelSpec; + + protected AlbumSetSlidingWindow mDataWindow; + private SlotView mSlotView; + + private int mPressedIndex = -1; + private boolean mAnimatePressedUp; + private Path mHighlightItemPath = null; + private boolean mInSelectionMode; + + public static class LabelSpec { + public int labelBackgroundHeight; + public int titleOffset; + public int countOffset; + public int titleFontSize; + public int countFontSize; + public int leftMargin; + public int iconSize; + public int titleRightMargin; + public int backgroundColor; + public int titleColor; + public int countColor; + public int borderSize; + } + + public AlbumSetSlotRenderer(AbstractGalleryActivity activity, + SelectionManager selectionManager, + SlotView slotView, LabelSpec labelSpec, int placeholderColor) { + super (activity); + mActivity = activity; + mSelectionManager = selectionManager; + mSlotView = slotView; + mLabelSpec = labelSpec; + mPlaceholderColor = placeholderColor; + + mWaitLoadingTexture = new ColorTexture(mPlaceholderColor); + mWaitLoadingTexture.setSize(1, 1); + mCameraOverlay = new ResourceTexture(activity, + R.drawable.ic_cameraalbum_overlay); + } + + public void setPressedIndex(int index) { + if (mPressedIndex == index) return; + mPressedIndex = index; + mSlotView.invalidate(); + } + + public void setPressedUp() { + if (mPressedIndex == -1) return; + mAnimatePressedUp = true; + mSlotView.invalidate(); + } + + public void setHighlightItemPath(Path path) { + if (mHighlightItemPath == path) return; + mHighlightItemPath = path; + mSlotView.invalidate(); + } + + public void setModel(AlbumSetDataLoader model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + mDataWindow = null; + mSlotView.setSlotCount(0); + } + if (model != null) { + mDataWindow = new AlbumSetSlidingWindow( + mActivity, model, mLabelSpec, CACHE_SIZE); + mDataWindow.setListener(new MyCacheListener()); + mSlotView.setSlotCount(mDataWindow.size()); + } + } + + private static Texture checkLabelTexture(Texture texture) { + return ((texture instanceof UploadedTexture) + && ((UploadedTexture) texture).isUploading()) + ? null + : texture; + } + + private static Texture checkContentTexture(Texture texture) { + return ((texture instanceof TiledTexture) + && !((TiledTexture) texture).isReady()) + ? null + : texture; + } + + @Override + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) { + AlbumSetEntry entry = mDataWindow.get(index); + int renderRequestFlags = 0; + renderRequestFlags |= renderContent(canvas, entry, width, height); + renderRequestFlags |= renderLabel(canvas, entry, width, height); + renderRequestFlags |= renderOverlay(canvas, index, entry, width, height); + return renderRequestFlags; + } + + protected int renderOverlay( + GLCanvas canvas, int index, AlbumSetEntry entry, int width, int height) { + int renderRequestFlags = 0; + if (entry.album != null && entry.album.isCameraRoll()) { + int uncoveredHeight = height - mLabelSpec.labelBackgroundHeight; + int dim = uncoveredHeight / 2; + mCameraOverlay.draw(canvas, (width - dim) / 2, + (uncoveredHeight - dim) / 2, dim, dim); + } + if (mPressedIndex == index) { + if (mAnimatePressedUp) { + drawPressedUpFrame(canvas, width, height); + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + if (isPressedUpFrameFinished()) { + mAnimatePressedUp = false; + mPressedIndex = -1; + } + } else { + drawPressedFrame(canvas, width, height); + } + } else if ((mHighlightItemPath != null) && (mHighlightItemPath == entry.setPath)) { + drawSelectedFrame(canvas, width, height); + } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.setPath)) { + drawSelectedFrame(canvas, width, height); + } + return renderRequestFlags; + } + + protected int renderContent( + GLCanvas canvas, AlbumSetEntry entry, int width, int height) { + int renderRequestFlags = 0; + + Texture content = checkContentTexture(entry.content); + if (content == null) { + content = mWaitLoadingTexture; + entry.isWaitLoadingDisplayed = true; + } else if (entry.isWaitLoadingDisplayed) { + entry.isWaitLoadingDisplayed = false; + content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture); + entry.content = content; + } + drawContent(canvas, content, width, height, entry.rotation); + if ((content instanceof FadeInTexture) && + ((FadeInTexture) content).isAnimating()) { + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + } + + return renderRequestFlags; + } + + protected int renderLabel( + GLCanvas canvas, AlbumSetEntry entry, int width, int height) { + Texture content = checkLabelTexture(entry.labelTexture); + if (content == null) { + content = mWaitLoadingTexture; + } + int b = AlbumLabelMaker.getBorderSize(); + int h = mLabelSpec.labelBackgroundHeight; + content.draw(canvas, -b, height - h + b, width + b + b, h); + + return 0; + } + + @Override + public void prepareDrawing() { + mInSelectionMode = mSelectionManager.inSelectionMode(); + } + + private class MyCacheListener implements AlbumSetSlidingWindow.Listener { + + @Override + public void onSizeChanged(int size) { + mSlotView.setSlotCount(size); + } + + @Override + public void onContentChanged() { + mSlotView.invalidate(); + } + } + + public void pause() { + mDataWindow.pause(); + } + + public void resume() { + mDataWindow.resume(); + } + + @Override + public void onVisibleRangeChanged(int visibleStart, int visibleEnd) { + if (mDataWindow != null) { + mDataWindow.setActiveWindow(visibleStart, visibleEnd); + } + } + + @Override + public void onSlotSizeChanged(int width, int height) { + if (mDataWindow != null) { + mDataWindow.onSlotSizeChanged(width, height); + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java new file mode 100644 index 000000000..fec7d1e92 --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.os.Message; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumDataLoader; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TiledTexture; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.JobLimiter; + +public class AlbumSlidingWindow implements AlbumDataLoader.DataListener { + @SuppressWarnings("unused") + private static final String TAG = "AlbumSlidingWindow"; + + private static final int MSG_UPDATE_ENTRY = 0; + private static final int JOB_LIMIT = 2; + + public static interface Listener { + public void onSizeChanged(int size); + public void onContentChanged(); + } + + public static class AlbumEntry { + public MediaItem item; + public Path path; + public boolean isPanorama; + public int rotation; + public int mediaType; + public boolean isWaitDisplayed; + public TiledTexture bitmapTexture; + public Texture content; + private BitmapLoader contentLoader; + private PanoSupportListener mPanoSupportListener; + } + + private final AlbumDataLoader mSource; + private final AlbumEntry mData[]; + private final SynchronizedHandler mHandler; + private final JobLimiter mThreadPool; + private final TiledTexture.Uploader mTileUploader; + + private int mSize; + + private int mContentStart = 0; + private int mContentEnd = 0; + + private int mActiveStart = 0; + private int mActiveEnd = 0; + + private Listener mListener; + + private int mActiveRequestCount = 0; + private boolean mIsActive = false; + + private class PanoSupportListener implements PanoramaSupportCallback { + public final AlbumEntry mEntry; + public PanoSupportListener (AlbumEntry entry) { + mEntry = entry; + } + @Override + public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama, + boolean isPanorama360) { + if (mEntry != null) mEntry.isPanorama = isPanorama; + } + } + + public AlbumSlidingWindow(AbstractGalleryActivity activity, + AlbumDataLoader source, int cacheSize) { + source.setDataListener(this); + mSource = source; + mData = new AlbumEntry[cacheSize]; + mSize = source.size(); + + mHandler = new SynchronizedHandler(activity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + Utils.assertTrue(message.what == MSG_UPDATE_ENTRY); + ((ThumbnailLoader) message.obj).updateEntry(); + } + }; + + mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT); + mTileUploader = new TiledTexture.Uploader(activity.getGLRoot()); + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public AlbumEntry get(int slotIndex) { + if (!isActiveSlot(slotIndex)) { + Utils.fail("invalid slot: %s outsides (%s, %s)", + slotIndex, mActiveStart, mActiveEnd); + } + return mData[slotIndex % mData.length]; + } + + public boolean isActiveSlot(int slotIndex) { + return slotIndex >= mActiveStart && slotIndex < mActiveEnd; + } + + private void setContentWindow(int contentStart, int contentEnd) { + if (contentStart == mContentStart && contentEnd == mContentEnd) return; + + if (!mIsActive) { + mContentStart = contentStart; + mContentEnd = contentEnd; + mSource.setActiveWindow(contentStart, contentEnd); + return; + } + + if (contentStart >= mContentEnd || mContentStart >= contentEnd) { + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } else { + for (int i = mContentStart; i < contentStart; ++i) { + freeSlotContent(i); + } + for (int i = contentEnd, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + mSource.setActiveWindow(contentStart, contentEnd); + for (int i = contentStart, n = mContentStart; i < n; ++i) { + prepareSlotContent(i); + } + for (int i = mContentEnd; i < contentEnd; ++i) { + prepareSlotContent(i); + } + } + + mContentStart = contentStart; + mContentEnd = contentEnd; + } + + public void setActiveWindow(int start, int end) { + if (!(start <= end && end - start <= mData.length && end <= mSize)) { + Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize); + } + AlbumEntry data[] = mData; + + mActiveStart = start; + mActiveEnd = end; + + int contentStart = Utils.clamp((start + end) / 2 - data.length / 2, + 0, Math.max(0, mSize - data.length)); + int contentEnd = Math.min(contentStart + data.length, mSize); + setContentWindow(contentStart, contentEnd); + updateTextureUploadQueue(); + if (mIsActive) updateAllImageRequests(); + } + + private void uploadBgTextureInSlot(int index) { + if (index < mContentEnd && index >= mContentStart) { + AlbumEntry entry = mData[index % mData.length]; + if (entry.bitmapTexture != null) { + mTileUploader.addTexture(entry.bitmapTexture); + } + } + } + + private void updateTextureUploadQueue() { + if (!mIsActive) return; + mTileUploader.clear(); + + // add foreground textures + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + AlbumEntry entry = mData[i % mData.length]; + if (entry.bitmapTexture != null) { + mTileUploader.addTexture(entry.bitmapTexture); + } + } + + // add background textures + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0; i < range; ++i) { + uploadBgTextureInSlot(mActiveEnd + i); + uploadBgTextureInSlot(mActiveStart - i - 1); + } + } + + // We would like to request non active slots in the following order: + // Order: 8 6 4 2 1 3 5 7 + // |---------|---------------|---------| + // |<- active ->| + // |<-------- cached range ----------->| + private void requestNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + requestSlotImage(mActiveEnd + i); + requestSlotImage(mActiveStart - 1 - i); + } + } + + // return whether the request is in progress or not + private boolean requestSlotImage(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false; + AlbumEntry entry = mData[slotIndex % mData.length]; + if (entry.content != null || entry.item == null) return false; + + // Set up the panorama callback + entry.mPanoSupportListener = new PanoSupportListener(entry); + entry.item.getPanoramaSupport(entry.mPanoSupportListener); + + entry.contentLoader.startLoad(); + return entry.contentLoader.isRequestInProgress(); + } + + private void cancelNonactiveImages() { + int range = Math.max( + (mContentEnd - mActiveEnd), (mActiveStart - mContentStart)); + for (int i = 0 ;i < range; ++i) { + cancelSlotImage(mActiveEnd + i); + cancelSlotImage(mActiveStart - 1 - i); + } + } + + private void cancelSlotImage(int slotIndex) { + if (slotIndex < mContentStart || slotIndex >= mContentEnd) return; + AlbumEntry item = mData[slotIndex % mData.length]; + if (item.contentLoader != null) item.contentLoader.cancelLoad(); + } + + private void freeSlotContent(int slotIndex) { + AlbumEntry data[] = mData; + int index = slotIndex % data.length; + AlbumEntry entry = data[index]; + if (entry.contentLoader != null) entry.contentLoader.recycle(); + if (entry.bitmapTexture != null) entry.bitmapTexture.recycle(); + data[index] = null; + } + + private void prepareSlotContent(int slotIndex) { + AlbumEntry entry = new AlbumEntry(); + MediaItem item = mSource.get(slotIndex); // item could be null; + entry.item = item; + entry.mediaType = (item == null) + ? MediaItem.MEDIA_TYPE_UNKNOWN + : entry.item.getMediaType(); + entry.path = (item == null) ? null : item.getPath(); + entry.rotation = (item == null) ? 0 : item.getRotation(); + entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item); + mData[slotIndex % mData.length] = entry; + } + + private void updateAllImageRequests() { + mActiveRequestCount = 0; + for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { + if (requestSlotImage(i)) ++mActiveRequestCount; + } + if (mActiveRequestCount == 0) { + requestNonactiveImages(); + } else { + cancelNonactiveImages(); + } + } + + private class ThumbnailLoader extends BitmapLoader { + private final int mSlotIndex; + private final MediaItem mItem; + + public ThumbnailLoader(int slotIndex, MediaItem item) { + mSlotIndex = slotIndex; + mItem = item; + } + + @Override + protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) { + return mThreadPool.submit( + mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this); + } + + @Override + protected void onLoadComplete(Bitmap bitmap) { + mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget(); + } + + public void updateEntry() { + Bitmap bitmap = getBitmap(); + if (bitmap == null) return; // error or recycled + AlbumEntry entry = mData[mSlotIndex % mData.length]; + entry.bitmapTexture = new TiledTexture(bitmap); + entry.content = entry.bitmapTexture; + + if (isActiveSlot(mSlotIndex)) { + mTileUploader.addTexture(entry.bitmapTexture); + --mActiveRequestCount; + if (mActiveRequestCount == 0) requestNonactiveImages(); + if (mListener != null) mListener.onContentChanged(); + } else { + mTileUploader.addTexture(entry.bitmapTexture); + } + } + } + + @Override + public void onSizeChanged(int size) { + if (mSize != size) { + mSize = size; + if (mListener != null) mListener.onSizeChanged(mSize); + if (mContentEnd > mSize) mContentEnd = mSize; + if (mActiveEnd > mSize) mActiveEnd = mSize; + } + } + + @Override + public void onContentChanged(int index) { + if (index >= mContentStart && index < mContentEnd && mIsActive) { + freeSlotContent(index); + prepareSlotContent(index); + updateAllImageRequests(); + if (mListener != null && isActiveSlot(index)) { + mListener.onContentChanged(); + } + } + } + + public void resume() { + mIsActive = true; + TiledTexture.prepareResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + prepareSlotContent(i); + } + updateAllImageRequests(); + } + + public void pause() { + mIsActive = false; + mTileUploader.clear(); + TiledTexture.freeResources(); + for (int i = mContentStart, n = mContentEnd; i < n; ++i) { + freeSlotContent(i); + } + } +} diff --git a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java new file mode 100644 index 000000000..dc6c89b0e --- /dev/null +++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.app.AlbumDataLoader; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.ColorTexture; +import com.android.gallery3d.glrenderer.FadeInTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.glrenderer.TiledTexture; + +public class AlbumSlotRenderer extends AbstractSlotRenderer { + @SuppressWarnings("unused") + private static final String TAG = "AlbumView"; + + public interface SlotFilter { + public boolean acceptSlot(int index); + } + + private final int mPlaceholderColor; + private static final int CACHE_SIZE = 96; + + private AlbumSlidingWindow mDataWindow; + private final AbstractGalleryActivity mActivity; + private final ColorTexture mWaitLoadingTexture; + private final SlotView mSlotView; + private final SelectionManager mSelectionManager; + + private int mPressedIndex = -1; + private boolean mAnimatePressedUp; + private Path mHighlightItemPath = null; + private boolean mInSelectionMode; + + private SlotFilter mSlotFilter; + + public AlbumSlotRenderer(AbstractGalleryActivity activity, SlotView slotView, + SelectionManager selectionManager, int placeholderColor) { + super(activity); + mActivity = activity; + mSlotView = slotView; + mSelectionManager = selectionManager; + mPlaceholderColor = placeholderColor; + + mWaitLoadingTexture = new ColorTexture(mPlaceholderColor); + mWaitLoadingTexture.setSize(1, 1); + } + + public void setPressedIndex(int index) { + if (mPressedIndex == index) return; + mPressedIndex = index; + mSlotView.invalidate(); + } + + public void setPressedUp() { + if (mPressedIndex == -1) return; + mAnimatePressedUp = true; + mSlotView.invalidate(); + } + + public void setHighlightItemPath(Path path) { + if (mHighlightItemPath == path) return; + mHighlightItemPath = path; + mSlotView.invalidate(); + } + + public void setModel(AlbumDataLoader model) { + if (mDataWindow != null) { + mDataWindow.setListener(null); + mSlotView.setSlotCount(0); + mDataWindow = null; + } + if (model != null) { + mDataWindow = new AlbumSlidingWindow(mActivity, model, CACHE_SIZE); + mDataWindow.setListener(new MyDataModelListener()); + mSlotView.setSlotCount(model.size()); + } + } + + private static Texture checkTexture(Texture texture) { + return (texture instanceof TiledTexture) + && !((TiledTexture) texture).isReady() + ? null + : texture; + } + + @Override + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) { + if (mSlotFilter != null && !mSlotFilter.acceptSlot(index)) return 0; + + AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index); + + int renderRequestFlags = 0; + + Texture content = checkTexture(entry.content); + if (content == null) { + content = mWaitLoadingTexture; + entry.isWaitDisplayed = true; + } else if (entry.isWaitDisplayed) { + entry.isWaitDisplayed = false; + content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture); + entry.content = content; + } + drawContent(canvas, content, width, height, entry.rotation); + if ((content instanceof FadeInTexture) && + ((FadeInTexture) content).isAnimating()) { + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + } + + if (entry.mediaType == MediaObject.MEDIA_TYPE_VIDEO) { + drawVideoOverlay(canvas, width, height); + } + + if (entry.isPanorama) { + drawPanoramaIcon(canvas, width, height); + } + + renderRequestFlags |= renderOverlay(canvas, index, entry, width, height); + + return renderRequestFlags; + } + + private int renderOverlay(GLCanvas canvas, int index, + AlbumSlidingWindow.AlbumEntry entry, int width, int height) { + int renderRequestFlags = 0; + if (mPressedIndex == index) { + if (mAnimatePressedUp) { + drawPressedUpFrame(canvas, width, height); + renderRequestFlags |= SlotView.RENDER_MORE_FRAME; + if (isPressedUpFrameFinished()) { + mAnimatePressedUp = false; + mPressedIndex = -1; + } + } else { + drawPressedFrame(canvas, width, height); + } + } else if ((entry.path != null) && (mHighlightItemPath == entry.path)) { + drawSelectedFrame(canvas, width, height); + } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.path)) { + drawSelectedFrame(canvas, width, height); + } + return renderRequestFlags; + } + + private class MyDataModelListener implements AlbumSlidingWindow.Listener { + @Override + public void onContentChanged() { + mSlotView.invalidate(); + } + + @Override + public void onSizeChanged(int size) { + mSlotView.setSlotCount(size); + } + } + + public void resume() { + mDataWindow.resume(); + } + + public void pause() { + mDataWindow.pause(); + } + + @Override + public void prepareDrawing() { + mInSelectionMode = mSelectionManager.inSelectionMode(); + } + + @Override + public void onVisibleRangeChanged(int visibleStart, int visibleEnd) { + if (mDataWindow != null) { + mDataWindow.setActiveWindow(visibleStart, visibleEnd); + } + } + + @Override + public void onSlotSizeChanged(int width, int height) { + // Do nothing + } + + public void setSlotFilter(SlotFilter slotFilter) { + mSlotFilter = slotFilter; + } +} diff --git a/src/com/android/gallery3d/ui/AnimationTime.java b/src/com/android/gallery3d/ui/AnimationTime.java new file mode 100644 index 000000000..063677423 --- /dev/null +++ b/src/com/android/gallery3d/ui/AnimationTime.java @@ -0,0 +1,45 @@ + +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.os.SystemClock; + +// +// The animation time should ideally be the vsync time the frame will be +// displayed, but that is an unknown time in the future. So we use the system +// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called) +// as a approximation. +// +public class AnimationTime { + private static volatile long sTime; + + // Sets current time as the animation time. + public static void update() { + sTime = SystemClock.uptimeMillis(); + } + + // Returns the animation time. + public static long get() { + return sTime; + } + + public static long startTime() { + sTime = SystemClock.uptimeMillis(); + return sTime; + } +} diff --git a/src/com/android/gallery3d/ui/BitmapLoader.java b/src/com/android/gallery3d/ui/BitmapLoader.java new file mode 100644 index 000000000..a708a90f3 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapLoader.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; + +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; + +// We use this class to +// 1.) load bitmaps in background. +// 2.) as a place holder for the loaded bitmap +public abstract class BitmapLoader implements FutureListener<Bitmap> { + @SuppressWarnings("unused") + private static final String TAG = "BitmapLoader"; + + /* Transition Map: + * INIT -> REQUESTED, RECYCLED + * REQUESTED -> INIT (cancel), LOADED, ERROR, RECYCLED + * LOADED, ERROR -> RECYCLED + */ + private static final int STATE_INIT = 0; + private static final int STATE_REQUESTED = 1; + private static final int STATE_LOADED = 2; + private static final int STATE_ERROR = 3; + private static final int STATE_RECYCLED = 4; + + private int mState = STATE_INIT; + // mTask is not null only when a task is on the way + private Future<Bitmap> mTask; + private Bitmap mBitmap; + + @Override + public void onFutureDone(Future<Bitmap> future) { + synchronized (this) { + mTask = null; + mBitmap = future.get(); + if (mState == STATE_RECYCLED) { + if (mBitmap != null) { + GalleryBitmapPool.getInstance().put(mBitmap); + mBitmap = null; + } + return; // don't call callback + } + if (future.isCancelled() && mBitmap == null) { + if (mState == STATE_REQUESTED) mTask = submitBitmapTask(this); + return; // don't call callback + } else { + mState = mBitmap == null ? STATE_ERROR : STATE_LOADED; + } + } + onLoadComplete(mBitmap); + } + + public synchronized void startLoad() { + if (mState == STATE_INIT) { + mState = STATE_REQUESTED; + if (mTask == null) mTask = submitBitmapTask(this); + } + } + + public synchronized void cancelLoad() { + if (mState == STATE_REQUESTED) { + mState = STATE_INIT; + if (mTask != null) mTask.cancel(); + } + } + + // Recycle the loader and the bitmap + public synchronized void recycle() { + mState = STATE_RECYCLED; + if (mBitmap != null) { + GalleryBitmapPool.getInstance().put(mBitmap); + mBitmap = null; + } + if (mTask != null) mTask.cancel(); + } + + public synchronized boolean isRequestInProgress() { + return mState == STATE_REQUESTED; + } + + public synchronized boolean isRecycled() { + return mState == STATE_RECYCLED; + } + + public synchronized Bitmap getBitmap() { + return mBitmap; + } + + abstract protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l); + abstract protected void onLoadComplete(Bitmap bitmap); +} diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java new file mode 100644 index 000000000..a3d403946 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import com.android.gallery3d.glrenderer.BitmapTexture; +import com.android.gallery3d.glrenderer.GLCanvas; + +public class BitmapScreenNail implements ScreenNail { + private final BitmapTexture mBitmapTexture; + + public BitmapScreenNail(Bitmap bitmap) { + mBitmapTexture = new BitmapTexture(bitmap); + } + + @Override + public int getWidth() { + return mBitmapTexture.getWidth(); + } + + @Override + public int getHeight() { + return mBitmapTexture.getHeight(); + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + mBitmapTexture.draw(canvas, x, y, width, height); + } + + @Override + public void noDraw() { + // do nothing + } + + @Override + public void recycle() { + mBitmapTexture.recycle(); + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF dest) { + canvas.drawTexture(mBitmapTexture, source, dest); + } +} diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java new file mode 100644 index 000000000..e1a8b7644 --- /dev/null +++ b/src/com/android/gallery3d/ui/BitmapTileProvider.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; + +import com.android.gallery3d.common.BitmapUtils; +import com.android.photos.data.GalleryBitmapPool; + +import java.util.ArrayList; + +public class BitmapTileProvider implements TileImageView.TileSource { + private final ScreenNail mScreenNail; + private final Bitmap[] mMipmaps; + private final Config mConfig; + private final int mImageWidth; + private final int mImageHeight; + + private boolean mRecycled = false; + + public BitmapTileProvider(Bitmap bitmap, int maxBackupSize) { + mImageWidth = bitmap.getWidth(); + mImageHeight = bitmap.getHeight(); + ArrayList<Bitmap> list = new ArrayList<Bitmap>(); + list.add(bitmap); + while (bitmap.getWidth() > maxBackupSize + || bitmap.getHeight() > maxBackupSize) { + bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false); + list.add(bitmap); + } + + mScreenNail = new BitmapScreenNail(list.remove(list.size() - 1)); + mMipmaps = list.toArray(new Bitmap[list.size()]); + mConfig = Config.ARGB_8888; + } + + @Override + public ScreenNail getScreenNail() { + return mScreenNail; + } + + @Override + public int getImageHeight() { + return mImageHeight; + } + + @Override + public int getImageWidth() { + return mImageWidth; + } + + @Override + public int getLevelCount() { + return mMipmaps.length; + } + + @Override + public Bitmap getTile(int level, int x, int y, int tileSize) { + x >>= level; + y >>= level; + + Bitmap result = GalleryBitmapPool.getInstance().get(tileSize, tileSize); + if (result == null) { + result = Bitmap.createBitmap(tileSize, tileSize, mConfig); + } else { + result.eraseColor(0); + } + + Bitmap mipmap = mMipmaps[level]; + Canvas canvas = new Canvas(result); + int offsetX = -x; + int offsetY = -y; + canvas.drawBitmap(mipmap, offsetX, offsetY, null); + return result; + } + + public void recycle() { + if (mRecycled) return; + mRecycled = true; + for (Bitmap bitmap : mMipmaps) { + BitmapUtils.recycleSilently(bitmap); + } + if (mScreenNail != null) { + mScreenNail.recycle(); + } + } +} diff --git a/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java new file mode 100644 index 000000000..46f7a2433 --- /dev/null +++ b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.os.StatFs; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.io.File; + +public class CacheStorageUsageInfo { + @SuppressWarnings("unused") + private static final String TAG = "CacheStorageUsageInfo"; + + // number of bytes the storage has. + private long mTotalBytes; + + // number of bytes already used. + private long mUsedBytes; + + // number of bytes used for the cache (should be less then usedBytes). + private long mUsedCacheBytes; + + // number of bytes used for the cache if all pending downloads (and removals) are completed. + private long mTargetCacheBytes; + + private AbstractGalleryActivity mActivity; + private Context mContext; + private long mUserChangeDelta; + + public CacheStorageUsageInfo(AbstractGalleryActivity activity) { + mActivity = activity; + mContext = activity.getAndroidContext(); + } + + public void increaseTargetCacheSize(long delta) { + mUserChangeDelta += delta; + } + + public void loadStorageInfo(JobContext jc) { + File cacheDir = mContext.getExternalCacheDir(); + if (cacheDir == null) { + cacheDir = mContext.getCacheDir(); + } + + String path = cacheDir.getAbsolutePath(); + StatFs stat = new StatFs(path); + long blockSize = stat.getBlockSize(); + long availableBlocks = stat.getAvailableBlocks(); + long totalBlocks = stat.getBlockCount(); + + mTotalBytes = blockSize * totalBlocks; + mUsedBytes = blockSize * (totalBlocks - availableBlocks); + mUsedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize(); + mTargetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize(); + } + + public long getTotalBytes() { + return mTotalBytes; + } + + public long getExpectedUsedBytes() { + return mUsedBytes - mUsedCacheBytes + mTargetCacheBytes + mUserChangeDelta; + } + + public long getUsedBytes() { + // Should it be usedBytes - usedCacheBytes + targetCacheBytes ? + return mUsedBytes; + } + + public long getFreeBytes() { + return mTotalBytes - mUsedBytes; + } +} diff --git a/src/com/android/gallery3d/ui/CaptureAnimation.java b/src/com/android/gallery3d/ui/CaptureAnimation.java new file mode 100644 index 000000000..87c054ab3 --- /dev/null +++ b/src/com/android/gallery3d/ui/CaptureAnimation.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +public class CaptureAnimation { + // The amount of change for zooming out. + private static final float ZOOM_DELTA = 0.2f; + // Pre-calculated value for convenience. + private static final float ZOOM_IN_BEGIN = 1f - ZOOM_DELTA; + + private static final Interpolator sZoomOutInterpolator = + new DecelerateInterpolator(); + private static final Interpolator sZoomInInterpolator = + new AccelerateInterpolator(); + private static final Interpolator sSlideInterpolator = + new AccelerateDecelerateInterpolator(); + + // Calculate the slide factor based on the give time fraction. + public static float calculateSlide(float fraction) { + return sSlideInterpolator.getInterpolation(fraction); + } + + // Calculate the scale factor based on the given time fraction. + public static float calculateScale(float fraction) { + float value; + if (fraction <= 0.5f) { + // Zoom in for the beginning. + value = 1f - ZOOM_DELTA * + sZoomOutInterpolator.getInterpolation(fraction * 2); + } else { + // Zoom out for the ending. + value = ZOOM_IN_BEGIN + ZOOM_DELTA * + sZoomInInterpolator.getInterpolation((fraction - 0.5f) * 2f); + } + return value; + } +} diff --git a/src/com/android/gallery3d/ui/DetailsAddressResolver.java b/src/com/android/gallery3d/ui/DetailsAddressResolver.java new file mode 100644 index 000000000..8de667745 --- /dev/null +++ b/src/com/android/gallery3d/ui/DetailsAddressResolver.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.location.Address; +import android.os.Handler; +import android.os.Looper; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.FutureListener; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ReverseGeocoder; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +public class DetailsAddressResolver { + private AddressResolvingListener mListener; + private final AbstractGalleryActivity mContext; + private Future<Address> mAddressLookupJob; + private final Handler mHandler; + + private class AddressLookupJob implements Job<Address> { + private double[] mLatlng; + + protected AddressLookupJob(double[] latlng) { + mLatlng = latlng; + } + + @Override + public Address run(JobContext jc) { + ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext()); + return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true); + } + } + + public interface AddressResolvingListener { + public void onAddressAvailable(String address); + } + + public DetailsAddressResolver(AbstractGalleryActivity context) { + mContext = context; + mHandler = new Handler(Looper.getMainLooper()); + } + + public String resolveAddress(double[] latlng, AddressResolvingListener listener) { + mListener = listener; + mAddressLookupJob = mContext.getThreadPool().submit( + new AddressLookupJob(latlng), + new FutureListener<Address>() { + @Override + public void onFutureDone(final Future<Address> future) { + mAddressLookupJob = null; + if (!future.isCancelled()) { + mHandler.post(new Runnable() { + @Override + public void run() { + updateLocation(future.get()); + } + }); + } + } + }); + return GalleryUtils.formatLatitudeLongitude("(%f,%f)", latlng[0], latlng[1]); + } + + private void updateLocation(Address address) { + if (address != null) { + Context context = mContext.getAndroidContext(); + String parts[] = { + address.getAdminArea(), + address.getSubAdminArea(), + address.getLocality(), + address.getSubLocality(), + address.getThoroughfare(), + address.getSubThoroughfare(), + address.getPremises(), + address.getPostalCode(), + address.getCountryName() + }; + + String addressText = ""; + for (int i = 0; i < parts.length; i++) { + if (parts[i] == null || parts[i].isEmpty()) continue; + if (!addressText.isEmpty()) { + addressText += ", "; + } + addressText += parts[i]; + } + String text = String.format("%s : %s", DetailsHelper.getDetailsName( + context, MediaDetails.INDEX_LOCATION), addressText); + mListener.onAddressAvailable(text); + } + } + + public void cancel() { + if (mAddressLookupJob != null) { + mAddressLookupJob.cancel(); + mAddressLookupJob = null; + } + } +} diff --git a/src/com/android/gallery3d/ui/DetailsHelper.java b/src/com/android/gallery3d/ui/DetailsHelper.java new file mode 100644 index 000000000..47296f655 --- /dev/null +++ b/src/com/android/gallery3d/ui/DetailsHelper.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.view.View.MeasureSpec; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener; + +public class DetailsHelper { + private static DetailsAddressResolver sAddressResolver; + private DetailsViewContainer mContainer; + + public interface DetailsSource { + public int size(); + public int setIndex(); + public MediaDetails getDetails(); + } + + public interface CloseListener { + public void onClose(); + } + + public interface DetailsViewContainer { + public void reloadDetails(); + public void setCloseListener(CloseListener listener); + public void show(); + public void hide(); + } + + public interface ResolutionResolvingListener { + public void onResolutionAvailable(int width, int height); + } + + public DetailsHelper(AbstractGalleryActivity activity, GLView rootPane, DetailsSource source) { + mContainer = new DialogDetailsView(activity, source); + } + + public void layout(int left, int top, int right, int bottom) { + if (mContainer instanceof GLView) { + GLView view = (GLView) mContainer; + view.measure(MeasureSpec.UNSPECIFIED, + MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.AT_MOST)); + view.layout(0, top, view.getMeasuredWidth(), top + view.getMeasuredHeight()); + } + } + + public void reloadDetails() { + mContainer.reloadDetails(); + } + + public void setCloseListener(CloseListener listener) { + mContainer.setCloseListener(listener); + } + + public static String resolveAddress(AbstractGalleryActivity activity, double[] latlng, + AddressResolvingListener listener) { + if (sAddressResolver == null) { + sAddressResolver = new DetailsAddressResolver(activity); + } else { + sAddressResolver.cancel(); + } + return sAddressResolver.resolveAddress(latlng, listener); + } + + public static void resolveResolution(String path, ResolutionResolvingListener listener) { + Bitmap bitmap = BitmapFactory.decodeFile(path); + if (bitmap == null) return; + listener.onResolutionAvailable(bitmap.getWidth(), bitmap.getHeight()); + } + + public static void pause() { + if (sAddressResolver != null) sAddressResolver.cancel(); + } + + public void show() { + mContainer.show(); + } + + public void hide() { + mContainer.hide(); + } + + public static String getDetailsName(Context context, int key) { + switch (key) { + case MediaDetails.INDEX_TITLE: + return context.getString(R.string.title); + case MediaDetails.INDEX_DESCRIPTION: + return context.getString(R.string.description); + case MediaDetails.INDEX_DATETIME: + return context.getString(R.string.time); + case MediaDetails.INDEX_LOCATION: + return context.getString(R.string.location); + case MediaDetails.INDEX_PATH: + return context.getString(R.string.path); + case MediaDetails.INDEX_WIDTH: + return context.getString(R.string.width); + case MediaDetails.INDEX_HEIGHT: + return context.getString(R.string.height); + case MediaDetails.INDEX_ORIENTATION: + return context.getString(R.string.orientation); + case MediaDetails.INDEX_DURATION: + return context.getString(R.string.duration); + case MediaDetails.INDEX_MIMETYPE: + return context.getString(R.string.mimetype); + case MediaDetails.INDEX_SIZE: + return context.getString(R.string.file_size); + case MediaDetails.INDEX_MAKE: + return context.getString(R.string.maker); + case MediaDetails.INDEX_MODEL: + return context.getString(R.string.model); + case MediaDetails.INDEX_FLASH: + return context.getString(R.string.flash); + case MediaDetails.INDEX_APERTURE: + return context.getString(R.string.aperture); + case MediaDetails.INDEX_FOCAL_LENGTH: + return context.getString(R.string.focal_length); + case MediaDetails.INDEX_WHITE_BALANCE: + return context.getString(R.string.white_balance); + case MediaDetails.INDEX_EXPOSURE_TIME: + return context.getString(R.string.exposure_time); + case MediaDetails.INDEX_ISO: + return context.getString(R.string.iso); + default: + return "Unknown key" + key; + } + } +} + + diff --git a/src/com/android/gallery3d/ui/DialogDetailsView.java b/src/com/android/gallery3d/ui/DialogDetailsView.java new file mode 100644 index 000000000..058c03654 --- /dev/null +++ b/src/com/android/gallery3d/ui/DialogDetailsView.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaDetails; +import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener; +import com.android.gallery3d.ui.DetailsHelper.CloseListener; +import com.android.gallery3d.ui.DetailsHelper.DetailsSource; +import com.android.gallery3d.ui.DetailsHelper.DetailsViewContainer; +import com.android.gallery3d.ui.DetailsHelper.ResolutionResolvingListener; + +import java.util.ArrayList; +import java.util.Map.Entry; + +public class DialogDetailsView implements DetailsViewContainer { + @SuppressWarnings("unused") + private static final String TAG = "DialogDetailsView"; + + private final AbstractGalleryActivity mActivity; + private DetailsAdapter mAdapter; + private MediaDetails mDetails; + private final DetailsSource mSource; + private int mIndex; + private Dialog mDialog; + private CloseListener mListener; + + public DialogDetailsView(AbstractGalleryActivity activity, DetailsSource source) { + mActivity = activity; + mSource = source; + } + + @Override + public void show() { + reloadDetails(); + mDialog.show(); + } + + @Override + public void hide() { + mDialog.hide(); + } + + @Override + public void reloadDetails() { + int index = mSource.setIndex(); + if (index == -1) return; + MediaDetails details = mSource.getDetails(); + if (details != null) { + if (mIndex == index && mDetails == details) return; + mIndex = index; + mDetails = details; + setDetails(details); + } + } + + private void setDetails(MediaDetails details) { + mAdapter = new DetailsAdapter(details); + String title = String.format( + mActivity.getAndroidContext().getString(R.string.details_title), + mIndex + 1, mSource.size()); + ListView detailsList = (ListView) LayoutInflater.from(mActivity.getAndroidContext()).inflate( + R.layout.details_list, null, false); + detailsList.setAdapter(mAdapter); + mDialog = new AlertDialog.Builder(mActivity) + .setView(detailsList) + .setTitle(title) + .setPositiveButton(R.string.close, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + mDialog.dismiss(); + } + }) + .create(); + + mDialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (mListener != null) { + mListener.onClose(); + } + } + }); + } + + + private class DetailsAdapter extends BaseAdapter + implements AddressResolvingListener, ResolutionResolvingListener { + private final ArrayList<String> mItems; + private int mLocationIndex; + private int mWidthIndex = -1; + private int mHeightIndex = -1; + + public DetailsAdapter(MediaDetails details) { + Context context = mActivity.getAndroidContext(); + mItems = new ArrayList<String>(details.size()); + mLocationIndex = -1; + setDetails(context, details); + } + + private void setDetails(Context context, MediaDetails details) { + boolean resolutionIsValid = true; + String path = null; + for (Entry<Integer, Object> detail : details) { + String value; + switch (detail.getKey()) { + case MediaDetails.INDEX_LOCATION: { + double[] latlng = (double[]) detail.getValue(); + mLocationIndex = mItems.size(); + value = DetailsHelper.resolveAddress(mActivity, latlng, this); + break; + } + case MediaDetails.INDEX_SIZE: { + value = Formatter.formatFileSize( + context, (Long) detail.getValue()); + break; + } + case MediaDetails.INDEX_WHITE_BALANCE: { + value = "1".equals(detail.getValue()) + ? context.getString(R.string.manual) + : context.getString(R.string.auto); + break; + } + case MediaDetails.INDEX_FLASH: { + MediaDetails.FlashState flash = + (MediaDetails.FlashState) detail.getValue(); + // TODO: camera doesn't fill in the complete values, show more information + // when it is fixed. + if (flash.isFlashFired()) { + value = context.getString(R.string.flash_on); + } else { + value = context.getString(R.string.flash_off); + } + break; + } + case MediaDetails.INDEX_EXPOSURE_TIME: { + value = (String) detail.getValue(); + double time = Double.valueOf(value); + if (time < 1.0f) { + value = String.format("1/%d", (int) (0.5f + 1 / time)); + } else { + int integer = (int) time; + time -= integer; + value = String.valueOf(integer) + "''"; + if (time > 0.0001) { + value += String.format(" 1/%d", (int) (0.5f + 1 / time)); + } + } + break; + } + case MediaDetails.INDEX_WIDTH: + mWidthIndex = mItems.size(); + value = detail.getValue().toString(); + if (value.equalsIgnoreCase("0")) { + value = context.getString(R.string.unknown); + resolutionIsValid = false; + } + break; + case MediaDetails.INDEX_HEIGHT: { + mHeightIndex = mItems.size(); + value = detail.getValue().toString(); + if (value.equalsIgnoreCase("0")) { + value = context.getString(R.string.unknown); + resolutionIsValid = false; + } + break; + } + case MediaDetails.INDEX_PATH: + // Get the path and then fall through to the default case + path = detail.getValue().toString(); + default: { + Object valueObj = detail.getValue(); + // This shouldn't happen, log its key to help us diagnose the problem. + if (valueObj == null) { + Utils.fail("%s's value is Null", + DetailsHelper.getDetailsName(context, detail.getKey())); + } + value = valueObj.toString(); + } + } + int key = detail.getKey(); + if (details.hasUnit(key)) { + value = String.format("%s: %s %s", DetailsHelper.getDetailsName( + context, key), value, context.getString(details.getUnit(key))); + } else { + value = String.format("%s: %s", DetailsHelper.getDetailsName( + context, key), value); + } + mItems.add(value); + if (!resolutionIsValid) { + DetailsHelper.resolveResolution(path, this); + } + } + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + return false; + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mDetails.getDetail(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView tv; + if (convertView == null) { + tv = (TextView) LayoutInflater.from(mActivity.getAndroidContext()).inflate( + R.layout.details, parent, false); + } else { + tv = (TextView) convertView; + } + tv.setText(mItems.get(position)); + return tv; + } + + @Override + public void onAddressAvailable(String address) { + mItems.set(mLocationIndex, address); + notifyDataSetChanged(); + } + + @Override + public void onResolutionAvailable(int width, int height) { + if (width == 0 || height == 0) return; + // Update the resolution with the new width and height + Context context = mActivity.getAndroidContext(); + String widthString = String.format("%s: %d", DetailsHelper.getDetailsName( + context, MediaDetails.INDEX_WIDTH), width); + String heightString = String.format("%s: %d", DetailsHelper.getDetailsName( + context, MediaDetails.INDEX_HEIGHT), height); + mItems.set(mWidthIndex, String.valueOf(widthString)); + mItems.set(mHeightIndex, String.valueOf(heightString)); + notifyDataSetChanged(); + } + } + + @Override + public void setCloseListener(CloseListener listener) { + mListener = listener; + } +} diff --git a/src/com/android/gallery3d/ui/DownUpDetector.java b/src/com/android/gallery3d/ui/DownUpDetector.java new file mode 100644 index 000000000..19db77262 --- /dev/null +++ b/src/com/android/gallery3d/ui/DownUpDetector.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.view.MotionEvent; + +public class DownUpDetector { + public interface DownUpListener { + void onDown(MotionEvent e); + void onUp(MotionEvent e); + } + + private boolean mStillDown; + private DownUpListener mListener; + + public DownUpDetector(DownUpListener listener) { + mListener = listener; + } + + private void setState(boolean down, MotionEvent e) { + if (down == mStillDown) return; + mStillDown = down; + if (down) { + mListener.onDown(e); + } else { + mListener.onUp(e); + } + } + + public void onTouchEvent(MotionEvent ev) { + switch (ev.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + setState(true, ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_POINTER_DOWN: // Multitouch event - abort. + setState(false, ev); + break; + } + } + + public boolean isDown() { + return mStillDown; + } +} diff --git a/src/com/android/gallery3d/ui/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java new file mode 100644 index 000000000..87ff0c5d3 --- /dev/null +++ b/src/com/android/gallery3d/ui/EdgeEffect.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.R; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; + +// This is copied from android.widget.EdgeEffect with some small modifications: +// (1) Copy the images (overscroll_{edge|glow}.png) to local resources. +// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter. +// (3) Use a private Drawable class (which inherits from ResourceTexture) +// instead of android.graphics.drawable.Drawable to hold the images. +// The private Drawable class is used to translate original Canvas calls to +// corresponding GLCanvas calls. + +/** + * This class performs the graphical effect used at the edges of scrollable widgets + * when the user scrolls beyond the content bounds in 2D space. + * + * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an + * instance for each edge that should show the effect, feed it input data using + * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()}, + * and draw the effect using {@link #draw(Canvas)} in the widget's overridden + * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns + * false after drawing, the edge effect's animation is not yet complete and the widget + * should schedule another drawing pass to continue the animation.</p> + * + * <p>When drawing, widgets should draw their main content and child views first, + * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code> + * method. (This will invoke onDraw and dispatch drawing to child views as needed.) + * The edge effect may then be drawn on top of the view's content using the + * {@link #draw(Canvas)} method.</p> + */ +public class EdgeEffect { + @SuppressWarnings("unused") + private static final String TAG = "EdgeEffect"; + + // Time it will take the effect to fully recede in ms + private static final int RECEDE_TIME = 1000; + + // Time it will take before a pulled glow begins receding in ms + private static final int PULL_TIME = 167; + + // Time it will take in ms for a pulled glow to decay to partial strength before release + private static final int PULL_DECAY_TIME = 1000; + + private static final float MAX_ALPHA = 0.8f; + private static final float HELD_EDGE_ALPHA = 0.7f; + private static final float HELD_EDGE_SCALE_Y = 0.5f; + private static final float HELD_GLOW_ALPHA = 0.5f; + private static final float HELD_GLOW_SCALE_Y = 0.5f; + + private static final float MAX_GLOW_HEIGHT = 4.f; + + private static final float PULL_GLOW_BEGIN = 1.f; + private static final float PULL_EDGE_BEGIN = 0.6f; + + // Minimum velocity that will be absorbed + private static final int MIN_VELOCITY = 100; + + private static final float EPSILON = 0.001f; + + private final Drawable mEdge; + private final Drawable mGlow; + private int mWidth; + private int mHeight; + private final int MIN_WIDTH = 300; + private final int mMinWidth; + + private float mEdgeAlpha; + private float mEdgeScaleY; + private float mGlowAlpha; + private float mGlowScaleY; + + private float mEdgeAlphaStart; + private float mEdgeAlphaFinish; + private float mEdgeScaleYStart; + private float mEdgeScaleYFinish; + private float mGlowAlphaStart; + private float mGlowAlphaFinish; + private float mGlowScaleYStart; + private float mGlowScaleYFinish; + + private long mStartTime; + private float mDuration; + + private final Interpolator mInterpolator; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RECEDE = 3; + private static final int STATE_PULL_DECAY = 4; + + // How much dragging should effect the height of the edge image. + // Number determined by user testing. + private static final int PULL_DISTANCE_EDGE_FACTOR = 7; + + // How much dragging should effect the height of the glow image. + // Number determined by user testing. + private static final int PULL_DISTANCE_GLOW_FACTOR = 7; + private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f; + + private static final int VELOCITY_EDGE_FACTOR = 8; + private static final int VELOCITY_GLOW_FACTOR = 16; + + private int mState = STATE_IDLE; + + private float mPullDistance; + + /** + * Construct a new EdgeEffect with a theme appropriate for the provided context. + * @param context Context used to provide theming and resource information for the EdgeEffect + */ + public EdgeEffect(Context context) { + mEdge = new Drawable(context, R.drawable.overscroll_edge); + mGlow = new Drawable(context, R.drawable.overscroll_glow); + + mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f); + mInterpolator = new DecelerateInterpolator(); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + /** + * Reports if this EdgeEffect's animation is finished. If this method returns false + * after a call to {@link #draw(Canvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return mState == STATE_IDLE; + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + mState = STATE_IDLE; + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + */ + public void onPull(float deltaDistance) { + final long now = AnimationTime.get(); + if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { + return; + } + if (mState != STATE_PULL) { + mGlowScaleY = PULL_GLOW_BEGIN; + } + mState = STATE_PULL; + + mStartTime = now; + mDuration = PULL_TIME; + + mPullDistance += deltaDistance; + float distance = Math.abs(mPullDistance); + + mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA)); + mEdgeScaleY = mEdgeScaleYStart = Math.max( + HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f)); + + mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, + mGlowAlpha + + (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + + float glowChange = Math.abs(deltaDistance); + if (deltaDistance > 0 && mPullDistance < 0) { + glowChange = -glowChange; + } + if (mPullDistance == 0) { + mGlowScaleY = 0; + } + + // Do not allow glow to get larger than MAX_GLOW_HEIGHT. + mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max( + 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR)); + + mEdgeAlphaFinish = mEdgeAlpha; + mEdgeScaleYFinish = mEdgeScaleY; + mGlowAlphaFinish = mGlowAlpha; + mGlowScaleYFinish = mGlowScaleY; + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} and thereby + * draw the results accordingly. + */ + public void onRelease() { + mPullDistance = 0; + + if (mState != STATE_PULL && mState != STATE_PULL_DECAY) { + return; + } + + mState = STATE_RECEDE; + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + + mStartTime = AnimationTime.get(); + mDuration = RECEDE_TIME; + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method <code>getCurrVelocity</code> will provide a reasonable approximation + * to use here.</p> + * + * @param velocity Velocity at impact in pixels per second. + */ + public void onAbsorb(int velocity) { + mState = STATE_ABSORB; + velocity = Math.max(MIN_VELOCITY, Math.abs(velocity)); + + mStartTime = AnimationTime.get(); + mDuration = 0.1f + (velocity * 0.03f); + + // The edge should always be at least partially visible, regardless + // of velocity. + mEdgeAlphaStart = 0.f; + mEdgeScaleY = mEdgeScaleYStart = 0.f; + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = 0.5f; + mGlowScaleYStart = 0.f; + + // Factor the velocity by 8. Testing on device shows this works best to + // reflect the strength of the user's scrolling. + mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1)); + // Edge should never get larger than the size of its asset. + mEdgeScaleYFinish = Math.max( + HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f)); + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + } + + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(GLCanvas canvas) { + update(); + + final int edgeHeight = mEdge.getIntrinsicHeight(); + final int edgeWidth = mEdge.getIntrinsicWidth(); + final int glowHeight = mGlow.getIntrinsicHeight(); + final int glowWidth = mGlow.getIntrinsicWidth(); + + mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255)); + + int glowBottom = (int) Math.min( + glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f, + glowHeight * MAX_GLOW_HEIGHT); + if (mWidth < mMinWidth) { + // Center the glow and clip it. + int glowLeft = (mWidth - mMinWidth)/2; + mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom); + } else { + // Stretch the glow to fit. + mGlow.setBounds(0, 0, mWidth, glowBottom); + } + + mGlow.draw(canvas); + + mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255)); + + int edgeBottom = (int) (edgeHeight * mEdgeScaleY); + if (mWidth < mMinWidth) { + // Center the edge and clip it. + int edgeLeft = (mWidth - mMinWidth)/2; + mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom); + } else { + // Stretch the edge to fit. + mEdge.setBounds(0, 0, mWidth, edgeBottom); + } + mEdge.draw(canvas); + + return mState != STATE_IDLE; + } + + private void update() { + final long time = AnimationTime.get(); + final float t = Math.min((time - mStartTime) / mDuration, 1.f); + + final float interp = mInterpolator.getInterpolation(t); + + mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp; + mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp; + mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; + mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + + if (t >= 1.f - EPSILON) { + switch (mState) { + case STATE_ABSORB: + mState = STATE_RECEDE; + mStartTime = AnimationTime.get(); + mDuration = RECEDE_TIME; + + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After absorb, the glow and edge should fade to nothing. + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL: + mState = STATE_PULL_DECAY; + mStartTime = AnimationTime.get(); + mDuration = PULL_DECAY_TIME; + + mEdgeAlphaStart = mEdgeAlpha; + mEdgeScaleYStart = mEdgeScaleY; + mGlowAlphaStart = mGlowAlpha; + mGlowScaleYStart = mGlowScaleY; + + // After pull, the glow and edge should fade to nothing. + mEdgeAlphaFinish = 0.f; + mEdgeScaleYFinish = 0.f; + mGlowAlphaFinish = 0.f; + mGlowScaleYFinish = 0.f; + break; + case STATE_PULL_DECAY: + // When receding, we want edge to decrease more slowly + // than the glow. + float factor = mGlowScaleYFinish != 0 ? 1 + / (mGlowScaleYFinish * mGlowScaleYFinish) + : Float.MAX_VALUE; + mEdgeScaleY = mEdgeScaleYStart + + (mEdgeScaleYFinish - mEdgeScaleYStart) * + interp * factor; + mState = STATE_RECEDE; + break; + case STATE_RECEDE: + mState = STATE_IDLE; + break; + } + } + } + + private static class Drawable extends ResourceTexture { + private Rect mBounds = new Rect(); + private int mAlpha = 255; + + public Drawable(Context context, int resId) { + super(context, resId); + } + + public int getIntrinsicWidth() { + return getWidth(); + } + + public int getIntrinsicHeight() { + return getHeight(); + } + + public void setBounds(int left, int top, int right, int bottom) { + mBounds.set(left, top, right, bottom); + } + + public void setAlpha(int alpha) { + mAlpha = alpha; + } + + public void draw(GLCanvas canvas) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(mAlpha / 255.0f); + Rect b = mBounds; + draw(canvas, b.left, b.top, b.width(), b.height()); + canvas.restore(); + } + } +} diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java new file mode 100644 index 000000000..051de18fa --- /dev/null +++ b/src/com/android/gallery3d/ui/EdgeView.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.opengl.Matrix; + +import com.android.gallery3d.glrenderer.GLCanvas; + +// EdgeView draws EdgeEffect (blue glow) at four sides of the view. +public class EdgeView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "EdgeView"; + + public static final int INVALID_DIRECTION = -1; + public static final int TOP = 0; + public static final int LEFT = 1; + public static final int BOTTOM = 2; + public static final int RIGHT = 3; + + // Each edge effect has a transform matrix, and each matrix has 16 elements. + // We put all the elements in one array. These constants specify the + // starting index of each matrix. + private static final int TOP_M = TOP * 16; + private static final int LEFT_M = LEFT * 16; + private static final int BOTTOM_M = BOTTOM * 16; + private static final int RIGHT_M = RIGHT * 16; + + private EdgeEffect[] mEffect = new EdgeEffect[4]; + private float[] mMatrix = new float[4 * 16]; + + public EdgeView(Context context) { + for (int i = 0; i < 4; i++) { + mEffect[i] = new EdgeEffect(context); + } + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + if (!changeSize) return; + + int w = right - left; + int h = bottom - top; + for (int i = 0; i < 4; i++) { + if ((i & 1) == 0) { // top or bottom + mEffect[i].setSize(w, h); + } else { // left or right + mEffect[i].setSize(h, w); + } + } + + // Set up transforms for the four edges. Without transforms an + // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y + // is some factor < 1. For other edges we need to move, rotate, and + // flip the effects into proper places. + Matrix.setIdentityM(mMatrix, TOP_M); + Matrix.setIdentityM(mMatrix, LEFT_M); + Matrix.setIdentityM(mMatrix, BOTTOM_M); + Matrix.setIdentityM(mMatrix, RIGHT_M); + + Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1); + Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1); + + Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0); + Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1); + + Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0); + Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + boolean more = false; + for (int i = 0; i < 4; i++) { + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + canvas.multiplyMatrix(mMatrix, i * 16); + more |= mEffect[i].draw(canvas); + canvas.restore(); + } + if (more) { + invalidate(); + } + } + + // Called when the content is pulled away from the edge. + // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}. + public void onPull(int offset, int direction) { + int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight(); + mEffect[direction].onPull((float)offset / fullLength); + if (!mEffect[direction].isFinished()) { + invalidate(); + } + } + + // Call when the object is released after being pulled. + public void onRelease() { + boolean more = false; + for (int i = 0; i < 4; i++) { + mEffect[i].onRelease(); + more |= !mEffect[i].isFinished(); + } + if (more) { + invalidate(); + } + } + + // Call when the effect absorbs an impact at the given velocity. + // Used when a fling reaches the scroll boundary. velocity is in pixels + // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}. + public void onAbsorb(int velocity, int direction) { + mEffect[direction].onAbsorb(velocity); + if (!mEffect[direction].isFinished()) { + invalidate(); + } + } +} diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java new file mode 100644 index 000000000..6f98c64f9 --- /dev/null +++ b/src/com/android/gallery3d/ui/FlingScroller.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + + +// This is a customized version of Scroller, with a interface similar to +// android.widget.Scroller. It does fling only, not scroll. +// +// The differences between the this Scroller and the system one are: +// +// (1) The velocity does not change because of min/max limit. +// (2) The duration is different. +// (3) The deceleration curve is different. +class FlingScroller { + @SuppressWarnings("unused") + private static final String TAG = "FlingController"; + + // The fling duration (in milliseconds) when velocity is 1 pixel/second + private static final float FLING_DURATION_PARAM = 50f; + private static final int DECELERATED_FACTOR = 4; + + private int mStartX, mStartY; + private int mMinX, mMinY, mMaxX, mMaxY; + private double mSinAngle; + private double mCosAngle; + private int mDuration; + private int mDistance; + private int mFinalX, mFinalY; + + private int mCurrX, mCurrY; + private double mCurrV; + + public int getFinalX() { + return mFinalX; + } + + public int getFinalY() { + return mFinalY; + } + + public int getDuration() { + return mDuration; + } + + public int getCurrX() { + return mCurrX; + + } + + public int getCurrY() { + return mCurrY; + } + + public int getCurrVelocityX() { + return (int)Math.round(mCurrV * mCosAngle); + } + + public int getCurrVelocityY() { + return (int)Math.round(mCurrV * mSinAngle); + } + + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + mStartX = startX; + mStartY = startY; + mMinX = minX; + mMinY = minY; + mMaxX = maxX; + mMaxY = maxY; + + double velocity = Math.hypot(velocityX, velocityY); + mSinAngle = velocityY / velocity; + mCosAngle = velocityX / velocity; + // + // The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d) + // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T + // Thus, + // v0 = d * (e - s) / T => (e - s) = v0 * T / d + // + + // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second; + mDuration = (int)Math.round(FLING_DURATION_PARAM + * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1))); + + // (e - s) = v0 * T / d + mDistance = (int)Math.round( + velocity * mDuration / DECELERATED_FACTOR / 1000); + + mFinalX = getX(1.0f); + mFinalY = getY(1.0f); + } + + public void computeScrollOffset(float progress) { + progress = Math.min(progress, 1); + float f = 1 - progress; + f = 1 - (float) Math.pow(f, DECELERATED_FACTOR); + mCurrX = getX(f); + mCurrY = getY(f); + mCurrV = getV(progress); + } + + private int getX(float f) { + int r = (int) Math.round(mStartX + f * mDistance * mCosAngle); + if (mCosAngle > 0 && mStartX <= mMaxX) { + r = Math.min(r, mMaxX); + } else if (mCosAngle < 0 && mStartX >= mMinX) { + r = Math.max(r, mMinX); + } + return r; + } + + private int getY(float f) { + int r = (int) Math.round(mStartY + f * mDistance * mSinAngle); + if (mSinAngle > 0 && mStartY <= mMaxY) { + r = Math.min(r, mMaxY); + } else if (mSinAngle < 0 && mStartY >= mMinY) { + r = Math.max(r, mMinY); + } + return r; + } + + private double getV(float progress) { + // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T + return DECELERATED_FACTOR * mDistance * 1000 * + Math.pow(1 - progress, DECELERATED_FACTOR - 1) / mDuration; + } +} diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java new file mode 100644 index 000000000..33a82eaf7 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRoot.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Matrix; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.glrenderer.GLCanvas; + +public interface GLRoot { + + // Listener will be called when GL is idle AND before each frame. + // Mainly used for uploading textures. + public static interface OnGLIdleListener { + public boolean onGLIdle( + GLCanvas canvas, boolean renderRequested); + } + + public void addOnGLIdleListener(OnGLIdleListener listener); + public void registerLaunchedAnimation(CanvasAnimation animation); + public void requestRenderForced(); + public void requestRender(); + public void requestLayoutContentPane(); + + public void lockRenderThread(); + public void unlockRenderThread(); + + public void setContentPane(GLView content); + public void setOrientationSource(OrientationSource source); + public int getDisplayRotation(); + public int getCompensation(); + public Matrix getCompensationMatrix(); + public void freeze(); + public void unfreeze(); + public void setLightsOutMode(boolean enabled); + + public Context getContext(); +} diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java new file mode 100644 index 000000000..dc898d83d --- /dev/null +++ b/src/com/android/gallery3d/ui/GLRootView.java @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.PixelFormat; +import android.opengl.GLSurfaceView; +import android.os.Build; +import android.os.Process; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.View; + +import com.android.gallery3d.R; +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.BasicTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.GLES11Canvas; +import com.android.gallery3d.glrenderer.GLES20Canvas; +import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.MotionEventHelper; +import com.android.gallery3d.util.Profile; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +// The root component of all <code>GLView</code>s. The rendering is done in GL +// thread while the event handling is done in the main thread. To synchronize +// the two threads, the entry points of this package need to synchronize on the +// <code>GLRootView</code> instance unless it can be proved that the rendering +// thread won't access the same thing as the method. The entry points include: +// (1) The public methods of HeadUpDisplay +// (2) The public methods of CameraHeadUpDisplay +// (3) The overridden methods in GLRootView. +public class GLRootView extends GLSurfaceView + implements GLSurfaceView.Renderer, GLRoot { + private static final String TAG = "GLRootView"; + + private static final boolean DEBUG_FPS = false; + private int mFrameCount = 0; + private long mFrameCountingStart = 0; + + private static final boolean DEBUG_INVALIDATE = false; + private int mInvalidateColor = 0; + + private static final boolean DEBUG_DRAWING_STAT = false; + + private static final boolean DEBUG_PROFILE = false; + private static final boolean DEBUG_PROFILE_SLOW_ONLY = false; + + private static final int FLAG_INITIALIZED = 1; + private static final int FLAG_NEED_LAYOUT = 2; + + private GL11 mGL; + private GLCanvas mCanvas; + private GLView mContentView; + + private OrientationSource mOrientationSource; + // mCompensation is the difference between the UI orientation on GLCanvas + // and the framework orientation. See OrientationManager for details. + private int mCompensation; + // mCompensationMatrix maps the coordinates of touch events. It is kept sync + // with mCompensation. + private Matrix mCompensationMatrix = new Matrix(); + private int mDisplayRotation; + + private int mFlags = FLAG_NEED_LAYOUT; + private volatile boolean mRenderRequested = false; + + private final ArrayList<CanvasAnimation> mAnimations = + new ArrayList<CanvasAnimation>(); + + private final ArrayDeque<OnGLIdleListener> mIdleListeners = + new ArrayDeque<OnGLIdleListener>(); + + private final IdleRunner mIdleRunner = new IdleRunner(); + + private final ReentrantLock mRenderLock = new ReentrantLock(); + private final Condition mFreezeCondition = + mRenderLock.newCondition(); + private boolean mFreeze; + + private long mLastDrawFinishTime; + private boolean mInDownState = false; + private boolean mFirstDraw = true; + + public GLRootView(Context context) { + this(context, null); + } + + public GLRootView(Context context, AttributeSet attrs) { + super(context, attrs); + mFlags |= FLAG_INITIALIZED; + setBackgroundDrawable(null); + setEGLContextClientVersion(ApiHelper.HAS_GLES20_REQUIRED ? 2 : 1); + if (ApiHelper.USE_888_PIXEL_FORMAT) { + setEGLConfigChooser(8, 8, 8, 0, 0, 0); + } else { + setEGLConfigChooser(5, 6, 5, 0, 0, 0); + } + setRenderer(this); + if (ApiHelper.USE_888_PIXEL_FORMAT) { + getHolder().setFormat(PixelFormat.RGB_888); + } else { + getHolder().setFormat(PixelFormat.RGB_565); + } + + // Uncomment this to enable gl error check. + // setDebugFlags(DEBUG_CHECK_GL_ERROR); + } + + @Override + public void registerLaunchedAnimation(CanvasAnimation animation) { + // Register the newly launched animation so that we can set the start + // time more precisely. (Usually, it takes much longer for first + // rendering, so we set the animation start time as the time we + // complete rendering) + mAnimations.add(animation); + } + + @Override + public void addOnGLIdleListener(OnGLIdleListener listener) { + synchronized (mIdleListeners) { + mIdleListeners.addLast(listener); + mIdleRunner.enable(); + } + } + + @Override + public void setContentPane(GLView content) { + if (mContentView == content) return; + if (mContentView != null) { + if (mInDownState) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mContentView.dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + mInDownState = false; + } + mContentView.detachFromRoot(); + BasicTexture.yieldAllTextures(); + } + mContentView = content; + if (content != null) { + content.attachToRoot(this); + requestLayoutContentPane(); + } + } + + @Override + public void requestRenderForced() { + superRequestRender(); + } + + @Override + public void requestRender() { + if (DEBUG_INVALIDATE) { + StackTraceElement e = Thread.currentThread().getStackTrace()[4]; + String caller = e.getFileName() + ":" + e.getLineNumber() + " "; + Log.d(TAG, "invalidate: " + caller); + } + if (mRenderRequested) return; + mRenderRequested = true; + if (ApiHelper.HAS_POST_ON_ANIMATION) { + postOnAnimation(mRequestRenderOnAnimationFrame); + } else { + super.requestRender(); + } + } + + private Runnable mRequestRenderOnAnimationFrame = new Runnable() { + @Override + public void run() { + superRequestRender(); + } + }; + + private void superRequestRender() { + super.requestRender(); + } + + @Override + public void requestLayoutContentPane() { + mRenderLock.lock(); + try { + if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return; + + // "View" system will invoke onLayout() for initialization(bug ?), we + // have to ignore it since the GLThread is not ready yet. + if ((mFlags & FLAG_INITIALIZED) == 0) return; + + mFlags |= FLAG_NEED_LAYOUT; + requestRender(); + } finally { + mRenderLock.unlock(); + } + } + + private void layoutContentPane() { + mFlags &= ~FLAG_NEED_LAYOUT; + + int w = getWidth(); + int h = getHeight(); + int displayRotation = 0; + int compensation = 0; + + // Get the new orientation values + if (mOrientationSource != null) { + displayRotation = mOrientationSource.getDisplayRotation(); + compensation = mOrientationSource.getCompensation(); + } else { + displayRotation = 0; + compensation = 0; + } + + if (mCompensation != compensation) { + mCompensation = compensation; + if (mCompensation % 180 != 0) { + mCompensationMatrix.setRotate(mCompensation); + // move center to origin before rotation + mCompensationMatrix.preTranslate(-w / 2, -h / 2); + // align with the new origin after rotation + mCompensationMatrix.postTranslate(h / 2, w / 2); + } else { + mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2); + } + } + mDisplayRotation = displayRotation; + + // Do the actual layout. + if (mCompensation % 180 != 0) { + int tmp = w; + w = h; + h = tmp; + } + Log.i(TAG, "layout content pane " + w + "x" + h + + " (compensation " + mCompensation + ")"); + if (mContentView != null && w != 0 && h != 0) { + mContentView.layout(0, 0, w, h); + } + // Uncomment this to dump the view hierarchy. + //mContentView.dumpTree(""); + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (changed) requestLayoutContentPane(); + } + + /** + * Called when the context is created, possibly after automatic destruction. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceCreated(GL10 gl1, EGLConfig config) { + GL11 gl = (GL11) gl1; + if (mGL != null) { + // The GL Object has changed + Log.i(TAG, "GLObject has changed from " + mGL + " to " + gl); + } + mRenderLock.lock(); + try { + mGL = gl; + mCanvas = ApiHelper.HAS_GLES20_REQUIRED ? new GLES20Canvas() : new GLES11Canvas(gl); + BasicTexture.invalidateAllTextures(); + } finally { + mRenderLock.unlock(); + } + + if (DEBUG_FPS || DEBUG_PROFILE) { + setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } else { + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + } + } + + /** + * Called when the OpenGL surface is recreated without destroying the + * context. + */ + // This is a GLSurfaceView.Renderer callback + @Override + public void onSurfaceChanged(GL10 gl1, int width, int height) { + Log.i(TAG, "onSurfaceChanged: " + width + "x" + height + + ", gl10: " + gl1.toString()); + Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); + GalleryUtils.setRenderThread(); + if (DEBUG_PROFILE) { + Log.d(TAG, "Start profiling"); + Profile.enable(20); // take a sample every 20ms + } + GL11 gl = (GL11) gl1; + Utils.assertTrue(mGL == gl); + + mCanvas.setSize(width, height); + } + + private void outputFps() { + long now = System.nanoTime(); + if (mFrameCountingStart == 0) { + mFrameCountingStart = now; + } else if ((now - mFrameCountingStart) > 1000000000) { + Log.d(TAG, "fps: " + (double) mFrameCount + * 1000000000 / (now - mFrameCountingStart)); + mFrameCountingStart = now; + mFrameCount = 0; + } + ++mFrameCount; + } + + @Override + public void onDrawFrame(GL10 gl) { + AnimationTime.update(); + long t0; + if (DEBUG_PROFILE_SLOW_ONLY) { + Profile.hold(); + t0 = System.nanoTime(); + } + mRenderLock.lock(); + + while (mFreeze) { + mFreezeCondition.awaitUninterruptibly(); + } + + try { + onDrawFrameLocked(gl); + } finally { + mRenderLock.unlock(); + } + + // We put a black cover View in front of the SurfaceView and hide it + // after the first draw. This prevents the SurfaceView being transparent + // before the first draw. + if (mFirstDraw) { + mFirstDraw = false; + post(new Runnable() { + @Override + public void run() { + View root = getRootView(); + View cover = root.findViewById(R.id.gl_root_cover); + cover.setVisibility(GONE); + } + }); + } + + if (DEBUG_PROFILE_SLOW_ONLY) { + long t = System.nanoTime(); + long durationInMs = (t - mLastDrawFinishTime) / 1000000; + long durationDrawInMs = (t - t0) / 1000000; + mLastDrawFinishTime = t; + + if (durationInMs > 34) { // 34ms -> we skipped at least 2 frames + Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" + + durationInMs + ") -----"); + Profile.commit(); + } else { + Profile.drop(); + } + } + } + + private void onDrawFrameLocked(GL10 gl) { + if (DEBUG_FPS) outputFps(); + + // release the unbound textures and deleted buffers. + mCanvas.deleteRecycledResources(); + + // reset texture upload limit + UploadedTexture.resetUploadLimit(); + + mRenderRequested = false; + + if ((mOrientationSource != null + && mDisplayRotation != mOrientationSource.getDisplayRotation()) + || (mFlags & FLAG_NEED_LAYOUT) != 0) { + layoutContentPane(); + } + + mCanvas.save(GLCanvas.SAVE_FLAG_ALL); + rotateCanvas(-mCompensation); + if (mContentView != null) { + mContentView.render(mCanvas); + } else { + // Make sure we always draw something to prevent displaying garbage + mCanvas.clearBuffer(); + } + mCanvas.restore(); + + if (!mAnimations.isEmpty()) { + long now = AnimationTime.get(); + for (int i = 0, n = mAnimations.size(); i < n; i++) { + mAnimations.get(i).setStartTime(now); + } + mAnimations.clear(); + } + + if (UploadedTexture.uploadLimitReached()) { + requestRender(); + } + + synchronized (mIdleListeners) { + if (!mIdleListeners.isEmpty()) mIdleRunner.enable(); + } + + if (DEBUG_INVALIDATE) { + mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor); + mInvalidateColor = ~mInvalidateColor; + } + + if (DEBUG_DRAWING_STAT) { + mCanvas.dumpStatisticsAndClear(); + } + } + + private void rotateCanvas(int degrees) { + if (degrees == 0) return; + int w = getWidth(); + int h = getHeight(); + int cx = w / 2; + int cy = h / 2; + mCanvas.translate(cx, cy); + mCanvas.rotate(degrees, 0, 0, 1); + if (degrees % 180 != 0) { + mCanvas.translate(-cy, -cx); + } else { + mCanvas.translate(-cx, -cy); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!isEnabled()) return false; + + int action = event.getAction(); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mInDownState = false; + } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { + return false; + } + + if (mCompensation != 0) { + event = MotionEventHelper.transformEvent(event, mCompensationMatrix); + } + + mRenderLock.lock(); + try { + // If this has been detached from root, we don't need to handle event + boolean handled = mContentView != null + && mContentView.dispatchTouchEvent(event); + if (action == MotionEvent.ACTION_DOWN && handled) { + mInDownState = true; + } + return handled; + } finally { + mRenderLock.unlock(); + } + } + + private class IdleRunner implements Runnable { + // true if the idle runner is in the queue + private boolean mActive = false; + + @Override + public void run() { + OnGLIdleListener listener; + synchronized (mIdleListeners) { + mActive = false; + if (mIdleListeners.isEmpty()) return; + listener = mIdleListeners.removeFirst(); + } + mRenderLock.lock(); + boolean keepInQueue; + try { + keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested); + } finally { + mRenderLock.unlock(); + } + synchronized (mIdleListeners) { + if (keepInQueue) mIdleListeners.addLast(listener); + if (!mRenderRequested && !mIdleListeners.isEmpty()) enable(); + } + } + + public void enable() { + // Who gets the flag can add it to the queue + if (mActive) return; + mActive = true; + queueEvent(this); + } + } + + @Override + public void lockRenderThread() { + mRenderLock.lock(); + } + + @Override + public void unlockRenderThread() { + mRenderLock.unlock(); + } + + @Override + public void onPause() { + unfreeze(); + super.onPause(); + if (DEBUG_PROFILE) { + Log.d(TAG, "Stop profiling"); + Profile.disableAll(); + Profile.dumpToFile("/sdcard/gallery.prof"); + Profile.reset(); + } + } + + @Override + public void setOrientationSource(OrientationSource source) { + mOrientationSource = source; + } + + @Override + public int getDisplayRotation() { + return mDisplayRotation; + } + + @Override + public int getCompensation() { + return mCompensation; + } + + @Override + public Matrix getCompensationMatrix() { + return mCompensationMatrix; + } + + @Override + public void freeze() { + mRenderLock.lock(); + mFreeze = true; + mRenderLock.unlock(); + } + + @Override + public void unfreeze() { + mRenderLock.lock(); + mFreeze = false; + mFreezeCondition.signalAll(); + mRenderLock.unlock(); + } + + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + public void setLightsOutMode(boolean enabled) { + if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return; + + int flags = 0; + if (enabled) { + flags = STATUS_BAR_HIDDEN; + if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) { + flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } + setSystemUiVisibility(flags); + } + + // We need to unfreeze in the following methods and in onPause(). + // These methods will wait on GLThread. If we have freezed the GLRootView, + // the GLThread will wait on main thread to call unfreeze and cause dead + // lock. + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + unfreeze(); + super.surfaceChanged(holder, format, w, h); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + unfreeze(); + super.surfaceCreated(holder); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + unfreeze(); + super.surfaceDestroyed(holder); + } + + @Override + protected void onDetachedFromWindow() { + unfreeze(); + super.onDetachedFromWindow(); + } + + @Override + protected void finalize() throws Throwable { + try { + unfreeze(); + } finally { + super.finalize(); + } + } +} diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java new file mode 100644 index 000000000..83de19fe4 --- /dev/null +++ b/src/com/android/gallery3d/ui/GLView.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Rect; +import android.os.SystemClock; +import android.view.MotionEvent; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.anim.StateTransitionAnimation; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.GLCanvas; + +import java.util.ArrayList; + +// GLView is a UI component. It can render to a GLCanvas and accept touch +// events. A GLView may have zero or more child GLView and they form a tree +// structure. The rendering and event handling will pass through the tree +// structure. +// +// A GLView tree should be attached to a GLRoot before event dispatching and +// rendering happens. GLView asks GLRoot to re-render or re-layout the +// GLView hierarchy using requestRender() and requestLayoutContentPane(). +// +// The render() method is called in a separate thread. Before calling +// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the +// rendering thread running at the same time. If there are other entry points +// from main thread (like a Handler) in your GLView, you need to call +// lockRendering() if the rendering thread should not run at the same time. +// +public class GLView { + private static final String TAG = "GLView"; + + public static final int VISIBLE = 0; + public static final int INVISIBLE = 1; + + private static final int FLAG_INVISIBLE = 1; + private static final int FLAG_SET_MEASURED_SIZE = 2; + private static final int FLAG_LAYOUT_REQUESTED = 4; + + public interface OnClickListener { + void onClick(GLView v); + } + + protected final Rect mBounds = new Rect(); + protected final Rect mPaddings = new Rect(); + + private GLRoot mRoot; + protected GLView mParent; + private ArrayList<GLView> mComponents; + private GLView mMotionTarget; + + private CanvasAnimation mAnimation; + + private int mViewFlags = 0; + + protected int mMeasuredWidth = 0; + protected int mMeasuredHeight = 0; + + private int mLastWidthSpec = -1; + private int mLastHeightSpec = -1; + + protected int mScrollY = 0; + protected int mScrollX = 0; + protected int mScrollHeight = 0; + protected int mScrollWidth = 0; + + private float [] mBackgroundColor; + private StateTransitionAnimation mTransition; + + public void startAnimation(CanvasAnimation animation) { + GLRoot root = getGLRoot(); + if (root == null) throw new IllegalStateException(); + mAnimation = animation; + if (mAnimation != null) { + mAnimation.start(); + root.registerLaunchedAnimation(mAnimation); + } + invalidate(); + } + + // Sets the visiblity of this GLView (either GLView.VISIBLE or + // GLView.INVISIBLE). + public void setVisibility(int visibility) { + if (visibility == getVisibility()) return; + if (visibility == VISIBLE) { + mViewFlags &= ~FLAG_INVISIBLE; + } else { + mViewFlags |= FLAG_INVISIBLE; + } + onVisibilityChanged(visibility); + invalidate(); + } + + // Returns GLView.VISIBLE or GLView.INVISIBLE + public int getVisibility() { + return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE; + } + + // This should only be called on the content pane (the topmost GLView). + public void attachToRoot(GLRoot root) { + Utils.assertTrue(mParent == null && mRoot == null); + onAttachToRoot(root); + } + + // This should only be called on the content pane (the topmost GLView). + public void detachFromRoot() { + Utils.assertTrue(mParent == null && mRoot != null); + onDetachFromRoot(); + } + + // Returns the number of children of the GLView. + public int getComponentCount() { + return mComponents == null ? 0 : mComponents.size(); + } + + // Returns the children for the given index. + public GLView getComponent(int index) { + if (mComponents == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mComponents.get(index); + } + + // Adds a child to this GLView. + public void addComponent(GLView component) { + // Make sure the component doesn't have a parent currently. + if (component.mParent != null) throw new IllegalStateException(); + + // Build parent-child links + if (mComponents == null) { + mComponents = new ArrayList<GLView>(); + } + mComponents.add(component); + component.mParent = this; + + // If this is added after we have a root, tell the component. + if (mRoot != null) { + component.onAttachToRoot(mRoot); + } + } + + // Removes a child from this GLView. + public boolean removeComponent(GLView component) { + if (mComponents == null) return false; + if (mComponents.remove(component)) { + removeOneComponent(component); + return true; + } + return false; + } + + // Removes all children of this GLView. + public void removeAllComponents() { + for (int i = 0, n = mComponents.size(); i < n; ++i) { + removeOneComponent(mComponents.get(i)); + } + mComponents.clear(); + } + + private void removeOneComponent(GLView component) { + if (mMotionTarget == component) { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + dispatchTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + component.onDetachFromRoot(); + component.mParent = null; + } + + public Rect bounds() { + return mBounds; + } + + public int getWidth() { + return mBounds.right - mBounds.left; + } + + public int getHeight() { + return mBounds.bottom - mBounds.top; + } + + public GLRoot getGLRoot() { + return mRoot; + } + + // Request re-rendering of the view hierarchy. + // This is used for animation or when the contents changed. + public void invalidate() { + GLRoot root = getGLRoot(); + if (root != null) root.requestRender(); + } + + // Request re-layout of the view hierarchy. + public void requestLayout() { + mViewFlags |= FLAG_LAYOUT_REQUESTED; + mLastHeightSpec = -1; + mLastWidthSpec = -1; + if (mParent != null) { + mParent.requestLayout(); + } else { + // Is this a content pane ? + GLRoot root = getGLRoot(); + if (root != null) root.requestLayoutContentPane(); + } + } + + protected void render(GLCanvas canvas) { + boolean transitionActive = false; + if (mTransition != null && mTransition.calculate(AnimationTime.get())) { + invalidate(); + transitionActive = mTransition.isActive(); + } + renderBackground(canvas); + canvas.save(); + if (transitionActive) { + mTransition.applyContentTransform(this, canvas); + } + for (int i = 0, n = getComponentCount(); i < n; ++i) { + renderChild(canvas, getComponent(i)); + } + canvas.restore(); + if (transitionActive) { + mTransition.applyOverlay(this, canvas); + } + } + + public void setIntroAnimation(StateTransitionAnimation intro) { + mTransition = intro; + if (mTransition != null) mTransition.start(); + } + + public float [] getBackgroundColor() { + return mBackgroundColor; + } + + public void setBackgroundColor(float [] color) { + mBackgroundColor = color; + } + + protected void renderBackground(GLCanvas view) { + if (mBackgroundColor != null) { + view.clearBuffer(mBackgroundColor); + } + if (mTransition != null && mTransition.isActive()) { + mTransition.applyBackground(this, view); + return; + } + } + + protected void renderChild(GLCanvas canvas, GLView component) { + if (component.getVisibility() != GLView.VISIBLE + && component.mAnimation == null) return; + + int xoffset = component.mBounds.left - mScrollX; + int yoffset = component.mBounds.top - mScrollY; + + canvas.translate(xoffset, yoffset); + + CanvasAnimation anim = component.mAnimation; + if (anim != null) { + canvas.save(anim.getCanvasSaveFlags()); + if (anim.calculate(AnimationTime.get())) { + invalidate(); + } else { + component.mAnimation = null; + } + anim.apply(canvas); + } + component.render(canvas); + if (anim != null) canvas.restore(); + canvas.translate(-xoffset, -yoffset); + } + + protected boolean onTouch(MotionEvent event) { + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event, + int x, int y, GLView component, boolean checkBounds) { + Rect rect = component.mBounds; + int left = rect.left; + int top = rect.top; + if (!checkBounds || rect.contains(x, y)) { + event.offsetLocation(-left, -top); + if (component.dispatchTouchEvent(event)) { + event.offsetLocation(left, top); + return true; + } + event.offsetLocation(left, top); + } + return false; + } + + protected boolean dispatchTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + int action = event.getAction(); + if (mMotionTarget != null) { + if (action == MotionEvent.ACTION_DOWN) { + MotionEvent cancel = MotionEvent.obtain(event); + cancel.setAction(MotionEvent.ACTION_CANCEL); + dispatchTouchEvent(cancel, x, y, mMotionTarget, false); + mMotionTarget = null; + } else { + dispatchTouchEvent(event, x, y, mMotionTarget, false); + if (action == MotionEvent.ACTION_CANCEL + || action == MotionEvent.ACTION_UP) { + mMotionTarget = null; + } + return true; + } + } + if (action == MotionEvent.ACTION_DOWN) { + // in the reverse rendering order + for (int i = getComponentCount() - 1; i >= 0; --i) { + GLView component = getComponent(i); + if (component.getVisibility() != GLView.VISIBLE) continue; + if (dispatchTouchEvent(event, x, y, component, true)) { + mMotionTarget = component; + return true; + } + } + } + return onTouch(event); + } + + public Rect getPaddings() { + return mPaddings; + } + + public void layout(int left, int top, int right, int bottom) { + boolean sizeChanged = setBounds(left, top, right, bottom); + mViewFlags &= ~FLAG_LAYOUT_REQUESTED; + // We call onLayout no matter sizeChanged is true or not because the + // orientation may change without changing the size of the View (for + // example, rotate the device by 180 degrees), and we want to handle + // orientation change in onLayout. + onLayout(sizeChanged, left, top, right, bottom); + } + + private boolean setBounds(int left, int top, int right, int bottom) { + boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) + || (bottom - top) != (mBounds.bottom - mBounds.top); + mBounds.set(left, top, right, bottom); + return sizeChanged; + } + + public void measure(int widthSpec, int heightSpec) { + if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec + && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { + return; + } + + mLastWidthSpec = widthSpec; + mLastHeightSpec = heightSpec; + + mViewFlags &= ~FLAG_SET_MEASURED_SIZE; + onMeasure(widthSpec, heightSpec); + if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { + throw new IllegalStateException(getClass().getName() + + " should call setMeasuredSize() in onMeasure()"); + } + } + + protected void onMeasure(int widthSpec, int heightSpec) { + } + + protected void setMeasuredSize(int width, int height) { + mViewFlags |= FLAG_SET_MEASURED_SIZE; + mMeasuredWidth = width; + mMeasuredHeight = height; + } + + public int getMeasuredWidth() { + return mMeasuredWidth; + } + + public int getMeasuredHeight() { + return mMeasuredHeight; + } + + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + } + + /** + * Gets the bounds of the given descendant that relative to this view. + */ + public boolean getBoundsOf(GLView descendant, Rect out) { + int xoffset = 0; + int yoffset = 0; + GLView view = descendant; + while (view != this) { + if (view == null) return false; + Rect bounds = view.mBounds; + xoffset += bounds.left; + yoffset += bounds.top; + view = view.mParent; + } + out.set(xoffset, yoffset, xoffset + descendant.getWidth(), + yoffset + descendant.getHeight()); + return true; + } + + protected void onVisibilityChanged(int visibility) { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + GLView child = getComponent(i); + if (child.getVisibility() == GLView.VISIBLE) { + child.onVisibilityChanged(visibility); + } + } + } + + protected void onAttachToRoot(GLRoot root) { + mRoot = root; + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onAttachToRoot(root); + } + } + + protected void onDetachFromRoot() { + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).onDetachFromRoot(); + } + mRoot = null; + } + + public void lockRendering() { + if (mRoot != null) { + mRoot.lockRenderThread(); + } + } + + public void unlockRendering() { + if (mRoot != null) { + mRoot.unlockRenderThread(); + } + } + + // This is for debugging only. + // Dump the view hierarchy into log. + void dumpTree(String prefix) { + Log.d(TAG, prefix + getClass().getSimpleName()); + for (int i = 0, n = getComponentCount(); i < n; ++i) { + getComponent(i).dumpTree(prefix + "...."); + } + } +} diff --git a/src/com/android/gallery3d/ui/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java new file mode 100644 index 000000000..1e5250b9b --- /dev/null +++ b/src/com/android/gallery3d/ui/GestureRecognizer.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector, and DownUpDetector. +public class GestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "GestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onScroll(float dx, float dy, float totalX, float totalY); + boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + void onScaleEnd(); + void onDown(float x, float y); + void onUp(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final DownUpDetector mDownUpDetector; + private final Listener mListener; + + public GestureRecognizer(Context context, Listener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + mDownUpDetector = new DownUpDetector(new MyDownUpListener()); + } + + public void onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + mDownUpDetector.onTouchEvent(event); + } + + public boolean isDown() { + return mDownUpDetector.isDown(); + } + + public void cancelScale() { + long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain( + now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mScaleDetector.onTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll( + dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY()); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(e1, e2, velocityX, velocityY); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } + + private class MyDownUpListener implements DownUpDetector.DownUpListener { + @Override + public void onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + } + + @Override + public void onUp(MotionEvent e) { + mListener.onUp(); + } + } +} diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java new file mode 100644 index 000000000..5570763bb --- /dev/null +++ b/src/com/android/gallery3d/ui/Log.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +// TODO: Delete this +public class Log { + public static int v(String tag, String msg) { + return android.util.Log.v(tag, msg); + } + public static int v(String tag, String msg, Throwable tr) { + return android.util.Log.v(tag, msg, tr); + } + public static int d(String tag, String msg) { + return android.util.Log.d(tag, msg); + } + public static int d(String tag, String msg, Throwable tr) { + return android.util.Log.d(tag, msg, tr); + } + public static int i(String tag, String msg) { + return android.util.Log.i(tag, msg); + } + public static int i(String tag, String msg, Throwable tr) { + return android.util.Log.i(tag, msg, tr); + } + public static int w(String tag, String msg) { + return android.util.Log.w(tag, msg); + } + public static int w(String tag, String msg, Throwable tr) { + return android.util.Log.w(tag, msg, tr); + } + public static int w(String tag, Throwable tr) { + return android.util.Log.w(tag, tr); + } + public static int e(String tag, String msg) { + return android.util.Log.e(tag, msg); + } + public static int e(String tag, String msg, Throwable tr) { + return android.util.Log.e(tag, msg, tr); + } +} diff --git a/src/com/android/gallery3d/ui/ManageCacheDrawer.java b/src/com/android/gallery3d/ui/ManageCacheDrawer.java new file mode 100644 index 000000000..d210bd1f1 --- /dev/null +++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.DataSourceType; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.StringTexture; +import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry; + +public class ManageCacheDrawer extends AlbumSetSlotRenderer { + private final ResourceTexture mCheckedItem; + private final ResourceTexture mUnCheckedItem; + private final SelectionManager mSelectionManager; + + private final ResourceTexture mLocalAlbumIcon; + private final StringTexture mCachingText; + + private final int mCachePinSize; + private final int mCachePinMargin; + + public ManageCacheDrawer(AbstractGalleryActivity activity, SelectionManager selectionManager, + SlotView slotView, LabelSpec labelSpec, int cachePinSize, int cachePinMargin) { + super(activity, selectionManager, slotView, labelSpec, + activity.getResources().getColor(R.color.cache_placeholder)); + Context context = activity; + mCheckedItem = new ResourceTexture( + context, R.drawable.btn_make_offline_normal_on_holo_dark); + mUnCheckedItem = new ResourceTexture( + context, R.drawable.btn_make_offline_normal_off_holo_dark); + mLocalAlbumIcon = new ResourceTexture( + context, R.drawable.btn_make_offline_disabled_on_holo_dark); + String cachingLabel = context.getString(R.string.caching_label); + mCachingText = StringTexture.newInstance(cachingLabel, 12, 0xffffffff); + mSelectionManager = selectionManager; + mCachePinSize = cachePinSize; + mCachePinMargin = cachePinMargin; + } + + private static boolean isLocal(int dataSourceType) { + return dataSourceType != DataSourceType.TYPE_PICASA; + } + + @Override + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) { + AlbumSetEntry entry = mDataWindow.get(index); + + boolean wantCache = entry.cacheFlag == MediaSet.CACHE_FLAG_FULL; + boolean isCaching = wantCache && ( + entry.cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL); + boolean selected = mSelectionManager.isItemSelected(entry.setPath); + boolean chooseToCache = wantCache ^ selected; + boolean available = isLocal(entry.sourceType) || chooseToCache; + + int renderRequestFlags = 0; + + if (!available) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(0.6f); + } + renderRequestFlags |= renderContent(canvas, entry, width, height); + if (!available) canvas.restore(); + + renderRequestFlags |= renderLabel(canvas, entry, width, height); + + drawCachingPin(canvas, entry.setPath, + entry.sourceType, isCaching, chooseToCache, width, height); + + renderRequestFlags |= renderOverlay(canvas, index, entry, width, height); + return renderRequestFlags; + } + + private void drawCachingPin(GLCanvas canvas, Path path, int dataSourceType, + boolean isCaching, boolean chooseToCache, int width, int height) { + ResourceTexture icon; + if (isLocal(dataSourceType)) { + icon = mLocalAlbumIcon; + } else if (chooseToCache) { + icon = mCheckedItem; + } else { + icon = mUnCheckedItem; + } + + // show the icon in right bottom + int s = mCachePinSize; + int m = mCachePinMargin; + icon.draw(canvas, width - m - s, height - s, s, s); + + if (isCaching) { + int w = mCachingText.getWidth(); + int h = mCachingText.getHeight(); + // Show the caching text in bottom center + mCachingText.draw(canvas, (width - w) / 2, height - h); + } + } +} diff --git a/src/com/android/gallery3d/ui/MeasureHelper.java b/src/com/android/gallery3d/ui/MeasureHelper.java new file mode 100644 index 000000000..f65dc10b3 --- /dev/null +++ b/src/com/android/gallery3d/ui/MeasureHelper.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Rect; +import android.view.View.MeasureSpec; + +class MeasureHelper { + + private static MeasureHelper sInstance = new MeasureHelper(null); + + private GLView mComponent; + private int mPreferredWidth; + private int mPreferredHeight; + + private MeasureHelper(GLView component) { + mComponent = component; + } + + public static MeasureHelper getInstance(GLView component) { + sInstance.mComponent = component; + return sInstance; + } + + public MeasureHelper setPreferredContentSize(int width, int height) { + mPreferredWidth = width; + mPreferredHeight = height; + return this; + } + + public void measure(int widthSpec, int heightSpec) { + Rect p = mComponent.getPaddings(); + setMeasuredSize( + getLength(widthSpec, mPreferredWidth + p.left + p.right), + getLength(heightSpec, mPreferredHeight + p.top + p.bottom)); + } + + private static int getLength(int measureSpec, int prefered) { + int specLength = MeasureSpec.getSize(measureSpec); + switch(MeasureSpec.getMode(measureSpec)) { + case MeasureSpec.EXACTLY: return specLength; + case MeasureSpec.AT_MOST: return Math.min(prefered, specLength); + default: return prefered; + } + } + + protected void setMeasuredSize(int width, int height) { + mComponent.setMeasuredSize(width, height); + } + +} diff --git a/src/com/android/gallery3d/ui/MenuExecutor.java b/src/com/android/gallery3d/ui/MenuExecutor.java new file mode 100644 index 000000000..29def0527 --- /dev/null +++ b/src/com/android/gallery3d/ui/MenuExecutor.java @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Handler; +import android.os.Message; +import android.view.Menu; +import android.view.MenuItem; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.filtershow.crop.CropActivity; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.ThreadPool.Job; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.ArrayList; + +public class MenuExecutor { + @SuppressWarnings("unused") + private static final String TAG = "MenuExecutor"; + + private static final int MSG_TASK_COMPLETE = 1; + private static final int MSG_TASK_UPDATE = 2; + private static final int MSG_TASK_START = 3; + private static final int MSG_DO_SHARE = 4; + + public static final int EXECUTION_RESULT_SUCCESS = 1; + public static final int EXECUTION_RESULT_FAIL = 2; + public static final int EXECUTION_RESULT_CANCEL = 3; + + private ProgressDialog mDialog; + private Future<?> mTask; + // wait the operation to finish when we want to stop it. + private boolean mWaitOnStop; + private boolean mPaused; + + private final AbstractGalleryActivity mActivity; + private final SelectionManager mSelectionManager; + private final Handler mHandler; + + private static ProgressDialog createProgressDialog( + Context context, int titleId, int progressMax) { + ProgressDialog dialog = new ProgressDialog(context); + dialog.setTitle(titleId); + dialog.setMax(progressMax); + dialog.setCancelable(false); + dialog.setIndeterminate(false); + if (progressMax > 1) { + dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + } + return dialog; + } + + public interface ProgressListener { + public void onConfirmDialogShown(); + public void onConfirmDialogDismissed(boolean confirmed); + public void onProgressStart(); + public void onProgressUpdate(int index); + public void onProgressComplete(int result); + } + + public MenuExecutor( + AbstractGalleryActivity activity, SelectionManager selectionManager) { + mActivity = Utils.checkNotNull(activity); + mSelectionManager = Utils.checkNotNull(selectionManager); + mHandler = new SynchronizedHandler(mActivity.getGLRoot()) { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_TASK_START: { + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressStart(); + } + break; + } + case MSG_TASK_COMPLETE: { + stopTaskAndDismissDialog(); + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressComplete(message.arg1); + } + mSelectionManager.leaveSelectionMode(); + break; + } + case MSG_TASK_UPDATE: { + if (mDialog != null && !mPaused) mDialog.setProgress(message.arg1); + if (message.obj != null) { + ProgressListener listener = (ProgressListener) message.obj; + listener.onProgressUpdate(message.arg1); + } + break; + } + case MSG_DO_SHARE: { + ((Activity) mActivity).startActivity((Intent) message.obj); + break; + } + } + } + }; + } + + private void stopTaskAndDismissDialog() { + if (mTask != null) { + if (!mWaitOnStop) mTask.cancel(); + if (mDialog != null && mDialog.isShowing()) mDialog.dismiss(); + mDialog = null; + mTask = null; + } + } + + public void resume() { + mPaused = false; + if (mDialog != null) mDialog.show(); + } + + public void pause() { + mPaused = true; + if (mDialog != null && mDialog.isShowing()) mDialog.hide(); + } + + public void destroy() { + stopTaskAndDismissDialog(); + } + + private void onProgressUpdate(int index, ProgressListener listener) { + mHandler.sendMessage( + mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener)); + } + + private void onProgressStart(ProgressListener listener) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_START, listener)); + } + + private void onProgressComplete(int result, ProgressListener listener) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener)); + } + + public static void updateMenuOperation(Menu menu, int supported) { + boolean supportDelete = (supported & MediaObject.SUPPORT_DELETE) != 0; + boolean supportRotate = (supported & MediaObject.SUPPORT_ROTATE) != 0; + boolean supportCrop = (supported & MediaObject.SUPPORT_CROP) != 0; + boolean supportTrim = (supported & MediaObject.SUPPORT_TRIM) != 0; + boolean supportMute = (supported & MediaObject.SUPPORT_MUTE) != 0; + boolean supportShare = (supported & MediaObject.SUPPORT_SHARE) != 0; + boolean supportSetAs = (supported & MediaObject.SUPPORT_SETAS) != 0; + boolean supportShowOnMap = (supported & MediaObject.SUPPORT_SHOW_ON_MAP) != 0; + boolean supportCache = (supported & MediaObject.SUPPORT_CACHE) != 0; + boolean supportEdit = (supported & MediaObject.SUPPORT_EDIT) != 0; + boolean supportInfo = (supported & MediaObject.SUPPORT_INFO) != 0; + + setMenuItemVisible(menu, R.id.action_delete, supportDelete); + setMenuItemVisible(menu, R.id.action_rotate_ccw, supportRotate); + setMenuItemVisible(menu, R.id.action_rotate_cw, supportRotate); + setMenuItemVisible(menu, R.id.action_crop, supportCrop); + setMenuItemVisible(menu, R.id.action_trim, supportTrim); + setMenuItemVisible(menu, R.id.action_mute, supportMute); + // Hide panorama until call to updateMenuForPanorama corrects it + setMenuItemVisible(menu, R.id.action_share_panorama, false); + setMenuItemVisible(menu, R.id.action_share, supportShare); + setMenuItemVisible(menu, R.id.action_setas, supportSetAs); + setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap); + setMenuItemVisible(menu, R.id.action_edit, supportEdit); + setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit); + setMenuItemVisible(menu, R.id.action_details, supportInfo); + } + + public static void updateMenuForPanorama(Menu menu, boolean shareAsPanorama360, + boolean disablePanorama360Options) { + setMenuItemVisible(menu, R.id.action_share_panorama, shareAsPanorama360); + if (disablePanorama360Options) { + setMenuItemVisible(menu, R.id.action_rotate_ccw, false); + setMenuItemVisible(menu, R.id.action_rotate_cw, false); + } + } + + private static void setMenuItemVisible(Menu menu, int itemId, boolean visible) { + MenuItem item = menu.findItem(itemId); + if (item != null) item.setVisible(visible); + } + + private Path getSingleSelectedPath() { + ArrayList<Path> ids = mSelectionManager.getSelected(true); + Utils.assertTrue(ids.size() == 1); + return ids.get(0); + } + + private Intent getIntentBySingleSelectedPath(String action) { + DataManager manager = mActivity.getDataManager(); + Path path = getSingleSelectedPath(); + String mimeType = getMimeType(manager.getMediaType(path)); + return new Intent(action).setDataAndType(manager.getContentUri(path), mimeType); + } + + private void onMenuClicked(int action, ProgressListener listener) { + onMenuClicked(action, listener, false, true); + } + + public void onMenuClicked(int action, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { + int title; + switch (action) { + case R.id.action_select_all: + if (mSelectionManager.inSelectAllMode()) { + mSelectionManager.deSelectAll(); + } else { + mSelectionManager.selectAll(); + } + return; + case R.id.action_crop: { + Intent intent = getIntentBySingleSelectedPath(CropActivity.CROP_ACTION); + ((Activity) mActivity).startActivity(intent); + return; + } + case R.id.action_edit: { + Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_EDIT) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + ((Activity) mActivity).startActivity(Intent.createChooser(intent, null)); + return; + } + case R.id.action_setas: { + Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_ATTACH_DATA) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.putExtra("mimeType", intent.getType()); + Activity activity = mActivity; + activity.startActivity(Intent.createChooser( + intent, activity.getString(R.string.set_as))); + return; + } + case R.id.action_delete: + title = R.string.delete; + break; + case R.id.action_rotate_cw: + title = R.string.rotate_right; + break; + case R.id.action_rotate_ccw: + title = R.string.rotate_left; + break; + case R.id.action_show_on_map: + title = R.string.show_on_map; + break; + default: + return; + } + startAction(action, title, listener, waitOnStop, showDialog); + } + + private class ConfirmDialogListener implements OnClickListener, OnCancelListener { + private final int mActionId; + private final ProgressListener mListener; + + public ConfirmDialogListener(int actionId, ProgressListener listener) { + mActionId = actionId; + mListener = listener; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + if (mListener != null) { + mListener.onConfirmDialogDismissed(true); + } + onMenuClicked(mActionId, mListener); + } else { + if (mListener != null) { + mListener.onConfirmDialogDismissed(false); + } + } + } + + @Override + public void onCancel(DialogInterface dialog) { + if (mListener != null) { + mListener.onConfirmDialogDismissed(false); + } + } + } + + public void onMenuClicked(MenuItem menuItem, String confirmMsg, + final ProgressListener listener) { + final int action = menuItem.getItemId(); + + if (confirmMsg != null) { + if (listener != null) listener.onConfirmDialogShown(); + ConfirmDialogListener cdl = new ConfirmDialogListener(action, listener); + new AlertDialog.Builder(mActivity.getAndroidContext()) + .setMessage(confirmMsg) + .setOnCancelListener(cdl) + .setPositiveButton(R.string.ok, cdl) + .setNegativeButton(R.string.cancel, cdl) + .create().show(); + } else { + onMenuClicked(action, listener); + } + } + + public void startAction(int action, int title, ProgressListener listener) { + startAction(action, title, listener, false, true); + } + + public void startAction(int action, int title, ProgressListener listener, + boolean waitOnStop, boolean showDialog) { + ArrayList<Path> ids = mSelectionManager.getSelected(false); + stopTaskAndDismissDialog(); + + Activity activity = mActivity; + if (showDialog) { + mDialog = createProgressDialog(activity, title, ids.size()); + mDialog.show(); + } else { + mDialog = null; + } + MediaOperation operation = new MediaOperation(action, ids, listener); + mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null); + mWaitOnStop = waitOnStop; + } + + public void startSingleItemAction(int action, Path targetPath) { + ArrayList<Path> ids = new ArrayList<Path>(1); + ids.add(targetPath); + mDialog = null; + MediaOperation operation = new MediaOperation(action, ids, null); + mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null); + mWaitOnStop = false; + } + + public static String getMimeType(int type) { + switch (type) { + case MediaObject.MEDIA_TYPE_IMAGE : + return GalleryUtils.MIME_TYPE_IMAGE; + case MediaObject.MEDIA_TYPE_VIDEO : + return GalleryUtils.MIME_TYPE_VIDEO; + default: return GalleryUtils.MIME_TYPE_ALL; + } + } + + private boolean execute( + DataManager manager, JobContext jc, int cmd, Path path) { + boolean result = true; + Log.v(TAG, "Execute cmd: " + cmd + " for " + path); + long startTime = System.currentTimeMillis(); + + switch (cmd) { + case R.id.action_delete: + manager.delete(path); + break; + case R.id.action_rotate_cw: + manager.rotate(path, 90); + break; + case R.id.action_rotate_ccw: + manager.rotate(path, -90); + break; + case R.id.action_toggle_full_caching: { + MediaObject obj = manager.getMediaObject(path); + int cacheFlag = obj.getCacheFlag(); + if (cacheFlag == MediaObject.CACHE_FLAG_FULL) { + cacheFlag = MediaObject.CACHE_FLAG_SCREENNAIL; + } else { + cacheFlag = MediaObject.CACHE_FLAG_FULL; + } + obj.cache(cacheFlag); + break; + } + case R.id.action_show_on_map: { + MediaItem item = (MediaItem) manager.getMediaObject(path); + double latlng[] = new double[2]; + item.getLatLong(latlng); + if (GalleryUtils.isValidLocation(latlng[0], latlng[1])) { + GalleryUtils.showOnMap(mActivity, latlng[0], latlng[1]); + } + break; + } + default: + throw new AssertionError(); + } + Log.v(TAG, "It takes " + (System.currentTimeMillis() - startTime) + + " ms to execute cmd for " + path); + return result; + } + + private class MediaOperation implements Job<Void> { + private final ArrayList<Path> mItems; + private final int mOperation; + private final ProgressListener mListener; + + public MediaOperation(int operation, ArrayList<Path> items, + ProgressListener listener) { + mOperation = operation; + mItems = items; + mListener = listener; + } + + @Override + public Void run(JobContext jc) { + int index = 0; + DataManager manager = mActivity.getDataManager(); + int result = EXECUTION_RESULT_SUCCESS; + try { + onProgressStart(mListener); + for (Path id : mItems) { + if (jc.isCancelled()) { + result = EXECUTION_RESULT_CANCEL; + break; + } + if (!execute(manager, jc, mOperation, id)) { + result = EXECUTION_RESULT_FAIL; + } + onProgressUpdate(index++, mListener); + } + } catch (Throwable th) { + Log.e(TAG, "failed to execute operation " + mOperation + + " : " + th); + } finally { + onProgressComplete(result, mListener); + } + return null; + } + } +} diff --git a/src/com/android/gallery3d/ui/OrientationSource.java b/src/com/android/gallery3d/ui/OrientationSource.java new file mode 100644 index 000000000..e13ce1cec --- /dev/null +++ b/src/com/android/gallery3d/ui/OrientationSource.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +public interface OrientationSource { + public int getDisplayRotation(); + public int getCompensation(); +} diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java new file mode 100644 index 000000000..b36f5c3a2 --- /dev/null +++ b/src/com/android/gallery3d/ui/Paper.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Rect; +import android.opengl.Matrix; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.common.Utils; + +// This class does the overscroll effect. +class Paper { + @SuppressWarnings("unused") + private static final String TAG = "Paper"; + private static final int ROTATE_FACTOR = 4; + private EdgeAnimation mAnimationLeft = new EdgeAnimation(); + private EdgeAnimation mAnimationRight = new EdgeAnimation(); + private int mWidth; + private float[] mMatrix = new float[16]; + + public void overScroll(float distance) { + distance /= mWidth; // make it relative to width + if (distance < 0) { + mAnimationLeft.onPull(-distance); + } else { + mAnimationRight.onPull(distance); + } + } + + public void edgeReached(float velocity) { + velocity /= mWidth; // make it relative to width + if (velocity < 0) { + mAnimationRight.onAbsorb(-velocity); + } else { + mAnimationLeft.onAbsorb(velocity); + } + } + + public void onRelease() { + mAnimationLeft.onRelease(); + mAnimationRight.onRelease(); + } + + public boolean advanceAnimation() { + // Note that we use "|" because we want both animations get updated. + return mAnimationLeft.update() | mAnimationRight.update(); + } + + public void setSize(int width, int height) { + mWidth = width; + } + + public float[] getTransform(Rect rect, float scrollX) { + float left = mAnimationLeft.getValue(); + float right = mAnimationRight.getValue(); + float screenX = rect.centerX() - scrollX; + // We linearly interpolate the value [left, right] for the screenX + // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside + // the screen, we still get some transform. + float x = screenX + mWidth / 4; + int range = 3 * mWidth / 2; + float t = ((range - x) * left - x * right) / range; + // compress t to the range (-1, 1) by the function + // f(t) = (1 / (1 + e^-t) - 0.5) * 2 + // then multiply by 90 to make the range (-45, 45) + float degrees = + (1 / (1 + (float) Math.exp(-t * ROTATE_FACTOR)) - 0.5f) * 2 * -45; + Matrix.setIdentityM(mMatrix, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, rect.centerX(), rect.centerY(), 0); + Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0); + Matrix.translateM(mMatrix, 0, mMatrix, 0, -rect.width() / 2, -rect.height() / 2, 0); + return mMatrix; + } +} + +// This class follows the structure of frameworks's EdgeEffect class. +class EdgeAnimation { + @SuppressWarnings("unused") + private static final String TAG = "EdgeAnimation"; + + private static final int STATE_IDLE = 0; + private static final int STATE_PULL = 1; + private static final int STATE_ABSORB = 2; + private static final int STATE_RELEASE = 3; + + // Time it will take the effect to fully done in ms + private static final int ABSORB_TIME = 200; + private static final int RELEASE_TIME = 500; + + private static final float VELOCITY_FACTOR = 0.1f; + + private final Interpolator mInterpolator; + + private int mState; + private float mValue; + + private float mValueStart; + private float mValueFinish; + private long mStartTime; + private long mDuration; + + public EdgeAnimation() { + mInterpolator = new DecelerateInterpolator(); + mState = STATE_IDLE; + } + + private void startAnimation(float start, float finish, long duration, + int newState) { + mValueStart = start; + mValueFinish = finish; + mDuration = duration; + mStartTime = now(); + mState = newState; + } + + // The deltaDistance's magnitude is in the range of -1 (no change) to 1. + // The value 1 is the full length of the view. Negative values means the + // movement is in the opposite direction. + public void onPull(float deltaDistance) { + if (mState == STATE_ABSORB) return; + mValue = Utils.clamp(mValue + deltaDistance, -1.0f, 1.0f); + mState = STATE_PULL; + } + + public void onRelease() { + if (mState == STATE_IDLE || mState == STATE_ABSORB) return; + startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE); + } + + public void onAbsorb(float velocity) { + float finish = Utils.clamp(mValue + velocity * VELOCITY_FACTOR, + -1.0f, 1.0f); + startAnimation(mValue, finish, ABSORB_TIME, STATE_ABSORB); + } + + public boolean update() { + if (mState == STATE_IDLE) return false; + if (mState == STATE_PULL) return true; + + float t = Utils.clamp((float)(now() - mStartTime) / mDuration, 0.0f, 1.0f); + /* Use linear interpolation for absorb, quadratic for others */ + float interp = (mState == STATE_ABSORB) + ? t : mInterpolator.getInterpolation(t); + + mValue = mValueStart + (mValueFinish - mValueStart) * interp; + + if (t >= 1.0f) { + switch (mState) { + case STATE_ABSORB: + startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE); + break; + case STATE_RELEASE: + mState = STATE_IDLE; + break; + } + } + + return true; + } + + public float getValue() { + return mValue; + } + + private long now() { + return AnimationTime.get(); + } +} diff --git a/src/com/android/gallery3d/ui/PhotoFallbackEffect.java b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java new file mode 100644 index 000000000..4603285a4 --- /dev/null +++ b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.AlbumSlotRenderer.SlotFilter; + +import java.util.ArrayList; + +public class PhotoFallbackEffect extends Animation implements SlotFilter { + + private static final int ANIM_DURATION = 300; + private static final Interpolator ANIM_INTERPOLATE = new DecelerateInterpolator(1.5f); + + public static class Entry { + public int index; + public Path path; + public Rect source; + public Rect dest; + public RawTexture texture; + + public Entry(Path path, Rect source, RawTexture texture) { + this.path = path; + this.source = source; + this.texture = texture; + } + } + + public interface PositionProvider { + public Rect getPosition(int index); + public int getItemIndex(Path path); + } + + private RectF mSource = new RectF(); + private RectF mTarget = new RectF(); + private float mProgress; + private PositionProvider mPositionProvider; + + private ArrayList<Entry> mList = new ArrayList<Entry>(); + + public PhotoFallbackEffect() { + setDuration(ANIM_DURATION); + setInterpolator(ANIM_INTERPOLATE); + } + + public void addEntry(Path path, Rect rect, RawTexture texture) { + mList.add(new Entry(path, rect, texture)); + } + + public Entry getEntry(Path path) { + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + if (entry.path == path) return entry; + } + return null; + } + + public boolean draw(GLCanvas canvas) { + boolean more = calculate(AnimationTime.get()); + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + if (entry.index < 0) continue; + entry.dest = mPositionProvider.getPosition(entry.index); + drawEntry(canvas, entry); + } + return more; + } + + private void drawEntry(GLCanvas canvas, Entry entry) { + if (!entry.texture.isLoaded()) return; + + int w = entry.texture.getWidth(); + int h = entry.texture.getHeight(); + + Rect s = entry.source; + Rect d = entry.dest; + + // the following calculation is based on d.width() == d.height() + + float p = mProgress; + + float fullScale = (float) d.height() / Math.min(s.width(), s.height()); + float scale = fullScale * p + 1 * (1 - p); + + float cx = d.centerX() * p + s.centerX() * (1 - p); + float cy = d.centerY() * p + s.centerY() * (1 - p); + + float ch = s.height() * scale; + float cw = s.width() * scale; + + if (w > h) { + // draw the center part + mTarget.set(cx - ch / 2, cy - ch / 2, cx + ch / 2, cy + ch / 2); + mSource.set((w - h) / 2, 0, (w + h) / 2, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(1 - p); + + // draw the left part + mTarget.set(cx - cw / 2, cy - ch / 2, cx - ch / 2, cy + ch / 2); + mSource.set(0, 0, (w - h) / 2, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + // draw the right part + mTarget.set(cx + ch / 2, cy - ch / 2, cx + cw / 2, cy + ch / 2); + mSource.set((w + h) / 2, 0, w, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.restore(); + } else { + // draw the center part + mTarget.set(cx - cw / 2, cy - cw / 2, cx + cw / 2, cy + cw / 2); + mSource.set(0, (h - w) / 2, w, (h + w) / 2); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(1 - p); + + // draw the upper part + mTarget.set(cx - cw / 2, cy - ch / 2, cx + cw / 2, cy - cw / 2); + mSource.set(0, 0, w, (h - w) / 2); + canvas.drawTexture(entry.texture, mSource, mTarget); + + // draw the bottom part + mTarget.set(cx - cw / 2, cy + cw / 2, cx + cw / 2, cy + ch / 2); + mSource.set(0, (w + h) / 2, w, h); + canvas.drawTexture(entry.texture, mSource, mTarget); + + canvas.restore(); + } + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + + public void setPositionProvider(PositionProvider provider) { + mPositionProvider = provider; + if (mPositionProvider != null) { + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + entry.index = mPositionProvider.getItemIndex(entry.path); + } + } + } + + @Override + public boolean acceptSlot(int index) { + for (int i = 0, n = mList.size(); i < n; ++i) { + Entry entry = mList.get(i); + if (entry.index == index) return false; + } + return true; + } +} diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java new file mode 100644 index 000000000..7afa20348 --- /dev/null +++ b/src/com/android/gallery3d/ui/PhotoView.java @@ -0,0 +1,1858 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.os.Build; +import android.os.Message; +import android.util.FloatMath; +import android.view.MotionEvent; +import android.view.View.MeasureSpec; +import android.view.animation.AccelerateInterpolator; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaObject; +import com.android.gallery3d.data.Path; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.StringTexture; +import com.android.gallery3d.glrenderer.Texture; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.RangeArray; +import com.android.gallery3d.util.UsageStatistics; + +public class PhotoView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "PhotoView"; + private final int mPlaceholderColor; + + public static final int INVALID_SIZE = -1; + public static final long INVALID_DATA_VERSION = + MediaObject.INVALID_DATA_VERSION; + + public static class Size { + public int width; + public int height; + } + + public interface Model extends TileImageView.TileSource { + public int getCurrentIndex(); + public void moveTo(int index); + + // Returns the size for the specified picture. If the size information is + // not avaiable, width = height = 0. + public void getImageSize(int offset, Size size); + + // Returns the media item for the specified picture. + public MediaItem getMediaItem(int offset); + + // Returns the rotation for the specified picture. + public int getImageRotation(int offset); + + // This amends the getScreenNail() method of TileImageView.Model to get + // ScreenNail at previous (negative offset) or next (positive offset) + // positions. Returns null if the specified ScreenNail is unavailable. + public ScreenNail getScreenNail(int offset); + + // Set this to true if we need the model to provide full images. + public void setNeedFullImage(boolean enabled); + + // Returns true if the item is the Camera preview. + public boolean isCamera(int offset); + + // Returns true if the item is the Panorama. + public boolean isPanorama(int offset); + + // Returns true if the item is a static image that represents camera + // preview. + public boolean isStaticCamera(int offset); + + // Returns true if the item is a Video. + public boolean isVideo(int offset); + + // Returns true if the item can be deleted. + public boolean isDeletable(int offset); + + public static final int LOADING_INIT = 0; + public static final int LOADING_COMPLETE = 1; + public static final int LOADING_FAIL = 2; + + public int getLoadingState(int offset); + + // When data change happens, we need to decide which MediaItem to focus + // on. + // + // 1. If focus hint path != null, we try to focus on it if we can find + // it. This is used for undo a deletion, so we can focus on the + // undeleted item. + // + // 2. Otherwise try to focus on the MediaItem that is currently focused, + // if we can find it. + // + // 3. Otherwise try to focus on the previous MediaItem or the next + // MediaItem, depending on the value of focus hint direction. + public static final int FOCUS_HINT_NEXT = 0; + public static final int FOCUS_HINT_PREVIOUS = 1; + public void setFocusHintDirection(int direction); + public void setFocusHintPath(Path path); + } + + public interface Listener { + public void onSingleTapUp(int x, int y); + public void onFullScreenChanged(boolean full); + public void onActionBarAllowed(boolean allowed); + public void onActionBarWanted(); + public void onCurrentImageUpdated(); + public void onDeleteImage(Path path, int offset); + public void onUndoDeleteImage(); + public void onCommitDeleteImage(); + public void onFilmModeChanged(boolean enabled); + public void onPictureCenter(boolean isCamera); + public void onUndoBarVisibilityChanged(boolean visible); + } + + // The rules about orientation locking: + // + // (1) We need to lock the orientation if we are in page mode camera + // preview, so there is no (unwanted) rotation animation when the user + // rotates the device. + // + // (2) We need to unlock the orientation if we want to show the action bar + // because the action bar follows the system orientation. + // + // The rules about action bar: + // + // (1) If we are in film mode, we don't show action bar. + // + // (2) If we go from camera to gallery with capture animation, we show + // action bar. + private static final int MSG_CANCEL_EXTRA_SCALING = 2; + private static final int MSG_SWITCH_FOCUS = 3; + private static final int MSG_CAPTURE_ANIMATION_DONE = 4; + private static final int MSG_DELETE_ANIMATION_DONE = 5; + private static final int MSG_DELETE_DONE = 6; + private static final int MSG_UNDO_BAR_TIMEOUT = 7; + private static final int MSG_UNDO_BAR_FULL_CAMERA = 8; + + private static final float SWIPE_THRESHOLD = 300f; + + private static final float DEFAULT_TEXT_SIZE = 20; + private static float TRANSITION_SCALE_FACTOR = 0.74f; + private static final int ICON_RATIO = 6; + + // whether we want to apply card deck effect in page mode. + private static final boolean CARD_EFFECT = true; + + // whether we want to apply offset effect in film mode. + private static final boolean OFFSET_EFFECT = true; + + // Used to calculate the scaling factor for the card deck effect. + private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f); + + // Used to calculate the alpha factor for the fading animation. + private AccelerateInterpolator mAlphaInterpolator = + new AccelerateInterpolator(0.9f); + + // We keep this many previous ScreenNails. (also this many next ScreenNails) + public static final int SCREEN_NAIL_MAX = 3; + + // These are constants for the delete gesture. + private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec + private static final int MAX_DISMISS_VELOCITY = 2500; // dp/sec + private static final int SWIPE_ESCAPE_DISTANCE = 150; // dp + + // The picture entries, the valid index is from -SCREEN_NAIL_MAX to + // SCREEN_NAIL_MAX. + private final RangeArray<Picture> mPictures = + new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX); + private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1]; + + private final MyGestureListener mGestureListener; + private final GestureRecognizer mGestureRecognizer; + private final PositionController mPositionController; + + private Listener mListener; + private Model mModel; + private StringTexture mNoThumbnailText; + private TileImageView mTileView; + private EdgeView mEdgeView; + private UndoBarView mUndoBar; + private Texture mVideoPlayIcon; + + private SynchronizedHandler mHandler; + + private boolean mCancelExtraScalingPending; + private boolean mFilmMode = false; + private boolean mWantPictureCenterCallbacks = false; + private int mDisplayRotation = 0; + private int mCompensation = 0; + private boolean mFullScreenCamera; + private Rect mCameraRelativeFrame = new Rect(); + private Rect mCameraRect = new Rect(); + private boolean mFirst = true; + + // [mPrevBound, mNextBound] is the range of index for all pictures in the + // model, if we assume the index of current focused picture is 0. So if + // there are some previous pictures, mPrevBound < 0, and if there are some + // next pictures, mNextBound > 0. + private int mPrevBound; + private int mNextBound; + + // This variable prevents us doing snapback until its values goes to 0. This + // happens if the user gesture is still in progress or we are in a capture + // animation. + private int mHolding; + private static final int HOLD_TOUCH_DOWN = 1; + private static final int HOLD_CAPTURE_ANIMATION = 2; + private static final int HOLD_DELETE = 4; + + // mTouchBoxIndex is the index of the box that is touched by the down + // gesture in film mode. The value Integer.MAX_VALUE means no box was + // touched. + private int mTouchBoxIndex = Integer.MAX_VALUE; + // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful + // if mTouchBoxIndex is not Integer.MAX_VALUE. + private boolean mTouchBoxDeletable; + // This is the index of the last deleted item. This is only used as a hint + // to hide the undo button when we are too far away from the deleted + // item. The value Integer.MAX_VALUE means there is no such hint. + private int mUndoIndexHint = Integer.MAX_VALUE; + + private Context mContext; + + public PhotoView(AbstractGalleryActivity activity) { + mTileView = new TileImageView(activity); + addComponent(mTileView); + mContext = activity.getAndroidContext(); + mPlaceholderColor = mContext.getResources().getColor( + R.color.photo_placeholder); + mEdgeView = new EdgeView(mContext); + addComponent(mEdgeView); + mUndoBar = new UndoBarView(mContext); + addComponent(mUndoBar); + mUndoBar.setVisibility(GLView.INVISIBLE); + mUndoBar.setOnClickListener(new OnClickListener() { + @Override + public void onClick(GLView v) { + mListener.onUndoDeleteImage(); + hideUndoBar(); + } + }); + mNoThumbnailText = StringTexture.newInstance( + mContext.getString(R.string.no_thumbnail), + DEFAULT_TEXT_SIZE, Color.WHITE); + + mHandler = new MyHandler(activity.getGLRoot()); + + mGestureListener = new MyGestureListener(); + mGestureRecognizer = new GestureRecognizer(mContext, mGestureListener); + + mPositionController = new PositionController(mContext, + new PositionController.Listener() { + + @Override + public void invalidate() { + PhotoView.this.invalidate(); + } + + @Override + public boolean isHoldingDown() { + return (mHolding & HOLD_TOUCH_DOWN) != 0; + } + + @Override + public boolean isHoldingDelete() { + return (mHolding & HOLD_DELETE) != 0; + } + + @Override + public void onPull(int offset, int direction) { + mEdgeView.onPull(offset, direction); + } + + @Override + public void onRelease() { + mEdgeView.onRelease(); + } + + @Override + public void onAbsorb(int velocity, int direction) { + mEdgeView.onAbsorb(velocity, direction); + } + }); + mVideoPlayIcon = new ResourceTexture(mContext, R.drawable.ic_control_play); + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + if (i == 0) { + mPictures.put(i, new FullPicture()); + } else { + mPictures.put(i, new ScreenNailPicture(i)); + } + } + } + + public void stopScrolling() { + mPositionController.stopScrolling(); + } + + public void setModel(Model model) { + mModel = model; + mTileView.setModel(mModel); + } + + class MyHandler extends SynchronizedHandler { + public MyHandler(GLRoot root) { + super(root); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_CANCEL_EXTRA_SCALING: { + mGestureRecognizer.cancelScale(); + mPositionController.setExtraScalingRange(false); + mCancelExtraScalingPending = false; + break; + } + case MSG_SWITCH_FOCUS: { + switchFocus(); + break; + } + case MSG_CAPTURE_ANIMATION_DONE: { + // message.arg1 is the offset parameter passed to + // switchWithCaptureAnimation(). + captureAnimationDone(message.arg1); + break; + } + case MSG_DELETE_ANIMATION_DONE: { + // message.obj is the Path of the MediaItem which should be + // deleted. message.arg1 is the offset of the image. + mListener.onDeleteImage((Path) message.obj, message.arg1); + // Normally a box which finishes delete animation will hold + // position until the underlying MediaItem is actually + // deleted, and HOLD_DELETE will be cancelled that time. In + // case the MediaItem didn't actually get deleted in 2 + // seconds, we will cancel HOLD_DELETE and make it bounce + // back. + + // We make sure there is at most one MSG_DELETE_DONE + // in the handler. + mHandler.removeMessages(MSG_DELETE_DONE); + Message m = mHandler.obtainMessage(MSG_DELETE_DONE); + mHandler.sendMessageDelayed(m, 2000); + + int numberOfPictures = mNextBound - mPrevBound + 1; + if (numberOfPictures == 2) { + if (mModel.isCamera(mNextBound) + || mModel.isCamera(mPrevBound)) { + numberOfPictures--; + } + } + showUndoBar(numberOfPictures <= 1); + break; + } + case MSG_DELETE_DONE: { + if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) { + mHolding &= ~HOLD_DELETE; + snapback(); + } + break; + } + case MSG_UNDO_BAR_TIMEOUT: { + checkHideUndoBar(UNDO_BAR_TIMEOUT); + break; + } + case MSG_UNDO_BAR_FULL_CAMERA: { + checkHideUndoBar(UNDO_BAR_FULL_CAMERA); + break; + } + default: throw new AssertionError(message.what); + } + } + } + + public void setWantPictureCenterCallbacks(boolean wanted) { + mWantPictureCenterCallbacks = wanted; + } + + //////////////////////////////////////////////////////////////////////////// + // Data/Image change notifications + //////////////////////////////////////////////////////////////////////////// + + public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) { + mPrevBound = prevBound; + mNextBound = nextBound; + + // Update mTouchBoxIndex + if (mTouchBoxIndex != Integer.MAX_VALUE) { + int k = mTouchBoxIndex; + mTouchBoxIndex = Integer.MAX_VALUE; + for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) { + if (fromIndex[i] == k) { + mTouchBoxIndex = i - SCREEN_NAIL_MAX; + break; + } + } + } + + // Hide undo button if we are too far away + if (mUndoIndexHint != Integer.MAX_VALUE) { + if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) { + hideUndoBar(); + } + } + + // Update the ScreenNails. + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + Picture p = mPictures.get(i); + p.reload(); + mSizes[i + SCREEN_NAIL_MAX] = p.getSize(); + } + + boolean wasDeleting = mPositionController.hasDeletingBox(); + + // Move the boxes + mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0, + mModel.isCamera(0), mSizes); + + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + setPictureSize(i); + } + + boolean isDeleting = mPositionController.hasDeletingBox(); + + // If the deletion is done, make HOLD_DELETE persist for only the time + // needed for a snapback animation. + if (wasDeleting && !isDeleting) { + mHandler.removeMessages(MSG_DELETE_DONE); + Message m = mHandler.obtainMessage(MSG_DELETE_DONE); + mHandler.sendMessageDelayed( + m, PositionController.SNAPBACK_ANIMATION_TIME); + } + + invalidate(); + } + + public boolean isDeleting() { + return (mHolding & HOLD_DELETE) != 0 + && mPositionController.hasDeletingBox(); + } + + public void notifyImageChange(int index) { + if (index == 0) { + mListener.onCurrentImageUpdated(); + } + mPictures.get(index).reload(); + setPictureSize(index); + invalidate(); + } + + private void setPictureSize(int index) { + Picture p = mPictures.get(index); + mPositionController.setImageSize(index, p.getSize(), + index == 0 && p.isCamera() ? mCameraRect : null); + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + int w = right - left; + int h = bottom - top; + mTileView.layout(0, 0, w, h); + mEdgeView.layout(0, 0, w, h); + mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h); + + GLRoot root = getGLRoot(); + int displayRotation = root.getDisplayRotation(); + int compensation = root.getCompensation(); + if (mDisplayRotation != displayRotation + || mCompensation != compensation) { + mDisplayRotation = displayRotation; + mCompensation = compensation; + + // We need to change the size and rotation of the Camera ScreenNail, + // but we don't want it to animate because the size doen't actually + // change in the eye of the user. + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + Picture p = mPictures.get(i); + if (p.isCamera()) { + p.forceSize(); + } + } + } + + updateCameraRect(); + mPositionController.setConstrainedFrame(mCameraRect); + if (changeSize) { + mPositionController.setViewSize(getWidth(), getHeight()); + } + } + + // Update the camera rectangle due to layout change or camera relative frame + // change. + private void updateCameraRect() { + // Get the width and height in framework orientation because the given + // mCameraRelativeFrame is in that coordinates. + int w = getWidth(); + int h = getHeight(); + if (mCompensation % 180 != 0) { + int tmp = w; + w = h; + h = tmp; + } + int l = mCameraRelativeFrame.left; + int t = mCameraRelativeFrame.top; + int r = mCameraRelativeFrame.right; + int b = mCameraRelativeFrame.bottom; + + // Now convert it to the coordinates we are using. + switch (mCompensation) { + case 0: mCameraRect.set(l, t, r, b); break; + case 90: mCameraRect.set(h - b, l, h - t, r); break; + case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break; + case 270: mCameraRect.set(t, w - r, b, w - l); break; + } + + Log.d(TAG, "compensation = " + mCompensation + + ", CameraRelativeFrame = " + mCameraRelativeFrame + + ", mCameraRect = " + mCameraRect); + } + + public void setCameraRelativeFrame(Rect frame) { + mCameraRelativeFrame.set(frame); + updateCameraRect(); + // Originally we do + // mPositionController.setConstrainedFrame(mCameraRect); + // here, but it is moved to a parameter of the setImageSize() call, so + // it can be updated atomically with the CameraScreenNail's size change. + } + + // Returns the rotation we need to do to the camera texture before drawing + // it to the canvas, assuming the camera texture is correct when the device + // is in its natural orientation. + private int getCameraRotation() { + return (mCompensation - mDisplayRotation + 360) % 360; + } + + private int getPanoramaRotation() { + // This function is magic + // The issue here is that Pano makes bad assumptions about rotation and + // orientation. The first is it assumes only two rotations are possible, + // 0 and 90. Thus, if display rotation is >= 180, we invert the output. + // The second is that it assumes landscape is a 90 rotation from portrait, + // however on landscape devices this is not true. Thus, if we are in portrait + // on a landscape device, we need to invert the output + int orientation = mContext.getResources().getConfiguration().orientation; + boolean invertPortrait = (orientation == Configuration.ORIENTATION_PORTRAIT + && (mDisplayRotation == 90 || mDisplayRotation == 270)); + boolean invert = (mDisplayRotation >= 180); + if (invert != invertPortrait) { + return (mCompensation + 180) % 360; + } + return mCompensation; + } + + //////////////////////////////////////////////////////////////////////////// + // Pictures + //////////////////////////////////////////////////////////////////////////// + + private interface Picture { + void reload(); + void draw(GLCanvas canvas, Rect r); + void setScreenNail(ScreenNail s); + boolean isCamera(); // whether the picture is a camera preview + boolean isDeletable(); // whether the picture can be deleted + void forceSize(); // called when mCompensation changes + Size getSize(); + } + + class FullPicture implements Picture { + private int mRotation; + private boolean mIsCamera; + private boolean mIsPanorama; + private boolean mIsStaticCamera; + private boolean mIsVideo; + private boolean mIsDeletable; + private int mLoadingState = Model.LOADING_INIT; + private Size mSize = new Size(); + + @Override + public void reload() { + // mImageWidth and mImageHeight will get updated + mTileView.notifyModelInvalidated(); + + mIsCamera = mModel.isCamera(0); + mIsPanorama = mModel.isPanorama(0); + mIsStaticCamera = mModel.isStaticCamera(0); + mIsVideo = mModel.isVideo(0); + mIsDeletable = mModel.isDeletable(0); + mLoadingState = mModel.getLoadingState(0); + setScreenNail(mModel.getScreenNail(0)); + updateSize(); + } + + @Override + public Size getSize() { + return mSize; + } + + @Override + public void forceSize() { + updateSize(); + mPositionController.forceImageSize(0, mSize); + } + + private void updateSize() { + if (mIsPanorama) { + mRotation = getPanoramaRotation(); + } else if (mIsCamera && !mIsStaticCamera) { + mRotation = getCameraRotation(); + } else { + mRotation = mModel.getImageRotation(0); + } + + int w = mTileView.mImageWidth; + int h = mTileView.mImageHeight; + mSize.width = getRotated(mRotation, w, h); + mSize.height = getRotated(mRotation, h, w); + } + + @Override + public void draw(GLCanvas canvas, Rect r) { + drawTileView(canvas, r); + + // We want to have the following transitions: + // (1) Move camera preview out of its place: switch to film mode + // (2) Move camera preview into its place: switch to page mode + // The extra mWasCenter check makes sure (1) does not apply if in + // page mode, we move _to_ the camera preview from another picture. + + // Holdings except touch-down prevent the transitions. + if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return; + + if (mWantPictureCenterCallbacks && mPositionController.isCenter()) { + mListener.onPictureCenter(mIsCamera); + } + } + + @Override + public void setScreenNail(ScreenNail s) { + mTileView.setScreenNail(s); + } + + @Override + public boolean isCamera() { + return mIsCamera; + } + + @Override + public boolean isDeletable() { + return mIsDeletable; + } + + private void drawTileView(GLCanvas canvas, Rect r) { + float imageScale = mPositionController.getImageScale(); + int viewW = getWidth(); + int viewH = getHeight(); + float cx = r.exactCenterX(); + float cy = r.exactCenterY(); + float scale = 1f; // the scaling factor due to card effect + + canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); + float filmRatio = mPositionController.getFilmRatio(); + boolean wantsCardEffect = CARD_EFFECT && !mIsCamera + && filmRatio != 1f && !mPictures.get(-1).isCamera() + && !mPositionController.inOpeningAnimation(); + boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable + && filmRatio == 1f && r.centerY() != viewH / 2; + if (wantsCardEffect) { + // Calculate the move-out progress value. + int left = r.left; + int right = r.right; + float progress = calculateMoveOutProgress(left, right, viewW); + progress = Utils.clamp(progress, -1f, 1f); + + // We only want to apply the fading animation if the scrolling + // movement is to the right. + if (progress < 0) { + scale = getScrollScale(progress); + float alpha = getScrollAlpha(progress); + scale = interpolate(filmRatio, scale, 1f); + alpha = interpolate(filmRatio, alpha, 1f); + + imageScale *= scale; + canvas.multiplyAlpha(alpha); + + float cxPage; // the cx value in page mode + if (right - left <= viewW) { + // If the picture is narrower than the view, keep it at + // the center of the view. + cxPage = viewW / 2f; + } else { + // If the picture is wider than the view (it's + // zoomed-in), keep the left edge of the object align + // the the left edge of the view. + cxPage = (right - left) * scale / 2f; + } + cx = interpolate(filmRatio, cxPage, cx); + } + } else if (wantsOffsetEffect) { + float offset = (float) (r.centerY() - viewH / 2) / viewH; + float alpha = getOffsetAlpha(offset); + canvas.multiplyAlpha(alpha); + } + + // Draw the tile view. + setTileViewPosition(cx, cy, viewW, viewH, imageScale); + renderChild(canvas, mTileView); + + // Draw the play video icon and the message. + canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f)); + int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f); + if (mIsVideo) drawVideoPlayIcon(canvas, s); + if (mLoadingState == Model.LOADING_FAIL) { + drawLoadingFailMessage(canvas); + } + + // Draw a debug indicator showing which picture has focus (index == + // 0). + //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF); + + canvas.restore(); + } + + // Set the position of the tile view + private void setTileViewPosition(float cx, float cy, + int viewW, int viewH, float scale) { + // Find out the bitmap coordinates of the center of the view + int imageW = mPositionController.getImageWidth(); + int imageH = mPositionController.getImageHeight(); + int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f); + int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f); + + int inverseX = imageW - centerX; + int inverseY = imageH - centerY; + int x, y; + switch (mRotation) { + case 0: x = centerX; y = centerY; break; + case 90: x = centerY; y = inverseX; break; + case 180: x = inverseX; y = inverseY; break; + case 270: x = inverseY; y = centerX; break; + default: + throw new RuntimeException(String.valueOf(mRotation)); + } + mTileView.setPosition(x, y, scale, mRotation); + } + } + + private class ScreenNailPicture implements Picture { + private int mIndex; + private int mRotation; + private ScreenNail mScreenNail; + private boolean mIsCamera; + private boolean mIsPanorama; + private boolean mIsStaticCamera; + private boolean mIsVideo; + private boolean mIsDeletable; + private int mLoadingState = Model.LOADING_INIT; + private Size mSize = new Size(); + + public ScreenNailPicture(int index) { + mIndex = index; + } + + @Override + public void reload() { + mIsCamera = mModel.isCamera(mIndex); + mIsPanorama = mModel.isPanorama(mIndex); + mIsStaticCamera = mModel.isStaticCamera(mIndex); + mIsVideo = mModel.isVideo(mIndex); + mIsDeletable = mModel.isDeletable(mIndex); + mLoadingState = mModel.getLoadingState(mIndex); + setScreenNail(mModel.getScreenNail(mIndex)); + updateSize(); + } + + @Override + public Size getSize() { + return mSize; + } + + @Override + public void draw(GLCanvas canvas, Rect r) { + if (mScreenNail == null) { + // Draw a placeholder rectange if there should be a picture in + // this position (but somehow there isn't). + if (mIndex >= mPrevBound && mIndex <= mNextBound) { + drawPlaceHolder(canvas, r); + } + return; + } + int w = getWidth(); + int h = getHeight(); + if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) { + mScreenNail.noDraw(); + return; + } + + float filmRatio = mPositionController.getFilmRatio(); + boolean wantsCardEffect = CARD_EFFECT && mIndex > 0 + && filmRatio != 1f && !mPictures.get(0).isCamera(); + boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable + && filmRatio == 1f && r.centerY() != h / 2; + int cx = wantsCardEffect + ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f) + : r.centerX(); + int cy = r.centerY(); + canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA); + canvas.translate(cx, cy); + if (wantsCardEffect) { + float progress = (float) (w / 2 - r.centerX()) / w; + progress = Utils.clamp(progress, -1, 1); + float alpha = getScrollAlpha(progress); + float scale = getScrollScale(progress); + alpha = interpolate(filmRatio, alpha, 1f); + scale = interpolate(filmRatio, scale, 1f); + canvas.multiplyAlpha(alpha); + canvas.scale(scale, scale, 1); + } else if (wantsOffsetEffect) { + float offset = (float) (r.centerY() - h / 2) / h; + float alpha = getOffsetAlpha(offset); + canvas.multiplyAlpha(alpha); + } + if (mRotation != 0) { + canvas.rotate(mRotation, 0, 0, 1); + } + int drawW = getRotated(mRotation, r.width(), r.height()); + int drawH = getRotated(mRotation, r.height(), r.width()); + mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH); + if (isScreenNailAnimating()) { + invalidate(); + } + int s = Math.min(drawW, drawH); + if (mIsVideo) drawVideoPlayIcon(canvas, s); + if (mLoadingState == Model.LOADING_FAIL) { + drawLoadingFailMessage(canvas); + } + canvas.restore(); + } + + private boolean isScreenNailAnimating() { + return (mScreenNail instanceof TiledScreenNail) + && ((TiledScreenNail) mScreenNail).isAnimating(); + } + + @Override + public void setScreenNail(ScreenNail s) { + mScreenNail = s; + } + + @Override + public void forceSize() { + updateSize(); + mPositionController.forceImageSize(mIndex, mSize); + } + + private void updateSize() { + if (mIsPanorama) { + mRotation = getPanoramaRotation(); + } else if (mIsCamera && !mIsStaticCamera) { + mRotation = getCameraRotation(); + } else { + mRotation = mModel.getImageRotation(mIndex); + } + + if (mScreenNail != null) { + mSize.width = mScreenNail.getWidth(); + mSize.height = mScreenNail.getHeight(); + } else { + // If we don't have ScreenNail available, we can still try to + // get the size information of it. + mModel.getImageSize(mIndex, mSize); + } + + int w = mSize.width; + int h = mSize.height; + mSize.width = getRotated(mRotation, w, h); + mSize.height = getRotated(mRotation, h, w); + } + + @Override + public boolean isCamera() { + return mIsCamera; + } + + @Override + public boolean isDeletable() { + return mIsDeletable; + } + } + + // Draw a gray placeholder in the specified rectangle. + private void drawPlaceHolder(GLCanvas canvas, Rect r) { + canvas.fillRect(r.left, r.top, r.width(), r.height(), mPlaceholderColor); + } + + // Draw the video play icon (in the place where the spinner was) + private void drawVideoPlayIcon(GLCanvas canvas, int side) { + int s = side / ICON_RATIO; + // Draw the video play icon at the center + mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s); + } + + // Draw the "no thumbnail" message + private void drawLoadingFailMessage(GLCanvas canvas) { + StringTexture m = mNoThumbnailText; + m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2); + } + + private static int getRotated(int degree, int original, int theother) { + return (degree % 180 == 0) ? original : theother; + } + + //////////////////////////////////////////////////////////////////////////// + // Gestures Handling + //////////////////////////////////////////////////////////////////////////// + + @Override + protected boolean onTouch(MotionEvent event) { + mGestureRecognizer.onTouchEvent(event); + return true; + } + + private class MyGestureListener implements GestureRecognizer.Listener { + private boolean mIgnoreUpEvent = false; + // If we can change mode for this scale gesture. + private boolean mCanChangeMode; + // If we have changed the film mode in this scaling gesture. + private boolean mModeChanged; + // If this scaling gesture should be ignored. + private boolean mIgnoreScalingGesture; + // whether the down action happened while the view is scrolling. + private boolean mDownInScrolling; + // If we should ignore all gestures other than onSingleTapUp. + private boolean mIgnoreSwipingGesture; + // If a scrolling has happened after a down gesture. + private boolean mScrolledAfterDown; + // If the first scrolling move is in X direction. In the film mode, X + // direction scrolling is normal scrolling. but Y direction scrolling is + // a delete gesture. + private boolean mFirstScrollX; + // The accumulated Y delta that has been sent to mPositionController. + private int mDeltaY; + // The accumulated scaling change from a scaling gesture. + private float mAccScale; + // If an onFling happened after the last onDown + private boolean mHadFling; + + @Override + public boolean onSingleTapUp(float x, float y) { + // On crespo running Android 2.3.6 (gingerbread), a pinch out gesture results in the + // following call sequence: onDown(), onUp() and then onSingleTapUp(). The correct + // sequence for a single-tap-up gesture should be: onDown(), onSingleTapUp() and onUp(). + // The call sequence for a pinch out gesture in JB is: onDown(), then onUp() and there's + // no onSingleTapUp(). Base on these observations, the following condition is added to + // filter out the false alarm where onSingleTapUp() is called within a pinch out + // gesture. The framework fix went into ICS. Refer to b/4588114. + if (Build.VERSION.SDK_INT < ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) { + if ((mHolding & HOLD_TOUCH_DOWN) == 0) { + return true; + } + } + + // We do this in addition to onUp() because we want the snapback of + // setFilmMode to happen. + mHolding &= ~HOLD_TOUCH_DOWN; + + if (mFilmMode && !mDownInScrolling) { + switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f)); + + // If this is a lock screen photo, let the listener handle the + // event. Tapping on lock screen photo should take the user + // directly to the lock screen. + MediaItem item = mModel.getMediaItem(0); + int supported = 0; + if (item != null) supported = item.getSupportedOperations(); + if ((supported & MediaItem.SUPPORT_ACTION) == 0) { + setFilmMode(false); + mIgnoreUpEvent = true; + return true; + } + } + + if (mListener != null) { + // Do the inverse transform of the touch coordinates. + Matrix m = getGLRoot().getCompensationMatrix(); + Matrix inv = new Matrix(); + m.invert(inv); + float[] pts = new float[] {x, y}; + inv.mapPoints(pts); + mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f)); + } + return true; + } + + @Override + public boolean onDoubleTap(float x, float y) { + if (mIgnoreSwipingGesture) return true; + if (mPictures.get(0).isCamera()) return false; + PositionController controller = mPositionController; + float scale = controller.getImageScale(); + // onDoubleTap happened on the second ACTION_DOWN. + // We need to ignore the next UP event. + mIgnoreUpEvent = true; + if (scale <= .75f || controller.isAtMinimalScale()) { + controller.zoomIn(x, y, Math.max(1.0f, scale * 1.5f)); + } else { + controller.resetToFullView(); + } + return true; + } + + @Override + public boolean onScroll(float dx, float dy, float totalX, float totalY) { + if (mIgnoreSwipingGesture) return true; + if (!mScrolledAfterDown) { + mScrolledAfterDown = true; + mFirstScrollX = (Math.abs(dx) > Math.abs(dy)); + } + + int dxi = (int) (-dx + 0.5f); + int dyi = (int) (-dy + 0.5f); + if (mFilmMode) { + if (mFirstScrollX) { + mPositionController.scrollFilmX(dxi); + } else { + if (mTouchBoxIndex == Integer.MAX_VALUE) return true; + int newDeltaY = calculateDeltaY(totalY); + int d = newDeltaY - mDeltaY; + if (d != 0) { + mPositionController.scrollFilmY(mTouchBoxIndex, d); + mDeltaY = newDeltaY; + } + } + } else { + mPositionController.scrollPage(dxi, dyi); + } + return true; + } + + private int calculateDeltaY(float delta) { + if (mTouchBoxDeletable) return (int) (delta + 0.5f); + + // don't let items that can't be deleted be dragged more than + // maxScrollDistance, and make it harder and harder to drag. + int size = getHeight(); + float maxScrollDistance = 0.15f * size; + if (Math.abs(delta) >= size) { + delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; + } else { + delta = maxScrollDistance * + FloatMath.sin((delta / size) * (float) (Math.PI / 2)); + } + return (int) (delta + 0.5f); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (mIgnoreSwipingGesture) return true; + if (mModeChanged) return true; + if (swipeImages(velocityX, velocityY)) { + mIgnoreUpEvent = true; + } else { + flingImages(velocityX, velocityY, Math.abs(e2.getY() - e1.getY())); + } + mHadFling = true; + return true; + } + + private boolean flingImages(float velocityX, float velocityY, float dY) { + int vx = (int) (velocityX + 0.5f); + int vy = (int) (velocityY + 0.5f); + if (!mFilmMode) { + return mPositionController.flingPage(vx, vy); + } + if (Math.abs(velocityX) > Math.abs(velocityY)) { + return mPositionController.flingFilmX(vx); + } + // If we scrolled in Y direction fast enough, treat it as a delete + // gesture. + if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE + || !mTouchBoxDeletable) { + return false; + } + int maxVelocity = GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY); + int escapeVelocity = GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY); + int escapeDistance = GalleryUtils.dpToPixel(SWIPE_ESCAPE_DISTANCE); + int centerY = mPositionController.getPosition(mTouchBoxIndex) + .centerY(); + boolean fastEnough = (Math.abs(vy) > escapeVelocity) + && (Math.abs(vy) > Math.abs(vx)) + && ((vy > 0) == (centerY > getHeight() / 2)) + && dY >= escapeDistance; + if (fastEnough) { + vy = Math.min(vy, maxVelocity); + int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy); + if (duration >= 0) { + mPositionController.setPopFromTop(vy < 0); + deleteAfterAnimation(duration); + // We reset mTouchBoxIndex, so up() won't check if Y + // scrolled far enough to be a delete gesture. + mTouchBoxIndex = Integer.MAX_VALUE; + return true; + } + } + return false; + } + + private void deleteAfterAnimation(int duration) { + MediaItem item = mModel.getMediaItem(mTouchBoxIndex); + if (item == null) return; + mListener.onCommitDeleteImage(); + mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex; + mHolding |= HOLD_DELETE; + Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE); + m.obj = item.getPath(); + m.arg1 = mTouchBoxIndex; + mHandler.sendMessageDelayed(m, duration); + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (mIgnoreSwipingGesture) return true; + // We ignore the scaling gesture if it is a camera preview. + mIgnoreScalingGesture = mPictures.get(0).isCamera(); + if (mIgnoreScalingGesture) { + return true; + } + mPositionController.beginScale(focusX, focusY); + // We can change mode if we are in film mode, or we are in page + // mode and at minimal scale. + mCanChangeMode = mFilmMode + || mPositionController.isAtMinimalScale(); + mAccScale = 1f; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (mIgnoreSwipingGesture) return true; + if (mIgnoreScalingGesture) return true; + if (mModeChanged) return true; + if (Float.isNaN(scale) || Float.isInfinite(scale)) return false; + + int outOfRange = mPositionController.scaleBy(scale, focusX, focusY); + + // We wait for a large enough scale change before changing mode. + // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out + // or vice versa. + mAccScale *= scale; + boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f); + + // If mode changes, we treat this scaling gesture has ended. + if (mCanChangeMode && largeEnough) { + if ((outOfRange < 0 && !mFilmMode) || + (outOfRange > 0 && mFilmMode)) { + stopExtraScalingIfNeeded(); + + // Removing the touch down flag allows snapback to happen + // for film mode change. + mHolding &= ~HOLD_TOUCH_DOWN; + if (mFilmMode) { + UsageStatistics.setPendingTransitionCause( + UsageStatistics.TRANSITION_PINCH_OUT); + } else { + UsageStatistics.setPendingTransitionCause( + UsageStatistics.TRANSITION_PINCH_IN); + } + setFilmMode(!mFilmMode); + + + // We need to call onScaleEnd() before setting mModeChanged + // to true. + onScaleEnd(); + mModeChanged = true; + return true; + } + } + + if (outOfRange != 0) { + startExtraScalingIfNeeded(); + } else { + stopExtraScalingIfNeeded(); + } + return true; + } + + @Override + public void onScaleEnd() { + if (mIgnoreSwipingGesture) return; + if (mIgnoreScalingGesture) return; + if (mModeChanged) return; + mPositionController.endScale(); + } + + private void startExtraScalingIfNeeded() { + if (!mCancelExtraScalingPending) { + mHandler.sendEmptyMessageDelayed( + MSG_CANCEL_EXTRA_SCALING, 700); + mPositionController.setExtraScalingRange(true); + mCancelExtraScalingPending = true; + } + } + + private void stopExtraScalingIfNeeded() { + if (mCancelExtraScalingPending) { + mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING); + mPositionController.setExtraScalingRange(false); + mCancelExtraScalingPending = false; + } + } + + @Override + public void onDown(float x, float y) { + checkHideUndoBar(UNDO_BAR_TOUCHED); + + mDeltaY = 0; + mModeChanged = false; + + if (mIgnoreSwipingGesture) return; + + mHolding |= HOLD_TOUCH_DOWN; + + if (mFilmMode && mPositionController.isScrolling()) { + mDownInScrolling = true; + mPositionController.stopScrolling(); + } else { + mDownInScrolling = false; + } + mHadFling = false; + mScrolledAfterDown = false; + if (mFilmMode) { + int xi = (int) (x + 0.5f); + int yi = (int) (y + 0.5f); + // We only care about being within the x bounds, necessary for + // handling very wide images which are otherwise very hard to fling + mTouchBoxIndex = mPositionController.hitTest(xi, getHeight() / 2); + + if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) { + mTouchBoxIndex = Integer.MAX_VALUE; + } else { + mTouchBoxDeletable = + mPictures.get(mTouchBoxIndex).isDeletable(); + } + } else { + mTouchBoxIndex = Integer.MAX_VALUE; + } + } + + @Override + public void onUp() { + if (mIgnoreSwipingGesture) return; + + mHolding &= ~HOLD_TOUCH_DOWN; + mEdgeView.onRelease(); + + // If we scrolled in Y direction far enough, treat it as a delete + // gesture. + if (mFilmMode && mScrolledAfterDown && !mFirstScrollX + && mTouchBoxIndex != Integer.MAX_VALUE) { + Rect r = mPositionController.getPosition(mTouchBoxIndex); + int h = getHeight(); + if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) { + int duration = mPositionController + .flingFilmY(mTouchBoxIndex, 0); + if (duration >= 0) { + mPositionController.setPopFromTop(r.centerY() < h * 0.5f); + deleteAfterAnimation(duration); + } + } + } + + if (mIgnoreUpEvent) { + mIgnoreUpEvent = false; + return; + } + + if (!(mFilmMode && !mHadFling && mFirstScrollX + && snapToNeighborImage())) { + snapback(); + } + } + + public void setSwipingEnabled(boolean enabled) { + mIgnoreSwipingGesture = !enabled; + } + } + + public void setSwipingEnabled(boolean enabled) { + mGestureListener.setSwipingEnabled(enabled); + } + + private void updateActionBar() { + boolean isCamera = mPictures.get(0).isCamera(); + if (isCamera && !mFilmMode) { + // Move into camera in page mode, lock + mListener.onActionBarAllowed(false); + } else { + mListener.onActionBarAllowed(true); + if (mFilmMode) mListener.onActionBarWanted(); + } + } + + public void setFilmMode(boolean enabled) { + if (mFilmMode == enabled) return; + mFilmMode = enabled; + mPositionController.setFilmMode(mFilmMode); + mModel.setNeedFullImage(!enabled); + mModel.setFocusHintDirection( + mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT); + updateActionBar(); + mListener.onFilmModeChanged(enabled); + } + + public boolean getFilmMode() { + return mFilmMode; + } + + //////////////////////////////////////////////////////////////////////////// + // Framework events + //////////////////////////////////////////////////////////////////////////// + + public void pause() { + mPositionController.skipAnimation(); + mTileView.freeTextures(); + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) { + mPictures.get(i).setScreenNail(null); + } + hideUndoBar(); + } + + public void resume() { + mTileView.prepareTextures(); + mPositionController.skipToFinalPosition(); + } + + // move to the camera preview and show controls after resume + public void resetToFirstPicture() { + mModel.moveTo(0); + setFilmMode(false); + } + + //////////////////////////////////////////////////////////////////////////// + // Undo Bar + //////////////////////////////////////////////////////////////////////////// + + private int mUndoBarState; + private static final int UNDO_BAR_SHOW = 1; + private static final int UNDO_BAR_TIMEOUT = 2; + private static final int UNDO_BAR_TOUCHED = 4; + private static final int UNDO_BAR_FULL_CAMERA = 8; + private static final int UNDO_BAR_DELETE_LAST = 16; + + // "deleteLast" means if the deletion is on the last remaining picture in + // the album. + private void showUndoBar(boolean deleteLast) { + mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT); + mUndoBarState = UNDO_BAR_SHOW; + if(deleteLast) mUndoBarState |= UNDO_BAR_DELETE_LAST; + mUndoBar.animateVisibility(GLView.VISIBLE); + mHandler.sendEmptyMessageDelayed(MSG_UNDO_BAR_TIMEOUT, 3000); + if (mListener != null) mListener.onUndoBarVisibilityChanged(true); + } + + private void hideUndoBar() { + mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT); + mListener.onCommitDeleteImage(); + mUndoBar.animateVisibility(GLView.INVISIBLE); + mUndoBarState = 0; + mUndoIndexHint = Integer.MAX_VALUE; + mListener.onUndoBarVisibilityChanged(false); + } + + // Check if the one of the conditions for hiding the undo bar has been + // met. The conditions are: + // + // 1. It has been three seconds since last showing, and (a) the user has + // touched, or (b) the deleted picture is the last remaining picture in the + // album. + // + // 2. The camera is shown in full screen. + private void checkHideUndoBar(int addition) { + mUndoBarState |= addition; + if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return; + boolean timeout = (mUndoBarState & UNDO_BAR_TIMEOUT) != 0; + boolean touched = (mUndoBarState & UNDO_BAR_TOUCHED) != 0; + boolean fullCamera = (mUndoBarState & UNDO_BAR_FULL_CAMERA) != 0; + boolean deleteLast = (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0; + if ((timeout && deleteLast) || fullCamera || touched) { + hideUndoBar(); + } + } + + public boolean canUndo() { + return (mUndoBarState & UNDO_BAR_SHOW) != 0; + } + + //////////////////////////////////////////////////////////////////////////// + // Rendering + //////////////////////////////////////////////////////////////////////////// + + @Override + protected void render(GLCanvas canvas) { + if (mFirst) { + // Make sure the fields are properly initialized before checking + // whether isCamera() + mPictures.get(0).reload(); + } + // Check if the camera preview occupies the full screen. + boolean full = !mFilmMode && mPictures.get(0).isCamera() + && mPositionController.isCenter() + && mPositionController.isAtMinimalScale(); + if (mFirst || full != mFullScreenCamera) { + mFullScreenCamera = full; + mFirst = false; + mListener.onFullScreenChanged(full); + if (full) mHandler.sendEmptyMessage(MSG_UNDO_BAR_FULL_CAMERA); + } + + // Determine how many photos we need to draw in addition to the center + // one. + int neighbors; + if (mFullScreenCamera) { + neighbors = 0; + } else { + // In page mode, we draw only one previous/next photo. But if we are + // doing capture animation, we want to draw all photos. + boolean inPageMode = (mPositionController.getFilmRatio() == 0f); + boolean inCaptureAnimation = + ((mHolding & HOLD_CAPTURE_ANIMATION) != 0); + if (inPageMode && !inCaptureAnimation) { + neighbors = 1; + } else { + neighbors = SCREEN_NAIL_MAX; + } + } + + // Draw photos from back to front + for (int i = neighbors; i >= -neighbors; i--) { + Rect r = mPositionController.getPosition(i); + mPictures.get(i).draw(canvas, r); + } + + renderChild(canvas, mEdgeView); + renderChild(canvas, mUndoBar); + + mPositionController.advanceAnimation(); + checkFocusSwitching(); + } + + //////////////////////////////////////////////////////////////////////////// + // Film mode focus switching + //////////////////////////////////////////////////////////////////////////// + + // Runs in GL thread. + private void checkFocusSwitching() { + if (!mFilmMode) return; + if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return; + if (switchPosition() != 0) { + mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS); + } + } + + // Runs in main thread. + private void switchFocus() { + if (mHolding != 0) return; + switch (switchPosition()) { + case -1: + switchToPrevImage(); + break; + case 1: + switchToNextImage(); + break; + } + } + + // Returns -1 if we should switch focus to the previous picture, +1 if we + // should switch to the next, 0 otherwise. + private int switchPosition() { + Rect curr = mPositionController.getPosition(0); + int center = getWidth() / 2; + + if (curr.left > center && mPrevBound < 0) { + Rect prev = mPositionController.getPosition(-1); + int currDist = curr.left - center; + int prevDist = center - prev.right; + if (prevDist < currDist) { + return -1; + } + } else if (curr.right < center && mNextBound > 0) { + Rect next = mPositionController.getPosition(1); + int currDist = center - curr.right; + int nextDist = next.left - center; + if (nextDist < currDist) { + return 1; + } + } + + return 0; + } + + // Switch to the previous or next picture if the hit position is inside + // one of their boxes. This runs in main thread. + private void switchToHitPicture(int x, int y) { + if (mPrevBound < 0) { + Rect r = mPositionController.getPosition(-1); + if (r.right >= x) { + slideToPrevPicture(); + return; + } + } + + if (mNextBound > 0) { + Rect r = mPositionController.getPosition(1); + if (r.left <= x) { + slideToNextPicture(); + return; + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // Page mode focus switching + // + // We slide image to the next one or the previous one in two cases: 1: If + // the user did a fling gesture with enough velocity. 2 If the user has + // moved the picture a lot. + //////////////////////////////////////////////////////////////////////////// + + private boolean swipeImages(float velocityX, float velocityY) { + if (mFilmMode) return false; + + // Avoid swiping images if we're possibly flinging to view the + // zoomed in picture vertically. + PositionController controller = mPositionController; + boolean isMinimal = controller.isAtMinimalScale(); + int edges = controller.getImageAtEdges(); + if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX)) + if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0 + || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0) + return false; + + // If we are at the edge of the current photo and the sweeping velocity + // exceeds the threshold, slide to the next / previous image. + if (velocityX < -SWIPE_THRESHOLD && (isMinimal + || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) { + return slideToNextPicture(); + } else if (velocityX > SWIPE_THRESHOLD && (isMinimal + || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) { + return slideToPrevPicture(); + } + + return false; + } + + private void snapback() { + if ((mHolding & ~HOLD_DELETE) != 0) return; + if (mFilmMode || !snapToNeighborImage()) { + mPositionController.snapback(); + } + } + + private boolean snapToNeighborImage() { + Rect r = mPositionController.getPosition(0); + int viewW = getWidth(); + // Setting the move threshold proportional to the width of the view + int moveThreshold = viewW / 5 ; + int threshold = moveThreshold + gapToSide(r.width(), viewW); + + // If we have moved the picture a lot, switching. + if (viewW - r.right > threshold) { + return slideToNextPicture(); + } else if (r.left > threshold) { + return slideToPrevPicture(); + } + + return false; + } + + private boolean slideToNextPicture() { + if (mNextBound <= 0) return false; + switchToNextImage(); + mPositionController.startHorizontalSlide(); + return true; + } + + private boolean slideToPrevPicture() { + if (mPrevBound >= 0) return false; + switchToPrevImage(); + mPositionController.startHorizontalSlide(); + return true; + } + + private static int gapToSide(int imageWidth, int viewWidth) { + return Math.max(0, (viewWidth - imageWidth) / 2); + } + + //////////////////////////////////////////////////////////////////////////// + // Focus switching + //////////////////////////////////////////////////////////////////////////// + + public void switchToImage(int index) { + mModel.moveTo(index); + } + + private void switchToNextImage() { + mModel.moveTo(mModel.getCurrentIndex() + 1); + } + + private void switchToPrevImage() { + mModel.moveTo(mModel.getCurrentIndex() - 1); + } + + private void switchToFirstImage() { + mModel.moveTo(0); + } + + //////////////////////////////////////////////////////////////////////////// + // Opening Animation + //////////////////////////////////////////////////////////////////////////// + + public void setOpenAnimationRect(Rect rect) { + mPositionController.setOpenAnimationRect(rect); + } + + //////////////////////////////////////////////////////////////////////////// + // Capture Animation + //////////////////////////////////////////////////////////////////////////// + + public boolean switchWithCaptureAnimation(int offset) { + GLRoot root = getGLRoot(); + if(root == null) return false; + root.lockRenderThread(); + try { + return switchWithCaptureAnimationLocked(offset); + } finally { + root.unlockRenderThread(); + } + } + + private boolean switchWithCaptureAnimationLocked(int offset) { + if (mHolding != 0) return true; + if (offset == 1) { + if (mNextBound <= 0) return false; + // Temporary disable action bar until the capture animation is done. + if (!mFilmMode) mListener.onActionBarAllowed(false); + switchToNextImage(); + mPositionController.startCaptureAnimationSlide(-1); + } else if (offset == -1) { + if (mPrevBound >= 0) return false; + if (mFilmMode) setFilmMode(false); + + // If we are too far away from the first image (so that we don't + // have all the ScreenNails in-between), we go directly without + // animation. + if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) { + switchToFirstImage(); + mPositionController.skipToFinalPosition(); + return true; + } + + switchToFirstImage(); + mPositionController.startCaptureAnimationSlide(1); + } else { + return false; + } + mHolding |= HOLD_CAPTURE_ANIMATION; + Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0); + mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME); + return true; + } + + private void captureAnimationDone(int offset) { + mHolding &= ~HOLD_CAPTURE_ANIMATION; + if (offset == 1 && !mFilmMode) { + // Now the capture animation is done, enable the action bar. + mListener.onActionBarAllowed(true); + mListener.onActionBarWanted(); + } + snapback(); + } + + //////////////////////////////////////////////////////////////////////////// + // Card deck effect calculation + //////////////////////////////////////////////////////////////////////////// + + // Returns the scrolling progress value for an object moving out of a + // view. The progress value measures how much the object has moving out of + // the view. The object currently displays in [left, right), and the view is + // at [0, viewWidth]. + // + // The returned value is negative when the object is moving right, and + // positive when the object is moving left. The value goes to -1 or 1 when + // the object just moves out of the view completely. The value is 0 if the + // object currently fills the view. + private static float calculateMoveOutProgress(int left, int right, + int viewWidth) { + // w = object width + // viewWidth = view width + int w = right - left; + + // If the object width is smaller than the view width, + // |....view....| + // |<-->| progress = -1 when left = viewWidth + // |<-->| progress = 0 when left = viewWidth / 2 - w / 2 + // |<-->| progress = 1 when left = -w + if (w < viewWidth) { + int zx = viewWidth / 2 - w / 2; + if (left > zx) { + return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1] + } else { + return (left - zx) / (float) (-w - zx); // progress = [0, 1] + } + } + + // If the object width is larger than the view width, + // |..view..| + // |<--------->| progress = -1 when left = viewWidth + // |<--------->| progress = 0 between left = 0 + // |<--------->| and right = viewWidth + // |<--------->| progress = 1 when right = 0 + if (left > 0) { + return -left / (float) viewWidth; + } + + if (right < viewWidth) { + return (viewWidth - right) / (float) viewWidth; + } + + return 0; + } + + // Maps a scrolling progress value to the alpha factor in the fading + // animation. + private float getScrollAlpha(float scrollProgress) { + return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation( + 1 - Math.abs(scrollProgress)) : 1.0f; + } + + // Maps a scrolling progress value to the scaling factor in the fading + // animation. + private float getScrollScale(float scrollProgress) { + float interpolatedProgress = mScaleInterpolator.getInterpolation( + Math.abs(scrollProgress)); + float scale = (1 - interpolatedProgress) + + interpolatedProgress * TRANSITION_SCALE_FACTOR; + return scale; + } + + + // This interpolator emulates the rate at which the perceived scale of an + // object changes as its distance from a camera increases. When this + // interpolator is applied to a scale animation on a view, it evokes the + // sense that the object is shrinking due to moving away from the camera. + private static class ZInterpolator { + private float focalLength; + + public ZInterpolator(float foc) { + focalLength = foc; + } + + public float getInterpolation(float input) { + return (1.0f - focalLength / (focalLength + input)) / + (1.0f - focalLength / (focalLength + 1.0f)); + } + } + + // Returns an interpolated value for the page/film transition. + // When ratio = 0, the result is from. + // When ratio = 1, the result is to. + private static float interpolate(float ratio, float from, float to) { + return from + (to - from) * ratio * ratio; + } + + // Returns the alpha factor in film mode if a picture is not in the center. + // The 0.03 lower bound is to make the item always visible a bit. + private float getOffsetAlpha(float offset) { + offset /= 0.5f; + float alpha = (offset > 0) ? (1 - offset) : (1 + offset); + return Utils.clamp(alpha, 0.03f, 1f); + } + + //////////////////////////////////////////////////////////////////////////// + // Simple public utilities + //////////////////////////////////////////////////////////////////////////// + + public void setListener(Listener listener) { + mListener = listener; + } + + public Rect getPhotoRect(int index) { + return mPositionController.getPosition(index); + } + + public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) { + Rect location = new Rect(); + Utils.assertTrue(root.getBoundsOf(this, location)); + + Rect fullRect = bounds(); + PhotoFallbackEffect effect = new PhotoFallbackEffect(); + for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) { + MediaItem item = mModel.getMediaItem(i); + if (item == null) continue; + ScreenNail sc = mModel.getScreenNail(i); + if (!(sc instanceof TiledScreenNail) + || ((TiledScreenNail) sc).isShowingPlaceholder()) continue; + + // Now, sc is BitmapScreenNail and is not showing placeholder + Rect rect = new Rect(getPhotoRect(i)); + if (!Rect.intersects(fullRect, rect)) continue; + rect.offset(location.left, location.top); + + int width = sc.getWidth(); + int height = sc.getHeight(); + + int rotation = mModel.getImageRotation(i); + RawTexture texture; + if ((rotation % 180) == 0) { + texture = new RawTexture(width, height, true); + canvas.beginRenderTarget(texture); + canvas.translate(width / 2f, height / 2f); + } else { + texture = new RawTexture(height, width, true); + canvas.beginRenderTarget(texture); + canvas.translate(height / 2f, width / 2f); + } + + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-width / 2f, -height / 2f); + sc.draw(canvas, 0, 0, width, height); + canvas.endRenderTarget(); + effect.addEntry(item.getPath(), rect, texture); + } + return effect; + } +} diff --git a/src/com/android/gallery3d/ui/PopupList.java b/src/com/android/gallery3d/ui/PopupList.java new file mode 100644 index 000000000..248f50b25 --- /dev/null +++ b/src/com/android/gallery3d/ui/PopupList.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.android.gallery3d.R; + +import java.util.ArrayList; + +public class PopupList { + + public static interface OnPopupItemClickListener { + public boolean onPopupItemClick(int itemId); + } + + public static class Item { + public final int id; + public String title; + + public Item(int id, String title) { + this.id = id; + this.title = title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + private final Context mContext; + private final View mAnchorView; + private final ArrayList<Item> mItems = new ArrayList<Item>(); + private PopupWindow mPopupWindow; + private ListView mContentList; + private OnPopupItemClickListener mOnPopupItemClickListener; + private int mPopupOffsetX; + private int mPopupOffsetY; + private int mPopupWidth; + private int mPopupHeight; + + public PopupList(Context context, View anchorView) { + mContext = context; + mAnchorView = anchorView; + } + + public void setOnPopupItemClickListener(OnPopupItemClickListener listener) { + mOnPopupItemClickListener = listener; + } + + public void addItem(int id, String title) { + mItems.add(new Item(id, title)); + } + + public void clearItems() { + mItems.clear(); + } + + private final PopupWindow.OnDismissListener mOnDismissListener = + new PopupWindow.OnDismissListener() { + @SuppressWarnings("deprecation") + @Override + public void onDismiss() { + if (mPopupWindow == null) return; + mPopupWindow = null; + ViewTreeObserver observer = mAnchorView.getViewTreeObserver(); + if (observer.isAlive()) { + // We used the deprecated function for backward compatibility + // The new "removeOnGlobalLayoutListener" is introduced in API level 16 + observer.removeGlobalOnLayoutListener(mOnGLobalLayoutListener); + } + } + }; + + private final OnItemClickListener mOnItemClickListener = + new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + if (mPopupWindow == null) return; + mPopupWindow.dismiss(); + if (mOnPopupItemClickListener != null) { + mOnPopupItemClickListener.onPopupItemClick((int) id); + } + } + }; + + private final OnGlobalLayoutListener mOnGLobalLayoutListener = + new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mPopupWindow == null) return; + updatePopupLayoutParams(); + // Need to update the position of the popup window + mPopupWindow.update(mAnchorView, + mPopupOffsetX, mPopupOffsetY, mPopupWidth, mPopupHeight); + } + }; + + public void show() { + if (mPopupWindow != null) return; + mAnchorView.getViewTreeObserver() + .addOnGlobalLayoutListener(mOnGLobalLayoutListener); + mPopupWindow = createPopupWindow(); + updatePopupLayoutParams(); + mPopupWindow.setWidth(mPopupWidth); + mPopupWindow.setHeight(mPopupHeight); + mPopupWindow.showAsDropDown(mAnchorView, mPopupOffsetX, mPopupOffsetY); + } + + private void updatePopupLayoutParams() { + ListView content = mContentList; + PopupWindow popup = mPopupWindow; + + Rect p = new Rect(); + popup.getBackground().getPadding(p); + + int maxHeight = mPopupWindow.getMaxAvailableHeight(mAnchorView) - p.top - p.bottom; + mContentList.measure( + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)); + mPopupWidth = content.getMeasuredWidth() + p.top + p.bottom; + mPopupHeight = Math.min(maxHeight, content.getMeasuredHeight() + p.left + p.right); + mPopupOffsetX = -p.left; + mPopupOffsetY = -p.top; + } + + private PopupWindow createPopupWindow() { + PopupWindow popup = new PopupWindow(mContext); + popup.setOnDismissListener(mOnDismissListener); + + popup.setBackgroundDrawable(mContext.getResources().getDrawable( + R.drawable.menu_dropdown_panel_holo_dark)); + + mContentList = new ListView(mContext, null, + android.R.attr.dropDownListViewStyle); + mContentList.setAdapter(new ItemDataAdapter()); + mContentList.setOnItemClickListener(mOnItemClickListener); + popup.setContentView(mContentList); + popup.setFocusable(true); + popup.setOutsideTouchable(true); + + return popup; + } + + public Item findItem(int id) { + for (Item item : mItems) { + if (item.id == id) return item; + } + return null; + } + + private class ItemDataAdapter extends BaseAdapter { + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return mItems.get(position).id; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(mContext) + .inflate(R.layout.popup_list_item, null); + } + TextView text = (TextView) convertView.findViewById(android.R.id.text1); + text.setText(mItems.get(position).title); + return convertView; + } + } +} diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java new file mode 100644 index 000000000..6a4bcea87 --- /dev/null +++ b/src/com/android/gallery3d/ui/PositionController.java @@ -0,0 +1,1821 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.widget.Scroller; + +import com.android.gallery3d.app.PhotoPage; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.ui.PhotoView.Size; +import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.RangeArray; +import com.android.gallery3d.util.RangeIntArray; + +class PositionController { + private static final String TAG = "PositionController"; + + public static final int IMAGE_AT_LEFT_EDGE = 1; + public static final int IMAGE_AT_RIGHT_EDGE = 2; + public static final int IMAGE_AT_TOP_EDGE = 4; + public static final int IMAGE_AT_BOTTOM_EDGE = 8; + + public static final int CAPTURE_ANIMATION_TIME = 700; + public static final int SNAPBACK_ANIMATION_TIME = 600; + + // Special values for animation time. + private static final long NO_ANIMATION = -1; + private static final long LAST_ANIMATION = -2; + + private static final int ANIM_KIND_NONE = -1; + private static final int ANIM_KIND_SCROLL = 0; + private static final int ANIM_KIND_SCALE = 1; + private static final int ANIM_KIND_SNAPBACK = 2; + private static final int ANIM_KIND_SLIDE = 3; + private static final int ANIM_KIND_ZOOM = 4; + private static final int ANIM_KIND_OPENING = 5; + private static final int ANIM_KIND_FLING = 6; + private static final int ANIM_KIND_FLING_X = 7; + private static final int ANIM_KIND_DELETE = 8; + private static final int ANIM_KIND_CAPTURE = 9; + + // Animation time in milliseconds. The order must match ANIM_KIND_* above. + // + // The values for ANIM_KIND_FLING_X does't matter because we use + // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's + // faster for Animatable.advanceAnimation() to calculate the progress + // (always 1). + private static final int ANIM_TIME[] = { + 0, // ANIM_KIND_SCROLL + 0, // ANIM_KIND_SCALE + SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK + 400, // ANIM_KIND_SLIDE + 300, // ANIM_KIND_ZOOM + 300, // ANIM_KIND_OPENING + 0, // ANIM_KIND_FLING (the duration is calculated dynamically) + 0, // ANIM_KIND_FLING_X (see the comment above) + 0, // ANIM_KIND_DELETE (the duration is calculated dynamically) + CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE + }; + + // We try to scale up the image to fill the screen. But in order not to + // scale too much for small icons, we limit the max up-scaling factor here. + private static final float SCALE_LIMIT = 4; + + // For user's gestures, we give a temporary extra scaling range which goes + // above or below the usual scaling limits. + private static final float SCALE_MIN_EXTRA = 0.7f; + private static final float SCALE_MAX_EXTRA = 1.4f; + + // Setting this true makes the extra scaling range permanent (until this is + // set to false again). + private boolean mExtraScalingRange = false; + + // Film Mode v.s. Page Mode: in film mode we show smaller pictures. + private boolean mFilmMode = false; + + // These are the limits for width / height of the picture in film mode. + private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f; + private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f; + private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f; + private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f; + + // In addition to the focused box (index == 0). We also keep information + // about this many boxes on each side. + private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX; + private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1]; + + private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16); + private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12); + + // These are constants for the delete gesture. + private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms + private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms + + private Listener mListener; + private volatile Rect mOpenAnimationRect; + + // Use a large enough value, so we won't see the gray shadow in the beginning. + private int mViewW = 1200; + private int mViewH = 1200; + + // A scaling gesture is in progress. + private boolean mInScale; + // The focus point of the scaling gesture, relative to the center of the + // picture in bitmap pixels. + private float mFocusX, mFocusY; + + // whether there is a previous/next picture. + private boolean mHasPrev, mHasNext; + + // This is used by the fling animation (page mode). + private FlingScroller mPageScroller; + + // This is used by the fling animation (film mode). + private Scroller mFilmScroller; + + // The bound of the stable region that the focused box can stay, see the + // comments above calculateStableBound() for details. + private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom; + + // Constrained frame is a rectangle that the focused box should fit into if + // it is constrained. It has two effects: + // + // (1) In page mode, if the focused box is constrained, scaling for the + // focused box is adjusted to fit into the constrained frame, instead of the + // whole view. + // + // (2) In page mode, if the focused box is constrained, the mPlatform's + // default center (mDefaultX/Y) is moved to the center of the constrained + // frame, instead of the view center. + // + private Rect mConstrainedFrame = new Rect(); + + // Whether the focused box is constrained. + // + // Our current program's first call to moveBox() sets constrained = true, so + // we set the initial value of this variable to true, and we will not see + // see unwanted transition animation. + private boolean mConstrained = true; + + // + // ___________________________________________________________ + // | _____ _____ _____ _____ _____ | + // | | | | | | | | | | | | + // | | Box | | Box | | Box*| | Box | | Box | | + // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| | + // | Gap Gap Gap Gap | + // |___________________________________________________________| + // + // <-- Platform --> + // + // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY) + + private Platform mPlatform = new Platform(); + private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); + // The gap at the right of a Box i is at index i. The gap at the left of a + // Box i is at index i - 1. + private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); + private FilmRatio mFilmRatio = new FilmRatio(); + + // These are only used during moveBox(). + private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX); + private RangeArray<Gap> mTempGaps = + new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1); + + // The output of the PositionController. Available through getPosition(). + private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX); + + // The direction of a new picture should appear. New pictures pop from top + // if this value is true, or from bottom if this value is false. + boolean mPopFromTop; + + public interface Listener { + void invalidate(); + boolean isHoldingDown(); + boolean isHoldingDelete(); + + // EdgeView + void onPull(int offset, int direction); + void onRelease(); + void onAbsorb(int velocity, int direction); + } + + static { + // Initialize the CENTER_OUT_INDEX array. + // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX + // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX + for (int i = 0; i < CENTER_OUT_INDEX.length; i++) { + int j = (i + 1) / 2; + if ((i & 1) == 0) j = -j; + CENTER_OUT_INDEX[i] = j; + } + } + + public PositionController(Context context, Listener listener) { + mListener = listener; + mPageScroller = new FlingScroller(); + mFilmScroller = new Scroller(context, null, false); + + // Initialize the areas. + initPlatform(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mBoxes.put(i, new Box()); + initBox(i); + mRects.put(i, new Rect()); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mGaps.put(i, new Gap()); + initGap(i); + } + } + + public void setOpenAnimationRect(Rect r) { + mOpenAnimationRect = r; + } + + public void setViewSize(int viewW, int viewH) { + if (viewW == mViewW && viewH == mViewH) return; + + boolean wasMinimal = isAtMinimalScale(); + + mViewW = viewW; + mViewH = viewH; + initPlatform(); + + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + setBoxSize(i, viewW, viewH, true); + } + + updateScaleAndGapLimit(); + + // If the focused box was at minimal scale, we try to make it the + // minimal scale under the new view size. + if (wasMinimal) { + Box b = mBoxes.get(0); + b.mCurrentScale = b.mScaleMin; + } + + // If we have the opening animation, do it. Otherwise go directly to the + // right position. + if (!startOpeningAnimationIfNeeded()) { + skipToFinalPosition(); + } + } + + public void setConstrainedFrame(Rect cFrame) { + if (mConstrainedFrame.equals(cFrame)) return; + mConstrainedFrame.set(cFrame); + mPlatform.updateDefaultXY(); + updateScaleAndGapLimit(); + snapAndRedraw(); + } + + public void forceImageSize(int index, Size s) { + if (s.width == 0 || s.height == 0) return; + Box b = mBoxes.get(index); + b.mImageW = s.width; + b.mImageH = s.height; + return; + } + + public void setImageSize(int index, Size s, Rect cFrame) { + if (s.width == 0 || s.height == 0) return; + + boolean needUpdate = false; + if (cFrame != null && !mConstrainedFrame.equals(cFrame)) { + mConstrainedFrame.set(cFrame); + mPlatform.updateDefaultXY(); + needUpdate = true; + } + needUpdate |= setBoxSize(index, s.width, s.height, false); + + if (!needUpdate) return; + updateScaleAndGapLimit(); + snapAndRedraw(); + } + + // Returns false if the box size doesn't change. + private boolean setBoxSize(int i, int width, int height, boolean isViewSize) { + Box b = mBoxes.get(i); + boolean wasViewSize = b.mUseViewSize; + + // If we already have an image size, we don't want to use the view size. + if (!wasViewSize && isViewSize) return false; + + b.mUseViewSize = isViewSize; + + if (width == b.mImageW && height == b.mImageH) { + return false; + } + + // The ratio of the old size and the new size. + // + // If the aspect ratio changes, we don't know if it is because one side + // grows or the other side shrinks. Currently we just assume the view + // angle of the longer side doesn't change (so the aspect ratio change + // is because the view angle of the shorter side changes). This matches + // what camera preview does. + float ratio = (width > height) + ? (float) b.mImageW / width + : (float) b.mImageH / height; + + b.mImageW = width; + b.mImageH = height; + + // If this is the first time we receive an image size or we are in fullscreen, + // we change the scale directly. Otherwise adjust the scales by a ratio, + // and snapback will animate the scale into the min/max bounds if necessary. + if ((wasViewSize && !isViewSize) || !mFilmMode) { + b.mCurrentScale = getMinimalScale(b); + b.mAnimationStartTime = NO_ANIMATION; + } else { + b.mCurrentScale *= ratio; + b.mFromScale *= ratio; + b.mToScale *= ratio; + } + + if (i == 0) { + mFocusX /= ratio; + mFocusY /= ratio; + } + + return true; + } + + private boolean startOpeningAnimationIfNeeded() { + if (mOpenAnimationRect == null) return false; + Box b = mBoxes.get(0); + if (b.mUseViewSize) return false; + + // Start animation from the saved rectangle if we have one. + Rect r = mOpenAnimationRect; + mOpenAnimationRect = null; + + mPlatform.mCurrentX = r.centerX() - mViewW / 2; + b.mCurrentY = r.centerY() - mViewH / 2; + b.mCurrentScale = Math.max(r.width() / (float) b.mImageW, + r.height() / (float) b.mImageH); + startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, + ANIM_KIND_OPENING); + + // Animate from large gaps for neighbor boxes to avoid them + // shown on the screen during opening animation. + for (int i = -1; i < 1; i++) { + Gap g = mGaps.get(i); + g.mCurrentGap = mViewW; + g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING); + } + + return true; + } + + public void setFilmMode(boolean enabled) { + if (enabled == mFilmMode) return; + mFilmMode = enabled; + + mPlatform.updateDefaultXY(); + updateScaleAndGapLimit(); + stopAnimation(); + snapAndRedraw(); + } + + public void setExtraScalingRange(boolean enabled) { + if (mExtraScalingRange == enabled) return; + mExtraScalingRange = enabled; + if (!enabled) { + snapAndRedraw(); + } + } + + // This should be called whenever the scale range of boxes or the default + // gap size may change. Currently this can happen due to change of view + // size, image size, mFilmMode, mConstrained, and mConstrainedFrame. + private void updateScaleAndGapLimit() { + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + Box b = mBoxes.get(i); + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + } + + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + Gap g = mGaps.get(i); + g.mDefaultSize = getDefaultGapSize(i); + } + } + + // Returns the default gap size according the the size of the boxes around + // the gap and the current mode. + private int getDefaultGapSize(int i) { + if (mFilmMode) return IMAGE_GAP; + Box a = mBoxes.get(i); + Box b = mBoxes.get(i + 1); + return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b)); + } + + // Here is how we layout the boxes in the page mode. + // + // previous current next + // ___________ ________________ __________ + // | _______ | | __________ | | ______ | + // | | | | | | right->| | | | | | + // | | |<-------->|<--left | | | | | | + // | |_______| | | | |__________| | | |______| | + // |___________| | |________________| |__________| + // | <--> gapToSide() + // | + // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current)) + private int gapToSide(Box b) { + return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f); + } + + // Stop all animations at where they are now. + public void stopAnimation() { + mPlatform.mAnimationStartTime = NO_ANIMATION; + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mBoxes.get(i).mAnimationStartTime = NO_ANIMATION; + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mGaps.get(i).mAnimationStartTime = NO_ANIMATION; + } + } + + public void skipAnimation() { + if (mPlatform.mAnimationStartTime != NO_ANIMATION) { + mPlatform.mCurrentX = mPlatform.mToX; + mPlatform.mCurrentY = mPlatform.mToY; + mPlatform.mAnimationStartTime = NO_ANIMATION; + } + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + Box b = mBoxes.get(i); + if (b.mAnimationStartTime == NO_ANIMATION) continue; + b.mCurrentY = b.mToY; + b.mCurrentScale = b.mToScale; + b.mAnimationStartTime = NO_ANIMATION; + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + Gap g = mGaps.get(i); + if (g.mAnimationStartTime == NO_ANIMATION) continue; + g.mCurrentGap = g.mToGap; + g.mAnimationStartTime = NO_ANIMATION; + } + redraw(); + } + + public void snapback() { + snapAndRedraw(); + } + + public void skipToFinalPosition() { + stopAnimation(); + snapAndRedraw(); + skipAnimation(); + } + + //////////////////////////////////////////////////////////////////////////// + // Start an animations for the focused box + //////////////////////////////////////////////////////////////////////////// + + public void zoomIn(float tapX, float tapY, float targetScale) { + tapX -= mViewW / 2; + tapY -= mViewH / 2; + Box b = mBoxes.get(0); + + // Convert the tap position to distance to center in bitmap coordinates + float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale; + float tempY = (tapY - b.mCurrentY) / b.mCurrentScale; + + int x = (int) (-tempX * targetScale + 0.5f); + int y = (int) (-tempY * targetScale + 0.5f); + + calculateStableBound(targetScale); + int targetX = Utils.clamp(x, mBoundLeft, mBoundRight); + int targetY = Utils.clamp(y, mBoundTop, mBoundBottom); + targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax); + + startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM); + } + + public void resetToFullView() { + Box b = mBoxes.get(0); + startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM); + } + + public void beginScale(float focusX, float focusY) { + focusX -= mViewW / 2; + focusY -= mViewH / 2; + Box b = mBoxes.get(0); + Platform p = mPlatform; + mInScale = true; + mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f); + mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f); + } + + // Scales the image by the given factor. + // Returns an out-of-range indicator: + // 1 if the intended scale is too large for the stable range. + // 0 if the intended scale is in the stable range. + // -1 if the intended scale is too small for the stable range. + public int scaleBy(float s, float focusX, float focusY) { + focusX -= mViewW / 2; + focusY -= mViewH / 2; + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // We want to keep the focus point (on the bitmap) the same as when we + // begin the scale gesture, that is, + // + // (focusX' - currentX') / scale' = (focusX - currentX) / scale + // + s = b.clampScale(s * getTargetScale(b)); + int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f); + int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f); + startAnimation(x, y, s, ANIM_KIND_SCALE); + if (s < b.mScaleMin) return -1; + if (s > b.mScaleMax) return 1; + return 0; + } + + public void endScale() { + mInScale = false; + snapAndRedraw(); + } + + // Slide the focused box to the center of the view. + public void startHorizontalSlide() { + Box b = mBoxes.get(0); + startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE); + } + + // Slide the focused box to the center of the view with the capture + // animation. In addition to the sliding, the animation will also scale the + // the focused box, the specified neighbor box, and the gap between the + // two. The specified offset should be 1 or -1. + public void startCaptureAnimationSlide(int offset) { + Box b = mBoxes.get(0); + Box n = mBoxes.get(offset); // the neighbor box + Gap g = mGaps.get(offset); // the gap between the two boxes + + mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY, + ANIM_KIND_CAPTURE); + b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE); + n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE); + g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE); + redraw(); + } + + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + private boolean canScroll() { + Box b = mBoxes.get(0); + if (b.mAnimationStartTime == NO_ANIMATION) return true; + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + return true; + } + return false; + } + + public void scrollPage(int dx, int dy) { + if (!canScroll()) return; + + Box b = mBoxes.get(0); + Platform p = mPlatform; + + calculateStableBound(b.mCurrentScale); + + int x = p.mCurrentX + dx; + int y = b.mCurrentY + dy; + + // Vertical direction: If we have space to move in the vertical + // direction, we show the edge effect when scrolling reaches the edge. + if (mBoundTop != mBoundBottom) { + if (y < mBoundTop) { + mListener.onPull(mBoundTop - y, EdgeView.BOTTOM); + } else if (y > mBoundBottom) { + mListener.onPull(y - mBoundBottom, EdgeView.TOP); + } + } + + y = Utils.clamp(y, mBoundTop, mBoundBottom); + + // Horizontal direction: we show the edge effect when the scrolling + // tries to go left of the first image or go right of the last image. + if (!mHasPrev && x > mBoundRight) { + int pixels = x - mBoundRight; + mListener.onPull(pixels, EdgeView.LEFT); + x = mBoundRight; + } else if (!mHasNext && x < mBoundLeft) { + int pixels = mBoundLeft - x; + mListener.onPull(pixels, EdgeView.RIGHT); + x = mBoundLeft; + } + + startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL); + } + + public void scrollFilmX(int dx) { + if (!canScroll()) return; + + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // Only allow scrolling when we are not currently in an animation or we + // are in some animation with can be interrupted. + if (b.mAnimationStartTime != NO_ANIMATION) { + switch (b.mAnimationKind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + break; + default: + return; + } + } + + int x = p.mCurrentX + dx; + + // Horizontal direction: we show the edge effect when the scrolling + // tries to go left of the first image or go right of the last image. + x -= mPlatform.mDefaultX; + if (!mHasPrev && x > 0) { + mListener.onPull(x, EdgeView.LEFT); + x = 0; + } else if (!mHasNext && x < 0) { + mListener.onPull(-x, EdgeView.RIGHT); + x = 0; + } + x += mPlatform.mDefaultX; + startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL); + } + + public void scrollFilmY(int boxIndex, int dy) { + if (!canScroll()) return; + + Box b = mBoxes.get(boxIndex); + int y = b.mCurrentY + dy; + b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL); + redraw(); + } + + public boolean flingPage(int velocityX, int velocityY) { + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // We only want to do fling when the picture is zoomed-in. + if (viewWiderThanScaledImage(b.mCurrentScale) && + viewTallerThanScaledImage(b.mCurrentScale)) { + return false; + } + + // We only allow flinging in the directions where it won't go over the + // picture. + int edges = getImageAtEdges(); + if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) || + (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) { + velocityX = 0; + } + if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) || + (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) { + velocityY = 0; + } + + if (velocityX == 0 && velocityY == 0) return false; + + mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY, + mBoundLeft, mBoundRight, mBoundTop, mBoundBottom); + int targetX = mPageScroller.getFinalX(); + int targetY = mPageScroller.getFinalY(); + ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration(); + return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING); + } + + public boolean flingFilmX(int velocityX) { + if (velocityX == 0) return false; + + Box b = mBoxes.get(0); + Platform p = mPlatform; + + // If we are already at the edge, don't start the fling. + int defaultX = p.mDefaultX; + if ((!mHasPrev && p.mCurrentX >= defaultX) + || (!mHasNext && p.mCurrentX <= defaultX)) { + return false; + } + + mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0, + Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); + int targetX = mFilmScroller.getFinalX(); + return startAnimation( + targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X); + } + + // Moves the specified box out of screen. If velocityY is 0, a default + // velocity is used. Returns the time for the duration, or -1 if we cannot + // not do the animation. + public int flingFilmY(int boxIndex, int velocityY) { + Box b = mBoxes.get(boxIndex); + + // Calculate targetY + int h = heightOf(b); + int targetY; + int FUZZY = 3; // TODO: figure out why this is needed. + if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) { + targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY; + } else { + targetY = (mViewH + 1) / 2 + h / 2 + FUZZY; + } + + // Calculate duration + int duration; + if (velocityY != 0) { + duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f + / Math.abs(velocityY)); + duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration); + } else { + duration = DEFAULT_DELETE_ANIMATION_DURATION; + } + + // Start animation + ANIM_TIME[ANIM_KIND_DELETE] = duration; + if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) { + redraw(); + return duration; + } + return -1; + } + + // Returns the index of the box which contains the given point (x, y) + // Returns Integer.MAX_VALUE if there is no hit. There may be more than + // one box contains the given point, and we want to give priority to the + // one closer to the focused index (0). + public int hitTest(int x, int y) { + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + int j = CENTER_OUT_INDEX[i]; + Rect r = mRects.get(j); + if (r.contains(x, y)) { + return j; + } + } + + return Integer.MAX_VALUE; + } + + //////////////////////////////////////////////////////////////////////////// + // Redraw + // + // If a method changes box positions directly, redraw() + // should be called. + // + // If a method may also cause a snapback to happen, snapAndRedraw() should + // be called. + // + // If a method starts an animation to change the position of focused box, + // startAnimation() should be called. + // + // If time advances to change the box position, advanceAnimation() should + // be called. + //////////////////////////////////////////////////////////////////////////// + private void redraw() { + layoutAndSetPosition(); + mListener.invalidate(); + } + + private void snapAndRedraw() { + mPlatform.startSnapback(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mBoxes.get(i).startSnapback(); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mGaps.get(i).startSnapback(); + } + mFilmRatio.startSnapback(); + redraw(); + } + + private boolean startAnimation(int targetX, int targetY, float targetScale, + int kind) { + boolean changed = false; + changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind); + changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind); + if (changed) redraw(); + return changed; + } + + public void advanceAnimation() { + boolean changed = false; + changed |= mPlatform.advanceAnimation(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + changed |= mBoxes.get(i).advanceAnimation(); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + changed |= mGaps.get(i).advanceAnimation(); + } + changed |= mFilmRatio.advanceAnimation(); + if (changed) redraw(); + } + + public boolean inOpeningAnimation() { + return (mPlatform.mAnimationKind == ANIM_KIND_OPENING && + mPlatform.mAnimationStartTime != NO_ANIMATION) || + (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING && + mBoxes.get(0).mAnimationStartTime != NO_ANIMATION); + } + + //////////////////////////////////////////////////////////////////////////// + // Layout + //////////////////////////////////////////////////////////////////////////// + + // Returns the display width of this box. + private int widthOf(Box b) { + return (int) (b.mImageW * b.mCurrentScale + 0.5f); + } + + // Returns the display height of this box. + private int heightOf(Box b) { + return (int) (b.mImageH * b.mCurrentScale + 0.5f); + } + + // Returns the display width of this box, using the given scale. + private int widthOf(Box b, float scale) { + return (int) (b.mImageW * scale + 0.5f); + } + + // Returns the display height of this box, using the given scale. + private int heightOf(Box b, float scale) { + return (int) (b.mImageH * scale + 0.5f); + } + + // Convert the information in mPlatform and mBoxes to mRects, so the user + // can get the position of each box by getPosition(). + // + // Note we go from center-out because each box's X coordinate + // is relative to its anchor box (except the focused box). + private void layoutAndSetPosition() { + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + convertBoxToRect(CENTER_OUT_INDEX[i]); + } + //dumpState(); + } + + @SuppressWarnings("unused") + private void dumpState() { + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap); + } + + for (int i = 0; i < 2 * BOX_MAX + 1; i++) { + dumpRect(CENTER_OUT_INDEX[i]); + } + + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + for (int j = i + 1; j <= BOX_MAX; j++) { + if (Rect.intersects(mRects.get(i), mRects.get(j))) { + Log.d(TAG, "rect " + i + " and rect " + j + "intersects!"); + } + } + } + } + + private void dumpRect(int i) { + StringBuilder sb = new StringBuilder(); + Rect r = mRects.get(i); + sb.append("Rect " + i + ":"); + sb.append("("); + sb.append(r.centerX()); + sb.append(","); + sb.append(r.centerY()); + sb.append(") ["); + sb.append(r.width()); + sb.append("x"); + sb.append(r.height()); + sb.append("]"); + Log.d(TAG, sb.toString()); + } + + private void convertBoxToRect(int i) { + Box b = mBoxes.get(i); + Rect r = mRects.get(i); + int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2; + int w = widthOf(b); + int h = heightOf(b); + if (i == 0) { + int x = mPlatform.mCurrentX + mViewW / 2; + r.left = x - w / 2; + r.right = r.left + w; + } else if (i > 0) { + Rect a = mRects.get(i - 1); + Gap g = mGaps.get(i - 1); + r.left = a.right + g.mCurrentGap; + r.right = r.left + w; + } else { // i < 0 + Rect a = mRects.get(i + 1); + Gap g = mGaps.get(i); + r.right = a.left - g.mCurrentGap; + r.left = r.right - w; + } + r.top = y - h / 2; + r.bottom = r.top + h; + } + + // Returns the position of a box. + public Rect getPosition(int index) { + return mRects.get(index); + } + + //////////////////////////////////////////////////////////////////////////// + // Box management + //////////////////////////////////////////////////////////////////////////// + + // Initialize the platform to be at the view center. + private void initPlatform() { + mPlatform.updateDefaultXY(); + mPlatform.mCurrentX = mPlatform.mDefaultX; + mPlatform.mCurrentY = mPlatform.mDefaultY; + mPlatform.mAnimationStartTime = NO_ANIMATION; + } + + // Initialize a box to have the size of the view. + private void initBox(int index) { + Box b = mBoxes.get(index); + b.mImageW = mViewW; + b.mImageH = mViewH; + b.mUseViewSize = true; + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + b.mCurrentY = 0; + b.mCurrentScale = b.mScaleMin; + b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; + } + + // Initialize a box to a given size. + private void initBox(int index, Size size) { + if (size.width == 0 || size.height == 0) { + initBox(index); + return; + } + Box b = mBoxes.get(index); + b.mImageW = size.width; + b.mImageH = size.height; + b.mUseViewSize = false; + b.mScaleMin = getMinimalScale(b); + b.mScaleMax = getMaximalScale(b); + b.mCurrentY = 0; + b.mCurrentScale = b.mScaleMin; + b.mAnimationStartTime = NO_ANIMATION; + b.mAnimationKind = ANIM_KIND_NONE; + } + + // Initialize a gap. This can only be called after the boxes around the gap + // has been initialized. + private void initGap(int index) { + Gap g = mGaps.get(index); + g.mDefaultSize = getDefaultGapSize(index); + g.mCurrentGap = g.mDefaultSize; + g.mAnimationStartTime = NO_ANIMATION; + } + + private void initGap(int index, int size) { + Gap g = mGaps.get(index); + g.mDefaultSize = getDefaultGapSize(index); + g.mCurrentGap = size; + g.mAnimationStartTime = NO_ANIMATION; + } + + @SuppressWarnings("unused") + private void debugMoveBox(int fromIndex[]) { + StringBuilder s = new StringBuilder("moveBox:"); + for (int i = 0; i < fromIndex.length; i++) { + int j = fromIndex[i]; + if (j == Integer.MAX_VALUE) { + s.append(" N"); + } else { + s.append(" "); + s.append(fromIndex[i]); + } + } + Log.d(TAG, s.toString()); + } + + // Move the boxes: it may indicate focus change, box deleted, box appearing, + // box reordered, etc. + // + // Each element in the fromIndex array indicates where each box was in the + // old array. If the value is Integer.MAX_VALUE (pictured as N below), it + // means the box is new. + // + // For example: + // N N N N N N N -- all new boxes + // -3 -2 -1 0 1 2 3 -- nothing changed + // -2 -1 0 1 2 3 N -- focus goes to the next box + // N -3 -2 -1 0 1 2 -- focus goes to the previous box + // -3 -2 -1 1 2 3 N -- the focused box was deleted. + // + // hasPrev/hasNext indicates if there are previous/next boxes for the + // focused box. constrained indicates whether the focused box should be put + // into the constrained frame. + public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext, + boolean constrained, Size[] sizes) { + //debugMoveBox(fromIndex); + mHasPrev = hasPrev; + mHasNext = hasNext; + + RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX); + + // 1. Get the absolute X coordinates for the boxes. + layoutAndSetPosition(); + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + Box b = mBoxes.get(i); + Rect r = mRects.get(i); + b.mAbsoluteX = r.centerX() - mViewW / 2; + } + + // 2. copy boxes and gaps to temporary storage. + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + mTempBoxes.put(i, mBoxes.get(i)); + mBoxes.put(i, null); + } + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + mTempGaps.put(i, mGaps.get(i)); + mGaps.put(i, null); + } + + // 3. move back boxes that are used in the new array. + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + int j = from.get(i); + if (j == Integer.MAX_VALUE) continue; + mBoxes.put(i, mTempBoxes.get(j)); + mTempBoxes.put(j, null); + } + + // 4. move back gaps if both boxes around it are kept together. + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + int j = from.get(i); + if (j == Integer.MAX_VALUE) continue; + int k = from.get(i + 1); + if (k == Integer.MAX_VALUE) continue; + if (j + 1 == k) { + mGaps.put(i, mTempGaps.get(j)); + mTempGaps.put(j, null); + } + } + + // 5. recycle the boxes that are not used in the new array. + int k = -BOX_MAX; + for (int i = -BOX_MAX; i <= BOX_MAX; i++) { + if (mBoxes.get(i) != null) continue; + while (mTempBoxes.get(k) == null) { + k++; + } + mBoxes.put(i, mTempBoxes.get(k++)); + initBox(i, sizes[i + BOX_MAX]); + } + + // 6. Now give the recycled box a reasonable absolute X position. + // + // First try to find the first and the last box which the absolute X + // position is known. + int first, last; + for (first = -BOX_MAX; first <= BOX_MAX; first++) { + if (from.get(first) != Integer.MAX_VALUE) break; + } + for (last = BOX_MAX; last >= -BOX_MAX; last--) { + if (from.get(last) != Integer.MAX_VALUE) break; + } + // If there is no box has known X position at all, make the focused one + // as known. + if (first > BOX_MAX) { + mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX; + first = last = 0; + } + // Now for those boxes between first and last, assign their position to + // align to the previous box or the next box with known position. For + // the boxes before first or after last, we will use a new default gap + // size below. + + // Align to the previous box + for (int i = Math.max(0, first + 1); i < last; i++) { + if (from.get(i) != Integer.MAX_VALUE) continue; + Box a = mBoxes.get(i - 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + + getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } + } + + // Align to the next box + for (int i = Math.min(-1, last - 1); i > first; i--) { + if (from.get(i) != Integer.MAX_VALUE) continue; + Box a = mBoxes.get(i + 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) + - getDefaultGapSize(i); + if (mPopFromTop) { + b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2); + } else { + b.mCurrentY = (mViewH / 2 + heightOf(b) / 2); + } + } + + // 7. recycle the gaps that are not used in the new array. + k = -BOX_MAX; + for (int i = -BOX_MAX; i < BOX_MAX; i++) { + if (mGaps.get(i) != null) continue; + while (mTempGaps.get(k) == null) { + k++; + } + mGaps.put(i, mTempGaps.get(k++)); + Box a = mBoxes.get(i); + Box b = mBoxes.get(i + 1); + int wa = widthOf(a); + int wb = widthOf(b); + if (i >= first && i < last) { + int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2); + initGap(i, g); + } else { + initGap(i); + } + } + + // 8. calculate the new absolute X coordinates for those box before + // first or after last. + for (int i = first - 1; i >= -BOX_MAX; i--) { + Box a = mBoxes.get(i + 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + Gap g = mGaps.get(i); + b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap; + } + + for (int i = last + 1; i <= BOX_MAX; i++) { + Box a = mBoxes.get(i - 1); + Box b = mBoxes.get(i); + int wa = widthOf(a); + int wb = widthOf(b); + Gap g = mGaps.get(i - 1); + b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap; + } + + // 9. offset the Platform position + int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX; + mPlatform.mCurrentX += dx; + mPlatform.mFromX += dx; + mPlatform.mToX += dx; + mPlatform.mFlingOffset += dx; + + if (mConstrained != constrained) { + mConstrained = constrained; + mPlatform.updateDefaultXY(); + updateScaleAndGapLimit(); + } + + snapAndRedraw(); + } + + //////////////////////////////////////////////////////////////////////////// + // Public utilities + //////////////////////////////////////////////////////////////////////////// + + public boolean isAtMinimalScale() { + Box b = mBoxes.get(0); + return isAlmostEqual(b.mCurrentScale, b.mScaleMin); + } + + public boolean isCenter() { + Box b = mBoxes.get(0); + return mPlatform.mCurrentX == mPlatform.mDefaultX + && b.mCurrentY == 0; + } + + public int getImageWidth() { + Box b = mBoxes.get(0); + return b.mImageW; + } + + public int getImageHeight() { + Box b = mBoxes.get(0); + return b.mImageH; + } + + public float getImageScale() { + Box b = mBoxes.get(0); + return b.mCurrentScale; + } + + public int getImageAtEdges() { + Box b = mBoxes.get(0); + Platform p = mPlatform; + calculateStableBound(b.mCurrentScale); + int edges = 0; + if (p.mCurrentX <= mBoundLeft) { + edges |= IMAGE_AT_RIGHT_EDGE; + } + if (p.mCurrentX >= mBoundRight) { + edges |= IMAGE_AT_LEFT_EDGE; + } + if (b.mCurrentY <= mBoundTop) { + edges |= IMAGE_AT_BOTTOM_EDGE; + } + if (b.mCurrentY >= mBoundBottom) { + edges |= IMAGE_AT_TOP_EDGE; + } + return edges; + } + + public boolean isScrolling() { + return mPlatform.mAnimationStartTime != NO_ANIMATION + && mPlatform.mCurrentX != mPlatform.mToX; + } + + public void stopScrolling() { + if (mPlatform.mAnimationStartTime == NO_ANIMATION) return; + if (mFilmMode) mFilmScroller.forceFinished(true); + mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX; + } + + public float getFilmRatio() { + return mFilmRatio.mCurrentRatio; + } + + public void setPopFromTop(boolean top) { + mPopFromTop = top; + } + + public boolean hasDeletingBox() { + for(int i = -BOX_MAX; i <= BOX_MAX; i++) { + if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) { + return true; + } + } + return false; + } + + //////////////////////////////////////////////////////////////////////////// + // Private utilities + //////////////////////////////////////////////////////////////////////////// + + private float getMinimalScale(Box b) { + float wFactor = 1.0f; + float hFactor = 1.0f; + int viewW, viewH; + + if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty() + && b == mBoxes.get(0)) { + viewW = mConstrainedFrame.width(); + viewH = mConstrainedFrame.height(); + } else { + viewW = mViewW; + viewH = mViewH; + } + + if (mFilmMode) { + if (mViewH > mViewW) { // portrait + wFactor = FILM_MODE_PORTRAIT_WIDTH; + hFactor = FILM_MODE_PORTRAIT_HEIGHT; + } else { // landscape + wFactor = FILM_MODE_LANDSCAPE_WIDTH; + hFactor = FILM_MODE_LANDSCAPE_HEIGHT; + } + } + + float s = Math.min(wFactor * viewW / b.mImageW, + hFactor * viewH / b.mImageH); + return Math.min(SCALE_LIMIT, s); + } + + private float getMaximalScale(Box b) { + if (mFilmMode) return getMinimalScale(b); + if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b); + return SCALE_LIMIT; + } + + private static boolean isAlmostEqual(float a, float b) { + float diff = a - b; + return (diff < 0 ? -diff : diff) < 0.02f; + } + + // Calculates the stable region of mPlatform.mCurrentX and + // mBoxes.get(0).mCurrentY, where "stable" means + // + // (1) If the dimension of scaled image >= view dimension, we will not + // see black region outside the image (at that dimension). + // (2) If the dimension of scaled image < view dimension, we will center + // the scaled image. + // + // We might temporarily go out of this stable during user interaction, + // but will "snap back" after user stops interaction. + // + // The results are stored in mBound{Left/Right/Top/Bottom}. + // + // An extra parameter "horizontalSlack" (which has the value of 0 usually) + // is used to extend the stable region by some pixels on each side + // horizontally. + private void calculateStableBound(float scale, int horizontalSlack) { + Box b = mBoxes.get(0); + + // The width and height of the box in number of view pixels + int w = widthOf(b, scale); + int h = heightOf(b, scale); + + // When the edge of the view is aligned with the edge of the box + mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack; + mBoundRight = w / 2 - mViewW / 2 + horizontalSlack; + mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2; + mBoundBottom = h / 2 - mViewH / 2; + + // If the scaled height is smaller than the view height, + // force it to be in the center. + if (viewTallerThanScaledImage(scale)) { + mBoundTop = mBoundBottom = 0; + } + + // Same for width + if (viewWiderThanScaledImage(scale)) { + mBoundLeft = mBoundRight = mPlatform.mDefaultX; + } + } + + private void calculateStableBound(float scale) { + calculateStableBound(scale, 0); + } + + private boolean viewTallerThanScaledImage(float scale) { + return mViewH >= heightOf(mBoxes.get(0), scale); + } + + private boolean viewWiderThanScaledImage(float scale) { + return mViewW >= widthOf(mBoxes.get(0), scale); + } + + private float getTargetScale(Box b) { + return b.mAnimationStartTime == NO_ANIMATION + ? b.mCurrentScale : b.mToScale; + } + + //////////////////////////////////////////////////////////////////////////// + // Animatable: an thing which can do animation. + //////////////////////////////////////////////////////////////////////////// + private abstract static class Animatable { + public long mAnimationStartTime; + public int mAnimationKind; + public int mAnimationDuration; + + // This should be overridden in subclass to change the animation values + // give the progress value in [0, 1]. + protected abstract boolean interpolate(float progress); + public abstract boolean startSnapback(); + + // Returns true if the animation values changes, so things need to be + // redrawn. + public boolean advanceAnimation() { + if (mAnimationStartTime == NO_ANIMATION) { + return false; + } + if (mAnimationStartTime == LAST_ANIMATION) { + mAnimationStartTime = NO_ANIMATION; + return startSnapback(); + } + + float progress; + if (mAnimationDuration == 0) { + progress = 1; + } else { + long now = AnimationTime.get(); + progress = + (float) (now - mAnimationStartTime) / mAnimationDuration; + } + + if (progress >= 1) { + progress = 1; + } else { + progress = applyInterpolationCurve(mAnimationKind, progress); + } + + boolean done = interpolate(progress); + + if (done) { + mAnimationStartTime = LAST_ANIMATION; + } + + return true; + } + + private static float applyInterpolationCurve(int kind, float progress) { + float f = 1 - progress; + switch (kind) { + case ANIM_KIND_SCROLL: + case ANIM_KIND_FLING: + case ANIM_KIND_FLING_X: + case ANIM_KIND_DELETE: + case ANIM_KIND_CAPTURE: + progress = 1 - f; // linear + break; + case ANIM_KIND_OPENING: + case ANIM_KIND_SCALE: + progress = 1 - f * f; // quadratic + break; + case ANIM_KIND_SNAPBACK: + case ANIM_KIND_ZOOM: + case ANIM_KIND_SLIDE: + progress = 1 - f * f * f * f * f; // x^5 + break; + } + return progress; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Platform: captures the global X/Y movement. + //////////////////////////////////////////////////////////////////////////// + private class Platform extends Animatable { + public int mCurrentX, mFromX, mToX, mDefaultX; + public int mCurrentY, mFromY, mToY, mDefaultY; + public int mFlingOffset; + + @Override + public boolean startSnapback() { + if (mAnimationStartTime != NO_ANIMATION) return false; + if (mAnimationKind == ANIM_KIND_SCROLL + && mListener.isHoldingDown()) return false; + if (mInScale) return false; + + Box b = mBoxes.get(0); + float scaleMin = mExtraScalingRange ? + b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin; + float scaleMax = mExtraScalingRange ? + b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax; + float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax); + int x = mCurrentX; + int y = mDefaultY; + if (mFilmMode) { + x = mDefaultX; + } else { + calculateStableBound(scale, HORIZONTAL_SLACK); + // If the picture is zoomed-in, we want to keep the focus point + // stay in the same position on screen, so we need to adjust + // target mCurrentX (which is the center of the focused + // box). The position of the focus point on screen (relative the + // the center of the view) is: + // + // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX + // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX + // + if (!viewWiderThanScaledImage(scale)) { + float scaleDiff = b.mCurrentScale - scale; + x += (int) (mFocusX * scaleDiff + 0.5f); + } + x = Utils.clamp(x, mBoundLeft, mBoundRight); + } + if (mCurrentX != x || mCurrentY != y) { + return doAnimation(x, y, ANIM_KIND_SNAPBACK); + } + return false; + } + + // The updateDefaultXY() should be called whenever these variables + // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4) + // mFilmMode + public void updateDefaultXY() { + // We don't check mFilmMode and return 0 for mDefaultX. Because + // otherwise if we decide to leave film mode because we are + // centered, we will immediately back into film mode because we find + // we are not centered. + if (mConstrained && !mConstrainedFrame.isEmpty()) { + mDefaultX = mConstrainedFrame.centerX() - mViewW / 2; + mDefaultY = mFilmMode ? 0 : + mConstrainedFrame.centerY() - mViewH / 2; + } else { + mDefaultX = 0; + mDefaultY = 0; + } + } + + // Starts an animation for the platform. + private boolean doAnimation(int targetX, int targetY, int kind) { + if (mCurrentX == targetX && mCurrentY == targetY) return false; + mAnimationKind = kind; + mFromX = mCurrentX; + mFromY = mCurrentY; + mToX = targetX; + mToY = targetY; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[kind]; + mFlingOffset = 0; + advanceAnimation(); + return true; + } + + @Override + protected boolean interpolate(float progress) { + if (mAnimationKind == ANIM_KIND_FLING) { + return interpolateFlingPage(progress); + } else if (mAnimationKind == ANIM_KIND_FLING_X) { + return interpolateFlingFilm(progress); + } else { + return interpolateLinear(progress); + } + } + + private boolean interpolateFlingFilm(float progress) { + mFilmScroller.computeScrollOffset(); + mCurrentX = mFilmScroller.getCurrX() + mFlingOffset; + + int dir = EdgeView.INVALID_DIRECTION; + if (mCurrentX < mDefaultX) { + if (!mHasNext) { + dir = EdgeView.RIGHT; + } + } else if (mCurrentX > mDefaultX) { + if (!mHasPrev) { + dir = EdgeView.LEFT; + } + } + if (dir != EdgeView.INVALID_DIRECTION) { + // TODO: restore this onAbsorb call + //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f); + //mListener.onAbsorb(v, dir); + mFilmScroller.forceFinished(true); + mCurrentX = mDefaultX; + } + return mFilmScroller.isFinished(); + } + + private boolean interpolateFlingPage(float progress) { + mPageScroller.computeScrollOffset(progress); + Box b = mBoxes.get(0); + calculateStableBound(b.mCurrentScale); + + int oldX = mCurrentX; + mCurrentX = mPageScroller.getCurrX(); + + // Check if we hit the edges; show edge effects if we do. + if (oldX > mBoundLeft && mCurrentX == mBoundLeft) { + int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f); + mListener.onAbsorb(v, EdgeView.RIGHT); + } else if (oldX < mBoundRight && mCurrentX == mBoundRight) { + int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f); + mListener.onAbsorb(v, EdgeView.LEFT); + } + + return progress >= 1; + } + + private boolean interpolateLinear(float progress) { + // Other animations + if (progress >= 1) { + mCurrentX = mToX; + mCurrentY = mToY; + return true; + } else { + if (mAnimationKind == ANIM_KIND_CAPTURE) { + progress = CaptureAnimation.calculateSlide(progress); + } + mCurrentX = (int) (mFromX + progress * (mToX - mFromX)); + mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); + if (mAnimationKind == ANIM_KIND_CAPTURE) { + return false; + } else { + return (mCurrentX == mToX && mCurrentY == mToY); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // Box: represents a rectangular area which shows a picture. + //////////////////////////////////////////////////////////////////////////// + private class Box extends Animatable { + // Size of the bitmap + public int mImageW, mImageH; + + // This is true if we assume the image size is the same as view size + // until we know the actual size of image. This is also used to + // determine if there is an image ready to show. + public boolean mUseViewSize; + + // The minimum and maximum scale we allow for this box. + public float mScaleMin, mScaleMax; + + // The X/Y value indicates where the center of the box is on the view + // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the + // actual values used currently. Note that the X values are implicitly + // defined by Platform and Gaps. + public int mCurrentY, mFromY, mToY; + public float mCurrentScale, mFromScale, mToScale; + + // The absolute X coordinate of the center of the box. This is only used + // during moveBox(). + public int mAbsoluteX; + + @Override + public boolean startSnapback() { + if (mAnimationStartTime != NO_ANIMATION) return false; + if (mAnimationKind == ANIM_KIND_SCROLL + && mListener.isHoldingDown()) return false; + if (mAnimationKind == ANIM_KIND_DELETE + && mListener.isHoldingDelete()) return false; + if (mInScale && this == mBoxes.get(0)) return false; + + int y = mCurrentY; + float scale; + + if (this == mBoxes.get(0)) { + float scaleMin = mExtraScalingRange ? + mScaleMin * SCALE_MIN_EXTRA : mScaleMin; + float scaleMax = mExtraScalingRange ? + mScaleMax * SCALE_MAX_EXTRA : mScaleMax; + scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax); + if (mFilmMode) { + y = 0; + } else { + calculateStableBound(scale, HORIZONTAL_SLACK); + // If the picture is zoomed-in, we want to keep the focus + // point stay in the same position on screen. See the + // comment in Platform.startSnapback for details. + if (!viewTallerThanScaledImage(scale)) { + float scaleDiff = mCurrentScale - scale; + y += (int) (mFocusY * scaleDiff + 0.5f); + } + y = Utils.clamp(y, mBoundTop, mBoundBottom); + } + } else { + y = 0; + scale = mScaleMin; + } + + if (mCurrentY != y || mCurrentScale != scale) { + return doAnimation(y, scale, ANIM_KIND_SNAPBACK); + } + return false; + } + + private boolean doAnimation(int targetY, float targetScale, int kind) { + targetScale = clampScale(targetScale); + + if (mCurrentY == targetY && mCurrentScale == targetScale + && kind != ANIM_KIND_CAPTURE) { + return false; + } + + // Now starts an animation for the box. + mAnimationKind = kind; + mFromY = mCurrentY; + mFromScale = mCurrentScale; + mToY = targetY; + mToScale = targetScale; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[kind]; + advanceAnimation(); + return true; + } + + // Clamps the input scale to the range that doAnimation() can reach. + public float clampScale(float s) { + return Utils.clamp(s, + SCALE_MIN_EXTRA * mScaleMin, + SCALE_MAX_EXTRA * mScaleMax); + } + + @Override + protected boolean interpolate(float progress) { + if (mAnimationKind == ANIM_KIND_FLING) { + return interpolateFlingPage(progress); + } else { + return interpolateLinear(progress); + } + } + + private boolean interpolateFlingPage(float progress) { + mPageScroller.computeScrollOffset(progress); + calculateStableBound(mCurrentScale); + + int oldY = mCurrentY; + mCurrentY = mPageScroller.getCurrY(); + + // Check if we hit the edges; show edge effects if we do. + if (oldY > mBoundTop && mCurrentY == mBoundTop) { + int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f); + mListener.onAbsorb(v, EdgeView.BOTTOM); + } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) { + int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f); + mListener.onAbsorb(v, EdgeView.TOP); + } + + return progress >= 1; + } + + private boolean interpolateLinear(float progress) { + if (progress >= 1) { + mCurrentY = mToY; + mCurrentScale = mToScale; + return true; + } else { + mCurrentY = (int) (mFromY + progress * (mToY - mFromY)); + mCurrentScale = mFromScale + progress * (mToScale - mFromScale); + if (mAnimationKind == ANIM_KIND_CAPTURE) { + float f = CaptureAnimation.calculateScale(progress); + mCurrentScale *= f; + return false; + } else { + return (mCurrentY == mToY && mCurrentScale == mToScale); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // Gap: represents a rectangular area which is between two boxes. + //////////////////////////////////////////////////////////////////////////// + private class Gap extends Animatable { + // The default gap size between two boxes. The value may vary for + // different image size of the boxes and for different modes (page or + // film). + public int mDefaultSize; + + // The gap size between the two boxes. + public int mCurrentGap, mFromGap, mToGap; + + @Override + public boolean startSnapback() { + if (mAnimationStartTime != NO_ANIMATION) return false; + return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK); + } + + // Starts an animation for a gap. + public boolean doAnimation(int targetSize, int kind) { + if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) { + return false; + } + mAnimationKind = kind; + mFromGap = mCurrentGap; + mToGap = targetSize; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[mAnimationKind]; + advanceAnimation(); + return true; + } + + @Override + protected boolean interpolate(float progress) { + if (progress >= 1) { + mCurrentGap = mToGap; + return true; + } else { + mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap)); + if (mAnimationKind == ANIM_KIND_CAPTURE) { + float f = CaptureAnimation.calculateScale(progress); + mCurrentGap = (int) (mCurrentGap * f); + return false; + } else { + return (mCurrentGap == mToGap); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // FilmRatio: represents the progress of film mode change. + //////////////////////////////////////////////////////////////////////////// + private class FilmRatio extends Animatable { + // The film ratio: 1 means switching to film mode is complete, 0 means + // switching to page mode is complete. + public float mCurrentRatio, mFromRatio, mToRatio; + + @Override + public boolean startSnapback() { + float target = mFilmMode ? 1f : 0f; + if (target == mToRatio) return false; + return doAnimation(target, ANIM_KIND_SNAPBACK); + } + + // Starts an animation for the film ratio. + private boolean doAnimation(float targetRatio, int kind) { + mAnimationKind = kind; + mFromRatio = mCurrentRatio; + mToRatio = targetRatio; + mAnimationStartTime = AnimationTime.startTime(); + mAnimationDuration = ANIM_TIME[mAnimationKind]; + advanceAnimation(); + return true; + } + + @Override + protected boolean interpolate(float progress) { + if (progress >= 1) { + mCurrentRatio = mToRatio; + return true; + } else { + mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio); + return (mCurrentRatio == mToRatio); + } + } + } +} diff --git a/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java new file mode 100644 index 000000000..ce672f211 --- /dev/null +++ b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java @@ -0,0 +1,85 @@ +package com.android.gallery3d.ui; + +import android.os.ConditionVariable; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.RawTexture; +import com.android.gallery3d.ui.GLRoot.OnGLIdleListener; + +public class PreparePageFadeoutTexture implements OnGLIdleListener { + private static final long TIMEOUT = 200; + public static final String KEY_FADE_TEXTURE = "fade_texture"; + + private RawTexture mTexture; + private ConditionVariable mResultReady = new ConditionVariable(false); + private boolean mCancelled = false; + private GLView mRootPane; + + public PreparePageFadeoutTexture(GLView rootPane) { + if (rootPane == null) { + mCancelled = true; + return; + } + int w = rootPane.getWidth(); + int h = rootPane.getHeight(); + if (w == 0 || h == 0) { + mCancelled = true; + return; + } + mTexture = new RawTexture(w, h, true); + mRootPane = rootPane; + } + + public boolean isCancelled() { + return mCancelled; + } + + public synchronized RawTexture get() { + if (mCancelled) { + return null; + } else if (mResultReady.block(TIMEOUT)) { + return mTexture; + } else { + mCancelled = true; + return null; + } + } + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + if (!mCancelled) { + try { + canvas.beginRenderTarget(mTexture); + mRootPane.render(canvas); + canvas.endRenderTarget(); + } catch (RuntimeException e) { + mTexture = null; + } + } else { + mTexture = null; + } + mResultReady.open(); + return false; + } + + public static void prepareFadeOutTexture(AbstractGalleryActivity activity, + GLView rootPane) { + PreparePageFadeoutTexture task = new PreparePageFadeoutTexture(rootPane); + if (task.isCancelled()) return; + GLRoot root = activity.getGLRoot(); + RawTexture texture = null; + root.unlockRenderThread(); + try { + root.addOnGLIdleListener(task); + texture = task.get(); + } finally { + root.lockRenderThread(); + } + + if (texture == null) { + return; + } + activity.getTransitionStore().put(KEY_FADE_TEXTURE, texture); + } +} diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java new file mode 100644 index 000000000..1b31af278 --- /dev/null +++ b/src/com/android/gallery3d/ui/ProgressSpinner.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; + +import com.android.gallery3d.R; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.ResourceTexture; + +public class ProgressSpinner { + private static float ROTATE_SPEED_OUTER = 1080f / 3500f; + private static float ROTATE_SPEED_INNER = -720f / 3500f; + private final ResourceTexture mOuter; + private final ResourceTexture mInner; + private final int mWidth; + private final int mHeight; + + private float mInnerDegree = 0f; + private float mOuterDegree = 0f; + private long mAnimationTimestamp = -1; + + public ProgressSpinner(Context context) { + mOuter = new ResourceTexture(context, R.drawable.spinner_76_outer_holo); + mInner = new ResourceTexture(context, R.drawable.spinner_76_inner_holo); + + mWidth = Math.max(mOuter.getWidth(), mInner.getWidth()); + mHeight = Math.max(mOuter.getHeight(), mInner.getHeight()); + } + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public void startAnimation() { + mAnimationTimestamp = -1; + mOuterDegree = 0; + mInnerDegree = 0; + } + + public void draw(GLCanvas canvas, int x, int y) { + long now = AnimationTime.get(); + if (mAnimationTimestamp == -1) mAnimationTimestamp = now; + mOuterDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_OUTER; + mInnerDegree += (now - mAnimationTimestamp) * ROTATE_SPEED_INNER; + + mAnimationTimestamp = now; + + // just preventing overflow + if (mOuterDegree > 360) mOuterDegree -= 360f; + if (mInnerDegree < 0) mInnerDegree += 360f; + + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + + canvas.translate(x + mWidth / 2, y + mHeight / 2); + canvas.rotate(mInnerDegree, 0, 0, 1); + mOuter.draw(canvas, -mOuter.getWidth() / 2, -mOuter.getHeight() / 2); + canvas.rotate(mOuterDegree - mInnerDegree, 0, 0, 1); + mInner.draw(canvas, -mInner.getWidth() / 2, -mInner.getHeight() / 2); + canvas.restore(); + } +} diff --git a/src/com/android/gallery3d/ui/RelativePosition.java b/src/com/android/gallery3d/ui/RelativePosition.java new file mode 100644 index 000000000..0f2bfd812 --- /dev/null +++ b/src/com/android/gallery3d/ui/RelativePosition.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +public class RelativePosition { + private float mAbsoluteX; + private float mAbsoluteY; + private float mReferenceX; + private float mReferenceY; + + public void setAbsolutePosition(int absoluteX, int absoluteY) { + mAbsoluteX = absoluteX; + mAbsoluteY = absoluteY; + } + + public void setReferencePosition(int x, int y) { + mReferenceX = x; + mReferenceY = y; + } + + public float getX() { + return mAbsoluteX - mReferenceX; + } + + public float getY() { + return mAbsoluteY - mReferenceY; + } +} diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java new file mode 100644 index 000000000..965bf0b54 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScreenNail.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.ui; + +import android.graphics.RectF; + +import com.android.gallery3d.glrenderer.GLCanvas; + +public interface ScreenNail { + public int getWidth(); + public int getHeight(); + public void draw(GLCanvas canvas, int x, int y, int width, int height); + + // We do not need to draw this ScreenNail in this frame. + public void noDraw(); + + // This ScreenNail will not be used anymore. Release related resources. + public void recycle(); + + // This is only used by TileImageView to back up the tiles not yet loaded. + public void draw(GLCanvas canvas, RectF source, RectF dest); +} diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java new file mode 100644 index 000000000..34fbcef7a --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollBarView.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.util.TypedValue; + +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.NinePatchTexture; + +public class ScrollBarView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "ScrollBarView"; + + private int mBarHeight; + + private int mGripHeight; + private int mGripPosition; // left side of the grip + private int mGripWidth; // zero if the grip is disabled + private int mGivenGripWidth; + + private int mContentPosition; + private int mContentTotal; + + private NinePatchTexture mScrollBarTexture; + + public ScrollBarView(Context context, int gripHeight, int gripWidth) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute( + android.R.attr.scrollbarThumbHorizontal, outValue, true); + mScrollBarTexture = new NinePatchTexture( + context, outValue.resourceId); + mGripPosition = 0; + mGripWidth = 0; + mGivenGripWidth = gripWidth; + mGripHeight = gripHeight; + } + + @Override + protected void onLayout( + boolean changed, int left, int top, int right, int bottom) { + if (!changed) return; + mBarHeight = bottom - top; + } + + // The content position is between 0 to "total". The current position is + // in "position". + public void setContentPosition(int position, int total) { + if (position == mContentPosition && total == mContentTotal) { + return; + } + + invalidate(); + + mContentPosition = position; + mContentTotal = total; + + // If the grip cannot move, don't draw it. + if (mContentTotal <= 0) { + mGripPosition = 0; + mGripWidth = 0; + return; + } + + // Map from the content range to scroll bar range. + // + // mContentTotal --> getWidth() - mGripWidth + // mContentPosition --> mGripPosition + mGripWidth = mGivenGripWidth; + float r = (getWidth() - mGripWidth) / (float) mContentTotal; + mGripPosition = Math.round(r * mContentPosition); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + if (mGripWidth == 0) return; + Rect b = bounds(); + int y = (mBarHeight - mGripHeight) / 2; + mScrollBarTexture.draw(canvas, mGripPosition, y, mGripWidth, mGripHeight); + } +} diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java new file mode 100644 index 000000000..aa68d19d9 --- /dev/null +++ b/src/com/android/gallery3d/ui/ScrollerHelper.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.view.ViewConfiguration; + +import com.android.gallery3d.common.OverScroller; +import com.android.gallery3d.common.Utils; + +public class ScrollerHelper { + private OverScroller mScroller; + private int mOverflingDistance; + private boolean mOverflingEnabled; + + public ScrollerHelper(Context context) { + mScroller = new OverScroller(context); + ViewConfiguration configuration = ViewConfiguration.get(context); + mOverflingDistance = configuration.getScaledOverflingDistance(); + } + + public void setOverfling(boolean enabled) { + mOverflingEnabled = enabled; + } + + /** + * Call this when you want to know the new location. The position will be + * updated and can be obtained by getPosition(). Returns true if the + * animation is not yet finished. + */ + public boolean advanceAnimation(long currentTimeMillis) { + return mScroller.computeScrollOffset(); + } + + public boolean isFinished() { + return mScroller.isFinished(); + } + + public void forceFinished() { + mScroller.forceFinished(true); + } + + public int getPosition() { + return mScroller.getCurrX(); + } + + public float getCurrVelocity() { + return mScroller.getCurrVelocity(); + } + + public void setPosition(int position) { + mScroller.startScroll( + position, 0, // startX, startY + 0, 0, 0); // dx, dy, duration + + // This forces the scroller to reach the final position. + mScroller.abortAnimation(); + } + + public void fling(int velocity, int min, int max) { + int currX = getPosition(); + mScroller.fling( + currX, 0, // startX, startY + velocity, 0, // velocityX, velocityY + min, max, // minX, maxX + 0, 0, // minY, maxY + mOverflingEnabled ? mOverflingDistance : 0, 0); + } + + // Returns the distance that over the scroll limit. + public int startScroll(int distance, int min, int max) { + int currPosition = mScroller.getCurrX(); + int finalPosition = mScroller.isFinished() ? currPosition : + mScroller.getFinalX(); + int newPosition = Utils.clamp(finalPosition + distance, min, max); + if (newPosition != currPosition) { + mScroller.startScroll( + currPosition, 0, // startX, startY + newPosition - currPosition, 0, 0); // dx, dy, duration + } + return finalPosition + distance - newPosition; + } +} diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java new file mode 100644 index 000000000..be6811bc1 --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionManager.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.data.DataManager; +import com.android.gallery3d.data.MediaItem; +import com.android.gallery3d.data.MediaSet; +import com.android.gallery3d.data.Path; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +public class SelectionManager { + @SuppressWarnings("unused") + private static final String TAG = "SelectionManager"; + + public static final int ENTER_SELECTION_MODE = 1; + public static final int LEAVE_SELECTION_MODE = 2; + public static final int SELECT_ALL_MODE = 3; + + private Set<Path> mClickedSet; + private MediaSet mSourceMediaSet; + private SelectionListener mListener; + private DataManager mDataManager; + private boolean mInverseSelection; + private boolean mIsAlbumSet; + private boolean mInSelectionMode; + private boolean mAutoLeave = true; + private int mTotal; + + public interface SelectionListener { + public void onSelectionModeChange(int mode); + public void onSelectionChange(Path path, boolean selected); + } + + public SelectionManager(AbstractGalleryActivity activity, boolean isAlbumSet) { + mDataManager = activity.getDataManager(); + mClickedSet = new HashSet<Path>(); + mIsAlbumSet = isAlbumSet; + mTotal = -1; + } + + // Whether we will leave selection mode automatically once the number of + // selected items is down to zero. + public void setAutoLeaveSelectionMode(boolean enable) { + mAutoLeave = enable; + } + + public void setSelectionListener(SelectionListener listener) { + mListener = listener; + } + + public void selectAll() { + mInverseSelection = true; + mClickedSet.clear(); + enterSelectionMode(); + if (mListener != null) mListener.onSelectionModeChange(SELECT_ALL_MODE); + } + + public void deSelectAll() { + leaveSelectionMode(); + mInverseSelection = false; + mClickedSet.clear(); + } + + public boolean inSelectAllMode() { + return mInverseSelection; + } + + public boolean inSelectionMode() { + return mInSelectionMode; + } + + public void enterSelectionMode() { + if (mInSelectionMode) return; + + mInSelectionMode = true; + if (mListener != null) mListener.onSelectionModeChange(ENTER_SELECTION_MODE); + } + + public void leaveSelectionMode() { + if (!mInSelectionMode) return; + + mInSelectionMode = false; + mInverseSelection = false; + mClickedSet.clear(); + if (mListener != null) mListener.onSelectionModeChange(LEAVE_SELECTION_MODE); + } + + public boolean isItemSelected(Path itemId) { + return mInverseSelection ^ mClickedSet.contains(itemId); + } + + private int getTotalCount() { + if (mSourceMediaSet == null) return -1; + + if (mTotal < 0) { + mTotal = mIsAlbumSet + ? mSourceMediaSet.getSubMediaSetCount() + : mSourceMediaSet.getMediaItemCount(); + } + return mTotal; + } + + public int getSelectedCount() { + int count = mClickedSet.size(); + if (mInverseSelection) { + count = getTotalCount() - count; + } + return count; + } + + public void toggle(Path path) { + if (mClickedSet.contains(path)) { + mClickedSet.remove(path); + } else { + enterSelectionMode(); + mClickedSet.add(path); + } + + // Convert to inverse selection mode if everything is selected. + int count = getSelectedCount(); + if (count == getTotalCount()) { + selectAll(); + } + + if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path)); + if (count == 0 && mAutoLeave) { + leaveSelectionMode(); + } + } + + private static boolean expandMediaSet(ArrayList<Path> items, MediaSet set, int maxSelection) { + int subCount = set.getSubMediaSetCount(); + for (int i = 0; i < subCount; i++) { + if (!expandMediaSet(items, set.getSubMediaSet(i), maxSelection)) { + return false; + } + } + int total = set.getMediaItemCount(); + int batch = 50; + int index = 0; + + while (index < total) { + int count = index + batch < total + ? batch + : total - index; + ArrayList<MediaItem> list = set.getMediaItem(index, count); + if (list != null + && list.size() > (maxSelection - items.size())) { + return false; + } + for (MediaItem item : list) { + items.add(item.getPath()); + } + index += batch; + } + return true; + } + + public ArrayList<Path> getSelected(boolean expandSet) { + return getSelected(expandSet, Integer.MAX_VALUE); + } + + public ArrayList<Path> getSelected(boolean expandSet, int maxSelection) { + ArrayList<Path> selected = new ArrayList<Path>(); + if (mIsAlbumSet) { + if (mInverseSelection) { + int total = getTotalCount(); + for (int i = 0; i < total; i++) { + MediaSet set = mSourceMediaSet.getSubMediaSet(i); + Path id = set.getPath(); + if (!mClickedSet.contains(id)) { + if (expandSet) { + if (!expandMediaSet(selected, set, maxSelection)) { + return null; + } + } else { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + } + } else { + for (Path id : mClickedSet) { + if (expandSet) { + if (!expandMediaSet(selected, mDataManager.getMediaSet(id), + maxSelection)) { + return null; + } + } else { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + } + } else { + if (mInverseSelection) { + int total = getTotalCount(); + int index = 0; + while (index < total) { + int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT); + ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count); + for (MediaItem item : list) { + Path id = item.getPath(); + if (!mClickedSet.contains(id)) { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + index += count; + } + } else { + for (Path id : mClickedSet) { + selected.add(id); + if (selected.size() > maxSelection) { + return null; + } + } + } + } + return selected; + } + + public void setSourceMediaSet(MediaSet set) { + mSourceMediaSet = set; + mTotal = -1; + } +} diff --git a/src/com/android/gallery3d/ui/SelectionMenu.java b/src/com/android/gallery3d/ui/SelectionMenu.java new file mode 100644 index 000000000..5b0828328 --- /dev/null +++ b/src/com/android/gallery3d/ui/SelectionMenu.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; + +import com.android.gallery3d.R; +import com.android.gallery3d.ui.PopupList.OnPopupItemClickListener; + +public class SelectionMenu implements OnClickListener { + @SuppressWarnings("unused") + private static final String TAG = "SelectionMenu"; + + private final Context mContext; + private final Button mButton; + private final PopupList mPopupList; + + public SelectionMenu(Context context, Button button, OnPopupItemClickListener listener) { + mContext = context; + mButton = button; + mPopupList = new PopupList(context, mButton); + mPopupList.addItem(R.id.action_select_all, + context.getString(R.string.select_all)); + mPopupList.setOnPopupItemClickListener(listener); + mButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + mPopupList.show(); + } + + public void updateSelectAllMode(boolean inSelectAllMode) { + PopupList.Item item = mPopupList.findItem(R.id.action_select_all); + if (item != null) { + item.setTitle(mContext.getString( + inSelectAllMode ? R.string.deselect_all : R.string.select_all)); + } + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java new file mode 100644 index 000000000..43784232d --- /dev/null +++ b/src/com/android/gallery3d/ui/SlideshowView.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.PointF; + +import com.android.gallery3d.anim.CanvasAnimation; +import com.android.gallery3d.anim.FloatAnimation; +import com.android.gallery3d.glrenderer.BitmapTexture; +import com.android.gallery3d.glrenderer.GLCanvas; + +import java.util.Random; + +public class SlideshowView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlideshowView"; + + private static final int SLIDESHOW_DURATION = 3500; + private static final int TRANSITION_DURATION = 1000; + + private static final float SCALE_SPEED = 0.20f ; + private static final float MOVE_SPEED = SCALE_SPEED; + + private int mCurrentRotation; + private BitmapTexture mCurrentTexture; + private SlideshowAnimation mCurrentAnimation; + + private int mPrevRotation; + private BitmapTexture mPrevTexture; + private SlideshowAnimation mPrevAnimation; + + private final FloatAnimation mTransitionAnimation = + new FloatAnimation(0, 1, TRANSITION_DURATION); + + private Random mRandom = new Random(); + + public void next(Bitmap bitmap, int rotation) { + + mTransitionAnimation.start(); + + if (mPrevTexture != null) { + mPrevTexture.getBitmap().recycle(); + mPrevTexture.recycle(); + } + + mPrevTexture = mCurrentTexture; + mPrevAnimation = mCurrentAnimation; + mPrevRotation = mCurrentRotation; + + mCurrentRotation = rotation; + mCurrentTexture = new BitmapTexture(bitmap); + if (((rotation / 90) & 0x01) == 0) { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getWidth(), mCurrentTexture.getHeight(), + mRandom); + } else { + mCurrentAnimation = new SlideshowAnimation( + mCurrentTexture.getHeight(), mCurrentTexture.getWidth(), + mRandom); + } + mCurrentAnimation.start(); + + invalidate(); + } + + public void release() { + if (mPrevTexture != null) { + mPrevTexture.recycle(); + mPrevTexture = null; + } + if (mCurrentTexture != null) { + mCurrentTexture.recycle(); + mCurrentTexture = null; + } + } + + @Override + protected void render(GLCanvas canvas) { + long animTime = AnimationTime.get(); + boolean requestRender = mTransitionAnimation.calculate(animTime); + float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get(); + + if (mPrevTexture != null && alpha != 1f) { + requestRender |= mPrevAnimation.calculate(animTime); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(1f - alpha); + mPrevAnimation.apply(canvas); + canvas.rotate(mPrevRotation, 0, 0, 1); + mPrevTexture.draw(canvas, -mPrevTexture.getWidth() / 2, + -mPrevTexture.getHeight() / 2); + canvas.restore(); + } + if (mCurrentTexture != null) { + requestRender |= mCurrentAnimation.calculate(animTime); + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + canvas.setAlpha(alpha); + mCurrentAnimation.apply(canvas); + canvas.rotate(mCurrentRotation, 0, 0, 1); + mCurrentTexture.draw(canvas, -mCurrentTexture.getWidth() / 2, + -mCurrentTexture.getHeight() / 2); + canvas.restore(); + } + if (requestRender) invalidate(); + } + + private class SlideshowAnimation extends CanvasAnimation { + private final int mWidth; + private final int mHeight; + + private final PointF mMovingVector; + private float mProgress; + + public SlideshowAnimation(int width, int height, Random random) { + mWidth = width; + mHeight = height; + mMovingVector = new PointF( + MOVE_SPEED * mWidth * (random.nextFloat() - 0.5f), + MOVE_SPEED * mHeight * (random.nextFloat() - 0.5f)); + setDuration(SLIDESHOW_DURATION); + } + + @Override + public void apply(GLCanvas canvas) { + int viewWidth = getWidth(); + int viewHeight = getHeight(); + + float initScale = Math.min((float) + viewWidth / mWidth, (float) viewHeight / mHeight); + float scale = initScale * (1 + SCALE_SPEED * mProgress); + + float centerX = viewWidth / 2 + mMovingVector.x * mProgress; + float centerY = viewHeight / 2 + mMovingVector.y * mProgress; + + canvas.translate(centerX, centerY); + canvas.scale(scale, scale, 0); + } + + @Override + public int getCanvasSaveFlags() { + return GLCanvas.SAVE_FLAG_MATRIX; + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + } +} diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java new file mode 100644 index 000000000..bd0ffdc15 --- /dev/null +++ b/src/com/android/gallery3d/ui/SlotView.java @@ -0,0 +1,788 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Rect; +import android.os.Handler; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.animation.DecelerateInterpolator; + +import com.android.gallery3d.anim.Animation; +import com.android.gallery3d.app.AbstractGalleryActivity; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.GLCanvas; + +public class SlotView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "SlotView"; + + private static final boolean WIDE = true; + private static final int INDEX_NONE = -1; + + public static final int RENDER_MORE_PASS = 1; + public static final int RENDER_MORE_FRAME = 2; + + public interface Listener { + public void onDown(int index); + public void onUp(boolean followedByLongPress); + public void onSingleTapUp(int index); + public void onLongTap(int index); + public void onScrollPositionChanged(int position, int total); + } + + public static class SimpleListener implements Listener { + @Override public void onDown(int index) {} + @Override public void onUp(boolean followedByLongPress) {} + @Override public void onSingleTapUp(int index) {} + @Override public void onLongTap(int index) {} + @Override public void onScrollPositionChanged(int position, int total) {} + } + + public static interface SlotRenderer { + public void prepareDrawing(); + public void onVisibleRangeChanged(int visibleStart, int visibleEnd); + public void onSlotSizeChanged(int width, int height); + public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height); + } + + private final GestureDetector mGestureDetector; + private final ScrollerHelper mScroller; + private final Paper mPaper = new Paper(); + + private Listener mListener; + private UserInteractionListener mUIListener; + + private boolean mMoreAnimation = false; + private SlotAnimation mAnimation = null; + private final Layout mLayout = new Layout(); + private int mStartIndex = INDEX_NONE; + + // whether the down action happened while the view is scrolling. + private boolean mDownInScrolling; + private int mOverscrollEffect = OVERSCROLL_3D; + private final Handler mHandler; + + private SlotRenderer mRenderer; + + private int[] mRequestRenderSlots = new int[16]; + + public static final int OVERSCROLL_3D = 0; + public static final int OVERSCROLL_SYSTEM = 1; + public static final int OVERSCROLL_NONE = 2; + + // to prevent allocating memory + private final Rect mTempRect = new Rect(); + + public SlotView(AbstractGalleryActivity activity, Spec spec) { + mGestureDetector = new GestureDetector(activity, new MyGestureListener()); + mScroller = new ScrollerHelper(activity); + mHandler = new SynchronizedHandler(activity.getGLRoot()); + setSlotSpec(spec); + } + + public void setSlotRenderer(SlotRenderer slotDrawer) { + mRenderer = slotDrawer; + if (mRenderer != null) { + mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight); + mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd()); + } + } + + public void setCenterIndex(int index) { + int slotCount = mLayout.mSlotCount; + if (index < 0 || index >= slotCount) { + return; + } + Rect rect = mLayout.getSlotRect(index, mTempRect); + int position = WIDE + ? (rect.left + rect.right - getWidth()) / 2 + : (rect.top + rect.bottom - getHeight()) / 2; + setScrollPosition(position); + } + + public void makeSlotVisible(int index) { + Rect rect = mLayout.getSlotRect(index, mTempRect); + int visibleBegin = WIDE ? mScrollX : mScrollY; + int visibleLength = WIDE ? getWidth() : getHeight(); + int visibleEnd = visibleBegin + visibleLength; + int slotBegin = WIDE ? rect.left : rect.top; + int slotEnd = WIDE ? rect.right : rect.bottom; + + int position = visibleBegin; + if (visibleLength < slotEnd - slotBegin) { + position = visibleBegin; + } else if (slotBegin < visibleBegin) { + position = slotBegin; + } else if (slotEnd > visibleEnd) { + position = slotEnd - visibleLength; + } + + setScrollPosition(position); + } + + public void setScrollPosition(int position) { + position = Utils.clamp(position, 0, mLayout.getScrollLimit()); + mScroller.setPosition(position); + updateScrollPosition(position, false); + } + + public void setSlotSpec(Spec spec) { + mLayout.setSlotSpec(spec); + } + + @Override + public void addComponent(GLView view) { + throw new UnsupportedOperationException(); + } + + @Override + protected void onLayout(boolean changeSize, int l, int t, int r, int b) { + if (!changeSize) return; + + // Make sure we are still at a resonable scroll position after the size + // is changed (like orientation change). We choose to keep the center + // visible slot still visible. This is arbitrary but reasonable. + int visibleIndex = + (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2; + mLayout.setSize(r - l, b - t); + makeSlotVisible(visibleIndex); + if (mOverscrollEffect == OVERSCROLL_3D) { + mPaper.setSize(r - l, b - t); + } + } + + public void startScatteringAnimation(RelativePosition position) { + mAnimation = new ScatteringAnimation(position); + mAnimation.start(); + if (mLayout.mSlotCount != 0) invalidate(); + } + + public void startRisingAnimation() { + mAnimation = new RisingAnimation(); + mAnimation.start(); + if (mLayout.mSlotCount != 0) invalidate(); + } + + private void updateScrollPosition(int position, boolean force) { + if (!force && (WIDE ? position == mScrollX : position == mScrollY)) return; + if (WIDE) { + mScrollX = position; + } else { + mScrollY = position; + } + mLayout.setScrollPosition(position); + onScrollPositionChanged(position); + } + + protected void onScrollPositionChanged(int newPosition) { + int limit = mLayout.getScrollLimit(); + mListener.onScrollPositionChanged(newPosition, limit); + } + + public Rect getSlotRect(int slotIndex) { + return mLayout.getSlotRect(slotIndex, new Rect()); + } + + @Override + protected boolean onTouch(MotionEvent event) { + if (mUIListener != null) mUIListener.onUserInteraction(); + mGestureDetector.onTouchEvent(event); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownInScrolling = !mScroller.isFinished(); + mScroller.forceFinished(); + break; + case MotionEvent.ACTION_UP: + mPaper.onRelease(); + invalidate(); + break; + } + return true; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + public void setUserInteractionListener(UserInteractionListener listener) { + mUIListener = listener; + } + + public void setOverscrollEffect(int kind) { + mOverscrollEffect = kind; + mScroller.setOverfling(kind == OVERSCROLL_SYSTEM); + } + + private static int[] expandIntArray(int array[], int capacity) { + while (array.length < capacity) { + array = new int[array.length * 2]; + } + return array; + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + + if (mRenderer == null) return; + mRenderer.prepareDrawing(); + + long animTime = AnimationTime.get(); + boolean more = mScroller.advanceAnimation(animTime); + more |= mLayout.advanceAnimation(animTime); + int oldX = mScrollX; + updateScrollPosition(mScroller.getPosition(), false); + + boolean paperActive = false; + if (mOverscrollEffect == OVERSCROLL_3D) { + // Check if an edge is reached and notify mPaper if so. + int newX = mScrollX; + int limit = mLayout.getScrollLimit(); + if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) { + float v = mScroller.getCurrVelocity(); + if (newX == limit) v = -v; + + // I don't know why, but getCurrVelocity() can return NaN. + if (!Float.isNaN(v)) { + mPaper.edgeReached(v); + } + } + paperActive = mPaper.advanceAnimation(); + } + + more |= paperActive; + + if (mAnimation != null) { + more |= mAnimation.calculate(animTime); + } + + canvas.translate(-mScrollX, -mScrollY); + + int requestCount = 0; + int requestedSlot[] = expandIntArray(mRequestRenderSlots, + mLayout.mVisibleEnd - mLayout.mVisibleStart); + + for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) { + int r = renderItem(canvas, i, 0, paperActive); + if ((r & RENDER_MORE_FRAME) != 0) more = true; + if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i; + } + + for (int pass = 1; requestCount != 0; ++pass) { + int newCount = 0; + for (int i = 0; i < requestCount; ++i) { + int r = renderItem(canvas, + requestedSlot[i], pass, paperActive); + if ((r & RENDER_MORE_FRAME) != 0) more = true; + if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i; + } + requestCount = newCount; + } + + canvas.translate(mScrollX, mScrollY); + + if (more) invalidate(); + + final UserInteractionListener listener = mUIListener; + if (mMoreAnimation && !more && listener != null) { + mHandler.post(new Runnable() { + @Override + public void run() { + listener.onUserInteractionEnd(); + } + }); + } + mMoreAnimation = more; + } + + private int renderItem( + GLCanvas canvas, int index, int pass, boolean paperActive) { + canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX); + Rect rect = mLayout.getSlotRect(index, mTempRect); + if (paperActive) { + canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0); + } else { + canvas.translate(rect.left, rect.top, 0); + } + if (mAnimation != null && mAnimation.isActive()) { + mAnimation.apply(canvas, index, rect); + } + int result = mRenderer.renderSlot( + canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top); + canvas.restore(); + return result; + } + + public static abstract class SlotAnimation extends Animation { + protected float mProgress = 0; + + public SlotAnimation() { + setInterpolator(new DecelerateInterpolator(4)); + setDuration(1500); + } + + @Override + protected void onCalculate(float progress) { + mProgress = progress; + } + + abstract public void apply(GLCanvas canvas, int slotIndex, Rect target); + } + + public static class RisingAnimation extends SlotAnimation { + private static final int RISING_DISTANCE = 128; + + @Override + public void apply(GLCanvas canvas, int slotIndex, Rect target) { + canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress)); + } + } + + public static class ScatteringAnimation extends SlotAnimation { + private int PHOTO_DISTANCE = 1000; + private RelativePosition mCenter; + + public ScatteringAnimation(RelativePosition center) { + mCenter = center; + } + + @Override + public void apply(GLCanvas canvas, int slotIndex, Rect target) { + canvas.translate( + (mCenter.getX() - target.centerX()) * (1 - mProgress), + (mCenter.getY() - target.centerY()) * (1 - mProgress), + slotIndex * PHOTO_DISTANCE * (1 - mProgress)); + canvas.setAlpha(mProgress); + } + } + + // This Spec class is used to specify the size of each slot in the SlotView. + // There are two ways to do it: + // + // (1) Specify slotWidth and slotHeight: they specify the width and height + // of each slot. The number of rows and the gap between slots will be + // determined automatically. + // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number + // of rows in landscape/portrait mode and the gap between slots. The + // width and height of each slot is determined automatically. + // + // The initial value of -1 means they are not specified. + public static class Spec { + public int slotWidth = -1; + public int slotHeight = -1; + public int slotHeightAdditional = 0; + + public int rowsLand = -1; + public int rowsPort = -1; + public int slotGap = -1; + } + + public class Layout { + + private int mVisibleStart; + private int mVisibleEnd; + + private int mSlotCount; + private int mSlotWidth; + private int mSlotHeight; + private int mSlotGap; + + private Spec mSpec; + + private int mWidth; + private int mHeight; + + private int mUnitCount; + private int mContentLength; + private int mScrollPosition; + + private IntegerAnimation mVerticalPadding = new IntegerAnimation(); + private IntegerAnimation mHorizontalPadding = new IntegerAnimation(); + + public void setSlotSpec(Spec spec) { + mSpec = spec; + } + + public boolean setSlotCount(int slotCount) { + if (slotCount == mSlotCount) return false; + if (mSlotCount != 0) { + mHorizontalPadding.setEnabled(true); + mVerticalPadding.setEnabled(true); + } + mSlotCount = slotCount; + int hPadding = mHorizontalPadding.getTarget(); + int vPadding = mVerticalPadding.getTarget(); + initLayoutParameters(); + return vPadding != mVerticalPadding.getTarget() + || hPadding != mHorizontalPadding.getTarget(); + } + + public Rect getSlotRect(int index, Rect rect) { + int col, row; + if (WIDE) { + col = index / mUnitCount; + row = index - col * mUnitCount; + } else { + row = index / mUnitCount; + col = index - row * mUnitCount; + } + + int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap); + int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap); + rect.set(x, y, x + mSlotWidth, y + mSlotHeight); + return rect; + } + + public int getSlotWidth() { + return mSlotWidth; + } + + public int getSlotHeight() { + return mSlotHeight; + } + + // Calculate + // (1) mUnitCount: the number of slots we can fit into one column (or row). + // (2) mContentLength: the width (or height) we need to display all the + // columns (rows). + // (3) padding[]: the vertical and horizontal padding we need in order + // to put the slots towards to the center of the display. + // + // The "major" direction is the direction the user can scroll. The other + // direction is the "minor" direction. + // + // The comments inside this method are the description when the major + // directon is horizontal (X), and the minor directon is vertical (Y). + private void initLayoutParameters( + int majorLength, int minorLength, /* The view width and height */ + int majorUnitSize, int minorUnitSize, /* The slot width and height */ + int[] padding) { + int unitCount = (minorLength + mSlotGap) / (minorUnitSize + mSlotGap); + if (unitCount == 0) unitCount = 1; + mUnitCount = unitCount; + + // We put extra padding above and below the column. + int availableUnits = Math.min(mUnitCount, mSlotCount); + int usedMinorLength = availableUnits * minorUnitSize + + (availableUnits - 1) * mSlotGap; + padding[0] = (minorLength - usedMinorLength) / 2; + + // Then calculate how many columns we need for all slots. + int count = ((mSlotCount + mUnitCount - 1) / mUnitCount); + mContentLength = count * majorUnitSize + (count - 1) * mSlotGap; + + // If the content length is less then the screen width, put + // extra padding in left and right. + padding[1] = Math.max(0, (majorLength - mContentLength) / 2); + } + + private void initLayoutParameters() { + // Initialize mSlotWidth and mSlotHeight from mSpec + if (mSpec.slotWidth != -1) { + mSlotGap = 0; + mSlotWidth = mSpec.slotWidth; + mSlotHeight = mSpec.slotHeight; + } else { + int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort; + mSlotGap = mSpec.slotGap; + mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows); + mSlotWidth = mSlotHeight - mSpec.slotHeightAdditional; + } + + if (mRenderer != null) { + mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight); + } + + int[] padding = new int[2]; + if (WIDE) { + initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding); + mVerticalPadding.startAnimateTo(padding[0]); + mHorizontalPadding.startAnimateTo(padding[1]); + } else { + initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding); + mVerticalPadding.startAnimateTo(padding[1]); + mHorizontalPadding.startAnimateTo(padding[0]); + } + updateVisibleSlotRange(); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + initLayoutParameters(); + } + + private void updateVisibleSlotRange() { + int position = mScrollPosition; + + if (WIDE) { + int startCol = position / (mSlotWidth + mSlotGap); + int start = Math.max(0, mUnitCount * startCol); + int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) / + (mSlotWidth + mSlotGap); + int end = Math.min(mSlotCount, mUnitCount * endCol); + setVisibleRange(start, end); + } else { + int startRow = position / (mSlotHeight + mSlotGap); + int start = Math.max(0, mUnitCount * startRow); + int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) / + (mSlotHeight + mSlotGap); + int end = Math.min(mSlotCount, mUnitCount * endRow); + setVisibleRange(start, end); + } + } + + public void setScrollPosition(int position) { + if (mScrollPosition == position) return; + mScrollPosition = position; + updateVisibleSlotRange(); + } + + private void setVisibleRange(int start, int end) { + if (start == mVisibleStart && end == mVisibleEnd) return; + if (start < end) { + mVisibleStart = start; + mVisibleEnd = end; + } else { + mVisibleStart = mVisibleEnd = 0; + } + if (mRenderer != null) { + mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd); + } + } + + public int getVisibleStart() { + return mVisibleStart; + } + + public int getVisibleEnd() { + return mVisibleEnd; + } + + public int getSlotIndexByPosition(float x, float y) { + int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0); + int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition); + + absoluteX -= mHorizontalPadding.get(); + absoluteY -= mVerticalPadding.get(); + + if (absoluteX < 0 || absoluteY < 0) { + return INDEX_NONE; + } + + int columnIdx = absoluteX / (mSlotWidth + mSlotGap); + int rowIdx = absoluteY / (mSlotHeight + mSlotGap); + + if (!WIDE && columnIdx >= mUnitCount) { + return INDEX_NONE; + } + + if (WIDE && rowIdx >= mUnitCount) { + return INDEX_NONE; + } + + if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) { + return INDEX_NONE; + } + + if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) { + return INDEX_NONE; + } + + int index = WIDE + ? (columnIdx * mUnitCount + rowIdx) + : (rowIdx * mUnitCount + columnIdx); + + return index >= mSlotCount ? INDEX_NONE : index; + } + + public int getScrollLimit() { + int limit = WIDE ? mContentLength - mWidth : mContentLength - mHeight; + return limit <= 0 ? 0 : limit; + } + + public boolean advanceAnimation(long animTime) { + // use '|' to make sure both sides will be executed + return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime); + } + } + + private class MyGestureListener implements GestureDetector.OnGestureListener { + private boolean isDown; + + // We call the listener's onDown() when our onShowPress() is called and + // call the listener's onUp() when we receive any further event. + @Override + public void onShowPress(MotionEvent e) { + GLRoot root = getGLRoot(); + root.lockRenderThread(); + try { + if (isDown) return; + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) { + isDown = true; + mListener.onDown(index); + } + } finally { + root.unlockRenderThread(); + } + } + + private void cancelDown(boolean byLongPress) { + if (!isDown) return; + isDown = false; + mListener.onUp(byLongPress); + } + + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public boolean onFling(MotionEvent e1, + MotionEvent e2, float velocityX, float velocityY) { + cancelDown(false); + int scrollLimit = mLayout.getScrollLimit(); + if (scrollLimit == 0) return false; + float velocity = WIDE ? velocityX : velocityY; + mScroller.fling((int) -velocity, 0, scrollLimit); + if (mUIListener != null) mUIListener.onUserInteractionBegin(); + invalidate(); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, + MotionEvent e2, float distanceX, float distanceY) { + cancelDown(false); + float distance = WIDE ? distanceX : distanceY; + int overDistance = mScroller.startScroll( + Math.round(distance), 0, mLayout.getScrollLimit()); + if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) { + mPaper.overScroll(overDistance); + } + invalidate(); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + cancelDown(false); + if (mDownInScrolling) return true; + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onSingleTapUp(index); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + cancelDown(true); + if (mDownInScrolling) return; + lockRendering(); + try { + int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY()); + if (index != INDEX_NONE) mListener.onLongTap(index); + } finally { + unlockRendering(); + } + } + } + + public void setStartIndex(int index) { + mStartIndex = index; + } + + // Return true if the layout parameters have been changed + public boolean setSlotCount(int slotCount) { + boolean changed = mLayout.setSlotCount(slotCount); + + // mStartIndex is applied the first time setSlotCount is called. + if (mStartIndex != INDEX_NONE) { + setCenterIndex(mStartIndex); + mStartIndex = INDEX_NONE; + } + // Reset the scroll position to avoid scrolling over the updated limit. + setScrollPosition(WIDE ? mScrollX : mScrollY); + return changed; + } + + public int getVisibleStart() { + return mLayout.getVisibleStart(); + } + + public int getVisibleEnd() { + return mLayout.getVisibleEnd(); + } + + public int getScrollX() { + return mScrollX; + } + + public int getScrollY() { + return mScrollY; + } + + public Rect getSlotRect(int slotIndex, GLView rootPane) { + // Get slot rectangle relative to this root pane. + Rect offset = new Rect(); + rootPane.getBoundsOf(this, offset); + Rect r = getSlotRect(slotIndex); + r.offset(offset.left - getScrollX(), + offset.top - getScrollY()); + return r; + } + + private static class IntegerAnimation extends Animation { + private int mTarget; + private int mCurrent = 0; + private int mFrom = 0; + private boolean mEnabled = false; + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + public void startAnimateTo(int target) { + if (!mEnabled) { + mTarget = mCurrent = target; + return; + } + if (target == mTarget) return; + + mFrom = mCurrent; + mTarget = target; + setDuration(180); + start(); + } + + public int get() { + return mCurrent; + } + + public int getTarget() { + return mTarget; + } + + @Override + protected void onCalculate(float progress) { + mCurrent = Math.round(mFrom + progress * (mTarget - mFrom)); + if (progress == 1f) mEnabled = false; + } + } +} diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java new file mode 100644 index 000000000..18121e63b --- /dev/null +++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.annotation.TargetApi; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.glrenderer.ExtTexture; +import com.android.gallery3d.glrenderer.GLCanvas; + +@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) +public abstract class SurfaceTextureScreenNail implements ScreenNail, + SurfaceTexture.OnFrameAvailableListener { + @SuppressWarnings("unused") + private static final String TAG = "SurfaceTextureScreenNail"; + // This constant is not available in API level before 15, but it was just an + // oversight. + private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65; + + protected ExtTexture mExtTexture; + private SurfaceTexture mSurfaceTexture; + private int mWidth, mHeight; + private float[] mTransform = new float[16]; + private boolean mHasTexture = false; + + public SurfaceTextureScreenNail() { + } + + public void acquireSurfaceTexture(GLCanvas canvas) { + mExtTexture = new ExtTexture(canvas, GL_TEXTURE_EXTERNAL_OES); + mExtTexture.setSize(mWidth, mHeight); + mSurfaceTexture = new SurfaceTexture(mExtTexture.getId()); + setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight); + mSurfaceTexture.setOnFrameAvailableListener(this); + synchronized (this) { + mHasTexture = true; + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + private static void setDefaultBufferSize(SurfaceTexture st, int width, int height) { + if (ApiHelper.HAS_SET_DEFALT_BUFFER_SIZE) { + st.setDefaultBufferSize(width, height); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + private static void releaseSurfaceTexture(SurfaceTexture st) { + st.setOnFrameAvailableListener(null); + if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) { + st.release(); + } + } + + public SurfaceTexture getSurfaceTexture() { + return mSurfaceTexture; + } + + public void releaseSurfaceTexture() { + synchronized (this) { + mHasTexture = false; + } + mExtTexture.recycle(); + mExtTexture = null; + releaseSurfaceTexture(mSurfaceTexture); + mSurfaceTexture = null; + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public void resizeTexture() { + if (mExtTexture != null) { + mExtTexture.setSize(mWidth, mHeight); + setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight); + } + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + synchronized (this) { + if (!mHasTexture) return; + mSurfaceTexture.updateTexImage(); + mSurfaceTexture.getTransformMatrix(mTransform); + + // Flip vertically. + canvas.save(GLCanvas.SAVE_FLAG_MATRIX); + int cx = x + width / 2; + int cy = y + height / 2; + canvas.translate(cx, cy); + canvas.scale(1, -1, 1); + canvas.translate(-cx, -cy); + updateTransformMatrix(mTransform); + canvas.drawTexture(mExtTexture, mTransform, x, y, width, height); + canvas.restore(); + } + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF dest) { + throw new UnsupportedOperationException(); + } + + protected void updateTransformMatrix(float[] matrix) {} + + @Override + abstract public void noDraw(); + + @Override + abstract public void recycle(); + + @Override + abstract public void onFrameAvailable(SurfaceTexture surfaceTexture); +} diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java new file mode 100644 index 000000000..ba1035747 --- /dev/null +++ b/src/com/android/gallery3d/ui/SynchronizedHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.os.Handler; +import android.os.Message; + +import com.android.gallery3d.common.Utils; + +public class SynchronizedHandler extends Handler { + + private final GLRoot mRoot; + + public SynchronizedHandler(GLRoot root) { + mRoot = Utils.checkNotNull(root); + } + + @Override + public void dispatchMessage(Message message) { + mRoot.lockRenderThread(); + try { + super.dispatchMessage(message); + } finally { + mRoot.unlockRenderThread(); + } + } +} diff --git a/src/com/android/gallery3d/ui/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java new file mode 100644 index 000000000..3185c7598 --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageView.java @@ -0,0 +1,786 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.v4.util.LongSparseArray; +import android.util.DisplayMetrics; +import android.util.FloatMath; +import android.view.WindowManager; + +import com.android.gallery3d.app.GalleryContext; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.data.DecodeUtils; +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.UploadedTexture; +import com.android.gallery3d.util.Future; +import com.android.gallery3d.util.ThreadPool; +import com.android.gallery3d.util.ThreadPool.CancelListener; +import com.android.gallery3d.util.ThreadPool.JobContext; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class TileImageView extends GLView { + public static final int SIZE_UNKNOWN = -1; + + @SuppressWarnings("unused") + private static final String TAG = "TileImageView"; + private static final int UPLOAD_LIMIT = 1; + + // TILE_SIZE must be 2^N + private static int sTileSize; + + /* + * This is the tile state in the CPU side. + * Life of a Tile: + * ACTIVATED (initial state) + * --> IN_QUEUE - by queueForDecode() + * --> RECYCLED - by recycleTile() + * IN_QUEUE --> DECODING - by decodeTile() + * --> RECYCLED - by recycleTile) + * DECODING --> RECYCLING - by recycleTile() + * --> DECODED - by decodeTile() + * --> DECODE_FAIL - by decodeTile() + * RECYCLING --> RECYCLED - by decodeTile() + * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded) + * DECODED --> RECYCLED - by recycleTile() + * DECODE_FAIL -> RECYCLED - by recycleTile() + * RECYCLED --> ACTIVATED - by obtainTile() + */ + private static final int STATE_ACTIVATED = 0x01; + private static final int STATE_IN_QUEUE = 0x02; + private static final int STATE_DECODING = 0x04; + private static final int STATE_DECODED = 0x08; + private static final int STATE_DECODE_FAIL = 0x10; + private static final int STATE_RECYCLING = 0x20; + private static final int STATE_RECYCLED = 0x40; + + private TileSource mModel; + private ScreenNail mScreenNail; + protected int mLevelCount; // cache the value of mScaledBitmaps.length + + // The mLevel variable indicates which level of bitmap we should use. + // Level 0 means the original full-sized bitmap, and a larger value means + // a smaller scaled bitmap (The width and height of each scaled bitmap is + // half size of the previous one). If the value is in [0, mLevelCount), we + // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value + // is mLevelCount, and that means we use mScreenNail for display. + private int mLevel = 0; + + // The offsets of the (left, top) of the upper-left tile to the (left, top) + // of the view. + private int mOffsetX; + private int mOffsetY; + + private int mUploadQuota; + private boolean mRenderComplete; + + private final RectF mSourceRect = new RectF(); + private final RectF mTargetRect = new RectF(); + + private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>(); + + // The following three queue is guarded by TileImageView.this + private final TileQueue mRecycledQueue = new TileQueue(); + private final TileQueue mUploadQueue = new TileQueue(); + private final TileQueue mDecodeQueue = new TileQueue(); + + // The width and height of the full-sized bitmap + protected int mImageWidth = SIZE_UNKNOWN; + protected int mImageHeight = SIZE_UNKNOWN; + + protected int mCenterX; + protected int mCenterY; + protected float mScale; + protected int mRotation; + + // Temp variables to avoid memory allocation + private final Rect mTileRange = new Rect(); + private final Rect mActiveRange[] = {new Rect(), new Rect()}; + + private final TileUploader mTileUploader = new TileUploader(); + private boolean mIsTextureFreed; + private Future<Void> mTileDecoder; + private final ThreadPool mThreadPool; + private boolean mBackgroundTileUploaded; + + public static interface TileSource { + public int getLevelCount(); + public ScreenNail getScreenNail(); + public int getImageWidth(); + public int getImageHeight(); + + // The tile returned by this method can be specified this way: Assuming + // the image size is (width, height), first take the intersection of (0, + // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If + // in extending the region, we found some part of the region are outside + // the image, those pixels are filled with black. + // + // If level > 0, it does the same operation on a down-scaled version of + // the original image (down-scaled by a factor of 2^level), but (x, y) + // still refers to the coordinate on the original image. + // + // The method would be called in another thread. + public Bitmap getTile(int level, int x, int y, int tileSize); + } + + public static boolean isHighResolution(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + return metrics.heightPixels > 2048 || metrics.widthPixels > 2048; + } + + public TileImageView(GalleryContext context) { + mThreadPool = context.getThreadPool(); + mTileDecoder = mThreadPool.submit(new TileDecoder()); + if (sTileSize == 0) { + if (isHighResolution(context.getAndroidContext())) { + sTileSize = 512 ; + } else { + sTileSize = 256; + } + } + } + + public void setModel(TileSource model) { + mModel = model; + if (model != null) notifyModelInvalidated(); + } + + public void setScreenNail(ScreenNail s) { + mScreenNail = s; + } + + public void notifyModelInvalidated() { + invalidateTiles(); + if (mModel == null) { + mScreenNail = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + } else { + setScreenNail(mModel.getScreenNail()); + mImageWidth = mModel.getImageWidth(); + mImageHeight = mModel.getImageHeight(); + mLevelCount = mModel.getLevelCount(); + } + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + invalidate(); + } + + @Override + protected void onLayout( + boolean changeSize, int left, int top, int right, int bottom) { + super.onLayout(changeSize, left, top, right, bottom); + if (changeSize) layoutTiles(mCenterX, mCenterY, mScale, mRotation); + } + + // Prepare the tiles we want to use for display. + // + // 1. Decide the tile level we want to use for display. + // 2. Decide the tile levels we want to keep as texture (in addition to + // the one we use for display). + // 3. Recycle unused tiles. + // 4. Activate the tiles we want. + private void layoutTiles(int centerX, int centerY, float scale, int rotation) { + // The width and height of this view. + int width = getWidth(); + int height = getHeight(); + + // The tile levels we want to keep as texture is in the range + // [fromLevel, endLevel). + int fromLevel; + int endLevel; + + // We want to use a texture larger than or equal to the display size. + mLevel = Utils.clamp(Utils.floorLog2(1f / scale), 0, mLevelCount); + + // We want to keep one more tile level as texture in addition to what + // we use for display. So it can be faster when the scale moves to the + // next level. We choose a level closer to the current scale. + if (mLevel != mLevelCount) { + Rect range = mTileRange; + getRange(range, centerX, centerY, mLevel, scale, rotation); + mOffsetX = Math.round(width / 2f + (range.left - centerX) * scale); + mOffsetY = Math.round(height / 2f + (range.top - centerY) * scale); + fromLevel = scale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel; + } else { + // Activate the tiles of the smallest two levels. + fromLevel = mLevel - 2; + mOffsetX = Math.round(width / 2f - centerX * scale); + mOffsetY = Math.round(height / 2f - centerY * scale); + } + + fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2)); + endLevel = Math.min(fromLevel + 2, mLevelCount); + + Rect range[] = mActiveRange; + for (int i = fromLevel; i < endLevel; ++i) { + getRange(range[i - fromLevel], centerX, centerY, i, rotation); + } + + // If rotation is transient, don't update the tile. + if (rotation % 90 != 0) return; + + synchronized (this) { + mDecodeQueue.clean(); + mUploadQueue.clean(); + mBackgroundTileUploaded = false; + + // Recycle unused tiles: if the level of the active tile is outside the + // range [fromLevel, endLevel) or not in the visible range. + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + int level = tile.mTileLevel; + if (level < fromLevel || level >= endLevel + || !range[level - fromLevel].contains(tile.mX, tile.mY)) { + mActiveTiles.removeAt(i); + i--; + n--; + recycleTile(tile); + } + } + } + + for (int i = fromLevel; i < endLevel; ++i) { + int size = sTileSize << i; + Rect r = range[i - fromLevel]; + for (int y = r.top, bottom = r.bottom; y < bottom; y += size) { + for (int x = r.left, right = r.right; x < right; x += size) { + activateTile(x, y, i); + } + } + } + invalidate(); + } + + protected synchronized void invalidateTiles() { + mDecodeQueue.clean(); + mUploadQueue.clean(); + + // TODO disable decoder + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + recycleTile(tile); + } + mActiveTiles.clear(); + } + + private void getRange(Rect out, int cX, int cY, int level, int rotation) { + getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation); + } + + // If the bitmap is scaled by the given factor "scale", return the + // rectangle containing visible range. The left-top coordinate returned is + // aligned to the tile boundary. + // + // (cX, cY) is the point on the original bitmap which will be put in the + // center of the ImageViewer. + private void getRange(Rect out, + int cX, int cY, int level, float scale, int rotation) { + + double radians = Math.toRadians(-rotation); + double w = getWidth(); + double h = getHeight(); + + double cos = Math.cos(radians); + double sin = Math.sin(radians); + int width = (int) Math.ceil(Math.max( + Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h))); + int height = (int) Math.ceil(Math.max( + Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h))); + + int left = (int) FloatMath.floor(cX - width / (2f * scale)); + int top = (int) FloatMath.floor(cY - height / (2f * scale)); + int right = (int) FloatMath.ceil(left + width / scale); + int bottom = (int) FloatMath.ceil(top + height / scale); + + // align the rectangle to tile boundary + int size = sTileSize << level; + left = Math.max(0, size * (left / size)); + top = Math.max(0, size * (top / size)); + right = Math.min(mImageWidth, right); + bottom = Math.min(mImageHeight, bottom); + + out.set(left, top, right, bottom); + } + + // Calculate where the center of the image is, in the view coordinates. + public void getImageCenter(Point center) { + // The width and height of this view. + int viewW = getWidth(); + int viewH = getHeight(); + + // The distance between the center of the view to the center of the + // bitmap, in bitmap units. (mCenterX and mCenterY are the bitmap + // coordinates correspond to the center of view) + int distW, distH; + if (mRotation % 180 == 0) { + distW = mImageWidth / 2 - mCenterX; + distH = mImageHeight / 2 - mCenterY; + } else { + distW = mImageHeight / 2 - mCenterY; + distH = mImageWidth / 2 - mCenterX; + } + + // Convert to view coordinates. mScale translates from bitmap units to + // view units. + center.x = Math.round(viewW / 2f + distW * mScale); + center.y = Math.round(viewH / 2f + distH * mScale); + } + + public boolean setPosition(int centerX, int centerY, float scale, int rotation) { + if (mCenterX == centerX && mCenterY == centerY + && mScale == scale && mRotation == rotation) return false; + mCenterX = centerX; + mCenterY = centerY; + mScale = scale; + mRotation = rotation; + layoutTiles(centerX, centerY, scale, rotation); + invalidate(); + return true; + } + + public void freeTextures() { + mIsTextureFreed = true; + + if (mTileDecoder != null) { + mTileDecoder.cancel(); + mTileDecoder.get(); + mTileDecoder = null; + } + + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile texture = mActiveTiles.valueAt(i); + texture.recycle(); + } + mActiveTiles.clear(); + mTileRange.set(0, 0, 0, 0); + + synchronized (this) { + mUploadQueue.clean(); + mDecodeQueue.clean(); + Tile tile = mRecycledQueue.pop(); + while (tile != null) { + tile.recycle(); + tile = mRecycledQueue.pop(); + } + } + setScreenNail(null); + } + + public void prepareTextures() { + if (mTileDecoder == null) { + mTileDecoder = mThreadPool.submit(new TileDecoder()); + } + if (mIsTextureFreed) { + layoutTiles(mCenterX, mCenterY, mScale, mRotation); + mIsTextureFreed = false; + setScreenNail(mModel == null ? null : mModel.getScreenNail()); + } + } + + @Override + protected void render(GLCanvas canvas) { + mUploadQuota = UPLOAD_LIMIT; + mRenderComplete = true; + + int level = mLevel; + int rotation = mRotation; + int flags = 0; + if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX; + + if (flags != 0) { + canvas.save(flags); + if (rotation != 0) { + int centerX = getWidth() / 2, centerY = getHeight() / 2; + canvas.translate(centerX, centerY); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-centerX, -centerY); + } + } + try { + if (level != mLevelCount && !isScreenNailAnimating()) { + if (mScreenNail != null) { + mScreenNail.noDraw(); + } + + int size = (sTileSize << level); + float length = size * mScale; + Rect r = mTileRange; + + for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) { + float y = mOffsetY + i * length; + for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) { + float x = mOffsetX + j * length; + drawTile(canvas, tx, ty, level, x, y, length); + } + } + } else if (mScreenNail != null) { + mScreenNail.draw(canvas, mOffsetX, mOffsetY, + Math.round(mImageWidth * mScale), + Math.round(mImageHeight * mScale)); + if (isScreenNailAnimating()) { + invalidate(); + } + } + } finally { + if (flags != 0) canvas.restore(); + } + + if (mRenderComplete) { + if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas); + } else { + invalidate(); + } + } + + private boolean isScreenNailAnimating() { + return (mScreenNail instanceof TiledScreenNail) + && ((TiledScreenNail) mScreenNail).isAnimating(); + } + + private void uploadBackgroundTiles(GLCanvas canvas) { + mBackgroundTileUploaded = true; + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + if (!tile.isContentValid()) queueForDecode(tile); + } + } + + void queueForUpload(Tile tile) { + synchronized (this) { + mUploadQueue.push(tile); + } + if (mTileUploader.mActive.compareAndSet(false, true)) { + getGLRoot().addOnGLIdleListener(mTileUploader); + } + } + + synchronized void queueForDecode(Tile tile) { + if (tile.mTileState == STATE_ACTIVATED) { + tile.mTileState = STATE_IN_QUEUE; + if (mDecodeQueue.push(tile)) notifyAll(); + } + } + + boolean decodeTile(Tile tile) { + synchronized (this) { + if (tile.mTileState != STATE_IN_QUEUE) return false; + tile.mTileState = STATE_DECODING; + } + boolean decodeComplete = tile.decode(); + synchronized (this) { + if (tile.mTileState == STATE_RECYCLING) { + tile.mTileState = STATE_RECYCLED; + if (tile.mDecodedTile != null) { + GalleryBitmapPool.getInstance().put(tile.mDecodedTile); + tile.mDecodedTile = null; + } + mRecycledQueue.push(tile); + return false; + } + tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL; + return decodeComplete; + } + } + + private synchronized Tile obtainTile(int x, int y, int level) { + Tile tile = mRecycledQueue.pop(); + if (tile != null) { + tile.mTileState = STATE_ACTIVATED; + tile.update(x, y, level); + return tile; + } + return new Tile(x, y, level); + } + + synchronized void recycleTile(Tile tile) { + if (tile.mTileState == STATE_DECODING) { + tile.mTileState = STATE_RECYCLING; + return; + } + tile.mTileState = STATE_RECYCLED; + if (tile.mDecodedTile != null) { + GalleryBitmapPool.getInstance().put(tile.mDecodedTile); + tile.mDecodedTile = null; + } + mRecycledQueue.push(tile); + } + + private void activateTile(int x, int y, int level) { + long key = makeTileKey(x, y, level); + Tile tile = mActiveTiles.get(key); + if (tile != null) { + if (tile.mTileState == STATE_IN_QUEUE) { + tile.mTileState = STATE_ACTIVATED; + } + return; + } + tile = obtainTile(x, y, level); + mActiveTiles.put(key, tile); + } + + private Tile getTile(int x, int y, int level) { + return mActiveTiles.get(makeTileKey(x, y, level)); + } + + private static long makeTileKey(int x, int y, int level) { + long result = x; + result = (result << 16) | y; + result = (result << 16) | level; + return result; + } + + private class TileUploader implements GLRoot.OnGLIdleListener { + AtomicBoolean mActive = new AtomicBoolean(false); + + @Override + public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) { + // Skips uploading if there is a pending rendering request. + // Returns true to keep uploading in next rendering loop. + if (renderRequested) return true; + int quota = UPLOAD_LIMIT; + Tile tile = null; + while (quota > 0) { + synchronized (TileImageView.this) { + tile = mUploadQueue.pop(); + } + if (tile == null) break; + if (!tile.isContentValid()) { + boolean hasBeenLoaded = tile.isLoaded(); + Utils.assertTrue(tile.mTileState == STATE_DECODED); + tile.updateContent(canvas); + if (!hasBeenLoaded) tile.draw(canvas, 0, 0); + --quota; + } + } + if (tile == null) mActive.set(false); + return tile != null; + } + } + + // Draw the tile to a square at canvas that locates at (x, y) and + // has a side length of length. + public void drawTile(GLCanvas canvas, + int tx, int ty, int level, float x, float y, float length) { + RectF source = mSourceRect; + RectF target = mTargetRect; + target.set(x, y, x + length, y + length); + source.set(0, 0, sTileSize, sTileSize); + + Tile tile = getTile(tx, ty, level); + if (tile != null) { + if (!tile.isContentValid()) { + if (tile.mTileState == STATE_DECODED) { + if (mUploadQuota > 0) { + --mUploadQuota; + tile.updateContent(canvas); + } else { + mRenderComplete = false; + } + } else if (tile.mTileState != STATE_DECODE_FAIL){ + mRenderComplete = false; + queueForDecode(tile); + } + } + if (drawTile(tile, canvas, source, target)) return; + } + if (mScreenNail != null) { + int size = sTileSize << level; + float scaleX = (float) mScreenNail.getWidth() / mImageWidth; + float scaleY = (float) mScreenNail.getHeight() / mImageHeight; + source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX, + (ty + size) * scaleY); + mScreenNail.draw(canvas, source, target); + } + } + + static boolean drawTile( + Tile tile, GLCanvas canvas, RectF source, RectF target) { + while (true) { + if (tile.isContentValid()) { + canvas.drawTexture(tile, source, target); + return true; + } + + // Parent can be divided to four quads and tile is one of the four. + Tile parent = tile.getParentTile(); + if (parent == null) return false; + if (tile.mX == parent.mX) { + source.left /= 2f; + source.right /= 2f; + } else { + source.left = (sTileSize + source.left) / 2f; + source.right = (sTileSize + source.right) / 2f; + } + if (tile.mY == parent.mY) { + source.top /= 2f; + source.bottom /= 2f; + } else { + source.top = (sTileSize + source.top) / 2f; + source.bottom = (sTileSize + source.bottom) / 2f; + } + tile = parent; + } + } + + private class Tile extends UploadedTexture { + public int mX; + public int mY; + public int mTileLevel; + public Tile mNext; + public Bitmap mDecodedTile; + public volatile int mTileState = STATE_ACTIVATED; + + public Tile(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + GalleryBitmapPool.getInstance().put(bitmap); + } + + boolean decode() { + // Get a tile from the original image. The tile is down-scaled + // by (1 << mTilelevel) from a region in the original image. + try { + mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile( + mTileLevel, mX, mY, sTileSize)); + } catch (Throwable t) { + Log.w(TAG, "fail to decode tile", t); + } + return mDecodedTile != null; + } + + @Override + protected Bitmap onGetBitmap() { + Utils.assertTrue(mTileState == STATE_DECODED); + + // We need to override the width and height, so that we won't + // draw beyond the boundaries. + int rightEdge = ((mImageWidth - mX) >> mTileLevel); + int bottomEdge = ((mImageHeight - mY) >> mTileLevel); + setSize(Math.min(sTileSize, rightEdge), Math.min(sTileSize, bottomEdge)); + + Bitmap bitmap = mDecodedTile; + mDecodedTile = null; + mTileState = STATE_ACTIVATED; + return bitmap; + } + + // We override getTextureWidth() and getTextureHeight() here, so the + // texture can be re-used for different tiles regardless of the actual + // size of the tile (which may be small because it is a tile at the + // boundary). + @Override + public int getTextureWidth() { + return sTileSize; + } + + @Override + public int getTextureHeight() { + return sTileSize; + } + + public void update(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + invalidateContent(); + } + + public Tile getParentTile() { + if (mTileLevel + 1 == mLevelCount) return null; + int size = sTileSize << (mTileLevel + 1); + int x = size * (mX / size); + int y = size * (mY / size); + return getTile(x, y, mTileLevel + 1); + } + + @Override + public String toString() { + return String.format("tile(%s, %s, %s / %s)", + mX / sTileSize, mY / sTileSize, mLevel, mLevelCount); + } + } + + private static class TileQueue { + private Tile mHead; + + public Tile pop() { + Tile tile = mHead; + if (tile != null) mHead = tile.mNext; + return tile; + } + + public boolean push(Tile tile) { + boolean wasEmpty = mHead == null; + tile.mNext = mHead; + mHead = tile; + return wasEmpty; + } + + public void clean() { + mHead = null; + } + } + + private class TileDecoder implements ThreadPool.Job<Void> { + + private CancelListener mNotifier = new CancelListener() { + @Override + public void onCancel() { + synchronized (TileImageView.this) { + TileImageView.this.notifyAll(); + } + } + }; + + @Override + public Void run(JobContext jc) { + jc.setMode(ThreadPool.MODE_NONE); + jc.setCancelListener(mNotifier); + while (!jc.isCancelled()) { + Tile tile = null; + synchronized(TileImageView.this) { + tile = mDecodeQueue.pop(); + if (tile == null && !jc.isCancelled()) { + Utils.waitWithoutInterrupt(TileImageView.this); + } + } + if (tile == null) continue; + if (decodeTile(tile)) queueForUpload(tile); + } + return null; + } + } +} diff --git a/src/com/android/gallery3d/ui/TileImageViewAdapter.java b/src/com/android/gallery3d/ui/TileImageViewAdapter.java new file mode 100644 index 000000000..0c1f66d0c --- /dev/null +++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Rect; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.common.Utils; +import com.android.photos.data.GalleryBitmapPool; + +public class TileImageViewAdapter implements TileImageView.TileSource { + private static final String TAG = "TileImageViewAdapter"; + protected ScreenNail mScreenNail; + protected boolean mOwnScreenNail; + protected BitmapRegionDecoder mRegionDecoder; + protected int mImageWidth; + protected int mImageHeight; + protected int mLevelCount; + + public TileImageViewAdapter() { + } + + public synchronized void clear() { + mScreenNail = null; + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + mRegionDecoder = null; + } + + // Caller is responsible to recycle the ScreenNail + public synchronized void setScreenNail( + ScreenNail screenNail, int width, int height) { + Utils.checkNotNull(screenNail); + mScreenNail = screenNail; + mImageWidth = width; + mImageHeight = height; + mRegionDecoder = null; + mLevelCount = 0; + } + + public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) { + mRegionDecoder = Utils.checkNotNull(decoder); + mImageWidth = decoder.getWidth(); + mImageHeight = decoder.getHeight(); + mLevelCount = calculateLevelCount(); + } + + private int calculateLevelCount() { + return Math.max(0, Utils.ceilLog2( + (float) mImageWidth / mScreenNail.getWidth())); + } + + // Gets a sub image on a rectangle of the current photo. For example, + // getTile(1, 50, 50, 100, 3, pool) means to get the region located + // at (50, 50) with sample level 1 (ie, down sampled by 2^1) and the + // target tile size (after sampling) 100 with border 3. + // + // From this spec, we can infer the actual tile size to be + // 100 + 3x2 = 106, and the size of the region to be extracted from the + // photo to be 200 with border 6. + // + // As a result, we should decode region (50-6, 50-6, 250+6, 250+6) or + // (44, 44, 256, 256) from the original photo and down sample it to 106. + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public Bitmap getTile(int level, int x, int y, int tileSize) { + if (!ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER) { + return getTileWithoutReusingBitmap(level, x, y, tileSize); + } + + int t = tileSize << level; + + Rect wantRegion = new Rect(x, y, x + t, y + t); + + boolean needClear; + BitmapRegionDecoder regionDecoder = null; + + synchronized (this) { + regionDecoder = mRegionDecoder; + if (regionDecoder == null) return null; + + // We need to clear a reused bitmap, if wantRegion is not fully + // within the image. + needClear = !new Rect(0, 0, mImageWidth, mImageHeight) + .contains(wantRegion); + } + + Bitmap bitmap = GalleryBitmapPool.getInstance().get(tileSize, tileSize); + if (bitmap != null) { + if (needClear) bitmap.eraseColor(0); + } else { + bitmap = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inPreferQualityOverSpeed = true; + options.inSampleSize = (1 << level); + options.inBitmap = bitmap; + + try { + // In CropImage, we may call the decodeRegion() concurrently. + synchronized (regionDecoder) { + bitmap = regionDecoder.decodeRegion(wantRegion, options); + } + } finally { + if (options.inBitmap != bitmap && options.inBitmap != null) { + GalleryBitmapPool.getInstance().put(options.inBitmap); + options.inBitmap = null; + } + } + + if (bitmap == null) { + Log.w(TAG, "fail in decoding region"); + } + return bitmap; + } + + private Bitmap getTileWithoutReusingBitmap( + int level, int x, int y, int tileSize) { + int t = tileSize << level; + Rect wantRegion = new Rect(x, y, x + t, y + t); + + BitmapRegionDecoder regionDecoder; + Rect overlapRegion; + + synchronized (this) { + regionDecoder = mRegionDecoder; + if (regionDecoder == null) return null; + overlapRegion = new Rect(0, 0, mImageWidth, mImageHeight); + Utils.assertTrue(overlapRegion.intersect(wantRegion)); + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inPreferredConfig = Config.ARGB_8888; + options.inPreferQualityOverSpeed = true; + options.inSampleSize = (1 << level); + Bitmap bitmap = null; + + // In CropImage, we may call the decodeRegion() concurrently. + synchronized (regionDecoder) { + bitmap = regionDecoder.decodeRegion(overlapRegion, options); + } + + if (bitmap == null) { + Log.w(TAG, "fail in decoding region"); + } + + if (wantRegion.equals(overlapRegion)) return bitmap; + + Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); + Canvas canvas = new Canvas(result); + canvas.drawBitmap(bitmap, + (overlapRegion.left - wantRegion.left) >> level, + (overlapRegion.top - wantRegion.top) >> level, null); + return result; + } + + + @Override + public ScreenNail getScreenNail() { + return mScreenNail; + } + + @Override + public int getImageHeight() { + return mImageHeight; + } + + @Override + public int getImageWidth() { + return mImageWidth; + } + + @Override + public int getLevelCount() { + return mLevelCount; + } +} diff --git a/src/com/android/gallery3d/ui/TiledScreenNail.java b/src/com/android/gallery3d/ui/TiledScreenNail.java new file mode 100644 index 000000000..860e230bb --- /dev/null +++ b/src/com/android/gallery3d/ui/TiledScreenNail.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.graphics.Bitmap; +import android.graphics.RectF; + +import com.android.gallery3d.common.Utils; +import com.android.photos.data.GalleryBitmapPool; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.TiledTexture; + +// This is a ScreenNail wraps a Bitmap. There are some extra functions: +// +// - If we need to draw before the bitmap is available, we draw a rectange of +// placeholder color (gray). +// +// - When the the bitmap is available, and we have drawn the placeholder color +// before, we will do a fade-in animation. +public class TiledScreenNail implements ScreenNail { + @SuppressWarnings("unused") + private static final String TAG = "TiledScreenNail"; + + // The duration of the fading animation in milliseconds + private static final int DURATION = 180; + + private static int sMaxSide = 640; + + // These are special values for mAnimationStartTime + private static final long ANIMATION_NOT_NEEDED = -1; + private static final long ANIMATION_NEEDED = -2; + private static final long ANIMATION_DONE = -3; + + private int mWidth; + private int mHeight; + private long mAnimationStartTime = ANIMATION_NOT_NEEDED; + + private Bitmap mBitmap; + private TiledTexture mTexture; + + public TiledScreenNail(Bitmap bitmap) { + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + mBitmap = bitmap; + mTexture = new TiledTexture(bitmap); + } + + public TiledScreenNail(int width, int height) { + setSize(width, height); + } + + // This gets overridden by bitmap_screennail_placeholder + // in GalleryUtils.initialize + private static int mPlaceholderColor = 0xFF222222; + private static boolean mDrawPlaceholder = true; + + public static void setPlaceholderColor(int color) { + mPlaceholderColor = color; + } + + private void setSize(int width, int height) { + if (width == 0 || height == 0) { + width = sMaxSide; + height = sMaxSide * 3 / 4; + } + float scale = Math.min(1, (float) sMaxSide / Math.max(width, height)); + mWidth = Math.round(scale * width); + mHeight = Math.round(scale * height); + } + + // Combines the two ScreenNails. + // Returns the used one and recycle the unused one. + public ScreenNail combine(ScreenNail other) { + if (other == null) { + return this; + } + + if (!(other instanceof TiledScreenNail)) { + recycle(); + return other; + } + + // Now both are TiledScreenNail. Move over the information about width, + // height, and Bitmap, then recycle the other. + TiledScreenNail newer = (TiledScreenNail) other; + mWidth = newer.mWidth; + mHeight = newer.mHeight; + if (newer.mTexture != null) { + if (mBitmap != null) GalleryBitmapPool.getInstance().put(mBitmap); + if (mTexture != null) mTexture.recycle(); + mBitmap = newer.mBitmap; + mTexture = newer.mTexture; + newer.mBitmap = null; + newer.mTexture = null; + } + newer.recycle(); + return this; + } + + public void updatePlaceholderSize(int width, int height) { + if (mBitmap != null) return; + if (width == 0 || height == 0) return; + setSize(width, height); + } + + @Override + public int getWidth() { + return mWidth; + } + + @Override + public int getHeight() { + return mHeight; + } + + @Override + public void noDraw() { + } + + @Override + public void recycle() { + if (mTexture != null) { + mTexture.recycle(); + mTexture = null; + } + if (mBitmap != null) { + GalleryBitmapPool.getInstance().put(mBitmap); + mBitmap = null; + } + } + + public static void disableDrawPlaceholder() { + mDrawPlaceholder = false; + } + + public static void enableDrawPlaceholder() { + mDrawPlaceholder = true; + } + + @Override + public void draw(GLCanvas canvas, int x, int y, int width, int height) { + if (mTexture == null || !mTexture.isReady()) { + if (mAnimationStartTime == ANIMATION_NOT_NEEDED) { + mAnimationStartTime = ANIMATION_NEEDED; + } + if(mDrawPlaceholder) { + canvas.fillRect(x, y, width, height, mPlaceholderColor); + } + return; + } + + if (mAnimationStartTime == ANIMATION_NEEDED) { + mAnimationStartTime = AnimationTime.get(); + } + + if (isAnimating()) { + mTexture.drawMixed(canvas, mPlaceholderColor, getRatio(), x, y, + width, height); + } else { + mTexture.draw(canvas, x, y, width, height); + } + } + + @Override + public void draw(GLCanvas canvas, RectF source, RectF dest) { + if (mTexture == null || !mTexture.isReady()) { + canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(), + mPlaceholderColor); + return; + } + + mTexture.draw(canvas, source, dest); + } + + public boolean isAnimating() { + // The TiledTexture may not be uploaded completely yet. + // In that case, we count it as animating state and we will draw + // the placeholder in TileImageView. + if (mTexture == null || !mTexture.isReady()) return true; + if (mAnimationStartTime < 0) return false; + if (AnimationTime.get() - mAnimationStartTime >= DURATION) { + mAnimationStartTime = ANIMATION_DONE; + return false; + } + return true; + } + + private float getRatio() { + float r = (float) (AnimationTime.get() - mAnimationStartTime) / DURATION; + return Utils.clamp(1.0f - r, 0.0f, 1.0f); + } + + public boolean isShowingPlaceholder() { + return (mBitmap == null) || isAnimating(); + } + + public TiledTexture getTexture() { + return mTexture; + } + + public static void setMaxSide(int size) { + sMaxSide = size; + } +} diff --git a/src/com/android/gallery3d/ui/UndoBarView.java b/src/com/android/gallery3d/ui/UndoBarView.java new file mode 100644 index 000000000..42f12ae72 --- /dev/null +++ b/src/com/android/gallery3d/ui/UndoBarView.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.content.Context; +import android.view.MotionEvent; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.NinePatchTexture; +import com.android.gallery3d.glrenderer.ResourceTexture; +import com.android.gallery3d.glrenderer.StringTexture; +import com.android.gallery3d.util.GalleryUtils; + +public class UndoBarView extends GLView { + @SuppressWarnings("unused") + private static final String TAG = "UndoBarView"; + + private static final int WHITE = 0xFFFFFFFF; + private static final int GRAY = 0xFFAAAAAA; + + private final NinePatchTexture mPanel; + private final StringTexture mUndoText; + private final StringTexture mDeletedText; + private final ResourceTexture mUndoIcon; + private final int mBarHeight; + private final int mBarMargin; + private final int mUndoTextMargin; + private final int mIconSize; + private final int mIconMargin; + private final int mSeparatorTopMargin; + private final int mSeparatorBottomMargin; + private final int mSeparatorRightMargin; + private final int mSeparatorWidth; + private final int mDeletedTextMargin; + private final int mClickRegion; + + private OnClickListener mOnClickListener; + private boolean mDownOnButton; + + // This is the layout of UndoBarView. The unit is dp. + // + // +-+----+----------------+-+--+----+-+------+--+-+ + // 48 | | | Deleted | | | <- | | UNDO | | | + // +-+----+----------------+-+--+----+-+------+--+-+ + // 4 16 1 12 32 8 16 4 + public UndoBarView(Context context) { + mBarHeight = GalleryUtils.dpToPixel(48); + mBarMargin = GalleryUtils.dpToPixel(4); + mUndoTextMargin = GalleryUtils.dpToPixel(16); + mIconMargin = GalleryUtils.dpToPixel(8); + mIconSize = GalleryUtils.dpToPixel(32); + mSeparatorRightMargin = GalleryUtils.dpToPixel(12); + mSeparatorTopMargin = GalleryUtils.dpToPixel(10); + mSeparatorBottomMargin = GalleryUtils.dpToPixel(10); + mSeparatorWidth = GalleryUtils.dpToPixel(1); + mDeletedTextMargin = GalleryUtils.dpToPixel(16); + + mPanel = new NinePatchTexture(context, R.drawable.panel_undo_holo); + mUndoText = StringTexture.newInstance(context.getString(R.string.undo), + GalleryUtils.dpToPixel(12), GRAY, 0, true); + mDeletedText = StringTexture.newInstance( + context.getString(R.string.deleted), + GalleryUtils.dpToPixel(16), WHITE); + mUndoIcon = new ResourceTexture( + context, R.drawable.ic_menu_revert_holo_dark); + mClickRegion = mBarMargin + mUndoTextMargin + mUndoText.getWidth() + + mIconMargin + mIconSize + mSeparatorRightMargin; + } + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + setMeasuredSize(0 /* unused */, mBarHeight); + } + + @Override + protected void render(GLCanvas canvas) { + super.render(canvas); + advanceAnimation(); + + canvas.save(GLCanvas.SAVE_FLAG_ALPHA); + canvas.multiplyAlpha(mAlpha); + + int w = getWidth(); + int h = getHeight(); + mPanel.draw(canvas, mBarMargin, 0, w - mBarMargin * 2, mBarHeight); + + int x = w - mBarMargin; + int y; + + x -= mUndoTextMargin + mUndoText.getWidth(); + y = (mBarHeight - mUndoText.getHeight()) / 2; + mUndoText.draw(canvas, x, y); + + x -= mIconMargin + mIconSize; + y = (mBarHeight - mIconSize) / 2; + mUndoIcon.draw(canvas, x, y, mIconSize, mIconSize); + + x -= mSeparatorRightMargin + mSeparatorWidth; + y = mSeparatorTopMargin; + canvas.fillRect(x, y, mSeparatorWidth, + mBarHeight - mSeparatorTopMargin - mSeparatorBottomMargin, GRAY); + + x = mBarMargin + mDeletedTextMargin; + y = (mBarHeight - mDeletedText.getHeight()) / 2; + mDeletedText.draw(canvas, x, y); + + canvas.restore(); + } + + @Override + protected boolean onTouch(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mDownOnButton = inUndoButton(event); + break; + case MotionEvent.ACTION_UP: + if (mDownOnButton) { + if (mOnClickListener != null && inUndoButton(event)) { + mOnClickListener.onClick(this); + } + mDownOnButton = false; + } + break; + case MotionEvent.ACTION_CANCEL: + mDownOnButton = false; + break; + } + return true; + } + + // Check if the event is on the right of the separator + private boolean inUndoButton(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + int w = getWidth(); + int h = getHeight(); + return (x >= w - mClickRegion && x < w && y >= 0 && y < h); + } + + //////////////////////////////////////////////////////////////////////////// + // Alpha Animation + //////////////////////////////////////////////////////////////////////////// + + private static final long NO_ANIMATION = -1; + private static long ANIM_TIME = 200; + private long mAnimationStartTime = NO_ANIMATION; + private float mFromAlpha, mToAlpha; + private float mAlpha; + + private static float getTargetAlpha(int visibility) { + return (visibility == VISIBLE) ? 1f : 0f; + } + + @Override + public void setVisibility(int visibility) { + mAlpha = getTargetAlpha(visibility); + mAnimationStartTime = NO_ANIMATION; + super.setVisibility(visibility); + invalidate(); + } + + public void animateVisibility(int visibility) { + float target = getTargetAlpha(visibility); + if (mAnimationStartTime == NO_ANIMATION && mAlpha == target) return; + if (mAnimationStartTime != NO_ANIMATION && mToAlpha == target) return; + + mFromAlpha = mAlpha; + mToAlpha = target; + mAnimationStartTime = AnimationTime.startTime(); + + super.setVisibility(VISIBLE); + invalidate(); + } + + private void advanceAnimation() { + if (mAnimationStartTime == NO_ANIMATION) return; + + float delta = (float) (AnimationTime.get() - mAnimationStartTime) / + ANIM_TIME; + mAlpha = mFromAlpha + ((mToAlpha > mFromAlpha) ? delta : -delta); + mAlpha = Utils.clamp(mAlpha, 0f, 1f); + + if (mAlpha == mToAlpha) { + mAnimationStartTime = NO_ANIMATION; + if (mAlpha == 0) { + super.setVisibility(INVISIBLE); + } + } + invalidate(); + } +} diff --git a/src/com/android/gallery3d/ui/UserInteractionListener.java b/src/com/android/gallery3d/ui/UserInteractionListener.java new file mode 100644 index 000000000..bc4a71800 --- /dev/null +++ b/src/com/android/gallery3d/ui/UserInteractionListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +public interface UserInteractionListener { + // Called when a user interaction begins (for example, fling). + public void onUserInteractionBegin(); + // Called when the user interaction ends. + public void onUserInteractionEnd(); + // Other one-shot user interactions. + public void onUserInteraction(); +} diff --git a/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java new file mode 100644 index 000000000..ee61d8edb --- /dev/null +++ b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.ui; + +import android.app.Activity; +import android.content.Context; +import android.os.PowerManager; + +import com.android.gallery3d.app.AbstractGalleryActivity; + +public class WakeLockHoldingProgressListener implements MenuExecutor.ProgressListener { + static private final String DEFAULT_WAKE_LOCK_LABEL = "Gallery Progress Listener"; + private AbstractGalleryActivity mActivity; + private PowerManager.WakeLock mWakeLock; + + public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity) { + this(galleryActivity, DEFAULT_WAKE_LOCK_LABEL); + } + + public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity, String label) { + mActivity = galleryActivity; + PowerManager pm = + (PowerManager) ((Activity) mActivity).getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, label); + } + + @Override + public void onProgressComplete(int result) { + mWakeLock.release(); + } + + @Override + public void onProgressStart() { + mWakeLock.acquire(); + } + + protected AbstractGalleryActivity getActivity() { + return mActivity; + } + + @Override + public void onProgressUpdate(int index) { + } + + @Override + public void onConfirmDialogDismissed(boolean confirmed) { + } + + @Override + public void onConfirmDialogShown() { + } +} |