OSDN Git Service

Merge "Fix 5250813 Use new standardized no account screen on first launch"
authorRay Chen <raychen@google.com>
Fri, 7 Oct 2011 04:27:45 +0000 (21:27 -0700)
committerAndroid (Google) Code Review <android-gerrit@google.com>
Fri, 7 Oct 2011 04:27:45 +0000 (21:27 -0700)
59 files changed:
res/drawable-sw600dp/photoeditor_scale_seekbar_color.png [new file with mode: 0644]
res/drawable-sw600dp/photoeditor_scale_seekbar_generic.png [new file with mode: 0644]
res/drawable-sw600dp/photoeditor_scale_seekbar_light.png [new file with mode: 0644]
res/drawable-sw600dp/photoeditor_scale_seekbar_shadow.png [new file with mode: 0644]
res/drawable/photoeditor_effect_doodle.png
res/drawable/photoeditor_effect_facelift.png
res/drawable/photoeditor_scale_seekbar_color.9.png [deleted file]
res/drawable/photoeditor_scale_seekbar_color.png [new file with mode: 0644]
res/drawable/photoeditor_scale_seekbar_filllight.9.png [deleted file]
res/drawable/photoeditor_scale_seekbar_generic.9.png [deleted file]
res/drawable/photoeditor_scale_seekbar_generic.png [new file with mode: 0644]
res/drawable/photoeditor_scale_seekbar_highlight.9.png [deleted file]
res/drawable/photoeditor_scale_seekbar_light.png [new file with mode: 0644]
res/drawable/photoeditor_scale_seekbar_shadow.9.png [deleted file]
res/drawable/photoeditor_scale_seekbar_shadow.png [new file with mode: 0644]
res/drawable/photoeditor_seekbar_thumb.png
res/layout/photoeditor_actionbar.xml
res/values-sw320dp/photoeditor_dimens.xml
res/values-sw600dp/photoeditor_dimens.xml
res/values-sw800dp/photoeditor_dimens.xml
res/values/photoeditor_styles.xml
res/values/strings.xml
src/com/android/gallery3d/data/LocalSource.java
src/com/android/gallery3d/photoeditor/ActionBar.java
src/com/android/gallery3d/photoeditor/FilterStack.java
src/com/android/gallery3d/photoeditor/PhotoEditor.java
src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java
src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java
src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java
src/com/android/gallery3d/photoeditor/filters/CropFilter.java
src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
src/com/android/gallery3d/photoeditor/filters/FaceliftFilter.java
src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java
src/com/android/gallery3d/photoeditor/filters/Filter.java
src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java
src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
src/com/android/gallery3d/photoeditor/filters/GrainFilter.java
src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java
src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java
src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java
src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java
src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
src/com/android/gallery3d/photoeditor/filters/TintFilter.java
src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java
src/com/android/gallery3d/ui/FlingScroller.java [new file with mode: 0644]
src/com/android/gallery3d/ui/PhotoView.java
src/com/android/gallery3d/ui/PositionController.java

diff --git a/res/drawable-sw600dp/photoeditor_scale_seekbar_color.png b/res/drawable-sw600dp/photoeditor_scale_seekbar_color.png
new file mode 100644 (file)
index 0000000..e9f46e4
Binary files /dev/null and b/res/drawable-sw600dp/photoeditor_scale_seekbar_color.png differ
diff --git a/res/drawable-sw600dp/photoeditor_scale_seekbar_generic.png b/res/drawable-sw600dp/photoeditor_scale_seekbar_generic.png
new file mode 100644 (file)
index 0000000..5d93000
Binary files /dev/null and b/res/drawable-sw600dp/photoeditor_scale_seekbar_generic.png differ
diff --git a/res/drawable-sw600dp/photoeditor_scale_seekbar_light.png b/res/drawable-sw600dp/photoeditor_scale_seekbar_light.png
new file mode 100644 (file)
index 0000000..bbf2ac8
Binary files /dev/null and b/res/drawable-sw600dp/photoeditor_scale_seekbar_light.png differ
diff --git a/res/drawable-sw600dp/photoeditor_scale_seekbar_shadow.png b/res/drawable-sw600dp/photoeditor_scale_seekbar_shadow.png
new file mode 100644 (file)
index 0000000..e7d81e9
Binary files /dev/null and b/res/drawable-sw600dp/photoeditor_scale_seekbar_shadow.png differ
index b7e6e91..d23041f 100644 (file)
Binary files a/res/drawable/photoeditor_effect_doodle.png and b/res/drawable/photoeditor_effect_doodle.png differ
index ba845b5..3e86c1f 100644 (file)
Binary files a/res/drawable/photoeditor_effect_facelift.png and b/res/drawable/photoeditor_effect_facelift.png differ
diff --git a/res/drawable/photoeditor_scale_seekbar_color.9.png b/res/drawable/photoeditor_scale_seekbar_color.9.png
deleted file mode 100644 (file)
index d78ff9b..0000000
Binary files a/res/drawable/photoeditor_scale_seekbar_color.9.png and /dev/null differ
diff --git a/res/drawable/photoeditor_scale_seekbar_color.png b/res/drawable/photoeditor_scale_seekbar_color.png
new file mode 100644 (file)
index 0000000..25ec890
Binary files /dev/null and b/res/drawable/photoeditor_scale_seekbar_color.png differ
diff --git a/res/drawable/photoeditor_scale_seekbar_filllight.9.png b/res/drawable/photoeditor_scale_seekbar_filllight.9.png
deleted file mode 100644 (file)
index 43acaf3..0000000
Binary files a/res/drawable/photoeditor_scale_seekbar_filllight.9.png and /dev/null differ
diff --git a/res/drawable/photoeditor_scale_seekbar_generic.9.png b/res/drawable/photoeditor_scale_seekbar_generic.9.png
deleted file mode 100644 (file)
index 3ee3cc7..0000000
Binary files a/res/drawable/photoeditor_scale_seekbar_generic.9.png and /dev/null differ
diff --git a/res/drawable/photoeditor_scale_seekbar_generic.png b/res/drawable/photoeditor_scale_seekbar_generic.png
new file mode 100644 (file)
index 0000000..5b35aea
Binary files /dev/null and b/res/drawable/photoeditor_scale_seekbar_generic.png differ
diff --git a/res/drawable/photoeditor_scale_seekbar_highlight.9.png b/res/drawable/photoeditor_scale_seekbar_highlight.9.png
deleted file mode 100644 (file)
index 70f54b8..0000000
Binary files a/res/drawable/photoeditor_scale_seekbar_highlight.9.png and /dev/null differ
diff --git a/res/drawable/photoeditor_scale_seekbar_light.png b/res/drawable/photoeditor_scale_seekbar_light.png
new file mode 100644 (file)
index 0000000..5773bd1
Binary files /dev/null and b/res/drawable/photoeditor_scale_seekbar_light.png differ
diff --git a/res/drawable/photoeditor_scale_seekbar_shadow.9.png b/res/drawable/photoeditor_scale_seekbar_shadow.9.png
deleted file mode 100644 (file)
index b17a785..0000000
Binary files a/res/drawable/photoeditor_scale_seekbar_shadow.9.png and /dev/null differ
diff --git a/res/drawable/photoeditor_scale_seekbar_shadow.png b/res/drawable/photoeditor_scale_seekbar_shadow.png
new file mode 100644 (file)
index 0000000..e534bce
Binary files /dev/null and b/res/drawable/photoeditor_scale_seekbar_shadow.png differ
index 0d452c1..0d30816 100644 (file)
Binary files a/res/drawable/photoeditor_seekbar_thumb.png and b/res/drawable/photoeditor_seekbar_thumb.png differ
index 82ed41f..2437611 100644 (file)
             android:id="@+id/redo_button"
             style="@style/ImageActionButton"
             android:src="@drawable/photoeditor_redo"/>
-        <Button
-            android:id="@+id/save_button"
-            style="@style/TextActionButton"
-            android:text="@string/save"/>
+
+        <ViewSwitcher
+            android:id="@+id/save_share_buttons"
+            android:layout_width="wrap_content"
+            android:layout_height="fill_parent">
+            <Button
+                android:id="@+id/save_button"
+                style="@style/TextActionButton"
+                android:layout_width="fill_parent"
+                android:text="@string/save"/>
+            <ImageButton
+                android:id="@+id/share_button"
+                style="@style/ImageActionButton"
+                android:layout_width="fill_parent"
+                android:src="@drawable/ic_menu_share_holo_light"/>
+        </ViewSwitcher>
+
     </LinearLayout>
 </com.android.gallery3d.photoeditor.ActionBar>
index 5d894a6..66e9d5f 100755 (executable)
 -->
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
-    <dimen name="effect_label_text_size">11sp</dimen>
-    <dimen name="effect_label_width">98dp</dimen>
+    <dimen name="effect_label_text_size">11.5sp</dimen>
+    <dimen name="effect_label_width">100dp</dimen>
+    <dimen name="effect_label_margin_top">3dp</dimen>
     <dimen name="effect_icon_size">72dp</dimen>
