1 /*
2  * Copyright (C) 2021 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 package com.android.launcher3.taskbar;
17 
18 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
19 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
20 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
21 import static com.android.launcher3.anim.Interpolators.DEACCEL;
22 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
23 
24 import android.animation.Animator;
25 import android.animation.AnimatorListenerAdapter;
26 import android.animation.AnimatorSet;
27 import android.animation.Keyframe;
28 import android.animation.ObjectAnimator;
29 import android.animation.PropertyValuesHolder;
30 import android.animation.TimeInterpolator;
31 import android.content.res.Resources;
32 import android.text.TextUtils;
33 import android.view.View;
34 
35 import com.android.launcher3.R;
36 import com.android.launcher3.icons.BitmapInfo;
37 import com.android.launcher3.model.data.ItemInfo;
38 import com.android.launcher3.uioverrides.PredictedAppIcon;
39 
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.stream.Collectors;
43 
44 /** Handles the Taskbar Education flow. */
45 public class TaskbarEduController {
46 
47     private static final long WAVE_ANIM_DELAY = 250;
48     private static final long WAVE_ANIM_STAGGER = 50;
49     private static final long WAVE_ANIM_EACH_ICON_DURATION = 633;
50     private static final long WAVE_ANIM_SLOT_MACHINE_DURATION = 1085;
51     // The fraction of each icon's animation at which we reach the top point of the wave.
52     private static final float WAVE_ANIM_FRACTION_TOP = 0.4f;
53     // The fraction of each icon's animation at which we reach the bottom, before overshooting.
54     private static final float WAVE_ANIM_FRACTION_BOTTOM = 0.9f;
55     private static final TimeInterpolator WAVE_ANIM_TO_TOP_INTERPOLATOR = FAST_OUT_SLOW_IN;
56     private static final TimeInterpolator WAVE_ANIM_TO_BOTTOM_INTERPOLATOR = ACCEL_2;
57     private static final TimeInterpolator WAVE_ANIM_OVERSHOOT_INTERPOLATOR = DEACCEL;
58     private static final TimeInterpolator WAVE_ANIM_OVERSHOOT_RETURN_INTERPOLATOR = ACCEL_DEACCEL;
59     private static final float WAVE_ANIM_ICON_SCALE = 1.2f;
60     // How many icons to cycle through in the slot machine (+ the original icon at each end).
61     private static final int WAVE_ANIM_SLOT_MACHINE_NUM_ICONS = 3;
62 
63     private final TaskbarActivityContext mActivity;
64     private final float mWaveAnimTranslationY;
65     private final float mWaveAnimTranslationYReturnOvershoot;
66 
67     // Initialized in init.
68     TaskbarControllers mControllers;
69 
70     private TaskbarEduView mTaskbarEduView;
71     private Animator mAnim;
72 
TaskbarEduController(TaskbarActivityContext activity)73     public TaskbarEduController(TaskbarActivityContext activity) {
74         mActivity = activity;
75 
76         final Resources resources = activity.getResources();
77         mWaveAnimTranslationY = resources.getDimension(R.dimen.taskbar_edu_wave_anim_trans_y);
78         mWaveAnimTranslationYReturnOvershoot = resources.getDimension(
79                 R.dimen.taskbar_edu_wave_anim_trans_y_return_overshoot);
80     }
81 
init(TaskbarControllers controllers)82     public void init(TaskbarControllers controllers) {
83         mControllers = controllers;
84     }
85 
showEdu()86     void showEdu() {
87         mActivity.setTaskbarWindowFullscreen(true);
88         mActivity.getDragLayer().post(() -> {
89             mTaskbarEduView = (TaskbarEduView) mActivity.getLayoutInflater().inflate(
90                     R.layout.taskbar_edu, mActivity.getDragLayer(), false);
91             mTaskbarEduView.init(new TaskbarEduCallbacks());
92             mTaskbarEduView.addOnCloseListener(() -> mTaskbarEduView = null);
93             mTaskbarEduView.show();
94             startAnim(createWaveAnim());
95         });
96     }
97 
hideEdu()98     void hideEdu() {
99         if (mTaskbarEduView != null) {
100             mTaskbarEduView.close(true /* animate */);
101         }
102     }
103 
104     /**
105      * Starts the given animation, ending the previous animation first if it's still playing.
106      */
startAnim(Animator anim)107     private void startAnim(Animator anim) {
108         if (mAnim != null) {
109             mAnim.end();
110         }
111         mAnim = anim;
112         mAnim.addListener(new AnimatorListenerAdapter() {
113             @Override
114             public void onAnimationEnd(Animator animation) {
115                 mAnim = null;
116             }
117         });
118         mAnim.start();
119     }
120 
121     /**
122      * Creates a staggered "wave" animation where each icon translates and scales up in succession.
123      */
createWaveAnim()124     private Animator createWaveAnim() {
125         AnimatorSet waveAnim = new AnimatorSet();
126         View[] icons = mControllers.taskbarViewController.getIconViews();
127         for (int i = 0; i < icons.length; i++) {
128             View icon = icons[i];
129             AnimatorSet iconAnim = new AnimatorSet();
130 
131             Keyframe[] scaleKeyframes = new Keyframe[] {
132                     Keyframe.ofFloat(0, 1f),
133                     Keyframe.ofFloat(WAVE_ANIM_FRACTION_TOP, WAVE_ANIM_ICON_SCALE),
134                     Keyframe.ofFloat(WAVE_ANIM_FRACTION_BOTTOM, 1f),
135                     Keyframe.ofFloat(1f, 1f)
136             };
137             scaleKeyframes[1].setInterpolator(WAVE_ANIM_TO_TOP_INTERPOLATOR);
138             scaleKeyframes[2].setInterpolator(WAVE_ANIM_TO_BOTTOM_INTERPOLATOR);
139 
140             Keyframe[] translationYKeyframes = new Keyframe[] {
141                     Keyframe.ofFloat(0, 0f),
142                     Keyframe.ofFloat(WAVE_ANIM_FRACTION_TOP, -mWaveAnimTranslationY),
143                     Keyframe.ofFloat(WAVE_ANIM_FRACTION_BOTTOM, 0f),
144                     // Half of the remaining fraction overshoots, then the other half returns to 0.
145                     Keyframe.ofFloat(
146                             WAVE_ANIM_FRACTION_BOTTOM + (1 - WAVE_ANIM_FRACTION_BOTTOM) / 2f,
147                             mWaveAnimTranslationYReturnOvershoot),
148                     Keyframe.ofFloat(1f, 0f)
149             };
150             translationYKeyframes[1].setInterpolator(WAVE_ANIM_TO_TOP_INTERPOLATOR);
151             translationYKeyframes[2].setInterpolator(WAVE_ANIM_TO_BOTTOM_INTERPOLATOR);
152             translationYKeyframes[3].setInterpolator(WAVE_ANIM_OVERSHOOT_INTERPOLATOR);
153             translationYKeyframes[4].setInterpolator(WAVE_ANIM_OVERSHOOT_RETURN_INTERPOLATOR);
154 
155             iconAnim.play(ObjectAnimator.ofPropertyValuesHolder(icon,
156                     PropertyValuesHolder.ofKeyframe(SCALE_PROPERTY, scaleKeyframes))
157                     .setDuration(WAVE_ANIM_EACH_ICON_DURATION));
158             iconAnim.play(ObjectAnimator.ofPropertyValuesHolder(icon,
159                     PropertyValuesHolder.ofKeyframe(View.TRANSLATION_Y, translationYKeyframes))
160                     .setDuration(WAVE_ANIM_EACH_ICON_DURATION));
161 
162             if (icon instanceof PredictedAppIcon) {
163                 // Play slot machine animation through random icons from AllAppsList.
164                 PredictedAppIcon predictedAppIcon = (PredictedAppIcon) icon;
165                 ItemInfo itemInfo = (ItemInfo) icon.getTag();
166                 List<BitmapInfo> iconsToAnimate = mControllers.uiController.getAppIconsForEdu()
167                         .filter(appInfo -> !TextUtils.equals(appInfo.title, itemInfo.title))
168                         .map(appInfo -> appInfo.bitmap)
169                         .filter(bitmap -> !bitmap.isNullOrLowRes())
170                         .collect(Collectors.toList());
171                 // Pick n icons at random.
172                 Collections.shuffle(iconsToAnimate);
173                 if (iconsToAnimate.size() > WAVE_ANIM_SLOT_MACHINE_NUM_ICONS) {
174                     iconsToAnimate = iconsToAnimate.subList(0, WAVE_ANIM_SLOT_MACHINE_NUM_ICONS);
175                 }
176                 Animator slotMachineAnim = predictedAppIcon.createSlotMachineAnim(iconsToAnimate);
177                 if (slotMachineAnim != null) {
178                     iconAnim.play(slotMachineAnim.setDuration(WAVE_ANIM_SLOT_MACHINE_DURATION));
179                 }
180             }
181 
182             iconAnim.setStartDelay(WAVE_ANIM_STAGGER * i);
183             waveAnim.play(iconAnim);
184         }
185         waveAnim.setStartDelay(WAVE_ANIM_DELAY);
186         return waveAnim;
187     }
188 
189     /**
190      * Callbacks for {@link TaskbarEduView} to interact with its controller.
191      */
192     class TaskbarEduCallbacks {
onPageChanged(int currentPage, int pageCount)193         void onPageChanged(int currentPage, int pageCount) {
194             if (currentPage == 0) {
195                 mTaskbarEduView.updateStartButton(R.string.taskbar_edu_close,
196                         v -> mTaskbarEduView.close(true /* animate */));
197             } else {
198                 mTaskbarEduView.updateStartButton(R.string.taskbar_edu_previous,
199                         v -> mTaskbarEduView.snapToPage(currentPage - 1));
200             }
201             if (currentPage == pageCount - 1) {
202                 mTaskbarEduView.updateEndButton(R.string.taskbar_edu_done,
203                         v -> mTaskbarEduView.close(true /* animate */));
204             } else {
205                 mTaskbarEduView.updateEndButton(R.string.taskbar_edu_next,
206                         v -> mTaskbarEduView.snapToPage(currentPage + 1));
207             }
208         }
209     }
210 }
211