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                })
145                .withEndAction(new Runnable() {
146                    @Override
147                    public void run() {
148                        if (mChevronPositionAnimator == null) return;
149                        // reposition chevron
150                        final int posY = chevronPosY();
151                        mChevron.setTranslationY(posY + -mDialogView.getTranslationY());
152                    }
153                })
154                .start();
155
156        mContentsPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0)
157                .setDuration(scaledDuration(400));
158        mContentsPositionAnimator.addListener(new AnimatorListenerAdapter() {
159            private boolean mCancelled;
160
161            @Override
162            public void onAnimationEnd(Animator animation) {
163                if (mCancelled) return;
164                if (D.BUG) Log.d(TAG, "show.onAnimationEnd");
165                setShowing(false);
166            }
167            @Override
168            public void onAnimationCancel(Animator animation) {
169                if (D.BUG) Log.d(TAG, "show.onAnimationCancel");
170                mCancelled = true;
171            }
172        });
173        mContentsPositionAnimator.addUpdateListener(new AnimatorUpdateListener() {
174            @Override
175            public void onAnimationUpdate(ValueAnimator animation) {
176                float v = (Float) animation.getAnimatedValue();
177                mContents.setTranslationY(v + -mDialogView.getTranslationY());
178            }
179        });
180        mContentsPositionAnimator.setInterpolator(new LogDecelerateInterpolator());
181        mContentsPositionAnimator.start();
182
183        mContents.setAlpha(0);
184        mContents.animate()
185                .alpha(1)
186                .setDuration(scaledDuration(150))
187                .setInterpolator(new PathInterpolator(0f, 0f, .2f, 1f))
188                .start();
189
190        mChevronPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0)
191                .setDuration(scaledDuration(250));
192        mChevronPositionAnimator.setInterpolator(new PathInterpolator(.4f, 0f, .2f, 1f));
193        mChevronPositionAnimator.start();
194
195        mChevron.setAlpha(0);
196        mChevron.animate()
197                .alpha(1)
198                .setStartDelay(scaledDuration(50))
199                .setDuration(scaledDuration(150))
200                .setInterpolator(new PathInterpolator(.4f, 0f, 1f, 1f))
201                .start();
202    }
203
204    public void startDismiss(final Runnable onComplete) {
205        if (D.BUG) Log.d(TAG, "startDismiss");
206        if (mDismissing) return;
207        setDismissing(true);
208        if (mShowing) {
209            mDialogView.animate().cancel();
210            if (mContentsPositionAnimator != null) {
211                mContentsPositionAnimator.cancel();
212            }
213            mContents.animate().cancel();
214            if (mChevronPositionAnimator != null) {
215                mChevronPositionAnimator.cancel();
216            }
217            mChevron.animate().cancel();
218            setShowing(false);
219        }
220        mDialogView.animate()
221                .translationY(-mDialogView.getHeight())
222                .setDuration(scaledDuration(250))
223                .setInterpolator(new LogAccelerateInterpolator())
224                .setUpdateListener(new AnimatorUpdateListener() {
225                    @Override
226                    public void onAnimationUpdate(ValueAnimator animation) {
227                        mContents.setTranslationY(-mDialogView.getTranslationY());
228                        final int posY = chevronPosY();
229                        mChevron.setTranslationY(posY + -mDialogView.getTranslationY());
230                    }
231                })
232                .setListener(new AnimatorListenerAdapter() {
233                    private boolean mCancelled;
234                    @Override
235                    public void onAnimationEnd(Animator animation) {
236                        if (mCancelled) return;
237                        if (D.BUG) Log.d(TAG, "dismiss.onAnimationEnd");
238                        mHandler.postDelayed(new Runnable() {
239                            @Override
240                            public void run() {
241                                if (D.BUG) Log.d(TAG, "mDialog.dismiss()");
242                                mDialog.dismiss();
243                                onComplete.run();
244                                setDismissing(false);
245                            }
246                        }, PRE_DISMISS_DELAY);
247
248                    }
249                    @Override
250                    public void onAnimationCancel(Animator animation) {
251                        if (D.BUG) Log.d(TAG, "dismiss.onAnimationCancel");
252                        mCancelled = true;
253                    }
254                }).start();
255    }
256
257    private static int scaledDuration(int base) {
258        return (int) (base * ANIMATION_SCALE);
259    }
260
261    private static final class LogDecelerateInterpolator implements TimeInterpolator {
262        private final float mBase;
263        private final float mDrift;
264        private final float mTimeScale;
265        private final float mOutputScale;
266
267        private LogDecelerateInterpolator() {
268            this(400f, 1.4f, 0);
269        }
270
271        private LogDecelerateInterpolator(float base, float timeScale, float drift) {
272            mBase = base;
273            mDrift = drift;
274            mTimeScale = 1f / timeScale;
275
276            mOutputScale = 1f / computeLog(1f);
277        }
278
279        private float computeLog(float t) {
280            return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
281        }
282
283        @Override
284        public float getInterpolation(float t) {
285            return computeLog(t) * mOutputScale;
286        }
287    }
288
289    private static final class LogAccelerateInterpolator implements TimeInterpolator {
290        private final int mBase;
291        private final int mDrift;
292        private final float mLogScale;
293
294        private LogAccelerateInterpolator() {
295            this(100, 0);
296        }
297
298        private LogAccelerateInterpolator(int base, int drift) {
299            mBase = base;
300            mDrift = drift;
301            mLogScale = 1f / computeLog(1, mBase, mDrift);
302        }
303
304        private static float computeLog(float t, int base, int drift) {
305            return (float) -Math.pow(base, -t) + 1 + (drift * t);
306        }
307
308        @Override
309        public float getInterpolation(float t) {
310            return 1 - computeLog(1 - t, mBase, mDrift) * mLogScale;
311        }
312    }
313
314    public interface Callback {
315        void onAnimatingChanged(boolean animating);
316    }
317}
318