-    <dimen name="effect_padding_horizontal">2dp</dimen>
-    <dimen name="effects_container_padding">12dp</dimen>
+    <dimen name="effect_padding_horizontal">1dp</dimen>
+    <dimen name="effects_container_padding">8dp</dimen>
     <dimen name="action_bar_arrow_margin_left">3dp</dimen>
     <dimen name="action_bar_arrow_margin_right">-3dp</dimen>
     <dimen name="action_bar_icon_padding_left">0dp</dimen>
     <dimen name="action_bar_icon_padding_right">5dp</dimen>
     <dimen name="action_button_padding_horizontal">13dp</dimen>
-    <dimen name="effect_tool_panel_padding">12dp</dimen>
+    <dimen name="effect_tool_panel_padding">10dp</dimen>
     <dimen name="seekbar_width">290dp</dimen>
-    <dimen name="seekbar_height">29dp</dimen>
-    <dimen name="seekbar_margin_bottom">6dp</dimen>
+    <dimen name="seekbar_height">27dp</dimen>
+    <dimen name="seekbar_margin_bottom">3dp</dimen>
     <dimen name="crop_indicator_size">35dp</dimen>
 </resources>
index 2b9cf6f..1162b45 100755 (executable)
 -->
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
-    <dimen name="effect_label_text_size">14sp</dimen>
+    <dimen name="effect_label_text_size">13.5sp</dimen>
     <dimen name="effect_label_width">120dp</dimen>
+    <dimen name="effect_label_margin_top">3dp</dimen>
     <dimen name="effect_icon_size">90dp</dimen>
     <dimen name="effect_padding_horizontal">2dp</dimen>
-    <dimen name="effects_container_padding">12dp</dimen>
+    <dimen name="effects_container_padding">11dp</dimen>
     <dimen name="action_bar_arrow_margin_left">3dp</dimen>
     <dimen name="action_bar_arrow_margin_right">-3dp</dimen>
     <dimen name="action_bar_icon_padding_vertical">4dp</dimen>
@@ -27,9 +28,9 @@
     <dimen name="action_bar_icon_padding_right">5dp</dimen>
     <dimen name="action_button_padding_vertical">8dp</dimen>
     <dimen name="action_button_padding_horizontal">22dp</dimen>
-    <dimen name="effect_tool_panel_padding">16dp</dimen>
+    <dimen name="effect_tool_panel_padding">13dp</dimen>
     <dimen name="seekbar_width">560dp</dimen>
-    <dimen name="seekbar_height">33dp</dimen>
-    <dimen name="seekbar_margin_bottom">8dp</dimen>
+    <dimen name="seekbar_height">23dp</dimen>
+    <dimen name="seekbar_margin_bottom">4dp</dimen>
     <dimen name="crop_indicator_size">43dp</dimen>
 </resources>
index 9dab922..44791d5 100755 (executable)
 -->
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
-    <dimen name="effect_label_text_size">15sp</dimen>
-    <dimen name="effect_label_width">138dp</dimen>
+    <dimen name="effect_label_text_size">14.5sp</dimen>
+    <dimen name="effect_label_width">140dp</dimen>
+    <dimen name="effect_label_margin_top">4dp</dimen>
     <dimen name="effect_icon_size">100dp</dimen>
-    <dimen name="effect_padding_horizontal">3dp</dimen>
-    <dimen name="effects_container_padding">18dp</dimen>
+    <dimen name="effect_padding_horizontal">2dp</dimen>
+    <dimen name="effects_container_padding">13dp</dimen>
     <dimen name="action_bar_arrow_margin_left">3dp</dimen>
     <dimen name="action_bar_arrow_margin_right">-3dp</dimen>
     <dimen name="action_bar_icon_padding_vertical">4dp</dimen>
@@ -27,9 +28,9 @@
     <dimen name="action_bar_icon_padding_right">5dp</dimen>
     <dimen name="action_button_padding_vertical">8dp</dimen>
     <dimen name="action_button_padding_horizontal">28dp</dimen>
-    <dimen name="effect_tool_panel_padding">18dp</dimen>
+    <dimen name="effect_tool_panel_padding">15dp</dimen>
     <dimen name="seekbar_width">560dp</dimen>
-    <dimen name="seekbar_height">35dp</dimen>
-    <dimen name="seekbar_margin_bottom">9dp</dimen>
+    <dimen name="seekbar_height">23dp</dimen>
+    <dimen name="seekbar_margin_bottom">5dp</dimen>
     <dimen name="crop_indicator_size">48dp</dimen>
 </resources>
index fe5ad1a..964ecbf 100644 (file)
@@ -26,6 +26,7 @@
         <item name="android:layout_width">@dimen/effect_label_width</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_gravity">center_horizontal</item>
+        <item name="android:layout_marginTop">@dimen/effect_label_margin_top</item>
         <item name="android:gravity">center</item>
         <item name="android:textSize">@dimen/effect_label_text_size</item>
         <item name="android:textColor">#FFFFFF</item>
index 1ad81bf..26d80e7 100644 (file)
@@ -46,7 +46,7 @@
 
     <!-- Displayed in the title of those pictures that fails to be loaded
          [CHAR LIMIT=50]-->
-    <string name="fail_to_load">Failed to load</string>
+    <string name="fail_to_load">Couldn\'t load</string>
 
     <!-- Displayed in place of the picture when we fail to get the thumbnail of it.
          [CHAR LIMIT=50]-->
 
     <!-- Title of a menu item to indicate performing the image crop operation
          [CHAR LIMIT=20] -->
-    <string name="crop_save_text">Ok</string>
+    <string name="crop_save_text">OK</string>
     <!-- Button indicating that the cropped image should be reverted back to the original -->
     <!-- Hint that appears when cropping an image with more than one face -->
-    <string name="multiface_crop_help">Tap a face to begin.</string>
+    <string name="multiface_crop_help">Touch a face to begin.</string>
     <!-- Toast/alert that the image is being saved to the SD card -->
     <string name="saving_image">Saving picture\u2026</string>
     <!-- Eorror toast message that the image cannot be saved [CHAR LIMIT=40]-->
-    <string name="save_error">Cannot save the cropped image</string>
+    <string name="save_error">Couldn\'t save cropped image.</string>
 
     <!-- menu pick: crop the currently selected image -->
     <string name="crop_label">Crop picture</string>
              that is to be "set as" (e.g. set as contact photo or set as wallpaper) -->
     <string name="set_image">Set picture as</string>
     <!-- Toast/alert after saving wallpaper -->
-    <string name="wallpaper">Setting wallpaper, please wait\u2026</string>
+    <string name="wallpaper">Setting wallpaper\u2026</string>
     <string name="camera_setas_wallpaper">Wallpaper</string>
 
     <!-- Details dialog "OK" button. Dismisses dialog. -->
     <string name="delete">Delete</string>
-    <string name="confirm_delete">Confirm Delete</string>
+    <string name="confirm_delete">Delete</string>
     <string name="cancel">Cancel</string>
     <string name="share">Share</string>
 
     <!-- String indicating more actions are available -->
-    <string name="select_all">Select All</string>
-    <string name="deselect_all">Deselect All</string>
+    <string name="select_all">Select all</string>
+    <string name="deselect_all">Deselect all</string>
     <string name="slideshow">Slideshow</string>
 
     <string name="details">Details</string>
 
     <!-- String indicating timestamp of photo or video -->
     <string name="show_on_map">Show on map</string>
-    <string name="rotate_left">Rotate Left</string>
-    <string name="rotate_right">Rotate Right</string>
+    <string name="rotate_left">Rotate left</string>
+    <string name="rotate_right">Rotate right</string>
 
     <!-- Toast message prompted when the specified item is not found [CHAR LIMIT=40]-->
-    <string name="no_such_item">Item not found</string>
+    <string name="no_such_item">Couldn\'t find item.</string>
 
     <!-- String used as a menu label. The suer can choose to edit the image
          [CHAR_LIMIT=20]-->
 
     <!-- String used in a toast message indicating there is no application
          available to handle a request [CHAR LIMIT=50] -->
-    <string name="activity_not_found">No application available</string>
+    <string name="activity_not_found">No app is available to complete the action.</string>
 
     <!-- String used as a title of a progress dialog. The user can
          choose to cache some Picasa picture albums on device, so it can
          be viewed offline. This string is shown when the request is being
          processed. [CHAR LIMIT=50] -->
-    <string name="process_caching_requests">Process Caching Requests</string>
+    <string name="process_caching_requests">Processing caching requests</string>
 
     <!-- String used as a small notification label above a Picasa album.
          It means the pictures of the Picasa album is currently being
          transferred to local storage, so the pictures can later be viewed
          offline. [CHAR LIMIT=15] -->
-    <string name="caching_label">Caching...</string>
+    <string name="caching_label">Caching\u2026</string>
 
     <string name="crop_action">Crop</string>
     <string name="set_as">Set as</string>
 
     <!-- String indicating an approximate location eg. Around Palo Alto, CA -->
