diff --git a/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml b/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml new file mode 100644 index 0000000000..df7cd8e437 --- /dev/null +++ b/quickstep/recents_ui_overrides/res/drawable/all_apps_edu_circle.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml b/quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml new file mode 100644 index 0000000000..e7ef6e699a --- /dev/null +++ b/quickstep/recents_ui_overrides/res/layout/all_apps_edu_view.xml @@ -0,0 +1,6 @@ + + diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java new file mode 100644 index 0000000000..df89f74ae2 --- /dev/null +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.views; + +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7; +import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewGroup; + +import androidx.core.graphics.ColorUtils; + +import com.android.launcher3.AbstractFloatingView; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.states.StateAnimationConfig; +import com.android.launcher3.util.Themes; +import com.android.quickstep.util.MultiValueUpdateListener; + +/** + * View used to educate the user on how to access All Apps when in No Nav Button navigation mode. + */ +public class AllAppsEduView extends AbstractFloatingView { + + private Launcher mLauncher; + + private AnimatorSet mAnimation; + + private GradientDrawable mCircle; + private GradientDrawable mGradient; + + private int mCircleSizePx; + private int mPaddingPx; + private int mWidthPx; + private int mMaxHeightPx; + + public AllAppsEduView(Context context, AttributeSet attrs) { + super(context, attrs); + mCircle = (GradientDrawable) context.getDrawable(R.drawable.all_apps_edu_circle); + mCircleSizePx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_circle_size); + mPaddingPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_padding); + mWidthPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_width); + mMaxHeightPx = getResources().getDimensionPixelSize(R.dimen.swipe_edu_max_height); + setWillNotDraw(false); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + mGradient.draw(canvas); + mCircle.draw(canvas); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mIsOpen = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mIsOpen = false; + } + + @Override + protected void handleClose(boolean animate) { + mLauncher.getDragLayer().removeView(this); + } + + @Override + public void logActionCommand(int command) { + // TODO + } + + @Override + protected boolean isOfType(int type) { + return (type & TYPE_ALL_APPS_EDU) != 0; + } + + @Override + public boolean onControllerInterceptTouchEvent(MotionEvent ev) { + return mAnimation != null && mAnimation.isRunning(); + } + + private void playAnimation() { + if (mAnimation != null) { + return; + } + mAnimation = new AnimatorSet(); + + final Rect circleBoundsOg = new Rect(mCircle.getBounds()); + final Rect gradientBoundsOg = new Rect(mGradient.getBounds()); + final Rect temp = new Rect(); + final float transY = mMaxHeightPx - mCircleSizePx - mPaddingPx; + + // 1st: Circle alpha/scale + int firstPart = 600; + // 2nd: Circle animates upwards, Gradient alpha fades in, Gradient grows, All Apps hint + int secondPart = 1200; + int introDuration = firstPart + secondPart; + + StateAnimationConfig config = new StateAnimationConfig(); + config.setInterpolator(ANIM_ALL_APPS_FADE, Interpolators.clampToProgress(ACCEL, + 0, 0.08f)); + config.duration = secondPart; + config.userControlled = false; + AnimatorPlaybackController stateAnimationController = + mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, config); + float maxAllAppsProgress = 0.15f; + + ValueAnimator intro = ValueAnimator.ofFloat(0, 1f); + intro.setInterpolator(LINEAR); + intro.setDuration(introDuration); + intro.addUpdateListener((new MultiValueUpdateListener() { + FloatProp mCircleAlpha = new FloatProp(0, 255, 0, firstPart, LINEAR); + FloatProp mCircleScale = new FloatProp(2f, 1f, 0, firstPart, OVERSHOOT_1_7); + FloatProp mDeltaY = new FloatProp(0, transY, firstPart, secondPart, FAST_OUT_SLOW_IN); + FloatProp mGradientAlpha = new FloatProp(0, 255, firstPart, secondPart * 0.3f, LINEAR); + + @Override + public void onUpdate(float progress) { + temp.set(circleBoundsOg); + temp.offset(0, (int) -mDeltaY.value); + Utilities.scaleRectAboutCenter(temp, mCircleScale.value); + mCircle.setBounds(temp); + mCircle.setAlpha((int) mCircleAlpha.value); + mGradient.setAlpha((int) mGradientAlpha.value); + + temp.set(gradientBoundsOg); + temp.top -= mDeltaY.value; + mGradient.setBounds(temp); + invalidate(); + + float stateProgress = Utilities.mapToRange(mDeltaY.value, 0, transY, 0, + maxAllAppsProgress, LINEAR); + stateAnimationController.setPlayFraction(stateProgress); + } + })); + intro.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCircle.setAlpha(0); + mGradient.setAlpha(0); + } + }); + mAnimation.play(intro); + + ValueAnimator closeAllApps = ValueAnimator.ofFloat(maxAllAppsProgress, 0f); + closeAllApps.addUpdateListener(valueAnimator -> { + stateAnimationController.setPlayFraction((float) valueAnimator.getAnimatedValue()); + }); + closeAllApps.setInterpolator(FAST_OUT_SLOW_IN); + closeAllApps.setStartDelay(introDuration); + closeAllApps.setDuration(250); + mAnimation.play(closeAllApps); + + mAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimation = null; + stateAnimationController.dispatchOnCancel(); + handleClose(false); + } + }); + mAnimation.start(); + } + + private void init(Launcher launcher) { + mLauncher = launcher; + + int accentColor = Themes.getColorAccent(launcher); + mGradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, + Themes.getAttrBoolean(launcher, R.attr.isMainColorDark) + ? new int[] {0xB3FFFFFF, 0x00FFFFFF} + : new int[] {ColorUtils.setAlphaComponent(accentColor, 127), + ColorUtils.setAlphaComponent(accentColor, 0)}); + float r = mWidthPx / 2f; + mGradient.setCornerRadii(new float[] {r, r, r, r, 0, 0, 0, 0}); + + int top = mMaxHeightPx - mCircleSizePx + mPaddingPx; + mCircle.setBounds(mPaddingPx, top, mPaddingPx + mCircleSizePx, top + mCircleSizePx); + mGradient.setBounds(0, mMaxHeightPx - mCircleSizePx, mWidthPx, mMaxHeightPx); + + DeviceProfile grid = launcher.getDeviceProfile(); + DragLayer.LayoutParams lp = new DragLayer.LayoutParams(mWidthPx, mMaxHeightPx); + lp.ignoreInsets = true; + lp.leftMargin = (grid.widthPx - mWidthPx) / 2; + lp.topMargin = grid.heightPx - grid.hotseatBarSizePx - mMaxHeightPx; + setLayoutParams(lp); + } + + /** + * Shows the All Apps education view and plays the animation. + */ + public static void show(Launcher launcher) { + final DragLayer dragLayer = launcher.getDragLayer(); + ViewGroup parent = (ViewGroup) dragLayer.getParent(); + AllAppsEduView view = launcher.getViewCache().getView(R.layout.all_apps_edu_view, + launcher, parent); + view.init(launcher); + launcher.getDragLayer().addView(view); + view.requestLayout(); + view.playAnimation(); + } +} diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index f1ea6bbeb7..9d703167d9 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -92,4 +92,10 @@ 16dp 24dp 18dp + + + 8dp + 64dp + 80dp + 184dp diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java index 2d8bba2896..7e8222c228 100644 --- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java +++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java @@ -15,20 +15,27 @@ */ package com.android.quickstep.util; +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU; +import static com.android.launcher3.AbstractFloatingView.getOpenView; import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.HINT_STATE; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS; +import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON; import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview; import android.content.SharedPreferences; import com.android.launcher3.BaseQuickstepLauncher; import com.android.launcher3.LauncherState; +import com.android.launcher3.Workspace; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StateManager.StateListener; import com.android.launcher3.util.OnboardingPrefs; import com.android.quickstep.SysUINavigationMode; +import com.android.quickstep.views.AllAppsEduView; /** * Extends {@link OnboardingPrefs} for quickstep-specific onboarding data. @@ -92,5 +99,51 @@ public class QuickstepOnboardingPrefs extends OnboardingPrefs() { + private static final int MAX_NUM_SWIPES_TO_TRIGGER_EDU = 3; + + // Counts the number of consecutive swipes on nav bar without moving screens. + private int mCount = 0; + private boolean mShouldIncreaseCount; + + @Override + public void onStateTransitionStart(LauncherState toState) { + if (toState == NORMAL) { + return; + } + mShouldIncreaseCount = toState == HINT_STATE + && launcher.getWorkspace().getNextPage() == Workspace.DEFAULT_PAGE; + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + if (finalState == NORMAL) { + if (mCount == MAX_NUM_SWIPES_TO_TRIGGER_EDU) { + if (getOpenView(mLauncher, TYPE_ALL_APPS_EDU) == null) { + AllAppsEduView.show(launcher); + } + mCount = 0; + } + return; + } + + if (mShouldIncreaseCount && finalState == HINT_STATE) { + mCount++; + } else { + mCount = 0; + } + + if (finalState == ALL_APPS) { + AllAppsEduView view = getOpenView(mLauncher, TYPE_ALL_APPS_EDU); + if (view != null) { + view.close(false); + } + } + } + }); + } } } diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index bed8278bc7..1aa31446d5 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -59,6 +59,7 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch TYPE_DISCOVERY_BOUNCE, TYPE_SNACKBAR, TYPE_LISTENER, + TYPE_ALL_APPS_EDU, TYPE_TASK_MENU, TYPE_OPTIONS_POPUP @@ -74,25 +75,28 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch public static final int TYPE_DISCOVERY_BOUNCE = 1 << 6; public static final int TYPE_SNACKBAR = 1 << 7; public static final int TYPE_LISTENER = 1 << 8; + public static final int TYPE_ALL_APPS_EDU = 1 << 9; // Popups related to quickstep UI - public static final int TYPE_TASK_MENU = 1 << 9; - public static final int TYPE_OPTIONS_POPUP = 1 << 10; + public static final int TYPE_TASK_MENU = 1 << 10; + public static final int TYPE_OPTIONS_POPUP = 1 << 11; public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU - | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER; + | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU; // Type of popups which should be kept open during launcher rebind public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET - | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE; + | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE + | TYPE_ALL_APPS_EDU; // Usually we show the back button when a floating view is open. Instead, hide for these types. public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_SNACKBAR | TYPE_WIDGET_RESIZE_FRAME | TYPE_LISTENER; - public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER; + public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER + & ~TYPE_ALL_APPS_EDU; // These view all have particular operation associated with swipe down interaction. public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET | diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 69193399f7..869dbbc9ab 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -164,6 +164,10 @@ public final class FeatureFlags { "ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS", false, "Always use hardware optimization for folder animations."); + public static final BooleanFlag ENABLE_ALL_APPS_EDU = getDebugFlag( + "ENABLE_ALL_APPS_EDU", true, + "Shows user a tutorial on how to get to All Apps after X amount of attempts."); + public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) {