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