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>
-->
<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>
-->
<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>
<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>
-->
<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>
<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>
<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>
<!-- 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>
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;
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);
}
}
@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;
}
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
+import android.widget.ViewSwitcher;
import com.android.gallery3d.R;
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();
+ }
+ }
}
}
package com.android.gallery3d.photoeditor;
import android.graphics.Bitmap;
-import android.media.effect.EffectContext;
import com.android.gallery3d.photoeditor.filters.Filter;
private final PhotoView photoView;
private final StackListener stackListener;
- private EffectContext effectContext;
private Photo source;
private Runnable queuedTopFilterChange;
private volatile boolean paused;
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());
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);
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;
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();
@Override
public void run() {
// Create effect context after GL context is created or recreated.
- effectContext = EffectContext.createWithCurrentGlContext();
+ Filter.createContextWithCurrentGlContext();
}
});
paused = false;
*/
public class PhotoEditor extends Activity {
- private Uri uri;
+ private Uri sourceUri;
+ private Uri saveUri;
private FilterStack filterStack;
private ActionBar actionBar;
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),
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));
}
});
}
};
- new LoadScreennailTask(this, callback).execute(uri);
+ new LoadScreennailTask(this, callback).execute(sourceUri);
}
private Runnable createUndoRedoRunnable(final boolean undo, final EffectsBar effectsBar) {
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);
}
});
}
};
}
+ 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() {
public class EffectToolFactory {
public enum ScalePickerType {
- FILLLIGHT, HIGHLIGHT, SHADOW, COLOR, GENERIC
+ LIGHT, SHADOW, COLOR, GENERIC
}
private final ViewGroup effectToolPanel;
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;
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
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
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;
}
@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());
}
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;
}
@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());
}
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;
}
@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());
package com.android.gallery3d.photoeditor.filters;
-import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import com.android.gallery3d.photoeditor.Photo;
}
@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());
}
}
package com.android.gallery3d.photoeditor.filters;
-import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import com.android.gallery3d.photoeditor.Photo;
}
@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());
}
}
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;
}
@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);
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());
}
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;
}
@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());
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;
}
@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());
}
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;
}
@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());
}
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.
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);
}
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;
}
@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());
}
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;
}
@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());
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;
}
@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());
}
package com.android.gallery3d.photoeditor.filters;
-import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import com.android.gallery3d.photoeditor.Photo;
}
@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());
}
}
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;
}
@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());
package com.android.gallery3d.photoeditor.filters;
-import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import com.android.gallery3d.photoeditor.Photo;
}
@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());
}
}
package com.android.gallery3d.photoeditor.filters;
-import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import com.android.gallery3d.photoeditor.Photo;
}
@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());
}
}
package com.android.gallery3d.photoeditor.filters;
-import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import com.android.gallery3d.photoeditor.Photo;
}
@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());
}
}
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;
}
@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) {
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;
}
@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());
}
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;
}
@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());
}
package com.android.gallery3d.photoeditor.filters;
-import android.media.effect.EffectContext;
import android.media.effect.EffectFactory;
import com.android.gallery3d.photoeditor.Photo;
}
@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());
}
}
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;
}
@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());
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;
}
@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());
}
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;
}
@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());
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;
}
@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());
}
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;
}
@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());
}
--- /dev/null
+/*
+ * 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);
+ }
+}
mScreenNails[i] = new ScreenNailEntry();
}
- mPositionController = new PositionController(this);
+ mPositionController = new PositionController(this, context);
mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
}
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);
@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;
}
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);
}
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(
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.
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) {
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;
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;
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);
}
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) {
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);
- }
}
}
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() {
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) {
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();
}
}
}
- 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) {
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.
public boolean startSnapback() {
boolean needAnimation = false;
- int x = mCurrentX;
- int y = mCurrentY;
float scale = mCurrentScale;
if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
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) {
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() {