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