Visual updates for PageDotIndicators

TODOs have been added for verifying and updating the Outline, right to left mode,
and the entry animation. This CL is complex, and those have been left out on purpose.

Bug: 394355070
Test: Verified this behavior looks good in LTR on the workspace.
Flag: com.android.launcher3.enable_launcher_visual_refresh
Change-Id: I8f0192f461d344360572411e1a62921fd65ba600
This commit is contained in:
Stefan Andonian
2025-02-25 09:40:22 -08:00
parent c7e545674d
commit ae8ab4c403
2 changed files with 133 additions and 40 deletions
+1
View File
@@ -307,6 +307,7 @@
<!-- Folders -->
<dimen name="page_indicator_dot_size">6dp</dimen>
<dimen name="page_indicator_gap_width">4dp</dimen>
<dimen name="page_indicator_size">10dp</dimen>
@@ -16,6 +16,7 @@
package com.android.launcher3.pageindicators;
import static com.android.launcher3.Flags.enableLauncherVisualRefresh;
import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
import android.animation.Animator;
@@ -57,8 +58,8 @@ import com.android.launcher3.util.Themes;
public class PageIndicatorDots extends View implements Insettable, PageIndicator {
private static final float SHIFT_PER_ANIMATION = 0.5f;
private static final float SHIFT_THRESHOLD = 0.1f;
private static final long ANIMATION_DURATION = 150;
private static final float SHIFT_THRESHOLD = (enableLauncherVisualRefresh() ? 0.5f : 0.2f);
private static final long ANIMATION_DURATION = (enableLauncherVisualRefresh() ? 200 : 150);
private static final int PAGINATION_FADE_DELAY = ViewConfiguration.getScrollDefaultDelay();
private static final int PAGINATION_FADE_IN_DURATION = 83;
private static final int PAGINATION_FADE_OUT_DURATION = 167;
@@ -78,6 +79,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
// This value approximately overshoots to 1.5 times the original size.
private static final float ENTER_ANIMATION_OVERSHOOT_TENSION = 4.9f;
// This is used to optimize the onDraw method by not constructing a new RectF each draw.
private static final RectF sTempRect = new RectF();
private static final FloatProperty<PageIndicatorDots> CURRENT_POSITION =
@@ -93,7 +95,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
obj.invalidate();
obj.invalidateOutline();
}
};
};
private static final IntProperty<PageIndicatorDots> PAGINATION_ALPHA =
new IntProperty<PageIndicatorDots>("pagination_alpha") {
@@ -111,6 +113,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
private final Handler mDelayedPaginationFadeHandler = new Handler(Looper.getMainLooper());
private final float mDotRadius;
private final float mGapWidth;
private final float mCircleGap;
private final boolean mIsRtl;
@@ -130,6 +133,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
* 1.0 => Active dot is at position 1
*/
private float mCurrentPosition;
private int mLastPosition;
private float mFinalPosition;
private boolean mIsScrollPaused;
@VisibleForTesting
@@ -157,7 +161,10 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
mPaginationPaint.setStyle(Style.FILL);
mPaginationPaint.setColor(Themes.getAttrColor(context, R.attr.pageIndicatorDotColor));
mDotRadius = getResources().getDimension(R.dimen.page_indicator_dot_size) / 2;
mCircleGap = DOT_GAP_FACTOR * mDotRadius;
mGapWidth = getResources().getDimension(R.dimen.page_indicator_gap_width);
mCircleGap = (enableLauncherVisualRefresh())
? mDotRadius * 2 + mGapWidth
: DOT_GAP_FACTOR * mDotRadius;
setOutlineProvider(new MyOutlineProver());
mIsRtl = Utilities.isRtl(getResources());
}
@@ -188,29 +195,40 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
mTotalScroll = totalScroll;
int scrollPerPage = totalScroll / (mNumPages - 1);
int pageToLeft = scrollPerPage == 0 ? 0 : currentScroll / scrollPerPage;
int pageToLeftScroll = pageToLeft * scrollPerPage;
int pageToRightScroll = pageToLeftScroll + scrollPerPage;
if (enableLauncherVisualRefresh()) {
float scrollPerPage = (float) totalScroll / (mNumPages - 1);
float position = currentScroll / scrollPerPage;
animateToPosition(Math.round(position));
float scrollThreshold = SHIFT_THRESHOLD * scrollPerPage;
if (currentScroll < pageToLeftScroll + scrollThreshold) {
// scroll is within the left page's threshold
animateToPosition(pageToLeft);
if (mShouldAutoHide) {
hideAfterDelay();
}
} else if (currentScroll > pageToRightScroll - scrollThreshold) {
// scroll is far enough from left page to go to the right page
animateToPosition(pageToLeft + 1);
if (mShouldAutoHide) {
float delta = Math.abs((int) position - position);
if (mShouldAutoHide && (delta < 0.1 || delta > 0.9)) {
hideAfterDelay();
}
} else {
// scroll is between left and right page
animateToPosition(pageToLeft + SHIFT_PER_ANIMATION);
if (mShouldAutoHide) {
mDelayedPaginationFadeHandler.removeCallbacksAndMessages(null);
int scrollPerPage = totalScroll / (mNumPages - 1);
int pageToLeft = scrollPerPage == 0 ? 0 : currentScroll / scrollPerPage;
int pageToLeftScroll = pageToLeft * scrollPerPage;
int pageToRightScroll = pageToLeftScroll + scrollPerPage;
float scrollThreshold = SHIFT_THRESHOLD * scrollPerPage;
if (currentScroll < pageToLeftScroll + scrollThreshold) {
// scroll is within the left page's threshold
animateToPosition(pageToLeft);
if (mShouldAutoHide) {
hideAfterDelay();
}
} else if (currentScroll > pageToRightScroll - scrollThreshold) {
// scroll is far enough from left page to go to the right page
animateToPosition(pageToLeft + 1);
if (mShouldAutoHide) {
hideAfterDelay();
}
} else {
// scroll is between left and right page
animateToPosition(pageToLeft + SHIFT_PER_ANIMATION);
if (mShouldAutoHide) {
mDelayedPaginationFadeHandler.removeCallbacksAndMessages(null);
}
}
}
}
@@ -283,15 +301,23 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
private void animateToPosition(float position) {
mFinalPosition = position;
if (Math.abs(mCurrentPosition - mFinalPosition) < SHIFT_THRESHOLD) {
if (!enableLauncherVisualRefresh()
&& Math.abs(mCurrentPosition - mFinalPosition) < SHIFT_THRESHOLD) {
mCurrentPosition = mFinalPosition;
}
if (mAnimator == null && Float.compare(mCurrentPosition, mFinalPosition) != 0) {
float positionForThisAnim = mCurrentPosition > mFinalPosition ?
mCurrentPosition - SHIFT_PER_ANIMATION : mCurrentPosition + SHIFT_PER_ANIMATION;
if (mAnimator == null && Float.compare(mCurrentPosition, position) != 0) {
float positionForThisAnim = enableLauncherVisualRefresh()
? position
: (mCurrentPosition > mFinalPosition
? mCurrentPosition - SHIFT_PER_ANIMATION
: mCurrentPosition + SHIFT_PER_ANIMATION);
mAnimator = ObjectAnimator.ofFloat(this, CURRENT_POSITION, positionForThisAnim);
mAnimator.addListener(new AnimationCycleListener());
mAnimator.setDuration(ANIMATION_DURATION);
if (enableLauncherVisualRefresh()) {
mLastPosition = (int) mCurrentPosition;
mAnimator.setInterpolator(new OvershootInterpolator());
}
mAnimator.start();
}
}
@@ -314,6 +340,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
invalidate();
}
// TODO(b/394355070): Verify Folder Entry Animation works correctly with visual updates
public void playEntryAnimation() {
int count = mEntryAnimationRadiusFactors.length;
if (count == 0) {
@@ -391,6 +418,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
@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.
int width = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY ?
MeasureSpec.getSize(widthMeasureSpec) : (int) ((mNumPages * 3 + 2) * mDotRadius);
@@ -410,17 +438,14 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
return;
}
// Draw all page indicators;
float circleGap = mCircleGap;
float startX = ((float) getWidth() / 2)
- (mCircleGap * (((float) mNumPages - 1) / 2))
- mDotRadius;
float x = startX + mDotRadius;
float x = ((float) getWidth() / 2) - (mCircleGap * ((float) mNumPages - 1) / 2);
float y = getHeight() / 2;
if (mEntryAnimationRadiusFactors != null) {
// During entry animation, only draw the circles
// TODO(b/394355070): Verify Folder Entry Animation works correctly - visual updates
if (mIsRtl) {
x = getWidth() - x;
circleGap = -circleGap;
@@ -432,18 +457,84 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
x += circleGap;
}
} else {
// Save the current alpha value, so we can reset to it again after drawing the dots
int alpha = mPaginationPaint.getAlpha();
// Here we draw the dots
mPaginationPaint.setAlpha((int) (alpha * DOT_ALPHA_FRACTION));
for (int i = 0; i < mNumPages; i++) {
canvas.drawCircle(x, y, mDotRadius, mPaginationPaint);
x += circleGap;
if (enableLauncherVisualRefresh()) {
int nonActiveAlpha = (int) (alpha * DOT_ALPHA_FRACTION);
float diameter = 2 * mDotRadius;
sTempRect.top = y - mDotRadius;
sTempRect.bottom = y + mDotRadius;
sTempRect.left = x - diameter;
float posDif = Math.abs(mLastPosition - mCurrentPosition);
float boundedPosition = (posDif > 1)
? Math.round(mCurrentPosition)
: mCurrentPosition;
float bounceProgress = (posDif > 1) ? posDif - 1 : 0;
float bounceAdjustment = Math.abs(mCurrentPosition - boundedPosition) * diameter;
// 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
// 1.6 => 000000 00000011 1111000000
// 2.0 => 000000 000000 111111000000
for (int i = 0; i < mNumPages; i++) {
mPaginationPaint.setAlpha(nonActiveAlpha);
float delta = Math.abs(boundedPosition - i);
if (delta <= SHIFT_THRESHOLD) {
mPaginationPaint.setAlpha(alpha);
}
// If boundedPosition is 3.3, both 3 and 4 should enter this condition.
// If boundedPosition is 3, only 3 should enter this condition.
if (delta < 1) {
sTempRect.right = sTempRect.left + diameter + ((1 - delta) * diameter);
// While the animation is shifting the active pagination dots size from
// the previously active one, to the newly active dot, there is no bounce
// adjustment. The bounce happens in the "Overshoot" phase of the animation.
// mLastPosition is used to determine when the currentPosition is just
// leaving the page, or if it is in the overshoot phase.
if (boundedPosition == i && bounceProgress != 0) {
if (mLastPosition < mCurrentPosition) {
sTempRect.left -= bounceAdjustment;
} else {
sTempRect.right += bounceAdjustment;
}
}
} else {
sTempRect.right = sTempRect.left + diameter;
if (mLastPosition == i && bounceProgress != 0) {
if (mLastPosition > mCurrentPosition) {
sTempRect.left += bounceAdjustment;
} else {
sTempRect.right -= bounceAdjustment;
}
}
}
canvas.drawRoundRect(sTempRect, mDotRadius, mDotRadius, mPaginationPaint);
// TODO(b/394355070) Verify RTL experience works correctly with visual updates
sTempRect.left = sTempRect.right + mGapWidth;
}
} else {
// Here we draw the dots
mPaginationPaint.setAlpha((int) (alpha * DOT_ALPHA_FRACTION));
for (int i = 0; i < mNumPages; i++) {
canvas.drawCircle(x, y, mDotRadius, mPaginationPaint);
x += circleGap;
}
// Here we draw the current page indicator
mPaginationPaint.setAlpha(alpha);
canvas.drawRoundRect(getActiveRect(), mDotRadius, mDotRadius, mPaginationPaint);
}
// Here we draw the current page indicator
// Reset the alpha so it doesn't become progressively more transparent each onDraw call
mPaginationPaint.setAlpha(alpha);
canvas.drawRoundRect(getActiveRect(), mDotRadius, mDotRadius, mPaginationPaint);
}
}
@@ -499,6 +590,7 @@ public class PageIndicatorDots extends View implements Insettable, PageIndicator
@Override
public void getOutline(View view, Outline outline) {
if (mEntryAnimationRadiusFactors == null) {
// TODO(b/394355070): Verify Outline works correctly with visual updates
RectF activeRect = getActiveRect();
outline.setRoundRect(
(int) activeRect.left,