diff --git a/quickstep/res/color/keyboard_quick_switch_scroll_button_bg.xml b/quickstep/res/color/keyboard_quick_switch_scroll_button_bg.xml new file mode 100644 index 0000000000..1592055b2c --- /dev/null +++ b/quickstep/res/color/keyboard_quick_switch_scroll_button_bg.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/quickstep/res/color/keyboard_quick_switch_scroll_button_fg.xml b/quickstep/res/color/keyboard_quick_switch_scroll_button_fg.xml new file mode 100644 index 0000000000..051c18fbde --- /dev/null +++ b/quickstep/res/color/keyboard_quick_switch_scroll_button_fg.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/quickstep/res/color/keyboard_quick_switch_scroll_button_icon.xml b/quickstep/res/color/keyboard_quick_switch_scroll_button_icon.xml new file mode 100644 index 0000000000..74df84babe --- /dev/null +++ b/quickstep/res/color/keyboard_quick_switch_scroll_button_icon.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/quickstep/res/drawable/bg_keyboard_quick_switch_scroll_button.xml b/quickstep/res/drawable/bg_keyboard_quick_switch_scroll_button.xml new file mode 100644 index 0000000000..7067f131a9 --- /dev/null +++ b/quickstep/res/drawable/bg_keyboard_quick_switch_scroll_button.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/quickstep/res/drawable/fg_keyboard_quick_switch_scroll_button.xml b/quickstep/res/drawable/fg_keyboard_quick_switch_scroll_button.xml new file mode 100644 index 0000000000..dd63f54e72 --- /dev/null +++ b/quickstep/res/drawable/fg_keyboard_quick_switch_scroll_button.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/quickstep/res/drawable/ic_chevron_end.xml b/quickstep/res/drawable/ic_chevron_end.xml new file mode 100644 index 0000000000..9ca4f3a3b4 --- /dev/null +++ b/quickstep/res/drawable/ic_chevron_end.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/quickstep/res/drawable/ic_chevron_start.xml b/quickstep/res/drawable/ic_chevron_start.xml new file mode 100644 index 0000000000..913da026da --- /dev/null +++ b/quickstep/res/drawable/ic_chevron_start.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml index 345b97c400..885bdb9f3b 100644 --- a/quickstep/res/layout/keyboard_quick_switch_view.xml +++ b/quickstep/res/layout/keyboard_quick_switch_view.xml @@ -67,6 +67,24 @@ + + + app:layout_constraintStart_toEndOf="@id/scroll_button_start" + app:layout_constraintEnd_toStartOf="@id/scroll_button_end"> + + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 52ebdae31c..6196be42fa 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -530,6 +530,11 @@ 360dp 16dp 20dp + 36dp + 56dp + 12dp + 32dp + 18dp 48dp diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 8e70a2b835..959908eb83 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -335,6 +335,13 @@ %1$s and %2$s + + Scroll left + + Scroll right + Bubble diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java index 4b4d68dd23..336ef48c64 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java @@ -30,10 +30,12 @@ import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.widget.HorizontalScrollView; +import android.widget.ImageButton; import android.widget.TextView; import android.window.OnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher; @@ -45,6 +47,7 @@ import androidx.constraintlayout.widget.ConstraintLayout; import com.android.app.animation.Interpolators; import com.android.internal.jank.Cuj; +import com.android.launcher3.Flags; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; @@ -102,8 +105,12 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { private HorizontalScrollView mScrollView; private ConstraintLayout mContent; - private int mTaskViewWidth; - private int mTaskViewHeight; + private boolean mSupportsScrollArrows = false; + private ImageButton mStartScrollArrow; + private ImageButton mEndScrollArrow; + + private int mTaskViewBorderWidth; + private int mTaskViewRadius; private int mSpacing; private int mSmallSpacing; private int mOutlineRadius; @@ -112,11 +119,13 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { private int mOverviewTaskIndex = -1; private int mDesktopTaskIndex = -1; - @Nullable private AnimatorSet mOpenAnimation; + @Nullable + private AnimatorSet mOpenAnimation; private boolean mIsBackCallbackRegistered = false; - @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks; + @Nullable + private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks; public KeyboardQuickSwitchView(@NonNull Context context) { this(context, null); @@ -152,18 +161,35 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { mNoRecentItemsPane = findViewById(R.id.no_recent_items_pane); mScrollView = findViewById(R.id.scroll_view); mContent = findViewById(R.id.content); + mStartScrollArrow = findViewById(R.id.scroll_button_start); + mEndScrollArrow = findViewById(R.id.scroll_button_end); + + setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); Resources resources = getResources(); - mTaskViewWidth = resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_taskview_width); - mTaskViewHeight = resources.getDimensionPixelSize( - R.dimen.keyboard_quick_switch_taskview_height); mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing); mSmallSpacing = resources.getDimensionPixelSize( R.dimen.keyboard_quick_switch_view_small_spacing); mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius); + mTaskViewBorderWidth = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_border_width); + mTaskViewRadius = resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_task_view_radius); + mIsRtl = Utilities.isRtl(resources); + if (Flags.taskbarOverflow()) { + initializeScrollArrows(); + + if (mIsRtl) { + mStartScrollArrow.setContentDescription( + resources.getString(R.string.quick_switch_scroll_arrow_right)); + mEndScrollArrow.setContentDescription( + resources.getString(R.string.quick_switch_scroll_arrow_left)); + } + } + + TypefaceUtils.setTypeface( mNoRecentItemsPane.findViewById(R.id.no_recent_items_text), TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE); @@ -331,6 +357,78 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { }); } + private void initializeScrollArrows() { + mSupportsScrollArrows = true; + + mStartScrollArrow.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mIsRtl) { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(mScrollView.getWidth(), 0); + }); + } else { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0); + }); + } + } + }); + + mEndScrollArrow.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mIsRtl) { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(-mScrollView.getWidth(), 0); + }); + } else { + runScrollCommand(false, () -> { + mScrollView.smoothScrollBy(mScrollView.getWidth(), 0); + }); + } + } + }); + + // Add listeners to disable arrow buttons when the scroll view cannot be further scrolled in + // the associated direction. + mScrollView.setOnScrollChangeListener(new OnScrollChangeListener() { + @Override + public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, + int oldScrollY) { + updateArrowButtonsEnabledState(); + } + }); + + // Update scroll view outline to clip its contents with rounded corners. + mScrollView.setClipToOutline(true); + mScrollView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + int spacingWithoutBorder = mSpacing - mTaskViewBorderWidth; + outline.setRoundRect(spacingWithoutBorder, + spacingWithoutBorder, view.getWidth() - spacingWithoutBorder, + view.getHeight() - spacingWithoutBorder, + mTaskViewRadius); + } + }); + } + + private void updateArrowButtonsEnabledState() { + if (!mDisplayingRecentTasks) { + return; + } + + int scrollX = mScrollView.getScrollX(); + if (mIsRtl) { + mEndScrollArrow.setEnabled(scrollX > 0); + mStartScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth()); + } else { + mStartScrollArrow.setEnabled(scrollX > 0); + mEndScrollArrow.setEnabled(scrollX < mContent.getWidth() - mScrollView.getWidth()); + } + } + int getOverviewTaskIndex() { return mOverviewTaskIndex; } @@ -346,6 +444,21 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { mViewCallbacks = null; } + private void animateDisplayedContentForClose(View view, AnimatorSet animator) { + Animator translationYAnimation = ObjectAnimator.ofFloat( + view, + TRANSLATION_Y, + 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP)); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR); + animator.play(translationYAnimation); + + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 1f, 0f); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + animator.play(contentAlphaAnimation); + + } + protected Animator getCloseAnimation() { AnimatorSet closeAnimation = new AnimatorSet(); @@ -360,17 +473,11 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { closeAnimation.play(alphaAnimation); View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane; - Animator translationYAnimation = ObjectAnimator.ofFloat( - displayedContent, - TRANSLATION_Y, - 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP)); - translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); - translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR); - closeAnimation.play(translationYAnimation); - - Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 1f, 0f); - contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); - closeAnimation.play(contentAlphaAnimation); + animateDisplayedContentForClose(displayedContent, closeAnimation); + if (mSupportsScrollArrows) { + animateDisplayedContentForClose(mStartScrollArrow, closeAnimation); + animateDisplayedContentForClose(mEndScrollArrow, closeAnimation); + } closeAnimation.addListener(new AnimatorListenerAdapter() { @Override @@ -385,6 +492,31 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { return closeAnimation; } + private void animateDisplayedContentForOpen(View view, AnimatorSet animator) { + Animator translationXAnimation = ObjectAnimator.ofFloat( + view, + TRANSLATION_X, + -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0); + translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS); + translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR); + animator.play(translationXAnimation); + + Animator translationYAnimation = ObjectAnimator.ofFloat( + view, + TRANSLATION_Y, + -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0); + translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); + translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR); + animator.play(translationYAnimation); + + view.setAlpha(0.0f); + Animator contentAlphaAnimation = ObjectAnimator.ofFloat(view, ALPHA, 0f, + 1f); + contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS); + contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); + animator.play(contentAlphaAnimation); + } + protected void animateOpen(int currentFocusIndexOverride) { if (mOpenAnimation != null) { // Restart animation since currentFocusIndexOverride can change the initial scroll. @@ -407,26 +539,12 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { mOpenAnimation.play(alphaAnimation); View displayedContent = mDisplayingRecentTasks ? mScrollView : mNoRecentItemsPane; - Animator translationXAnimation = ObjectAnimator.ofFloat( - displayedContent, - TRANSLATION_X, - -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0); - translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS); - translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR); - mOpenAnimation.play(translationXAnimation); + animateDisplayedContentForOpen(displayedContent, mOpenAnimation); + if (mSupportsScrollArrows) { + animateDisplayedContentForOpen(mStartScrollArrow, mOpenAnimation); + animateDisplayedContentForOpen(mEndScrollArrow, mOpenAnimation); + } - Animator translationYAnimation = ObjectAnimator.ofFloat( - displayedContent, - TRANSLATION_Y, - -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0); - translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS); - translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR); - mOpenAnimation.play(translationYAnimation); - - Animator contentAlphaAnimation = ObjectAnimator.ofFloat(displayedContent, ALPHA, 0f, 1f); - contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS); - contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS); - mOpenAnimation.play(contentAlphaAnimation); ViewOutlineProvider outlineProvider = getOutlineProvider(); mOpenAnimation.addListener(new AnimatorListenerAdapter() { @@ -461,6 +579,27 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { OPEN_OUTLINE_INTERPOLATOR)); } }); + + if (mSupportsScrollArrows) { + mScrollView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + if (mScrollView.getWidth() == 0) { + return; + } + + if (mContent.getWidth() > mScrollView.getWidth()) { + mStartScrollArrow.setVisibility(VISIBLE); + mEndScrollArrow.setVisibility(VISIBLE); + updateArrowButtonsEnabledState(); + } + mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener( + this); + } + }); + } + animateFocusMove(-1, Math.min( getTaskCount() - 1, currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride));