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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user