summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/photoeditor/FilterStack.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/photoeditor/FilterStack.java')
-rw-r--r--src/com/android/gallery3d/photoeditor/FilterStack.java253
1 files changed, 253 insertions, 0 deletions
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;
+ }
+}