diff options
Diffstat (limited to 'samples/PhotoEditor/src/com/android/photoeditor')
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()); + } +} |