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 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 canInterceptEventsInSystemGestureRegion()115 public boolean canInterceptEventsInSystemGestureRegion() { 116 return true; 117 } 118 119 shouldInterceptTouch(MotionEvent ev)120 private boolean shouldInterceptTouch(MotionEvent ev) { 121 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 122 mCanInterceptTouch = (ev.getEdgeFlags() & EDGE_NAV_BAR) == 0; 123 } 124 return mCanInterceptTouch; 125 } 126 127 @Override onControllerTouchEvent(MotionEvent ev)128 public boolean onControllerTouchEvent(MotionEvent ev) { 129 if (shouldInterceptTouch(ev)) { 130 mTouchController.onControllerTouchEvent(ev); 131 updateAnimationOnTouchEvent(ev); 132 } 133 return true; 134 } 135 updateAnimationOnTouchEvent(MotionEvent ev)136 private void updateAnimationOnTouchEvent(MotionEvent ev) { 137 if (mAnimation == null) { 138 return; 139 } 140 switch (ev.getActionMasked()) { 141 case MotionEvent.ACTION_DOWN: 142 mAnimation.pause(); 143 return; 144 case MotionEvent.ACTION_UP: 145 case MotionEvent.ACTION_CANCEL: 146 mAnimation.resume(); 147 return; 148 } 149 150 if (mTouchController.isDraggingOrSettling()) { 151 mAnimation = null; 152 handleClose(false); 153 } 154 } 155 156 @Override onControllerInterceptTouchEvent(MotionEvent ev)157 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 158 if (shouldInterceptTouch(ev)) { 159 mTouchController.onControllerInterceptTouchEvent(ev); 160 updateAnimationOnTouchEvent(ev); 161 } 162 return true; 163 } 164 playAnimation()165 private void playAnimation() { 166 if (mAnimation != null) { 167 return; 168 } 169 mAnimation = new AnimatorSet(); 170 171 final Rect circleBoundsOg = new Rect(mCircle.getBounds()); 172 final Rect gradientBoundsOg = new Rect(mGradient.getBounds()); 173 final Rect temp = new Rect(); 174 final float transY = mMaxHeightPx - mCircleSizePx - mPaddingPx; 175 176 // 1st: Circle alpha/scale 177 int firstPart = 600; 178 // 2nd: Circle animates upwards, Gradient alpha fades in, Gradient grows, All Apps hint 179 int secondPart = 1200; 180 int introDuration = firstPart + secondPart; 181 182 AnimatorPlaybackController stateAnimationController = 183 mTouchController.initAllAppsAnimation(); 184 float maxAllAppsProgress = 0.75f; 185 186 ValueAnimator intro = ValueAnimator.ofFloat(0, 1f); 187 intro.setInterpolator(LINEAR); 188 intro.setDuration(introDuration); 189 intro.addUpdateListener((new MultiValueUpdateListener() { 190 FloatProp mCircleAlpha = new FloatProp(0, 255, 0, firstPart, LINEAR); 191 FloatProp mCircleScale = new FloatProp(2f, 1f, 0, firstPart, OVERSHOOT_1_7); 192 FloatProp mDeltaY = new FloatProp(0, transY, firstPart, secondPart, FAST_OUT_SLOW_IN); 193 FloatProp mGradientAlpha = new FloatProp(0, 255, firstPart, secondPart * 0.3f, LINEAR); 194 195 @Override 196 public void onUpdate(float progress, boolean initOnly) { 197 temp.set(circleBoundsOg); 198 temp.offset(0, (int) -mDeltaY.value); 199 Utilities.scaleRectAboutCenter(temp, mCircleScale.value); 200 mCircle.setBounds(temp); 201 mCircle.setAlpha((int) mCircleAlpha.value); 202 mGradient.setAlpha((int) mGradientAlpha.value); 203 204 temp.set(gradientBoundsOg); 205 temp.top -= mDeltaY.value; 206 mGradient.setBounds(temp); 207 invalidate(); 208 209 float stateProgress = Utilities.mapToRange(mDeltaY.value, 0, transY, 0, 210 maxAllAppsProgress, LINEAR); 211 stateAnimationController.setPlayFraction(stateProgress); 212 } 213 })); 214 intro.addListener(new AnimatorListenerAdapter() { 215 @Override 216 public void onAnimationEnd(Animator animation) { 217 mCircle.setAlpha(0); 218 mGradient.setAlpha(0); 219 } 220 }); 221 mLauncher.getAppsView().setVisibility(View.VISIBLE); 222 mAnimation.play(intro); 223 224 ValueAnimator closeAllApps = ValueAnimator.ofFloat(maxAllAppsProgress, 0f); 225 closeAllApps.addUpdateListener(valueAnimator -> { 226 stateAnimationController.setPlayFraction((float) valueAnimator.getAnimatedValue()); 227 }); 228 closeAllApps.setInterpolator(FAST_OUT_SLOW_IN); 229 closeAllApps.setStartDelay(introDuration); 230 closeAllApps.setDuration(250); 231 mAnimation.play(closeAllApps); 232 233 mAnimation.addListener(new AnimatorListenerAdapter() { 234 @Override 235 public void onAnimationEnd(Animator animation) { 236 mAnimation = null; 237 // Handles cancelling the animation used to hint towards All Apps. 238 mLauncher.getStateManager().goToState(NORMAL, false); 239 handleClose(false); 240 } 241 }); 242 mAnimation.start(); 243 } 244 init(Launcher launcher)245 private void init(Launcher launcher) { 246 mLauncher = launcher; 247 mTouchController = new AllAppsEduTouchController(mLauncher); 248 249 int accentColor = Themes.getColorAccent(launcher); 250 mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, 251 Themes.getAttrBoolean(launcher, R.attr.isMainColorDark) 252 ? new int[]{0xB3FFFFFF, 0x00FFFFFF} 253 : new int[]{ColorUtils.setAlphaComponent(accentColor, 127), 254 ColorUtils.setAlphaComponent(accentColor, 0)}); 255 float r = mWidthPx / 2f; 256 mGradient.setCornerRadii(new float[]{r, r, r, r, 0, 0, 0, 0}); 257 258 int top = mMaxHeightPx - mCircleSizePx + mPaddingPx; 259 mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx); 260 mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx); 261 262 DeviceProfile grid = launcher.getDeviceProfile(); 263 DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx); 264 lp.ignoreInsets = true; 265 lp.leftMargin = (grid.widthPx - mWidthPx) / 2; 266 lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx; 267 setLayoutParams(lp); 268 } 269 270 /** 271 * Shows the All Apps education view and plays the animation. 272 */ show(Launcher launcher)273 public static void show(Launcher launcher) { 274 final DragLayer dragLayer = launcher.getDragLayer(); 275 AllAppsEduView view = (AllAppsEduView) launcher.getLayoutInflater().inflate( 276 R.layout.all_apps_edu_view, dragLayer, false); 277 view.init(launcher); 278 launcher.getDragLayer().addView(view); 279 launcher.getStatsLogManager().logger().log(LAUNCHER_ALL_APPS_EDU_SHOWN); 280 281 view.requestLayout(); 282 view.playAnimation(); 283 } 284 285 private static class AllAppsEduTouchController extends PortraitStatesTouchController { 286 AllAppsEduTouchController(Launcher l)287 private AllAppsEduTouchController(Launcher l) { 288 super(l); 289 } 290 291 @Override canInterceptTouch(MotionEvent ev)292 protected boolean canInterceptTouch(MotionEvent ev) { 293 return true; 294 } 295 initAllAppsAnimation()296 private AnimatorPlaybackController initAllAppsAnimation() { 297 mFromState = NORMAL; 298 mToState = ALL_APPS; 299 mProgressMultiplier = initCurrentAnimation(); 300 return mCurrentAnimation; 301 } 302 isDraggingOrSettling()303 private boolean isDraggingOrSettling() { 304 return mDetector.isDraggingOrSettling(); 305 } 306 } 307 } 308