diff options
-rw-r--r-- | AndroidManifest.xml | 4 | ||||
-rw-r--r-- | res/drawable/photopage_bottom_button_background.xml | 5 | ||||
-rw-r--r-- | res/layout/camera_filmstrip.xml | 14 | ||||
-rw-r--r-- | res/layout/filmstrip_bottom_controls.xml | 68 | ||||
-rw-r--r-- | src/com/android/camera/CameraActivity.java | 20 | ||||
-rw-r--r-- | src/com/android/camera/VideoModule.java | 1 | ||||
-rw-r--r-- | src/com/android/camera/data/LocalData.java | 6 | ||||
-rw-r--r-- | src/com/android/camera/data/LocalMediaData.java | 15 | ||||
-rw-r--r-- | src/com/android/camera/data/SimpleViewData.java | 10 | ||||
-rw-r--r-- | src/com/android/camera/ui/FilmStripView.java | 173 | ||||
-rw-r--r-- | src/com/android/camera/ui/FilmstripBottomControls.java | 140 | ||||
-rw-r--r-- | src/com/android/camera/ui/ZoomView.java | 551 |
12 files changed, 915 insertions, 92 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b80cd2703..346971790 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -71,6 +71,10 @@ <action android:name="android.media.action.VIDEO_CAPTURE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> <meta-data android:name="com.android.keyguard.layout" diff --git a/res/drawable/photopage_bottom_button_background.xml b/res/drawable/photopage_bottom_button_background.xml new file mode 100644 index 000000000..0c772ad21 --- /dev/null +++ b/res/drawable/photopage_bottom_button_background.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@android:color/holo_blue_light" android:state_pressed="true"/> + <item android:drawable="@color/button_dark_transparent_background" android:state_selected="false"/> +</selector> diff --git a/res/layout/camera_filmstrip.xml b/res/layout/camera_filmstrip.xml index d94a9d2a8..4281aac3d 100644 --- a/res/layout/camera_filmstrip.xml +++ b/res/layout/camera_filmstrip.xml @@ -24,19 +24,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - <ImageButton - android:id="@+id/filmstrip_bottom_control_panorama" - android:layout_width="70dp" - android:layout_height="70dp" - android:layout_gravity="bottom|center_horizontal" - android:background="@drawable/transparent_button_background" - android:clickable="true" - android:paddingBottom="5dp" - android:paddingLeft="5dp" - android:paddingRight="5dp" - android:paddingTop="5dp" - android:visibility="gone" - android:src="@drawable/ic_view_photosphere" /> + <include layout="@layout/filmstrip_bottom_controls" /> <LinearLayout android:id="@+id/pano_stitching_progress_panel" diff --git a/res/layout/filmstrip_bottom_controls.xml b/res/layout/filmstrip_bottom_controls.xml new file mode 100644 index 000000000..14f0ee9fd --- /dev/null +++ b/res/layout/filmstrip_bottom_controls.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2013 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. +--> + +<com.android.camera.ui.FilmstripBottomControls + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/filmstrip_bottom_controls" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:orientation="horizontal" + android:padding="10dp" + android:visibility="visible" > + + <ImageButton + android:id="@+id/filmstrip_bottom_control_edit" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:background="@drawable/photopage_bottom_button_background" + android:paddingBottom="5dp" + android:paddingLeft="15dp" + android:paddingRight="15dp" + android:paddingTop="5dp" + android:src="@drawable/ic_menu_edit_holo_dark" + android:visibility="gone" /> + + <ImageButton + android:id="@+id/filmstrip_bottom_control_panorama" + android:layout_width="70dp" + android:layout_height="70dp" + android:layout_alignParentBottom="true" + android:layout_centerHorizontal="true" + android:background="@drawable/transparent_button_background" + android:clickable="true" + android:src="@drawable/ic_view_photosphere" + android:visibility="gone" /> + + <ImageButton + android:id="@+id/filmstrip_bottom_control_tiny_planet" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:background="@drawable/photopage_bottom_button_background" + android:paddingBottom="5dp" + android:paddingLeft="15dp" + android:paddingRight="15dp" + android:paddingTop="5dp" + android:src="@drawable/ic_menu_tiny_planet" + android:visibility="gone" /> + +</com.android.camera.ui.FilmstripBottomControls>
\ No newline at end of file diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java index e300b5d08..b58262b71 100644 --- a/src/com/android/camera/CameraActivity.java +++ b/src/com/android/camera/CameraActivity.java @@ -57,14 +57,14 @@ import com.android.camera.data.LocalData; import com.android.camera.data.LocalDataAdapter; import com.android.camera.data.MediaDetails; import com.android.camera.data.SimpleViewData; -import com.android.camera.util.ApiHelper; import com.android.camera.ui.CameraSwitcher; import com.android.camera.ui.CameraSwitcher.CameraSwitchListener; import com.android.camera.ui.DetailsDialog; import com.android.camera.ui.FilmStripView; +import com.android.camera.util.ApiHelper; import com.android.camera.util.CameraUtil; -import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper; import com.android.camera.util.PhotoSphereHelper; +import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper; import com.android.camera2.R; public class CameraActivity extends Activity @@ -235,12 +235,13 @@ public class CameraActivity extends Activity } @Override - public void onToggleActionBarVisibility() { + public boolean onToggleActionBarVisibility() { if (mActionBar.isShowing()) { mActionBar.hide(); } else { mActionBar.show(); } + return mActionBar.isShowing(); } }; @@ -439,7 +440,7 @@ public class CameraActivity extends Activity // TODO: add the functionality. return true; case R.id.action_edit: - // TODO: add the functionality. + launchEditor(localData); return true; case R.id.action_trim: // This is going to be handled by the Gallery app. @@ -628,7 +629,6 @@ public class CameraActivity extends Activity @Override public void onStart() { super.onStart(); - mPanoramaViewHelper.onStart(); } @@ -776,6 +776,16 @@ public class CameraActivity extends Activity } } + /** + * Launches an ACTION_EDIT intent for the given local data item. + */ + public void launchEditor(LocalData data) { + Intent intent = new Intent(Intent.ACTION_EDIT) + .setDataAndType(data.getContentUri(), data.getMimeType()) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(intent, null)); + } + private void openModule(CameraModule module) { module.init(this, mRootView); module.onResumeBeforeSuper(); diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java index 0a6faba6a..9f5ca08e9 100644 --- a/src/com/android/camera/VideoModule.java +++ b/src/com/android/camera/VideoModule.java @@ -2211,6 +2211,7 @@ public class VideoModule implements CameraModule, // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture(); // Disable all camera controls. mSwitchingCamera = true; + switchCamera(); } diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java index e25e93327..82567120e 100644 --- a/src/com/android/camera/data/LocalData.java +++ b/src/com/android/camera/data/LocalData.java @@ -103,6 +103,12 @@ public interface LocalData extends FilmStripView.ImageData { Uri getContentUri(); /** + * @return The mimetype of this data item, or null, if this item has no + * mimetype associated with it. + */ + String getMimeType(); + + /** * Return media data (such as EXIF) for the item. */ MediaDetails getMediaDetails(Context context); diff --git a/src/com/android/camera/data/LocalMediaData.java b/src/com/android/camera/data/LocalMediaData.java index 131c7e7db..7a63ddf17 100644 --- a/src/com/android/camera/data/LocalMediaData.java +++ b/src/com/android/camera/data/LocalMediaData.java @@ -211,6 +211,11 @@ public abstract class LocalMediaData implements LocalData { } @Override + public String getMimeType() { + return mimeType; + } + + @Override public abstract int getViewType(); protected abstract BitmapLoadTask getBitmapLoadTask( @@ -392,6 +397,11 @@ public abstract class LocalMediaData implements LocalData { } @Override + public boolean isPhoto() { + return true; + } + + @Override protected BitmapLoadTask getBitmapLoadTask( ImageView v, int decodeWidth, int decodeHeight) { return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight); @@ -616,6 +626,11 @@ public abstract class LocalMediaData implements LocalData { } @Override + public boolean isPhoto() { + return false; + } + + @Override protected BitmapLoadTask getBitmapLoadTask( ImageView v, int decodeWidth, int decodeHeight) { return new VideoBitmapLoadTask(v); diff --git a/src/com/android/camera/data/SimpleViewData.java b/src/com/android/camera/data/SimpleViewData.java index 59d5f2cd5..06ff3501b 100644 --- a/src/com/android/camera/data/SimpleViewData.java +++ b/src/com/android/camera/data/SimpleViewData.java @@ -155,4 +155,14 @@ public class SimpleViewData implements LocalData { public double[] getLatLong() { return null; } + + @Override + public boolean isPhoto() { + return false; + } + + @Override + public String getMimeType() { + return null; + } } diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java index 40f46b650..7b5505769 100644 --- a/src/com/android/camera/ui/FilmStripView.java +++ b/src/com/android/camera/ui/FilmStripView.java @@ -29,14 +29,17 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; -import android.widget.ImageButton; import android.widget.Scroller; +import com.android.camera.CameraActivity; +import com.android.camera.data.LocalData; import com.android.camera.ui.FilmStripView.ImageData.PanoramaSupportCallback; +import com.android.camera.ui.FilmstripBottomControls.BottomControlsListener; +import com.android.camera.util.CameraUtil; import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper; import com.android.camera2.R; -public class FilmStripView extends ViewGroup { +public class FilmStripView extends ViewGroup implements BottomControlsListener { @SuppressWarnings("unused") private static final String TAG = "CAM_FilmStripView"; @@ -47,7 +50,7 @@ public class FilmStripView extends ViewGroup { // Only check for intercepting touch events within first 500ms private static final int SWIPE_TIME_OUT = 500; - private Context mContext; + private CameraActivity mActivity; private FilmStripGestureRecognizer mGestureRecognizer; private DataAdapter mDataAdapter; private int mViewGap; @@ -67,7 +70,7 @@ public class FilmStripView extends ViewGroup { private int mSlop; private TimeInterpolator mViewAnimInterpolator; - private ImageButton mViewPhotoSphereButton; + private FilmstripBottomControls mBottomControls; private PanoramaViewHelper mPanoramaViewHelper; private long mLastItemId = -1; @@ -115,10 +118,10 @@ public class FilmStripView extends ViewGroup { /** * SIZE_FULL can be returned by {@link ImageData#getWidth()} and - * {@link ImageData#getHeight()}. - * When SIZE_FULL is returned for width/height, it means the the - * width or height will be disregarded when deciding the view size - * of this ImageData, just use full screen size. + * {@link ImageData#getHeight()}. When SIZE_FULL is returned for + * width/height, it means the the width or height will be disregarded + * when deciding the view size of this ImageData, just use full screen + * size. */ public static final int SIZE_FULL = -2; @@ -155,15 +158,15 @@ public class FilmStripView extends ViewGroup { * Checks if the UI action is supported. * * @param action The UI actions to check. - * @return {@code false} if at least one of the actions is not - * supported. {@code true} otherwise. + * @return {@code false} if at least one of the actions is not + * supported. {@code true} otherwise. */ public boolean isUIActionSupported(int action); /** * Gives the data a hint when its view is going to be displayed. - * {@code FilmStripView} should always call this function before - * showing its corresponding view every time. + * {@code FilmStripView} should always call this function before showing + * its corresponding view every time. */ public void prepare(); @@ -186,6 +189,9 @@ public class FilmStripView extends ViewGroup { * the viewer. */ public void viewPhotoSphere(PanoramaViewHelper helper); + + /** Whether this item is a photo. */ + public boolean isPhoto(); } /** @@ -194,8 +200,8 @@ public class FilmStripView extends ViewGroup { */ public interface DataAdapter { /** - * An interface which defines the update report used to return to - * the {@link com.android.camera.ui.FilmStripView.Listener}. + * An interface which defines the update report used to return to the + * {@link com.android.camera.ui.FilmStripView.Listener}. */ public interface UpdateReporter { /** Checks if the data of dataID is removed. */ @@ -229,10 +235,10 @@ public class FilmStripView extends ViewGroup { /** * Returns the view to visually present the image data. * - * @param context The {@link Context} to create the view. - * @param dataID The ID of the image data to be presented. - * @return The view representing the image data. Null if - * unavailable or the {@code dataID} is out of range. + * @param context The {@link Context} to create the view. + * @param dataID The ID of the image data to be presented. + * @return The view representing the image data. Null if unavailable or + * the {@code dataID} is out of range. */ public View getView(Context context, int dataID); @@ -240,13 +246,13 @@ public class FilmStripView extends ViewGroup { * Returns the {@link ImageData} specified by the ID. * * @param dataID The ID of the {@link ImageData}. - * @return The specified {@link ImageData}. Null if not available. + * @return The specified {@link ImageData}. Null if not available. */ public ImageData getImageData(int dataID); /** - * Suggests the data adapter the maximum possible size of the layout - * so the {@link DataAdapter} can optimize the view returned for the + * Suggests the data adapter the maximum possible size of the layout so + * the {@link DataAdapter} can optimize the view returned for the * {@link ImageData}. * * @param w Maximum width. @@ -266,8 +272,8 @@ public class FilmStripView extends ViewGroup { * gesture when in full-screen. * * @param dataID The ID of the data. - * @return {@code true} if the view can be moved, - * {@code false} otherwise. + * @return {@code true} if the view can be moved, {@code false} + * otherwise. */ public boolean canSwipeInFullScreen(int dataID); } @@ -291,12 +297,12 @@ public class FilmStripView extends ViewGroup { public void onDataDemoted(int dataID); /** - * The callback when the item enters/leaves full-screen. - * TODO: Call this function actually. + * The callback when the item enters/leaves full-screen. TODO: Call this + * function actually. * - * @param dataID The ID of the image data. - * @param fullScreen {@code true} if the data is entering full-screen. - * {@code false} otherwise. + * @param dataID The ID of the image data. + * @param fullScreen {@code true} if the data is entering full-screen. + * {@code false} otherwise. */ public void onDataFullScreenChange(int dataID, boolean fullScreen); @@ -304,23 +310,25 @@ public class FilmStripView extends ViewGroup { * Callback when entering/leaving camera mode. * * @param toCamera {@code true} if entering camera mode. Otherwise, - * {@code false} + * {@code false} */ public void onSwitchMode(boolean toCamera); /** * The callback when the item is centered/off-centered. * - * @param dataID The ID of the image data. - * @param current {@code true} if the data is the current one. - * {@code false} otherwise. + * @param dataID The ID of the image data. + * @param current {@code true} if the data is the current one. + * {@code false} otherwise. */ public void onCurrentDataChanged(int dataID, boolean current); /** * Toggles the visibility of the ActionBar. + * + * @return The ActionBar visibility after the toggle. */ - public void onToggleActionBarVisibility(); + public boolean onToggleActionBarVisibility(); } /** @@ -362,6 +370,7 @@ public class FilmStripView extends ViewGroup { /** * Constructor. + * * @param id The id of the data from {@link DataAdapter}. * @param v The {@code View} representing the data. */ @@ -434,12 +443,12 @@ public class FilmStripView extends ViewGroup { } /** - * Layouts the view in the area assuming the center of the area is at - * a specific point of the whole filmstrip. + * Layouts the view in the area assuming the center of the area is at a + * specific point of the whole filmstrip. * * @param drawArea The area when filmstrip will show in. * @param refCenter The absolute X coordination in the whole filmstrip - * of the center of {@code drawArea}. + * of the center of {@code drawArea}. * @param scale The current scale of the filmstrip. */ public void layoutIn(Rect drawArea, int refCenter, float scale) { @@ -463,37 +472,36 @@ public class FilmStripView extends ViewGroup { } } - /** Constructor. */ public FilmStripView(Context context) { super(context); - init(context); + init((CameraActivity) context); } /** Constructor. */ public FilmStripView(Context context, AttributeSet attrs) { super(context, attrs); - init(context); + init((CameraActivity) context); } /** Constructor. */ public FilmStripView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - init(context); + init((CameraActivity) context); } - private void init(Context context) { + private void init(CameraActivity cameraActivity) { // This is for positioning camera controller at the same place in // different orientations. setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); setWillNotDraw(false); - mContext = context; + mActivity = cameraActivity; mScale = 1.0f; - mController = new MyController(context); + mController = new MyController(cameraActivity); mViewAnimInterpolator = new DecelerateInterpolator(); mGestureRecognizer = - new FilmStripGestureRecognizer(context, new MyGestureReceiver()); + new FilmStripGestureRecognizer(cameraActivity, new MyGestureReceiver()); mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop); } @@ -595,7 +603,9 @@ public class FilmStripView extends ViewGroup { } for (ViewInfo info : mViewInfo) { - if (info == null) continue; + if (info == null) { + continue; + } int id = info.getID(); int[] dim = calculateChildDimension( @@ -613,14 +623,13 @@ public class FilmStripView extends ViewGroup { @Override protected boolean fitSystemWindows(Rect insets) { - if (mViewPhotoSphereButton != null) { + if (mBottomControls != null) { // Set the position of the "View Photo Sphere" button. - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mViewPhotoSphereButton + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mBottomControls .getLayoutParams(); params.bottomMargin = insets.bottom; - mViewPhotoSphereButton.setLayoutParams(params); + mBottomControls.setLayoutParams(params); } - return super.fitSystemWindows(insets); } @@ -659,7 +668,7 @@ public class FilmStripView extends ViewGroup { return null; } data.prepare(); - View v = mDataAdapter.getView(mContext, dataID); + View v = mDataAdapter.getView(mActivity, dataID); if (v == null) { return null; } @@ -769,13 +778,28 @@ public class FilmStripView extends ViewGroup { * If the current photo is a photo sphere, this will launch the Photo Sphere * panorama viewer. */ - private void showPhotoSphere() { + @Override + public void onViewPhotoSphere() { ViewInfo curr = mViewInfo[mCurrentInfo]; if (curr != null) { mDataAdapter.getImageData(curr.getID()).viewPhotoSphere(mPanoramaViewHelper); } } + @Override + public void onEdit() { + ImageData data = mDataAdapter.getImageData(getCurrentId()); + if (data == null || !(data instanceof LocalData)) { + return; + } + mActivity.launchEditor((LocalData) data); + } + + @Override + public void onTinyPlanet() { + // TODO: Bring tiny planet to Camera2. + } + /** * @return The ID of the current item, or -1. */ @@ -788,19 +812,16 @@ public class FilmStripView extends ViewGroup { } /** - * Updates the visibility of the View Photo Sphere button. + * Updates the visibility of the bottom controls depending on the current + * data item. */ - private void updatePhotoSphereViewButton() { - if (mViewPhotoSphereButton == null) { - mViewPhotoSphereButton = (ImageButton) ((View) getParent()) - .findViewById(R.id.filmstrip_bottom_control_panorama); - mViewPhotoSphereButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - showPhotoSphere(); - } - }); + private void updateBottomControls() { + if (mBottomControls == null) { + mBottomControls = (FilmstripBottomControls) ((View) getParent()) + .findViewById(R.id.filmstrip_bottom_controls); + mBottomControls.setListener(this); } + final int requestId = getCurrentId(); // Check if the item has changed since the last time we updated the @@ -811,19 +832,20 @@ public class FilmStripView extends ViewGroup { } ImageData data = mDataAdapter.getImageData(requestId); - data.isPhotoSphere(mContext, new PanoramaSupportCallback() { + + // We can only edit photos, not videos. + mBottomControls.setEditButtonVisibility(data.isPhoto()); + + // If this is a photo sphere, show the button to view it. If it's a full + // 360 photo sphere, show the tiny planet button. + data.isPhotoSphere(mActivity, new PanoramaSupportCallback() { @Override public void panoramaInfoAvailable(final boolean isPanorama, boolean isPanorama360) { // Make sure the returned data is for the current image. if (requestId == getCurrentId()) { - mViewPhotoSphereButton.post(new Runnable() { - @Override - public void run() { - mViewPhotoSphereButton.setVisibility(isPanorama ? View.VISIBLE - : View.GONE); - } - }); + mBottomControls.setViewPhotoSphereButtonVisibility(isPanorama); + mBottomControls.setTinyPlanetButtonVisibility(isPanorama360); } } }); @@ -869,8 +891,8 @@ public class FilmStripView extends ViewGroup { int fullScreenWidth = mDrawArea.width() + mViewGap; /** * Transformed scale fraction between 0 and 1. 0 if the scale is - * {@link FILM_STRIP_SCALE}. 1 if the scale is - * {@link FULL_SCREEN_SCALE}. + * {@link FILM_STRIP_SCALE}. 1 if the scale is {@link FULL_SCREEN_SCALE} + * . */ float scaleFraction = mViewAnimInterpolator.getInterpolation( (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE)); @@ -931,7 +953,7 @@ public class FilmStripView extends ViewGroup { adjustChildZOrder(); snapInCenter(); invalidate(); - updatePhotoSphereViewButton(); + updateBottomControls(); mLastItemId = getCurrentId(); } @@ -1553,6 +1575,7 @@ public class FilmStripView extends ViewGroup { scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST); if (mListener != null) { mListener.onSwitchMode(false); + mBottomControls.setVisibility(View.VISIBLE); } } @@ -1572,6 +1595,7 @@ public class FilmStripView extends ViewGroup { // this call when the view on the center of the screen is // camera preview mListener.onSwitchMode(true); + mBottomControls.setVisibility(View.GONE); } if (inFullScreen()) { return; @@ -1645,7 +1669,8 @@ public class FilmStripView extends ViewGroup { return true; } } else if (inFullScreen()) { - mListener.onToggleActionBarVisibility(); + boolean visible = mListener.onToggleActionBarVisibility(); + mBottomControls.setVisibility(visible ? View.VISIBLE : View.GONE); return true; } return false; diff --git a/src/com/android/camera/ui/FilmstripBottomControls.java b/src/com/android/camera/ui/FilmstripBottomControls.java new file mode 100644 index 000000000..3b0435b35 --- /dev/null +++ b/src/com/android/camera/ui/FilmstripBottomControls.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2013 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.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.RelativeLayout; + +import com.android.camera2.R; + +/** + * Shows controls at the bottom of the screen for editing, viewing a photo + * sphere image and creating a tiny planet from a photo sphere image. + */ +public class FilmstripBottomControls extends RelativeLayout { + + /** + * Classes implementing this interface can listen for events on the bottom + * controls. + */ + public static interface BottomControlsListener { + /** + * Called when the user pressed the "view photosphere" button. + */ + public void onViewPhotoSphere(); + + /** + * Called when the user pressed the "edit" button. + */ + public void onEdit(); + + /** + * Called when the user pressed the "tiny planet" button. + */ + public void onTinyPlanet(); + } + + private BottomControlsListener mListener; + private ImageButton mEditButton; + private ImageButton mViewPhotoSphereButton; + private ImageButton mTinyPlanetButton; + + public FilmstripBottomControls(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mEditButton = (ImageButton) + findViewById(R.id.filmstrip_bottom_control_edit); + mEditButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (mListener != null) { + mListener.onEdit(); + } + } + }); + + mViewPhotoSphereButton = (ImageButton) + findViewById(R.id.filmstrip_bottom_control_panorama); + mViewPhotoSphereButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (mListener != null) { + mListener.onViewPhotoSphere(); + } + } + }); + + mTinyPlanetButton = (ImageButton) + findViewById(R.id.filmstrip_bottom_control_tiny_planet); + mTinyPlanetButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (mListener != null) { + mListener.onTinyPlanet(); + } + } + }); + } + + /** + * Sets a new or replaces an existing listener for bottom control events. + */ + public void setListener(BottomControlsListener listener) { + mListener = listener; + } + + /** + * Sets the visibility of the edit button. + */ + public void setEditButtonVisibility(boolean visible) { + setVisibility(mEditButton, visible); + } + + /** + * Sets the visibility of the view-photosphere button. + */ + public void setViewPhotoSphereButtonVisibility(boolean visible) { + setVisibility(mViewPhotoSphereButton, visible); + } + + /** + * Sets the visibility of the tiny-planet button. + */ + public void setTinyPlanetButtonVisibility(final boolean visible) { + setVisibility(mTinyPlanetButton, visible); + } + + /** + * Sets the visibility of the given view. + */ + private static void setVisibility(final View view, final boolean visible) { + view.post(new Runnable() { + @Override + public void run() { + view.setVisibility(visible ? View.VISIBLE + : View.GONE); + } + }); + } +} diff --git a/src/com/android/camera/ui/ZoomView.java b/src/com/android/camera/ui/ZoomView.java new file mode 100644 index 000000000..edf151367 --- /dev/null +++ b/src/com/android/camera/ui/ZoomView.java @@ -0,0 +1,551 @@ +/* + * Copyright (C) 2013 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.camera.ui; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.TypeEvaluator; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class ZoomView extends FrameLayout { + + private static final String TAG = "ZoomView"; + + // Gesture statuses + private static final int IDLE = 0; + private static final int SCALE = 1; + private static final int SCROLL = 2; + + // When the image is zoomed in to within TOLERANCE pixels of its original size, + // we consider it all the way zoomed in + private static final int TOLERANCE = 10; + + private static final int ANIMATION_DURATION_MS = 200; + + private int mViewportWidth = 0; + private int mViewportHeight = 0; + + private ScaleGestureDetector mScaleDetector; + private GestureDetector mGesturesDetector; + private int mGestureState = IDLE; + + private ImageView mPartialImage; + private ImageView mFullImage; + + private RectF mInitialRect; + private int mFullResImageWidth; + private int mFullResImageHeight; + + private BitmapRegionDecoder mRegionDecoder; + private DecodePartialBitmap mPartialDecodingTask; + private LoadBitmapTask mFullImageDecodingTask; + private RectF mBitmapRect; + + private float mLastScalePivotX; + private float mLastScalePivotY; + + private ObjectAnimator mAnimator; + private Uri mUri; + + private TypeEvaluator<Matrix> mEvaluator = new TypeEvaluator<Matrix>() { + @Override + public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) { + + RectF startRect = new RectF(); + startValue.mapRect(startRect, mBitmapRect); + RectF endRect = new RectF(); + endValue.mapRect(endRect, mBitmapRect); + + float top = startRect.top + (endRect.top - startRect.top) * fraction; + float left = startRect.left + (endRect.left - startRect.left) * fraction; + float right = startRect.right + (endRect.right - startRect.right) * fraction; + float bottom = startRect.bottom + (endRect.bottom - startRect.bottom) * fraction; + RectF currentRect = new RectF(left, top, right, bottom); + + Matrix m = new Matrix(); + m.setRectToRect(mBitmapRect, currentRect, Matrix.ScaleToFit.CENTER); + return m; + } + }; + + private GestureDetector.SimpleOnGestureListener mGestureListener + = new GestureDetector.SimpleOnGestureListener(){ + @Override + public boolean onDoubleTap(MotionEvent e) { + zoomAt(e.getX(), e.getY()); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + if (mGestureState == SCALE) { + return false; + } + mGestureState = SCROLL; + + // Translate image matrix + Matrix m = new Matrix(mFullImage.getImageMatrix()); + m.postTranslate(-distanceX, -distanceY); + mFullImage.setImageMatrix(m); + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent ev) { + showPartiallyDecodedImage(true); + return true; + } + }; + + private ScaleGestureDetector.OnScaleGestureListener mScaleListener + = new ScaleGestureDetector.OnScaleGestureListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scaleFactor = detector.getScaleFactor(); + mLastScalePivotX = detector.getFocusX(); + mLastScalePivotY = detector.getFocusY(); + + // Scale image matrix + Matrix m = new Matrix(mFullImage.getImageMatrix()); + m.postScale(scaleFactor, scaleFactor, mLastScalePivotX, mLastScalePivotY); + mFullImage.setImageMatrix(m); + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + mGestureState = SCALE; + cancelPartialDecodingTask(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mGestureState = IDLE; + snapBack(); + } + }; + + private void cancelPartialDecodingTask() { + if (mPartialDecodingTask != null) { + mPartialDecodingTask.cancel(true); + } + } + + private class LoadBitmapTask extends AsyncTask<Object, Void, Bitmap> { + @Override + protected Bitmap doInBackground(Object... params) { + // Params[0]: BitmapFactory.options + if (params.length < 1) { + return null; + } + InputStream is = getInputStream(); + if (isCancelled()) { + return null; + } + BitmapFactory.Options options = (BitmapFactory.Options) params[0]; + return BitmapFactory.decodeStream(is, null, options); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap == null) { + Log.e(TAG, "Failed to load bitmap"); + return; + } + initFullImageView(bitmap); + mFullImageDecodingTask = null; + } + } + + private class DecodePartialBitmap extends AsyncTask<RectF, Void, Bitmap> { + + @Override + protected Bitmap doInBackground(RectF... params) { + RectF endRect = params[0]; + // Find intersection with the screen + RectF visibleRect = new RectF(endRect); + visibleRect.intersect(0, 0, mViewportWidth, mViewportHeight); + + Matrix m2 = new Matrix(); + m2.setRectToRect(endRect, new RectF(0, 0, mFullResImageWidth, mFullResImageHeight), + Matrix.ScaleToFit.CENTER); + RectF visibleInImage = new RectF(); + m2.mapRect(visibleInImage, visibleRect); + + // Decode region + Rect v = new Rect(); + visibleInImage.round(v); + if (isCancelled()) { + return null; + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = getSampleFactor(v.width(), v.height()); + Bitmap b = mRegionDecoder.decodeRegion(v, options); + return b; + } + + @Override + protected void onPostExecute(Bitmap b) { + if (b == null) { + return; + } + mPartialImage.setImageBitmap(b); + showPartiallyDecodedImage(true); + mPartialDecodingTask = null; + } + } + + public ZoomView(Context context, Uri uri) { + super(context); + mUri = uri; + mPartialImage = new ImageView(context); + mFullImage = new ImageView(context); + LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + addView(mPartialImage, lp); + addView(mFullImage, lp); + mFullImage.setScaleType(ImageView.ScaleType.MATRIX); + InputStream is = getInputStream(); + try { + mRegionDecoder = BitmapRegionDecoder.newInstance(is, false); + is.close(); + } catch (IOException e) { + Log.e(TAG, "Fail to instantiate region decoder"); + } + + addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + int w = right - left; + int h = bottom - top; + if (mViewportHeight != h || mViewportWidth != w) { + mViewportWidth = w; + mViewportHeight = h; + loadBitmap(); + } + } + }); + mGesturesDetector = new GestureDetector(getContext(), mGestureListener); + mScaleDetector = new ScaleGestureDetector(getContext(), mScaleListener); + } + + private void initFullImageView(Bitmap bitmap) { + mFullImage.setImageBitmap(bitmap); + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + mBitmapRect = new RectF(0, 0, w, h); + Matrix initialMatrix = new Matrix(); + initialMatrix.setRectToRect(mBitmapRect, mInitialRect, Matrix.ScaleToFit.CENTER); + mFullImage.setImageMatrix(initialMatrix); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + // TODO: The touch event handling could use some refinement + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + endAnimation(); + cancelPartialDecodingTask(); + // Show down-sampled full image when there is touch interaction + showPartiallyDecodedImage(false); + mGestureState = IDLE; + } else if (ev.getActionMasked() == MotionEvent.ACTION_UP + && mGestureState == SCROLL) { + snapBack(); + mGestureState = IDLE; + } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + mGestureState = SCALE; + } + boolean ret = mGesturesDetector.onTouchEvent(ev); + return mScaleDetector.onTouchEvent(ev) || ret; + } + + private void showPartiallyDecodedImage(boolean show) { + if (show) { + mPartialImage.setVisibility(View.VISIBLE); + mFullImage.setVisibility(View.GONE); + } else { + mFullImage.setVisibility(View.VISIBLE); + mPartialImage.setVisibility(View.GONE); + } + } + + /** + * Snap back to the screen bounds from current position + */ + private void snapBack() { + RectF endRect = new RectF(); + mFullImage.getImageMatrix().mapRect(endRect, mBitmapRect); + snapBack(endRect); + } + + /** + * Snap back to the screen bounds from given position + */ + private void snapBack(RectF endRect) { + + if (endRect.width() < mViewportWidth && endRect.height() < mViewportHeight) { + snapToInitialRect(true); + return; + } + + float dx = 0, dy = 0; + Matrix startMatrix = mFullImage.getImageMatrix(); + Matrix endMatrix = new Matrix(startMatrix); + boolean needsSnapping = false; + + if (endRect.width() > mFullResImageWidth) { + needsSnapping = true; + float x = mScaleDetector.getFocusX(); + float y = mScaleDetector.getFocusY(); + float scale = mFullResImageWidth / endRect.width(); + endMatrix.postScale(scale, scale, x, y); + endMatrix.mapRect(endRect, mBitmapRect); + } + + if (endRect.width() < mViewportWidth) { + // Center it + dx = mViewportWidth / 2 - (endRect.left + endRect.right) / 2; + } else { + if (endRect.left > 0) { + dx = -endRect.left; + } else if (endRect.right < mViewportWidth) { + dx = mViewportWidth - endRect.right; + } + } + + if (endRect.height() < mViewportHeight) { + dy = mViewportHeight / 2 - (endRect.top + endRect.bottom) / 2; + } else { + if (endRect.top > 0) { + dy = -endRect.top; + } else if (endRect.bottom < mViewportHeight) { + dy = mViewportHeight - endRect.bottom; + } + } + + if (dx != 0 || dy != 0 || needsSnapping) { + endRect.offset(dx, dy); + endMatrix.postTranslate(dx, dy); + startAnimation(startMatrix, endMatrix); + } + + startPartialDecodingTask(endRect); + } + + private void snapToInitialRect(boolean withAnimation) { + // Restore to initial rect + Matrix endMatrix = new Matrix(); + endMatrix.setRectToRect(mBitmapRect, mInitialRect, Matrix.ScaleToFit.CENTER); + if (withAnimation) { + startAnimation(mFullImage.getImageMatrix(), endMatrix); + } else { + mFullImage.setImageMatrix(endMatrix); + } + } + + private void zoomAt(float x, float y) { + Matrix startMatrix = mFullImage.getImageMatrix(); + Matrix endMatrix = new Matrix(); + RectF currentImageRect = new RectF(); + startMatrix.mapRect(currentImageRect, mBitmapRect); + + if (currentImageRect.width() < mFullResImageWidth - TOLERANCE) { + // Zoom in + float scale = ((float) mFullResImageWidth) / currentImageRect.width(); + endMatrix.set(startMatrix); + endMatrix.postScale(scale, scale, x, y); + // Start animation + startAnimation(startMatrix, endMatrix); + + RectF endRect = new RectF(); + endMatrix.mapRect(endRect, mBitmapRect); + startPartialDecodingTask(endRect); + } else { + // Zoom out + endMatrix.setRectToRect(mBitmapRect, mInitialRect, Matrix.ScaleToFit.CENTER); + // Start animation + startAnimation(startMatrix, endMatrix); + } + + } + + private void startAnimation(Matrix startMatrix, final Matrix endMatrix) { + endAnimation(); + showPartiallyDecodedImage(false); + mAnimator = ObjectAnimator.ofObject(mFullImage, "imageMatrix", mEvaluator, + startMatrix, endMatrix) + .setDuration(ANIMATION_DURATION_MS); + mAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + // Set end value + mFullImage.setImageMatrix(endMatrix); + mAnimator.removeAllListeners(); + mAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }); + mAnimator.start(); + } + + private void endAnimation() { + if (mAnimator == null) { + return; + } + if (mAnimator.isRunning()) { + mAnimator.end(); + } + } + + private void startPartialDecodingTask(RectF endRect) { + // Cancel on-going partial decoding tasks + cancelPartialDecodingTask(); + mPartialDecodingTask = new DecodePartialBitmap(); + mPartialDecodingTask.execute(endRect); + } + + private void loadBitmap() { + if (mFullResImageHeight == 0 || mFullResImageWidth == 0) { + decodeImageSize(); + } + + if (mViewportHeight != 0 && mViewportWidth != 0) { + // Calculate where the bitmap rect should be positioned based on viewport size + calculateInitialRect(); + if (mBitmapRect != null && mBitmapRect.width() > mInitialRect.width() + && mBitmapRect.height() > mInitialRect.height()) { + // No need to reload bitmap + snapToInitialRect(false); + } else { + BitmapFactory.Options option = new BitmapFactory.Options(); + // Down-sample the bitmap whenever possible to be efficient + int sampleFactor = getSampleFactor(mFullResImageWidth, mFullResImageHeight); + option.inSampleSize = sampleFactor; + if (mFullImageDecodingTask != null) { + mFullImageDecodingTask.cancel(true); + } + mFullImageDecodingTask = new LoadBitmapTask(); + mFullImageDecodingTask.execute(option); + } + } + } + + private void calculateInitialRect() { + float fitWidthScale = ((float) mViewportWidth) / ((float) mFullResImageWidth); + float fitHeightScale = ((float) mViewportHeight) / ((float) mFullResImageHeight); + float scale = Math.min(fitHeightScale, fitWidthScale); + + int centerX = mViewportWidth / 2; + int centerY = mViewportHeight / 2; + int width = (int) (scale * mFullResImageWidth); + int height = (int) (scale * mFullResImageHeight); + + mInitialRect = new RectF(centerX - width / 2, centerY - height / 2, + centerX + width / 2, centerY + height / 2); + } + + private void decodeImageSize() { + BitmapFactory.Options option = new BitmapFactory.Options(); + option.inJustDecodeBounds = true; + InputStream is = getInputStream(); + BitmapFactory.decodeStream(is, null, option); + try { + is.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close input stream"); + } + mFullResImageWidth = option.outWidth; + mFullResImageHeight = option.outHeight; + } + + // TODO: Cache the inputstream + private InputStream getInputStream() { + InputStream is = null; + try { + is = getContext().getContentResolver().openInputStream(mUri); + } catch (FileNotFoundException e) { + Log.e(TAG, "File not found at: " + mUri); + } + return is; + } + + /** + * Find closest sample factor that is power of 2, based on the given width and height + * + * @param width width of the partial region to decode + * @param height height of the partial region to decode + * @return sample factor + */ + private int getSampleFactor(int width, int height) { + + float fitWidthScale = ((float) mViewportWidth) / ((float) width); + float fitHeightScale = ((float) mViewportHeight) / ((float) height); + + float scale = Math.min(fitHeightScale, fitWidthScale); + + // Find the closest sample factor that is power of 2 + int sampleFactor = (int) (1f / scale); + if (sampleFactor <=1) { + return 1; + } + for (int i = 0; i < 32; i++) { + if ((1 << (i + 1)) > sampleFactor) { + sampleFactor = (1 << i); + break; + } + } + return sampleFactor; + } +} |