diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml index bc8bd3ad4b..124cd57bce 100644 --- a/quickstep/AndroidManifest.xml +++ b/quickstep/AndroidManifest.xml @@ -100,7 +100,6 @@ diff --git a/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml b/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml deleted file mode 100644 index 9c9549779e..0000000000 --- a/quickstep/res/drawable/default_sandbox_app_previous_task_thumbnail.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml index cfb3eb07ec..0f0119071e 100644 --- a/quickstep/res/layout/gesture_tutorial_fragment.xml +++ b/quickstep/res/layout/gesture_tutorial_fragment.xml @@ -41,13 +41,54 @@ android:layout_height="20dp" android:visibility="invisible" /> - + android:visibility="invisible"> + + + + + + + + + app:layout_constraintStart_toStartOf="parent"/> + app:layout_constraintStart_toStartOf="parent"/> 16dp 24dp 18dp + 72dp + 18dp 40dp diff --git a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java new file mode 100644 index 0000000000..53ad138b6a --- /dev/null +++ b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java @@ -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 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); + } +} diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java index ff1743f726..9d60e1bab6 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java @@ -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 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); } diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java index 30430ff3f4..0c7b35b1c6 100644 --- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java @@ -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); diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index ef62fd6704..94fb55621e 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -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())); } diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java index 1d78c6bcca..89be1a6190 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java @@ -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