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