Home | History | Annotate | Download | only in photoeditor
      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