Update gesture navigation tutorial for foldable devices.

- Updated mock conversation layout to work on large devices.
- Added AnimatedTaskView helper view to animate previous task view.
- Added animation from single previous app to two peaking previous apps.
- Updated app orientation to work with larger screens (forced protrait was causing letterboxing).

Test: manual, tested tutorial on non-foldable device, foldable device (closed, opened in landscape, opened in portait and switching between the three)
Bug: 198434693
Bug: 187701213
Known issue: 200579260

Change-Id: I98cb269f7d25985989cf66e791bf535228ab5293
This commit is contained in:
Schneider Victor-tulias
2021-09-27 19:55:53 +00:00
parent 03be53443d
commit ab60c1fc6e
10 changed files with 320 additions and 40 deletions
@@ -0,0 +1,202 @@
/*
* Copyright (C) 2021 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.interaction;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
import java.util.ArrayList;
/**
* Helper View for the gesture tutorial mock previous app task view.
*
* This helper class allows animating from a single-row layout to a two-row layout as seen in
* large screen devices.
*/
public class AnimatedTaskView extends ConstraintLayout {
private View mFullTaskView;
private CardView mTopTaskView;
private CardView mBottomTaskView;
private ViewOutlineProvider mTaskViewOutlineProvider = null;
private final Rect mTaskViewAnimatedRect = new Rect();
private float mTaskViewAnimatedRadius;
public AnimatedTaskView(@NonNull Context context) {
super(context);
}
public AnimatedTaskView(@NonNull Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
}
public AnimatedTaskView(
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public AnimatedTaskView(
@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFullTaskView = findViewById(R.id.full_task_view);
mTopTaskView = findViewById(R.id.top_task_view);
mBottomTaskView = findViewById(R.id.bottom_task_view);
setToSingleRowLayout(false);
}
AnimatorSet createAnimationToMultiRowLayout() {
if (mTaskViewOutlineProvider == null) {
// This is an illegal state.
return null;
}
Outline startOutline = new Outline();
mTaskViewOutlineProvider.getOutline(this, startOutline);
Rect outlineStartRect = new Rect();
startOutline.getRect(outlineStartRect);
int endRectBottom = mTopTaskView.getHeight();
float outlineStartRadius = startOutline.getRadius();
float outlineEndRadius = getContext().getResources().getDimensionPixelSize(
R.dimen.gesture_tutorial_small_task_view_corner_radius);
ValueAnimator outlineAnimator = ValueAnimator.ofFloat(0f, 1f);
outlineAnimator.addUpdateListener(valueAnimator -> {
float progress = (float) valueAnimator.getAnimatedValue();
mTaskViewAnimatedRect.bottom = (int) (outlineStartRect.bottom
+ progress * (endRectBottom - outlineStartRect.bottom));
mTaskViewAnimatedRadius = outlineStartRadius
+ progress * (outlineEndRadius - outlineStartRadius);
mFullTaskView.invalidateOutline();
});
outlineAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mTaskViewAnimatedRect.set(outlineStartRect);
mTaskViewAnimatedRadius = outlineStartRadius;
mFullTaskView.setClipToOutline(true);
mFullTaskView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(mTaskViewAnimatedRect, mTaskViewAnimatedRadius);
}
});
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mFullTaskView.setOutlineProvider(mTaskViewOutlineProvider);
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mFullTaskView.setOutlineProvider(mTaskViewOutlineProvider);
}
});
ArrayList<Animator> animations = new ArrayList<>();
animations.add(ObjectAnimator.ofFloat(
mBottomTaskView, View.TRANSLATION_X, -mBottomTaskView.getWidth(), 0));
animations.add(outlineAnimator);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animations);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
setToSingleRowLayout(true);
setPadding(0, outlineStartRect.top, 0, getHeight() - outlineStartRect.bottom);
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
setToMultiRowLayout();
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
setToMultiRowLayout();
}
});
return animatorSet;
}
void setToSingleRowLayout(boolean forAnimation) {
mFullTaskView.setVisibility(VISIBLE);
mTopTaskView.setVisibility(INVISIBLE);
mBottomTaskView.setVisibility(forAnimation ? VISIBLE : INVISIBLE);
}
void setToMultiRowLayout() {
mFullTaskView.setVisibility(INVISIBLE);
mTopTaskView.setVisibility(VISIBLE);
mBottomTaskView.setVisibility(VISIBLE);
}
void setFakeTaskViewFillColor(@ColorInt int colorResId) {
mFullTaskView.setBackgroundColor(colorResId);
mTopTaskView.setCardBackgroundColor(colorResId);
mBottomTaskView.setCardBackgroundColor(colorResId);
}
@Override
public void setClipToOutline(boolean clipToOutline) {
mFullTaskView.setClipToOutline(clipToOutline);
}
@Override
public void setOutlineProvider(ViewOutlineProvider provider) {
mTaskViewOutlineProvider = provider;
mFullTaskView.setOutlineProvider(mTaskViewOutlineProvider);
}
}
@@ -17,6 +17,7 @@ package com.android.quickstep.interaction;
import static com.android.launcher3.anim.Interpolators.ACCEL;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.annotation.TargetApi;
import android.graphics.PointF;
@@ -29,6 +30,8 @@ import com.android.quickstep.SwipeUpAnimationLogic;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
import java.util.ArrayList;
/** A {@link TutorialController} for the Overview tutorial. */
@TargetApi(Build.VERSION_CODES.R)
final class OverviewGestureTutorialController extends SwipeUpGestureTutorialController {
@@ -123,10 +126,24 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont
}
public void animateTaskViewToOverview() {
PendingAnimation anim = new PendingAnimation(300);
PendingAnimation anim = new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
anim.setFloat(mTaskViewSwipeUpAnimation
.getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
AnimatorSet animset = anim.buildAnim();
ArrayList<Animator> animators = new ArrayList<>();
if (mTutorialFragment.isLargeScreen()) {
Animator multiRowAnimation = mFakePreviousTaskView.createAnimationToMultiRowLayout();
if (multiRowAnimation != null) {
multiRowAnimation.setDuration(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
animators.add(multiRowAnimation);
}
}
animators.add(anim.buildAnim());
AnimatorSet animset = new AnimatorSet();
animset.playTogether(animators);
animset.start();
mRunningWindowAnim = SwipeUpAnimationLogic.RunningWindowAnim.wrap(animset);
}
@@ -63,6 +63,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController {
private static final int FAKE_PREVIOUS_TASK_MARGIN = Utilities.dpToPx(12);
protected static final long TASK_VIEW_END_ANIMATION_DURATION_MILLIS = 300;
private static final long HOME_SWIPE_ANIMATION_DURATION_MILLIS = 625;
private static final long OVERVIEW_SWIPE_ANIMATION_DURATION_MILLIS = 1000;
@@ -89,6 +90,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController {
mFakeTaskView.setAlpha(1);
mFakePreviousTaskView.setVisibility(View.INVISIBLE);
mFakePreviousTaskView.setAlpha(1);
mFakePreviousTaskView.setToSingleRowLayout(false);
mShowTasks = false;
mShowPreviousTasks = false;
mRunningWindowAnim = null;
@@ -146,7 +148,8 @@ abstract class SwipeUpGestureTutorialController extends TutorialController {
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
PendingAnimation fadeAnim = new PendingAnimation(300);
PendingAnimation fadeAnim =
new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
if (reset) {
fadeAnim.setFloat(mTaskViewSwipeUpAnimation
.getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
@@ -159,6 +162,23 @@ abstract class SwipeUpGestureTutorialController extends TutorialController {
fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable));
}
AnimatorSet animset = fadeAnim.buildAnim();
if (reset && mTutorialFragment.isLargeScreen()) {
animset.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
Animator multiRowAnimation =
mFakePreviousTaskView.createAnimationToMultiRowLayout();
if (multiRowAnimation != null) {
multiRowAnimation.setDuration(
TASK_VIEW_END_ANIMATION_DURATION_MILLIS).start();
}
}
});
}
animset.setStartDelay(100);
animset.start();
mRunningWindowAnim = RunningWindowAnim.wrap(animset);
@@ -301,7 +321,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController {
public RectF getWindowTargetRect() {
int fakeHomeIconSizePx = Utilities.dpToPx(60);
int fakeHomeIconLeft = mFakeHotseatView.getLeft();
int fakeHomeIconTop = mDp.heightPx - Utilities.dpToPx(216);
int fakeHomeIconTop = mFakeHotseatView.getTop();
return new RectF(fakeHomeIconLeft, fakeHomeIconTop,
fakeHomeIconLeft + fakeHomeIconSizePx,
fakeHomeIconTop + fakeHomeIconSizePx);
@@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.ColorRes;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.AnimatedVectorDrawable;
@@ -87,7 +88,7 @@ abstract class TutorialController implements BackGestureAttemptCallback,
final ImageView mFakeHotseatView;
final ClipIconView mFakeIconView;
final FrameLayout mFakeTaskView;
final View mFakePreviousTaskView;
final AnimatedTaskView mFakePreviousTaskView;
final View mRippleView;
final RippleDrawable mRippleDrawable;
final Button mActionButton;
@@ -177,9 +178,9 @@ abstract class TutorialController implements BackGestureAttemptCallback,
return View.NO_ID;
}
@DrawableRes
protected int getMockPreviousAppTaskThumbnailResId() {
return R.drawable.default_sandbox_app_previous_task_thumbnail;
@ColorRes
protected int getMockPreviousAppTaskThumbnailColorResId() {
return R.color.gesture_tutorial_fake_previous_task_view_color;
}
@DrawableRes
@@ -442,8 +443,8 @@ abstract class TutorialController implements BackGestureAttemptCallback,
updateFakeAppTaskViewLayout(getMockAppTaskLayoutResId());
mFakeTaskView.animate().alpha(1).setListener(
AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
mFakePreviousTaskView.setBackground(AppCompatResources.getDrawable(
mContext, getMockPreviousAppTaskThumbnailResId()));
mFakePreviousTaskView.setFakeTaskViewFillColor(mContext.getResources().getColor(
getMockPreviousAppTaskThumbnailColorResId()));
mFakeIconView.setBackground(AppCompatResources.getDrawable(
mContext, getMockAppIconResId()));
}
@@ -20,6 +20,7 @@ import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Insets;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
@@ -40,6 +41,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
import com.android.quickstep.interaction.TutorialController.TutorialType;
@@ -63,6 +65,8 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener {
private boolean mFragmentStopped = false;
private boolean mIsLargeScreen;
public static TutorialFragment newInstance(TutorialType tutorialType) {
TutorialFragment fragment = getFragmentForTutorialType(tutorialType);
if (fragment == null) {
@@ -130,6 +134,21 @@ abstract class TutorialFragment extends Fragment implements OnTouchListener {
mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
mNavBarGestureHandler = new NavBarGestureHandler(getContext());
mIsLargeScreen = InvariantDeviceProfile.INSTANCE.get(getContext())
.getDeviceProfile(getContext()).isTablet;
if (mIsLargeScreen) {
((Activity) getContext()).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
} else {
// Temporary until UI mocks for landscape mode for phones are created.
((Activity) getContext()).setRequestedOrientation(
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
public boolean isLargeScreen() {
return mIsLargeScreen;
}
@Override