-    <string name="video_err">Unable to play video</string>
+    <string name="video_err">Can\'t play video.</string>
 
     <!-- Strings for grouping operations in the menu. The photos can be grouped
          by their location, taken time, or tags. -->
 
     <!-- When grouping photos by locations, the label used for photos that don't
          have location information in them [CHAR LIMIT=20]-->
-    <string name="no_location">No Location</string>
+    <string name="no_location">No location</string>
 
     <!-- This toast message is shown when network connection is lost while doing clustering -->
-    <string name="no_connectivity">Some locations could not be identified due to network connectivity issues</string>
+    <string name="no_connectivity">Some locations couldn\'t be identified due to network problems.</string>
 
     <!-- The title of the menu item to let user choose the which portion of
          the media items the user wants to see. When pressed, a submenu will
     <string name="appwidget_title">Photo Gallery</string>
 
     <!-- Text for the empty state of the StackView AppWidget [CHAR LIMIT=30] -->
-    <string name="appwidget_empty_text">No Photos</string>
+    <string name="appwidget_empty_text">No photos.</string>
 
     <!-- Toast message shown when the cropped image has been saved in the
          download folder [CHAR LIMIT=50]-->
-    <string name="crop_saved">The cropped image has been saved in download</string>
+    <string name="crop_saved">Cropped image saved to Downloads.</string>
 
     <!-- Toast message shown when the cropped image is not saved
          [CHAR LIMIT=50]-->
-    <string name="crop_not_saved">The cropped image is not saved</string>
+    <string name="crop_not_saved">Cropped image wasn\'t saved.</string>
 
     <!-- Toast message shown when there is no albums available [CHAR LIMIT=50]-->
-    <string name="no_albums_alert">There are no albums available</string>
+    <string name="no_albums_alert">No albums available.</string>
 
     <!-- Toast message shown when we close the AlbumPage because it is empty
             [CHAR LIMIT=50] -->
-    <string name="empty_album">There are no images/videos available</string>
+    <string name="empty_album">O images/videos available.</string>
 
     <!-- Album label used to indicate the collection of PWA Buzz/Post photos -->
     <string name="picasa_posts">Posts</string>
     <!-- Text indicating the duration of a video item in details window [CHAR LIMIT=14] -->
     <string name="duration">Duration</string>
     <!-- Text indicating the mime type of a media item in details window [CHAR LIMIT=14] -->
-    <string name="mimetype">MIME Type</string>
+    <string name="mimetype">MIME type</string>
     <!-- Text indicating the file size of a media item in details window [CHAR LIMIT=14] -->
-    <string name="file_size">File Size</string>
+    <string name="file_size">File size</string>
     <!-- Text indicating the maker of a media item in details window [CHAR LIMIT=14] -->
     <string name="maker">Maker</string>
     <!-- Text indicating the model of a media item in details window [CHAR LIMIT=14] -->
     <!-- Text indicating the focal length of a media item in details window [CHAR LIMIT=14] -->
     <string name="focal_length">Focal Length</string>
     <!-- Text indicating the white balance of a media item in details window [CHAR LIMIT=14] -->
-    <string name="white_balance">White Balance</string>
+    <string name="white_balance">White balance</string>
     <!-- Text indicating the exposure time of a media item in details window [CHAR LIMIT=14] -->
-    <string name="exposure_time">Exposure Time</string>
+    <string name="exposure_time">Exposure time</string>
     <!-- Text indicating the ISO speed rating of a media item in details window [CHAR LIMIT=14] -->
     <string name="iso">ISO</string>
     <!-- String indicating the time units in seconds. [CHAR LIMIT=8] -->
 
     <!-- Toast message shown after we make some album(s) available offline [CHAR LIMIT=50] -->
     <plurals name="make_albums_available_offline">
-        <item quantity="one">Making album available offline</item>
-        <item quantity="other">Making albums available offline</item>
+        <item quantity="one">Making album available offline.</item>
+        <item quantity="other">Making albums available offline.</item>
     </plurals>
 
     <!-- Toast message shown after we try to make a local album available offline
 
     <!-- A label shown on the action bar. It indicates that the user is
          viewing all available albums [CHAR LIMIT=20] -->
-    <string name="set_label_all_albums">All Albums</string>
+    <string name="set_label_all_albums">All albums</string>
 
     <!-- A label shown on the action bar. It indicates that the user is
          viewing albums stored locally on the device [CHAR LIMIT=20] -->
-    <string name="set_label_local_albums">Local Albums</string>
+    <string name="set_label_local_albums">Local albums</string>
 
     <!-- A label shown on the action bar. It indicates that the user is
          viewing MTP devices connected (like other digital cameras).
          [CHAR LIMIT=20] -->
-    <string name="set_label_mtp_devices">MTP Devices</string>
+    <string name="set_label_mtp_devices">MTP devices</string>
 
     <!-- A label shown on the action bar. It indicates that the user is
          viewing Picasa albums [CHAR LIMIT=20] -->
-    <string name="set_label_picasa_albums">Picasa Albums</string>
+    <string name="set_label_picasa_albums">Picasa albums</string>
 
     <!-- Label indicating the amount on free space on the device. The parameter
          is a string representation of the amount of free space, eg. "20MB".
 
     <!-- A label shown on the action bar. It indicates whether the import
          operation succeeds or fails. [CHAR LIMIT=20] -->
-    <string name="import_complete">Import Complete</string>
-    <string name="import_fail">Import Fail</string>
+    <string name="import_complete">Import complete</string>
+    <string name="import_fail">Import unsuccessful</string>
 
     <!-- A toast indicating a camera is connected to the device [CHAR LIMIT=30]-->
-    <string name="camera_connected">Camera connected</string>
+    <string name="camera_connected">Camera connected.</string>
     <!-- A toast indicating a camera is disconnected [CHAR LIMIT=30] -->
-    <string name="camera_disconnected">Camera disconnected</string>
+    <string name="camera_disconnected">Camera disconnected.</string>
     <!-- A label shown on MTP albums thumbnail to instruct users to import
         [CHAR LIMIT=40] -->
     <string name="click_import">Touch here to import</string>
     <!-- The label on the radio button for the widget type that shows the images in an album. [CHAR LIMIT=30]-->
     <string name="widget_type_shuffle">Shuffle all images</string>
     <!-- The label on the radio button for the widget type that shows only one image. [CHAR LIMIT=30]-->
-    <string name="widget_type_photo">Pick an image</string>
+    <string name="widget_type_photo">Choose an image</string>
 
     <!-- The title of the dialog for choosing the type of widget. [CHAR LIMIT=20] -->
-    <string name="widget_type">Widget Type</string>
+    <string name="widget_type">Widget type</string>
 
     <!-- Title of the Android Dreams slideshow screensaver. [CHAR LIMIT=20] -->
     <string name="slideshow_dream_name">Slideshow</string>
index 58ac224..9bb561b 100644 (file)
 
 package com.android.gallery3d.data;
 
-import com.android.gallery3d.app.Gallery;
-import com.android.gallery3d.app.GalleryApp;
-import com.android.gallery3d.data.MediaSet.ItemConsumer;
-
 import android.content.ContentProviderClient;
 import android.content.ContentUris;
 import android.content.UriMatcher;
 import android.net.Uri;
 import android.provider.MediaStore;
 
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -143,8 +143,7 @@ class LocalSource extends MediaSource {
             case MEDIA_TYPE_VIDEO:
                 return Path.fromString("/local/video").getChild(id);
             default:
-                return Path.fromString("/merge/{/local/image,/local/video}")
-                        .getChild(id);
+                return Path.fromString("/local/all").getChild(id);
         }
     }
 
