| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS 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.os.Bundle; |
| |
| import com.android.gallery3d.photoeditor.filters.Filter; |
| |
| import java.util.ArrayList; |
| 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 static final String APPLIED_STACK_KEY = "applied_stack"; |
| private static final String REDO_STACK_KEY = "redo_stack"; |
| |
| 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 Photo source; |
| private Runnable queuedTopFilterChange; |
| private boolean outputTopFilter; |
| private volatile boolean paused; |
| |
| public FilterStack(PhotoView photoView, StackListener stackListener, Bundle savedState) { |
| this.photoView = photoView; |
| this.stackListener = stackListener; |
| if (savedState != null) { |
| appliedStack.addAll(getFilters(savedState, APPLIED_STACK_KEY)); |
| redoStack.addAll(getFilters(savedState, REDO_STACK_KEY)); |
| outputTopFilter = true; |
| stackListener.onStackChanged(!appliedStack.empty(), !redoStack.empty()); |
| } |
| } |
| |
| private ArrayList<Filter> getFilters(Bundle savedState, String key) { |
| // Infer Filter array-list from the Parcelable array-list by the specified returned type. |
| return savedState.getParcelableArrayList(key); |
| } |
| |
| public void saveStacks(Bundle outState) { |
| outState.putParcelableArrayList(APPLIED_STACK_KEY, new ArrayList<Filter>(appliedStack)); |
| outState.putParcelableArrayList(REDO_STACK_KEY, new ArrayList<Filter>(redoStack)); |
| } |
| |
| 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. |
| for (int i = 0; i < buffers.length; i++) { |
| if (buffers[i] != null) { |
| buffers[i].clear(); |
| buffers[i] = null; |
| } |
| } |
| if (source != null) { |
| buffers[0] = Photo.create(source.width(), source.height()); |
| reallocateBuffer(1); |
| |
| // Source photo will be displayed if there is no filter stacked. |
| Photo photo = source; |
| int size = outputTopFilter ? appliedStack.size() : appliedStack.size() - 1; |
| for (int i = 0; i < size && !paused; i++) { |
| photo = runFilter(i); |
| } |
| // Clear photo-view transformation when the top filter will be outputted. |
| photoView.setPhoto(photo, outputTopFilter); |
| } |
| } |
| |
| private void invalidateTopFilter() { |
| if (!appliedStack.empty()) { |
| outputTopFilter = true; |
| photoView.setPhoto(runFilter(appliedStack.size() - 1), true); |
| } |
| } |
| |
| 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(input, buffers[out]); |
| nativeEglSetFenceAndWait(); |
| 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) { |
| // GL thread calls back to report UI thread the task is done. |
| photoView.post(new Runnable() { |
| |
| @Override |
| public void run() { |
| callback.onDone(); |
| } |
| }); |
| } |
| |
| private void stackChanged() { |
| // GL thread calls back to report UI thread the stack is changed. |
| final boolean canUndo = !appliedStack.empty(); |
| final boolean canRedo = !redoStack.empty(); |
| photoView.post(new Runnable() { |
| |
| @Override |
| public void run() { |
| stackListener.onStackChanged(canUndo, canRedo); |
| } |
| }); |
| } |
| |
| public void getOutputBitmap(final OnDoneBitmapCallback callback) { |
| photoView.queue(new Runnable() { |
| |
| @Override |
| public void run() { |
| int filterIndex = appliedStack.size() - (outputTopFilter ? 1 : 2); |
| Photo photo = (filterIndex < 0) ? source : buffers[getOutBufferIndex(filterIndex)]; |
| 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); |
| } |
| }); |
| } |
| |
| private void pushFilterInternal(Filter filter) { |
| appliedStack.push(filter); |
| outputTopFilter = false; |
| stackChanged(); |
| } |
| |
| public void pushFilter(final Filter filter) { |
| photoView.queue(new Runnable() { |
| |
| @Override |
| public void run() { |
| while (!redoStack.empty()) { |
| redoStack.pop().release(); |
| } |
| pushFilterInternal(filter); |
| } |
| }); |
| } |
| |
| 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()) { |
| pushFilterInternal(redoStack.pop()); |
| 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 the flag to break from lengthy invalidate() in GL thread for not blocking onPause(). |
| paused = true; |
| photoView.flush(); |
| photoView.queueEvent(new Runnable() { |
| |
| @Override |
| public void run() { |
| Filter.releaseContext(); |
| // Textures will be automatically deleted when GL context is lost. |
| photoView.setPhoto(null, false); |
| source = null; |
| for (int i = 0; i < buffers.length; i++) { |
| buffers[i] = null; |
| } |
| } |
| }); |
| photoView.onPause(); |
| } |
| |
| public void onResume() { |
| photoView.onResume(); |
| paused = false; |
| } |
| |
| static { |
| System.loadLibrary("jni_eglfence"); |
| } |
| |
| private native void nativeEglSetFenceAndWait(); |
| } |