1 /*
2  * Copyright (C) 2016 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.systemui.statusbar;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.util.ArrayMap;
23 import android.util.ArraySet;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.view.animation.Interpolator;
27 
28 import com.android.systemui.Interpolators;
29 import com.android.systemui.R;
30 import com.android.systemui.statusbar.notification.TransformState;
31 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
32 
33 import java.util.Stack;
34 
35 /**
36  * A view that can be transformed to and from.
37  */
38 public class ViewTransformationHelper implements TransformableView,
39         TransformState.TransformInfo {
40 
41     private static final int TAG_CONTAINS_TRANSFORMED_VIEW = R.id.contains_transformed_view;
42 
43     private ArrayMap<Integer, View> mTransformedViews = new ArrayMap<>();
44     private ArraySet<Integer> mKeysTransformingToSimilar = new ArraySet<>();
45     private ArrayMap<Integer, CustomTransformation> mCustomTransformations = new ArrayMap<>();
46     private ValueAnimator mViewTransformationAnimation;
47 
addTransformedView(int key, View transformedView)48     public void addTransformedView(int key, View transformedView) {
49         mTransformedViews.put(key, transformedView);
50     }
51 
addTransformedView(View transformedView)52     public void addTransformedView(View transformedView) {
53         int key = transformedView.getId();
54         if (key == View.NO_ID) {
55             throw new IllegalArgumentException("View argument does not have a valid id");
56         }
57         addTransformedView(key, transformedView);
58     }
59 
60     /**
61      * Add a view that transforms to a similar sibling, meaning that we should consider any mapping
62      * found treated as the same viewType. This is useful for imageViews, where it's hard to compare
63      * if the source images are the same when they are bitmap based.
64      *
65      * @param key The key how this is added
66      * @param transformedView the view that is added
67      */
addViewTransformingToSimilar(int key, View transformedView)68     public void addViewTransformingToSimilar(int key, View transformedView) {
69         addTransformedView(key, transformedView);
70         mKeysTransformingToSimilar.add(key);
71     }
72 
addViewTransformingToSimilar(View transformedView)73     public void addViewTransformingToSimilar(View transformedView) {
74         int key = transformedView.getId();
75         if (key == View.NO_ID) {
76             throw new IllegalArgumentException("View argument does not have a valid id");
77         }
78         addViewTransformingToSimilar(key, transformedView);
79     }
80 
reset()81     public void reset() {
82         mTransformedViews.clear();
83         mKeysTransformingToSimilar.clear();
84     }
85 
setCustomTransformation(CustomTransformation transformation, int viewType)86     public void setCustomTransformation(CustomTransformation transformation, int viewType) {
87         mCustomTransformations.put(viewType, transformation);
88     }
89 
90     @Override
getCurrentState(int fadingView)91     public TransformState getCurrentState(int fadingView) {
92         View view = mTransformedViews.get(fadingView);
93         if (view != null && view.getVisibility() != View.GONE) {
94             TransformState transformState = TransformState.createFrom(view, this);
95             if (mKeysTransformingToSimilar.contains(fadingView)) {
96                 transformState.setIsSameAsAnyView(true);
97             }
98             return transformState;
99         }
100         return null;
101     }
102 
103     @Override
transformTo(final TransformableView notification, final Runnable endRunnable)104     public void transformTo(final TransformableView notification, final Runnable endRunnable) {
105         if (mViewTransformationAnimation != null) {
106             mViewTransformationAnimation.cancel();
107         }
108         mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
109         mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
110             @Override
111             public void onAnimationUpdate(ValueAnimator animation) {
112                 transformTo(notification, animation.getAnimatedFraction());
113             }
114         });
115         mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
116         mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
117         mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
118             public boolean mCancelled;
119 
120             @Override
121             public void onAnimationEnd(Animator animation) {
122                 if (!mCancelled) {
123                     if (endRunnable != null) {
124                         endRunnable.run();
125                     }
126                     setVisible(false);
127                     mViewTransformationAnimation = null;
128                 } else {
129                     abortTransformations();
130                 }
131             }
132 
133             @Override
134             public void onAnimationCancel(Animator animation) {
135                 mCancelled = true;
136             }
137         });
138         mViewTransformationAnimation.start();
139     }
140 
141     @Override
transformTo(TransformableView notification, float transformationAmount)142     public void transformTo(TransformableView notification, float transformationAmount) {
143         for (Integer viewType : mTransformedViews.keySet()) {
144             TransformState ownState = getCurrentState(viewType);
145             if (ownState != null) {
146                 CustomTransformation customTransformation = mCustomTransformations.get(viewType);
147                 if (customTransformation != null && customTransformation.transformTo(
148                         ownState, notification, transformationAmount)) {
149                     ownState.recycle();
150                     continue;
151                 }
152                 TransformState otherState = notification.getCurrentState(viewType);
153                 if (otherState != null) {
154                     ownState.transformViewTo(otherState, transformationAmount);
155                     otherState.recycle();
156                 } else {
157                     ownState.disappear(transformationAmount, notification);
158                 }
159                 ownState.recycle();
160             }
161         }
162     }
163 
164     @Override
transformFrom(final TransformableView notification)165     public void transformFrom(final TransformableView notification) {
166         if (mViewTransformationAnimation != null) {
167             mViewTransformationAnimation.cancel();
168         }
169         mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
170         mViewTransformationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
171             @Override
172             public void onAnimationUpdate(ValueAnimator animation) {
173                 transformFrom(notification, animation.getAnimatedFraction());
174             }
175         });
176         mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
177             public boolean mCancelled;
178 
179             @Override
180             public void onAnimationEnd(Animator animation) {
181                 if (!mCancelled) {
182                     setVisible(true);
183                 } else {
184                     abortTransformations();
185                 }
186             }
187 
188             @Override
189             public void onAnimationCancel(Animator animation) {
190                 mCancelled = true;
191             }
192         });
193         mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
194         mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
195         mViewTransformationAnimation.start();
196     }
197 
198     @Override
transformFrom(TransformableView notification, float transformationAmount)199     public void transformFrom(TransformableView notification, float transformationAmount) {
200         for (Integer viewType : mTransformedViews.keySet()) {
201             TransformState ownState = getCurrentState(viewType);
202             if (ownState != null) {
203                 CustomTransformation customTransformation = mCustomTransformations.get(viewType);
204                 if (customTransformation != null && customTransformation.transformFrom(
205                         ownState, notification, transformationAmount)) {
206                     ownState.recycle();
207                     continue;
208                 }
209                 TransformState otherState = notification.getCurrentState(viewType);
210                 if (otherState != null) {
211                     ownState.transformViewFrom(otherState, transformationAmount);
212                     otherState.recycle();
213                 } else {
214                     ownState.appear(transformationAmount, notification);
215                 }
216                 ownState.recycle();
217             }
218         }
219     }
220 
221     @Override
setVisible(boolean visible)222     public void setVisible(boolean visible) {
223         if (mViewTransformationAnimation != null) {
224             mViewTransformationAnimation.cancel();
225         }
226         for (Integer viewType : mTransformedViews.keySet()) {
227             TransformState ownState = getCurrentState(viewType);
228             if (ownState != null) {
229                 ownState.setVisible(visible, false /* force */);
230                 ownState.recycle();
231             }
232         }
233     }
234 
abortTransformations()235     private void abortTransformations() {
236         for (Integer viewType : mTransformedViews.keySet()) {
237             TransformState ownState = getCurrentState(viewType);
238             if (ownState != null) {
239                 ownState.abortTransformation();
240                 ownState.recycle();
241             }
242         }
243     }
244 
245     /**
246      * Add the remaining transformation views such that all views are being transformed correctly
247      * @param viewRoot the root below which all elements need to be transformed
248      */
addRemainingTransformTypes(View viewRoot)249     public void addRemainingTransformTypes(View viewRoot) {
250         // lets now tag the right views
251         int numValues = mTransformedViews.size();
252         for (int i = 0; i < numValues; i++) {
253             View view = mTransformedViews.valueAt(i);
254             while (view != viewRoot.getParent()) {
255                 view.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, true);
256                 view = (View) view.getParent();
257             }
258         }
259         Stack<View> stack = new Stack<>();
260         // Add the right views now
261         stack.push(viewRoot);
262         while (!stack.isEmpty()) {
263             View child = stack.pop();
264             Boolean containsView = (Boolean) child.getTag(TAG_CONTAINS_TRANSFORMED_VIEW);
265             if (containsView == null) {
266                 // This one is unhandled, let's add it to our list.
267                 int id = child.getId();
268                 if (id != View.NO_ID) {
269                     // We only fade views with an id
270                     addTransformedView(id, child);
271                     continue;
272                 }
273             }
274             child.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, null);
275             if (child instanceof ViewGroup && !mTransformedViews.containsValue(child)){
276                 ViewGroup group = (ViewGroup) child;
277                 for (int i = 0; i < group.getChildCount(); i++) {
278                     stack.push(group.getChildAt(i));
279                 }
280             }
281         }
282     }
283 
resetTransformedView(View view)284     public void resetTransformedView(View view) {
285         TransformState state = TransformState.createFrom(view, this);
286         state.setVisible(true /* visible */, true /* force */);
287         state.recycle();
288     }
289 
290     /**
291      * @return a set of all views are being transformed.
292      */
getAllTransformingViews()293     public ArraySet<View> getAllTransformingViews() {
294         return new ArraySet<>(mTransformedViews.values());
295     }
296 
297     @Override
isAnimating()298     public boolean isAnimating() {
299         return mViewTransformationAnimation != null && mViewTransformationAnimation.isRunning();
300     }
301 
302     public static abstract class CustomTransformation {
303         /**
304          * Transform a state to the given view
305          * @param ownState the state to transform
306          * @param notification the view to transform to
307          * @param transformationAmount how much transformation should be done
308          * @return whether a custom transformation is performed
309          */
transformTo(TransformState ownState, TransformableView notification, float transformationAmount)310         public abstract boolean transformTo(TransformState ownState,
311                 TransformableView notification,
312                 float transformationAmount);
313 
314         /**
315          * Transform to this state from the given view
316          * @param ownState the state to transform to
317          * @param notification the view to transform from
318          * @param transformationAmount how much transformation should be done
319          * @return whether a custom transformation is performed
320          */
transformFrom(TransformState ownState, TransformableView notification, float transformationAmount)321         public abstract boolean transformFrom(TransformState ownState,
322                 TransformableView notification,
323                 float transformationAmount);
324 
325         /**
326          * Perform a custom initialisation before transforming.
327          *
328          * @param ownState our own state
329          * @param otherState the other state
330          * @return whether a custom initialization is done
331          */
initTransformation(TransformState ownState, TransformState otherState)332         public boolean initTransformation(TransformState ownState,
333                 TransformState otherState) {
334             return false;
335         }
336 
customTransformTarget(TransformState ownState, TransformState otherState)337         public boolean customTransformTarget(TransformState ownState,
338                 TransformState otherState) {
339             return false;
340         }
341 
342         /**
343          * Get a custom interpolator for this animation
344          * @param interpolationType the type of the interpolation, i.e TranslationX / TranslationY
345          * @param isFrom true if this transformation from the other view
346          */
getCustomInterpolator(int interpolationType, boolean isFrom)347         public Interpolator getCustomInterpolator(int interpolationType, boolean isFrom) {
348             return null;
349         }
350     }
351 }
352