@@ -176,12 +175,9 @@ class LocalSource extends MediaSource {
     @Override
     public Path getDefaultSetOf(Path item) {
         MediaObject object = mApplication.getDataManager().getMediaObject(item);
-        if (object instanceof LocalImage) {
-            return Path.fromString("/local/image/").getChild(
-                    String.valueOf(((LocalImage) object).getBucketId()));
-        } else if (object instanceof LocalVideo) {
-            return Path.fromString("/local/video/").getChild(
-                    String.valueOf(((LocalVideo) object).getBucketId()));
+        if (object instanceof LocalMediaItem) {
+            return Path.fromString("/local/all").getChild(
+                    String.valueOf(((LocalMediaItem) object).getBucketId()));
         }
         return null;
     }
index 5a423b9..db18c74 100644 (file)
@@ -20,6 +20,7 @@ import android.content.Context;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.RelativeLayout;
+import android.widget.ViewSwitcher;
 
 import com.android.gallery3d.R;
 
@@ -104,8 +105,19 @@ public class ActionBar extends RelativeLayout {
         View button = findViewById(buttonId);
         button.setEnabled(enabled);
         button.setAlpha(enabled ? ENABLED_ALPHA : DISABLED_ALPHA);
-
         // Track buttons whose enabled status has been updated.
         changedButtons.add(buttonId);
+
+        if (buttonId == R.id.save_button) {
+            // Show share-button only after photo is edited and saved; otherwise, show save-button.
+            // TODO: Fix the assumption of undo enabled status must be updated before reaching here.
+            boolean showShare = findViewById(R.id.undo_button).isEnabled() && !enabled;
+            ViewSwitcher switcher = (ViewSwitcher) findViewById(R.id.save_share_buttons);
+            int next = switcher.getNextView().getId();
+            if ((showShare && (next == R.id.share_button))
+                    || (!showShare && (next == R.id.save_button))) {
+                switcher.showNext();
+            }
+        }
     }
 }
index 5b8d0d7..67b904d 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor;
 
 import android.graphics.Bitmap;
-import android.media.effect.EffectContext;
 
 import com.android.gallery3d.photoeditor.filters.Filter;
 
@@ -44,7 +43,6 @@ public class FilterStack {
     private final PhotoView photoView;
     private final StackListener stackListener;
 
-    private EffectContext effectContext;
     private Photo source;
     private Runnable queuedTopFilterChange;
     private volatile boolean paused;
@@ -54,15 +52,6 @@ public class FilterStack {
         this.stackListener = stackListener;
     }
 
-    private void clearBuffers() {
-        for (int i = 0; i < buffers.length; i++) {
-            if (buffers[i] != null) {
-                buffers[i].clear();
-                buffers[i] = null;
-            }
-        }
-    }
-
     private void reallocateBuffer(int target) {
         int other = target ^ 1;
         buffers[target] = Photo.create(buffers[other].width(), buffers[other].height());
@@ -70,7 +59,12 @@ public class FilterStack {
 
     private void invalidate() {
         // In/out buffers need redrawn by re-applying filters on source photo.
-        clearBuffers();
+        for (int i = 0; i < buffers.length; i++) {
+            if (buffers[i] != null) {
+                buffers[i].clear();
+                buffers[i] = null;
+            }
+        }
         if (source != null) {
             buffers[0] = Photo.create(source.width(), source.height());
             reallocateBuffer(1);
@@ -98,7 +92,7 @@ public class FilterStack {
                 buffers[out].clear();
                 reallocateBuffer(out);
             }
-            appliedStack.get(filterIndex).process(effectContext, input, buffers[out]);
+            appliedStack.get(filterIndex).process(input, buffers[out]);
             return buffers[out];
         }
         return null;
@@ -229,19 +223,18 @@ public class FilterStack {
 
     public void onPause() {
         // Flush pending queued operations and release effect-context before GL context is lost.
-        // Use pause-flag to avoid lengthy runnable in GL thread blocking onPause().
+        // Use the flag to break from lengthy invalidate() in GL thread for not blocking onPause().
         paused = true;
         photoView.flush();
         photoView.queueEvent(new Runnable() {
 
             @Override
             public void run() {
-                if (effectContext != null) {
-                    effectContext.release();
-                    effectContext = null;
+                Filter.releaseContext();
+                for (int i = 0; i < buffers.length; i++) {
+                    // Textures will be automatically deleted when GL context is lost.
+                    buffers[i] = null;
                 }
-                photoView.setPhoto(null);
-                clearBuffers();
             }
         });
         photoView.onPause();
@@ -254,7 +247,7 @@ public class FilterStack {
             @Override
             public void run() {
                 // Create effect context after GL context is created or recreated.
-                effectContext = EffectContext.createWithCurrentGlContext();
+                Filter.createContextWithCurrentGlContext();
             }
         });
         paused = false;
index e36071e..3a6face 100644 (file)
@@ -33,7 +33,8 @@ import com.android.gallery3d.R;
  */
 public class PhotoEditor extends Activity {
 
-    private Uri uri;
+    private Uri sourceUri;
+    private Uri saveUri;
     private FilterStack filterStack;
     private ActionBar actionBar;
 
@@ -43,7 +44,9 @@ public class PhotoEditor extends Activity {
         setContentView(R.layout.photoeditor_main);
 
         Intent intent = getIntent();
-        uri = Intent.ACTION_EDIT.equalsIgnoreCase(intent.getAction()) ? intent.getData() : null;
+        if (Intent.ACTION_EDIT.equalsIgnoreCase(intent.getAction())) {
+            sourceUri = intent.getData();
+        }
 
         actionBar = (ActionBar) findViewById(R.id.action_bar);
         filterStack = new FilterStack((PhotoView) findViewById(R.id.photo_view),
@@ -63,6 +66,7 @@ public class PhotoEditor extends Activity {
         actionBar.setRunnable(R.id.undo_button, createUndoRedoRunnable(true, effectsBar));
         actionBar.setRunnable(R.id.redo_button, createUndoRedoRunnable(false, effectsBar));
         actionBar.setRunnable(R.id.save_button, createSaveRunnable(effectsBar));
+        actionBar.setRunnable(R.id.share_button, createShareRunnable(effectsBar));
         actionBar.setRunnable(R.id.action_bar_back, createBackRunnable(effectsBar));
     }
 
@@ -106,7 +110,7 @@ public class PhotoEditor extends Activity {
                 });
             }
         };
-        new LoadScreennailTask(this, callback).execute(uri);
+        new LoadScreennailTask(this, callback).execute(sourceUri);
     }
 
     private Runnable createUndoRedoRunnable(final boolean undo, final EffectsBar effectsBar) {
@@ -157,9 +161,11 @@ public class PhotoEditor extends Activity {
                                     public void onComplete(Uri result) {
                                         progressDialog.dismiss();
                                         actionBar.enableButton(R.id.save_button, (result == null));
+                                        saveUri = result;
                                     }
                                 };
-                                new SaveCopyTask(PhotoEditor.this, uri, callback).execute(bitmap);
+                                new SaveCopyTask(PhotoEditor.this, sourceUri, callback).execute(
+                                        bitmap);
                             }
                         });
                     }
@@ -168,6 +174,27 @@ public class PhotoEditor extends Activity {
         };
     }
 
+    private Runnable createShareRunnable(final EffectsBar effectsBar) {
+        return new Runnable() {
+
+            @Override
+            public void run() {
+                effectsBar.exit(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        if (saveUri != null) {
+                            Intent intent = new Intent(Intent.ACTION_SEND);
+                            intent.putExtra(Intent.EXTRA_STREAM, saveUri);
+                            intent.setType("image/*");
+                            startActivity(intent);
+                        }
+                    }
+                });
+            }
+        };
+    }
+
     private Runnable createBackRunnable(final EffectsBar effectsBar) {
         return new Runnable() {
 
index 4bc49c5..2c69735 100644 (file)
@@ -29,7 +29,7 @@ import com.android.gallery3d.photoeditor.PhotoView;
 public class EffectToolFactory {
 
     public enum ScalePickerType {
-        FILLLIGHT, HIGHLIGHT, SHADOW, COLOR, GENERIC
+        LIGHT, SHADOW, COLOR, GENERIC
     }
 
     private final ViewGroup effectToolPanel;
@@ -59,11 +59,8 @@ public class EffectToolFactory {
 
     private int getScalePickerBackground(ScalePickerType type) {
         switch (type) {
-            case FILLLIGHT:
-                return R.drawable.photoeditor_scale_seekbar_filllight;
-
-            case HIGHLIGHT:
-                return R.drawable.photoeditor_scale_seekbar_highlight;
+            case LIGHT:
+                return R.drawable.photoeditor_scale_seekbar_light;
 
             case SHADOW:
                 return R.drawable.photoeditor_scale_seekbar_shadow;
index e5a743c..962486b 100644 (file)
@@ -38,7 +38,7 @@ public class FillLightAction extends EffectAction {
     public void doBegin() {
         final FillLightFilter filter = new FillLightFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.FILLLIGHT);
+        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.LIGHT);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
index 06feedc..cd8f4b2 100644 (file)
@@ -38,7 +38,7 @@ public class HighlightAction extends EffectAction {
     public void doBegin() {
         final HighlightFilter filter = new HighlightFilter();
 
-        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.HIGHLIGHT);
+        scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.LIGHT);
         scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
 
             @Override
index a219abe..78153d0 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class AutoFixFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_AUTOFIX);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_AUTOFIX);
         effect.setParameter("scale", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index dc6f1a7..f9c6400 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class ColorTemperatureFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_TEMPERATURE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_TEMPERATURE);
         effect.setParameter("scale", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index 372279e..f984f3b 100644 (file)
@@ -18,7 +18,6 @@ package com.android.gallery3d.photoeditor.filters;
 
 import android.graphics.RectF;
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -39,11 +38,11 @@ public class CropFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
+    public void process(Photo src, Photo dst) {
         dst.changeDimension(Math.round(bounds.width() * src.width()),
                 Math.round(bounds.height() * src.height()));
 
-        Effect effect = getEffect(context, EffectFactory.EFFECT_CROP);
+        Effect effect = getEffect(EffectFactory.EFFECT_CROP);
         effect.setParameter("xorigin", Math.round(bounds.left * src.width()));
         effect.setParameter("yorigin", Math.round(bounds.top * src.height()));
         effect.setParameter("width", dst.width());
index f03bf5f..bea8a27 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.photoeditor.filters;
 
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -31,8 +30,8 @@ public class CrossProcessFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        getEffect(context, EffectFactory.EFFECT_CROSSPROCESS).apply(
+    public void process(Photo src, Photo dst) {
+        getEffect(EffectFactory.EFFECT_CROSSPROCESS).apply(
                 src.texture(), src.width(), src.height(), dst.texture());
     }
 }
index 800d59a..4075b27 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.photoeditor.filters;
 
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -31,8 +30,8 @@ public class DocumentaryFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        getEffect(context, EffectFactory.EFFECT_DOCUMENTARY).apply(
+    public void process(Photo src, Photo dst) {
+        getEffect(EffectFactory.EFFECT_DOCUMENTARY).apply(
                 src.texture(), src.width(), src.height(), dst.texture());
     }
 }
index 4b7b4c9..d9e904a 100644 (file)
@@ -23,7 +23,6 @@ import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.RectF;
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -64,7 +63,7 @@ public class DoodleFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
+    public void process(Photo src, Photo dst) {
         Bitmap bitmap = Bitmap.createBitmap(src.width(), src.height(), Bitmap.Config.ARGB_8888);
         Canvas canvas = new Canvas(bitmap);
 
@@ -81,7 +80,7 @@ public class DoodleFilter extends Filter {
             canvas.drawPath(drawingPath, paint);
         }
 
-        Effect effect = getEffect(context, EffectFactory.EFFECT_BITMAPOVERLAY);
+        Effect effect = getEffect(EffectFactory.EFFECT_BITMAPOVERLAY);
         effect.setParameter("bitmap", bitmap);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index 68db831..89e95b9 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -37,8 +36,8 @@ public class DuotoneFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_DUOTONE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_DUOTONE);
         effect.setParameter("first_color", firstColor);
         effect.setParameter("second_color", secondColor);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
index 3fa2e31..3c7a731 100644 (file)
@@ -17,8 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
-import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
 
@@ -40,9 +38,8 @@ public class FaceliftFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context,
-                "com.google.android.media.effect.effects.FaceliftEffect");
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect("com.google.android.media.effect.effects.FaceliftEffect");
         effect.setParameter("blend", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index 29fab3c..2346953 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class FillLightFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_FILLLIGHT);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_FILLLIGHT);
         effect.setParameter("strength", backlight);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index c2d3fe5..1fd1f7e 100644 (file)
@@ -21,31 +21,59 @@ import android.media.effect.EffectContext;
 
 import com.android.gallery3d.photoeditor.Photo;
 
+import java.util.HashMap;
+
 /**
- * Image filter for photo editing.
+ * Image filter for photo editing; most of its methods must be called from a single GL thread except
+ * validate()/isValid() that are called from UI thread.
  */
 public abstract class Filter {
 
     // TODO: This should be set in MFF instead.
     private static final int DEFAULT_TILE_SIZE = 640;
 
+    private static final HashMap<Filter, Effect> effects = new HashMap<Filter, Effect>();
+    private static EffectContext context;
+
     private boolean isValid;
-    private EffectContext context;
-    private Effect effect;
 
-    protected void validate() {
-        isValid = true;
+    public static void createContextWithCurrentGlContext() {
+        context = EffectContext.createWithCurrentGlContext();
+    }
+
+    public static void releaseContext() {
+        if (context != null) {
+            // Release all effects created with the releasing context.
+            for (Effect effect : effects.values()) {
+                effect.release();
+            }
+            effects.clear();
+            context.release();
+            context = null;
+        }
     }
 
-    protected Effect getEffect(EffectContext context, String name) {
-        if (this.context != context) {
+    public void release() {
+        Effect effect = effects.remove(this);
+        if (effect != null) {
+            effect.release();
+        }
+    }
+
+    protected Effect getEffect(String name) {
+        Effect effect = effects.get(this);
+        if (effect == null) {
             effect = context.getFactory().createEffect(name);
             effect.setParameter("tile_size", DEFAULT_TILE_SIZE);
-            this.context = context;
+            effects.put(this, effect);
         }
         return effect;
     }
 
+    protected void validate() {
+        isValid = true;
+    }
+
     /**
      * Some filters, e.g. lighting filters, are initially invalid until set up with parameters while
      * others, e.g. Sepia or Posterize filters, are initially valid without parameters.
@@ -54,19 +82,11 @@ public abstract class Filter {
         return isValid;
     }
 
-    public void release() {
-        if (effect != null) {
-            effect.release();
-            effect = null;
-        }
-    }
-
     /**
      * Processes the source bitmap and matrix and output the destination bitmap and matrix.
      *
-     * @param context effect context bound to a GL context to create GL effect.
      * @param src source photo as the input.
      * @param dst destination photo having the same dimension as source photo as the output.
      */
-    public abstract void process(EffectContext context, Photo src, Photo dst);
+    public abstract void process(Photo src, Photo dst);
 }
index b1eb2d1..6bd406c 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class FisheyeFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_FISHEYE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_FISHEYE);
         effect.setParameter("scale", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index 184f590..7035912 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -37,8 +36,8 @@ public class FlipFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_FLIP);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_FLIP);
         effect.setParameter("horizontal", flipHorizontal);
         effect.setParameter("vertical", flipVertical);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
index 191cb44..ddaad7a 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class GrainFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_GRAIN);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_GRAIN);
         effect.setParameter("strength", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index ce6cdef..d5ef8a0 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.photoeditor.filters;
 
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -31,8 +30,8 @@ public class GrayscaleFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        getEffect(context, EffectFactory.EFFECT_GRAYSCALE).apply(
+    public void process(Photo src, Photo dst) {
+        getEffect(EffectFactory.EFFECT_GRAYSCALE).apply(
                 src.texture(), src.width(), src.height(), dst.texture());
     }
 }
index 3169d04..dfaaa65 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class HighlightFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_BLACKWHITE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_BLACKWHITE);
         effect.setParameter("black", 0f);
         effect.setParameter("white", white);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
index 95ff627..140f2d6 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.photoeditor.filters;
 
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -31,8 +30,8 @@ public class LomoishFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        getEffect(context, EffectFactory.EFFECT_LOMOISH).apply(
+    public void process(Photo src, Photo dst) {
+        getEffect(EffectFactory.EFFECT_LOMOISH).apply(
                 src.texture(), src.width(), src.height(), dst.texture());
     }
 }
index 0b3837f..94bf87e 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.photoeditor.filters;
 
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -31,8 +30,8 @@ public class NegativeFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        getEffect(context, EffectFactory.EFFECT_NEGATIVE).apply(
+    public void process(Photo src, Photo dst) {
+        getEffect(EffectFactory.EFFECT_NEGATIVE).apply(
                 src.texture(), src.width(), src.height(), dst.texture());
     }
 }
index 51202dc..96f5985 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.photoeditor.filters;
 
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -31,8 +30,8 @@ public class PosterizeFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        getEffect(context, EffectFactory.EFFECT_POSTERIZE).apply(
+    public void process(Photo src, Photo dst) {
+        getEffect(EffectFactory.EFFECT_POSTERIZE).apply(
                 src.texture(), src.width(), src.height(), dst.texture());
     }
 }
index 559819d..b499154 100644 (file)
@@ -18,7 +18,6 @@ package com.android.gallery3d.photoeditor.filters;
 
 import android.graphics.PointF;
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -41,8 +40,8 @@ public class RedEyeFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_REDEYE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_REDEYE);
         float[] centers = new float[redeyes.size() * 2];
         int i = 0;
         for (PointF eye : redeyes) {
index 17f99fd..548cc59 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -35,11 +34,11 @@ public class RotateFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
+    public void process(Photo src, Photo dst) {
         if (degrees % 180 != 0) {
             dst.changeDimension(src.height(), src.width());
         }
-        Effect effect = getEffect(context, EffectFactory.EFFECT_ROTATE);
+        Effect effect = getEffect(EffectFactory.EFFECT_ROTATE);
         effect.setParameter("angle", (int) degrees);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index d3b5f70..b2c7cce 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class SaturationFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_SATURATE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_SATURATE);
         effect.setParameter("scale", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index 3fdda15..170b95d 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.android.gallery3d.photoeditor.filters;
 
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -31,8 +30,8 @@ public class SepiaFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        getEffect(context, EffectFactory.EFFECT_SEPIA).apply(
+    public void process(Photo src, Photo dst) {
+        getEffect(EffectFactory.EFFECT_SEPIA).apply(
                 src.texture(), src.width(), src.height(), dst.texture());
     }
 }
index b23ef1c..7931874 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class ShadowFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_BLACKWHITE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_BLACKWHITE);
         effect.setParameter("black", black);
         effect.setParameter("white", 1f);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
index a809712..e6f7cd5 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class SharpenFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_SHARPEN);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_SHARPEN);
         effect.setParameter("scale", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index ffeb445..30a8ac5 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -37,8 +36,8 @@ public class StraightenFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_STRAIGHTEN);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_STRAIGHTEN);
         effect.setParameter("maxAngle", MAX_DEGREES);
         effect.setParameter("angle", angle);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
