summaryrefslogtreecommitdiff
path: root/samples/PhotoEditor/src/com/android/photoeditor
diff options
context:
space:
mode:
Diffstat (limited to 'samples/PhotoEditor/src/com/android/photoeditor')
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/ActionBar.java166
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/BitmapUtils.java201
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/EffectsBar.java241
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/EffectsGroup.java123
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/FilterStack.java263
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/IconIndicator.java80
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/LoadScreennailTask.java68
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/Photo.java87
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/PhotoEditor.java157
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/PhotoOutputCallback.java25
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/PhotoView.java205
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/RectUtils.java88
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/SaveCopyTask.java142
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/SpinnerProgressDialog.java69
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/Toolbar.java206
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/ToolbarIdleHandler.java102
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/ToolbarLayoutHandler.java50
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/AutoFixAction.java60
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/ColorPath.java57
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/ColorTemperatureAction.java57
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/ColorWheel.java330
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/CropAction.java77
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/CropView.java330
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/CrossProcessAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/DocumentaryAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleAction.java83
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleView.java138
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/DuotoneAction.java48
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/FillLightAction.java57
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/FilterAction.java184
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/FisheyeAction.java60
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/FlipAction.java99
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/GrainAction.java60
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/GrayscaleAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/HighlightAction.java57
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/LomoishAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/NegativeAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/PosterizeAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/RedEyeAction.java53
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/RotateAction.java115
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/RotateView.java222
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/SaturationAction.java57
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/ScaleWheel.java226
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/SepiaAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/ShadowAction.java57
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/SharpenAction.java60
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/SoftFocusAction.java57
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/StraightenAction.java88
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/TintAction.java61
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/TouchView.java117
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/VignetteAction.java60
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/actions/WarmifyAction.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/animation/AnimationPair.java41
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/animation/FadeAnimation.java38
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/animation/Rotate3DAnimation.java144
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/AutoFixFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/ColorTemperatureFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/CropFilter.java43
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/CrossProcessFilter.java34
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/DocumentaryFilter.java36
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/DoodleFilter.java63
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/DuotoneFilter.java39
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/FillLightFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/Filter.java47
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/FisheyeFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/FlipFilter.java47
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/GrainFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/GrayscaleFilter.java34
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/HighlightFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/ImageUtils.java60
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/LomoishFilter.java37
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/NegativeFilter.java34
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/PosterizeFilter.java34
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/RedEyeFilter.java49
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/RotateFilter.java40
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/SaturationFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/SepiaFilter.java34
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/ShadowFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/SharpenFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/SoftFocusFilter.java42
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/StraightenFilter.java54
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/TintFilter.java37
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/VignetteFilter.java45
-rw-r--r--samples/PhotoEditor/src/com/android/photoeditor/filters/WarmifyFilter.java34
84 files changed, 6923 insertions, 0 deletions
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/ActionBar.java b/samples/PhotoEditor/src/com/android/photoeditor/ActionBar.java
new file mode 100644
index 0000000..4465741
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/ActionBar.java
@@ -0,0 +1,166 @@
+/*
+ * 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.photoeditor;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ViewSwitcher;
+
+/**
+ * Action bar that contains buttons such as undo, redo, save, etc. and listens to stack changes for
+ * enabling/disabling buttons.
+ */
+public class ActionBar extends ViewSwitcher implements FilterStack.StackListener {
+
+ /**
+ * Listener of action button clicked.
+ */
+ public interface ActionBarListener {
+
+ void onQuickview(boolean on);
+
+ void onUndo();
+
+ void onRedo();
+
+ void onSave();
+ }
+
+ private static final int ENABLE_BUTTON = 1;
+ private static final int ENABLED_ALPHA = 255;
+ private static final int DISABLED_ALPHA = 120;
+
+ private final Handler handler;
+ private ImageButton save;
+ private ImageButton undo;
+ private ImageButton redo;
+ private ImageButton quickview;
+
+ 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(quickview, canUndo);
+ enableButton(save, canUndo);
+ enableButton(undo, canUndo);
+ enableButton(redo, canRedo);
+ break;
+ }
+ }
+ };
+ }
+
+ /**
+ * Initializes with a non-null ActionBarListener.
+ */
+ public void initialize(final ActionBarListener listener) {
+ save = (ImageButton) findViewById(R.id.save_button);
+ save.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ listener.onSave();
+ }
+ }
+ });
+
+ undo = (ImageButton) findViewById(R.id.undo_button);
+ undo.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ listener.onUndo();
+ }
+ }
+ });
+
+ redo = (ImageButton) findViewById(R.id.redo_button);
+ redo.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ listener.onRedo();
+ }
+ }
+ });
+
+ quickview = (ImageButton) findViewById(R.id.quickview_button);
+ quickview.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ ActionBar.this.showNext();
+ listener.onQuickview(true);
+ }
+ }
+ });
+
+ View quickviewOn = findViewById(R.id.quickview_on_button);
+ quickviewOn.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ ActionBar.this.showNext();
+ listener.onQuickview(false);
+ }
+ }
+ });
+
+ resetButtons();
+ }
+
+ public void resetButtons() {
+ // Disable buttons immediately instead of waiting for ENABLE_BUTTON messages which may
+ // happen some time later after stack changes.
+ enableButton(save, false);
+ enableButton(undo, false);
+ enableButton(redo, false);
+ enableButton(quickview, false);
+ }
+
+ public void disableSave() {
+ enableButton(save, false);
+ }
+
+ private void enableButton(ImageButton 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/samples/PhotoEditor/src/com/android/photoeditor/BitmapUtils.java b/samples/PhotoEditor/src/com/android/photoeditor/BitmapUtils.java
new file mode 100644
index 0000000..8678f10
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/BitmapUtils.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.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.Matrix;
+import android.graphics.Rect;
+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;
+ }
+
+ /**
+ * Returns an immutable bitmap from subset of source bitmap transformed by the given matrix.
+ */
+ public static Bitmap createBitmap(Bitmap bitmap, Matrix m) {
+ // TODO: Re-implement createBitmap to avoid ARGB -> RBG565 conversion on platforms
+ // prior to honeycomb.
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true);
+ }
+
+ 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);
+ if ((cursor != null) && cursor.moveToNext()) {
+ orientation = cursor.getInt(INDEX_ORIENTATION);
+ }
+ return orientation;
+ }
+
+ /**
+ * Decodes immutable bitmap that keeps aspect-ratio and spans most within the given rectangle.
+ */
+ 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, 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, 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/samples/PhotoEditor/src/com/android/photoeditor/EffectsBar.java b/samples/PhotoEditor/src/com/android/photoeditor/EffectsBar.java
new file mode 100644
index 0000000..63cbf3f
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/EffectsBar.java
@@ -0,0 +1,241 @@
+/*
+ * 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.photoeditor;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.android.photoeditor.actions.AutoFixAction;
+import com.android.photoeditor.actions.ColorTemperatureAction;
+import com.android.photoeditor.actions.CropAction;
+import com.android.photoeditor.actions.CrossProcessAction;
+import com.android.photoeditor.actions.DocumentaryAction;
+import com.android.photoeditor.actions.DoodleAction;
+import com.android.photoeditor.actions.DuotoneAction;
+import com.android.photoeditor.actions.FillLightAction;
+import com.android.photoeditor.actions.FilterAction;
+import com.android.photoeditor.actions.FisheyeAction;
+import com.android.photoeditor.actions.FlipAction;
+import com.android.photoeditor.actions.GrainAction;
+import com.android.photoeditor.actions.GrayscaleAction;
+import com.android.photoeditor.actions.HighlightAction;
+import com.android.photoeditor.actions.LomoishAction;
+import com.android.photoeditor.actions.NegativeAction;
+import com.android.photoeditor.actions.PosterizeAction;
+import com.android.photoeditor.actions.RedEyeAction;
+import com.android.photoeditor.actions.RotateAction;
+import com.android.photoeditor.actions.SaturationAction;
+import com.android.photoeditor.actions.SepiaAction;
+import com.android.photoeditor.actions.ShadowAction;
+import com.android.photoeditor.actions.SharpenAction;
+import com.android.photoeditor.actions.StraightenAction;
+import com.android.photoeditor.actions.TintAction;
+import com.android.photoeditor.actions.VignetteAction;
+import com.android.photoeditor.actions.WarmifyAction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Scroll view that contains all effects for editing photo by mapping each effect to trigger one
+ * corresponding FilterAction.
+ */
+public class EffectsBar extends ScrollView {
+
+ private final List<Effect> effects = new ArrayList<Effect>();
+ private TextView effectName;
+
+ public EffectsBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void initialize(FilterStack filterStack, PhotoView photoView, ViewGroup tools) {
+ effects.add(new Effect(R.id.autofix_effect,
+ new AutoFixAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.crop_effect,
+ new CropAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.crossprocess_effect,
+ new CrossProcessAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.documentary_effect,
+ new DocumentaryAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.doodle_effect,
+ new DoodleAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.duotone_effect,
+ new DuotoneAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.filllight_effect,
+ new FillLightAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.fisheye_effect,
+ new FisheyeAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.flip_effect,
+ new FlipAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.grain_effect,
+ new GrainAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.grayscale_effect,
+ new GrayscaleAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.highlight_effect,
+ new HighlightAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.lomoish_effect,
+ new LomoishAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.negative_effect,
+ new NegativeAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.posterize_effect,
+ new PosterizeAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.redeye_effect,
+ new RedEyeAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.rotate_effect,
+ new RotateAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.saturation_effect,
+ new SaturationAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.sepia_effect,
+ new SepiaAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.shadow_effect,
+ new ShadowAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.sharpen_effect,
+ new SharpenAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.straighten_effect,
+ new StraightenAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.temperature_effect,
+ new ColorTemperatureAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.tint_effect,
+ new TintAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.vignette_effect,
+ new VignetteAction(filterStack, tools)));
+
+ effects.add(new Effect(R.id.warmify_effect,
+ new WarmifyAction(filterStack, tools)));
+
+ effectName = (TextView) tools.findViewById(R.id.action_effect_name);
+
+ // Disable hardware acceleration on this view to make alpha animations work for idle fading.
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+ setEnabled(false);
+ }
+
+ public void effectsOff(Runnable runnableOnEffectsOff) {
+ for (Effect effect : effects) {
+ if (effect.on) {
+ effect.turnOff(runnableOnEffectsOff);
+ return;
+ }
+ }
+ // Just execute the runnable right away if all effects are already off.
+ if (runnableOnEffectsOff != null) {
+ runnableOnEffectsOff.run();
+ }
+ }
+
+ public boolean hasEffectOn() {
+ for (Effect effect : effects) {
+ if (effect.on) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private class Effect implements FilterAction.FilterActionListener {
+
+ private final FilterAction action;
+ private final CharSequence name;
+ private final IconIndicator button;
+ private boolean on;
+ private Runnable runnableOnODone;
+
+ public Effect(int effectId, FilterAction action) {
+ this.action = action;
+
+ View view = findViewById(effectId);
+ name = ((TextView) view.findViewById(R.id.effect_label)).getText();
+ button = (IconIndicator) view.findViewById(R.id.effect_button);
+ button.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (isEnabled()) {
+ if (on) {
+ turnOff(null);
+ } else {
+ // Have other effects done turning off first and then turn on itself.
+ effectsOff(new Runnable() {
+
+ @Override
+ public void run() {
+ turnOn();
+ }
+ });
+ }
+ }
+ }
+ });
+ }
+
+ private void turnOn() {
+ effectName.setText(name);
+ button.setMode("on");
+ on = true;
+ action.begin(this);
+ }
+
+ private void turnOff(Runnable runnableOnODone) {
+ this.runnableOnODone = runnableOnODone;
+ action.end();
+ }
+
+ @Override
+ public void onDone() {
+ if (on) {
+ effectName.setText("");
+ button.setMode("off");
+ on = false;
+
+ if (runnableOnODone != null) {
+ runnableOnODone.run();
+ runnableOnODone = null;
+ }
+ }
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/EffectsGroup.java b/samples/PhotoEditor/src/com/android/photoeditor/EffectsGroup.java
new file mode 100644
index 0000000..7c1f9af
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/EffectsGroup.java
@@ -0,0 +1,123 @@
+/*
+ * 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.photoeditor;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Groups effects in an accordion menu style that could either expand or fold effects.
+ */
+public class EffectsGroup extends LinearLayout {
+
+ private static final int ANIMATION_INTERVAL = 75;
+
+ private final Drawable downArrow;
+ private final Drawable rightArrow;
+
+ private boolean expandEffects;
+
+ public EffectsGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ downArrow = context.getResources().getDrawable(R.drawable.arrow_down);
+ rightArrow = context.getResources().getDrawable(R.drawable.arrow_right);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ if (expandEffects) {
+ requestRectangleOnScreen(new Rect(0, 0, 0, getHeight()));
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ final ImageView arrow = (ImageView) findViewById(R.id.group_arrow);
+ final ViewGroup container = (ViewGroup) findViewById(R.id.grouped_effects);
+ final List<View> effects = new ArrayList<View>();
+ for (int i = 0; i < container.getChildCount(); i++) {
+ effects.add(container.getChildAt(i));
+ }
+
+ findViewById(R.id.group_header).setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+
+ if (container.getVisibility() == VISIBLE) {
+ expandEffects = false;
+
+ int delay = 0;
+ for (int i = effects.size() - 1; i >= 0; i--) {
+ final View effect = effects.get(i);
+
+ postDelayed(new Runnable() {
+
+ @Override
+ public void run() {
+ effect.setVisibility(GONE);
+ }
+ }, delay);
+ delay += ANIMATION_INTERVAL;
+ }
+
+ postDelayed(new Runnable() {
+
+ @Override
+ public void run() {
+ container.setVisibility(GONE);
+ arrow.setImageDrawable(rightArrow);
+ }
+ }, delay - ANIMATION_INTERVAL);
+ } else {
+ expandEffects = true;
+
+ arrow.setImageDrawable(downArrow);
+ container.setVisibility(VISIBLE);
+
+ int delay = 0;
+ for (int i = 0; i < effects.size(); i++) {
+ final View effect = effects.get(i);
+
+ postDelayed(new Runnable() {
+
+ @Override
+ public void run() {
+ effect.setVisibility(VISIBLE);
+ }
+ }, delay);
+ delay += ANIMATION_INTERVAL;
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/FilterStack.java b/samples/PhotoEditor/src/com/android/photoeditor/FilterStack.java
new file mode 100644
index 0000000..9a9b8d7
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/FilterStack.java
@@ -0,0 +1,263 @@
+/*
+ * 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.photoeditor;
+
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import com.android.photoeditor.filters.Filter;
+
+import java.util.Stack;
+import java.util.Vector;
+
+/**
+ * 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 static class OutputMessageObj {
+ PhotoOutputCallback callback;
+ Photo result;
+ }
+
+ private static final int COPY_SOURCE = 1;
+ private static final int COPY_RESULT = 2;
+ private static final int SET_SOURCE = 3;
+ private static final int CLEAR_SOURCE = 4;
+ private static final int CLEAR_STACKS = 5;
+ private static final int PUSH_FILTER = 6;
+ private static final int UNDO = 7;
+ private static final int REDO = 8;
+ private static final int TOP_FILTER_CHANGE = 9;
+ private static final int OUTPUT = 10;
+
+ private final Stack<Filter> appliedStack = new Stack<Filter>();
+ private final Stack<Filter> redoStack = new Stack<Filter>();
+ private final Vector<Message> pendingMessages = new Vector<Message>();
+ private final Handler mainHandler;
+ private final Handler workerHandler;
+
+ // Use two photo buffers as in and out in turns to apply filters in the stack.
+ private final Photo[] buffers = new Photo[2];
+
+ private Photo source;
+ private StackListener stackListener;
+
+ public FilterStack() {
+ HandlerThread workerThread = new HandlerThread("FilterStackWorker");
+ workerThread.start();
+
+ mainHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case OUTPUT:
+ OutputMessageObj obj = (OutputMessageObj) msg.obj;
+ obj.callback.onReady(obj.result);
+ break;
+ }
+ }
+ };
+ workerHandler = new Handler(workerThread.getLooper()) {
+
+ private void output(PhotoOutputCallback callback, Photo target) {
+ // Copy target photo in rgb-565 format to update photo-view or save.
+ OutputMessageObj obj = new OutputMessageObj();
+ obj.callback = callback;
+ obj.result = (target != null) ? target.copy(Bitmap.Config.RGB_565) : null;
+ mainHandler.sendMessage(mainHandler.obtainMessage(OUTPUT, obj));
+ }
+
+ private void clearBuffers() {
+ pendingMessages.clear();
+ workerHandler.removeMessages(TOP_FILTER_CHANGE);
+ mainHandler.removeMessages(OUTPUT);
+ 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(Bitmap.createBitmap(
+ buffers[other].width(), buffers[other].height(), Bitmap.Config.ARGB_8888));
+ }
+
+ private void invalidate() {
+ // In/out buffers need redrawn by reloading source photo and re-applying filters.
+ clearBuffers();
+ buffers[0] = (source != null) ? source.copy(Bitmap.Config.ARGB_8888) : null;
+ if (buffers[0] != null) {
+ reallocateBuffer(1);
+
+ int out = 1;
+ for (Filter filter : appliedStack) {
+ runFilter(filter, out);
+ out = out ^ 1;
+ }
+ }
+ }
+
+ private void runFilter(Filter filter, int out) {
+ if ((buffers[0] != null) && (buffers[1] != null)) {
+ int in = out ^ 1;
+ filter.process(buffers[in], buffers[out]);
+ if (!buffers[in].matchDimension(buffers[out])) {
+ buffers[in].clear();
+ reallocateBuffer(in);
+ }
+ }
+ }
+
+ private int getOutBufferIndex() {
+ // 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].
+ return appliedStack.size() % 2;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case COPY_SOURCE:
+ output((PhotoOutputCallback) msg.obj, source);
+ break;
+
+ case COPY_RESULT:
+ output((PhotoOutputCallback) msg.obj, buffers[getOutBufferIndex()]);
+ break;
+
+ case SET_SOURCE:
+ source = (Photo) msg.obj;
+ invalidate();
+ break;
+
+ case CLEAR_SOURCE:
+ if (source != null) {
+ source.clear();
+ source = null;
+ }
+ clearBuffers();
+ break;
+
+ case CLEAR_STACKS:
+ redoStack.clear();
+ appliedStack.clear();
+ break;
+
+ case PUSH_FILTER:
+ redoStack.clear();
+ appliedStack.push((Filter) msg.obj);
+ stackChanged();
+ break;
+
+ case UNDO:
+ if (!appliedStack.empty()) {
+ redoStack.push(appliedStack.pop());
+ stackChanged();
+ invalidate();
+ }
+ output((PhotoOutputCallback) msg.obj, buffers[getOutBufferIndex()]);
+ break;
+
+ case REDO:
+ if (!redoStack.empty()) {
+ Filter filter = redoStack.pop();
+ appliedStack.push(filter);
+ stackChanged();
+ runFilter(filter, getOutBufferIndex());
+ }
+ output((PhotoOutputCallback) msg.obj, buffers[getOutBufferIndex()]);
+ break;
+
+ case TOP_FILTER_CHANGE:
+ if (pendingMessages.remove(msg) && !appliedStack.empty()) {
+ int out = getOutBufferIndex();
+ runFilter(appliedStack.peek(), out);
+ output((PhotoOutputCallback) msg.obj, buffers[out]);
+ }
+ break;
+ }
+ }
+ };
+ }
+
+ public void getSourceCopy(PhotoOutputCallback callback) {
+ workerHandler.sendMessage(workerHandler.obtainMessage(COPY_SOURCE, callback));
+ }
+
+ public void getResultCopy(PhotoOutputCallback callback) {
+ workerHandler.sendMessage(workerHandler.obtainMessage(COPY_RESULT, callback));
+ }
+
+ public void setPhotoSource(Photo source) {
+ workerHandler.sendMessage(workerHandler.obtainMessage(SET_SOURCE, source));
+ }
+
+ public void clearPhotoSource() {
+ workerHandler.sendMessage(workerHandler.obtainMessage(CLEAR_SOURCE));
+ }
+
+ public void clearStacks() {
+ workerHandler.sendMessage(workerHandler.obtainMessage(CLEAR_STACKS));
+ }
+
+ public void pushFilter(Filter filter) {
+ workerHandler.sendMessage(workerHandler.obtainMessage(PUSH_FILTER, filter));
+ }
+
+ public void undo(PhotoOutputCallback callback) {
+ workerHandler.sendMessage(workerHandler.obtainMessage(UNDO, callback));
+ }
+
+ public void redo(PhotoOutputCallback callback) {
+ workerHandler.sendMessage(workerHandler.obtainMessage(REDO, callback));
+ }
+
+ public void topFilterChanged(PhotoOutputCallback callback) {
+ // Flush outdated top-filter messages before sending new ones.
+ Message msg = workerHandler.obtainMessage(TOP_FILTER_CHANGE, callback);
+ pendingMessages.clear();
+ pendingMessages.add(msg);
+ workerHandler.removeMessages(TOP_FILTER_CHANGE);
+ workerHandler.sendMessage(msg);
+ }
+
+ public synchronized void setStackListener(StackListener listener) {
+ stackListener = listener;
+ }
+
+ private synchronized void stackChanged() {
+ if (stackListener != null) {
+ stackListener.onStackChanged(!appliedStack.empty(), !redoStack.empty());
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/IconIndicator.java b/samples/PhotoEditor/src/com/android/photoeditor/IconIndicator.java
new file mode 100644
index 0000000..173e88e
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/IconIndicator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.photoeditor;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * Represents image icons/buttons having various modes.
+ */
+public class IconIndicator extends ImageView {
+
+ private Drawable[] mIcons;
+ private CharSequence[] mModes;
+
+ public IconIndicator(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ TypedArray a = context
+ .obtainStyledAttributes(attrs, R.styleable.IconIndicator, defStyle, 0);
+ Drawable icons[] = loadIcons(context.getResources(), a.getResourceId(
+ R.styleable.IconIndicator_icons, 0));
+ CharSequence modes[] = a.getTextArray(R.styleable.IconIndicator_modes);
+ a.recycle();
+
+ setModesAndIcons(modes, icons);
+ setImageDrawable(mIcons.length > 0 ? mIcons[0] : null);
+ }
+
+ public IconIndicator(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ private Drawable[] loadIcons(Resources resources, int iconsId) {
+ TypedArray array = resources.obtainTypedArray(iconsId);
+ int n = array.length();
+ Drawable drawable[] = new Drawable[n];
+ for (int i = 0; i < n; ++i) {
+ int id = array.getResourceId(i, 0);
+ drawable[i] = id == 0 ? null : resources.getDrawable(id);
+ }
+ array.recycle();
+ return drawable;
+ }
+
+ private void setModesAndIcons(CharSequence[] modes, Drawable icons[]) {
+ if (modes.length != icons.length || icons.length == 0) {
+ throw new IllegalArgumentException();
+ }
+ mIcons = icons;
+ mModes = modes;
+ }
+
+ public void setMode(String mode) {
+ for (int i = 0, n = mModes.length; i < n; ++i) {
+ if (mModes[i].equals(mode)) {
+ setImageDrawable(mIcons[i]);
+ return;
+ }
+ }
+ throw new IllegalArgumentException("unknown mode: " + mode);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/LoadScreennailTask.java b/samples/PhotoEditor/src/com/android/photoeditor/LoadScreennailTask.java
new file mode 100644
index 0000000..89ba0e2
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/LoadScreennailTask.java
@@ -0,0 +1,68 @@
+/*
+ * 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.photoeditor;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.widget.Toast;
+
+/**
+ * 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);
+ }
+
+ // TODO: Support 1280x960 once OOM is fixed.
+ private static final int SCREENNAIL_WIDTH = 1024;
+ private static final int SCREENNAIL_HEIGHT = 768;
+
+ 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.makeText(context, R.string.loading_failure, Toast.LENGTH_SHORT).show();
+ }
+ callback.onComplete(bitmap);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/Photo.java b/samples/PhotoEditor/src/com/android/photoeditor/Photo.java
new file mode 100644
index 0000000..07898a9
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/Photo.java
@@ -0,0 +1,87 @@
+/*
+ * 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.photoeditor;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+
+/**
+ * Photo that is used for editing/display and should be synchronized for concurrent access.
+ */
+public class Photo {
+
+ private Bitmap bitmap;
+
+ /**
+ * Factory method to ensure every Photo instance holds a non-null bitmap.
+ */
+ public static Photo create(Bitmap bitmap) {
+ return (bitmap != null) ? new Photo(bitmap) : null;
+ }
+
+ private Photo(Bitmap bitmap) {
+ this.bitmap = bitmap;
+ }
+
+ public Bitmap bitmap() {
+ return bitmap;
+ }
+
+ public Photo copy(Bitmap.Config config) {
+ Bitmap copy = bitmap.copy(config, true);
+ return (copy != null) ? new Photo(copy) : null;
+ }
+
+ public boolean matchDimension(Photo photo) {
+ return ((photo.width() == width()) && (photo.height() == height()));
+ }
+
+ public int width() {
+ return bitmap.getWidth();
+ }
+
+ public int height() {
+ return bitmap.getHeight();
+ }
+
+ public void transform(Matrix matrix) {
+ // Copy immutable transformed photo; no-op if it fails to ensure bitmap isn't assigned null.
+ Bitmap transformed = BitmapUtils.createBitmap(bitmap, matrix).copy(
+ bitmap.getConfig(), true);
+ if (transformed != null) {
+ bitmap.recycle();
+ bitmap = transformed;
+ }
+ }
+
+ public void crop(int left, int top, int width, int height) {
+ // Copy immutable cropped photo; no-op if it fails to ensure bitmap isn't assigned null.
+ Bitmap cropped = Bitmap.createBitmap(bitmap, left, top, width, height).copy(
+ bitmap.getConfig(), true);
+ if (cropped != null) {
+ bitmap.recycle();
+ bitmap = cropped;
+ }
+ }
+
+ /**
+ * Recycles bitmaps; this instance should not be used after its clear() is called.
+ */
+ public void clear() {
+ bitmap.recycle();
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/PhotoEditor.java b/samples/PhotoEditor/src/com/android/photoeditor/PhotoEditor.java
new file mode 100644
index 0000000..928f523
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/PhotoEditor.java
@@ -0,0 +1,157 @@
+/*
+ * 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.photoeditor;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.android.photoeditor.filters.ImageUtils;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.View;
+
+/**
+ * Main activity of the photo editor.
+ */
+public class PhotoEditor extends Activity {
+
+ private Toolbar toolbar;
+ private View backButton;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+ {
+ // HACK: create faked view in order to read bitcode in resource
+ View view = new View(getApplication());
+ byte[] pgm;
+ int pgmLength;
+
+ // read bitcode in res
+ InputStream is = view.getResources().openRawResource(R.raw.libjni_photoeditor_portable);
+ try {
+ try {
+ pgm = new byte[1024];
+ pgmLength = 0;
+
+ while(true) {
+ int bytesLeft = pgm.length - pgmLength;
+ if (bytesLeft == 0) {
+ byte[] buf2 = new byte[pgm.length * 2];
+ System.arraycopy(pgm, 0, buf2, 0, pgm.length);
+ pgm = buf2;
+ bytesLeft = pgm.length - pgmLength;
+ }
+ int bytesRead = is.read(pgm, pgmLength, bytesLeft);
+ if (bytesRead <= 0) {
+ break;
+ }
+ pgmLength += bytesRead;
+ }
+ ImageUtils.init(pgm, pgmLength);
+ } finally {
+ is.close();
+ }
+ } catch(IOException e) {
+ throw new Resources.NotFoundException();
+ }
+ }
+
+ toolbar = (Toolbar) findViewById(R.id.toolbar);
+ toolbar.initialize();
+
+ final EffectsBar effectsBar = (EffectsBar) findViewById(R.id.effects_bar);
+ final View actionBar = findViewById(R.id.action_bar);
+ final View quickviewOn = findViewById(R.id.quickview_on_button);
+ backButton = findViewById(R.id.action_bar_back);
+ backButton.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (actionBar.isEnabled()) {
+ if (quickviewOn.getVisibility() == View.VISIBLE) {
+ quickviewOn.performClick();
+ } else if (effectsBar.hasEffectOn()) {
+ effectsBar.effectsOff(null);
+ } else {
+ tryRun(new Runnable() {
+
+ @Override
+ public void run() {
+ finish();
+ }
+ });
+ }
+ }
+ }
+ });
+
+ Intent intent = getIntent();
+ if (Intent.ACTION_EDIT.equalsIgnoreCase(intent.getAction()) && (intent.getData() != null)) {
+ toolbar.openPhoto(intent.getData());
+ }
+ }
+
+ 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();
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/PhotoOutputCallback.java b/samples/PhotoEditor/src/com/android/photoeditor/PhotoOutputCallback.java
new file mode 100644
index 0000000..75651f3
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/PhotoOutputCallback.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.photoeditor;
+
+/**
+ * Callback of photo output that will only be called in UI thread.
+ */
+public interface PhotoOutputCallback {
+
+ void onReady(Photo photo);
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/PhotoView.java b/samples/PhotoEditor/src/com/android/photoeditor/PhotoView.java
new file mode 100644
index 0000000..1853d7c
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/PhotoView.java
@@ -0,0 +1,205 @@
+/*
+ * 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.photoeditor;
+
+import android.content.Context;
+import android.graphics.Canvas;
+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.View;
+import android.view.animation.Animation;
+
+import com.android.photoeditor.animation.AnimationPair;
+
+/**
+ * Displays photo in the view. All its methods should be called from UI thread.
+ */
+public class PhotoView extends View {
+
+ private final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+ private final Matrix displayMatrix = new Matrix();
+ private Photo photo;
+ private RectF clipBounds;
+ private AnimationPair transitions;
+
+ public PhotoView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (photo != null) {
+ canvas.save();
+ if (clipBounds != null) {
+ canvas.clipRect(clipBounds);
+ }
+ canvas.concat(displayMatrix);
+ canvas.drawBitmap(photo.bitmap(), 0, 0, paint);
+ canvas.restore();
+ }
+ }
+
+ /**
+ * Maps x and y to a percentage position relative to displayed photo.
+ */
+ public PointF mapPhotoPoint(float x, float y) {
+ if ((photo == null) || (photo.width() == 0) || (photo.height() == 0)) {
+ return new PointF();
+ }
+ float[] point = new float[] {x, y};
+ Matrix matrix = new Matrix();
+ displayMatrix.invert(matrix);
+ matrix.mapPoints(point);
+ return new PointF(point[0] / photo.width(), point[1] / photo.height());
+ }
+
+ public void mapPhotoPath(Path src, Path dst) {
+ // TODO: Use percentages representing paths for saving photo larger than previewed photo.
+ Matrix matrix = new Matrix();
+ displayMatrix.invert(matrix);
+ src.transform(matrix, dst);
+ }
+
+ public RectF getPhotoDisplayBounds() {
+ RectF bounds = getPhotoBounds();
+ displayMatrix.mapRect(bounds);
+ return bounds;
+ }
+
+ public RectF getPhotoBounds() {
+ return (photo != null) ? new RectF(0, 0, photo.width(), photo.height()) : new RectF();
+ }
+
+ /**
+ * Transforms display by replacing the display matrix of photo-view with the given matrix.
+ */
+ public void transformDisplay(Matrix matrix) {
+ RectF bounds = getPhotoBounds();
+ matrix.mapRect(bounds);
+ displayMatrix.set(matrix);
+ RectUtils.postCenterMatrix(bounds, this, displayMatrix);
+ invalidate();
+ }
+
+ public void clipPhoto(RectF bounds) {
+ clipBounds = bounds;
+ invalidate();
+ }
+
+ /**
+ * Updates the photo with animations (if any) and also updates photo display-matrix.
+ */
+ public void update(final Photo photo) {
+ if (transitions == null) {
+ setPhoto(photo);
+ invalidate();
+ } else if (getAnimation() != null) {
+ // Clear old running transitions.
+ clearTransitionAnimations();
+ setPhoto(photo);
+ invalidate();
+ } else {
+ // TODO: Use AnimationSet to chain two animations.
+ transitions.first().setAnimationListener(new Animation.AnimationListener() {
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(final Animation animation) {
+ post(new Runnable() {
+
+ @Override
+ public void run() {
+ if ((transitions != null) && (animation == transitions.first())) {
+ startAnimation(transitions.second());
+ }
+ }
+ });
+ }
+ });
+ transitions.second().setAnimationListener(new Animation.AnimationListener() {
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ setPhoto(photo);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ post(new Runnable() {
+
+ @Override
+ public void run() {
+ clearTransitionAnimations();
+ }
+ });
+ }
+ });
+ startAnimation(transitions.first());
+ }
+ }
+
+ private void setPhoto(Photo photo) {
+ if (this.photo != null) {
+ this.photo.clear();
+ this.photo = null;
+ }
+ this.photo = photo;
+
+ // Scale-down (if necessary) and center the photo for display.
+ displayMatrix.reset();
+ RectF bounds = getPhotoBounds();
+ float scale = RectUtils.getDisplayScale(bounds, this);
+ displayMatrix.setScale(scale, scale);
+ displayMatrix.mapRect(bounds);
+ RectUtils.postCenterMatrix(bounds, this, displayMatrix);
+ }
+
+ private void clearTransitionAnimations() {
+ if (transitions != null) {
+ transitions.first().setAnimationListener(null);
+ transitions.second().setAnimationListener(null);
+ transitions = null;
+ clearAnimation();
+ }
+ }
+
+ /**
+ * Sets transition animations in the next update() to transit out the current bitmap and
+ * transit in the new replacing one. Transition animations will be cleared once done.
+ */
+ public void setTransitionAnimations(AnimationPair transitions) {
+ clearTransitionAnimations();
+ this.transitions = transitions;
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/RectUtils.java b/samples/PhotoEditor/src/com/android/photoeditor/RectUtils.java
new file mode 100644
index 0000000..e3945ca
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/RectUtils.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.photoeditor;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.view.View;
+
+/**
+ * Utils for rectangles/bounds related calculations.
+ */
+public class RectUtils {
+
+ private static final float MATH_PI = (float) Math.PI;
+ private static final float DEGREES_TO_RADIAN = MATH_PI / 180.0f;
+
+ /**
+ * Gets straighten matrix for the given bounds and degrees.
+ */
+ public static void getStraightenMatrix(RectF bounds, float degrees, Matrix matrix) {
+ matrix.reset();
+ if ((degrees != 0) && !bounds.isEmpty()) {
+ float w = bounds.width() / 2;
+ float h = bounds.height() / 2;
+ float adjustAngle;
+ if ((degrees < 0 && w > h) || (degrees > 0 && w <= h)) {
+ // The top left point is the boundary.
+ adjustAngle = (float) Math.atan(h / -w) + MATH_PI + degrees * DEGREES_TO_RADIAN;
+ } else {
+ // The top right point is the boundary.
+ adjustAngle = (float) Math.atan(h / w) - MATH_PI + degrees * DEGREES_TO_RADIAN;
+ }
+ float radius = (float) Math.hypot(w, h);
+ float scaleX = (float) Math.abs(radius * Math.cos(adjustAngle)) / w;
+ float scaleY = (float) Math.abs(radius * Math.sin(adjustAngle)) / h;
+ float scale = Math.max(scaleX, scaleY);
+
+ postRotateMatrix(degrees, new RectF(bounds), matrix);
+ matrix.postScale(scale, scale);
+ }
+ }
+
+ /**
+ * Post rotates the matrix and bounds for the given bounds and degrees.
+ */
+ public static void postRotateMatrix(float degrees, RectF bounds, Matrix matrix) {
+ matrix.postRotate(degrees);
+ matrix.mapRect(bounds);
+ matrix.postTranslate(-bounds.left, -bounds.top);
+ }
+
+ /**
+ * Post translates the matrix to center the given bounds inside the view.
+ */
+ public static void postCenterMatrix(RectF contentBounds, View view, Matrix matrix) {
+ matrix.postTranslate((view.getWidth() - contentBounds.width()) / 2,
+ (view.getHeight() - contentBounds.height()) / 2);
+ }
+
+ /**
+ * Gets the proper scale value that scales down the content and keeps its aspect ratio to
+ * display inside the view.
+ */
+ public static float getDisplayScale(RectF contentBounds, View view) {
+ if (contentBounds.isEmpty()) {
+ return 1;
+ }
+
+ float scale = Math.min(view.getWidth() / contentBounds.width(),
+ view.getHeight() / contentBounds.height());
+ // Avoid scaling up the content.
+ return Math.min(scale, 1);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/SaveCopyTask.java b/samples/PhotoEditor/src/com/android/photoeditor/SaveCopyTask.java
new file mode 100644
index 0000000..3706600
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/SaveCopyTask.java
@@ -0,0 +1,142 @@
+/*
+ * 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.photoeditor;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap.CompressFormat;
+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.widget.Toast;
+
+import java.io.File;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+
+/**
+ * Asynchronous task for loading source photo in target dimensions and saving edits as a new copy.
+ */
+public class SaveCopyTask extends AsyncTask<Photo, 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 saveFileName;
+
+ public SaveCopyTask(Context context, Uri sourceUri, Callback callback) {
+ this.context = context;
+ this.sourceUri = sourceUri;
+ this.callback = callback;
+
+ saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(
+ new Date(System.currentTimeMillis()));
+ }
+
+ /**
+ * The task should be executed with one given photo to be saved.
+ */
+ @Override
+ protected Uri doInBackground(Photo... params) {
+ // TODO: Support larger dimensions for photo saving.
+ if (params[0] == null) {
+ return null;
+ }
+ Photo photo = params[0];
+ File file = save(photo);
+ Uri uri = (file != null) ? insertContent(file) : null;
+ photo.clear();
+ return uri;
+ }
+
+ @Override
+ protected void onPostExecute(Uri result) {
+ String message = (result == null) ? context.getString(R.string.saving_failure)
+ : context.getString(R.string.photo_saved, saveFileName);
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+
+ callback.onComplete(result);
+ }
+
+ private File save(Photo photo) {
+ String directory = Environment.getExternalStorageDirectory().toString() + "/"
+ + context.getString(R.string.edited_photo_bucket_name);
+
+ return new BitmapUtils(context).saveBitmap(
+ photo.bitmap(), directory, saveFileName, 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);
+ if ((cursor != null) && cursor.moveToNext()) {
+ dateTaken = cursor.getLong(INDEX_DATE_TAKEN);
+ latitude = cursor.getDouble(INDEX_LATITUDE);
+ longitude = cursor.getDouble(INDEX_LONGITUDE);
+ }
+
+ 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/samples/PhotoEditor/src/com/android/photoeditor/SpinnerProgressDialog.java b/samples/PhotoEditor/src/com/android/photoeditor/SpinnerProgressDialog.java
new file mode 100644
index 0000000..4e29298
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/SpinnerProgressDialog.java
@@ -0,0 +1,69 @@
+/*
+ * 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.photoeditor;
+
+import android.app.Dialog;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.ProgressBar;
+
+/**
+ * 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.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.onInterceptTouchEvent(event);
+ }
+
+ private void enableTools(boolean enabled) {
+ for (int i = 0; i < tools.getChildCount(); i++) {
+ tools.getChildAt(i).setEnabled(enabled);
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/Toolbar.java b/samples/PhotoEditor/src/com/android/photoeditor/Toolbar.java
new file mode 100644
index 0000000..106cf75
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/Toolbar.java
@@ -0,0 +1,206 @@
+/*
+ * 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.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.photoeditor.animation.FadeAnimation;
+import com.android.photoeditor.animation.Rotate3DAnimation;
+import com.android.photoeditor.animation.Rotate3DAnimation.Rotate;
+
+/**
+ * Toolbar that contains all tools and handles all operations for editing photo.
+ */
+public class Toolbar extends RelativeLayout {
+
+ private static final int UNDO_REDO_ANIMATION_DURATION = 100;
+ private static final int QUICKVIEW_ANIMATION_DURATION = 150;
+
+ private final FilterStack filterStack = new FilterStack();
+ private ToolbarLayoutHandler layoutHandler;
+ private ToolbarIdleHandler idleHandler;
+ private EffectsBar effectsBar;
+ private ActionBar actionBar;
+ private PhotoView photoView;
+ private SpinnerProgressDialog progressDialog;
+ private Uri sourceUri;
+
+ public Toolbar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ idleHandler.killIdle();
+ return false;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ layoutHandler.extraLayout(l, t, r, b);
+ }
+
+ public void initialize() {
+ photoView = (PhotoView) findViewById(R.id.photo_view);
+ initializeEffectsBar();
+ initializeActionBar();
+ layoutHandler = new ToolbarLayoutHandler(this);
+ idleHandler = new ToolbarIdleHandler(this);
+ idleHandler.killIdle();
+ }
+
+ private void initializeEffectsBar() {
+ effectsBar = (EffectsBar) findViewById(R.id.effects_bar);
+ effectsBar.initialize(filterStack, photoView, this);
+ }
+
+ private void initializeActionBar() {
+ final PhotoOutputCallback callback = new PhotoOutputCallback() {
+
+ @Override
+ public void onReady(Photo photo) {
+ photoView.setTransitionAnimations(
+ FadeAnimation.getFadeOutInAnimations(UNDO_REDO_ANIMATION_DURATION));
+ photoView.update(photo);
+ progressDialog.dismiss();
+ }
+ };
+
+ actionBar = (ActionBar) findViewById(R.id.action_bar);
+ actionBar.initialize(new ActionBar.ActionBarListener() {
+
+ @Override
+ public void onUndo() {
+ effectsBar.effectsOff(new Runnable() {
+
+ @Override
+ public void run() {
+ progressDialog = SpinnerProgressDialog.show(Toolbar.this);
+ filterStack.undo(callback);
+ }
+ });
+ }
+
+ @Override
+ public void onRedo() {
+ effectsBar.effectsOff(new Runnable() {
+
+ @Override
+ public void run() {
+ progressDialog = SpinnerProgressDialog.show(Toolbar.this);
+ filterStack.redo(callback);
+ }
+ });
+ }
+
+ @Override
+ public void onQuickview(final boolean on) {
+ final PhotoOutputCallback callback = new PhotoOutputCallback() {
+
+ @Override
+ public void onReady(Photo photo) {
+ photoView.setTransitionAnimations(Rotate3DAnimation.getFlipAnimations(
+ on ? Rotate.RIGHT : Rotate.LEFT, QUICKVIEW_ANIMATION_DURATION));
+ photoView.update(photo);
+ }
+ };
+
+ if (on) {
+ effectsBar.effectsOff(new Runnable() {
+
+ @Override
+ public void run() {
+ effectsBar.setVisibility(INVISIBLE);
+ filterStack.getSourceCopy(callback);
+ }
+ });
+ } else {
+ effectsBar.setVisibility(VISIBLE);
+ filterStack.getResultCopy(callback);
+ }
+ }
+
+ @Override
+ public void onSave() {
+ effectsBar.effectsOff(new Runnable() {
+
+ @Override
+ public void run() {
+ savePhoto(null);
+ }
+ });
+ }
+ });
+ }
+
+ public void openPhoto(Uri uri) {
+ sourceUri = uri;
+ filterStack.setStackListener(actionBar);
+
+ // clearPhotoSource() should be called before loading a new source photo to avoid OOM.
+ progressDialog = SpinnerProgressDialog.show(this);
+ filterStack.clearPhotoSource();
+ new LoadScreennailTask(getContext(), new LoadScreennailTask.Callback() {
+
+ @Override
+ public void onComplete(Bitmap bitmap) {
+ filterStack.setPhotoSource(Photo.create(bitmap));
+ filterStack.getResultCopy(new PhotoOutputCallback() {
+
+ @Override
+ public void onReady(Photo photo) {
+ photoView.update(photo);
+ progressDialog.dismiss();
+ effectsBar.setEnabled(photo != null);
+ }
+ });
+ }
+ }).execute(sourceUri);
+ }
+
+ /**
+ * Saves photo and executes runnable (if provided) after saving done.
+ */
+ public void savePhoto(final Runnable runnable) {
+ progressDialog = SpinnerProgressDialog.show(this);
+ filterStack.getResultCopy(new PhotoOutputCallback() {
+
+ @Override
+ public void onReady(Photo photo) {
+ 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(photo);
+ }
+ });
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/ToolbarIdleHandler.java b/samples/PhotoEditor/src/com/android/photoeditor/ToolbarIdleHandler.java
new file mode 100644
index 0000000..07b8f50
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/ToolbarIdleHandler.java
@@ -0,0 +1,102 @@
+/*
+ * 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.photoeditor;
+
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handler that controls idle/awake behaviors of toolbar's child views.
+ */
+class ToolbarIdleHandler {
+
+ private static final String IDLE_VIEW_TAG = "fadeOnIdle";
+ 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;
+
+ /**
+ * Constructor should only be invoked after toolbar has done inflation and added all its child
+ * views; then its child views could be found by findViewById calls.
+ */
+ public ToolbarIdleHandler(ViewGroup toolbar) {
+ mainHandler = new Handler() {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MAKE_IDLE:
+ if (!idle) {
+ idle = true;
+ for (View view : childViews) {
+ makeIdle(view);
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ fadeIn = AnimationUtils.loadAnimation(toolbar.getContext(), R.anim.fade_in);
+ fadeOut = AnimationUtils.loadAnimation(toolbar.getContext(), R.anim.fade_out);
+
+ for (int i = 0; i < toolbar.getChildCount(); i++) {
+ View view = toolbar.getChildAt(i);
+ String tag = (String) view.getTag();
+ if ((tag != null) && tag.equals(IDLE_VIEW_TAG)) {
+ childViews.add(view);
+ }
+ }
+ // Alpha animations don't work well on scroll-view; apply them on container linear-layout.
+ childViews.add(toolbar.findViewById(R.id.effects_container));
+ }
+
+ public void killIdle() {
+ mainHandler.removeMessages(MAKE_IDLE);
+ if (idle) {
+ idle = false;
+ for (View view : childViews) {
+ makeAwake(view);
+ }
+ }
+ mainHandler.sendEmptyMessageDelayed(MAKE_IDLE, TIMEOUT_IDLE);
+ }
+
+ private void makeAwake(View view) {
+ if (view.getVisibility() == View.VISIBLE) {
+ view.startAnimation(fadeIn);
+ }
+ }
+
+ private void makeIdle(View view) {
+ if (view.getVisibility() == View.VISIBLE) {
+ view.startAnimation(fadeOut);
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/ToolbarLayoutHandler.java b/samples/PhotoEditor/src/com/android/photoeditor/ToolbarLayoutHandler.java
new file mode 100644
index 0000000..93253d7
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/ToolbarLayoutHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.photoeditor;
+
+import android.view.View;
+
+/**
+ * Handler that adjusts layouts of toolbar's child views whose positions need being calculated
+ * according to the current screen dimensions.
+ */
+class ToolbarLayoutHandler {
+
+ private final View tools;
+
+ /**
+ * Constructor should only be invoked after toolbar has done inflation and added all its child
+ * views; then its child views could be found by findViewById calls.
+ */
+ public ToolbarLayoutHandler(View toolbar) {
+ this.tools = toolbar;
+ }
+
+ /**
+ * Layouts child tool views' positions that need being updated when toolbar is being layout.
+ */
+ public void extraLayout(int left, int top, int right, int bottom) {
+ // Wheels need being centered vertically.
+ int height = bottom - top;
+
+ View scaleWheel = tools.findViewById(R.id.scale_wheel);
+ scaleWheel.offsetTopAndBottom((height - scaleWheel.getHeight()) / 2);
+
+ View colorWheel = tools.findViewById(R.id.color_wheel);
+ colorWheel.offsetTopAndBottom((height - colorWheel.getHeight()) / 2);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/AutoFixAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/AutoFixAction.java
new file mode 100644
index 0000000..0c9f174
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/AutoFixAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.AutoFixFilter;
+
+/**
+ * An action handling auto-fix effect.
+ */
+public class AutoFixAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public AutoFixAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final AutoFixFilter filter = new AutoFixFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setScale(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorPath.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorPath.java
new file mode 100644
index 0000000..1aedfb6
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorPath.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+
+/**
+ * Colored path that could be painted on canvas.
+ */
+public class ColorPath {
+
+ private final int color;
+ private final Path path;
+
+ public ColorPath(int color, Path path) {
+ this.color = Color.argb(192, Color.red(color), Color.green(color), Color.blue(color));
+ this.path = path;
+ }
+
+ public Path path() {
+ return path;
+ }
+
+ public void draw(Canvas canvas, Paint paint) {
+ paint.setColor(color);
+ canvas.drawPath(path, paint);
+ }
+
+ /**
+ * Creates a paint to draw color paths.
+ */
+ public static Paint createPaint() {
+ Paint paint = new Paint(Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeJoin(Paint.Join.ROUND);
+ paint.setStrokeCap(Paint.Cap.ROUND);
+ paint.setStrokeWidth(12);
+ return paint;
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorTemperatureAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorTemperatureAction.java
new file mode 100644
index 0000000..db2850f
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorTemperatureAction.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.ColorTemperatureFilter;
+
+/**
+ * An action handling color temperature effect.
+ */
+public class ColorTemperatureAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public ColorTemperatureAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final ColorTemperatureFilter filter = new ColorTemperatureFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setColorTemperature(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorWheel.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorWheel.java
new file mode 100644
index 0000000..7c7e715
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/ColorWheel.java
@@ -0,0 +1,330 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+
+import com.android.photoeditor.R;
+
+/**
+ * Wheel that has a draggable thumb to set and get the predefined color set.
+ */
+class ColorWheel extends View {
+
+ /**
+ * Listens to color changes.
+ */
+ public interface OnColorChangeListener {
+
+ void onColorChanged(int color, boolean fromUser);
+ }
+
+ private static final float MATH_PI = (float) Math.PI;
+ private static final float MATH_HALF_PI = MATH_PI / 2;
+
+ // All angles used in this object are defined between PI and -PI.
+ private static final float ANGLE_SPANNED = MATH_PI * 4 / 3;
+ private static final float ANGLE_BEGIN = ANGLE_SPANNED / 2.0f;
+ private static final float DEGREES_BEGIN = 360 - (float) Math.toDegrees(ANGLE_BEGIN);
+ private static final float STROKE_WIDTH = 3.0f;
+
+ private static final float THUMB_RADIUS_RATIO = 0.363f;
+ private static final float INNER_RADIUS_RATIO = 0.173f;
+
+ private static final int PADDING = 4;
+ private static final int COLOR_METER_THICKNESS = 18;
+
+ private final Drawable thumb;
+ private final Paint fillPaint;
+ private final Paint strokePaint;
+ private final int thumbSize;
+ private final int borderColor;
+ private final int[] colorsDefined;
+ private final float radiantInterval;
+ private Bitmap background;
+ private int thumbRadius;
+ private int innerRadius;
+ private int centerXY;
+ private int colorIndex;
+ private float angle;
+ private boolean dragThumb;
+ private OnColorChangeListener listener;
+
+ public ColorWheel(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources resources = context.getResources();
+ thumbSize = (int) resources.getDimension(R.dimen.wheel_thumb_size);
+
+ // Set the number of total colors and compute the radiant interval between colors.
+ TypedArray colors = resources.obtainTypedArray(R.array.color_picker_wheel_colors);
+ colorsDefined = new int[colors.length()];
+ for (int c = 0; c < colors.length(); c++) {
+ colorsDefined[c] = colors.getColor(c, 0x000000);
+ }
+ colors.recycle();
+
+ radiantInterval = ANGLE_SPANNED / colorsDefined.length;
+
+ thumb = resources.getDrawable(R.drawable.wheel_knot_selector);
+ borderColor = resources.getColor(R.color.color_picker_border_color);
+
+ fillPaint = new Paint();
+ fillPaint.setAntiAlias(true);
+ fillPaint.setStyle(Paint.Style.FILL);
+ strokePaint = new Paint();
+ strokePaint.setAntiAlias(true);
+ strokePaint.setStrokeWidth(STROKE_WIDTH);
+ strokePaint.setStyle(Paint.Style.STROKE);
+ }
+
+ public void setColorIndex(int colorIndex) {
+ if (updateColorIndex(colorIndex, false)) {
+ updateThumbPositionByColorIndex();
+ }
+ }
+
+ public int getColor() {
+ return colorsDefined[colorIndex];
+ }
+
+ public void setOnColorChangeListener(OnColorChangeListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ startAnimation(AnimationUtils.loadAnimation(getContext(),
+ (visibility == VISIBLE) ? R.anim.wheel_show : R.anim.wheel_hide));
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ int wheelSize = Math.min(w, h) - PADDING;
+ thumbRadius = (int) (wheelSize * THUMB_RADIUS_RATIO);
+ innerRadius = (int) (wheelSize * INNER_RADIUS_RATIO);
+
+ // The wheel would be centered at (centerXY, centerXY) and have outer-radius centerXY.
+ centerXY = wheelSize / 2;
+ updateThumbPositionByColorIndex();
+ }
+
+ private Bitmap prepareBackground() {
+ int diameter = centerXY * 2;
+ Bitmap bitmap = Bitmap.createBitmap(diameter, diameter, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ // the colors to be selected.
+ float radiantDegrees = (float) Math.toDegrees(radiantInterval);
+ RectF drawBound = new RectF(0, 0, diameter, diameter);
+ for (int c = 0; c < colorsDefined.length; c++) {
+ fillPaint.setColor(colorsDefined[c]);
+ canvas.drawArc(drawBound, DEGREES_BEGIN + radiantDegrees * c,
+ radiantDegrees, true, fillPaint);
+ }
+
+ // clear the inner area.
+ fillPaint.setColor(Color.BLACK);
+ fillPaint.setAlpha(160);
+ canvas.drawCircle(centerXY, centerXY, centerXY - COLOR_METER_THICKNESS, fillPaint);
+
+ // the border for the inner ball
+ fillPaint.setColor(borderColor);
+ canvas.drawCircle(centerXY, centerXY, innerRadius + STROKE_WIDTH, fillPaint);
+
+ return bitmap;
+ }
+
+ private void drawBackground(Canvas canvas) {
+ if (background == null) {
+ background = prepareBackground();
+ }
+ canvas.drawBitmap(background, 0, 0, fillPaint);
+ }
+
+ private void drawHighlighter(Canvas canvas) {
+ strokePaint.setColor(borderColor);
+ int diameter = centerXY * 2;
+ RectF drawBound = new RectF(0, 0, diameter, diameter);
+ float radiantDegrees = (float) Math.toDegrees(radiantInterval);
+ float startAngle = DEGREES_BEGIN + radiantDegrees * colorIndex;
+ canvas.drawArc(drawBound, startAngle, radiantDegrees, false, strokePaint);
+ drawBound.inset(COLOR_METER_THICKNESS, COLOR_METER_THICKNESS);
+ canvas.drawArc(drawBound, startAngle, radiantDegrees, false, strokePaint);
+
+ float lineAngle = ANGLE_BEGIN - radiantInterval * colorIndex;
+ float cosAngle = (float) Math.cos(lineAngle);
+ float sinAngle = (float) Math.sin(lineAngle);
+ int innerRadius = centerXY - COLOR_METER_THICKNESS;
+ canvas.drawLine(centerXY + centerXY * cosAngle, centerXY - centerXY * sinAngle,
+ centerXY + innerRadius * cosAngle,
+ centerXY - innerRadius * sinAngle, strokePaint);
+
+ lineAngle -= radiantInterval;
+ cosAngle = (float) Math.cos(lineAngle);
+ sinAngle = (float) Math.sin(lineAngle);
+ canvas.drawLine(centerXY + centerXY * cosAngle, centerXY - centerXY * sinAngle,
+ centerXY + innerRadius * cosAngle,
+ centerXY - innerRadius * sinAngle, strokePaint);
+ }
+
+ private void drawInnerCircle(Canvas canvas) {
+ fillPaint.setColor(colorsDefined[colorIndex]);
+ canvas.drawCircle(centerXY, centerXY, innerRadius, fillPaint);
+ }
+
+ private void drawThumb(Canvas canvas) {
+ int thumbX = (int) (thumbRadius * Math.cos(angle) + centerXY);
+ int thumbY = (int) (centerXY - thumbRadius * Math.sin(angle));
+ int halfSize = thumbSize / 2;
+ thumb.setBounds(thumbX - halfSize, thumbY - halfSize, thumbX + halfSize, thumbY + halfSize);
+ thumb.draw(canvas);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ drawBackground(canvas);
+ drawInnerCircle(canvas);
+ drawHighlighter(canvas);
+ drawThumb(canvas);
+ }
+
+ private boolean updateAngle(float x, float y) {
+ float angle;
+ if (x == 0) {
+ if (y >= 0) {
+ angle = MATH_HALF_PI;
+ } else {
+ angle = -MATH_HALF_PI;
+ }
+ } else {
+ angle = (float) Math.atan((double) y / x);
+ }
+
+ if (angle >= 0 && x < 0) {
+ angle = angle - MATH_PI;
+ } else if (angle < 0 && x < 0) {
+ angle = MATH_PI + angle;
+ }
+
+ if (angle > ANGLE_BEGIN || angle <= ANGLE_BEGIN - ANGLE_SPANNED) {
+ return false;
+ }
+
+ this.angle = angle;
+ return true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ super.onTouchEvent(ev);
+
+ if (isEnabled()) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ updateThumbState(
+ isHittingThumbArea(ev.getX() - centerXY, centerXY - ev.getY()));
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ final float x = ev.getX() - centerXY;
+ final float y = centerXY - ev.getY();
+ if (!dragThumb && !updateThumbState(isHittingThumbArea(x, y))) {
+ // The thumb wasn't dragged and isn't being dragged, either.
+ break;
+ }
+
+ if (updateAngle(x, y)) {
+ int index = (int) ((ANGLE_BEGIN - angle) / radiantInterval);
+ if (updateColorIndex(index, true)) {
+ updateThumbPositionByColorIndex();
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ updateThumbState(false);
+ break;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if the user is hitting the correct thumb area.
+ */
+ private boolean isHittingThumbArea(float x, float y) {
+ final float radius = (float) Math.sqrt((x * x) + (y * y));
+ return (radius > innerRadius) && (radius < centerXY);
+ }
+
+
+ private boolean updateColorIndex(int index, boolean fromUser) {
+ if (index < 0 || index >= colorsDefined.length) {
+ return false;
+ }
+ if (colorIndex != index) {
+ colorIndex = index;
+
+ if (listener != null) {
+ listener.onColorChanged(colorsDefined[colorIndex], fromUser);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Set the thumb position according to the selected color.
+ * The thumb will always be placed in the middle of the selected color.
+ */
+ private void updateThumbPositionByColorIndex() {
+ angle = ANGLE_BEGIN - (colorIndex + 0.5f) * radiantInterval;
+ invalidate();
+ }
+
+ private boolean updateThumbState(boolean dragThumb) {
+ if (this.dragThumb == dragThumb) {
+ // The state hasn't been changed; no need for updates.
+ return false;
+ }
+
+ this.dragThumb = dragThumb;
+ thumb.setState(dragThumb ? PRESSED_ENABLED_STATE_SET : ENABLED_STATE_SET);
+ invalidate();
+ return true;
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/CropAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/CropAction.java
new file mode 100644
index 0000000..5e6dd53
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/CropAction.java
@@ -0,0 +1,77 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.R;
+import com.android.photoeditor.filters.CropFilter;
+
+/**
+ * An action handling crop effect.
+ */
+public class CropAction extends FilterAction {
+
+ private static final float DEFAULT_CROP = 0.2f;
+ private CropFilter filter;
+
+ public CropAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools, R.string.crop_tooltip);
+ }
+
+ private RectF mapPhotoBounds(RectF bounds, RectF photoBounds) {
+ return new RectF((bounds.left - photoBounds.left) / photoBounds.width(),
+ (bounds.top - photoBounds.top) / photoBounds.height(),
+ (bounds.right - photoBounds.left) / photoBounds.width(),
+ (bounds.bottom - photoBounds.top) / photoBounds.height());
+ }
+
+ @Override
+ public void onBegin() {
+ filter = new CropFilter();
+
+ final RectF photoBounds = photoView.getPhotoDisplayBounds();
+ cropView.setPhotoBounds(new RectF(photoBounds));
+ cropView.setOnCropChangeListener(new CropView.OnCropChangeListener() {
+
+ @Override
+ public void onCropChanged(RectF bounds, boolean fromUser) {
+ if (fromUser) {
+ filter.setCropBounds(mapPhotoBounds(bounds, photoBounds));
+ notifyFilterChanged(filter, false);
+ }
+ }
+ });
+ RectF cropBounds = new RectF(photoBounds);
+ cropBounds.inset(photoBounds.width() * DEFAULT_CROP, photoBounds.height() * DEFAULT_CROP);
+ cropView.setCropBounds(cropBounds);
+ if (!cropView.fullPhotoCropped()) {
+ filter.setCropBounds(mapPhotoBounds(cropBounds, photoBounds));
+ notifyFilterChanged(filter, false);
+ }
+ cropView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ notifyFilterChanged(filter, true);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/CropView.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/CropView.java
new file mode 100644
index 0000000..6486c33
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/CropView.java
@@ -0,0 +1,330 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.photoeditor.R;
+
+import java.util.Vector;
+
+/**
+ * A view that track touch motions and adjust crop bounds accordingly.
+ */
+class CropView extends View {
+
+ /**
+ * Listener of crop bounds.
+ */
+ public interface OnCropChangeListener {
+
+ void onCropChanged(RectF bounds, boolean fromUser);
+ }
+
+ private static final int TOUCH_AREA_NONE = 0;
+ private static final int TOUCH_AREA_LEFT = 1;
+ private static final int TOUCH_AREA_TOP = 2;
+ private static final int TOUCH_AREA_RIGHT = 4;
+ private static final int TOUCH_AREA_BOTTOM = 8;
+ private static final int TOUCH_AREA_INSIDE = 15;
+ private static final int TOUCH_AREA_OUTSIDE = 16;
+ private static final int TOUCH_AREA_TOP_LEFT = 3;
+ private static final int TOUCH_AREA_TOP_RIGHT = 6;
+ private static final int TOUCH_AREA_BOTTOM_LEFT = 9;
+ private static final int TOUCH_AREA_BOTTOM_RIGHT = 12;
+
+ private static final int BORDER_COLOR = 0xFF008AFF;
+ private static final int OUTER_COLOR = 0xA0000000;
+ private static final int INDICATION_COLOR = 0xFFCC9900;
+ private static final int TOUCH_AREA_SPAN = 20;
+ private static final int TOUCH_AREA_SPAN2 = TOUCH_AREA_SPAN * 2;
+ private static final float BORDER_WIDTH = 2.0f;
+
+ private final Paint outerAreaPaint;
+ private final Paint borderPaint;
+ private final Paint highlightPaint;
+
+ private final Drawable heightIndicator;
+ private final Drawable widthIndicator;
+ private final int indicatorSize;
+
+ private RectF cropBounds;
+ private RectF photoBounds;
+
+ private OnCropChangeListener listener;
+
+ private float lastX;
+ private float lastY;
+ private int currentTouchArea;
+
+ public CropView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources resources = context.getResources();
+ heightIndicator = resources.getDrawable(R.drawable.crop_height_holo);
+ widthIndicator = resources.getDrawable(R.drawable.crop_width_holo);
+ indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
+
+ outerAreaPaint = new Paint();
+ outerAreaPaint.setStyle(Paint.Style.FILL);
+ outerAreaPaint.setColor(OUTER_COLOR);
+
+ borderPaint = new Paint();
+ borderPaint.setStyle(Paint.Style.STROKE);
+ borderPaint.setColor(BORDER_COLOR);
+ borderPaint.setStrokeWidth(BORDER_WIDTH);
+
+ highlightPaint = new Paint();
+ highlightPaint.setStyle(Paint.Style.STROKE);
+ highlightPaint.setColor(INDICATION_COLOR);
+ highlightPaint.setStrokeWidth(BORDER_WIDTH);
+
+ currentTouchArea = TOUCH_AREA_NONE;
+ }
+
+ public void setOnCropChangeListener(OnCropChangeListener listener) {
+ this.listener = listener;
+ }
+
+ private void notifyCropChange(boolean fromUser) {
+ if (listener != null) {
+ listener.onCropChanged(cropBounds, fromUser);
+ }
+ }
+
+ public void setCropBounds(RectF bounds) {
+ bounds.intersect(photoBounds);
+ cropBounds = bounds;
+ if (photoBounds.width() <= TOUCH_AREA_SPAN2) {
+ cropBounds.left = photoBounds.left;
+ cropBounds.right = photoBounds.right;
+ }
+ if (photoBounds.height() <= TOUCH_AREA_SPAN2) {
+ cropBounds.top = photoBounds.top;
+ cropBounds.bottom = photoBounds.bottom;
+ }
+ notifyCropChange(false);
+ invalidate();
+ }
+
+ /**
+ * Sets bounds to crop within.
+ */
+ public void setPhotoBounds(RectF bounds) {
+ photoBounds = bounds;
+ }
+
+ public boolean fullPhotoCropped() {
+ return cropBounds.contains(photoBounds);
+ }
+
+ private int detectTouchArea(float x, float y) {
+ RectF area = new RectF();
+ area.set(cropBounds);
+ area.inset(-TOUCH_AREA_SPAN, -TOUCH_AREA_SPAN);
+ if (!area.contains(x, y)) {
+ return TOUCH_AREA_OUTSIDE;
+ }
+
+ // left
+ area.set(cropBounds.left - TOUCH_AREA_SPAN, cropBounds.top + TOUCH_AREA_SPAN,
+ cropBounds.left + TOUCH_AREA_SPAN, cropBounds.bottom - TOUCH_AREA_SPAN);
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_LEFT;
+ }
+ // right
+ area.offset(cropBounds.width(), 0f);
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_RIGHT;
+ }
+ // top
+ area.set(cropBounds.left + TOUCH_AREA_SPAN, cropBounds.top - TOUCH_AREA_SPAN,
+ cropBounds.right - TOUCH_AREA_SPAN, cropBounds.top + TOUCH_AREA_SPAN);
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_TOP;
+ }
+ // bottom
+ area.offset(0f, cropBounds.height());
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_BOTTOM;
+ }
+ // top left
+ area.set(cropBounds.left - TOUCH_AREA_SPAN, cropBounds.top - TOUCH_AREA_SPAN,
+ cropBounds.left + TOUCH_AREA_SPAN, cropBounds.top + TOUCH_AREA_SPAN);
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_TOP_LEFT;
+ }
+ // top right
+ area.offset(cropBounds.width(), 0f);
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_TOP_RIGHT;
+ }
+ // bottom right
+ area.offset(0f, cropBounds.height());
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_BOTTOM_RIGHT;
+ }
+ // bottom left
+ area.offset(-cropBounds.width(), 0f);
+ if (area.contains(x, y)) {
+ return TOUCH_AREA_BOTTOM_LEFT;
+ }
+ return TOUCH_AREA_INSIDE;
+ }
+
+ private void performMove(float deltaX, float deltaY) {
+ if (currentTouchArea == TOUCH_AREA_INSIDE){ // moving the rect.
+ cropBounds.offset(deltaX, deltaY);
+ if (cropBounds.left < photoBounds.left) {
+ cropBounds.offset(photoBounds.left - cropBounds.left, 0f);
+ } else if (cropBounds.right > photoBounds.right) {
+ cropBounds.offset(photoBounds.right - cropBounds.right, 0f);
+ }
+ if (cropBounds.top < photoBounds.top) {
+ cropBounds.offset(0f, photoBounds.top - cropBounds.top);
+ } else if (cropBounds.bottom > photoBounds.bottom) {
+ cropBounds.offset(0f, photoBounds.bottom - cropBounds.bottom);
+ }
+ } else { // adjusting bounds.
+ if ((currentTouchArea & TOUCH_AREA_LEFT) != 0) {
+ cropBounds.left = Math.min(cropBounds.left + deltaX,
+ cropBounds.right - TOUCH_AREA_SPAN2);
+ }
+ if ((currentTouchArea & TOUCH_AREA_TOP) != 0) {
+ cropBounds.top = Math.min(cropBounds.top + deltaY,
+ cropBounds.bottom - TOUCH_AREA_SPAN2);
+ }
+ if ((currentTouchArea & TOUCH_AREA_RIGHT) != 0) {
+ cropBounds.right = Math.max(cropBounds.right + deltaX,
+ cropBounds.left + TOUCH_AREA_SPAN2);
+ }
+ if ((currentTouchArea & TOUCH_AREA_BOTTOM) != 0) {
+ cropBounds.bottom = Math.max(cropBounds.bottom + deltaY,
+ cropBounds.top + TOUCH_AREA_SPAN2);
+ }
+ cropBounds.intersect(photoBounds);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (!isEnabled()) {
+ return true;
+ }
+ float x = event.getX();
+ float y = event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ currentTouchArea = detectTouchArea(x, y);
+ lastX = x;
+ lastY = y;
+ invalidate();
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ performMove(x - lastX, y - lastY);
+
+ lastX = x;
+ lastY = y;
+ notifyCropChange(true);
+ invalidate();
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ currentTouchArea = TOUCH_AREA_NONE;
+ 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;
+ int right = left + indicatorSize;
+ int bottom = top + indicatorSize;
+ indicator.setBounds(left, top, right, bottom);
+ indicator.draw(canvas);
+ }
+
+ private void drawIndicators(Canvas canvas) {
+ drawIndicator(canvas, heightIndicator, cropBounds.centerX(), cropBounds.top);
+ drawIndicator(canvas, heightIndicator, cropBounds.centerX(), cropBounds.bottom);
+ drawIndicator(canvas, widthIndicator, cropBounds.left, cropBounds.centerY());
+ drawIndicator(canvas, widthIndicator, cropBounds.right, cropBounds.centerY());
+ }
+
+ private void drawTouchHighlights(Canvas canvas) {
+ if ((currentTouchArea & TOUCH_AREA_TOP) != 0) {
+ canvas.drawLine(cropBounds.left, cropBounds.top, cropBounds.right, cropBounds.top,
+ highlightPaint);
+ }
+ if ((currentTouchArea & TOUCH_AREA_BOTTOM) != 0) {
+ canvas.drawLine(cropBounds.left, cropBounds.bottom, cropBounds.right,
+ cropBounds.bottom, highlightPaint);
+ }
+ if ((currentTouchArea & TOUCH_AREA_LEFT) != 0) {
+ canvas.drawLine(cropBounds.left, cropBounds.top, cropBounds.left, cropBounds.bottom,
+ highlightPaint);
+ }
+ if ((currentTouchArea & TOUCH_AREA_RIGHT) != 0) {
+ canvas.drawLine(cropBounds.right, cropBounds.top, cropBounds.right, cropBounds.bottom,
+ highlightPaint);
+ }
+ }
+
+ private void drawBounds(Canvas canvas) {
+ Rect r = new Rect();
+ photoBounds.roundOut(r);
+ Region drawRegion = new Region(r);
+ cropBounds.roundOut(r);
+ drawRegion.op(r, Region.Op.DIFFERENCE);
+ RegionIterator iter = new RegionIterator(drawRegion);
+ while (iter.next(r)) {
+ canvas.drawRect(r, outerAreaPaint);
+ }
+
+ canvas.drawRect(cropBounds, borderPaint);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ drawBounds(canvas);
+ if (currentTouchArea != TOUCH_AREA_NONE) {
+ drawTouchHighlights(canvas);
+ drawIndicators(canvas);
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/CrossProcessAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/CrossProcessAction.java
new file mode 100644
index 0000000..c4565ee
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.CrossProcessFilter;
+
+/**
+ * An action handling cross-process effect.
+ */
+public class CrossProcessAction extends FilterAction {
+
+ public CrossProcessAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new CrossProcessFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/DocumentaryAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/DocumentaryAction.java
new file mode 100644
index 0000000..b8bd9c6
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.DocumentaryFilter;
+
+/**
+ * An action handling the preset "Documentary" effect.
+ */
+public class DocumentaryAction extends FilterAction {
+
+ public DocumentaryAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new DocumentaryFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleAction.java
new file mode 100644
index 0000000..16cc37e
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleAction.java
@@ -0,0 +1,83 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.graphics.Path;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.R;
+import com.android.photoeditor.filters.DoodleFilter;
+
+/**
+ * An action handling doodle effect.
+ */
+public class DoodleAction extends FilterAction {
+
+ private static final int DEFAULT_COLOR_INDEX = 4;
+
+ private DoodleFilter filter;
+
+ public DoodleAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools, R.string.doodle_tooltip);
+ }
+
+ @Override
+ public void onBegin() {
+ filter = new DoodleFilter();
+
+ colorWheel.setOnColorChangeListener(new ColorWheel.OnColorChangeListener() {
+
+ @Override
+ public void onColorChanged(int color, boolean fromUser){
+ if (fromUser) {
+ doodleView.startPath(color);
+ filter.addPath(color);
+ }
+ }
+ });
+ colorWheel.setColorIndex(DEFAULT_COLOR_INDEX);
+ colorWheel.setVisibility(View.VISIBLE);
+
+ // Directly draw on doodle-view instead of waiting for top-filter output callback.
+ doodleView.setOnDoodleChangeListener(new DoodleView.OnDoodleChangeListener() {
+
+ private final Path transformPath = new Path();
+
+ @Override
+ public void onLastPathChanged(Path path) {
+ photoView.mapPhotoPath(path, transformPath);
+ filter.updateLastPath(transformPath);
+ notifyFilterChanged(filter, false);
+ }
+ });
+ doodleView.clear();
+ doodleView.clipBounds(photoView.getPhotoDisplayBounds());
+ doodleView.setVisibility(View.VISIBLE);
+
+ int color = colorWheel.getColor();
+ doodleView.startPath(color);
+
+ filter.addPath(color);
+ }
+
+ @Override
+ public void onEnd() {
+ notifyFilterChanged(filter, true);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleView.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleView.java
new file mode 100644
index 0000000..e975965
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/DoodleView.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.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.Vector;
+
+/**
+ * A view that track touch motions as paths and paint them as doodles.
+ */
+class DoodleView extends View {
+
+ /**
+ * Listener of doodle paths.
+ */
+ public interface OnDoodleChangeListener {
+
+ void onLastPathChanged(Path path);
+ }
+
+ private final Vector<ColorPath> colorPaths = new Vector<ColorPath>();
+ private final Paint paint = ColorPath.createPaint();
+
+ private OnDoodleChangeListener listener;
+ private RectF clipBounds;
+ private float lastX;
+ private float lastY;
+
+ public DoodleView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setOnDoodleChangeListener(OnDoodleChangeListener listener) {
+ this.listener = listener;
+ }
+
+ public void startPath(int color) {
+ // Remove last empty path before adding a new path.
+ if (!colorPaths.isEmpty() && colorPaths.lastElement().path().isEmpty()) {
+ colorPaths.remove(colorPaths.size() - 1);
+ }
+ colorPaths.add(new ColorPath(color, new Path()));
+ colorPaths.lastElement().path().moveTo(lastX, lastY);
+ }
+
+ /**
+ * Clears clip bounds and paths drawn.
+ */
+ public void clear() {
+ colorPaths.clear();
+ clipBounds = null;
+ }
+
+ /**
+ * Clips bounds for paths being drawn.
+ */
+ public void clipBounds(RectF bounds) {
+ clipBounds = bounds;
+ }
+
+ private void pathUpdated(Path path) {
+ invalidate();
+ if (listener != null) {
+ listener.onLastPathChanged(path);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (isEnabled() && !colorPaths.isEmpty()) {
+ Path path = colorPaths.lastElement().path();
+ float x = event.getX();
+ float y = event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ path.moveTo(x, y);
+ pathUpdated(path);
+ lastX = x;
+ lastY = y;
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ path.quadTo(lastX, lastY, (lastX + x) / 2, (lastY + y) / 2);
+ pathUpdated(path);
+ lastX = x;
+ lastY = y;
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ // Line to last position with offset to draw at least dots for single clicks.
+ path.lineTo(lastX + 1, lastY + 1);
+ pathUpdated(path);
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ canvas.save();
+ if (clipBounds != null) {
+ canvas.clipRect(clipBounds);
+ }
+ for (ColorPath colorPath : colorPaths) {
+ colorPath.draw(canvas, paint);
+ }
+ canvas.restore();
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/DuotoneAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/DuotoneAction.java
new file mode 100644
index 0000000..80eaf0f
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.DuotoneFilter;
+
+/**
+ * An action handling duo-tone effect.
+ */
+public class DuotoneAction extends FilterAction {
+
+ private static final int DEFAULT_FIRST_COLOR = 0x004488;
+ private static final int DEFAULT_SECOND_COLOR = 0xffff00;
+
+ public DuotoneAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ // 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);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/FillLightAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/FillLightAction.java
new file mode 100644
index 0000000..60ddcd3
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/FillLightAction.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.FillLightFilter;
+
+/**
+ * An action handling fill-light effect.
+ */
+public class FillLightAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0f;
+
+ public FillLightAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final FillLightFilter filter = new FillLightFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setBacklight(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/FilterAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/FilterAction.java
new file mode 100644
index 0000000..08c9fd2
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/FilterAction.java
@@ -0,0 +1,184 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.Photo;
+import com.android.photoeditor.PhotoOutputCallback;
+import com.android.photoeditor.PhotoView;
+import com.android.photoeditor.R;
+import com.android.photoeditor.SpinnerProgressDialog;
+import com.android.photoeditor.filters.Filter;
+
+/**
+ * An action binding UI controls and filter operation for editing photo.
+ */
+public abstract class FilterAction {
+
+ /**
+ * Listens to when this FilterAction has done editing and should be ended.
+ */
+ public interface FilterActionListener {
+
+ void onDone();
+ }
+
+ protected final FilterStack filterStack;
+ protected final PhotoView photoView;
+ protected final ScaleWheel scaleWheel;
+ protected final ColorWheel colorWheel;
+ protected final DoodleView doodleView;
+ protected final TouchView touchView;
+ protected final RotateView rotateView;
+ protected final CropView cropView;
+ private final ViewGroup tools;
+
+ private Toast tooltip;
+ private boolean pushedFilter;
+ private OutputCallback lastOutputCallback;
+ private FilterActionListener listener;
+
+ public FilterAction(FilterStack filterStack, ViewGroup tools) {
+ this.filterStack = filterStack;
+ this.tools = tools;
+
+ photoView = (PhotoView) tools.findViewById(R.id.photo_view);
+ scaleWheel = (ScaleWheel) tools.findViewById(R.id.scale_wheel);
+ colorWheel = (ColorWheel) tools.findViewById(R.id.color_wheel);
+ doodleView = (DoodleView) tools.findViewById(R.id.doodle_view);
+ touchView = (TouchView) tools.findViewById(R.id.touch_view);
+ rotateView = (RotateView) tools.findViewById(R.id.rotate_view);
+ cropView = (CropView) tools.findViewById(R.id.crop_view);
+ }
+
+ public FilterAction(FilterStack filterStack, ViewGroup tools, int tooltipId) {
+ this(filterStack, tools);
+
+ tooltip = Toast.makeText(tools.getContext(), tooltipId, Toast.LENGTH_SHORT);
+ }
+
+ protected void notifyFilterChanged(Filter filter, boolean output) {
+ if (!pushedFilter && filter.isValid()) {
+ filterStack.pushFilter(filter);
+ pushedFilter = true;
+ }
+ if (pushedFilter && output) {
+ // Notify the stack to output the changed top filter.
+ lastOutputCallback = new OutputCallback();
+ filterStack.topFilterChanged(lastOutputCallback);
+ }
+ }
+
+ public void begin(FilterActionListener listener) {
+ this.listener = listener;
+ if (tooltip != null) {
+ tooltip.show();
+ }
+ onBegin();
+ }
+
+ public void end() {
+ onEnd();
+
+ // Wait till last output callback is done before finishing.
+ if ((lastOutputCallback == null) || lastOutputCallback.done) {
+ finish();
+ } else {
+ final SpinnerProgressDialog progressDialog = SpinnerProgressDialog.show(tools);
+ lastOutputCallback.runnableOnDone = new Runnable() {
+
+ @Override
+ public void run() {
+ progressDialog.dismiss();
+ finish();
+ }
+ };
+ }
+ }
+
+ private void finish() {
+ // Close the tooltip if it's still showing.
+ if ((tooltip != null) && (tooltip.getView().getParent() != null)) {
+ tooltip.cancel();
+ }
+ if (scaleWheel.getVisibility() == View.VISIBLE) {
+ scaleWheel.setOnScaleChangeListener(null);
+ scaleWheel.setVisibility(View.INVISIBLE);
+ }
+ if (colorWheel.getVisibility() == View.VISIBLE) {
+ colorWheel.setOnColorChangeListener(null);
+ colorWheel.setVisibility(View.INVISIBLE);
+ }
+ if (doodleView.getVisibility() == View.VISIBLE) {
+ doodleView.setOnDoodleChangeListener(null);
+ doodleView.setVisibility(View.INVISIBLE);
+ }
+ if (touchView.getVisibility() == View.VISIBLE) {
+ touchView.setSingleTapListener(null);
+ touchView.setSwipeListener(null);
+ touchView.setVisibility(View.INVISIBLE);
+ }
+ if (rotateView.getVisibility() == View.VISIBLE) {
+ rotateView.setOnAngleChangeListener(null);
+ rotateView.setVisibility(View.INVISIBLE);
+ }
+ if (cropView.getVisibility() == View.VISIBLE) {
+ cropView.setOnCropChangeListener(null);
+ cropView.setVisibility(View.INVISIBLE);
+ }
+ photoView.clipPhoto(null);
+ // Notify the listener that this action is done finishing.
+ listener.onDone();
+ listener = null;
+ lastOutputCallback = null;
+ pushedFilter = false;
+ }
+
+ /**
+ * Called when the action is about to begin; subclasses should creates a specific filter and
+ * binds the filter to necessary UI controls here.
+ */
+ protected abstract void onBegin();
+
+ /**
+ * Called when the action is about to end; subclasses could do specific ending operations here.
+ */
+ protected abstract void onEnd();
+
+ /**
+ * Output callback for top filter changes.
+ */
+ private class OutputCallback implements PhotoOutputCallback {
+
+ private boolean done;
+ private Runnable runnableOnDone;
+
+ @Override
+ public void onReady(Photo photo) {
+ photoView.update(photo);
+ done = true;
+
+ if (runnableOnDone != null) {
+ runnableOnDone.run();
+ }
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/FisheyeAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/FisheyeAction.java
new file mode 100644
index 0000000..efd00e6
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/FisheyeAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.FisheyeFilter;
+
+/**
+ * An action handling fisheye effect.
+ */
+public class FisheyeAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public FisheyeAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final FisheyeFilter filter = new FisheyeFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setScale(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/FlipAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/FlipAction.java
new file mode 100644
index 0000000..26402f7
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/FlipAction.java
@@ -0,0 +1,99 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.R;
+import com.android.photoeditor.animation.Rotate3DAnimation;
+import com.android.photoeditor.filters.FlipFilter;
+
+/**
+ * An action handling flip effect.
+ */
+public class FlipAction extends FilterAction {
+
+ private static final int ANIMATION_DURATION = 500;
+
+ private boolean flipHorizontal;
+ private boolean flipVertical;
+
+ public FlipAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools, R.string.flip_tooltip);
+ }
+
+ @Override
+ public void onBegin() {
+ final FlipFilter filter = new FlipFilter();
+
+ touchView.setSwipeListener(new TouchView.SwipeListener() {
+
+ @Override
+ public void onSwipeDown() {
+ setFlipAnimations(Rotate3DAnimation.Rotate.DOWN);
+ flipFilterVertically(filter);
+ }
+
+ @Override
+ public void onSwipeLeft() {
+ setFlipAnimations(Rotate3DAnimation.Rotate.LEFT);
+ flipFilterHorizontally(filter);
+ }
+
+ @Override
+ public void onSwipeRight() {
+ setFlipAnimations(Rotate3DAnimation.Rotate.RIGHT);
+ flipFilterHorizontally(filter);
+ }
+
+ @Override
+ public void onSwipeUp() {
+ setFlipAnimations(Rotate3DAnimation.Rotate.UP);
+ flipFilterVertically(filter);
+ }
+ });
+ touchView.setVisibility(View.VISIBLE);
+
+ flipHorizontal = false;
+ flipVertical = false;
+ setFlipAnimations(Rotate3DAnimation.Rotate.RIGHT);
+ flipFilterHorizontally(filter);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+
+ private void setFlipAnimations(Rotate3DAnimation.Rotate rotate) {
+ photoView.setTransitionAnimations(
+ Rotate3DAnimation.getFlipAnimations(rotate, ANIMATION_DURATION));
+ }
+
+ 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/samples/PhotoEditor/src/com/android/photoeditor/actions/GrainAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/GrainAction.java
new file mode 100644
index 0000000..a841d5d
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/GrainAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.GrainFilter;
+
+/**
+ * An action handling the film-grain effect.
+ */
+public class GrainAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public GrainAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final GrainFilter filter = new GrainFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setScale(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/GrayscaleAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/GrayscaleAction.java
new file mode 100644
index 0000000..aa13f58
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.GrayscaleFilter;
+
+/**
+ * An action handling grayscale effect.
+ */
+public class GrayscaleAction extends FilterAction {
+
+ public GrayscaleAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new GrayscaleFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/HighlightAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/HighlightAction.java
new file mode 100644
index 0000000..a5cc9d9
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/HighlightAction.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.HighlightFilter;
+
+/**
+ * An action handling highlight effect.
+ */
+public class HighlightAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0f;
+
+ public HighlightAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final HighlightFilter filter = new HighlightFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setHighlight(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/LomoishAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/LomoishAction.java
new file mode 100644
index 0000000..0642023
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.LomoishFilter;
+
+/**
+ * An action handling the preset "Lomo-ish" effect.
+ */
+public class LomoishAction extends FilterAction {
+
+ public LomoishAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new LomoishFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/NegativeAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/NegativeAction.java
new file mode 100644
index 0000000..421d5cc
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.NegativeFilter;
+
+/**
+ * An action handling negative effect.
+ */
+public class NegativeAction extends FilterAction {
+
+ public NegativeAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new NegativeFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/PosterizeAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/PosterizeAction.java
new file mode 100644
index 0000000..e1c0528
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.PosterizeFilter;
+
+/**
+ * An action handling the "Posterize" effect.
+ */
+public class PosterizeAction extends FilterAction {
+
+ public PosterizeAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new PosterizeFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/RedEyeAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/RedEyeAction.java
new file mode 100644
index 0000000..9a36adb
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/RedEyeAction.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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.R;
+import com.android.photoeditor.filters.RedEyeFilter;
+
+/**
+ * An action handling red-eye removal.
+ */
+public class RedEyeAction extends FilterAction {
+
+ public RedEyeAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools, R.string.redeye_tooltip);
+ }
+
+ @Override
+ public void onBegin() {
+ final RedEyeFilter filter = new RedEyeFilter();
+
+ touchView.setSingleTapListener(new TouchView.SingleTapListener() {
+
+ @Override
+ public void onSingleTap(float x, float y) {
+ filter.addRedEyePosition(photoView.mapPhotoPoint(x, y));
+ notifyFilterChanged(filter, true);
+ }
+ });
+ touchView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/RotateAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/RotateAction.java
new file mode 100644
index 0000000..a04f9d3
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/RotateAction.java
@@ -0,0 +1,115 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.R;
+import com.android.photoeditor.RectUtils;
+import com.android.photoeditor.filters.RotateFilter;
+
+/**
+ * An action handling rotate effect.
+ */
+public class RotateAction extends FilterAction {
+
+ private static final float DEFAULT_ANGLE = 0.0f;
+ private static final float DEFAULT_ROTATE_SPAN = 360.0f;
+
+ private RotateFilter filter;
+ private float rotateDegrees;
+
+ public RotateAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools, R.string.rotate_tooltip);
+ }
+
+ @Override
+ public void onBegin() {
+ filter = new RotateFilter();
+ rotateDegrees = 0;
+
+ final Matrix matrix = new Matrix();
+ final RectF rotateBounds = new RectF();
+ final RectF photoBounds = photoView.getPhotoBounds();
+
+ // Directly transform photo-view instead of waiting for top-filter output callback.
+ rotateView.setOnAngleChangeListener(new RotateView.OnRotateChangeListener() {
+
+ @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(float degrees) {
+ matrix.reset();
+ rotateBounds.set(photoBounds);
+ RectUtils.postRotateMatrix(degrees, rotateBounds, matrix);
+ float scale = RectUtils.getDisplayScale(rotateBounds, photoView);
+ matrix.postScale(scale, scale);
+ photoView.transformDisplay(matrix);
+ }
+ });
+ rotateView.setGridBounds(null);
+ rotateView.setRotatedAngle(DEFAULT_ANGLE);
+ rotateView.setRotateSpan(DEFAULT_ROTATE_SPAN);
+ rotateView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ // 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/samples/PhotoEditor/src/com/android/photoeditor/actions/RotateView.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/RotateView.java
new file mode 100644
index 0000000..b0cea16
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/RotateView.java
@@ -0,0 +1,222 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * View that shows grids and handles touch-events to adjust angle of rotation.
+ */
+class RotateView extends View {
+
+ /**
+ * 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 final RectF referenceLineBounds = new RectF();
+
+ private OnRotateChangeListener listener;
+ 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) {
+ currentRotatedAngle = -degrees / RADIAN_TO_DEGREE;
+ notifyAngleChange(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;
+ }
+
+ /**
+ * Sets grid bounds to be drawn or null to hide grids right before the view is visible.
+ */
+ public void setGridBounds(RectF bounds) {
+ grids.reset();
+ referenceLine.reset();
+ if (bounds != null) {
+ float delta = bounds.width() / 4.0f;
+ for (float x = bounds.left + delta; x < bounds.right; x += delta) {
+ grids.moveTo(x, bounds.top);
+ grids.lineTo(x, bounds.bottom);
+ }
+ delta = bounds.height() / 4.0f;
+ for (float y = bounds.top + delta; y < bounds.bottom; y += delta) {
+ grids.moveTo(bounds.left, y);
+ grids.lineTo(bounds.right, y);
+ }
+
+ // Make reference line long enough to cross the bounds diagonally after being rotated.
+ referenceLineBounds.set(bounds);
+ float radius = (float) Math.hypot(centerX, centerY);
+ delta = radius - centerX;
+ referenceLine.moveTo(-delta, centerY);
+ referenceLine.lineTo(getWidth() + delta, centerY);
+
+ delta = radius - centerY;
+ referenceLine.moveTo(centerX, -delta);
+ referenceLine.lineTo(centerX, getHeight() + delta);
+ }
+ invalidate();
+ }
+
+ public void setOnAngleChangeListener(OnRotateChangeListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ centerX = w / 2;
+ centerY = h / 2;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (!grids.isEmpty()) {
+ dashStrokePaint.setStrokeWidth(2f);
+ dashStrokePaint.setColor(0x99CCCCCC);
+ canvas.drawPath(grids, dashStrokePaint);
+ }
+
+ if (!referenceLine.isEmpty()) {
+ dashStrokePaint.setStrokeWidth(2f);
+ dashStrokePaint.setColor(0x99FFCC77);
+ canvas.save();
+ canvas.clipRect(referenceLineBounds);
+ canvas.rotate(-currentRotatedAngle * RADIAN_TO_DEGREE, centerX, centerY);
+ 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 {
+ currentRotatedAngle = rotatedAngle;
+ notifyAngleChange(true);
+ invalidate();
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (listener != null) {
+ listener.onStopTrackingTouch();
+ }
+ break;
+ }
+ }
+ return true;
+ }
+
+ private void notifyAngleChange(boolean fromUser) {
+ if (listener != null) {
+ listener.onAngleChanged(-currentRotatedAngle * RADIAN_TO_DEGREE, fromUser);
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/SaturationAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/SaturationAction.java
new file mode 100644
index 0000000..aad4178
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/SaturationAction.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.SaturationFilter;
+
+/**
+ * An action handling saturation effect.
+ */
+public class SaturationAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public SaturationAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final SaturationFilter filter = new SaturationFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setSaturation(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/ScaleWheel.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/ScaleWheel.java
new file mode 100644
index 0000000..fd25c87
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/ScaleWheel.java
@@ -0,0 +1,226 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+
+import com.android.photoeditor.R;
+
+/**
+ * Wheel that has a draggable thumb to set and get the normalized scale value from 0 to 1.
+ */
+class ScaleWheel extends View {
+
+ /**
+ * Listens to scale changes.
+ */
+ public interface OnScaleChangeListener {
+
+ void onProgressChanged(float progress, boolean fromUser);
+ }
+
+ private static final float MATH_PI = (float) Math.PI;
+ private static final float MATH_HALF_PI = MATH_PI / 2;
+
+ // Angles are defined between PI and -PI.
+ private static final float ANGLE_SPANNED = MATH_PI * 4 / 3;
+ private static final float ANGLE_BEGIN = ANGLE_SPANNED / 2.0f;
+
+ private static final float THUMB_RADIUS_RATIO = 0.363f;
+ private static final float INNER_RADIUS_RATIO = 0.24f;
+
+ private final Drawable thumb;
+ private final Drawable background;
+ private final int thumbSize;
+ private final Paint circlePaint;
+ private final int maxProgress;
+ private final float radiantInterval;
+ private int thumbRadius;
+ private int innerRadius;
+ private int centerXY;
+ private float angle;
+ private int progress;
+ private boolean dragThumb;
+ private OnScaleChangeListener listener;
+
+ public ScaleWheel(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources resources = context.getResources();
+ thumbSize = (int) resources.getDimension(R.dimen.wheel_thumb_size);
+
+ // Set the maximum progress and compute the radiant interval between progress values.
+ maxProgress = 100;
+ radiantInterval = ANGLE_SPANNED / maxProgress;
+
+ thumb = resources.getDrawable(R.drawable.wheel_knot_selector);
+ background = resources.getDrawable(R.drawable.scale_wheel_background);
+ background.setAlpha(160);
+
+ circlePaint = new Paint();
+ circlePaint.setAntiAlias(true);
+ circlePaint.setColor(resources.getColor(R.color.scale_wheel_interior_color));
+ }
+
+ public void setProgress(float progress) {
+ if (updateProgress((int) (progress * maxProgress), false)) {
+ updateThumbPositionByProgress();
+ }
+ }
+
+ public void setOnScaleChangeListener(OnScaleChangeListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ startAnimation(AnimationUtils.loadAnimation(getContext(),
+ (visibility == VISIBLE) ? R.anim.wheel_show : R.anim.wheel_hide));
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ int wheelSize = Math.min(w, h);
+ thumbRadius = (int) (wheelSize * THUMB_RADIUS_RATIO);
+ innerRadius = (int) (wheelSize * INNER_RADIUS_RATIO);
+
+ // The wheel would be centered at (centerXY, centerXY) and have outer-radius centerXY.
+ centerXY = wheelSize / 2;
+ updateThumbPositionByProgress();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ background.setBounds(0, 0, getWidth(), getHeight());
+ background.draw(canvas);
+
+ int thumbX = (int) (thumbRadius * Math.cos(angle) + centerXY);
+ int thumbY = (int) (centerXY - thumbRadius * Math.sin(angle));
+ int halfSize = thumbSize / 2;
+ thumb.setBounds(thumbX - halfSize, thumbY - halfSize, thumbX + halfSize, thumbY + halfSize);
+ thumb.draw(canvas);
+
+ float radius = (progress * innerRadius) / maxProgress;
+ canvas.drawCircle(centerXY, centerXY, radius, circlePaint);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ super.onTouchEvent(ev);
+
+ if (isEnabled()) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ updateThumbState(
+ isHittingThumbArea(ev.getX() - centerXY, centerXY - ev.getY()));
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ float x = ev.getX() - centerXY;
+ float y = centerXY - ev.getY();
+ if (!dragThumb && !updateThumbState(isHittingThumbArea(x, y))) {
+ // The thumb wasn't dragged and isn't being dragged, either.
+ break;
+ }
+
+ if (updateAngle(x, y)) {
+ int progress = (int) ((ANGLE_BEGIN - angle) / radiantInterval);
+ if (updateProgress(progress, true)) {
+ invalidate();
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ updateThumbState(false);
+ break;
+ }
+ }
+ return true;
+ }
+
+ private boolean isHittingThumbArea(float x, float y) {
+ double radius = Math.sqrt((x * x) + (y * y));
+ return (radius > innerRadius) && (radius < centerXY);
+ }
+
+ private boolean updateAngle(float x, float y) {
+ 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;
+ }
+
+ if ((angle > ANGLE_BEGIN) || (angle <= ANGLE_BEGIN - ANGLE_SPANNED)) {
+ return false;
+ }
+
+ this.angle = angle;
+ return true;
+ }
+
+ private boolean updateProgress(int progress, boolean fromUser) {
+ if ((this.progress != progress) && (progress >= 0) && (progress <= maxProgress)) {
+ this.progress = progress;
+
+ if (listener != null) {
+ listener.onProgressChanged((float) progress / maxProgress, fromUser);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private void updateThumbPositionByProgress() {
+ angle = ANGLE_BEGIN - progress * radiantInterval;
+ invalidate();
+ }
+
+ private boolean updateThumbState(boolean dragThumb) {
+ if (this.dragThumb == dragThumb) {
+ // The state hasn't been changed; no need for updates.
+ return false;
+ }
+
+ this.dragThumb = dragThumb;
+ thumb.setState(dragThumb ? PRESSED_ENABLED_STATE_SET : ENABLED_STATE_SET);
+ invalidate();
+ return true;
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/SepiaAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/SepiaAction.java
new file mode 100644
index 0000000..26eea2e
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.SepiaFilter;
+
+/**
+ * An action handling sepia effect.
+ */
+public class SepiaAction extends FilterAction {
+
+ public SepiaAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new SepiaFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/ShadowAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/ShadowAction.java
new file mode 100644
index 0000000..b159ba5
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/ShadowAction.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.ShadowFilter;
+
+/**
+ * An action handling shadow effect.
+ */
+public class ShadowAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0f;
+
+ public ShadowAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final ShadowFilter filter = new ShadowFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setShadow(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/SharpenAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/SharpenAction.java
new file mode 100644
index 0000000..1c0542f
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/SharpenAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.SharpenFilter;
+
+/**
+ * An action handling sharpen effect.
+ */
+public class SharpenAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public SharpenAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final SharpenFilter filter = new SharpenFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setSharpen(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+
+ filter.setSharpen(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/SoftFocusAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/SoftFocusAction.java
new file mode 100644
index 0000000..34dc201
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/SoftFocusAction.java
@@ -0,0 +1,57 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.SoftFocusFilter;
+
+/**
+ * An action handling soft focus effect.
+ */
+public class SoftFocusAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0f;
+
+ public SoftFocusAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final SoftFocusFilter filter = new SoftFocusFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setSoftFocus(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/StraightenAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/StraightenAction.java
new file mode 100644
index 0000000..0e84d9b
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/StraightenAction.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.photoeditor.actions;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.R;
+import com.android.photoeditor.RectUtils;
+import com.android.photoeditor.filters.StraightenFilter;
+
+/**
+ * An action handling straighten effect.
+ */
+public class StraightenAction extends FilterAction {
+
+ private static final float DEFAULT_ANGLE = 0.0f;
+ private static final float DEFAULT_ROTATE_SPAN = 60.0f;
+
+ private StraightenFilter filter;
+
+ public StraightenAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools, R.string.straighten_tooltip);
+ }
+
+ @Override
+ public void onBegin() {
+ filter = new StraightenFilter();
+
+ final Matrix matrix = new Matrix();
+ final RectF photoBounds = photoView.getPhotoBounds();
+ final float displayScale = RectUtils.getDisplayScale(photoBounds, photoView);
+
+ RectF displayBounds = photoView.getPhotoDisplayBounds();
+ photoView.clipPhoto(displayBounds);
+
+ // Directly transform photo-view instead of waiting for top-filter output callback.
+ rotateView.setOnAngleChangeListener(new RotateView.OnRotateChangeListener() {
+
+ @Override
+ public void onAngleChanged(float angle, boolean fromUser){
+ if (fromUser) {
+ filter.setAngle(angle);
+ notifyFilterChanged(filter, false);
+ RectUtils.getStraightenMatrix(photoBounds, angle, matrix);
+ matrix.postScale(displayScale, displayScale);
+ photoView.transformDisplay(matrix);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch() {
+ // no-op
+ }
+
+ @Override
+ public void onStopTrackingTouch() {
+ // no-op
+ }
+ });
+ rotateView.setGridBounds(displayBounds);
+ rotateView.setRotatedAngle(DEFAULT_ANGLE);
+ rotateView.setRotateSpan(DEFAULT_ROTATE_SPAN);
+ rotateView.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onEnd() {
+ notifyFilterChanged(filter, true);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/TintAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/TintAction.java
new file mode 100644
index 0000000..23cca2b
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/TintAction.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.TintFilter;
+
+/**
+ * An action handling tint effect.
+ */
+public class TintAction extends FilterAction {
+
+ private static final int DEFAULT_COLOR_INDEX = 13;
+
+ public TintAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final TintFilter filter = new TintFilter();
+
+ colorWheel.setOnColorChangeListener(new ColorWheel.OnColorChangeListener() {
+
+ @Override
+ public void onColorChanged(int color, boolean fromUser){
+ if (fromUser) {
+ filter.setTint(color);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ // Tint photo with the default color.
+ colorWheel.setColorIndex(DEFAULT_COLOR_INDEX);
+ colorWheel.setVisibility(View.VISIBLE);
+
+ filter.setTint(colorWheel.getColor());
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/TouchView.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/TouchView.java
new file mode 100644
index 0000000..5c000fb
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/TouchView.java
@@ -0,0 +1,117 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * A view that detects user gestures and touch motions.
+ */
+class TouchView extends View {
+
+ /**
+ * Listener of swipes.
+ */
+ public interface SwipeListener {
+
+ void onSwipeLeft();
+
+ void onSwipeRight();
+
+ void onSwipeUp();
+
+ void onSwipeDown();
+ }
+
+ /**
+ * Listener of single tap confirmed (click).
+ */
+ public interface SingleTapListener {
+
+ void onSingleTap(float x, float y);
+ }
+
+ 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) {
+ singleTapListener.onSingleTap(e.getX(), e.getY());
+ }
+ 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/samples/PhotoEditor/src/com/android/photoeditor/actions/VignetteAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/VignetteAction.java
new file mode 100644
index 0000000..6b54142
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/VignetteAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.photoeditor.actions;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.VignetteFilter;
+
+/**
+ * An action handling vignette effect.
+ */
+public class VignetteAction extends FilterAction {
+
+ private static final float DEFAULT_SCALE = 0.5f;
+
+ public VignetteAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ final VignetteFilter filter = new VignetteFilter();
+
+ scaleWheel.setOnScaleChangeListener(new ScaleWheel.OnScaleChangeListener() {
+
+ @Override
+ public void onProgressChanged(float progress, boolean fromUser) {
+ if (fromUser) {
+ filter.setScale(progress);
+ notifyFilterChanged(filter, true);
+ }
+ }
+ });
+ scaleWheel.setProgress(DEFAULT_SCALE);
+ scaleWheel.setVisibility(View.VISIBLE);
+
+ filter.setScale(DEFAULT_SCALE);
+ notifyFilterChanged(filter, true);
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/actions/WarmifyAction.java b/samples/PhotoEditor/src/com/android/photoeditor/actions/WarmifyAction.java
new file mode 100644
index 0000000..002e107
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/actions/WarmifyAction.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.photoeditor.actions;
+
+import android.view.ViewGroup;
+
+import com.android.photoeditor.FilterStack;
+import com.android.photoeditor.filters.WarmifyFilter;
+
+/**
+ * An action handling warmify effect.
+ */
+public class WarmifyAction extends FilterAction {
+
+ public WarmifyAction(FilterStack filterStack, ViewGroup tools) {
+ super(filterStack, tools);
+ }
+
+ @Override
+ public void onBegin() {
+ notifyFilterChanged(new WarmifyFilter(), true);
+ end();
+ }
+
+ @Override
+ public void onEnd() {
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/animation/AnimationPair.java b/samples/PhotoEditor/src/com/android/photoeditor/animation/AnimationPair.java
new file mode 100644
index 0000000..153855c
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/animation/AnimationPair.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.photoeditor.animation;
+
+import android.view.animation.Animation;
+
+/**
+ * A pair of animations that will be played sequentially, e.g. fade-out and then fade-in.
+ */
+public class AnimationPair {
+
+ private final Animation first;
+ private final Animation second;
+
+ AnimationPair(Animation first, Animation second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public Animation first() {
+ return first;
+ }
+
+ public Animation second() {
+ return second;
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/animation/FadeAnimation.java b/samples/PhotoEditor/src/com/android/photoeditor/animation/FadeAnimation.java
new file mode 100644
index 0000000..45a7c33
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/animation/FadeAnimation.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.photoeditor.animation;
+
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+
+/**
+ * Fading animation that either fades in or out the view by alpha animations.
+ */
+public class FadeAnimation {
+
+ /**
+ * Gets animations that fade out and then fade in the view.
+ */
+ public static AnimationPair getFadeOutInAnimations(int duration) {
+ Animation fadeOut = new AlphaAnimation(1.0f, 0.5f);
+ Animation fadeIn = new AlphaAnimation(0.5f, 1.0f);
+ fadeOut.setDuration(duration);
+ fadeIn.setDuration(duration);
+
+ return new AnimationPair(fadeOut, fadeIn);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/animation/Rotate3DAnimation.java b/samples/PhotoEditor/src/com/android/photoeditor/animation/Rotate3DAnimation.java
new file mode 100644
index 0000000..01e5a92
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/animation/Rotate3DAnimation.java
@@ -0,0 +1,144 @@
+/*
+ * 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.photoeditor.animation;
+
+import android.graphics.Camera;
+import android.graphics.Matrix;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Transformation;
+
+/**
+ * Rotation animation that rotates a 2D view 90 degrees on either X or Y axis in 3D space. The
+ * rotation is performed around the view center point by its defined rotation direction and position
+ * it will start from or end to. The rotation duration can be specified as well as whether the
+ * rotation should be accelerated or decelerated in time.
+ */
+public class Rotate3DAnimation extends Animation {
+
+ public enum Rotate {
+ UP, DOWN, LEFT, RIGHT
+ }
+
+ private enum Angle {
+ FROM_DEGREES_0, TO_DEGREES_0
+ }
+
+ private static final float DEPTH_Z = 250.0f;
+ private static final float ROTATE_DEGREES = 90f;
+
+ private final Rotate rotate;
+ private final Angle angle;
+ private final int duration;
+ private final boolean accelerate;
+
+ private Camera camera;
+ private float centerX;
+ private float centerY;
+ private float fromDegrees;
+ private float toDegrees;
+
+ /**
+ * Gets animations that flip the view by 3D rotations.
+ */
+ public static AnimationPair getFlipAnimations(Rotate rotate, int duration) {
+ return new AnimationPair(
+ new Rotate3DAnimation(rotate, Angle.FROM_DEGREES_0, duration, true),
+ new Rotate3DAnimation(rotate, Angle.TO_DEGREES_0, duration, false));
+ }
+
+ private Rotate3DAnimation(Rotate rotate, Angle angle, int duration, boolean accelerate) {
+ this.rotate = rotate;
+ this.angle = angle;
+ this.duration = duration;
+ this.accelerate = accelerate;
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ super.setDuration(duration);
+ super.setInterpolator(
+ accelerate ? new AccelerateInterpolator() : new DecelerateInterpolator());
+
+ camera = new Camera();
+ centerX = width / 2.0f;
+ centerY = height / 2.0f;
+
+ if (angle == Angle.FROM_DEGREES_0) {
+ fromDegrees = 0;
+
+ switch (rotate) {
+ case RIGHT:
+ case UP:
+ toDegrees = ROTATE_DEGREES;
+ break;
+
+ case LEFT:
+ case DOWN:
+ toDegrees = -ROTATE_DEGREES;
+ break;
+ }
+ } else if (angle == Angle.TO_DEGREES_0) {
+ toDegrees = 0;
+
+ switch (rotate) {
+ case RIGHT:
+ case UP:
+ fromDegrees = -ROTATE_DEGREES;
+ break;
+
+ case LEFT:
+ case DOWN:
+ fromDegrees = ROTATE_DEGREES;
+ break;
+ }
+ }
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ camera.save();
+
+ if (angle == Angle.FROM_DEGREES_0) {
+ camera.translate(0.0f, 0.0f, DEPTH_Z * interpolatedTime);
+ } else if (angle == Angle.TO_DEGREES_0) {
+ camera.translate(0.0f, 0.0f, DEPTH_Z * (1.0f - interpolatedTime));
+ }
+
+ float degrees = fromDegrees + ((toDegrees - fromDegrees) * interpolatedTime);
+ switch (rotate) {
+ case RIGHT:
+ case LEFT:
+ camera.rotateY(degrees);
+ break;
+
+ case UP:
+ case DOWN:
+ camera.rotateX(degrees);
+ break;
+ }
+
+ final Matrix matrix = t.getMatrix();
+ camera.getMatrix(matrix);
+ camera.restore();
+
+ matrix.preTranslate(-centerX, -centerY);
+ matrix.postTranslate(centerX, centerY);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/AutoFixFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/AutoFixFilter.java
new file mode 100644
index 0000000..027f090
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/AutoFixFilter.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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeHEQ(src.bitmap(), dst.bitmap(), scale);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/ColorTemperatureFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/ColorTemperatureFilter.java
new file mode 100644
index 0000000..4fb00af
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/ColorTemperatureFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Color temperature filter applied to the image.
+ */
+public class ColorTemperatureFilter extends Filter {
+
+ private float colorTemperature;
+
+ /**
+ * Sets the color temperature level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setColorTemperature(float scale) {
+ this.colorTemperature = (scale - 0.5f);
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeColorTemp(src.bitmap(), dst.bitmap(), colorTemperature);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/CropFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/CropFilter.java
new file mode 100644
index 0000000..5795bbb
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/CropFilter.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.photoeditor.filters;
+
+import android.graphics.RectF;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Crop filter applied to the image.
+ */
+public class CropFilter extends Filter {
+
+ private RectF bounds;
+
+ /**
+ * The coordinates used here should range from 0 to 1.
+ */
+ public void setCropBounds(RectF bounds) {
+ this.bounds = bounds;
+ validate();
+ }
+
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeCopy(src.bitmap(), dst.bitmap());
+ dst.crop((int) (src.width() * bounds.left), (int) (src.height() * bounds.top),
+ (int) (src.width() * bounds.width()), (int) (src.height() * bounds.height()));
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/CrossProcessFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/CrossProcessFilter.java
new file mode 100644
index 0000000..943f2a8
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/CrossProcessFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Cross-process filter applied to the image.
+ */
+public class CrossProcessFilter extends Filter {
+
+ public CrossProcessFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeCrossProcess(src.bitmap(), dst.bitmap());
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/DocumentaryFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/DocumentaryFilter.java
new file mode 100644
index 0000000..0a0c097
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/DocumentaryFilter.java
@@ -0,0 +1,36 @@
+/*
+ * 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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Preset "Documentary" applied to the image.
+ */
+public class DocumentaryFilter extends Filter {
+
+ public DocumentaryFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeWhiteBlack(src.bitmap(), dst.bitmap(), 0.5f, 0f);
+ ImageUtils.nativeGrayscale(dst.bitmap(), dst.bitmap(), 1.0f);
+ ImageUtils.nativeVignetting(dst.bitmap(), dst.bitmap(), 0.83f);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/DoodleFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/DoodleFilter.java
new file mode 100644
index 0000000..365a737
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/DoodleFilter.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.photoeditor.filters;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+
+import com.android.photoeditor.actions.ColorPath;
+import com.android.photoeditor.Photo;
+
+import java.util.Vector;
+
+/**
+ * Doodle filter applied to the image.
+ */
+public class DoodleFilter extends Filter {
+
+ private final Vector<ColorPath> doodles = new Vector<ColorPath>();
+ private final Paint paint = ColorPath.createPaint();
+ private final Canvas canvas = new Canvas();
+
+ public void addPath(int color) {
+ // Remove last empty path before adding a new one.
+ if (!doodles.isEmpty() && doodles.lastElement().path().isEmpty()) {
+ doodles.remove(doodles.size() - 1);
+ }
+ doodles.add(new ColorPath(color, new Path()));
+ }
+
+ public synchronized void updateLastPath(Path path) {
+ if (!doodles.isEmpty()) {
+ doodles.lastElement().path().set(path);
+ validate();
+ }
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeCopy(src.bitmap(), dst.bitmap());
+
+ if (!doodles.isEmpty()) {
+ canvas.setBitmap(dst.bitmap());
+ for (ColorPath doodle : doodles) {
+ doodle.draw(canvas, paint);
+ }
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/DuotoneFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/DuotoneFilter.java
new file mode 100644
index 0000000..e56f2d5
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/DuotoneFilter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeDuotone(src.bitmap(), dst.bitmap(), firstColor, secondColor);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/FillLightFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/FillLightFilter.java
new file mode 100644
index 0000000..547f082
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/FillLightFilter.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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeBacklight(src.bitmap(), dst.bitmap(), backlight);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/Filter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/Filter.java
new file mode 100644
index 0000000..11afd51
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/Filter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Image filter for photo editing.
+ */
+public abstract class Filter {
+
+ private boolean isValid;
+
+ protected void validate() {
+ isValid = true;
+ }
+
+ /**
+ * Some filters, e.g. lighting filters, are initially invalid until set up with parameters while
+ * others, e.g. sepia or flip filters, are initially valid without parameters.
+ */
+ public boolean isValid() {
+ return isValid;
+ }
+
+ /**
+ * Processes the source bitmap and matrix and output the destination bitmap and matrix.
+ *
+ * @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(Photo src, Photo dst);
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/FisheyeFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/FisheyeFilter.java
new file mode 100644
index 0000000..f60918a
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/FisheyeFilter.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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeFisheye(src.bitmap(), dst.bitmap(), 0.5f, 0.5f, scale);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/FlipFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/FlipFilter.java
new file mode 100644
index 0000000..291afa2
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/FlipFilter.java
@@ -0,0 +1,47 @@
+/*
+ * 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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ if (flipHorizontal && flipVertical) {
+ ImageUtils.nativeFlipBoth(src.bitmap(), dst.bitmap());
+ } else if (flipHorizontal) {
+ ImageUtils.nativeFlipHorizontal(src.bitmap(), dst.bitmap());
+ } else if (flipVertical){
+ ImageUtils.nativeFlipVertical(src.bitmap(), dst.bitmap());
+ } else {
+ ImageUtils.nativeCopy(src.bitmap(), dst.bitmap());
+ }
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/GrainFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/GrainFilter.java
new file mode 100644
index 0000000..8ee81d2
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/GrainFilter.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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeGrain(src.bitmap(), dst.bitmap(), scale);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/GrayscaleFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/GrayscaleFilter.java
new file mode 100644
index 0000000..164aaa0
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/GrayscaleFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Grayscale filter applied to the image.
+ */
+public class GrayscaleFilter extends Filter {
+
+ public GrayscaleFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeGrayscale(src.bitmap(), dst.bitmap(), 1.0f);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/HighlightFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/HighlightFilter.java
new file mode 100644
index 0000000..22efbe1
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/HighlightFilter.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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeWhiteBlack(src.bitmap(), dst.bitmap(), white, 0f);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/ImageUtils.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/ImageUtils.java
new file mode 100644
index 0000000..e37a30f
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/ImageUtils.java
@@ -0,0 +1,60 @@
+/*
+ * 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.photoeditor.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.PointF;
+
+
+/**
+ * Image utilities that calls to JNI methods for image processing.
+ */
+public class ImageUtils {
+
+ static {
+ System.loadLibrary("jni_photoeditor");
+ }
+
+ public static native boolean init(byte[] pgm, int pgmLength);
+
+ public static native void nativeBacklight(Bitmap src, Bitmap dst, float backlight);
+ public static native void nativeBlur(Bitmap src, Bitmap dst, float scale);
+ public static native void nativeColorTemp(Bitmap src, Bitmap dst, float scale);
+ public static native void nativeCopy(Bitmap src, Bitmap dst);
+ public static native void nativeCrossProcess(Bitmap src, Bitmap dst);
+ public static native void nativeDuotone(Bitmap src, Bitmap dst, int firstColor,
+ int secondColor);
+ public static native void nativeFisheye(Bitmap src, Bitmap dst, float focusX,
+ float focusY, float scale);
+ public static native void nativeFlipBoth(Bitmap src, Bitmap dst);
+ public static native void nativeFlipHorizontal(Bitmap src, Bitmap dst);
+ public static native void nativeFlipVertical(Bitmap src, Bitmap dst);
+ public static native void nativeGrain(Bitmap src, Bitmap dst, float scale);
+ public static native void nativeGrayscale(Bitmap src, Bitmap dst, float scale);
+ public static native void nativeHEQ(Bitmap src, Bitmap dst, float scale);
+ public static native void nativeNegative(Bitmap src, Bitmap dst);
+ public static native void nativeQuantize(Bitmap src, Bitmap dst);
+ public static native void nativeRedEye(Bitmap src, Bitmap dst, PointF[] redeyes,
+ float radius, float intensity);
+ public static native void nativeSaturation(Bitmap src, Bitmap dst, float scale);
+ public static native void nativeSepia(Bitmap src, Bitmap dst);
+ public static native void nativeSharpen(Bitmap src, Bitmap dst, float scale);
+ public static native void nativeTint(Bitmap src, Bitmap dst, int tint);
+ public static native void nativeVignetting(Bitmap src, Bitmap dst, float range);
+ public static native void nativeWarmify(Bitmap src, Bitmap dst);
+ public static native void nativeWhiteBlack(Bitmap src, Bitmap dst, float white, float black);
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/LomoishFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/LomoishFilter.java
new file mode 100644
index 0000000..1028081
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/LomoishFilter.java
@@ -0,0 +1,37 @@
+/*
+ * 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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Preset "Lomo-ish" applied to the image.
+ */
+public class LomoishFilter extends Filter {
+
+ public LomoishFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeBlur(src.bitmap(), dst.bitmap(), 0.3f);
+ ImageUtils.nativeCrossProcess(dst.bitmap(), dst.bitmap());
+ ImageUtils.nativeWhiteBlack(dst.bitmap(), dst.bitmap(), 0.8f, 0.15f);
+ ImageUtils.nativeVignetting(dst.bitmap(), dst.bitmap(), 0.73f);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/NegativeFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/NegativeFilter.java
new file mode 100644
index 0000000..fda1862
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/NegativeFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Negative filter applied to the image.
+ */
+public class NegativeFilter extends Filter {
+
+ public NegativeFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeNegative(src.bitmap(), dst.bitmap());
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/PosterizeFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/PosterizeFilter.java
new file mode 100644
index 0000000..4001a90
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/PosterizeFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * "Posterize" filter applied to the image.
+ */
+public class PosterizeFilter extends Filter {
+
+ public PosterizeFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeQuantize(src.bitmap(), dst.bitmap());
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/RedEyeFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/RedEyeFilter.java
new file mode 100644
index 0000000..0965c61
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/RedEyeFilter.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.photoeditor.filters;
+
+import android.graphics.PointF;
+
+import com.android.photoeditor.Photo;
+
+import java.util.Vector;
+
+/**
+ * Red-eye removal filter applied to the image.
+ */
+public class RedEyeFilter extends Filter {
+
+ private static final float RADIUS_RATIO = 0.06f;
+ private static final float MIN_RADIUS = 10.0f;
+ private static final float DEFAULT_RED_INTENSITY = 1.30f; // an empirical value
+
+ private final Vector<PointF> redeyePositions = new Vector<PointF>();
+
+ public void addRedEyePosition(PointF point) {
+ redeyePositions.add(point);
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ float radius = Math.max(MIN_RADIUS, RADIUS_RATIO * Math.min(src.width(), src.height()));
+
+ PointF[] a = new PointF[redeyePositions.size()];
+ ImageUtils.nativeRedEye(src.bitmap(), dst.bitmap(),
+ redeyePositions.toArray(a), radius, DEFAULT_RED_INTENSITY);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/RotateFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/RotateFilter.java
new file mode 100644
index 0000000..5152aca
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/RotateFilter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.photoeditor.filters;
+
+import android.graphics.Matrix;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Rotate filter applied to the image.
+ */
+public class RotateFilter extends Filter {
+
+ private final Matrix matrix = new Matrix();
+
+ public void setAngle(float degrees) {
+ matrix.setRotate(degrees);
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeCopy(src.bitmap(), dst.bitmap());
+ dst.transform(matrix);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/SaturationFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/SaturationFilter.java
new file mode 100644
index 0000000..6590670
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/SaturationFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Saturation filter applied to the image.
+ */
+public class SaturationFilter extends Filter {
+
+ private float saturation;
+
+ /**
+ * Sets the saturation level.
+ *
+ * @param saturation ranges from 0 to 1.
+ */
+ public void setSaturation(float saturation) {
+ this.saturation = (saturation - 0.5f) * 2;
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeSaturation(src.bitmap(), dst.bitmap(), saturation);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/SepiaFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/SepiaFilter.java
new file mode 100644
index 0000000..696f578
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/SepiaFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Sepia filter applied to the image.
+ */
+public class SepiaFilter extends Filter {
+
+ public SepiaFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeSepia(src.bitmap(), dst.bitmap());
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/ShadowFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/ShadowFilter.java
new file mode 100644
index 0000000..5008e6a
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/ShadowFilter.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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeWhiteBlack(src.bitmap(), dst.bitmap(), 1f, black);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/SharpenFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/SharpenFilter.java
new file mode 100644
index 0000000..7d3aa0d
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/SharpenFilter.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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeSharpen(src.bitmap(), dst.bitmap(), scale);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/SoftFocusFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/SoftFocusFilter.java
new file mode 100644
index 0000000..d2e41a5
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/SoftFocusFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Soft focus filter applied to the image.
+ */
+public class SoftFocusFilter extends Filter {
+
+ private float scale;
+
+ /**
+ * Sets the soft focus level.
+ *
+ * @param scale ranges from 0 to 1.
+ */
+ public void setSoftFocus(float scale) {
+ this.scale = scale;
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeBlur(src.bitmap(), dst.bitmap(), scale);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/StraightenFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/StraightenFilter.java
new file mode 100644
index 0000000..8f265bd
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/StraightenFilter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.photoeditor.filters;
+
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+import com.android.photoeditor.Photo;
+import com.android.photoeditor.RectUtils;
+
+/**
+ * Straighten filter applied to the image.
+ */
+public class StraightenFilter extends Filter {
+
+ private final RectF bounds = new RectF();
+ private final Matrix matrix = new Matrix();
+ private final Canvas canvas = new Canvas();
+ private final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
+
+ private float angle;
+
+ public void setAngle(float degrees) {
+ this.angle = degrees;
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ bounds.set(0, 0, src.width(), src.height());
+ RectUtils.getStraightenMatrix(bounds, angle, matrix);
+ matrix.mapRect(bounds);
+ matrix.postTranslate((src.width() - bounds.width()) / 2,
+ (src.height() - bounds.height()) / 2);
+ canvas.setBitmap(dst.bitmap());
+ canvas.drawBitmap(src.bitmap(), matrix, paint);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/TintFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/TintFilter.java
new file mode 100644
index 0000000..ef06fee
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/TintFilter.java
@@ -0,0 +1,37 @@
+/*
+ * 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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeTint(src.bitmap(), dst.bitmap(), tint);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/VignetteFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/VignetteFilter.java
new file mode 100644
index 0000000..3d5b566
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/VignetteFilter.java
@@ -0,0 +1,45 @@
+/*
+ * 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.photoeditor.filters;
+
+import com.android.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(Photo src, Photo dst) {
+ ImageUtils.nativeVignetting(src.bitmap(), dst.bitmap(), range);
+ }
+}
diff --git a/samples/PhotoEditor/src/com/android/photoeditor/filters/WarmifyFilter.java b/samples/PhotoEditor/src/com/android/photoeditor/filters/WarmifyFilter.java
new file mode 100644
index 0000000..dc745a0
--- /dev/null
+++ b/samples/PhotoEditor/src/com/android/photoeditor/filters/WarmifyFilter.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.photoeditor.filters;
+
+import com.android.photoeditor.Photo;
+
+/**
+ * Warmify filter applied to the image.
+ */
+public class WarmifyFilter extends Filter {
+
+ public WarmifyFilter() {
+ validate();
+ }
+
+ @Override
+ public void process(Photo src, Photo dst) {
+ ImageUtils.nativeWarmify(src.bitmap(), dst.bitmap());
+ }
+}