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.app.animation.Interpolators;
29 import com.android.systemui.res.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(
110                 animation -> transformTo(notification, animation.getAnimatedFraction()));
111         mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
112         mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
113         mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
114             public boolean mCancelled;
115 
116             @Override
117             public void onAnimationEnd(Animator animation) {
118                 if (!mCancelled) {
119                     if (endRunnable != null) {
120                         endRunnable.run();
121                     }
122                     setVisible(false);
123                     mViewTransformationAnimation = null;
124                 } else {
125                     abortTransformations();
126                 }
127             }
128 
129             @Override
130             public void onAnimationCancel(Animator animation) {
131                 mCancelled = true;
132             }
133         });
134         mViewTransformationAnimation.start();
135     }
136 
137     @Override
transformTo(TransformableView notification, float transformationAmount)138     public void transformTo(TransformableView notification, float transformationAmount) {
139         for (Integer viewType : mTransformedViews.keySet()) {
140             TransformState ownState = getCurrentState(viewType);
141             if (ownState != null) {
142                 CustomTransformation customTransformation = mCustomTransformations.get(viewType);
143                 if (customTransformation != null && customTransformation.transformTo(
144                         ownState, notification, transformationAmount)) {
145                     ownState.recycle();
146                     continue;
147                 }
148                 TransformState otherState = notification.getCurrentState(viewType);
149                 if (otherState != null) {
150                     ownState.transformViewTo(otherState, transformationAmount);
151                     otherState.recycle();
152                 } else {
153                     ownState.disappear(transformationAmount, notification);
154                 }
155                 ownState.recycle();
156             }
157         }
158     }
159 
160     @Override
transformFrom(final TransformableView notification)161     public void transformFrom(final TransformableView notification) {
162         if (mViewTransformationAnimation != null) {
163             mViewTransformationAnimation.cancel();
164         }
165         mViewTransformationAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
166         mViewTransformationAnimation.addUpdateListener(
167                 animation -> transformFrom(notification, animation.getAnimatedFraction()));
168         mViewTransformationAnimation.addListener(new AnimatorListenerAdapter() {
169             public boolean mCancelled;
170 
171             @Override
172             public void onAnimationEnd(Animator animation) {
173                 if (!mCancelled) {
174                     setVisible(true);
175                 } else {
176                     abortTransformations();
177                 }
178             }
179 
180             @Override
181             public void onAnimationCancel(Animator animation) {
182                 mCancelled = true;
183             }
184         });
185         mViewTransformationAnimation.setInterpolator(Interpolators.LINEAR);
186         mViewTransformationAnimation.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
187         mViewTransformationAnimation.start();
188     }
189 
190     @Override
transformFrom(TransformableView notification, float transformationAmount)191     public void transformFrom(TransformableView notification, float transformationAmount) {
192         for (Integer viewType : mTransformedViews.keySet()) {
193             TransformState ownState = getCurrentState(viewType);
194             if (ownState != null) {
195                 CustomTransformation customTransformation = mCustomTransformations.get(viewType);
196                 if (customTransformation != null && customTransformation.transformFrom(
197                         ownState, notification, transformationAmount)) {
198                     ownState.recycle();
199                     continue;
200                 }
201                 TransformState otherState = notification.getCurrentState(viewType);
202                 if (otherState != null) {
203                     ownState.transformViewFrom(otherState, transformationAmount);
204                     otherState.recycle();
205                 } else {
206                     ownState.appear(transformationAmount, notification);
207                 }
208                 ownState.recycle();
209             }
210         }
211     }
212 
213     @Override
setVisible(boolean visible)214     public void setVisible(boolean visible) {
215         if (mViewTransformationAnimation != null) {
216             mViewTransformationAnimation.cancel();
217         }
218         for (Integer viewType : mTransformedViews.keySet()) {
219             TransformState ownState = getCurrentState(viewType);
220             if (ownState != null) {
221                 ownState.setVisible(visible, false /* force */);
222                 ownState.recycle();
223             }
224         }
225     }
226 
abortTransformations()227     private void abortTransformations() {
228         for (Integer viewType : mTransformedViews.keySet()) {
229             TransformState ownState = getCurrentState(viewType);
230             if (ownState != null) {
231                 ownState.abortTransformation();
232                 ownState.recycle();
233             }
234         }
235     }
236 
237     /**
238      * Add the remaining transformation views such that all views are being transformed correctly
239      * @param viewRoot the root below which all elements need to be transformed
240      */
addRemainingTransformTypes(View viewRoot)241     public void addRemainingTransformTypes(View viewRoot) {
242         // lets now tag the right views
243         int numValues = mTransformedViews.size();
244         for (int i = 0; i < numValues; i++) {
245             View view = mTransformedViews.valueAt(i);
246             while (view != viewRoot.getParent()) {
247                 view.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, true);
248                 view = (View) view.getParent();
249             }
250         }
251         Stack<View> stack = new Stack<>();
252         // Add the right views now
253         stack.push(viewRoot);
254         while (!stack.isEmpty()) {
255             View child = stack.pop();
256             Boolean containsView = (Boolean) child.getTag(TAG_CONTAINS_TRANSFORMED_VIEW);
257             if (containsView == null) {
258                 // This one is unhandled, let's add it to our list.
259                 int id = child.getId();
260                 if (id != View.NO_ID) {
261                     // We only fade views with an id
262                     addTransformedView(id, child);
263                     continue;
264                 }
265             }
266             child.setTag(TAG_CONTAINS_TRANSFORMED_VIEW, null);
267             if (child instanceof ViewGroup && !mTransformedViews.containsValue(child)){
268                 ViewGroup group = (ViewGroup) child;
269                 for (int i = 0; i < group.getChildCount(); i++) {
270                     stack.push(group.getChildAt(i));
271                 }
272             }
273         }
274     }
275 
resetTransformedView(View view)276     public void resetTransformedView(View view) {
277         TransformState state = TransformState.createFrom(view, this);
278         state.setVisible(true /* visible */, true /* force */);
279         state.recycle();
280     }
281 
282     /**
283      * @return a set of all views are being transformed.
284      */
getAllTransformingViews()285     public ArraySet<View> getAllTransformingViews() {
286         return new ArraySet<>(mTransformedViews.values());
287     }
288 
289     @Override
isAnimating()290     public boolean isAnimating() {
291         return mViewTransformationAnimation != null && mViewTransformationAnimation.isRunning();
292     }
293 
294     public static abstract class CustomTransformation {
295         /**
296          * Transform a state to the given view
297          * @param ownState the state to transform
298          * @param notification the view to transform to
299          * @param transformationAmount how much transformation should be done
300          * @return whether a custom transformation is performed
301          */
transformTo(TransformState ownState, TransformableView notification, float transformationAmount)302         public abstract boolean transformTo(TransformState ownState,
303                 TransformableView notification,
304                 float transformationAmount);
305 
306         /**
307          * Transform to this state from the given view
308          * @param ownState the state to transform to
309          * @param notification the view to transform from
310          * @param transformationAmount how much transformation should be done
311          * @return whether a custom transformation is performed
312          */
transformFrom(TransformState ownState, TransformableView notification, float transformationAmount)313         public abstract boolean transformFrom(TransformState ownState,
314                 TransformableView notification,
315                 float transformationAmount);
316 
317         /**
318          * Perform a custom initialisation before transforming.
319          *
320          * @param ownState our own state
321          * @param otherState the other state
322          * @return whether a custom initialization is done
323          */
initTransformation(TransformState ownState, TransformState otherState)324         public boolean initTransformation(TransformState ownState,
325                 TransformState otherState) {
326             return false;
327         }
328 
customTransformTarget(TransformState ownState, TransformState otherState)329         public boolean customTransformTarget(TransformState ownState,
330                 TransformState otherState) {
331             return false;
332         }
333 
334         /**
335          * Get a custom interpolator for this animation
336          * @param interpolationType the type of the interpolation, i.e TranslationX / TranslationY
337          * @param isFrom true if this transformation from the other view
338          */
getCustomInterpolator(int interpolationType, boolean isFrom)339         public Interpolator getCustomInterpolator(int interpolationType, boolean isFrom) {
340             return null;
341         }
342     }
343 }
344