index bbaf9c7..e5e467b 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -35,8 +34,8 @@ public class TintFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_TINT);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_TINT);
         effect.setParameter("tint", tint);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
index 2c15972..ec39393 100644 (file)
@@ -17,7 +17,6 @@
 package com.android.gallery3d.photoeditor.filters;
 
 import android.media.effect.Effect;
-import android.media.effect.EffectContext;
 import android.media.effect.EffectFactory;
 
 import com.android.gallery3d.photoeditor.Photo;
@@ -40,8 +39,8 @@ public class VignetteFilter extends Filter {
     }
 
     @Override
-    public void process(EffectContext context, Photo src, Photo dst) {
-        Effect effect = getEffect(context, EffectFactory.EFFECT_VIGNETTE);
+    public void process(Photo src, Photo dst) {
+        Effect effect = getEffect(EffectFactory.EFFECT_VIGNETTE);
         effect.setParameter("scale", scale);
         effect.apply(src.texture(), src.width(), src.height(), dst.texture());
     }
diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java
new file mode 100644 (file)
index 0000000..0ba3d5d
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import com.android.gallery3d.common.Utils;
+
+// 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 {
+    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;
+
+    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 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);
+    }
+
+    private int getX(float f) {
+        return (int) Utils.clamp(
+                Math.round(mStartX + f * mDistance * mCosAngle), mMinX, mMaxX);
+    }
+
+    private int getY(float f) {
+        return (int) Utils.clamp(
+                Math.round(mStartY + f * mDistance * mSinAngle), mMinY, mMaxY);
+    }
+}
index dea9301..f0e5142 100644 (file)
@@ -145,7 +145,7 @@ public class PhotoView extends GLView {
             mScreenNails[i] = new ScreenNailEntry();
         }
 
