Add Arrows to Folder PageIndicatorDots for Accessibility Purposes.
Bug: 383974843
Test: Verified via go/web-hv that the hitbox sizes were correct. Measured the width and height within the folder for 1 through 7 pages, and everything looked great.Change title worked, and pressing on arrows to change page worked great.
Flag: com.android.launcher3.enable_launcher_visual_refresh
Change-Id: Ic095b2abe330544882467fb4803724e8d50a1798
diff --git a/quickstep/res/drawable/ic_chevron_end.xml b/res/drawable/ic_chevron_end.xml
similarity index 100%
rename from quickstep/res/drawable/ic_chevron_end.xml
rename to res/drawable/ic_chevron_end.xml
diff --git a/quickstep/res/drawable/ic_chevron_start.xml b/res/drawable/ic_chevron_start.xml
similarity index 100%
rename from quickstep/res/drawable/ic_chevron_start.xml
rename to res/drawable/ic_chevron_start.xml
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index bebe1a4..0963421 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -47,6 +47,7 @@
import com.android.launcher3.model.data.AppPairInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pageindicators.Direction;
import com.android.launcher3.pageindicators.PageIndicatorDots;
import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import com.android.launcher3.util.Thunk;
@@ -129,6 +130,8 @@
public void setFolder(Folder folder) {
mFolder = folder;
mPageIndicator = folder.findViewById(R.id.folder_page_indicator);
+ mPageIndicator.setArrowClickListener(direction -> snapToPageImmediately(
+ (Direction.END == direction) ? mCurrentPage + 1 : mCurrentPage - 1));
initParentViews(folder);
}
diff --git a/src/com/android/launcher3/pageindicators/PageIndicator.java b/src/com/android/launcher3/pageindicators/PageIndicator.java
index 0640bf3..a6f76c4 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicator.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.pageindicators;
+import java.util.function.Consumer;
+
/**
* Base class for a page indicator.
*/
@@ -27,6 +29,14 @@
void setMarkersCount(int numMarkers);
/**
+ * This is only going to be used by the FolderPagedView's PageIndicator. A refactor is planned
+ * to separate the two purposes of this class, but in the meantime, this indicator will serve to
+ * let the folder snap to the page of its click, and also tell the PageIndicator not to draw
+ * arrows if the click listener is null (at least until after this is refactored).
+ */
+ void setArrowClickListener(Consumer<Direction> listener);
+
+ /**
* Sets a flag indicating whether to pause scroll.
* <p>Should be set to {@code true} while the screen is binding or new data is being applied,
* and to {@code false} once done. This prevents animation conflicts due to scrolling during
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt b/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt
new file mode 100644
index 0000000..970d210
--- /dev/null
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorArrowClickListener.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2025 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.launcher3.pageindicators
+
+interface PageIndicatorArrowClickListener {
+ fun onArrowClick(direction: Direction)
+}
+
+enum class Direction {
+ END,
+ START,
+}
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index 37f5189..f0c9bd9 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -32,11 +32,13 @@
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.IntProperty;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewOutlineProvider;
@@ -51,6 +53,8 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.util.Themes;
+import java.util.function.Consumer;
+
/**
* {@link PageIndicator} which shows dots per page. The active page is shown with the current
* accent color.
@@ -68,6 +72,11 @@
private static final int ENTER_ANIMATION_STAGGERED_DELAY = 150;
private static final int ENTER_ANIMATION_DURATION = 400;
+ private static final int LARGE_HEIGHT_MULTIPLIER = 12;
+ private static final int SMALL_HEIGHT_MULTIPLIER = 4;
+ private static final int LARGE_WIDTH_MULTIPLIER = 5;
+ private static final int SMALL_WIDTH_MULTIPLIER = 3;
+
private static final int PAGE_INDICATOR_ALPHA = 255;
private static final int DOT_ALPHA = 128;
private static final float DOT_ALPHA_FRACTION = 0.5f;
@@ -75,6 +84,7 @@
private static final int VISIBLE_ALPHA = 255;
private static final int INVISIBLE_ALPHA = 0;
private Paint mPaginationPaint;
+ private Consumer<Direction> mOnArrowClickListener;
// This value approximately overshoots to 1.5 times the original size.
private static final float ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f;
@@ -99,23 +109,27 @@
private static final IntProperty<PageIndicatorDots> PAGINATION_ALPHA =
new IntProperty<PageIndicatorDots>("pagination_alpha") {
- @Override
- public Integer get(PageIndicatorDots obj) {
- return obj.mPaginationPaint.getAlpha();
- }
+ @Override
+ public Integer get(PageIndicatorDots obj) {
+ return obj.mPaginationPaint.getAlpha();
+ }
- @Override
- public void setValue(PageIndicatorDots obj, int alpha) {
- obj.mPaginationPaint.setAlpha(alpha);
- obj.invalidate();
- }
- };
+ @Override
+ public void setValue(PageIndicatorDots obj, int alpha) {
+ obj.mPaginationPaint.setAlpha(alpha);
+ obj.invalidate();
+ }
+ };
private final Handler mDelayedPaginationFadeHandler = new Handler(Looper.getMainLooper());
private final float mDotRadius;
private final float mGapWidth;
private final float mCircleGap;
private final boolean mIsRtl;
+ private final VectorDrawable mArrowEnd;
+ private final VectorDrawable mArrowStart;
+ private final Rect mArrowEndBounds = new Rect();
+ private final Rect mArrowStartBounds = new Rect();
private int mNumPages;
private int mActivePage;
@@ -167,6 +181,8 @@
: DOT_GAP_FACTOR * mDotRadius;
setOutlineProvider(new MyOutlineProver());
mIsRtl = Utilities.isRtl(getResources());
+ mArrowEnd = (VectorDrawable) getResources().getDrawable(R.drawable.ic_chevron_end);
+ mArrowStart = (VectorDrawable) getResources().getDrawable(R.drawable.ic_chevron_start);
}
@Override
@@ -405,6 +421,11 @@
}
@Override
+ public void setArrowClickListener(Consumer<Direction> listener) {
+ mOnArrowClickListener = listener;
+ }
+
+ @Override
public void setPauseScroll(boolean pause, boolean isTwoPanels) {
mIsTwoPanels = isTwoPanels;
@@ -419,11 +440,16 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO(b/394355070): Verify Folder Entry Animation works correctly with visual updates
- // Add extra spacing of mDotRadius on all sides so than entry animation could be run.
+ // Add extra spacing of mDotRadius on all sides so than entry animation could be run
+ // and so the hitboxes of arrows can be clicked easier.
int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ?
- MeasureSpec.getSize(widthMeasureSpec) : (int) ((mNumPages * 3 + 2) * mDotRadius);
+ MeasureSpec.getSize(widthMeasureSpec)
+ : (int) ((mNumPages * ((enableLauncherVisualRefresh())
+ ? LARGE_WIDTH_MULTIPLIER : SMALL_WIDTH_MULTIPLIER) + 2) * mDotRadius);
int height = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
- ? MeasureSpec.getSize(heightMeasureSpec) : (int) (4 * mDotRadius);
+ ? MeasureSpec.getSize(heightMeasureSpec)
+ : (int) (((enableLauncherVisualRefresh())
+ ? LARGE_HEIGHT_MULTIPLIER : SMALL_HEIGHT_MULTIPLIER) * mDotRadius);
setMeasuredDimension(width, height);
}
@@ -475,6 +501,18 @@
float bounceProgress = (posDif > 1) ? posDif - 1 : 0;
float bounceAdjustment = Math.abs(mCurrentPosition - boundedPosition) * diameter;
+ if (mOnArrowClickListener != null && boundedPosition >= 1) {
+ // Here we draw the Left Arrow
+ mArrowStart.setAlpha(alpha);
+ int size = (int) (mGapWidth * 4);
+ mArrowStartBounds.left = (int) (sTempRect.left - mGapWidth - size);
+ mArrowStartBounds.top = (int) (y - size / 2);
+ mArrowStartBounds.right = (int) (sTempRect.left - mGapWidth);
+ mArrowStartBounds.bottom = (int) (y + size / 2);
+ mArrowStart.setBounds(mArrowStartBounds);
+ mArrowStart.draw(canvas);
+ }
+
// Here we draw the dots, one at a time from the left-most dot to the right-most dot
// 1.0 => 000000 000000111111 000000
// 1.3 => 000000 0000001111 11000000
@@ -520,6 +558,18 @@
// TODO(b/394355070) Verify RTL experience works correctly with visual updates
sTempRect.left = sTempRect.right + mGapWidth;
}
+
+ if (mOnArrowClickListener != null && boundedPosition <= mNumPages - 2) {
+ // Here we draw the Right Arrow
+ mArrowEnd.setAlpha(alpha);
+ int size = (int) (mGapWidth * 4);
+ mArrowEndBounds.left = (int) sTempRect.left;
+ mArrowEndBounds.top = (int) (y - size / 2);
+ mArrowEndBounds.right = (int) (int) (sTempRect.left + size);
+ mArrowEndBounds.bottom = (int) (y + size / 2);
+ mArrowEnd.setBounds(mArrowEndBounds);
+ mArrowEnd.draw(canvas);
+ }
} else {
// Here we draw the dots
mPaginationPaint.setAlpha((int) (alpha * DOT_ALPHA_FRACTION));
@@ -538,6 +588,34 @@
}
}
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mOnArrowClickListener == null) {
+ // No - Op. Don't care about touch events
+ } else if (withinExpandedBounds(mArrowStartBounds, ev)) {
+ mOnArrowClickListener.accept(Direction.START);
+ } else if (withinExpandedBounds(mArrowEndBounds, ev)) {
+ mOnArrowClickListener.accept(Direction.END);
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ // For larger Touch box
+ private boolean withinExpandedBounds(Rect rect, MotionEvent ev) {
+ Rect scaledRect = new Rect();
+ scaledRect.set(rect);
+
+ float verticalAdjustment = (scaledRect.bottom - scaledRect.top) * 2;
+ scaledRect.top -= verticalAdjustment;
+ scaledRect.bottom += verticalAdjustment;
+
+ float horizontalAdjustment = (scaledRect.right - scaledRect.left) * 2;
+ scaledRect.left -= horizontalAdjustment;
+ scaledRect.right += horizontalAdjustment;
+
+ return scaledRect.contains((int) ev.getX(), (int) ev.getY());
+ }
+
private RectF getActiveRect() {
float startCircle = (int) mCurrentPosition;
float delta = mCurrentPosition - startCircle;
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
index e94f3a0..185c3ee 100644
--- a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -26,9 +26,12 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
+import com.android.launcher3.pageindicators.Direction;
import com.android.launcher3.pageindicators.PageIndicator;
import com.android.launcher3.views.ActivityContext;
+import java.util.function.Consumer;
+
/**
* Supports two indicator colors, dedicated for personal and work tabs.
*/
@@ -78,6 +81,11 @@
}
@Override
+ public void setArrowClickListener(Consumer<Direction> listener) {
+ // No-Op. All Apps doesn't need accessibility arrows for single click navigation.
+ }
+
+ @Override
public boolean hasOverlappingRendering() {
return false;
}