Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 517cec53 authored by Jon Miranda's avatar Jon Miranda
Browse files

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
parent 0a1cefa4
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
<?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>
 No newline at end of file
+6 −0
Original line number Diff line number Diff line
<?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"/>
+237 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
@@ -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>
+53 −0
Original line number Diff line number Diff line
@@ -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);
                        }
                    }
                }
            });
        }
    }
}
Loading