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.app.animation.Interpolators.FAST_OUT_SLOW_IN; 19 import static com.android.app.animation.Interpolators.LINEAR; 20 import static com.android.app.animation.Interpolators.OVERSHOOT_1_7; 21 import static com.android.launcher3.LauncherState.ALL_APPS; 22 import static com.android.launcher3.LauncherState.NORMAL; 23 import static com.android.launcher3.Utilities.EDGE_NAV_BAR; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALL_APPS_EDU_SHOWN; 25 import static com.android.quickstep.util.AnimUtils.clampToDuration; 26 27 import android.animation.Animator; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.AnimatorSet; 30 import android.animation.ValueAnimator; 31 import android.content.Context; 32 import android.graphics.Canvas; 33 import android.graphics.Rect; 34 import android.graphics.drawable.GradientDrawable; 35 import android.util.AttributeSet; 36 import android.view.MotionEvent; 37 import android.view.View; 38 39 import androidx.annotation.Nullable; 40 import androidx.core.graphics.ColorUtils; 41 42 import com.android.launcher3.AbstractFloatingView; 43 import com.android.launcher3.DeviceProfile; 44 import com.android.launcher3.Launcher; 45 import com.android.launcher3.R; 46 import com.android.launcher3.Utilities; 47 import com.android.launcher3.anim.AnimatorPlaybackController; 48 import com.android.launcher3.dragndrop.DragLayer; 49 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController; 50 import com.android.launcher3.util.Themes; 51 import com.android.quickstep.util.MultiValueUpdateListener; 52 53 /** 54 * View used to educate the user on how to access All Apps when in No Nav Button navigation mode. 55 * If the user drags on the view, the animation is overridden so the user can swipe to All Apps or 56 * Home. 57 */ 58 public class AllAppsEduView extends AbstractFloatingView { 59 60 private Launcher mLauncher; 61 private AllAppsEduTouchController mTouchController; 62 63 @Nullable 64 private AnimatorSet mAnimation; 65 66 private GradientDrawable mCircle; 67 private GradientDrawable mGradient; 68 69 private int mCircleSizePx; 70 private int mPaddingPx; 71 private int mWidthPx; 72 private int mMaxHeightPx; 73 74 private boolean mCanInterceptTouch; 75 AllAppsEduView(Context context, AttributeSet attrs)76 public AllAppsEduView(Context context, AttributeSet attrs) { 77 super(context, attrs); 78 mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle); 79 mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size); 80 mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding); 81 mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width); 82 mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height); 83 setWillNotDraw(false); 84 } 85 86 @Override onDraw(Canvas canvas)87 protected void onDraw(Canvas canvas) { 88 super.onDraw(canvas); 89 mGradient.draw(canvas); 90 mCircle.draw(canvas); 91 } 92 93 @Override onAttachedToWindow()94 protected void onAttachedToWindow() { 95 super.onAttachedToWindow(); 96 mIsOpen = true; 97 } 98 99 @Override onDetachedFromWindow()100 protected void onDetachedFromWindow() { 101 super.onDetachedFromWindow(); 102 mIsOpen = false; 103 } 104 105 @Override handleClose(boolean animate)106 protected void handleClose(boolean animate) { 107 mLauncher.getDragLayer().removeView(this); 108 } 109 110 @Override isOfType(int type)111 protected boolean isOfType(int type) { 112 return (type & TYPE_ALL_APPS_EDU) != 0; 113 } 114 115 @Override canInterceptEventsInSystemGestureRegion()116 public boolean canInterceptEventsInSystemGestureRegion() { 117 return true; 118 } 119 120 shouldInterceptTouch(MotionEvent ev)121 private boolean shouldInterceptTouch(MotionEvent ev) { 122 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 123 mCanInterceptTouch = (ev.getEdgeFlags() & EDGE_NAV_BAR) == 0; 124 } 125 return mCanInterceptTouch; 126 } 127 128 @Override onControllerTouchEvent(MotionEvent ev)129 public boolean onControllerTouchEvent(MotionEvent ev) { 130 if (shouldInterceptTouch(ev)) { 131 mTouchController.onControllerTouchEvent(ev); 132 updateAnimationOnTouchEvent(ev); 133 } 134 return true; 135 } 136 updateAnimationOnTouchEvent(MotionEvent ev)137 private void updateAnimationOnTouchEvent(MotionEvent ev) { 138 if (mAnimation == null) { 139 return; 140 } 141 switch (ev.getActionMasked()) { 142 case MotionEvent.ACTION_DOWN: 143 mAnimation.pause(); 144 return; 145 case MotionEvent.ACTION_UP: 146 case MotionEvent.ACTION_CANCEL: 147 mAnimation.resume(); 148 return; 149 } 150 151 if (mTouchController.isDraggingOrSettling()) { 152 mAnimation = null; 153 handleClose(false); 154 } 155 } 156 157 @Override onControllerInterceptTouchEvent(MotionEvent ev)158 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 159 if (shouldInterceptTouch(ev)) { 160 mTouchController.onControllerInterceptTouchEvent(ev); 161 updateAnimationOnTouchEvent(ev); 162 } 163 return true; 164 } 165 playAnimation()166 private void playAnimation() { 167 if (mAnimation != null) { 168 return; 169 } 170 mAnimation = new AnimatorSet(); 171 172 final Rect circleBoundsOg = new Rect(mCircle.getBounds()); 173 final Rect gradientBoundsOg = new Rect(mGradient.getBounds()); 174 final Rect temp = new Rect(); 175 final float transY = mMaxHeightPx - mCircleSizePx - mPaddingPx; 176 177 // 1st: Circle alpha/scale 178 int firstPart = 600; 179 // 2nd: Circle animates upwards, Gradient alpha fades in, Gradient grows, All Apps hint 180 int secondPart = 1200; 181 int introDuration = firstPart + secondPart; 182 183 AnimatorPlaybackController stateAnimationController = 184 mTouchController.initAllAppsAnimation(); 185 float maxAllAppsProgress = 0.75f; 186 187 ValueAnimator intro = ValueAnimator.ofFloat(0, 1f); 188 intro.setInterpolator(LINEAR); 189 intro.setDuration(introDuration); 190 intro.addUpdateListener((new MultiValueUpdateListener() { 191 FloatProp mCircleAlpha = new FloatProp(0, 255, 192 clampToDuration(LINEAR, 0, firstPart, introDuration)); 193 FloatProp mCircleScale = new FloatProp(2f, 1f, 194 clampToDuration(OVERSHOOT_1_7, 0, firstPart, introDuration)); 195 FloatProp mDeltaY = new FloatProp(0, transY, 196 clampToDuration(FAST_OUT_SLOW_IN, firstPart, secondPart, introDuration)); 197 FloatProp mGradientAlpha = new FloatProp(0, 255, 198 clampToDuration(LINEAR, firstPart, secondPart * 0.3f, introDuration)); 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