1 /*
2  * Copyright (C) 2020 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.quickstep.views;
17 
18 import static com.android.launcher3.LauncherState.ALL_APPS;
19 import static com.android.launcher3.LauncherState.NORMAL;
20 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
21 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
22 import static com.android.launcher3.anim.Interpolators.LINEAR;
23 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_EDU_SHOWN;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.AnimatorSet;
29 import android.animation.ValueAnimator;
30 import android.content.Context;
31 import android.graphics.Canvas;
32 import android.graphics.Rect;
33 import android.graphics.drawable.GradientDrawable;
34 import android.util.AttributeSet;
35 import android.view.MotionEvent;
36 import android.view.View;
37 
38 import androidx.annotation.Nullable;
39 import androidx.core.graphics.ColorUtils;
40 
41 import com.android.launcher3.AbstractFloatingView;
42 import com.android.launcher3.DeviceProfile;
43 import com.android.launcher3.Launcher;
44 import com.android.launcher3.R;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.anim.AnimatorPlaybackController;
47 import com.android.launcher3.dragndrop.DragLayer;
48 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
49 import com.android.launcher3.util.Themes;
50 import com.android.quickstep.util.MultiValueUpdateListener;
51 
52 /**
53  * View used to educate the user on how to access All Apps when in No Nav Button navigation mode.
54  * If the user drags on the view, the animation is overridden so the user can swipe to All Apps or
55  * Home.
56  */
57 public class AllAppsEduView extends AbstractFloatingView {
58 
59     private Launcher mLauncher;
60     private AllAppsEduTouchController mTouchController;
61 
62     @Nullable
63     private AnimatorSet mAnimation;
64 
65     private GradientDrawable mCircle;
66     private GradientDrawable mGradient;
67 
68     private int mCircleSizePx;
69     private int mPaddingPx;
70     private int mWidthPx;
71     private int mMaxHeightPx;
72 
73     private boolean mCanInterceptTouch;
74 
AllAppsEduView(Context context, AttributeSet attrs)75     public AllAppsEduView(Context context, AttributeSet attrs) {
76         super(context, attrs);
77         mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle);
78         mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size);
79         mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding);
80         mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width);
81         mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height);
82         setWillNotDraw(false);
83     }
84 
85     @Override
onDraw(Canvas canvas)86     protected void onDraw(Canvas canvas) {
87         super.onDraw(canvas);
88         mGradient.draw(canvas);
89         mCircle.draw(canvas);
90     }
91 
92     @Override
onAttachedToWindow()93     protected void onAttachedToWindow() {
94         super.onAttachedToWindow();
95         mIsOpen = true;
96     }
97 
98     @Override
onDetachedFromWindow()99     protected void onDetachedFromWindow() {
100         super.onDetachedFromWindow();
101         mIsOpen = false;
102     }
103 
104     @Override
handleClose(boolean animate)105     protected void handleClose(boolean animate) {
106         mLauncher.getDragLayer().removeView(this);
107     }
108 
109     @Override
isOfType(int type)110     protected boolean isOfType(int type) {
111         return (type & TYPE_ALL_APPS_EDU) != 0;
112     }
113 
114     @Override
onBackPressed()115     public boolean onBackPressed() {
116         return true;
117     }
118 
119     @Override
canInterceptEventsInSystemGestureRegion()120     public boolean canInterceptEventsInSystemGestureRegion() {
121         return true;
122     }
123 
124 
shouldInterceptTouch(MotionEvent ev)125     private boolean shouldInterceptTouch(MotionEvent ev) {
126         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
127             mCanInterceptTouch = (ev.getEdgeFlags() & EDGE_NAV_BAR) == 0;
128         }
129         return mCanInterceptTouch;
130     }
131 
132     @Override
onControllerTouchEvent(MotionEvent ev)133     public boolean onControllerTouchEvent(MotionEvent ev) {
134         if (shouldInterceptTouch(ev)) {
135             mTouchController.onControllerTouchEvent(ev);
136             updateAnimationOnTouchEvent(ev);
137         }
138         return true;
139     }
140 
updateAnimationOnTouchEvent(MotionEvent ev)141     private void updateAnimationOnTouchEvent(MotionEvent ev) {
142         if (mAnimation == null) {
143             return;
144         }
145         switch (ev.getActionMasked()) {
146             case MotionEvent.ACTION_DOWN:
147                 mAnimation.pause();
148                 return;
149             case MotionEvent.ACTION_UP:
150             case MotionEvent.ACTION_CANCEL:
151                 mAnimation.resume();
152                 return;
153         }
154 
155         if (mTouchController.isDraggingOrSettling()) {
156             mAnimation = null;
157             handleClose(false);
158         }
159     }
160 
161     @Override
onControllerInterceptTouchEvent(MotionEvent ev)162     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
163         if (shouldInterceptTouch(ev)) {
164             mTouchController.onControllerInterceptTouchEvent(ev);
165             updateAnimationOnTouchEvent(ev);
166         }
167         return true;
168     }
169 
playAnimation()170     private void playAnimation() {
171         if (mAnimation != null) {
172             return;
173         }
174         mAnimation = new AnimatorSet();
175 
176         final Rect circleBoundsOg = new Rect(mCircle.getBounds());
177         final Rect gradientBoundsOg = new Rect(mGradient.getBounds());
178         final Rect temp = new Rect();
179         final float transY = mMaxHeightPx - mCircleSizePx - mPaddingPx;
180 
181         // 1st: Circle alpha/scale
182         int firstPart = 600;
183         // 2nd: Circle animates upwards, Gradient alpha fades in, Gradient grows, All Apps hint
184         int secondPart = 1200;
185         int introDuration = firstPart + secondPart;
186 
187         AnimatorPlaybackController stateAnimationController =
188                 mTouchController.initAllAppsAnimation();
189         float maxAllAppsProgress = 0.75f;
190 
191         ValueAnimator intro = ValueAnimator.ofFloat(0, 1f);
192         intro.setInterpolator(LINEAR);
193         intro.setDuration(introDuration);
194         intro.addUpdateListener((new MultiValueUpdateListener() {
195             FloatProp mCircleAlpha = new FloatProp(0, 255, 0, firstPart, LINEAR);
196             FloatProp mCircleScale = new FloatProp(2f, 1f, 0, firstPart, OVERSHOOT_1_7);
197             FloatProp mDeltaY = new FloatProp(0, transY, firstPart, secondPart, FAST_OUT_SLOW_IN);
198             FloatProp mGradientAlpha = new FloatProp(0, 255, firstPart, secondPart * 0.3f, LINEAR);
199 
200             @Override
201             public void onUpdate(float progress, boolean initOnly) {
202                 temp.set(circleBoundsOg);
203                 temp.offset(0, (int) -mDeltaY.value);
204                 Utilities.scaleRectAboutCenter(temp, mCircleScale.value);
205                 mCircle.setBounds(temp);
206                 mCircle.setAlpha((int) mCircleAlpha.value);
207                 mGradient.setAlpha((int) mGradientAlpha.value);
208 
209                 temp.set(gradientBoundsOg);
210                 temp.top -= mDeltaY.value;
211                 mGradient.setBounds(temp);
212                 invalidate();
213 
214                 float stateProgress = Utilities.mapToRange(mDeltaY.value, 0, transY, 0,
215                         maxAllAppsProgress, LINEAR);
216                 stateAnimationController.setPlayFraction(stateProgress);
217             }
218         }));
219         intro.addListener(new AnimatorListenerAdapter() {
220             @Override
221             public void onAnimationEnd(Animator animation) {
222                 mCircle.setAlpha(0);
223                 mGradient.setAlpha(0);
224             }
225         });
226         mLauncher.getAppsView().setVisibility(View.VISIBLE);
227         mAnimation.play(intro);
228 
229         ValueAnimator closeAllApps = ValueAnimator.ofFloat(maxAllAppsProgress, 0f);
230         closeAllApps.addUpdateListener(valueAnimator -> {
231             stateAnimationController.setPlayFraction((float) valueAnimator.getAnimatedValue());
232         });
233         closeAllApps.setInterpolator(FAST_OUT_SLOW_IN);
234         closeAllApps.setStartDelay(introDuration);
235         closeAllApps.setDuration(250);
236         mAnimation.play(closeAllApps);
237 
238         mAnimation.addListener(new AnimatorListenerAdapter() {
239             @Override
240             public void onAnimationEnd(Animator animation) {
241                 mAnimation = null;
242                 // Handles cancelling the animation used to hint towards All Apps.
243                 mLauncher.getStateManager().goToState(NORMAL, false);
244                 handleClose(false);
245             }
246         });
247         mAnimation.start();
248     }
249 
init(Launcher launcher)250     private void init(Launcher launcher) {
251         mLauncher = launcher;
252         mTouchController = new AllAppsEduTouchController(mLauncher);
253 
254         int accentColor = Themes.getColorAccent(launcher);
255         mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM,
256                 Themes.getAttrBoolean(launcher, R.attr.isMainColorDark)
257                         ? new int[]{0xB3FFFFFF, 0x00FFFFFF}
258                         : new int[]{ColorUtils.setAlphaComponent(accentColor, 127),
259                                 ColorUtils.setAlphaComponent(accentColor, 0)});
260         float r = mWidthPx / 2f;
261         mGradient.setCornerRadii(new float[]{r, r, r, r, 0, 0, 0, 0});
262 
263         int top = mMaxHeightPx - mCircleSizePx + mPaddingPx;
264         mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx);
265         mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx);
266 
267         DeviceProfile grid = launcher.getDeviceProfile();
268         DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx);
269         lp.ignoreInsets = true;
270         lp.leftMargin = (grid.widthPx - mWidthPx) / 2;
271         lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx;
272         setLayoutParams(lp);
273     }
274 
275     /**
276      * Shows the All Apps education view and plays the animation.
277      */
show(Launcher launcher)278     public static void show(Launcher launcher) {
279         final DragLayer dragLayer = launcher.getDragLayer();
280         AllAppsEduView view = (AllAppsEduView) launcher.getLayoutInflater().inflate(
281                 R.layout.all_apps_edu_view, dragLayer, false);
282         view.init(launcher);
283         launcher.getDragLayer().addView(view);
284         launcher.getStatsLogManager().logger().log(LAUNCHER_ALL_APPS_EDU_SHOWN);
285 
286         view.requestLayout();
287         view.playAnimation();
288     }
289 
290     private static class AllAppsEduTouchController extends PortraitStatesTouchController {
291 
AllAppsEduTouchController(Launcher l)292         private AllAppsEduTouchController(Launcher l) {
293             super(l);
294         }
295 
296         @Override
canInterceptTouch(MotionEvent ev)297         protected boolean canInterceptTouch(MotionEvent ev) {
298             return true;
299         }
300 
initAllAppsAnimation()301         private AnimatorPlaybackController initAllAppsAnimation() {
302             mFromState = NORMAL;
303             mToState = ALL_APPS;
304             mProgressMultiplier = initCurrentAnimation();
305             return mCurrentAnimation;
306         }
307 
isDraggingOrSettling()308         private boolean isDraggingOrSettling() {
309             return mDetector.isDraggingOrSettling();
310         }
311     }
312 }
313