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