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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user