Add all apps education tutorial.

* Added FeatureFlag.ENABLE_ALL_APPS_EDU
* When user swipes up on nav bar three times and goes to hint state
  consecutively, we show the new All Apps education tutorial.
* For now we block interaction while the animation is playing,
  and we remove the view when the animation is done.
* Future CL will leave view up until user successfully reaches All Apps state.

Bug: 151768994
Change-Id: I903e0a3914d0558950ecb8cd714d97ddc10ca06b
This commit is contained in:
Jon Miranda
2020-05-19 14:04:59 -07:00
parent 0a1cefa497
commit 517cec5344
7 changed files with 337 additions and 5 deletions
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?android:colorAccent"/>
<size android:height="@dimen/swipe_edu_circle_size"
android:width="@dimen/swipe_edu_circle_size" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<com.android.quickstep.views.AllAppsEduView
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="@dimen/swipe_edu_width"
android:layout_height="@dimen/swipe_edu_max_height"/>
@@ -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();
}
}
+6
View File
@@ -92,4 +92,10 @@
<dimen name="gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
<dimen name="gesture_tutorial_feedback_margin_start_end">24dp</dimen>
<dimen name="gesture_tutorial_button_margin_start_end">18dp</dimen>
<!-- All Apps Education tutorial -->
<dimen name="swipe_edu_padding">8dp</dimen>
<dimen name="swipe_edu_circle_size">64dp</dimen>
<dimen name="swipe_edu_width">80dp</dimen>
<dimen name="swipe_edu_max_height">184dp</dimen>
</resources>
@@ -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<BaseQuickstepLaunc
}
});
}
if (SysUINavigationMode.getMode(launcher) == NO_BUTTON
&& FeatureFlags.ENABLE_ALL_APPS_EDU.get()) {
stateManager.addStateListener(new StateListener<LauncherState>() {
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);
}
}
}
});
}
}
}
@@ -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 |
@@ -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) {