summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/photoeditor
diff options
context:
space:
mode:
authorYuli Huang <yuli@google.com>2011-09-12 22:25:30 +0800
committerYuli Huang <yuli@google.com>2011-09-13 00:36:00 +0800
commitab5055ac921f194f4d43f173881d17cf76719efd (patch)
tree7b1c64bd30c8185b6363069c006b701a9878cc34 /src/com/android/gallery3d/photoeditor
parent6a5a246ca7973054364f5ee8ca2df3164bf38458 (diff)
downloadandroid_packages_apps_Snap-ab5055ac921f194f4d43f173881d17cf76719efd.tar.gz
android_packages_apps_Snap-ab5055ac921f194f4d43f173881d17cf76719efd.tar.bz2
android_packages_apps_Snap-ab5055ac921f194f4d43f173881d17cf76719efd.zip
Fix b/4643148: Make PhotoEditor integrated into Gallery.
1. Move PhotoEditor code/resources into Gallery for single apk. 2. Change PhotoEditor package to com.android.gallery3d.photoeditor. 3. Rename PhotoEditor resources to avoid mess up Gallery resources. 4. Move Doodle effect from fix-effects to color-effects. 5. Update PhotoEditor bottom action-bar background. Change-Id: I1a2f7d27d89a14fe6a0435575b993ed8b75e6bf4
Diffstat (limited to 'src/com/android/gallery3d/photoeditor')
-rw-r--r--src/com/android/gallery3d/photoeditor/ActionBar.java138
-rw-r--r--src/com/android/gallery3d/photoeditor/BitmapUtils.java239
-rw-r--r--src/com/android/gallery3d/photoeditor/EffectsBar.java186
-rw-r--r--src/com/android/gallery3d/photoeditor/FilterStack.java253
-rw-r--r--src/com/android/gallery3d/photoeditor/LoadScreennailTask.java72
-rw-r--r--src/com/android/gallery3d/photoeditor/OnDoneBitmapCallback.java27
-rw-r--r--src/com/android/gallery3d/photoeditor/OnDoneCallback.java25
-rw-r--r--src/com/android/gallery3d/photoeditor/Photo.java81
-rw-r--r--src/com/android/gallery3d/photoeditor/PhotoEditor.java130
-rw-r--r--src/com/android/gallery3d/photoeditor/PhotoView.java161
-rw-r--r--src/com/android/gallery3d/photoeditor/RendererUtils.java285
-rw-r--r--src/com/android/gallery3d/photoeditor/SaveCopyTask.java153
-rw-r--r--src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java72
-rw-r--r--src/com/android/gallery3d/photoeditor/Toolbar.java159
-rw-r--r--src/com/android/gallery3d/photoeditor/ToolbarIdleHandler.java91
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java59
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java46
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java131
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java59
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/CropAction.java66
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/CropView.java250
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java42
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java42
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/DoodleAction.java81
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/DoodlePaint.java34
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/DoodleView.java171
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/EffectAction.java168
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java103
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/FillLightAction.java59
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java62
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/FlipAction.java86
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/FullscreenToolView.java93
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/GrainAction.java62
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java42
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/HighlightAction.java59
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/LomoishAction.java42
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/NegativeAction.java42
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java42
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java55
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/RotateAction.java113
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/RotateView.java213
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/SaturationAction.java59
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/ScaleSeekBar.java63
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/SepiaAction.java42
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/ShadowAction.java59
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/SharpenAction.java62
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/StraightenAction.java72
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/TintAction.java62
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/TouchView.java119
-rw-r--r--src/com/android/gallery3d/photoeditor/actions/VignetteAction.java62
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/CropFilter.java53
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java38
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java38
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java88
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java46
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/Filter.java72
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/FlipFilter.java46
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/GrainFilter.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java38
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java49
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java38
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java38
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java38
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java55
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/RotateFilter.java46
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java38
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java49
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java48
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java46
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/TintFilter.java43
-rw-r--r--src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java51
77 files changed, 6188 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/photoeditor/ActionBar.java b/src/com/android/gallery3d/photoeditor/ActionBar.java
new file mode 100644
index 000000000..52dfb0bd7
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/ActionBar.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+/**
+ * Action bar that contains buttons such as undo, redo, save, etc. and listens to stack changes for
+ * enabling/disabling buttons.
+ */
+public class ActionBar extends RelativeLayout implements FilterStack.StackListener {
+
+ /**
+ * Listener of action button clicked.
+ */
+ public interface ActionBarListener {
+
+ void onUndo();
+
+ void onRedo();
+
+ void onSave();
+ }
+
+ private static final int ENABLE_BUTTON = 1;
+ private static final float ENABLED_ALPHA = 1;
+ private static final float DISABLED_ALPHA = 0.47f;
+
+ private final Handler handler;
+ private View undo;
+ private View redo;
+ private View save;
+
+ public ActionBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ handler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ENABLE_BUTTON:
+ boolean canUndo = (msg.arg1 > 0);
+ boolean canRedo = (msg.arg2 > 0);
+ enableButton(undo, canUndo);
+ enableButton(redo, canRedo);
+ enableButton(save, canUndo);
+ break;
+ }
+ }
+ };
+ }
+
+ /**
+ * Initializes with a non-null ActionBarListener.
+ */
+ public void initialize(final ActionBarListener listener) {
+ undo = findViewById(R.id.undo_button);
+ undo.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ listener.onUndo();
+ }
+ }
+ });
+
+ redo = findViewById(R.id.redo_button);
+ redo.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ listener.onRedo();
+ }
+ }
+ });
+
+ save = findViewById(R.id.save_button);
+ save.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ listener.onSave();
+ }
+ }
+ });
+
+ resetButtons();
+ }
+
+ public void resetButtons() {
+ // Disable buttons immediately instead of waiting for ENABLE_BUTTON messages which may
+ // happen some time later after stack changes.
+ enableButton(undo, false);
+ enableButton(redo, false);
+ enableButton(save, false);
+ }
+
+ public void disableSave() {
+ enableButton(save, false);
+ }
+
+ private void enableButton(View button, boolean enabled) {
+ button.setEnabled(enabled);
+ button.setAlpha(enabled ? ENABLED_ALPHA : DISABLED_ALPHA);
+ }
+
+ @Override
+ public void onStackChanged(boolean canUndo, boolean canRedo) {
+ // Listens to stack changes that may come from the worker thread; send messages to enable
+ // buttons only in the UI thread.
+ handler.sendMessage(handler.obtainMessage(ENABLE_BUTTON, canUndo ? 1 : 0, canRedo ? 1 : 0));
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/BitmapUtils.java b/src/com/android/gallery3d/photoeditor/BitmapUtils.java
new file mode 100644
index 000000000..5211a8379
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/BitmapUtils.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Utils for bitmap operations.
+ */
+public class BitmapUtils {
+
+ private static final String TAG = "BitmapUtils";
+ private static final int DEFAULT_COMPRESS_QUALITY = 90;
+ private static final int INDEX_ORIENTATION = 0;
+
+ private static final String[] IMAGE_PROJECTION = new String[] {
+ ImageColumns.ORIENTATION
+ };
+
+ private final Context context;
+
+ public BitmapUtils(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Creates a mutable bitmap from subset of source bitmap, transformed by the optional matrix.
+ */
+ public static Bitmap createBitmap(
+ Bitmap source, int x, int y, int width, int height, Matrix m) {
+ // Re-implement Bitmap createBitmap() to always return a mutable bitmap.
+ Canvas canvas = new Canvas();
+
+ Bitmap bitmap;
+ Paint paint;
+ if ((m == null) || m.isIdentity()) {
+ bitmap = Bitmap.createBitmap(width, height, source.getConfig());
+ paint = null;
+ } else {
+ RectF rect = new RectF(0, 0, width, height);
+ m.mapRect(rect);
+ bitmap = Bitmap.createBitmap(
+ Math.round(rect.width()), Math.round(rect.height()), source.getConfig());
+
+ canvas.translate(-rect.left, -rect.top);
+ canvas.concat(m);
+
+ paint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ if (!m.rectStaysRect()) {
+ paint.setAntiAlias(true);
+ }
+ }
+ bitmap.setDensity(source.getDensity());
+ canvas.setBitmap(bitmap);
+
+ Rect srcBounds = new Rect(x, y, x + width, y + height);
+ RectF dstBounds = new RectF(0, 0, width, height);
+ canvas.drawBitmap(source, srcBounds, dstBounds, paint);
+ return bitmap;
+ }
+
+ private void closeStream(Closeable stream) {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private Rect getBitmapBounds(Uri uri) {
+ Rect bounds = new Rect();
+ InputStream is = null;
+
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, options);
+
+ bounds.right = options.outWidth;
+ bounds.bottom = options.outHeight;
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } finally {
+ closeStream(is);
+ }
+
+ return bounds;
+ }
+
+ private int getOrientation(Uri uri) {
+ int orientation = 0;
+ Cursor cursor = context.getContentResolver().query(uri, IMAGE_PROJECTION, null, null, null);
+ try {
+ if ((cursor != null) && cursor.moveToNext()) {
+ orientation = cursor.getInt(INDEX_ORIENTATION);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return orientation;
+ }
+
+ /**
+ * Decodes bitmap (maybe immutable) that keeps aspect-ratio and spans most within the bounds.
+ */
+ private Bitmap decodeBitmap(Uri uri, int width, int height) {
+ InputStream is = null;
+ Bitmap bitmap = null;
+
+ try {
+ // TODO: Take max pixels allowed into account for calculation to avoid possible OOM.
+ Rect bounds = getBitmapBounds(uri);
+ int sampleSize = Math.max(bounds.width() / width, bounds.height() / height);
+ sampleSize = Math.min(sampleSize,
+ Math.max(bounds.width() / height, bounds.height() / width));
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = Math.max(sampleSize, 1);
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ is = context.getContentResolver().openInputStream(uri);
+ bitmap = BitmapFactory.decodeStream(is, null, options);
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "FileNotFoundException: " + uri);
+ } finally {
+ closeStream(is);
+ }
+
+ // Scale down the sampled bitmap if it's still larger than the desired dimension.
+ if (bitmap != null) {
+ float scale = Math.min((float) width / bitmap.getWidth(),
+ (float) height / bitmap.getHeight());
+ scale = Math.max(scale, Math.min((float) height / bitmap.getWidth(),
+ (float) width / bitmap.getHeight()));
+ if (scale < 1) {
+ Matrix m = new Matrix();
+ m.setScale(scale, scale);
+ Bitmap transformed = createBitmap(
+ bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m);
+ bitmap.recycle();
+ return transformed;
+ }
+ }
+ return bitmap;
+ }
+
+ /**
+ * Gets decoded bitmap that keeps orientation as well.
+ */
+ public Bitmap getBitmap(Uri uri, int width, int height) {
+ Bitmap bitmap = decodeBitmap(uri, width, height);
+
+ // Rotate the decoded bitmap according to its orientation if it's necessary.
+ if (bitmap != null) {
+ int orientation = getOrientation(uri);
+ if (orientation != 0) {
+ Matrix m = new Matrix();
+ m.setRotate(orientation);
+ Bitmap transformed = createBitmap(
+ bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m);
+ bitmap.recycle();
+ return transformed;
+ }
+ }
+ return bitmap;
+ }
+
+ /**
+ * Saves the bitmap by given directory, filename, and format; if the directory is given null,
+ * then saves it under the cache directory.
+ */
+ public File saveBitmap(
+ Bitmap bitmap, String directory, String filename, CompressFormat format) {
+
+ if (directory == null) {
+ directory = context.getCacheDir().getAbsolutePath();
+ } else {
+ // Check if the given directory exists or try to create it.
+ File file = new File(directory);
+ if (!file.isDirectory() && !file.mkdirs()) {
+ return null;
+ }
+ }
+
+ File file = null;
+ OutputStream os = null;
+
+ try {
+ filename = (format == CompressFormat.PNG) ? filename + ".png" : filename + ".jpg";
+ file = new File(directory, filename);
+ os = new FileOutputStream(file);
+ bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } finally {
+ closeStream(os);
+ }
+ return file;
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/EffectsBar.java b/src/com/android/gallery3d/photoeditor/EffectsBar.java
new file mode 100644
index 000000000..6e3178e1a
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/EffectsBar.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.photoeditor.actions.EffectAction;
+import com.android.gallery3d.photoeditor.actions.EffectToolFactory;
+
+/**
+ * Effects bar that contains all effects and shows them in categorized views.
+ */
+public class EffectsBar extends LinearLayout {
+
+ private FilterStack filterStack;
+ private LayoutInflater inflater;
+ private View effectsGallery;
+ private ViewGroup effectToolPanel;
+ private EffectAction activeEffect;
+
+ public EffectsBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void initialize(FilterStack filterStack) {
+ this.filterStack = filterStack;
+ inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ setupMenuToggle(R.id.exposure_button, R.layout.photoeditor_effects_exposure);
+ setupMenuToggle(R.id.artistic_button, R.layout.photoeditor_effects_artistic);
+ setupMenuToggle(R.id.color_button, R.layout.photoeditor_effects_color);
+ setupMenuToggle(R.id.fix_button, R.layout.photoeditor_effects_fix);
+
+ setEnabled(false);
+ }
+
+ private void setupMenuToggle(int toggleId, final int effectsId) {
+ final View toggle = findViewById(toggleId);
+ toggle.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ // Toggle off to exit effects gallery that is showing. Or toggle on to show effects
+ // gallery after exiting an active effect if applicable.
+ exit((toggle.isSelected() && (effectsGallery != null)) ? null : new Runnable() {
+
+ @Override
+ public void run() {
+ toggle.setSelected(true);
+ showEffectsGallery(effectsId);
+ }
+ });
+ }
+ });
+ }
+
+ private void setupEffectListener(final EffectAction effect) {
+ effect.setListener(new EffectAction.Listener() {
+
+ @Override
+ public void onClick() {
+ if (isEnabled()) {
+ // Set the clicked effect active before exiting effects-gallery.
+ activeEffect = effect;
+ exitEffectsGallery();
+ // Create effect tool panel first before the factory could create tools within.
+ createEffectToolPanel();
+ activeEffect.begin(
+ filterStack, new EffectToolFactory(effectToolPanel, inflater));
+ }
+ }
+
+ @Override
+ public void onDone() {
+ exit(null);
+ }
+ });
+ }
+
+ private void createEffectToolPanel() {
+ effectToolPanel = (ViewGroup) inflater.inflate(
+ R.layout.photoeditor_effect_tool_panel, this, false);
+ ((TextView) effectToolPanel.findViewById(R.id.effect_label)).setText(activeEffect.name());
+ addView(effectToolPanel, 0);
+ }
+
+ private void showEffectsGallery(int effectsId) {
+ // Inflate scrollable effects-gallery and desired effects into effects-bar.
+ effectsGallery = inflater.inflate(R.layout.photoeditor_effects_gallery, this, false);
+ ViewGroup scrollView = (ViewGroup) effectsGallery.findViewById(R.id.scroll_view);
+ ViewGroup effects = (ViewGroup) inflater.inflate(effectsId, scrollView, false);
+ for (int i = 0; i < effects.getChildCount(); i++) {
+ setupEffectListener((EffectAction) effects.getChildAt(i));
+ }
+ scrollView.addView(effects);
+ scrollView.scrollTo(0, 0);
+ addView(effectsGallery, 0);
+ }
+
+ private boolean exitEffectsGallery() {
+ if (effectsGallery != null) {
+ if (activeEffect != null) {
+ // Detach the active effect from effects-gallery that could be recycled by gc.
+ ViewGroup scrollView = (ViewGroup) effectsGallery.findViewById(R.id.scroll_view);
+ ((ViewGroup) scrollView.getChildAt(0)).removeView(activeEffect);
+ }
+ removeView(effectsGallery);
+ effectsGallery = null;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean exitActiveEffect(final Runnable runnableOnDone) {
+ if (activeEffect != null) {
+ final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(
+ (ViewGroup) getRootView().findViewById(R.id.toolbar));
+ activeEffect.end(new Runnable() {
+
+ @Override
+ public void run() {
+ progressDialog.dismiss();
+ View fullscreenTool = getRootView().findViewById(R.id.fullscreen_effect_tool);
+ if (fullscreenTool != null) {
+ ((ViewGroup) fullscreenTool.getParent()).removeView(fullscreenTool);
+ }
+ removeView(effectToolPanel);
+ effectToolPanel = null;
+ activeEffect = null;
+ if (runnableOnDone != null) {
+ runnableOnDone.run();
+ }
+ }
+ });
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Exits from effects gallery or the active effect; then executes the runnable if applicable.
+ *
+ * @return true if exiting from effects gallery or the active effect; otherwise, false.
+ */
+ public boolean exit(final Runnable runnableOnDone) {
+ // Exit effects-menu selected states.
+ ViewGroup menu = (ViewGroup) findViewById(R.id.effects_menu);
+ for (int i = 0; i < menu.getChildCount(); i++) {
+ View toggle = menu.getChildAt(i);
+ if (toggle.isSelected()) {
+ toggle.setSelected(false);
+ }
+ }
+
+ if (exitActiveEffect(runnableOnDone)) {
+ return true;
+ }
+
+ boolean exited = exitEffectsGallery();
+ if (runnableOnDone != null) {
+ runnableOnDone.run();
+ }
+ return exited;
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/FilterStack.java b/src/com/android/gallery3d/photoeditor/FilterStack.java
new file mode 100644
index 000000000..3b35aa3e1
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/FilterStack.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.graphics.Bitmap;
+import android.media.effect.EffectContext;
+
+import com.android.gallery3d.photoeditor.filters.Filter;
+
+import java.util.Stack;
+
+/**
+ * A stack of filters to be applied onto a photo.
+ */
+public class FilterStack {
+
+ /**
+ * Listener of stack changes.
+ */
+ public interface StackListener {
+
+ void onStackChanged(boolean canUndo, boolean canRedo);
+ }
+
+ private final Stack<Filter> appliedStack = new Stack<Filter>();
+ private final Stack<Filter> redoStack = new Stack<Filter>();
+
+ // Use two photo buffers as in and out in turns to apply filters in the stack.
+ private final Photo[] buffers = new Photo[2];
+ private final PhotoView photoView;
+ private final StackListener stackListener;
+
+ private EffectContext effectContext;
+ private Photo source;
+ private Runnable queuedTopFilterChange;
+ private volatile boolean paused;
+
+ public FilterStack(PhotoView photoView, StackListener stackListener) {
+ this.photoView = photoView;
+ 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();
+ if (source != null) {
+ buffers[0] = Photo.create(source.width(), source.height());
+ reallocateBuffer(1);
+
+ Photo photo = source;
+ for (int i = 0; i < appliedStack.size() && !paused; i++) {
+ photo = runFilter(i);
+ }
+ // Source photo will be displayed if there is no filter stacked.
+ photoView.setPhoto(photo);
+ }
+ }
+
+ private void invalidateTopFilter() {
+ if (!appliedStack.empty()) {
+ photoView.setPhoto(runFilter(appliedStack.size() - 1));
+ }
+ }
+
+ private Photo runFilter(int filterIndex) {
+ int out = getOutBufferIndex(filterIndex);
+ Photo input = (filterIndex > 0) ? buffers[out ^ 1] : source;
+ if ((input != null) && (buffers[out] != null)) {
+ if (!buffers[out].matchDimension(input)) {
+ buffers[out].clear();
+ reallocateBuffer(out);
+ }
+ appliedStack.get(filterIndex).process(effectContext, input, buffers[out]);
+ return buffers[out];
+ }
+ return null;
+ }
+
+ private int getOutBufferIndex(int filterIndex) {
+ // buffers[0] and buffers[1] are swapped in turns as the in/out buffers for
+ // processing stacked filters. For example, the first filter reads buffer[0] and
+ // writes buffer[1]; the second filter then reads buffer[1] and writes buffer[0].
+ // The returned index should only be used when the applied filter stack isn't empty.
+ return (filterIndex + 1) % 2;
+ }
+
+ private void callbackDone(final OnDoneCallback callback) {
+ // Call back in UI thread to report done.
+ photoView.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onDone();
+ }
+ });
+ }
+
+ private void stackChanged() {
+ stackListener.onStackChanged(!appliedStack.empty(), !redoStack.empty());
+ }
+
+ public void saveBitmap(final OnDoneBitmapCallback callback) {
+ photoView.queue(new Runnable() {
+
+ @Override
+ public void run() {
+ Photo photo = appliedStack.empty() ?
+ null : buffers[getOutBufferIndex(appliedStack.size() - 1)];
+ final Bitmap bitmap = (photo != null) ? photo.save() : null;
+ photoView.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onDone(bitmap);
+ }
+ });
+ }
+ });
+ }
+
+ public void setPhotoSource(final Bitmap bitmap, final OnDoneCallback callback) {
+ photoView.queue(new Runnable() {
+
+ @Override
+ public void run() {
+ source = Photo.create(bitmap);
+ invalidate();
+ callbackDone(callback);
+ }
+ });
+ }
+
+ public void pushFilter(final Filter filter) {
+ photoView.queue(new Runnable() {
+
+ @Override
+ public void run() {
+ while (!redoStack.empty()) {
+ redoStack.pop().release();
+ }
+ appliedStack.push(filter);
+ stackChanged();
+ }
+ });
+ }
+
+ public void undo(final OnDoneCallback callback) {
+ photoView.queue(new Runnable() {
+
+ @Override
+ public void run() {
+ if (!appliedStack.empty()) {
+ redoStack.push(appliedStack.pop());
+ stackChanged();
+ invalidate();
+ }
+ callbackDone(callback);
+ }
+ });
+ }
+
+ public void redo(final OnDoneCallback callback) {
+ photoView.queue(new Runnable() {
+
+ @Override
+ public void run() {
+ if (!redoStack.empty()) {
+ appliedStack.push(redoStack.pop());
+ stackChanged();
+ invalidateTopFilter();
+ }
+ callbackDone(callback);
+ }
+ });
+ }
+
+ public void topFilterChanged(final OnDoneCallback callback) {
+ // Remove the outdated top-filter change before queuing a new one.
+ if (queuedTopFilterChange != null) {
+ photoView.remove(queuedTopFilterChange);
+ }
+ queuedTopFilterChange = new Runnable() {
+
+ @Override
+ public void run() {
+ invalidateTopFilter();
+ callbackDone(callback);
+ }
+ };
+ photoView.queue(queuedTopFilterChange);
+ }
+
+ 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().
+ paused = true;
+ photoView.flush();
+ photoView.queueEvent(new Runnable() {
+
+ @Override
+ public void run() {
+ if (effectContext != null) {
+ effectContext.release();
+ effectContext = null;
+ }
+ photoView.setPhoto(null);
+ clearBuffers();
+ }
+ });
+ photoView.onPause();
+ }
+
+ public void onResume() {
+ photoView.onResume();
+ photoView.queue(new Runnable() {
+
+ @Override
+ public void run() {
+ // Create effect context after GL context is created or recreated.
+ effectContext = EffectContext.createWithCurrentGlContext();
+ }
+ });
+ paused = false;
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/LoadScreennailTask.java b/src/com/android/gallery3d/photoeditor/LoadScreennailTask.java
new file mode 100644
index 000000000..914d50b9d
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/LoadScreennailTask.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.view.Gravity;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+
+/**
+ * Asynchronous task for loading source photo screennail.
+ */
+public class LoadScreennailTask extends AsyncTask<Uri, Void, Bitmap> {
+
+ /**
+ * Callback for the completed asynchronous task.
+ */
+ public interface Callback {
+
+ void onComplete(Bitmap bitmap);
+ }
+
+ private static final int SCREENNAIL_WIDTH = 1280;
+ private static final int SCREENNAIL_HEIGHT = 960;
+
+ private final Context context;
+ private final Callback callback;
+
+ public LoadScreennailTask(Context context, Callback callback) {
+ this.context = context;
+ this.callback = callback;
+ }
+
+ /**
+ * The task should be executed with one given source photo uri.
+ */
+ @Override
+ protected Bitmap doInBackground(Uri... params) {
+ if (params[0] == null) {
+ return null;
+ }
+ return new BitmapUtils(context).getBitmap(params[0], SCREENNAIL_WIDTH, SCREENNAIL_HEIGHT);
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (bitmap == null) {
+ Toast toast = Toast.makeText(context, R.string.loading_failure, Toast.LENGTH_SHORT);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ }
+ callback.onComplete(bitmap);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/OnDoneBitmapCallback.java b/src/com/android/gallery3d/photoeditor/OnDoneBitmapCallback.java
new file mode 100644
index 000000000..a0ef4908c
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/OnDoneBitmapCallback.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.graphics.Bitmap;
+
+/**
+ * Callback that will only be called back in UI thread to notify that a bitmap is done.
+ */
+public interface OnDoneBitmapCallback {
+
+ void onDone(Bitmap bitmap);
+}
diff --git a/src/com/android/gallery3d/photoeditor/OnDoneCallback.java b/src/com/android/gallery3d/photoeditor/OnDoneCallback.java
new file mode 100644
index 000000000..80fbf3ca9
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/OnDoneCallback.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+/**
+ * Callback that will only be called back in UI thread to notify that an operation has been done.
+ */
+public interface OnDoneCallback {
+
+ void onDone();
+}
diff --git a/src/com/android/gallery3d/photoeditor/Photo.java b/src/com/android/gallery3d/photoeditor/Photo.java
new file mode 100644
index 000000000..4aaa90681
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/Photo.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.graphics.Bitmap;
+
+/**
+ * Photo that holds a GL texture and all its methods must be only accessed from the GL thread.
+ */
+public class Photo {
+
+ private int texture;
+ private int width;
+ private int height;
+
+ /**
+ * Factory method to ensure every Photo instance holds a valid texture.
+ */
+ public static Photo create(Bitmap bitmap) {
+ return (bitmap != null) ? new Photo(
+ RendererUtils.createTexture(bitmap), bitmap.getWidth(), bitmap.getHeight()) : null;
+ }
+
+ public static Photo create(int width, int height) {
+ return new Photo(RendererUtils.createTexture(), width, height);
+ }
+
+ private Photo(int texture, int width, int height) {
+ this.texture = texture;
+ this.width = width;
+ this.height = height;
+ }
+
+ public int texture() {
+ return texture;
+ }
+
+ public boolean matchDimension(Photo photo) {
+ return ((photo.width() == width()) && (photo.height() == height()));
+ }
+
+ public void changeDimension(int width, int height) {
+ this.width = width;
+ this.height = height;
+ RendererUtils.clearTexture(texture);
+ texture = RendererUtils.createTexture();
+ }
+
+ public int width() {
+ return width;
+ }
+
+ public int height() {
+ return height;
+ }
+
+ public Bitmap save() {
+ return RendererUtils.saveTexture(texture, width, height);
+ }
+
+ /**
+ * Clears the texture; this instance should not be used after its clear() is called.
+ */
+ public void clear() {
+ RendererUtils.clearTexture(texture);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/PhotoEditor.java b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
new file mode 100644
index 000000000..c7ec48f33
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/PhotoEditor.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+/**
+ * Main activity of the photo editor.
+ */
+public class PhotoEditor extends Activity {
+
+ private Uri uri;
+ private FilterStack filterStack;
+ private Toolbar toolbar;
+ private View backButton;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.photoeditor_main);
+
+ Intent intent = getIntent();
+ uri = Intent.ACTION_EDIT.equalsIgnoreCase(intent.getAction()) ? intent.getData() : null;
+
+ final ActionBar actionBar = (ActionBar) findViewById(R.id.action_bar);
+ filterStack = new FilterStack((PhotoView) findViewById(R.id.photo_view), actionBar);
+ toolbar = (Toolbar) findViewById(R.id.toolbar);
+ toolbar.initialize(filterStack);
+
+ final EffectsBar effectsBar = (EffectsBar) findViewById(R.id.effects_bar);
+ backButton = findViewById(R.id.action_bar_back);
+ backButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (actionBar.isEnabled()) {
+ // Exit effects or go back to the previous activity on pressing back button.
+ if (!effectsBar.exit(null)) {
+ tryRun(new Runnable() {
+
+ @Override
+ public void run() {
+ finish();
+ }
+ });
+ }
+ }
+ }
+ });
+ }
+
+ private void tryRun(final Runnable runnable) {
+ if (findViewById(R.id.save_button).isEnabled()) {
+ // Pop-up a dialog before executing the runnable to save unsaved photo.
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ toolbar.savePhoto(new Runnable() {
+
+ @Override
+ public void run() {
+ runnable.run();
+ }
+ });
+ }
+ })
+ .setNeutralButton(R.string.no, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ runnable.run();
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // no-op
+ }
+ });
+ builder.setMessage(R.string.save_photo).show();
+ return;
+ }
+
+ runnable.run();
+ }
+
+ @Override
+ public void onBackPressed() {
+ backButton.performClick();
+ }
+
+ @Override
+ protected void onPause() {
+ // TODO: Close running spinner progress dialogs as all pending operations will be paused.
+ super.onPause();
+ filterStack.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ filterStack.onResume();
+ toolbar.openPhoto(uri);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/PhotoView.java b/src/com/android/gallery3d/photoeditor/PhotoView.java
new file mode 100644
index 000000000..8c758ddb1
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/PhotoView.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.graphics.RectF;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+
+import java.util.Vector;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * Renders and displays photo in the surface view.
+ */
+public class PhotoView extends GLSurfaceView {
+
+ private final PhotoRenderer renderer;
+
+ public PhotoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ renderer = new PhotoRenderer();
+ setEGLContextClientVersion(2);
+ setRenderer(renderer);
+ setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ }
+
+ public RectF getPhotoBounds() {
+ RectF photoBounds;
+ synchronized (renderer.photoBounds) {
+ photoBounds = new RectF(renderer.photoBounds);
+ }
+ return photoBounds;
+ }
+
+ /**
+ * Queues a runnable and renders a frame after execution. Queued runnables could be later
+ * removed by remove() or flush().
+ */
+ public void queue(Runnable r) {
+ renderer.queue.add(r);
+ requestRender();
+ }
+
+ /**
+ * Removes the specified queued runnable.
+ */
+ public void remove(Runnable runnable) {
+ renderer.queue.remove(runnable);
+ }
+
+ /**
+ * Flushes all queued runnables to cancel their execution.
+ */
+ public void flush() {
+ renderer.queue.clear();
+ }
+
+ /**
+ * Sets photo for display; this method must be queued for GL thread.
+ */
+ public void setPhoto(Photo photo) {
+ renderer.setPhoto(photo);
+ }
+
+ /**
+ * Rotates displayed photo; this method must be queued for GL thread.
+ */
+ public void rotatePhoto(float degrees) {
+ renderer.rotatePhoto(degrees);
+ }
+
+ /**
+ * Renderer that renders the GL surface-view and only be called from the GL thread.
+ */
+ private class PhotoRenderer implements GLSurfaceView.Renderer {
+
+ final Vector<Runnable> queue = new Vector<Runnable>();
+ final RectF photoBounds = new RectF();
+ RendererUtils.RenderContext renderContext;
+ Photo photo;
+ int viewWidth;
+ int viewHeight;
+
+ void setPhoto(Photo photo) {
+ int width = (photo != null) ? photo.width() : 0;
+ int height = (photo != null) ? photo.height() : 0;
+ synchronized (photoBounds) {
+ photoBounds.set(0, 0, width, height);
+ }
+ this.photo = photo;
+ fitPhotoToSurface();
+ }
+
+ void fitPhotoToSurface() {
+ if (photo != null) {
+ RendererUtils.setRenderToFit(renderContext, photo.width(), photo.height(),
+ viewWidth, viewHeight);
+ }
+ }
+
+ void rotatePhoto(float degrees) {
+ if (photo != null) {
+ RendererUtils.setRenderToRotate(renderContext, photo.width(), photo.height(),
+ viewWidth, viewHeight, degrees);
+ }
+ }
+
+ void renderPhoto() {
+ if (photo != null) {
+ RendererUtils.renderTexture(renderContext, photo.texture(), viewWidth, viewHeight);
+ }
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ Runnable r = null;
+ synchronized (queue) {
+ if (!queue.isEmpty()) {
+ r = queue.remove(0);
+ }
+ }
+ if (r != null) {
+ r.run();
+ }
+ if (!queue.isEmpty()) {
+ requestRender();
+ }
+ renderPhoto();
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, int width, int height) {
+ viewWidth = width;
+ viewHeight = height;
+ fitPhotoToSurface();
+ }
+
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ renderContext = RendererUtils.createProgram();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/RendererUtils.java b/src/com/android/gallery3d/photoeditor/RendererUtils.java
new file mode 100644
index 000000000..a0cd5982d
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/RendererUtils.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.graphics.Bitmap;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+/**
+ * Utils for GL renderer.
+ */
+public class RendererUtils {
+
+ public static class RenderContext {
+ private int shaderProgram;
+ private int texSamplerHandle;
+ private int texCoordHandle;
+ private int posCoordHandle;
+ private FloatBuffer texVertices;
+ private FloatBuffer posVertices;
+ }
+
+ private static final float[] TEX_VERTICES = {
+ 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f
+ };
+
+ private static final float[] POS_VERTICES = {
+ -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f
+ };
+
+ private static final String VERTEX_SHADER =
+ "attribute vec4 a_position;\n" +
+ "attribute vec2 a_texcoord;\n" +
+ "varying vec2 v_texcoord;\n" +
+ "void main() {\n" +
+ " gl_Position = a_position;\n" +
+ " v_texcoord = a_texcoord;\n" +
+ "}\n";
+
+ private static final String FRAGMENT_SHADER =
+ "precision mediump float;\n" +
+ "uniform sampler2D tex_sampler;\n" +
+ "varying vec2 v_texcoord;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(tex_sampler, v_texcoord);\n" +
+ "}\n";
+
+ private static final int FLOAT_SIZE_BYTES = 4;
+ private static final float DEGREE_TO_RADIAN = (float) Math.PI / 180.0f;
+
+ public static int createTexture() {
+ int[] textures = new int[1];
+ GLES20.glGenTextures(textures.length, textures, 0);
+ checkGlError("glGenTextures");
+ return textures[0];
+ }
+
+ public static int createTexture(Bitmap bitmap) {
+ int texture = createTexture();
+
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ checkGlError("texImage2D");
+
+ return texture;
+ }
+
+ public static Bitmap saveTexture(int texture, int width, int height) {
+ int[] frame = new int[1];
+ GLES20.glGenFramebuffers(1, frame, 0);
+ checkGlError("glGenFramebuffers");
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frame[0]);
+ checkGlError("glBindFramebuffer");
+ GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
+ GLES20.GL_TEXTURE_2D, texture, 0);
+ checkGlError("glFramebufferTexture2D");
+
+ ByteBuffer buffer = ByteBuffer.allocate(width * height * 4);
+ GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer);
+ checkGlError("glReadPixels");
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmap.copyPixelsFromBuffer(buffer);
+
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+ checkGlError("glBindFramebuffer");
+ GLES20.glDeleteFramebuffers(1, frame, 0);
+ checkGlError("glDeleteFramebuffer");
+ return bitmap;
+ }
+
+ public static void clearTexture(int texture) {
+ int[] textures = new int[1];
+ textures[0] = texture;
+ GLES20.glDeleteTextures(textures.length, textures, 0);
+ checkGlError("glDeleteTextures");
+ }
+
+ public static void setRenderToFit(RenderContext context, int srcWidth, int srcHeight,
+ int dstWidth, int dstHeight) {
+ float srcAspectRatio = ((float) srcWidth) / srcHeight;
+ float dstAspectRatio = ((float) dstWidth) / dstHeight;
+ float relativeAspectRatio = dstAspectRatio / srcAspectRatio;
+
+ float vertices[] = new float[8];
+ System.arraycopy(POS_VERTICES, 0, vertices, 0, vertices.length);
+ if (relativeAspectRatio > 1.0f) {
+ // Screen is wider than the camera, scale down X
+ vertices[0] /= relativeAspectRatio;
+ vertices[2] /= relativeAspectRatio;
+ vertices[4] /= relativeAspectRatio;
+ vertices[6] /= relativeAspectRatio;
+ } else {
+ vertices[1] *= relativeAspectRatio;
+ vertices[3] *= relativeAspectRatio;
+ vertices[5] *= relativeAspectRatio;
+ vertices[7] *= relativeAspectRatio;
+ }
+ context.posVertices = createVerticesBuffer(vertices);
+ }
+
+ public static void setRenderToRotate(RenderContext context, int srcWidth, int srcHeight,
+ int dstWidth, int dstHeight, float degrees) {
+ float cosTheta = (float) Math.cos(-degrees * DEGREE_TO_RADIAN);
+ float sinTheta = (float) Math.sin(-degrees * DEGREE_TO_RADIAN);
+ float cosWidth = cosTheta * srcWidth;
+ float sinWidth = sinTheta * srcWidth;
+ float cosHeight = cosTheta * srcHeight;
+ float sinHeight = sinTheta * srcHeight;
+
+ float[] vertices = new float[8];
+ vertices[0] = -cosWidth + sinHeight;
+ vertices[1] = -sinWidth - cosHeight;
+ vertices[2] = cosWidth + sinHeight;
+ vertices[3] = sinWidth - cosHeight;
+ vertices[4] = -vertices[2];
+ vertices[5] = -vertices[3];
+ vertices[6] = -vertices[0];
+ vertices[7] = -vertices[1];
+
+ float maxWidth = Math.max(Math.abs(vertices[0]), Math.abs(vertices[2]));
+ float maxHeight = Math.max(Math.abs(vertices[1]), Math.abs(vertices[3]));
+ float scale = Math.min(dstWidth / maxWidth, dstHeight / maxHeight);
+
+ for (int i = 0; i < 8; i += 2) {
+ vertices[i] *= scale / dstWidth;
+ vertices[i + 1] *= scale / dstHeight;
+ }
+ context.posVertices = createVerticesBuffer(vertices);
+ }
+
+ public static void renderTexture(
+ RenderContext context, int texture, int viewWidth, int viewHeight) {
+ // Use our shader program
+ GLES20.glUseProgram(context.shaderProgram);
+ checkGlError("glUseProgram");
+
+ // Set viewport
+ GLES20.glViewport(0, 0, viewWidth, viewHeight);
+ checkGlError("glViewport");
+
+ // Disable blending
+ GLES20.glDisable(GLES20.GL_BLEND);
+
+ // Set the vertex attributes
+ GLES20.glVertexAttribPointer(
+ context.texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, context.texVertices);
+ GLES20.glEnableVertexAttribArray(context.texCoordHandle);
+ GLES20.glVertexAttribPointer(
+ context.posCoordHandle, 2, GLES20.GL_FLOAT, false, 0, context.posVertices);
+ GLES20.glEnableVertexAttribArray(context.posCoordHandle);
+ checkGlError("vertex attribute setup");
+
+ // Set the input texture
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ checkGlError("glActiveTexture");
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
+ checkGlError("glBindTexture");
+ GLES20.glUniform1i(context.texSamplerHandle, 0);
+
+ // Draw!
+ GLES20.glClearColor(0, 0, 0, 1);
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
+ }
+
+ public static RenderContext createProgram() {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
+ if (vertexShader == 0) {
+ return null;
+ }
+ int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
+ if (pixelShader == 0) {
+ return null;
+ }
+
+ int program = GLES20.glCreateProgram();
+ if (program != 0) {
+ GLES20.glAttachShader(program, vertexShader);
+ checkGlError("glAttachShader");
+ GLES20.glAttachShader(program, pixelShader);
+ checkGlError("glAttachShader");
+ GLES20.glLinkProgram(program);
+ int[] linkStatus = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+ if (linkStatus[0] != GLES20.GL_TRUE) {
+ String info = GLES20.glGetProgramInfoLog(program);
+ GLES20.glDeleteProgram(program);
+ program = 0;
+ throw new RuntimeException("Could not link program: " + info);
+ }
+ }
+
+ // Bind attributes and uniforms
+ RenderContext context = new RenderContext();
+ context.texSamplerHandle = GLES20.glGetUniformLocation(program, "tex_sampler");
+ context.texCoordHandle = GLES20.glGetAttribLocation(program, "a_texcoord");
+ context.posCoordHandle = GLES20.glGetAttribLocation(program, "a_position");
+ context.texVertices = createVerticesBuffer(TEX_VERTICES);
+ context.posVertices = createVerticesBuffer(POS_VERTICES);
+
+ context.shaderProgram = program;
+ return context;
+ }
+
+ private static int loadShader(int shaderType, String source) {
+ int shader = GLES20.glCreateShader(shaderType);
+ if (shader != 0) {
+ GLES20.glShaderSource(shader, source);
+ GLES20.glCompileShader(shader);
+ int[] compiled = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ String info = GLES20.glGetShaderInfoLog(shader);
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ throw new RuntimeException("Could not compile shader " + shaderType + ":" + info);
+ }
+ }
+ return shader;
+ }
+
+ private static FloatBuffer createVerticesBuffer(float[] vertices) {
+ if (vertices.length != 8) {
+ throw new RuntimeException("Number of vertices should be four.");
+ }
+
+ FloatBuffer buffer = ByteBuffer.allocateDirect(
+ vertices.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
+ buffer.put(vertices).position(0);
+ return buffer;
+ }
+
+ private static void checkGlError(String op) {
+ int error;
+ while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
+ throw new RuntimeException(op + ": glError " + error);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/SaveCopyTask.java b/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
new file mode 100644
index 000000000..e43959e94
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/SaveCopyTask.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.view.Gravity;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+
+import java.io.File;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+
+/**
+ * Asynchronous task for saving edited photo as a new copy.
+ */
+public class SaveCopyTask extends AsyncTask<Bitmap, Void, Uri> {
+
+ /**
+ * Callback for the completed asynchronous task.
+ */
+ public interface Callback {
+
+ void onComplete(Uri uri);
+ }
+
+ private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
+ private static final int INDEX_DATE_TAKEN = 0;
+ private static final int INDEX_LATITUDE = 1;
+ private static final int INDEX_LONGITUDE = 2;
+
+ private static final String[] IMAGE_PROJECTION = new String[] {
+ ImageColumns.DATE_TAKEN,
+ ImageColumns.LATITUDE,
+ ImageColumns.LONGITUDE,
+ };
+
+ private final Context context;
+ private final Uri sourceUri;
+ private final Callback callback;
+ private final String albumName;
+ private final String saveFileName;
+
+ public SaveCopyTask(Context context, Uri sourceUri, Callback callback) {
+ this.context = context;
+ this.sourceUri = sourceUri;
+ this.callback = callback;
+
+ albumName = context.getString(R.string.edited_photo_bucket_name);
+ saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(
+ new Date(System.currentTimeMillis()));
+ }
+
+ /**
+ * The task should be executed with one given bitmap to be saved.
+ */
+ @Override
+ protected Uri doInBackground(Bitmap... params) {
+ // TODO: Support larger dimensions for photo saving.
+ if (params[0] == null) {
+ return null;
+ }
+ Bitmap bitmap = params[0];
+ File file = save(bitmap);
+ Uri uri = (file != null) ? insertContent(file) : null;
+ bitmap.recycle();
+ return uri;
+ }
+
+ @Override
+ protected void onPostExecute(Uri result) {
+ String message = (result == null) ? context.getString(R.string.saving_failure)
+ : context.getString(R.string.photo_saved, albumName);
+ Toast toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+
+ callback.onComplete(result);
+ }
+
+ private File save(Bitmap bitmap) {
+ String directory = Environment.getExternalStorageDirectory().toString() + "/" + albumName;
+ return new BitmapUtils(context).saveBitmap(
+ bitmap, directory, saveFileName, Bitmap.CompressFormat.JPEG);
+ }
+
+ /**
+ * Insert the content (saved file) with proper source photo properties.
+ */
+ private Uri insertContent(File file) {
+ long now = System.currentTimeMillis() / 1000;
+ long dateTaken = now;
+ double latitude = 0f;
+ double longitude = 0f;
+
+ ContentResolver contentResolver = context.getContentResolver();
+ Cursor cursor = contentResolver.query(
+ sourceUri, IMAGE_PROJECTION, null, null, null);
+ try {
+ if ((cursor != null) && cursor.moveToNext()) {
+ dateTaken = cursor.getLong(INDEX_DATE_TAKEN);
+ latitude = cursor.getDouble(INDEX_LATITUDE);
+ longitude = cursor.getDouble(INDEX_LONGITUDE);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(Images.Media.TITLE, saveFileName);
+ values.put(Images.Media.DISPLAY_NAME, saveFileName);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.DATE_TAKEN, dateTaken);
+ values.put(Images.Media.DATE_MODIFIED, now);
+ values.put(Images.Media.DATE_ADDED, now);
+ values.put(Images.Media.ORIENTATION, 0);
+ values.put(Images.Media.DATA, file.getAbsolutePath());
+ values.put(Images.Media.SIZE, file.length());
+
+ // TODO: Change || to && after the default location issue is fixed.
+ if ((latitude != 0f) || (longitude != 0f)) {
+ values.put(Images.Media.LATITUDE, latitude);
+ values.put(Images.Media.LONGITUDE, longitude);
+ }
+ return contentResolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
new file mode 100644
index 000000000..9a3d8499a
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/SpinnerProgressDialog.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.app.Dialog;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ProgressBar;
+
+import com.android.gallery3d.R;
+
+/**
+ * Spinner model progress dialog that disables all tools for user interaction after it shows up and
+ * and re-enables them after it dismisses.
+ */
+public class SpinnerProgressDialog extends Dialog {
+
+ private final ViewGroup tools;
+
+ public static SpinnerProgressDialog show(ViewGroup tools) {
+ SpinnerProgressDialog dialog = new SpinnerProgressDialog(tools);
+ dialog.setCancelable(false);
+ dialog.show();
+ return dialog;
+ }
+
+ private SpinnerProgressDialog(ViewGroup tools) {
+ super(tools.getContext(), R.style.SpinnerProgressDialog);
+
+ addContentView(new ProgressBar(tools.getContext()), new LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+ this.tools = tools;
+ enableTools(false);
+ }
+
+ @Override
+ public void dismiss() {
+ super.dismiss();
+
+ enableTools(true);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ // Pass touch events to tools for killing idle even when the progress dialog is shown.
+ return tools.dispatchTouchEvent(event);
+ }
+
+ private void enableTools(boolean enabled) {
+ for (int i = 0; i < tools.getChildCount(); i++) {
+ tools.getChildAt(i).setEnabled(enabled);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/Toolbar.java b/src/com/android/gallery3d/photoeditor/Toolbar.java
new file mode 100644
index 000000000..72f243bab
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/Toolbar.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+/**
+ * Toolbar that contains all tools and handles all operations for editing photo.
+ */
+public class Toolbar extends RelativeLayout {
+
+ private final ToolbarIdleHandler idleHandler;
+ private FilterStack filterStack;
+ private EffectsBar effectsBar;
+ private ActionBar actionBar;
+ private Uri sourceUri;
+
+ public Toolbar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ idleHandler = new ToolbarIdleHandler(context);
+ setOnHierarchyChangeListener(idleHandler);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ idleHandler.killIdle();
+ return super.dispatchTouchEvent(ev);
+ }
+
+ public void initialize(FilterStack filterStack) {
+ this.filterStack = filterStack;
+ effectsBar = (EffectsBar) findViewById(R.id.effects_bar);
+ effectsBar.initialize(filterStack);
+ actionBar = (ActionBar) findViewById(R.id.action_bar);
+ actionBar.initialize(createActionBarListener());
+ idleHandler.killIdle();
+ }
+
+ private ActionBar.ActionBarListener createActionBarListener() {
+ actionBar = (ActionBar) findViewById(R.id.action_bar);
+ return new ActionBar.ActionBarListener() {
+
+ @Override
+ public void onUndo() {
+ effectsBar.exit(new Runnable() {
+
+ @Override
+ public void run() {
+ final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(
+ Toolbar.this);
+ filterStack.undo(new OnDoneCallback() {
+
+ @Override
+ public void onDone() {
+ progressDialog.dismiss();
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void onRedo() {
+ effectsBar.exit(new Runnable() {
+
+ @Override
+ public void run() {
+ final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(
+ Toolbar.this);
+ filterStack.redo(new OnDoneCallback() {
+
+ @Override
+ public void onDone() {
+ progressDialog.dismiss();
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void onSave() {
+ effectsBar.exit(new Runnable() {
+
+ @Override
+ public void run() {
+ savePhoto(null);
+ }
+ });
+ }
+ };
+ }
+
+ public void openPhoto(Uri uri) {
+ sourceUri = uri;
+
+ final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(this);
+ new LoadScreennailTask(getContext(), new LoadScreennailTask.Callback() {
+
+ @Override
+ public void onComplete(final Bitmap bitmap) {
+ filterStack.setPhotoSource(bitmap, new OnDoneCallback() {
+
+ @Override
+ public void onDone() {
+ progressDialog.dismiss();
+ }
+ });
+ }
+ }).execute(sourceUri);
+ }
+
+ /**
+ * Saves photo and executes runnable (if provided) after saving done.
+ */
+ public void savePhoto(final Runnable runnable) {
+ final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(this);
+ filterStack.saveBitmap(new OnDoneBitmapCallback() {
+
+ @Override
+ public void onDone(Bitmap bitmap) {
+ new SaveCopyTask(getContext(), sourceUri, new SaveCopyTask.Callback() {
+
+ @Override
+ public void onComplete(Uri uri) {
+ // TODO: Handle saving failure.
+ progressDialog.dismiss();
+ actionBar.disableSave();
+ if (runnable != null) {
+ runnable.run();
+ }
+ }
+ }).execute(bitmap);
+ }
+ });
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/ToolbarIdleHandler.java b/src/com/android/gallery3d/photoeditor/ToolbarIdleHandler.java
new file mode 100644
index 000000000..5cef56686
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/ToolbarIdleHandler.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.ViewGroup.OnHierarchyChangeListener;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handler that controls idle/awake behaviors of toolbar's child views from UI thread.
+ */
+class ToolbarIdleHandler implements OnHierarchyChangeListener {
+
+ private static final int MAKE_IDLE = 1;
+ private static final int TIMEOUT_IDLE = 8000;
+
+ private final List<View> childViews = new ArrayList<View>();
+ private final Handler mainHandler;
+ private final Animation fadeIn;
+ private final Animation fadeOut;
+ private boolean idle;
+
+ public ToolbarIdleHandler(Context context) {
+ mainHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MAKE_IDLE:
+ if (!idle) {
+ idle = true;
+ for (View view : childViews) {
+ view.startAnimation(fadeOut);
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ fadeIn = AnimationUtils.loadAnimation(context, R.anim.photoeditor_fade_in);
+ fadeOut = AnimationUtils.loadAnimation(context, R.anim.photoeditor_fade_out);
+ }
+
+ public void killIdle() {
+ mainHandler.removeMessages(MAKE_IDLE);
+ if (idle) {
+ idle = false;
+ for (View view : childViews) {
+ view.startAnimation(fadeIn);
+ }
+ }
+ mainHandler.sendEmptyMessageDelayed(MAKE_IDLE, TIMEOUT_IDLE);
+ }
+
+ @Override
+ public void onChildViewAdded(View parent, View child) {
+ // All child views, except photo-view, will fade out on inactivity timeout.
+ if (child.getId() != R.id.photo_view) {
+ childViews.add(child);
+ }
+ }
+
+ @Override
+ public void onChildViewRemoved(View parent, View child) {
+ childViews.remove(child);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java b/src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java
new file mode 100644
index 000000000..27a0bce0b
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/AbstractSeekBar.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+import com.android.gallery3d.R;
+
+/**
+ * Seek-bar base that implements a draggable thumb that fits seek-bar height.
+ */
+abstract class AbstractSeekBar extends SeekBar {
+
+ public AbstractSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ // Scale the thumb to fit seek-bar height.
+ Resources res = getResources();
+ Drawable thumb = res.getDrawable(R.drawable.photoeditor_seekbar_thumb);
+
+ // Set the left/right padding to half width of the thumb drawn.
+ int scaledWidth = thumb.getIntrinsicWidth() * h / thumb.getIntrinsicHeight();
+ int padding = (scaledWidth + 1) / 2;
+ setPadding(padding, 0, padding, 0);
+
+ Bitmap bitmap = Bitmap.createBitmap(scaledWidth, h, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ thumb.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ thumb.draw(canvas);
+
+ setThumb(new BitmapDrawable(res, bitmap));
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java b/src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java
new file mode 100644
index 000000000..26b5f51dc
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/AutoFixAction.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.AutoFixFilter;
+
+/**
+ * An action handling auto-fix effect.
+ */
+public class AutoFixAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public AutoFixAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ AutoFixFilter filter = new AutoFixFilter();
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java b/src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java
new file mode 100644
index 000000000..5f9809be0
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/ColorSeekBar.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Region.Op;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+import com.android.gallery3d.R;
+
+/**
+ * Seek-bar that has a draggable thumb to set and get the color from predefined color set.
+ */
+class ColorSeekBar extends AbstractSeekBar {
+
+ /**
+ * Listens to color changes.
+ */
+ public interface OnColorChangeListener {
+
+ void onColorChanged(int color, boolean fromUser);
+ }
+
+ private final int[] colors;
+ private Bitmap background;
+
+ public ColorSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Set up the predefined colors that could be indexed in the seek-bar.
+ TypedArray a = getResources().obtainTypedArray(R.array.color_picker_colors);
+ colors = new int[a.length()];
+ for (int i = 0; i < a.length(); i++) {
+ colors[i] = a.getColor(i, 0x000000);
+ }
+ a.recycle();
+ setMax(colors.length - 1);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ if (background != null) {
+ background.recycle();
+ }
+ background = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(background);
+
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setStyle(Paint.Style.FILL);
+
+ // Draw two half circles in the first and last colors at seek-bar left/right ends.
+ int radius = getThumbOffset();
+ float left = radius;
+ float right = w - radius;
+ float cy = h / 2;
+
+ canvas.save();
+ canvas.clipRect(left, 0, right, h, Op.DIFFERENCE);
+ paint.setColor(colors[0]);
+ canvas.drawCircle(left, cy, radius, paint);
+ paint.setColor(colors[colors.length - 1]);
+ canvas.drawCircle(right, cy, radius, paint);
+ canvas.restore();
+
+ // Draw color strips that make the thumb stop at every strip's center during seeking.
+ float strip = (right - left) / (colors.length - 1);
+ right = left + strip / 2;
+ paint.setColor(colors[0]);
+ canvas.drawRect(left, 0, right, h, paint);
+ left = right;
+ for (int i = 1; i < colors.length - 1; i++) {
+ right = left + strip;
+ paint.setColor(colors[i]);
+ canvas.drawRect(left, 0, right, h, paint);
+ left = right;
+ }
+ right = left + strip / 2;
+ paint.setColor(colors[colors.length - 1]);
+ canvas.drawRect(left, 0, right, h, paint);
+
+ setBackgroundDrawable(new BitmapDrawable(getResources(), background));
+ }
+
+ public void setOnColorChangeListener(final OnColorChangeListener listener) {
+ setOnSeekBarChangeListener((listener == null) ? null : new OnSeekBarChangeListener() {
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ listener.onColorChanged(colors[progress], fromUser);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ }
+
+ public void setColorIndex(int colorIndex) {
+ setProgress(colorIndex);
+ }
+
+ public int getColor() {
+ return colors[getProgress()];
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java b/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
new file mode 100644
index 000000000..41a89dbbe
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/ColorTemperatureAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.ColorTemperatureFilter;
+
+/**
+ * An action handling color temperature effect.
+ */
+public class ColorTemperatureAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ private ScaleSeekBar scalePicker;
+
+ public ColorTemperatureAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final ColorTemperatureFilter filter = new ColorTemperatureFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.COLOR);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setColorTemperature(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/CropAction.java b/src/com/android/gallery3d/photoeditor/actions/CropAction.java
new file mode 100644
index 000000000..60a01793f
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/CropAction.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.CropFilter;
+
+/**
+ * An action handling crop effect.
+ */
+public class CropAction extends EffectAction {
+
+ private static final float DEFAULT_CROP = 0.2f;
+
+ private CropFilter filter;
+ private CropView cropView;
+
+ public CropAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ filter = new CropFilter();
+
+ cropView = factory.createCropView();
+ cropView.setOnCropChangeListener(new CropView.OnCropChangeListener() {
+
+ @Override
+ public void onCropChanged(RectF cropBounds, boolean fromUser) {
+ if (fromUser) {
+ filter.setCropBounds(cropBounds);
+ notifyFilterChanged(filter, false);
+ }
+ }
+ });
+
+ RectF bounds = new RectF(DEFAULT_CROP, DEFAULT_CROP, 1 - DEFAULT_CROP, 1 - DEFAULT_CROP);
+ cropView.setCropBounds(bounds);
+ filter.setCropBounds(bounds);
+ notifyFilterChanged(filter, false);
+ }
+
+ @Override
+ public void doEnd() {
+ cropView.setOnCropChangeListener(null);
+ notifyFilterChanged(filter, true);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/CropView.java b/src/com/android/gallery3d/photoeditor/actions/CropView.java
new file mode 100644
index 000000000..75d74fcff
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/CropView.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+
+/**
+ * A view that tracks touch motions and adjusts crop bounds accordingly.
+ */
+class CropView extends FullscreenToolView {
+
+ /**
+ * Listener of crop bounds.
+ */
+ public interface OnCropChangeListener {
+
+ void onCropChanged(RectF cropBounds, boolean fromUser);
+ }
+
+ private static final int MOVE_LEFT = 1;
+ private static final int MOVE_TOP = 2;
+ private static final int MOVE_RIGHT = 4;
+ private static final int MOVE_BOTTOM = 8;
+ private static final int MOVE_BLOCK = 16;
+
+ private static final int MIN_CROP_WIDTH_HEIGHT = 2;
+ private static final int TOUCH_TOLERANCE = 25;
+ private static final int SHADOW_ALPHA = 160;
+ private static final int BORDER_COLOR = 0xFF008AFF;
+ private static final float BORDER_WIDTH = 2.0f;
+
+ private final Paint borderPaint;
+ private final Drawable heightIndicator;
+ private final Drawable widthIndicator;
+ private final int indicatorSize;
+ private final RectF cropBounds = new RectF(0, 0, 1, 1);
+
+ private float lastX;
+ private float lastY;
+ private int movingEdges;
+ private OnCropChangeListener listener;
+
+ public CropView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources resources = context.getResources();
+ heightIndicator = resources.getDrawable(R.drawable.camera_crop_height_holo);
+ widthIndicator = resources.getDrawable(R.drawable.camera_crop_width_holo);
+ indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
+
+ borderPaint = new Paint();
+ borderPaint.setStyle(Paint.Style.STROKE);
+ borderPaint.setColor(BORDER_COLOR);
+ borderPaint.setStrokeWidth(BORDER_WIDTH);
+ }
+
+ public void setOnCropChangeListener(OnCropChangeListener listener) {
+ this.listener = listener;
+ }
+
+ private void refreshByCropChange(boolean fromUser) {
+ if (listener != null) {
+ listener.onCropChanged(new RectF(cropBounds), fromUser);
+ }
+ invalidate();
+ }
+
+ /**
+ * Sets cropped bounds; modifies the bounds if it's smaller than the allowed dimensions.
+ */
+ public void setCropBounds(RectF bounds) {
+ // Avoid cropping smaller than minimum width or height.
+ if (bounds.width() * getPhotoWidth() < MIN_CROP_WIDTH_HEIGHT) {
+ bounds.set(0, bounds.top, 1, bounds.bottom);
+ }
+ if (bounds.height() * getPhotoHeight() < MIN_CROP_WIDTH_HEIGHT) {
+ bounds.set(bounds.left, 0, bounds.right, 1);
+ }
+ cropBounds.set(bounds);
+ refreshByCropChange(false);
+ }
+
+ private RectF getCropBoundsDisplayed() {
+ float width = displayBounds.width();
+ float height = displayBounds.height();
+ RectF cropped = new RectF(cropBounds.left * width, cropBounds.top * height,
+ cropBounds.right * width, cropBounds.bottom * height);
+ cropped.offset(displayBounds.left, displayBounds.top);
+ return cropped;
+ }
+
+ private void detectMovingEdges(float x, float y) {
+ RectF cropped = getCropBoundsDisplayed();
+ movingEdges = 0;
+
+ // Check left or right.
+ float left = Math.abs(x - cropped.left);
+ float right = Math.abs(x - cropped.right);
+ if ((left <= TOUCH_TOLERANCE) && (left < right)) {
+ movingEdges |= MOVE_LEFT;
+ }
+ else if (right <= TOUCH_TOLERANCE) {
+ movingEdges |= MOVE_RIGHT;
+ }
+
+ // Check top or bottom.
+ float top = Math.abs(y - cropped.top);
+ float bottom = Math.abs(y - cropped.bottom);
+ if ((top <= TOUCH_TOLERANCE) & (top < bottom)) {
+ movingEdges |= MOVE_TOP;
+ }
+ else if (bottom <= TOUCH_TOLERANCE) {
+ movingEdges |= MOVE_BOTTOM;
+ }
+
+ // Check inside block.
+ if (cropped.contains(x, y) && (movingEdges == 0)) {
+ movingEdges = MOVE_BLOCK;
+ }
+ invalidate();
+ }
+
+ private void moveEdges(float deltaX, float deltaY) {
+ RectF cropped = getCropBoundsDisplayed();
+ if (movingEdges == MOVE_BLOCK) {
+ // Move the whole cropped bounds within the photo display bounds.
+ deltaX = (deltaX > 0) ? Math.min(displayBounds.right - cropped.right, deltaX)
+ : Math.max(displayBounds.left - cropped.left, deltaX);
+ deltaY = (deltaY > 0) ? Math.min(displayBounds.bottom - cropped.bottom, deltaY)
+ : Math.max(displayBounds.top - cropped.top, deltaY);
+ cropped.offset(deltaX, deltaY);
+ } else {
+ // Adjust cropped bound dimensions within the photo display bounds.
+ float minWidth = MIN_CROP_WIDTH_HEIGHT * displayBounds.width() / getPhotoWidth();
+ float minHeight = MIN_CROP_WIDTH_HEIGHT * displayBounds.height() / getPhotoHeight();
+ if ((movingEdges & MOVE_LEFT) != 0) {
+ cropped.left = Math.min(cropped.left + deltaX, cropped.right - minWidth);
+ }
+ if ((movingEdges & MOVE_TOP) != 0) {
+ cropped.top = Math.min(cropped.top + deltaY, cropped.bottom - minHeight);
+ }
+ if ((movingEdges & MOVE_RIGHT) != 0) {
+ cropped.right = Math.max(cropped.right + deltaX, cropped.left + minWidth);
+ }
+ if ((movingEdges & MOVE_BOTTOM) != 0) {
+ cropped.bottom = Math.max(cropped.bottom + deltaY, cropped.top + minHeight);
+ }
+ cropped.intersect(displayBounds);
+ }
+ mapPhotoRect(cropped, cropBounds);
+ refreshByCropChange(true);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (isEnabled()) {
+ float x = event.getX();
+ float y = event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ detectMovingEdges(x, y);
+ lastX = x;
+ lastY = y;
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (movingEdges != 0) {
+ moveEdges(x - lastX, y - lastY);
+ }
+ lastX = x;
+ lastY = y;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ movingEdges = 0;
+ invalidate();
+ break;
+ }
+ }
+ return true;
+ }
+
+ private void drawIndicator(Canvas canvas, Drawable indicator, float centerX, float centerY) {
+ int left = (int) centerX - indicatorSize / 2;
+ int top = (int) centerY - indicatorSize / 2;
+ indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize);
+ indicator.draw(canvas);
+ }
+
+ private void drawShadow(Canvas canvas, float left, float top, float right, float bottom) {
+ canvas.save();
+ canvas.clipRect(left, top, right, bottom);
+ canvas.drawARGB(SHADOW_ALPHA, 0, 0, 0);
+ canvas.restore();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Draw shadow on non-cropped bounds and the border around cropped bounds.
+ RectF cropped = getCropBoundsDisplayed();
+ drawShadow(canvas, displayBounds.left, displayBounds.top, displayBounds.right, cropped.top);
+ drawShadow(canvas, displayBounds.left, cropped.top, cropped.left, displayBounds.bottom);
+ drawShadow(canvas, cropped.right, cropped.top, displayBounds.right, displayBounds.bottom);
+ drawShadow(canvas, cropped.left, cropped.bottom, cropped.right, displayBounds.bottom);
+ canvas.drawRect(cropped, borderPaint);
+
+ boolean block = movingEdges == MOVE_BLOCK;
+ if (((movingEdges & MOVE_TOP) != 0) || block) {
+ drawIndicator(canvas, heightIndicator, cropped.centerX(), cropped.top);
+ }
+ if (((movingEdges & MOVE_BOTTOM) != 0) || block) {
+ drawIndicator(canvas, heightIndicator, cropped.centerX(), cropped.bottom);
+ }
+ if (((movingEdges & MOVE_LEFT) != 0) || block) {
+ drawIndicator(canvas, widthIndicator, cropped.left, cropped.centerY());
+ }
+ if (((movingEdges & MOVE_RIGHT) != 0) || block) {
+ drawIndicator(canvas, widthIndicator, cropped.right, cropped.centerY());
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java b/src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java
new file mode 100644
index 000000000..8be60d307
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/CrossProcessAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.CrossProcessFilter;
+
+/**
+ * An action handling cross-process effect.
+ */
+public class CrossProcessAction extends EffectAction {
+
+ public CrossProcessAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ notifyFilterChanged(new CrossProcessFilter(), true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java b/src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java
new file mode 100644
index 000000000..0ec4a0205
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/DocumentaryAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.DocumentaryFilter;
+
+/**
+ * An action handling the preset "Documentary" effect.
+ */
+public class DocumentaryAction extends EffectAction {
+
+ public DocumentaryAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ notifyFilterChanged(new DocumentaryFilter(), true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java b/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
new file mode 100644
index 000000000..b82414dc8
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/DoodleAction.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.Path;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.DoodleFilter;
+
+/**
+ * An action handling doodle effect.
+ */
+public class DoodleAction extends EffectAction {
+
+ private static final int DEFAULT_COLOR_INDEX = 4;
+
+ private DoodleFilter filter;
+ private ColorSeekBar colorPicker;
+ private DoodleView doodleView;
+
+ public DoodleAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ filter = new DoodleFilter();
+
+ colorPicker = factory.createColorPicker();
+ colorPicker.setOnColorChangeListener(new ColorSeekBar.OnColorChangeListener() {
+
+ @Override
+ public void onColorChanged(int color, boolean fromUser) {
+ if (fromUser) {
+ doodleView.setColor(color);
+ }
+ }
+ });
+ colorPicker.setColorIndex(DEFAULT_COLOR_INDEX);
+
+ doodleView = factory.createDoodleView();
+ doodleView.setOnDoodleChangeListener(new DoodleView.OnDoodleChangeListener() {
+
+ @Override
+ public void onDoodleInPhotoBounds() {
+ // Notify the user has drawn within photo bounds and made visible changes on photo.
+ filter.setDoodledInPhotoBounds();
+ notifyFilterChanged(filter, false);
+ }
+
+ @Override
+ public void onDoodleFinished(Path path, int color) {
+ filter.addPath(path, color);
+ notifyFilterChanged(filter, false);
+ }
+ });
+ doodleView.setColor(colorPicker.getColor());
+ }
+
+ @Override
+ public void doEnd() {
+ colorPicker.setOnColorChangeListener(null);
+ doodleView.setOnDoodleChangeListener(null);
+ notifyFilterChanged(filter, true);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodlePaint.java b/src/com/android/gallery3d/photoeditor/actions/DoodlePaint.java
new file mode 100644
index 000000000..bcde9f1b0
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/DoodlePaint.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.graphics.Paint;
+
+/**
+ * A paint class for doodle effect.
+ */
+public class DoodlePaint extends Paint {
+
+ public DoodlePaint() {
+ super(Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG);
+
+ setStyle(Paint.Style.STROKE);
+ setStrokeJoin(Paint.Join.ROUND);
+ setStrokeCap(Paint.Cap.ROUND);
+ setStrokeWidth(15);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/DoodleView.java b/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
new file mode 100644
index 000000000..cc5af84be
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/DoodleView.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * A view that tracks touch motions as paths and paints them as doodles.
+ */
+class DoodleView extends FullscreenToolView {
+
+ /**
+ * Listener of doodle paths.
+ */
+ public interface OnDoodleChangeListener {
+
+ void onDoodleInPhotoBounds();
+
+ void onDoodleFinished(Path path, int color);
+ }
+
+ private final Path normalizedPath = new Path();
+ private final Path drawingPath = new Path();
+ private final Paint doodlePaint = new DoodlePaint();
+ private final Paint bitmapPaint = new Paint(Paint.DITHER_FLAG);
+ private final PointF lastPoint = new PointF();
+ private final Matrix pathMatrix = new Matrix();
+ private final Matrix displayMatrix = new Matrix();
+
+ private Bitmap bitmap;
+ private Canvas bitmapCanvas;
+ private OnDoodleChangeListener listener;
+
+ public DoodleView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setOnDoodleChangeListener(OnDoodleChangeListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ RectF r = new RectF(0, 0, getPhotoWidth(), getPhotoHeight());
+ if ((bitmap == null) && !r.isEmpty()) {
+ bitmap = Bitmap.createBitmap((int) r.width(), (int) r.height(),
+ Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(0x00000000);
+ bitmapCanvas = new Canvas(bitmap);
+
+ // Set up a matrix that maps back normalized paths to be drawn on the bitmap or canvas.
+ pathMatrix.setRectToRect(new RectF(0, 0, 1, 1), r, Matrix.ScaleToFit.FILL);
+ }
+ displayMatrix.setRectToRect(r, displayBounds, Matrix.ScaleToFit.FILL);
+ }
+
+ private void drawDoodle(Canvas canvas) {
+ if ((canvas != null) && !normalizedPath.isEmpty()) {
+ drawingPath.set(normalizedPath);
+ drawingPath.transform(pathMatrix);
+ canvas.drawPath(drawingPath, doodlePaint);
+ }
+ }
+
+ public void setColor(int color) {
+ // Reset path to draw in a new color.
+ finishCurrentPath();
+ normalizedPath.moveTo(lastPoint.x, lastPoint.y);
+ doodlePaint.setColor(Color.argb(192, Color.red(color), Color.green(color),
+ Color.blue(color)));
+ }
+
+ private void finishCurrentPath() {
+ if (!normalizedPath.isEmpty()) {
+ // Update the finished path to the bitmap.
+ drawDoodle(bitmapCanvas);
+ if (listener != null) {
+ listener.onDoodleFinished(new Path(normalizedPath), doodlePaint.getColor());
+ }
+ normalizedPath.rewind();
+ invalidate();
+ }
+ }
+
+ private void checkCurrentPathInBounds() {
+ if ((listener != null) && !normalizedPath.isEmpty()) {
+ RectF r = new RectF();
+ normalizedPath.computeBounds(r, false);
+ if (r.intersects(0, 0, 1, 1)) {
+ listener.onDoodleInPhotoBounds();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (isEnabled()) {
+ float x = event.getX();
+ float y = event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mapPhotoPoint(x, y, lastPoint);
+ normalizedPath.moveTo(lastPoint.x, lastPoint.y);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ float lastX = lastPoint.x;
+ float lastY = lastPoint.y;
+ mapPhotoPoint(x, y, lastPoint);
+ normalizedPath.quadTo(lastX, lastY, (lastX + lastPoint.x) / 2,
+ (lastY + lastPoint.y) / 2);
+ checkCurrentPathInBounds();
+ invalidate();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ // Line to last position with offset to draw at least dots for single clicks.
+ mapPhotoPoint(x + 1, y + 1, lastPoint);
+ normalizedPath.lineTo(lastPoint.x, lastPoint.y);
+ checkCurrentPathInBounds();
+ finishCurrentPath();
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ canvas.save();
+ canvas.clipRect(displayBounds);
+ canvas.concat(displayMatrix);
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, 0, 0, bitmapPaint);
+ }
+ drawDoodle(canvas);
+ canvas.restore();
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java b/src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java
new file mode 100644
index 000000000..b8da71e25
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/DuotoneAction.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.DuotoneFilter;
+
+/**
+ * An action handling duo-tone effect.
+ */
+public class DuotoneAction extends EffectAction {
+
+ private static final int DEFAULT_FIRST_COLOR = 0x004488;
+ private static final int DEFAULT_SECOND_COLOR = 0xffff00;
+
+ public DuotoneAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ // TODO: Add several sets of duo-tone colors to select from.
+ DuotoneFilter filter = new DuotoneFilter();
+ filter.setDuotone(DEFAULT_FIRST_COLOR, DEFAULT_SECOND_COLOR);
+ notifyFilterChanged(filter, true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/EffectAction.java b/src/com/android/gallery3d/photoeditor/actions/EffectAction.java
new file mode 100644
index 000000000..6c6a893e3
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/EffectAction.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.photoeditor.FilterStack;
+import com.android.gallery3d.photoeditor.OnDoneCallback;
+import com.android.gallery3d.photoeditor.filters.Filter;
+
+/**
+ * An action binding UI controls and effect operation for editing photo.
+ */
+public abstract class EffectAction extends LinearLayout {
+
+ /**
+ * Listener of effect action.
+ */
+ public interface Listener {
+
+ void onClick();
+
+ void onDone();
+ }
+
+ protected EffectToolFactory factory;
+
+ private Listener listener;
+ private Toast tooltip;
+ private FilterStack filterStack;
+ private boolean pushedFilter;
+ private FilterChangedCallback lastFilterChangedCallback;
+
+ public EffectAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setListener(Listener l) {
+ listener = l;
+ findViewById(R.id.effect_button).setOnClickListener(
+ (listener == null) ? null : new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ listener.onClick();
+ }
+ });
+ }
+
+ public CharSequence name() {
+ return ((TextView) findViewById(R.id.effect_label)).getText();
+ }
+
+ public void begin(FilterStack filterStack, EffectToolFactory factory) {
+ // This view is already detached from UI view hierarchy by reaching here; findViewById()
+ // could only access its own child views from here.
+ this.filterStack = filterStack;
+ this.factory = factory;
+
+ // Shows the tooltip if it's available.
+ if (getTag() != null) {
+ tooltip = Toast.makeText(getContext(), (String) getTag(), Toast.LENGTH_SHORT);
+ tooltip.setGravity(Gravity.CENTER, 0, 0);
+ tooltip.show();
+ }
+ doBegin();
+ }
+
+ /**
+ * Ends the effect and then executes the runnable after the effect is finished.
+ */
+ public void end(final Runnable runnableOnODone) {
+ doEnd();
+
+ // Wait till last output callback is done before finishing.
+ if ((lastFilterChangedCallback == null) || lastFilterChangedCallback.done) {
+ finish(runnableOnODone);
+ } else {
+ lastFilterChangedCallback.runnableOnReady = new Runnable() {
+
+ @Override
+ public void run() {
+ finish(runnableOnODone);
+ }
+ };
+ }
+ }
+
+ private void finish(Runnable runnableOnDone) {
+ // Close the tooltip if it's still showing.
+ if ((tooltip != null) && (tooltip.getView().getParent() != null)) {
+ tooltip.cancel();
+ tooltip = null;
+ }
+ pushedFilter = false;
+ lastFilterChangedCallback = null;
+
+ runnableOnDone.run();
+ }
+
+ protected void notifyDone() {
+ if (listener != null) {
+ listener.onDone();
+ }
+ }
+
+ protected void notifyFilterChanged(Filter filter, boolean output) {
+ if (!pushedFilter && filter.isValid()) {
+ filterStack.pushFilter(filter);
+ pushedFilter = true;
+ }
+ if (pushedFilter && output) {
+ // Notify the stack to execute the changed top filter and output the results.
+ lastFilterChangedCallback = new FilterChangedCallback();
+ filterStack.topFilterChanged(lastFilterChangedCallback);
+ }
+ }
+
+ /**
+ * Subclasses should creates a specific filter and binds the filter to necessary UI controls
+ * here when the action is about to begin.
+ */
+ protected abstract void doBegin();
+
+ /**
+ * Subclasses could do specific ending operations here when the action is about to end.
+ */
+ protected abstract void doEnd();
+
+ /**
+ * Done callback for executing top filter changes.
+ */
+ private class FilterChangedCallback implements OnDoneCallback {
+
+ private boolean done;
+ private Runnable runnableOnReady;
+
+ @Override
+ public void onDone() {
+ done = true;
+
+ if (runnableOnReady != null) {
+ runnableOnReady.run();
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java b/src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java
new file mode 100644
index 000000000..4bc49c5fe
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/EffectToolFactory.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.photoeditor.PhotoView;
+
+/**
+ * Factory to create tools that will be used by effect actions.
+ */
+public class EffectToolFactory {
+
+ public enum ScalePickerType {
+ FILLLIGHT, HIGHLIGHT, SHADOW, COLOR, GENERIC
+ }
+
+ private final ViewGroup effectToolPanel;
+ private final LayoutInflater inflater;
+
+ public EffectToolFactory(ViewGroup effectToolPanel, LayoutInflater inflater) {
+ this.effectToolPanel = effectToolPanel;
+ this.inflater = inflater;
+ }
+
+ private View createFullscreenTool(int toolId) {
+ // Create full screen effect tool on top of photo-view and place it within the same
+ // view group that contains photo-view.
+ View photoView = effectToolPanel.getRootView().findViewById(R.id.photo_view);
+ ViewGroup parent = (ViewGroup) photoView.getParent();
+ FullscreenToolView view = (FullscreenToolView) inflater.inflate(toolId, parent, false);
+ view.setPhotoBounds(((PhotoView) photoView).getPhotoBounds());
+ parent.addView(view, parent.indexOfChild(photoView) + 1);
+ return view;
+ }
+
+ private View createPanelTool(int toolId) {
+ View view = inflater.inflate(toolId, effectToolPanel, false);
+ effectToolPanel.addView(view, 0);
+ return view;
+ }
+
+ 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 SHADOW:
+ return R.drawable.photoeditor_scale_seekbar_shadow;
+
+ case COLOR:
+ return R.drawable.photoeditor_scale_seekbar_color;
+ }
+ return R.drawable.photoeditor_scale_seekbar_generic;
+ }
+
+ public ScaleSeekBar createScalePicker(ScalePickerType type) {
+ ScaleSeekBar scalePicker = (ScaleSeekBar) createPanelTool(
+ R.layout.photoeditor_scale_seekbar);
+ scalePicker.setBackgroundResource(getScalePickerBackground(type));
+ return scalePicker;
+ }
+
+ public ColorSeekBar createColorPicker() {
+ return (ColorSeekBar) createPanelTool(R.layout.photoeditor_color_seekbar);
+ }
+
+ public DoodleView createDoodleView() {
+ return (DoodleView) createFullscreenTool(R.layout.photoeditor_doodle_view);
+ }
+
+ public TouchView createTouchView() {
+ return (TouchView) createFullscreenTool(R.layout.photoeditor_touch_view);
+ }
+
+ public RotateView createRotateView() {
+ return (RotateView) createFullscreenTool(R.layout.photoeditor_rotate_view);
+ }
+
+ public CropView createCropView() {
+ return (CropView) createFullscreenTool(R.layout.photoeditor_crop_view);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java b/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
new file mode 100644
index 000000000..e5a743c00
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/FillLightAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.FillLightFilter;
+
+/**
+ * An action handling fill-light effect.
+ */
+public class FillLightAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0f;
+
+ private ScaleSeekBar scalePicker;
+
+ public FillLightAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final FillLightFilter filter = new FillLightFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.FILLLIGHT);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setBacklight(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java b/src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java
new file mode 100644
index 000000000..348f0048d
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/FisheyeAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.FisheyeFilter;
+
+/**
+ * An action handling fisheye effect.
+ */
+public class FisheyeAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ private ScaleSeekBar scalePicker;
+
+ public FisheyeAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final FisheyeFilter filter = new FisheyeFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setScale(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/FlipAction.java b/src/com/android/gallery3d/photoeditor/actions/FlipAction.java
new file mode 100644
index 000000000..00abc60a0
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/FlipAction.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.FlipFilter;
+
+/**
+ * An action handling flip effect.
+ */
+public class FlipAction extends EffectAction {
+
+ private boolean flipHorizontal;
+ private boolean flipVertical;
+ private TouchView touchView;
+
+ public FlipAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final FlipFilter filter = new FlipFilter();
+
+ touchView = factory.createTouchView();
+ touchView.setSwipeListener(new TouchView.SwipeListener() {
+
+ @Override
+ public void onSwipeDown() {
+ flipFilterVertically(filter);
+ }
+
+ @Override
+ public void onSwipeLeft() {
+ flipFilterHorizontally(filter);
+ }
+
+ @Override
+ public void onSwipeRight() {
+ flipFilterHorizontally(filter);
+ }
+
+ @Override
+ public void onSwipeUp() {
+ flipFilterVertically(filter);
+ }
+ });
+
+ flipHorizontal = false;
+ flipVertical = false;
+ flipFilterHorizontally(filter);
+ }
+
+ @Override
+ public void doEnd() {
+ touchView.setSwipeListener(null);
+ }
+
+ private void flipFilterHorizontally(final FlipFilter filter) {
+ flipHorizontal = !flipHorizontal;
+ filter.setFlip(flipHorizontal, flipVertical);
+ notifyFilterChanged(filter, true);
+ }
+
+ private void flipFilterVertically(final FlipFilter filter) {
+ flipVertical = !flipVertical;
+ filter.setFlip(flipHorizontal, flipVertical);
+ notifyFilterChanged(filter, true);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/FullscreenToolView.java b/src/com/android/gallery3d/photoeditor/actions/FullscreenToolView.java
new file mode 100644
index 000000000..5396d16ba
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/FullscreenToolView.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Full-screen tool view that gets photo display bounds and maps positions on photo display bounds
+ * back to exact coordinates on photo.
+ */
+abstract class FullscreenToolView extends View {
+
+ protected final RectF displayBounds = new RectF();
+ private final Matrix photoMatrix = new Matrix();
+ private RectF photoBounds;
+
+ public FullscreenToolView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Photo bounds must be set before onSizeChanged() and all other instance methods are invoked.
+ */
+ public void setPhotoBounds(RectF photoBounds) {
+ this.photoBounds = photoBounds;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ displayBounds.setEmpty();
+ photoMatrix.reset();
+ if (photoBounds.isEmpty()) {
+ return;
+ }
+
+ // Assumes photo-view is also full-screen as this tool-view and centers/scales photo to fit.
+ Matrix matrix = new Matrix();
+ if (matrix.setRectToRect(photoBounds, new RectF(0, 0, w, h), Matrix.ScaleToFit.CENTER)) {
+ matrix.mapRect(displayBounds, photoBounds);
+ }
+
+ matrix.invert(photoMatrix);
+ }
+
+ protected float getPhotoWidth() {
+ return photoBounds.width();
+ }
+
+ protected float getPhotoHeight() {
+ return photoBounds.height();
+ }
+
+ protected void mapPhotoPoint(float x, float y, PointF dst) {
+ if (photoBounds.isEmpty()) {
+ dst.set(0, 0);
+ } else {
+ float[] point = new float[] {x, y};
+ photoMatrix.mapPoints(point);
+ dst.set(point[0] / photoBounds.width(), point[1] / photoBounds.height());
+ }
+ }
+
+ protected void mapPhotoRect(RectF src, RectF dst) {
+ if (photoBounds.isEmpty()) {
+ dst.setEmpty();
+ } else {
+ photoMatrix.mapRect(dst, src);
+ dst.set(dst.left / photoBounds.width(), dst.top / photoBounds.height(),
+ dst.right / photoBounds.width(), dst.bottom / photoBounds.height());
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/GrainAction.java b/src/com/android/gallery3d/photoeditor/actions/GrainAction.java
new file mode 100644
index 000000000..258eb8aa8
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/GrainAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.GrainFilter;
+
+/**
+ * An action handling the film-grain effect.
+ */
+public class GrainAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ private ScaleSeekBar scalePicker;
+
+ public GrainAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final GrainFilter filter = new GrainFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setScale(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java b/src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java
new file mode 100644
index 000000000..ac89cd196
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/GrayscaleAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.GrayscaleFilter;
+
+/**
+ * An action handling grayscale effect.
+ */
+public class GrayscaleAction extends EffectAction {
+
+ public GrayscaleAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ notifyFilterChanged(new GrayscaleFilter(), true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java b/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
new file mode 100644
index 000000000..06feedcb9
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/HighlightAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.HighlightFilter;
+
+/**
+ * An action handling highlight effect.
+ */
+public class HighlightAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0f;
+
+ private ScaleSeekBar scalePicker;
+
+ public HighlightAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final HighlightFilter filter = new HighlightFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.HIGHLIGHT);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setHighlight(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/LomoishAction.java b/src/com/android/gallery3d/photoeditor/actions/LomoishAction.java
new file mode 100644
index 000000000..44ffc52fe
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/LomoishAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.LomoishFilter;
+
+/**
+ * An action handling the preset "Lomo-ish" effect.
+ */
+public class LomoishAction extends EffectAction {
+
+ public LomoishAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ notifyFilterChanged(new LomoishFilter(), true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/NegativeAction.java b/src/com/android/gallery3d/photoeditor/actions/NegativeAction.java
new file mode 100644
index 000000000..527642133
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/NegativeAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.NegativeFilter;
+
+/**
+ * An action handling negative effect.
+ */
+public class NegativeAction extends EffectAction {
+
+ public NegativeAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ notifyFilterChanged(new NegativeFilter(), true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java b/src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java
new file mode 100644
index 000000000..760539da5
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/PosterizeAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.PosterizeFilter;
+
+/**
+ * An action handling the "Posterize" effect.
+ */
+public class PosterizeAction extends EffectAction {
+
+ public PosterizeAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ notifyFilterChanged(new PosterizeFilter(), true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java b/src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java
new file mode 100644
index 000000000..a472ad985
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/RedEyeAction.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.RedEyeFilter;
+
+/**
+ * An action handling red-eye removal.
+ */
+public class RedEyeAction extends EffectAction {
+
+ private TouchView touchView;
+
+ public RedEyeAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final RedEyeFilter filter = new RedEyeFilter();
+
+ touchView = factory.createTouchView();
+ touchView.setSingleTapListener(new TouchView.SingleTapListener() {
+
+ @Override
+ public void onSingleTap(PointF point) {
+ filter.addRedEyePosition(point);
+ notifyFilterChanged(filter, true);
+ }
+ });
+ }
+
+ @Override
+ public void doEnd() {
+ touchView.setSingleTapListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/RotateAction.java b/src/com/android/gallery3d/photoeditor/actions/RotateAction.java
new file mode 100644
index 000000000..dee78f17a
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/RotateAction.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.photoeditor.PhotoView;
+import com.android.gallery3d.photoeditor.filters.RotateFilter;
+
+/**
+ * An action handling rotate effect.
+ */
+public class RotateAction extends EffectAction {
+
+ private static final float DEFAULT_ANGLE = 0.0f;
+ private static final float DEFAULT_ROTATE_SPAN = 360.0f;
+
+ private RotateFilter filter;
+ private float rotateDegrees;
+ private RotateView rotateView;
+
+ public RotateAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ filter = new RotateFilter();
+
+ rotateView = factory.createRotateView();
+ rotateView.setOnAngleChangeListener(new RotateView.OnRotateChangeListener() {
+
+ // Directly transform photo-view because running the rotation filter isn't fast enough.
+ PhotoView photoView = (PhotoView) rotateView.getRootView().findViewById(
+ R.id.photo_view);
+
+ @Override
+ public void onAngleChanged(float degrees, boolean fromUser){
+ if (fromUser) {
+ rotateDegrees = degrees;
+ filter.setAngle(degrees);
+ notifyFilterChanged(filter, false);
+ transformPhotoView(degrees);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch() {
+ // no-op
+ }
+
+ @Override
+ public void onStopTrackingTouch() {
+ if (roundFilterRotationDegrees()) {
+ notifyFilterChanged(filter, false);
+ transformPhotoView(rotateDegrees);
+ rotateView.setRotatedAngle(rotateDegrees);
+ }
+ }
+
+ private void transformPhotoView(final float degrees) {
+ photoView.queue(new Runnable() {
+
+ @Override
+ public void run() {
+ photoView.rotatePhoto(degrees);
+ }
+ });
+ }
+ });
+ rotateView.setRotatedAngle(DEFAULT_ANGLE);
+ rotateView.setRotateSpan(DEFAULT_ROTATE_SPAN);
+ rotateDegrees = 0;
+ }
+
+ @Override
+ public void doEnd() {
+ rotateView.setOnAngleChangeListener(null);
+ // Round the current rotation degrees in case rotation tracking has not stopped yet.
+ roundFilterRotationDegrees();
+ notifyFilterChanged(filter, true);
+ }
+
+ /**
+ * Rounds filter rotation degrees to multiples of 90 degrees.
+ *
+ * @return true if the rotation degrees has been changed.
+ */
+ private boolean roundFilterRotationDegrees() {
+ if (rotateDegrees % 90 != 0) {
+ rotateDegrees = Math.round(rotateDegrees / 90) * 90;
+ filter.setAngle(rotateDegrees);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/RotateView.java b/src/com/android/gallery3d/photoeditor/actions/RotateView.java
new file mode 100644
index 000000000..c7cee5919
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/RotateView.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * View that shows grids and handles touch-events to adjust angle of rotation.
+ */
+class RotateView extends FullscreenToolView {
+
+ /**
+ * Listens to rotate changes.
+ */
+ public interface OnRotateChangeListener {
+
+ void onAngleChanged(float degrees, boolean fromUser);
+
+ void onStartTrackingTouch();
+
+ void onStopTrackingTouch();
+ }
+
+ // All angles used are defined between PI and -PI.
+ private static final float MATH_PI = (float) Math.PI;
+ private static final float MATH_HALF_PI = MATH_PI / 2;
+ private static final float RADIAN_TO_DEGREE = 180f / MATH_PI;
+
+ private final Paint dashStrokePaint;
+ private final Path grids = new Path();
+ private final Path referenceLine = new Path();
+
+ private OnRotateChangeListener listener;
+ private boolean drawGrids;
+ private int centerX;
+ private int centerY;
+ private float maxRotatedAngle;
+ private float minRotatedAngle;
+ private float currentRotatedAngle;
+ private float lastRotatedAngle;
+ private float touchStartAngle;
+
+ public RotateView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ dashStrokePaint = new Paint();
+ dashStrokePaint.setAntiAlias(true);
+ dashStrokePaint.setStyle(Paint.Style.STROKE);
+ dashStrokePaint.setPathEffect(new DashPathEffect(new float[] {15.0f, 5.0f}, 1.0f));
+ }
+
+ public void setRotatedAngle(float degrees) {
+ refreshAngle(degrees, false);
+ }
+
+ /**
+ * Sets allowed degrees for rotation span before rotating the view.
+ */
+ public void setRotateSpan(float degrees) {
+ if (degrees >= 360f) {
+ maxRotatedAngle = Float.POSITIVE_INFINITY;
+ } else {
+ maxRotatedAngle = (degrees / RADIAN_TO_DEGREE) / 2;
+ }
+ minRotatedAngle = -maxRotatedAngle;
+ }
+
+ public void setOnAngleChangeListener(OnRotateChangeListener listener) {
+ this.listener = listener;
+ }
+
+ public void setDrawGrids(boolean drawGrids) {
+ this.drawGrids = drawGrids;
+ invalidate();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ centerX = w / 2;
+ centerY = h / 2;
+
+ // Make reference line long enough to cross the bounds diagonally after being rotated.
+ referenceLine.reset();
+ float radius = (float) Math.hypot(centerX, centerY);
+ float delta = radius - centerX;
+ referenceLine.moveTo(-delta, centerY);
+ referenceLine.lineTo(getWidth() + delta, centerY);
+ delta = radius - centerY;
+ referenceLine.moveTo(centerX, -delta);
+ referenceLine.lineTo(centerX, getHeight() + delta);
+
+ // Set grids inside photo display bounds.
+ grids.reset();
+ delta = displayBounds.width() / 4.0f;
+ for (float x = displayBounds.left + delta; x < displayBounds.right; x += delta) {
+ grids.moveTo(x, displayBounds.top);
+ grids.lineTo(x, displayBounds.bottom);
+ }
+ delta = displayBounds.height() / 4.0f;
+ for (float y = displayBounds.top + delta; y < displayBounds.bottom; y += delta) {
+ grids.moveTo(displayBounds.left, y);
+ grids.lineTo(displayBounds.right, y);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (drawGrids) {
+ canvas.save();
+ canvas.clipRect(displayBounds);
+ dashStrokePaint.setStrokeWidth(2f);
+ dashStrokePaint.setColor(0x99CCCCCC);
+ canvas.drawPath(grids, dashStrokePaint);
+
+ canvas.rotate(-currentRotatedAngle * RADIAN_TO_DEGREE, centerX, centerY);
+ dashStrokePaint.setStrokeWidth(2f);
+ dashStrokePaint.setColor(0x99FFCC77);
+ canvas.drawPath(referenceLine, dashStrokePaint);
+ canvas.restore();
+ }
+ }
+
+ private float calculateAngle(MotionEvent ev) {
+ float x = ev.getX() - centerX;
+ float y = centerY - ev.getY();
+
+ float angle;
+ if (x == 0) {
+ angle = (y >= 0) ? MATH_HALF_PI : -MATH_HALF_PI;
+ } else {
+ angle = (float) Math.atan(y / x);
+ }
+
+ if ((angle >= 0) && (x < 0)) {
+ angle = angle - MATH_PI;
+ } else if ((angle < 0) && (x < 0)) {
+ angle = MATH_PI + angle;
+ }
+ return angle;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ super.onTouchEvent(ev);
+
+ if (isEnabled()) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ lastRotatedAngle = currentRotatedAngle;
+ touchStartAngle = calculateAngle(ev);
+
+ if (listener != null) {
+ listener.onStartTrackingTouch();
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ float touchAngle = calculateAngle(ev);
+ float rotatedAngle = touchAngle - touchStartAngle + lastRotatedAngle;
+
+ if ((rotatedAngle > maxRotatedAngle) || (rotatedAngle < minRotatedAngle)) {
+ // Angles are out of range; restart rotating.
+ // TODO: Fix discontinuity around boundary.
+ lastRotatedAngle = currentRotatedAngle;
+ touchStartAngle = touchAngle;
+ } else {
+ refreshAngle(-rotatedAngle * RADIAN_TO_DEGREE, true);
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (listener != null) {
+ listener.onStopTrackingTouch();
+ }
+ break;
+ }
+ }
+ return true;
+ }
+
+ private void refreshAngle(float degrees, boolean fromUser) {
+ currentRotatedAngle = -degrees / RADIAN_TO_DEGREE;
+ if (listener != null) {
+ listener.onAngleChanged(degrees, fromUser);
+ }
+ invalidate();
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java b/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
new file mode 100644
index 000000000..31bcfd664
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/SaturationAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.SaturationFilter;
+
+/**
+ * An action handling saturation effect.
+ */
+public class SaturationAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ private ScaleSeekBar scalePicker;
+
+ public SaturationAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final SaturationFilter filter = new SaturationFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.COLOR);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setSaturation(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/ScaleSeekBar.java b/src/com/android/gallery3d/photoeditor/actions/ScaleSeekBar.java
new file mode 100644
index 000000000..6fb154c17
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/ScaleSeekBar.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+
+/**
+ * Seek-bar that has a draggable thumb to set and get the normalized scale value from 0 to 1.
+ */
+class ScaleSeekBar extends AbstractSeekBar {
+
+ /**
+ * Listens to scale changes.
+ */
+ public interface OnScaleChangeListener {
+
+ void onProgressChanged(float progress, boolean fromUser);
+ }
+
+ public ScaleSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setMax(100);
+ }
+
+ public void setOnScaleChangeListener(final OnScaleChangeListener listener) {
+ setOnSeekBarChangeListener((listener == null) ? null : new OnSeekBarChangeListener() {
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ listener.onProgressChanged((float) progress / getMax(), fromUser);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+ });
+ }
+
+ public void setProgress(float progress) {
+ setProgress((int) (progress * getMax()));
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/SepiaAction.java b/src/com/android/gallery3d/photoeditor/actions/SepiaAction.java
new file mode 100644
index 000000000..c431115f9
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/SepiaAction.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.SepiaFilter;
+
+/**
+ * An action handling sepia effect.
+ */
+public class SepiaAction extends EffectAction {
+
+ public SepiaAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ notifyFilterChanged(new SepiaFilter(), true);
+ notifyDone();
+ }
+
+ @Override
+ public void doEnd() {
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java b/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
new file mode 100644
index 000000000..185febc7c
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/ShadowAction.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.ShadowFilter;
+
+/**
+ * An action handling shadow effect.
+ */
+public class ShadowAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0f;
+
+ private ScaleSeekBar scalePicker;
+
+ public ShadowAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final ShadowFilter filter = new ShadowFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.SHADOW);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setShadow(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java b/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
new file mode 100644
index 000000000..7524c76ec
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/SharpenAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.SharpenFilter;
+
+/**
+ * An action handling sharpen effect.
+ */
+public class SharpenAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ private ScaleSeekBar scalePicker;
+
+ public SharpenAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final SharpenFilter filter = new SharpenFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setSharpen(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+
+ filter.setSharpen(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/StraightenAction.java b/src/com/android/gallery3d/photoeditor/actions/StraightenAction.java
new file mode 100644
index 000000000..2a8c549f6
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/StraightenAction.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.StraightenFilter;
+
+/**
+ * An action handling straighten effect.
+ */
+public class StraightenAction extends EffectAction {
+
+ private static final float DEFAULT_ANGLE = 0.0f;
+ private static final float DEFAULT_ROTATE_SPAN = StraightenFilter.MAX_DEGREES * 2;
+
+ private RotateView rotateView;
+
+ public StraightenAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final StraightenFilter filter = new StraightenFilter();
+
+ rotateView = factory.createRotateView();
+ rotateView.setOnAngleChangeListener(new RotateView.OnRotateChangeListener() {
+
+ @Override
+ public void onAngleChanged(float degrees, boolean fromUser){
+ if (fromUser) {
+ filter.setAngle(degrees);
+ notifyFilterChanged(filter, true);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch() {
+ // no-op
+ }
+
+ @Override
+ public void onStopTrackingTouch() {
+ // no-op
+ }
+ });
+ rotateView.setDrawGrids(true);
+ rotateView.setRotatedAngle(DEFAULT_ANGLE);
+ rotateView.setRotateSpan(DEFAULT_ROTATE_SPAN);
+ }
+
+ @Override
+ public void doEnd() {
+ rotateView.setOnAngleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/TintAction.java b/src/com/android/gallery3d/photoeditor/actions/TintAction.java
new file mode 100644
index 000000000..defd2a331
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/TintAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.TintFilter;
+
+/**
+ * An action handling tint effect.
+ */
+public class TintAction extends EffectAction {
+
+ private static final int DEFAULT_COLOR_INDEX = 13;
+
+ private ColorSeekBar colorPicker;
+
+ public TintAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final TintFilter filter = new TintFilter();
+
+ colorPicker = factory.createColorPicker();
+ colorPicker.setOnColorChangeListener(new ColorSeekBar.OnColorChangeListener() {
+
+ @Override
+ public void onColorChanged(int color, boolean fromUser) {
+ if (fromUser) {
+ filter.setTint(color);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ // Tint photo with the default color.
+ colorPicker.setColorIndex(DEFAULT_COLOR_INDEX);
+ filter.setTint(colorPicker.getColor());
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void doEnd() {
+ colorPicker.setOnColorChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/TouchView.java b/src/com/android/gallery3d/photoeditor/actions/TouchView.java
new file mode 100644
index 000000000..0548cc4b2
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/TouchView.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+/**
+ * A view that detects user gestures and touch motions.
+ */
+class TouchView extends FullscreenToolView {
+
+ /**
+ * Listener of swipes.
+ */
+ public interface SwipeListener {
+
+ void onSwipeLeft();
+
+ void onSwipeRight();
+
+ void onSwipeUp();
+
+ void onSwipeDown();
+ }
+
+ /**
+ * Listener of single tap on a point (relative to photo coordinates).
+ */
+ public interface SingleTapListener {
+
+ void onSingleTap(PointF point);
+ }
+
+ private final GestureDetector gestureDetector;
+
+ private SwipeListener swipeListener;
+ private SingleTapListener singleTapListener;
+
+ public TouchView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final int swipeThreshold = (int) (500 * getResources().getDisplayMetrics().density);
+ gestureDetector = new GestureDetector(
+ context, new GestureDetector.SimpleOnGestureListener() {
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ // GestureDetector onTouchEvent returns true for fling events only when their
+ // preceding down events are consumed.
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (singleTapListener != null) {
+ PointF point = new PointF();
+ mapPhotoPoint(e.getX(), e.getY(), point);
+ singleTapListener.onSingleTap(point);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onFling(
+ MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) {
+ if (swipeListener != null) {
+ float absX = Math.abs(velocityX);
+ float absY = Math.abs(velocityY);
+ float deltaX = me2.getX() - me1.getX();
+ float deltaY = me2.getY() - me1.getY();
+ int travelX = getWidth() / 4;
+ int travelY = getHeight() / 4;
+ if (velocityX > swipeThreshold && absY < absX && deltaX > travelX) {
+ swipeListener.onSwipeRight();
+ } else if (velocityX < -swipeThreshold && absY < absX && deltaX < -travelX) {
+ swipeListener.onSwipeLeft();
+ } else if (velocityY < -swipeThreshold && absX < absY && deltaY < -travelY) {
+ swipeListener.onSwipeUp();
+ } else if (velocityY > swipeThreshold && absX < absY / 2 && deltaY > travelY) {
+ swipeListener.onSwipeDown();
+ }
+ }
+ return true;
+ }
+ });
+ gestureDetector.setIsLongpressEnabled(false);
+ }
+
+ public void setSwipeListener(SwipeListener listener) {
+ swipeListener = listener;
+ }
+
+ public void setSingleTapListener(SingleTapListener listener) {
+ singleTapListener = listener;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return isEnabled() && gestureDetector.onTouchEvent(event);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/actions/VignetteAction.java b/src/com/android/gallery3d/photoeditor/actions/VignetteAction.java
new file mode 100644
index 000000000..f59c636db
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/actions/VignetteAction.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.photoeditor.filters.VignetteFilter;
+
+/**
+ * An action handling vignette effect.
+ */
+public class VignetteAction extends EffectAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ private ScaleSeekBar scalePicker;
+
+ public VignetteAction(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void doBegin() {
+ final VignetteFilter filter = new VignetteFilter();
+
+ scalePicker = factory.createScalePicker(EffectToolFactory.ScalePickerType.GENERIC);
+ scalePicker.setOnScaleChangeListener(new ScaleSeekBar.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setScale(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scalePicker.setProgress(DEFAULT_SCALE);
+
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void doEnd() {
+ scalePicker.setOnScaleChangeListener(null);
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java b/src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java
new file mode 100644
index 000000000..a219abe8d
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/AutoFixFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Auto-fix filter applied to the image.
+ */
+public class AutoFixFilter extends Filter {
+
+ private float scale;
+
+ /**
+ * Sets the auto-fix level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setScale(float scale) {
+ this.scale = scale;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_AUTOFIX);
+ effect.setParameter("scale", scale);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java b/src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java
new file mode 100644
index 000000000..dc6f1a7b6
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/ColorTemperatureFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Color temperature filter applied to the image.
+ */
+public class ColorTemperatureFilter extends Filter {
+
+ private float scale;
+
+ /**
+ * Sets the color temperature level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setColorTemperature(float scale) {
+ this.scale = scale;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_TEMPERATURE);
+ effect.setParameter("scale", scale);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/CropFilter.java b/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
new file mode 100644
index 000000000..372279ee2
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/CropFilter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.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;
+
+/**
+ * Crop filter applied to the image.
+ */
+public class CropFilter extends Filter {
+
+ private RectF bounds;
+
+ /**
+ * The rect coordinates used here should range from 0 to 1.
+ */
+ public void setCropBounds(RectF bounds) {
+ this.bounds = bounds;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, 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.setParameter("xorigin", Math.round(bounds.left * src.width()));
+ effect.setParameter("yorigin", Math.round(bounds.top * src.height()));
+ effect.setParameter("width", dst.width());
+ effect.setParameter("height", dst.height());
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java b/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
new file mode 100644
index 000000000..f03bf5fe9
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/CrossProcessFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Cross-process filter applied to the image.
+ */
+public class CrossProcessFilter extends Filter {
+
+ public CrossProcessFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ getEffect(context, EffectFactory.EFFECT_CROSSPROCESS).apply(
+ src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java b/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
new file mode 100644
index 000000000..800d59a05
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/DocumentaryFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Documentary filter applied to the image.
+ */
+public class DocumentaryFilter extends Filter {
+
+ public DocumentaryFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ getEffect(context, EffectFactory.EFFECT_DOCUMENTARY).apply(
+ src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java b/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
new file mode 100644
index 000000000..3c4bbf76e
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/DoodleFilter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+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;
+import com.android.gallery3d.photoeditor.actions.DoodlePaint;
+
+import java.util.Vector;
+
+/**
+ * Doodle filter applied to the image.
+ */
+public class DoodleFilter extends Filter {
+
+ private static class ColorPath {
+ private final int color;
+ private final Path path;
+
+ ColorPath(int color, Path path) {
+ this.color = color;
+ this.path = path;
+ }
+ }
+
+ private final Vector<ColorPath> doodles = new Vector<ColorPath>();
+
+ /**
+ * Signals once at least a doodle drawn within photo bounds; this filter is regarded as invalid
+ * (no-op on the photo) until not all its doodling is out of bounds.
+ */
+ public void setDoodledInPhotoBounds() {
+ validate();
+ }
+
+ /**
+ * The path coordinates used here should range from 0 to 1.
+ */
+ public void addPath(Path path, int color) {
+ doodles.add(new ColorPath(color, path));
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Bitmap bitmap = Bitmap.createBitmap(src.width(), src.height(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Matrix matrix = new Matrix();
+ matrix.setRectToRect(new RectF(0, 0, 1, 1),
+ new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()), Matrix.ScaleToFit.FILL);
+
+ Path drawingPath = new Path();
+ Paint paint = new DoodlePaint();
+ for (ColorPath doodle : doodles) {
+ paint.setColor(doodle.color);
+ drawingPath.set(doodle.path);
+ drawingPath.transform(matrix);
+ canvas.drawPath(drawingPath, paint);
+ }
+
+ Effect effect = getEffect(context, EffectFactory.EFFECT_DOODLE);
+ effect.setParameter("doodle", bitmap);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java b/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
new file mode 100644
index 000000000..68db831ac
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/DuotoneFilter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Duotone filter applied to the image.
+ */
+public class DuotoneFilter extends Filter {
+
+ private int firstColor;
+ private int secondColor;
+
+ public void setDuotone(int firstColor, int secondColor) {
+ this.firstColor = firstColor;
+ this.secondColor = secondColor;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_DUOTONE);
+ effect.setParameter("first_color", firstColor);
+ effect.setParameter("second_color", secondColor);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java b/src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java
new file mode 100644
index 000000000..a799e6f2a
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/FillLightFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Fill-light filter applied to the image.
+ */
+public class FillLightFilter extends Filter {
+
+ private float backlight;
+
+ /**
+ * Sets the backlight level.
+ *
+ * @param backlight ranges from 0 to 1.
+ */
+ public void setBacklight(float backlight) {
+ this.backlight = backlight;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_FILLLIGHT);
+ effect.setParameter("backlight", backlight);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/Filter.java b/src/com/android/gallery3d/photoeditor/filters/Filter.java
new file mode 100644
index 000000000..c2d3fe5af
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/Filter.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Image filter for photo editing.
+ */
+public abstract class Filter {
+
+ // TODO: This should be set in MFF instead.
+ private static final int DEFAULT_TILE_SIZE = 640;
+
+ private boolean isValid;
+ private EffectContext context;
+ private Effect effect;
+
+ protected void validate() {
+ isValid = true;
+ }
+
+ protected Effect getEffect(EffectContext context, String name) {
+ if (this.context != context) {
+ effect = context.getFactory().createEffect(name);
+ effect.setParameter("tile_size", DEFAULT_TILE_SIZE);
+ this.context = context;
+ }
+ return effect;
+ }
+
+ /**
+ * 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.
+ */
+ public boolean isValid() {
+ 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);
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java b/src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java
new file mode 100644
index 000000000..b1eb2d131
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/FisheyeFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Fisheye filter applied to the image.
+ */
+public class FisheyeFilter extends Filter {
+
+ private float scale;
+
+ /**
+ * Sets the fisheye distortion level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setScale(float scale) {
+ this.scale = scale;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_FISHEYE);
+ effect.setParameter("scale", scale);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java b/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
new file mode 100644
index 000000000..184f590a3
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/FlipFilter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Flip filter applied to the image.
+ */
+public class FlipFilter extends Filter {
+
+ private boolean flipHorizontal;
+ private boolean flipVertical;
+
+ public void setFlip(boolean flipHorizontal, boolean flipVertical) {
+ this.flipHorizontal = flipHorizontal;
+ this.flipVertical = flipVertical;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_FLIP);
+ effect.setParameter("horizontal", flipHorizontal);
+ effect.setParameter("vertical", flipVertical);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/GrainFilter.java b/src/com/android/gallery3d/photoeditor/filters/GrainFilter.java
new file mode 100644
index 000000000..1ee8a9c3f
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/GrainFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Film grain filter applied to the image.
+ */
+public class GrainFilter extends Filter {
+
+ private float scale;
+
+ /**
+ * Set the grain noise level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setScale(float scale) {
+ this.scale = scale;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_GRAIN);
+ effect.setParameter("scale", scale);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java b/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
new file mode 100644
index 000000000..ce6cdef9c
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/GrayscaleFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Grayscale filter applied to the image.
+ */
+public class GrayscaleFilter extends Filter {
+
+ public GrayscaleFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ getEffect(context, EffectFactory.EFFECT_GRAYSCALE).apply(
+ src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java b/src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java
new file mode 100644
index 000000000..3169d043f
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/HighlightFilter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Highlight filter applied to the image.
+ */
+public class HighlightFilter extends Filter {
+
+ private float white;
+
+ /**
+ * Sets the highlight level.
+ *
+ * @param highlight ranges from 0 to 1.
+ */
+ public void setHighlight(float highlight) {
+ white = 1f - highlight * 0.5f;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_BLACKWHITE);
+ effect.setParameter("black", 0f);
+ effect.setParameter("white", white);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java b/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
new file mode 100644
index 000000000..95ff62740
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/LomoishFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Lomo-ish filter applied to the image.
+ */
+public class LomoishFilter extends Filter {
+
+ public LomoishFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ getEffect(context, EffectFactory.EFFECT_LOMOISH).apply(
+ src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java b/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
new file mode 100644
index 000000000..0b3837f34
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/NegativeFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Negative filter applied to the image.
+ */
+public class NegativeFilter extends Filter {
+
+ public NegativeFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ getEffect(context, EffectFactory.EFFECT_NEGATIVE).apply(
+ src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java b/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
new file mode 100644
index 000000000..51202dcc1
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/PosterizeFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Posterize filter applied to the image.
+ */
+public class PosterizeFilter extends Filter {
+
+ public PosterizeFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ getEffect(context, EffectFactory.EFFECT_POSTERIZE).apply(
+ src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java b/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
new file mode 100644
index 000000000..559819d9c
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/RedEyeFilter.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.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;
+
+import java.util.Vector;
+
+/**
+ * Red-eye removal filter applied to the image.
+ */
+public class RedEyeFilter extends Filter {
+
+ private final Vector<PointF> redeyes = new Vector<PointF>();
+
+ /**
+ * The point coordinates used here should range from 0 to 1.
+ */
+ public void addRedEyePosition(PointF point) {
+ redeyes.add(point);
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_REDEYE);
+ float[] centers = new float[redeyes.size() * 2];
+ int i = 0;
+ for (PointF eye : redeyes) {
+ centers[i++] = eye.x;
+ centers[i++] = eye.y;
+ }
+ effect.setParameter("centers", centers);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java b/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
new file mode 100644
index 000000000..2823466d1
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/RotateFilter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Rotate filter applied to the image.
+ */
+public class RotateFilter extends Filter {
+
+ private float degrees;
+
+ public void setAngle(float degrees) {
+ this.degrees = degrees;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ if (degrees % 180 != 0) {
+ dst.changeDimension(src.height(), src.width());
+ }
+ Effect effect = getEffect(context, EffectFactory.EFFECT_ROTATE);
+ effect.setParameter("degree", degrees);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java b/src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java
new file mode 100644
index 000000000..d3b5f704f
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/SaturationFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Saturation filter applied to the image.
+ */
+public class SaturationFilter extends Filter {
+
+ private float scale;
+
+ /**
+ * Sets the saturation level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setSaturation(float scale) {
+ this.scale = (scale - 0.5f) * 2;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_SATURATE);
+ effect.setParameter("scale", scale);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java b/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
new file mode 100644
index 000000000..3fdda15f8
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/SepiaFilter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Sepia filter applied to the image.
+ */
+public class SepiaFilter extends Filter {
+
+ public SepiaFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ getEffect(context, EffectFactory.EFFECT_SEPIA).apply(
+ src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java b/src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java
new file mode 100644
index 000000000..b23ef1c67
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/ShadowFilter.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Shadow filter applied to the image.
+ */
+public class ShadowFilter extends Filter {
+
+ private float black;
+
+ /**
+ * Sets the shadow blackness level.
+ *
+ * @param shadow ranges from 0 to 1.
+ */
+ public void setShadow(float shadow) {
+ black = shadow * 0.5f;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_BLACKWHITE);
+ effect.setParameter("black", black);
+ effect.setParameter("white", 1f);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java b/src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java
new file mode 100644
index 000000000..a8097126f
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/SharpenFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Sharpen filter applied to the image.
+ */
+public class SharpenFilter extends Filter {
+
+ private float scale;
+
+ /**
+ * Sets the sharpen level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setSharpen(float scale) {
+ this.scale = scale;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_SHARPEN);
+ effect.setParameter("scale", scale);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java b/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
new file mode 100644
index 000000000..ffeb445a9
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/StraightenFilter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Straighten filter applied to the image.
+ */
+public class StraightenFilter extends Filter {
+
+ public static final float MAX_DEGREES = 30.0f;
+
+ private float angle;
+
+ public void setAngle(float degrees) {
+ angle = -degrees;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_STRAIGHTEN);
+ effect.setParameter("maxAngle", MAX_DEGREES);
+ effect.setParameter("angle", angle);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/TintFilter.java b/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
new file mode 100644
index 000000000..bbaf9c787
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/TintFilter.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Tint filter applied to the image.
+ */
+public class TintFilter extends Filter {
+
+ private int tint;
+
+ public void setTint(int color) {
+ tint = color;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_TINT);
+ effect.setParameter("tint", tint);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}
diff --git a/src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java b/src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java
new file mode 100644
index 000000000..d066c8b7b
--- /dev/null
+++ b/src/com/android/gallery3d/photoeditor/filters/VignetteFilter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.photoeditor.filters;
+
+import android.media.effect.Effect;
+import android.media.effect.EffectContext;
+import android.media.effect.EffectFactory;
+
+import com.android.gallery3d.photoeditor.Photo;
+
+/**
+ * Vignette filter applied to the image.
+ */
+public class VignetteFilter extends Filter {
+
+ private float range;
+
+ /**
+ * Sets the vignette range scale.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setScale(float scale) {
+ // The 'range' is between 1.3 to 0.6. When scale is zero then range is 1.3
+ // which means no vignette at all because the luminousity difference is
+ // less than 1/256 and will cause nothing.
+ range = 1.30f - (float) Math.sqrt(scale) * 0.7f;
+ validate();
+ }
+
+ @Override
+ public void process(EffectContext context, Photo src, Photo dst) {
+ Effect effect = getEffect(context, EffectFactory.EFFECT_VIGNETTE);
+ effect.setParameter("range", range);
+ effect.apply(src.texture(), src.width(), src.height(), dst.texture());
+ }
+}