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));