1/*
2 * Copyright (C) 2015 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.volume;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.TimeInterpolator;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.app.Dialog;
25import android.content.DialogInterface;
26import android.content.DialogInterface.OnDismissListener;
27import android.content.DialogInterface.OnShowListener;
28import android.os.Handler;
29import android.util.Log;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.animation.PathInterpolator;
33
34public class VolumeDialogMotion {
35    private static final String TAG = Util.logTag(VolumeDialogMotion.class);
36
37    private static final float ANIMATION_SCALE = 1.0f;
38    private static final int PRE_DISMISS_DELAY = 50;
39
40    private final Dialog mDialog;
41    private final View mDialogView;
42    private final ViewGroup mContents;  // volume rows + zen footer
43    private final View mChevron;
44    private final Handler mHandler = new Handler();
45    private final Callback mCallback;
46
47    private boolean mAnimating;  // show or dismiss animation is running
48    private boolean mShowing;  // show animation is running
49    private boolean mDismissing;  // dismiss animation is running
50    private ValueAnimator mChevronPositionAnimator;
51    private ValueAnimator mContentsPositionAnimator;
52
53    public VolumeDialogMotion(Dialog dialog, View dialogView, ViewGroup contents, View chevron,
54            Callback callback) {
55        mDialog = dialog;
56        mDialogView = dialogView;
57        mContents = contents;
58        mChevron = chevron;
59        mCallback = callback;
60        mDialog.setOnDismissListener(new OnDismissListener() {
61            @Override
62            public void onDismiss(DialogInterface dialog) {
63                if (D.BUG) Log.d(TAG, "mDialog.onDismiss");
64            }
65        });
66        mDialog.setOnShowListener(new OnShowListener() {
67            @Override
68            public void onShow(DialogInterface dialog) {
69                if (D.BUG) Log.d(TAG, "mDialog.onShow");
70                final int h = mDialogView.getHeight();
71                mDialogView.setTranslationY(-h);
72                startShowAnimation();
73            }
74        });
75    }
76
77    public boolean isAnimating() {
78        return mAnimating;
79    }
80
81    private void setShowing(boolean showing) {
82        if (showing == mShowing) return;
83        mShowing = showing;
84        if (D.BUG) Log.d(TAG, "mShowing = " + mShowing);
85        updateAnimating();
86    }
87
88    private void setDismissing(boolean dismissing) {
89        if (dismissing == mDismissing) return;
90        mDismissing = dismissing;
91        if (D.BUG) Log.d(TAG, "mDismissing = " + mDismissing);
92        updateAnimating();
93    }
94
95    private void updateAnimating() {
96        final boolean animating = mShowing || mDismissing;
97        if (animating == mAnimating) return;
98        mAnimating = animating;
99        if (D.BUG) Log.d(TAG, "mAnimating = " + mAnimating);
100        if (mCallback != null) {
101            mCallback.onAnimatingChanged(mAnimating);
102        }
103    }
104
105    public void startShow() {
106        if (D.BUG) Log.d(TAG, "startShow");
107        if (mShowing) return;
108        setShowing(true);
109        if (mDismissing) {
110            mDialogView.animate().cancel();
111            setDismissing(false);
112            startShowAnimation();
113            return;
114        }
115        if (D.BUG) Log.d(TAG, "mDialog.show()");
116        mDialog.show();
117    }
118
119    private int chevronDistance() {
120        return mChevron.getHeight() / 6;
121    }
122
123    private int chevronPosY() {
124        final Object tag = mChevron == null ? null : mChevron.getTag();
125        return tag == null ? 0 : (Integer) tag;
126    }
127
128    private void startShowAnimation() {
129        if (D.BUG) Log.d(TAG, "startShowAnimation");
130        mDialogView.animate()
131                .translationY(0)
132                .setDuration(scaledDuration(300))
133                .setInterpolator(new LogDecelerateInterpolator())
134                .setListener(null)
135                .setUpdateListener(new AnimatorUpdateListener() {
136                    @Override
137                    public void onAnimationUpdate(ValueAnimator animation) {
138                        if (mChevronPositionAnimator == null) return;
139                        // reposition chevron
140                        final float v = (Float) mChevronPositionAnimator.getAnimatedValue();
141                        final int posY = chevronPosY();
142                        mChevron.setTranslationY(posY + v + -mDialogView.getTranslationY());
143                    }})
144                .start();
145
146        mContentsPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0)
147                .setDuration(scaledDuration(400));
148        mContentsPositionAnimator.addListener(new AnimatorListenerAdapter() {
149            private boolean mCancelled;
150
151            @Override
152            public void onAnimationEnd(Animator animation) {
153                if (mCancelled) return;
154                if (D.BUG) Log.d(TAG, "show.onAnimationEnd");
155                setShowing(false);
156            }
157            @Override
158            public void onAnimationCancel(Animator animation) {
159                if (D.BUG) Log.d(TAG, "show.onAnimationCancel");
160                mCancelled = true;
161            }
162        });
163        mContentsPositionAnimator.addUpdateListener(new AnimatorUpdateListener() {
164            @Override
165            public void onAnimationUpdate(ValueAnimator animation) {
166                float v = (Float) animation.getAnimatedValue();
167                mContents.setTranslationY(v + -mDialogView.getTranslationY());
168            }
169        });
170        mContentsPositionAnimator.setInterpolator(new LogDecelerateInterpolator());
171        mContentsPositionAnimator.start();
172
173        mContents.setAlpha(0);
174        mContents.animate()
175                .alpha(1)
176                .setDuration(scaledDuration(150))
177                .setInterpolator(new PathInterpolator(0f, 0f, .2f, 1f))
178                .start();
179
180        mChevronPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0)
181                .setDuration(scaledDuration(250));
182        mChevronPositionAnimator.setInterpolator(new PathInterpolator(.4f, 0f, .2f, 1f));
183        mChevronPositionAnimator.start();
184
185        mChevron.setAlpha(0);
186        mChevron.animate()
187                .alpha(1)
188                .setStartDelay(scaledDuration(50))
189                .setDuration(scaledDuration(150))
190                .setInterpolator(new PathInterpolator(.4f, 0f, 1f, 1f))
191                .start();
192    }
193
194    public void startDismiss(final Runnable onComplete) {
195        if (D.BUG) Log.d(TAG, "startDismiss");
196        if (mDismissing) return;
197        setDismissing(true);
198        if (mShowing) {
199            mDialogView.animate().cancel();
200            if (mContentsPositionAnimator != null) {
201                mContentsPositionAnimator.cancel();
202            }
203            mContents.animate().cancel();
204            if (mChevronPositionAnimator != null) {
205                mChevronPositionAnimator.cancel();
206            }
207            mChevron.animate().cancel();
208            setShowing(false);
209        }
210        mDialogView.animate()
211                .translationY(-mDialogView.getHeight())
212                .setDuration(scaledDuration(250))
213                .setInterpolator(new LogAccelerateInterpolator())
214                .setUpdateListener(new AnimatorUpdateListener() {
215                    @Override
216                    public void onAnimationUpdate(ValueAnimator animation) {
217                        mContents.setTranslationY(-mDialogView.getTranslationY());
218                        final int posY = chevronPosY();
219                        mChevron.setTranslationY(posY + -mDialogView.getTranslationY());
220                    }
221                })
222                .setListener(new AnimatorListenerAdapter() {
223                    private boolean mCancelled;
224                    @Override
225                    public void onAnimationEnd(Animator animation) {
226                        if (mCancelled) return;
227                        if (D.BUG) Log.d(TAG, "dismiss.onAnimationEnd");
228                        mHandler.postDelayed(new Runnable() {
229                            @Override
230                            public void run() {
231                                if (D.BUG) Log.d(TAG, "mDialog.dismiss()");
232                                mDialog.dismiss();
233                                onComplete.run();
234                                setDismissing(false);
235                            }
236                        }, PRE_DISMISS_DELAY);
237
238                    }
239                    @Override
240                    public void onAnimationCancel(Animator animation) {
241                        if (D.BUG) Log.d(TAG, "dismiss.onAnimationCancel");
242                        mCancelled = true;
243                    }
244                }).start();
245    }
246
247    private static int scaledDuration(int base) {
248        return (int) (base * ANIMATION_SCALE);
249    }
250
251    private static final class LogDecelerateInterpolator implements TimeInterpolator {
252        private final float mBase;
253        private final float mDrift;
254        private final float mTimeScale;
255        private final float mOutputScale;
256
257        private LogDecelerateInterpolator() {
258            this(400f, 1.4f, 0);
259        }
260
261        private LogDecelerateInterpolator(float base, float timeScale, float drift) {
262            mBase = base;
263            mDrift = drift;
264            mTimeScale = 1f / timeScale;
265
266            mOutputScale = 1f / computeLog(1f);
267        }
268
269        private float computeLog(float t) {
270            return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
271        }
272
273        @Override
274        public float getInterpolation(float t) {
275            return computeLog(t) * mOutputScale;
276        }
277    }
278
279    private static final class LogAccelerateInterpolator implements TimeInterpolator {
280        private final int mBase;
281        private final int mDrift;
282        private final float mLogScale;
283
284        private LogAccelerateInterpolator() {
285            this(100, 0);
286        }
287
288        private LogAccelerateInterpolator(int base, int drift) {
289            mBase = base;
290            mDrift = drift;
291            mLogScale = 1f / computeLog(1, mBase, mDrift);
292        }
293
294        private static float computeLog(float t, int base, int drift) {
295            return (float) -Math.pow(base, -t) + 1 + (drift * t);
296        }
297
298        @Override
299        public float getInterpolation(float t) {
300            return 1 - computeLog(1 - t, mBase, mDrift) * mLogScale;
301        }
302    }
303
304    public interface Callback {
305        void onAnimatingChanged(boolean animating);
306    }
307}
308