-        mPositionController = new PositionController(this);
+        mPositionController = new PositionController(this, context);
         mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
     }
 
@@ -437,7 +437,7 @@ public class PhotoView extends GLView {
         if (mTransitionMode != TRANS_NONE) return false;
 
         // Decide whether to swiping to the next/prev image in the zoom-in case
-        RectF bounds = mPositionController.getImageBounds();
+        RectF bounds = controller.getImageBounds();
         int left = Math.round(bounds.left);
         int right = Math.round(bounds.right);
         int threshold = SWITCH_THRESHOLD + gapToSide(right - left, width);
@@ -480,9 +480,12 @@ public class PhotoView extends GLView {
         @Override
         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                 float velocityY) {
-            mIgnoreUpEvent = true;
-            if (!swipeImages(velocityX) && mTransitionMode == TRANS_NONE) {
-                mPositionController.up();
+            if (swipeImages(velocityX)) {
+                mIgnoreUpEvent = true;
+            } else if (mTransitionMode != TRANS_NONE) {
+                // do nothing
+            } else if (mPositionController.fling(velocityX, velocityY)) {
+                mIgnoreUpEvent = true;
             }
             return true;
         }
@@ -694,7 +697,13 @@ public class PhotoView extends GLView {
 
             int width = mTexture.getWidth();
             int height = mTexture.getHeight();
-            float s = mPositionController.getMinimalScale(width, height, mRotation);
+
+            // Calculate the initial scale that will used by PositionController
+            // (usually fit-to-screen)
+            float s = ((mRotation / 90) & 0x01) == 0
+                    ? mPositionController.getMinimalScale(width, height)
+                    : mPositionController.getMinimalScale(height, width);
+
             mDrawWidth = Math.round(width * s);
             mDrawHeight = Math.round(height * s);
         }
@@ -744,7 +753,8 @@ public class PhotoView extends GLView {
         mShowVideoPlayIcon = show;
     }
 
-    public Position retrieveOldPosition() {
+    // Returns the position saved by the previous page.
+    public Position retrieveSavedPosition() {
         if (mOpenedItemPath != null) {
             Position position = PositionRepository
                     .getInstance(mActivity).get(Long.valueOf(
index 8d5563e..5d66f70 100644 (file)
@@ -31,25 +31,32 @@ import android.os.SystemClock;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
+import android.widget.Scroller;
 
 class PositionController {
+    private static final String TAG = "PositionController";
     private long mAnimationStartTime = NO_ANIMATION;
     private static final long NO_ANIMATION = -1;
     private static final long LAST_ANIMATION = -2;
 
-    // Animation time in milliseconds.
-    private static final float ANIM_TIME_SCROLL = 0;
-    private static final float ANIM_TIME_SCALE = 50;
-    private static final float ANIM_TIME_SNAPBACK = 600;
-    private static final float ANIM_TIME_SLIDE = 400;
-    private static final float ANIM_TIME_ZOOM = 300;
-
     private int mAnimationKind;
+    private float mAnimationDuration;
     private final static int ANIM_KIND_SCROLL = 0;
     private final static int ANIM_KIND_SCALE = 1;
     private final static int ANIM_KIND_SNAPBACK = 2;
     private final static int ANIM_KIND_SLIDE = 3;
     private final static int ANIM_KIND_ZOOM = 4;
+    private final static int ANIM_KIND_FLING = 5;
+
+    // Animation time in milliseconds. The order must match ANIM_KIND_* above.
+    private final static int ANIM_TIME[] = {
+        0,    // ANIM_KIND_SCROLL
+        50,   // ANIM_KIND_SCALE
+        600,  // ANIM_KIND_SNAPBACK
+        400,  // ANIM_KIND_SLIDE
+        300,  // ANIM_KIND_ZOOM
+        0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
+    };
 
     // 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.
@@ -60,27 +67,37 @@ class PositionController {
     private int mViewW, mViewH;
 
     // The X, Y are the coordinate on bitmap which shows on the center of
-    // the view. We always keep the mCurrent{X,Y,SCALE} sync with the actual
+    // the view. We always keep the mCurrent{X,Y,Scale} sync with the actual
     // values used currently.
     private int mCurrentX, mFromX, mToX;
     private int mCurrentY, mFromY, mToY;
     private float mCurrentScale, mFromScale, mToScale;
 
-    // The offsets from the center of the view to the user's focus point,
-    // converted to the bitmap domain.
-    private float mPrevOffsetX;
-    private float mPrevOffsetY;
+    // The focus point of the scaling gesture (in bitmap coordinates).
+    private int mFocusBitmapX;
+    private int mFocusBitmapY;
     private boolean mInScale;
-    private boolean mUseViewSize = true;
 
-    // The limits for position and scale.
-    private float mScaleMin, mScaleMax = 4f;
+    // The minimum and maximum scale we allow.
+    private float mScaleMin, mScaleMax = SCALE_LIMIT;
+
+    // This is used by the fling animation
+    private FlingScroller mScroller;
+
+    // The bound of the stable region, see the comments above
+    // calculateStableBound() for details.
+    private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
+
+    // Assume the image size is the same as view size before we know the actual
+    // size of image.
+    private boolean mUseViewSize = true;
 
     private RectF mTempRect = new RectF();
     private float[] mTempPoints = new float[8];
 
-    PositionController(PhotoView viewer) {
+    public PositionController(PhotoView viewer, Context context) {
         mViewer = viewer;
+        mScroller = new FlingScroller();
     }
 
     public void setImageSize(int width, int height) {
@@ -103,6 +120,7 @@ class PositionController {
         float ratio = Math.min(
                 (float) mImageW / width, (float) mImageH / height);
 
+        // See the comment above translate() for details.
         mCurrentX = translate(mCurrentX, mImageW, width, ratio);
         mCurrentY = translate(mCurrentY, mImageH, height, ratio);
         mCurrentScale = mCurrentScale * ratio;
@@ -115,14 +133,19 @@ class PositionController {
         mToY = translate(mToY, mImageH, height, ratio);
         mToScale = mToScale * ratio;
 
+        mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
+        mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
+
         mImageW = width;
         mImageH = height;
 
-        mScaleMin = getMinimalScale(width, height, 0);
+        mScaleMin = getMinimalScale(mImageW, mImageH);
 
-        // Scale the new image to fit into the old one
-        Position position = mViewer.retrieveOldPosition();
+        // Start animation from the saved position if we have one.
+        Position position = mViewer.retrieveSavedPosition();
         if (position != null) {
+            // The animation starts from 240 pixels and centers at the image
+            // at the saved position.
             float scale = 240f / Math.min(width, height);
             mCurrentX = Math.round((mViewW / 2f - position.x) / scale) + mImageW / 2;
             mCurrentY = Math.round((mViewH / 2f - position.y) / scale) + mImageH / 2;
@@ -137,23 +160,14 @@ class PositionController {
 
     public void zoomIn(float tapX, float tapY, float targetScale) {
         if (targetScale > mScaleMax) targetScale = mScaleMax;
-        float scale = mCurrentScale;
-        float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX;
-        float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY;
-
-        // mCurrentX + (mViewW / 2) * (1 / targetScale) < mImageW
-        // mCurrentX - (mViewW / 2) * (1 / targetScale) > 0
-        float min = mViewW / 2.0f / targetScale;
-        float max = mImageW  - mViewW / 2.0f / targetScale;
-        int targetX = (int) Utils.clamp(tempX, min, max);
 
-        min = mViewH / 2.0f / targetScale;
-        max = mImageH  - mViewH / 2.0f / targetScale;
-        int targetY = (int) Utils.clamp(tempY,  min, max);
+        // Convert the tap position to image coordinate
+        int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
+        int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
 
-        // If the width of the image is less then the view, center the image
-        if (mImageW * targetScale < mViewW) targetX = mImageW / 2;
-        if (mImageH * targetScale < mViewH) targetY = mImageH / 2;
+        calculateStableBound(targetScale);
+        int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
+        int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
 
         startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
     }
@@ -162,15 +176,38 @@ class PositionController {
         startAnimation(mImageW / 2, mImageH / 2, mScaleMin, ANIM_KIND_ZOOM);
     }
 
-    public float getMinimalScale(int w, int h, int rotation) {
-        return Math.min(SCALE_LIMIT, ((rotation / 90) & 0x01) == 0
-                ? Math.min((float) mViewW / w, (float) mViewH / h)
-                : Math.min((float) mViewW / h, (float) mViewH / w));
+    public float getMinimalScale(int w, int h) {
+        return Math.min(SCALE_LIMIT,
+                Math.min((float) mViewW / w, (float) mViewH / h));
     }
 
-    private static int translate(int value, int size, int updateSize, float ratio) {
-        return Math.round(
-                (value + (updateSize * ratio - size) / 2f) / ratio);
+    // Translate a coordinate on bitmap if the bitmap size changes.
+    // If the aspect ratio doesn't change, it's easy:
+    //
+    //         r  = w / w' (= h / h')
+    //         x' = x / r
+    //         y' = y / r
+    //
+    // However the aspect ratio may change. That happens when the user slides
+    // a image before it's loaded, we don't know the actual aspect ratio, so
+    // we will assume one. When we receive the actual bitmap size, we need to
+    // translate the coordinate from the old bitmap into the new bitmap.
+    //
+    // What we want to do is center the bitmap at the original position.
+    //
+    //         ...+--+...
+    //         .  |  |  .
+    //         .  |  |  .
+    //         ...+--+...
+    //
+    // First we scale down the new bitmap by a factor r = min(w/w', h/h').
+    // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
+    // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
+    // the old bitmap maps to (x', y') in the new bitmap, where
+    //         x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
+    //         y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
+    private static int translate(int value, int size, int newSize, float ratio) {
+        return Math.round(newSize / 2f + (value - size / 2f) / ratio);
     }
 
     public void setViewSize(int viewW, int viewH) {
@@ -185,17 +222,24 @@ class PositionController {
             mCurrentX = mImageW / 2;
             mCurrentY = mImageH / 2;
             mCurrentScale = 1;
+            mScaleMin = 1;
+            mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+            return;
+        }
+
+        // In most cases we want to keep the scaling factor intact when the
+        // view size changes. The cases we want to reset the scaling factor
+        // (to fit the view if possible) are (1) the scaling factor is too
+        // small for the new view size (2) the scaling factor has not been
+        // changed by the user.
+        boolean wasMinScale = (mCurrentScale == mScaleMin);
+        mScaleMin = getMinimalScale(mImageW, mImageH);
+
+        if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
+            mCurrentX = mImageW / 2;
+            mCurrentY = mImageH / 2;
+            mCurrentScale = mScaleMin;
             mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
-        } else {
-            boolean wasMinScale = (mCurrentScale == mScaleMin);
-            mScaleMin = Math.min(SCALE_LIMIT, Math.min(
-                    (float) viewW / mImageW, (float) viewH / mImageH));
-            if (needLayout || mCurrentScale < mScaleMin || wasMinScale) {
-                mCurrentX = mImageW / 2;
-                mCurrentY = mImageH / 2;
-                mCurrentScale = mScaleMin;
-                mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
-            }
         }
     }
 
@@ -211,31 +255,26 @@ class PositionController {
         mCurrentScale = mToScale;
     }
 
-    public void scrollBy(float dx, float dy, int type) {
-        startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
-                getTargetY() + Math.round(dy / mCurrentScale),
-                mCurrentScale, type);
-    }
-
     public void beginScale(float focusX, float focusY) {
         mInScale = true;
-        mPrevOffsetX = (focusX - mViewW / 2f) / mCurrentScale;
-        mPrevOffsetY = (focusY - mViewH / 2f) / mCurrentScale;
+        mFocusBitmapX = Math.round(mCurrentX +
+                (focusX - mViewW / 2f) / mCurrentScale);
+        mFocusBitmapY = Math.round(mCurrentY +
+                (focusY - mViewH / 2f) / mCurrentScale);
     }
 
     public void scaleBy(float s, float focusX, float focusY) {
 
-        // The focus point should keep this position on the ImageView.
-        // So, mCurrentX + mPrevOffsetX = mCurrentX' + offsetX.
-        // mCurrentY + mPrevOffsetY = mCurrentY' + offsetY.
-        float offsetX = (focusX - mViewW / 2f) / mCurrentScale;
-        float offsetY = (focusY - mViewH / 2f) / mCurrentScale;
+        // We want to keep the focus point (on the bitmap) the same as when
+        // we begin the scale guesture, that is,
+        //
+        // mCurrentX' + (focusX - mViewW / 2f) / scale = mFocusBitmapX
+        //
+        s *= getTargetScale();
+        int x = Math.round(mFocusBitmapX - (focusX - mViewW / 2f) / s);
+        int y = Math.round(mFocusBitmapY - (focusY - mViewH / 2f) / s);
 
-        startAnimation(getTargetX() - Math.round(offsetX - mPrevOffsetX),
-                       getTargetY() - Math.round(offsetY - mPrevOffsetY),
-                       getTargetScale() * s, ANIM_KIND_SCALE);
-        mPrevOffsetX = offsetX;
-        mPrevOffsetY = offsetY;
+        startAnimation(x, y, s, ANIM_KIND_SCALE);
     }
 
     public void endScale() {
@@ -260,15 +299,26 @@ class PositionController {
         startSnapback();
     }
 
+    //             |<--| (1/2) * mImageW
+    // +-------+-------+-------+
+    // |       |       |       |
+    // |       |   o   |       |
+    // |       |       |       |
+    // +-------+-------+-------+
+    // |<----------| (3/2) * mImageW
+    // Slide in the image from left or right.
+    // Precondition: mCurrentScale = 1 (mView{W|H} == mImage{W|H}).
+    // Sliding from left:  mCurrentX = (1/2) * mImageW
+    //              right: mCurrentX = (3/2) * mImageW
     public void startSlideInAnimation(int direction) {
         int fromX = (direction == PhotoView.TRANS_SLIDE_IN_LEFT) ?
-                mViewW : -mViewW;
-        mFromX = Math.round(fromX + (mImageW - mViewW) / 2f);
+                mImageW / 2 : 3 * mImageW / 2;
+        mFromX = Math.round(fromX);
         mFromY = Math.round(mImageH / 2f);
         mCurrentX = mFromX;
         mCurrentY = mFromY;
-        startAnimation(mImageW / 2, mImageH / 2, mCurrentScale,
-                ANIM_KIND_SLIDE);
+        startAnimation(
+                mImageW / 2, mImageH / 2, mCurrentScale, ANIM_KIND_SLIDE);
     }
 
     public void startHorizontalSlide(int distance) {
@@ -279,27 +329,57 @@ class PositionController {
         scrollBy(dx, dy, ANIM_KIND_SCROLL);
     }
 
+    private void scrollBy(float dx, float dy, int type) {
+        startAnimation(getTargetX() + Math.round(dx / mCurrentScale),
+                getTargetY() + Math.round(dy / mCurrentScale),
+                mCurrentScale, type);
+    }
+
+    public boolean fling(float velocityX, float velocityY) {
+        // We only want to do fling when the picture is zoomed-in.
+        if (mImageW * mCurrentScale <= mViewW &&
+            mImageH * mCurrentScale <= mViewH) {
+            return false;
+        }
+
+        calculateStableBound(mCurrentScale);
+        mScroller.fling(mCurrentX, mCurrentY,
+                Math.round(-velocityX / mCurrentScale),
+                Math.round(-velocityY / mCurrentScale),
+                mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
+        int targetX = mScroller.getFinalX();
+        int targetY = mScroller.getFinalY();
+        mAnimationDuration = mScroller.getDuration();
+        startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
+        return true;
+    }
+
     private void startAnimation(
-            int centerX, int centerY, float scale, int kind) {
-        if (centerX == mCurrentX && centerY == mCurrentY
+            int targetX, int targetY, float scale, int kind) {
+        if (targetX == mCurrentX && targetY == mCurrentY
                 && scale == mCurrentScale) return;
 
         mFromX = mCurrentX;
         mFromY = mCurrentY;
         mFromScale = mCurrentScale;
 
-        mToX = centerX;
-        mToY = centerY;
+        mToX = targetX;
+        mToY = targetY;
         mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
 
-        // If the scaled dimension is smaller than the view,
+        // If the scaled height is smaller than the view height,
         // force it to be in the center.
+        // (We do for height only, not width, because the user may
+        // want to scroll to the previous/next image.)
         if (Math.floor(mImageH * mToScale) <= mViewH) {
             mToY = mImageH / 2;
         }
 
         mAnimationStartTime = SystemClock.uptimeMillis();
         mAnimationKind = kind;
+        if (mAnimationKind != ANIM_KIND_FLING) {
+            mAnimationDuration = ANIM_TIME[mAnimationKind];
+        }
         if (advanceAnimation()) mViewer.invalidate();
     }
 
@@ -317,25 +397,12 @@ class PositionController {
             }
         }
 
-        float animationTime;
-        if (mAnimationKind == ANIM_KIND_SCROLL) {
-            animationTime = ANIM_TIME_SCROLL;
-        } else if (mAnimationKind == ANIM_KIND_SCALE) {
-            animationTime = ANIM_TIME_SCALE;
-        } else if (mAnimationKind == ANIM_KIND_SLIDE) {
-            animationTime = ANIM_TIME_SLIDE;
-        } else if (mAnimationKind == ANIM_KIND_ZOOM) {
-            animationTime = ANIM_TIME_ZOOM;
-        } else /* if (mAnimationKind == ANIM_KIND_SNAPBACK) */ {
-            animationTime = ANIM_TIME_SNAPBACK;
-        }
-
+        long now = SystemClock.uptimeMillis();
         float progress;
-        if (animationTime == 0) {
+        if (mAnimationDuration == 0) {
             progress = 1;
         } else {
-            long now = SystemClock.uptimeMillis();
-            progress = (now - mAnimationStartTime) / animationTime;
+            progress = (now - mAnimationStartTime) / mAnimationDuration;
         }
 
         if (progress >= 1) {
@@ -346,37 +413,64 @@ class PositionController {
             mAnimationStartTime = LAST_ANIMATION;
         } else {
             float f = 1 - progress;
-            if (mAnimationKind == ANIM_KIND_SCROLL) {
-                progress = 1 - f;  // linear
-            } else if (mAnimationKind == ANIM_KIND_SCALE) {
-                progress = 1 - f * f;  // quadratic
-            } else /* if mAnimationKind is ANIM_KIND_SNAPBACK,
-                        ANIM_KIND_ZOOM or ANIM_KIND_SLIDE */ {
-                progress = 1 - f * f * f * f * f; // x^5
+            switch (mAnimationKind) {
+                case ANIM_KIND_SCROLL:
+                case ANIM_KIND_FLING:
+                    progress = 1 - f;  // linear
+                    break;
+                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;
+            }
+            if (mAnimationKind == ANIM_KIND_FLING) {
+                flingInterpolate(progress);
+            } else {
+                linearInterpolate(progress);
             }
-            linearInterpolate(progress);
         }
         mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
         return true;
     }
 
+    private void flingInterpolate(float progress) {
+        mScroller.computeScrollOffset(progress);
+        mCurrentX = mScroller.getCurrX();
+        mCurrentY = mScroller.getCurrY();
+        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+    }
+
+    // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
     private void linearInterpolate(float progress) {
-        // To linearly interpolate the position, we have to translate the
-        // coordinates. The meaning of the translated point (x, y) is the
-        // coordinates of the center of the bitmap on the view component.
-        float fromX = mViewW / 2f + (mImageW / 2f - mFromX) * mFromScale;
-        float toX = mViewW / 2f + (mImageW / 2f - mToX) * mToScale;
+        // To linearly interpolate the position on view coordinates, we do the
+        // following steps:
+        // (1) convert a bitmap position (x, y) to view coordinates:
+        //     from: (x - mFromX) * mFromScale + mViewW / 2
+        //     to: (x - mToX) * mToScale + mViewW / 2
+        // (2) interpolate between the "from" and "to" coordinates:
+        //     (x - mFromX) * mFromScale * (1 - p) + (x - mToX) * mToScale * p
+        //     + mViewW / 2
+        //     should be equal to
+        //     (x - mCurrentX) * mCurrentScale + mViewW / 2
+        // (3) The x-related terms in the above equation can be removed because
+        //     mFromScale * (1 - p) + ToScale * p = mCurrentScale
+        // (4) Solve for mCurrentX, we have mCurrentX =
+        // (mFromX * mFromScale * (1 - p) + mToX * mToScale * p) / mCurrentScale
+        float fromX = mFromX * mFromScale;
+        float toX = mToX * mToScale;
         float currentX = fromX + progress * (toX - fromX);
 
-        float fromY = mViewH / 2f + (mImageH / 2f - mFromY) * mFromScale;
-        float toY = mViewH / 2f + (mImageH / 2f - mToY) * mToScale;
+        float fromY = mFromY * mFromScale;
+        float toY = mToY * mToScale;
         float currentY = fromY + progress * (toY - fromY);
 
         mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
-        mCurrentX = Math.round(
-                mImageW / 2f + (mViewW / 2f - currentX) / mCurrentScale);
-        mCurrentY = Math.round(
-                mImageH / 2f + (mViewH / 2f - currentY) / mCurrentScale);
+        mCurrentX = Math.round(currentX / mCurrentScale);
+        mCurrentY = Math.round(currentY / mCurrentScale);
     }
 
     // Returns true if redraw is needed.
@@ -391,8 +485,6 @@ class PositionController {
 
     public boolean startSnapback() {
         boolean needAnimation = false;
-        int x = mCurrentX;
-        int y = mCurrentY;
         float scale = mCurrentScale;
 
         if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
@@ -400,36 +492,12 @@ class PositionController {
             scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
         }
 
-        // The number of pixels when the edge is aligned.
-        int left = (int) Math.ceil(mViewW / (2 * scale));
-        int right = mImageW - left;
-        int top = (int) Math.ceil(mViewH / (2 * scale));
-        int bottom = mImageH - top;
-
-        if (mImageW * scale > mViewW) {
-            if (mCurrentX < left) {
-                needAnimation = true;
-                x = left;
-            } else if (mCurrentX > right) {
-                needAnimation = true;
-                x = right;
-            }
-        } else if (mCurrentX != mImageW / 2) {
-            needAnimation = true;
-            x = mImageW / 2;
-        }
+        calculateStableBound(scale);
+        int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
+        int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
 
-        if (mImageH * scale > mViewH) {
-            if (mCurrentY < top) {
-                needAnimation = true;
-                y = top;
-            } else if (mCurrentY > bottom) {
-                needAnimation = true;
-                y = bottom;
-            }
-        } else if (mCurrentY != mImageH / 2) {
+        if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
             needAnimation = true;
-            y = mImageH / 2;
         }
 
         if (needAnimation) {
@@ -439,22 +507,54 @@ class PositionController {
         return needAnimation;
     }
 
+    // Calculates the stable region of mCurrent{X/Y}, 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}.
+    //
+    private void calculateStableBound(float scale) {
+        // The number of pixels between the center of the view
+        // and the edge when the edge is aligned.
+        mBoundLeft = (int) Math.ceil(mViewW / (2 * scale));
+        mBoundRight = mImageW - mBoundLeft;
+        mBoundTop = (int) Math.ceil(mViewH / (2 * scale));
+        mBoundBottom = mImageH - mBoundTop;
+
+        // If the scaled height is smaller than the view height,
+        // force it to be in the center.
+        if (Math.floor(mImageH * scale) <= mViewH) {
+            mBoundTop = mBoundBottom = mImageH / 2;
+        }
+
+        // Same for width
+        if (Math.floor(mImageW * scale) <= mViewW) {
+            mBoundLeft = mBoundRight = mImageW / 2;
+        }
+    }
+
+    private boolean useCurrentValueAsTarget() {
+        return mAnimationStartTime == NO_ANIMATION ||
+                mAnimationKind == ANIM_KIND_SNAPBACK ||
+                mAnimationKind == ANIM_KIND_FLING;
+    }
+
     private float getTargetScale() {
-        if (mAnimationStartTime == NO_ANIMATION
-                || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale;
-        return mToScale;
+        return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
     }
 
     private int getTargetX() {
-        if (mAnimationStartTime == NO_ANIMATION
-                || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX;
-        return mToX;
+        return useCurrentValueAsTarget() ? mCurrentX : mToX;
     }
 
     private int getTargetY() {
-        if (mAnimationStartTime == NO_ANIMATION
-                || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY;
-        return mToY;
+        return useCurrentValueAsTarget() ? mCurrentY : mToY;
     }
 
     public RectF getImageBounds() {