1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.gallery3d.photoeditor; 18 19 import android.graphics.Bitmap; 20 21 import com.android.gallery3d.photoeditor.filters.Filter; 22 23 import java.util.Stack; 24 25 /** 26 * A stack of filters to be applied onto a photo. 27 */ 28 public class FilterStack { 29 30 /** 31 * Listener of stack changes. 32 */ 33 public interface StackListener { 34 35 void onStackChanged(boolean canUndo, boolean canRedo); 36 } 37 38 private final Stack<Filter> appliedStack = new Stack<Filter>(); 39 private final Stack<Filter> redoStack = new Stack<Filter>(); 40 41 // Use two photo buffers as in and out in turns to apply filters in the stack. 42 private final Photo[] buffers = new Photo[2]; 43 private final PhotoView photoView; 44 private final StackListener stackListener; 45 46 private Photo source; 47 private Runnable queuedTopFilterChange; 48 private boolean topFilterOutputted; 49 private volatile boolean paused; 50 51 public FilterStack(PhotoView photoView, StackListener stackListener) { 52 this.photoView = photoView; 53 this.stackListener = stackListener; 54 } 55 56 private void reallocateBuffer(int target) { 57 int other = target ^ 1; 58 buffers[target] = Photo.create(buffers[other].width(), buffers[other].height()); 59 } 60 61 private void invalidate() { 62 // In/out buffers need redrawn by re-applying filters on source photo. 63 for (int i = 0; i < buffers.length; i++) { 64 if (buffers[i] != null) { 65 buffers[i].clear(); 66 buffers[i] = null; 67 } 68 } 69 if (source != null) { 70 buffers[0] = Photo.create(source.width(), source.height()); 71 reallocateBuffer(1); 72 73 // Source photo will be displayed if there is no filter stacked. 74 Photo photo = source; 75 int size = topFilterOutputted ? appliedStack.size() : appliedStack.size() - 1; 76 for (int i = 0; i < size && !paused; i++) { 77 photo = runFilter(i); 78 } 79 photoView.setPhoto(photo, topFilterOutputted); 80 } 81 } 82 83 private void invalidateTopFilter() { 84 if (!appliedStack.empty()) { 85 photoView.setPhoto(runFilter(appliedStack.size() - 1), true); 86 topFilterOutputted = true; 87 } 88 } 89 90 private Photo runFilter(int filterIndex) { 91 int out = getOutBufferIndex(filterIndex); 92 Photo input = (filterIndex > 0) ? buffers[out ^ 1] : source; 93 if ((input != null) && (buffers[out] != null)) { 94 if (!buffers[out].matchDimension(input)) { 95 buffers[out].clear(); 96 reallocateBuffer(out); 97 } 98 appliedStack.get(filterIndex).process(input, buffers[out]); 99 return buffers[out]; 100 } 101 return null; 102 } 103 104 private int getOutBufferIndex(int filterIndex) { 105 // buffers[0] and buffers[1] are swapped in turns as the in/out buffers for 106 // processing stacked filters. For example, the first filter reads buffer[0] and 107 // writes buffer[1]; the second filter then reads buffer[1] and writes buffer[0]. 108 // The returned index should only be used when the applied filter stack isn't empty. 109 return (filterIndex + 1) % 2; 110 } 111 112 private void callbackDone(final OnDoneCallback callback) { 113 // GL thread calls back to report UI thread the task is done. 114 photoView.post(new Runnable() { 115 116 @Override 117 public void run() { 118 callback.onDone(); 119 } 120 }); 121 } 122 123 private void stackChanged() { 124 // GL thread calls back to report UI thread the stack is changed. 125 final boolean canUndo = !appliedStack.empty(); 126 final boolean canRedo = !redoStack.empty(); 127 photoView.post(new Runnable() { 128 129 @Override 130 public void run() { 131 stackListener.onStackChanged(canUndo, canRedo); 132 } 133 }); 134 } 135 136 public void saveBitmap(final OnDoneBitmapCallback callback) { 137 photoView.queue(new Runnable() { 138 139 @Override 140 public void run() { 141 int filterIndex = appliedStack.size() - (topFilterOutputted ? 1 : 2); 142 Photo photo = (filterIndex < 0) ? source : buffers[getOutBufferIndex(filterIndex)]; 143 final Bitmap bitmap = (photo != null) ? photo.save() : null; 144 photoView.post(new Runnable() { 145 146 @Override 147 public void run() { 148 callback.onDone(bitmap); 149 } 150 }); 151 } 152 }); 153 } 154 155 public void setPhotoSource(final Bitmap bitmap, final OnDoneCallback callback) { 156 photoView.queue(new Runnable() { 157 158 @Override 159 public void run() { 160 source = Photo.create(bitmap); 161 invalidate(); 162 callbackDone(callback); 163 } 164 }); 165 } 166 167 private void pushFilterInternal(Filter filter) { 168 appliedStack.push(filter); 169 topFilterOutputted = false; 170 stackChanged(); 171 } 172 173 public void pushFilter(final Filter filter) { 174 photoView.queue(new Runnable() { 175 176 @Override 177 public void run() { 178 while (!redoStack.empty()) { 179 redoStack.pop().release(); 180 } 181 pushFilterInternal(filter); 182 } 183 }); 184 } 185 186 public void undo(final OnDoneCallback callback) { 187 photoView.queue(new Runnable() { 188 189 @Override 190 public void run() { 191 if (!appliedStack.empty()) { 192 redoStack.push(appliedStack.pop()); 193 stackChanged(); 194 invalidate(); 195 } 196 callbackDone(callback); 197 } 198 }); 199 } 200 201 public void redo(final OnDoneCallback callback) { 202 photoView.queue(new Runnable() { 203 204 @Override 205 public void run() { 206 if (!redoStack.empty()) { 207 pushFilterInternal(redoStack.pop()); 208 invalidateTopFilter(); 209 } 210 callbackDone(callback); 211 } 212 }); 213 } 214 215 public void topFilterChanged(final OnDoneCallback callback) { 216 // Remove the outdated top-filter change before queuing a new one. 217 if (queuedTopFilterChange != null) { 218 photoView.remove(queuedTopFilterChange); 219 } 220 queuedTopFilterChange = new Runnable() { 221 222 @Override 223 public void run() { 224 invalidateTopFilter(); 225 callbackDone(callback); 226 } 227 }; 228 photoView.queue(queuedTopFilterChange); 229 } 230 231 public void onPause() { 232 // Flush pending queued operations and release effect-context before GL context is lost. 233 // Use the flag to break from lengthy invalidate() in GL thread for not blocking onPause(). 234 paused = true; 235 photoView.flush(); 236 photoView.queueEvent(new Runnable() { 237 238 @Override 239 public void run() { 240 Filter.releaseContext(); 241 // Textures will be automatically deleted when GL context is lost. 242 photoView.setPhoto(null, false); 243 source = null; 244 for (int i = 0; i < buffers.length; i++) { 245 buffers[i] = null; 246 } 247 } 248 }); 249 photoView.onPause(); 250 } 251 252 public void onResume() { 253 photoView.onResume(); 254 paused = false; 255 } 256 } 257