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.graphics.drawable.Drawable;
29import android.os.Handler;
30import android.util.Log;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.animation.PathInterpolator;
34
35public class VolumeDialogMotion {
36    private static final String TAG = Util.logTag(VolumeDialogMotion.class);
37
38    private static final float ANIMATION_SCALE = 1.0f;
39    private static final int PRE_DISMISS_DELAY = 50;
40
41    private final Dialog mDialog;
42    private final View mDialogView;
43    private final ViewGroup mContents;  // volume rows + zen footer
44    private final View mChevron;
45    private final Handler mHandler = new Handler();
46    private final Callback mCallback;
47
48    private boolean mAnimating;  // show or dismiss animation is running
49    private boolean mShowing;  // show animation is running
50    private boolean mDismissing;  // dismiss animation is running
51    private ValueAnimator mChevronPositionAnimator;
52    private ValueAnimator mContentsPositionAnimator;
53
54    public VolumeDialogMotion(Dialog dialog, View dialogView, ViewGroup contents, View chevron,
55            Callback callback) {
56        mDialog = dialog;
57        mDialogView = dialogView;
58        mContents = contents;
59        mChevron = chevron;
60        mCallback = callback;
61        mDialog.setOnDismissListener(new OnDismissListener() {
62            @Override
63            public void onDismiss(DialogInterface dialog) {
64                if (D.BUG) Log.d(TAG, "mDialog.onDismiss");
65            }
66        });
67        mDialog.setOnShowListener(new OnShowListener() {
68            @Override
69            public void onShow(DialogInterface dialog) {
70                if (D.BUG) Log.d(TAG, "mDialog.onShow");
71                final int h = mDialogView.getHeight();
72                mDialogView.setTranslationY(-h);
73                startShowAnimation();
74            }
75        });
76    }
77
78    public boolean isAnimating() {
79        return mAnimating;
80    }
81
82    private void setShowing(boolean showing) {
83        if (showing == mShowing) return;
84        mShowing = showing;
85        if (D.BUG) Log.d(TAG, "mShowing = " + mShowing);
86        updateAnimating();
87    }
88
89    private void setDismissing(boolean dismissing) {
90        if (dismissing == mDismissing) return;
91        mDismissing = dismissing;
92        if (D.BUG) Log.d(TAG, "mDismissing = " + mDismissing);
93        updateAnimating();
94    }
95
96    private void updateAnimating() {
97        final boolean animating = mShowing || mDismissing;
98        if (animating == mAnimating) return;
99        mAnimating = animating;
100        if (D.BUG) Log.d(TAG, "mAnimating = " + mAnimating);
101        if (mCallback != null) {
102            mCallback.onAnimatingChanged(mAnimating);
103        }
104    }
105
106    public void startShow() {
107        if (D.BUG) Log.d(TAG, "startShow");
108        if (mShowing) return;
109        setShowing(true);
110        if (mDismissing) {
111            mDialogView.animate().cancel();
112            setDismissing(false);
113            startShowAnimation();
114            return;
115        }
116        if (D.BUG) Log.d(TAG, "mDialog.show()");
117        mDialog.show();
118    }
119
120    private int chevronDistance() {
121        return mChevron.getHeight() / 6;
122    }
123
124    private int chevronPosY() {
125        final Object tag = mChevron == null ? null : mChevron.getTag();
126        return tag == null ? 0 : (Integer) tag;
127    }
128
129    private void startShowAnimation() {
130        if (D.BUG) Log.d(TAG, "startShowAnimation");
131        mDialogView.animate()
132                .translationY(0)
133                .setDuration(scaledDuration(300))
134                .setInterpolator(new LogDecelerateInterpolator())
135                .setListener(null)
136                .setUpdateListener(animation -> {
137                    if (mChevronPositionAnimator != null) {
138                        final float v = (Float) mChevronPositionAnimator.getAnimatedValue();
139                        if (mChevronPositionAnimator == null) return;
140                        // reposition chevron
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    public 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        public 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    public static final class LogAccelerateInterpolator implements TimeInterpolator {
290        private final int mBase;
291        private final int mDrift;
292        private final float mLogScale;
293
294        public 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