Loading src/com/android/launcher3/folder/FolderIcon.java +10 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.launcher3.folder; import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION; Loading Loading @@ -627,7 +628,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel Utilities.scaleRectAboutCenter(iconBounds, iconScale); // If we are animating to the accepting state, animate the dot out. mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress()); mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress()); mDotParams.dotColor = mBackground.getDotColor(); mDotRenderer.draw(canvas, mDotParams); } Loading Loading @@ -801,6 +802,14 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } } @Override public void onHoverChanged(boolean hovered) { super.onHoverChanged(hovered); if (ENABLE_CURSOR_HOVER_STATES.get()) { mBackground.setHovered(hovered); } } /** * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon. */ Loading src/com/android/launcher3/folder/PreviewBackground.java +55 −33 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.launcher3.folder; import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR; import static com.android.launcher3.graphics.IconShape.getShape; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; Loading @@ -39,6 +41,9 @@ import android.graphics.Region; import android.graphics.Shader; import android.util.Property; import android.view.View; import android.view.animation.Interpolator; import androidx.annotation.VisibleForTesting; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; Loading @@ -55,7 +60,10 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { private static final boolean DRAW_SHADOW = false; private static final boolean DRAW_STROKE = false; private static final int CONSUMPTION_ANIMATION_DURATION = 100; @VisibleForTesting protected static final int CONSUMPTION_ANIMATION_DURATION = 100; @VisibleForTesting protected static final float HOVER_SCALE = 1.1f; @VisibleForTesting protected static final int HOVER_ANIMATION_DURATION = 300; private final PorterDuffXfermode mShadowPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); Loading Loading @@ -86,17 +94,21 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { public boolean isClipping = true; // Drawing / animation configurations private static final float ACCEPT_SCALE_FACTOR = 1.20f; @VisibleForTesting protected static final float ACCEPT_SCALE_FACTOR = 1.20f; // Expressed on a scale from 0 to 255. private static final int BG_OPACITY = 255; private static final int MAX_BG_OPACITY = 255; private static final int SHADOW_OPACITY = 40; private ValueAnimator mScaleAnimator; @VisibleForTesting protected ValueAnimator mScaleAnimator; private ObjectAnimator mStrokeAlphaAnimator; private ObjectAnimator mShadowAnimator; @VisibleForTesting protected boolean mIsAccepting; @VisibleForTesting protected boolean mIsHovered; @VisibleForTesting protected boolean mIsHoveredOrAnimating; private static final Property<PreviewBackground, Integer> STROKE_ALPHA = new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") { @Override Loading Loading @@ -203,11 +215,11 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { } /** * Returns the progress of the scale animation, where 0 means the scale is at 1f * and 1 means the scale is at ACCEPT_SCALE_FACTOR. * Returns the progress of the scale animation to accept state, where 0 means the scale is at * 1f and 1 means the scale is at ACCEPT_SCALE_FACTOR. Returns 0 when scaled due to hover. */ float getScaleProgress() { return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f); float getAcceptScaleProgress() { return mIsHoveredOrAnimating ? 0 : (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f); } void invalidate() { Loading Loading @@ -385,60 +397,70 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { return mDrawingDelegate != null; } private void animateScale(float finalScale, final Runnable onStart, final Runnable onEnd) { final float scale0 = mScale; final float scale1 = finalScale; protected void animateScale(boolean isAccepting, boolean isHovered) { if (mScaleAnimator != null) { mScaleAnimator.cancel(); } mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f); final float startScale = mScale; final float endScale = isAccepting ? ACCEPT_SCALE_FACTOR : (isHovered ? HOVER_SCALE : 1f); Interpolator interpolator = isAccepting != mIsAccepting ? ACCELERATE_DECELERATE : EMPHASIZED_DECELERATE; int duration = isAccepting != mIsAccepting ? CONSUMPTION_ANIMATION_DURATION : HOVER_ANIMATION_DURATION; mIsAccepting = isAccepting; mIsHovered = isHovered; if (startScale == endScale) { if (!mIsAccepting) { clearDrawingDelegate(); } mIsHoveredOrAnimating = mIsHovered; return; } mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f); mScaleAnimator.addUpdateListener(animation -> { float prog = animation.getAnimatedFraction(); mScale = prog * scale1 + (1 - prog) * scale0; mScale = prog * endScale + (1 - prog) * startScale; invalidate(); } }); mScaleAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { if (onStart != null) { onStart.run(); if (mIsHovered) { mIsHoveredOrAnimating = true; } } @Override public void onAnimationEnd(Animator animation) { if (onEnd != null) { onEnd.run(); if (!mIsAccepting) { clearDrawingDelegate(); } mIsHoveredOrAnimating = mIsHovered; mScaleAnimator = null; } }); mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); mScaleAnimator.setInterpolator(interpolator); mScaleAnimator.setDuration(duration); mScaleAnimator.start(); } public void animateToAccept(CellLayout cl, int cellX, int cellY) { animateScale(ACCEPT_SCALE_FACTOR, () -> delegateDrawing(cl, cellX, cellY), null); delegateDrawing(cl, cellX, cellY); animateScale(/* isAccepting= */ true, mIsHovered); } public void animateToRest() { // This can be called multiple times -- we need to make sure the drawing delegate // is saved and restored at the beginning of the animation, since cancelling the // existing animation can clear the delgate. CellLayout cl = mDrawingDelegate; int cellX = mDelegateCellX; int cellY = mDelegateCellY; animateScale(1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate); animateScale(/* isAccepting= */ false, mIsHovered); } public float getStrokeWidth() { return mStrokeWidth; } protected void setHovered(boolean hovered) { animateScale(mIsAccepting, /* isHovered= */ hovered); } } tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java 0 → 100644 +323 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.folder; import static com.android.launcher3.folder.PreviewBackground.ACCEPT_SCALE_FACTOR; import static com.android.launcher3.folder.PreviewBackground.CONSUMPTION_ANIMATION_DURATION; import static com.android.launcher3.folder.PreviewBackground.HOVER_ANIMATION_DURATION; import static com.android.launcher3.folder.PreviewBackground.HOVER_SCALE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.PathInterpolator; import androidx.test.filters.SmallTest; import com.android.launcher3.CellLayout; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class PreviewBackgroundTest { private static final float REST_SCALE = 1f; private static final float EPSILON = 0.00001f; @Mock CellLayout mCellLayout; private final PreviewBackground mPreviewBackground = new PreviewBackground(); @Before public void setUp() { MockitoAnnotations.initMocks(this); mPreviewBackground.mScale = REST_SCALE; mPreviewBackground.mIsAccepting = false; mPreviewBackground.mIsHovered = false; mPreviewBackground.mIsHoveredOrAnimating = false; mPreviewBackground.invalidate(); } @Test public void testAnimateScale_restToHovered() { mPreviewBackground.setHovered(true); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_restToNotHovered() { mPreviewBackground.setHovered(false); assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_hoveredToHovered() { mPreviewBackground.mScale = HOVER_SCALE; mPreviewBackground.mIsHovered = true; mPreviewBackground.mIsHoveredOrAnimating = true; mPreviewBackground.invalidate(); mPreviewBackground.setHovered(true); assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_hoveredToRest() { mPreviewBackground.mScale = HOVER_SCALE; mPreviewBackground.mIsHovered = true; mPreviewBackground.mIsHoveredOrAnimating = true; mPreviewBackground.invalidate(); mPreviewBackground.setHovered(false); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_restToAccept() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(1f); assertEquals("Scale changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); endAnimation(); assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1, EPSILON); } @Test public void testAnimateScale_restToRest() { mPreviewBackground.animateToRest(); assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_acceptToRest() { mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR; mPreviewBackground.mIsAccepting = true; mPreviewBackground.invalidate(); mPreviewBackground.animateToRest(); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_acceptToHover() { mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR; mPreviewBackground.mIsAccepting = true; mPreviewBackground.invalidate(); mPreviewBackground.mIsAccepting = false; mPreviewBackground.setHovered(true); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_hoverToAccept() { mPreviewBackground.mScale = HOVER_SCALE; mPreviewBackground.mIsHovered = true; mPreviewBackground.mIsHoveredOrAnimating = true; mPreviewBackground.invalidate(); mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); mPreviewBackground.mIsHovered = false; endAnimation(); assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1, EPSILON); } @Test public void testAnimateScale_midwayToHoverToAccept() { mPreviewBackground.setHovered(true); runAnimationToFraction(0.5f); assertTrue("Scale not changed.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); mPreviewBackground.mIsHovered = false; endAnimation(); assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); } @Test public void testAnimateScale_partWayToAcceptToHover() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(0.25f); assertTrue("Scale not changed part way.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR); mPreviewBackground.mIsAccepting = false; mPreviewBackground.setHovered(true); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_midwayToAcceptEqualsHover() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(0.5f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); mPreviewBackground.mIsAccepting = false; mPreviewBackground.setHovered(true); assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_midwayToHoverToRest() { mPreviewBackground.setHovered(true); runAnimationToFraction(0.5f); assertTrue("Scale not changed midway.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE); mPreviewBackground.mIsHovered = false; mPreviewBackground.animateToRest(); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_midwayToAcceptToRest() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(0.5f); assertTrue("Scale not changed.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR); mPreviewBackground.animateToRest(); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } private void runAnimationToFraction(float animationFraction) { mPreviewBackground.mScaleAnimator.setCurrentFraction(animationFraction); } private void endAnimation() { mPreviewBackground.mScaleAnimator.end(); } } Loading
src/com/android/launcher3/folder/FolderIcon.java +10 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.launcher3.folder; import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION; Loading Loading @@ -627,7 +628,7 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel Utilities.scaleRectAboutCenter(iconBounds, iconScale); // If we are animating to the accepting state, animate the dot out. mDotParams.scale = Math.max(0, mDotScale - mBackground.getScaleProgress()); mDotParams.scale = Math.max(0, mDotScale - mBackground.getAcceptScaleProgress()); mDotParams.dotColor = mBackground.getDotColor(); mDotRenderer.draw(canvas, mDotParams); } Loading Loading @@ -801,6 +802,14 @@ public class FolderIcon extends FrameLayout implements FolderListener, IconLabel } } @Override public void onHoverChanged(boolean hovered) { super.onHoverChanged(hovered); if (ENABLE_CURSOR_HOVER_STATES.get()) { mBackground.setHovered(hovered); } } /** * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon. */ Loading
src/com/android/launcher3/folder/PreviewBackground.java +55 −33 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.launcher3.folder; import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR; import static com.android.launcher3.graphics.IconShape.getShape; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; Loading @@ -39,6 +41,9 @@ import android.graphics.Region; import android.graphics.Shader; import android.util.Property; import android.view.View; import android.view.animation.Interpolator; import androidx.annotation.VisibleForTesting; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; Loading @@ -55,7 +60,10 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { private static final boolean DRAW_SHADOW = false; private static final boolean DRAW_STROKE = false; private static final int CONSUMPTION_ANIMATION_DURATION = 100; @VisibleForTesting protected static final int CONSUMPTION_ANIMATION_DURATION = 100; @VisibleForTesting protected static final float HOVER_SCALE = 1.1f; @VisibleForTesting protected static final int HOVER_ANIMATION_DURATION = 300; private final PorterDuffXfermode mShadowPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT); Loading Loading @@ -86,17 +94,21 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { public boolean isClipping = true; // Drawing / animation configurations private static final float ACCEPT_SCALE_FACTOR = 1.20f; @VisibleForTesting protected static final float ACCEPT_SCALE_FACTOR = 1.20f; // Expressed on a scale from 0 to 255. private static final int BG_OPACITY = 255; private static final int MAX_BG_OPACITY = 255; private static final int SHADOW_OPACITY = 40; private ValueAnimator mScaleAnimator; @VisibleForTesting protected ValueAnimator mScaleAnimator; private ObjectAnimator mStrokeAlphaAnimator; private ObjectAnimator mShadowAnimator; @VisibleForTesting protected boolean mIsAccepting; @VisibleForTesting protected boolean mIsHovered; @VisibleForTesting protected boolean mIsHoveredOrAnimating; private static final Property<PreviewBackground, Integer> STROKE_ALPHA = new Property<PreviewBackground, Integer>(Integer.class, "strokeAlpha") { @Override Loading Loading @@ -203,11 +215,11 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { } /** * Returns the progress of the scale animation, where 0 means the scale is at 1f * and 1 means the scale is at ACCEPT_SCALE_FACTOR. * Returns the progress of the scale animation to accept state, where 0 means the scale is at * 1f and 1 means the scale is at ACCEPT_SCALE_FACTOR. Returns 0 when scaled due to hover. */ float getScaleProgress() { return (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f); float getAcceptScaleProgress() { return mIsHoveredOrAnimating ? 0 : (mScale - 1f) / (ACCEPT_SCALE_FACTOR - 1f); } void invalidate() { Loading Loading @@ -385,60 +397,70 @@ public class PreviewBackground extends CellLayout.DelegatedCellDrawing { return mDrawingDelegate != null; } private void animateScale(float finalScale, final Runnable onStart, final Runnable onEnd) { final float scale0 = mScale; final float scale1 = finalScale; protected void animateScale(boolean isAccepting, boolean isHovered) { if (mScaleAnimator != null) { mScaleAnimator.cancel(); } mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f); final float startScale = mScale; final float endScale = isAccepting ? ACCEPT_SCALE_FACTOR : (isHovered ? HOVER_SCALE : 1f); Interpolator interpolator = isAccepting != mIsAccepting ? ACCELERATE_DECELERATE : EMPHASIZED_DECELERATE; int duration = isAccepting != mIsAccepting ? CONSUMPTION_ANIMATION_DURATION : HOVER_ANIMATION_DURATION; mIsAccepting = isAccepting; mIsHovered = isHovered; if (startScale == endScale) { if (!mIsAccepting) { clearDrawingDelegate(); } mIsHoveredOrAnimating = mIsHovered; return; } mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mScaleAnimator = ValueAnimator.ofFloat(0f, 1.0f); mScaleAnimator.addUpdateListener(animation -> { float prog = animation.getAnimatedFraction(); mScale = prog * scale1 + (1 - prog) * scale0; mScale = prog * endScale + (1 - prog) * startScale; invalidate(); } }); mScaleAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { if (onStart != null) { onStart.run(); if (mIsHovered) { mIsHoveredOrAnimating = true; } } @Override public void onAnimationEnd(Animator animation) { if (onEnd != null) { onEnd.run(); if (!mIsAccepting) { clearDrawingDelegate(); } mIsHoveredOrAnimating = mIsHovered; mScaleAnimator = null; } }); mScaleAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION); mScaleAnimator.setInterpolator(interpolator); mScaleAnimator.setDuration(duration); mScaleAnimator.start(); } public void animateToAccept(CellLayout cl, int cellX, int cellY) { animateScale(ACCEPT_SCALE_FACTOR, () -> delegateDrawing(cl, cellX, cellY), null); delegateDrawing(cl, cellX, cellY); animateScale(/* isAccepting= */ true, mIsHovered); } public void animateToRest() { // This can be called multiple times -- we need to make sure the drawing delegate // is saved and restored at the beginning of the animation, since cancelling the // existing animation can clear the delgate. CellLayout cl = mDrawingDelegate; int cellX = mDelegateCellX; int cellY = mDelegateCellY; animateScale(1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate); animateScale(/* isAccepting= */ false, mIsHovered); } public float getStrokeWidth() { return mStrokeWidth; } protected void setHovered(boolean hovered) { animateScale(mIsAccepting, /* isHovered= */ hovered); } }
tests/src/com/android/launcher3/folder/PreviewBackgroundTest.java 0 → 100644 +323 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.folder; import static com.android.launcher3.folder.PreviewBackground.ACCEPT_SCALE_FACTOR; import static com.android.launcher3.folder.PreviewBackground.CONSUMPTION_ANIMATION_DURATION; import static com.android.launcher3.folder.PreviewBackground.HOVER_ANIMATION_DURATION; import static com.android.launcher3.folder.PreviewBackground.HOVER_SCALE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.PathInterpolator; import androidx.test.filters.SmallTest; import com.android.launcher3.CellLayout; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class PreviewBackgroundTest { private static final float REST_SCALE = 1f; private static final float EPSILON = 0.00001f; @Mock CellLayout mCellLayout; private final PreviewBackground mPreviewBackground = new PreviewBackground(); @Before public void setUp() { MockitoAnnotations.initMocks(this); mPreviewBackground.mScale = REST_SCALE; mPreviewBackground.mIsAccepting = false; mPreviewBackground.mIsHovered = false; mPreviewBackground.mIsHoveredOrAnimating = false; mPreviewBackground.invalidate(); } @Test public void testAnimateScale_restToHovered() { mPreviewBackground.setHovered(true); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_restToNotHovered() { mPreviewBackground.setHovered(false); assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_hoveredToHovered() { mPreviewBackground.mScale = HOVER_SCALE; mPreviewBackground.mIsHovered = true; mPreviewBackground.mIsHoveredOrAnimating = true; mPreviewBackground.invalidate(); mPreviewBackground.setHovered(true); assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_hoveredToRest() { mPreviewBackground.mScale = HOVER_SCALE; mPreviewBackground.mIsHovered = true; mPreviewBackground.mIsHoveredOrAnimating = true; mPreviewBackground.invalidate(); mPreviewBackground.setHovered(false); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_restToAccept() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(1f); assertEquals("Scale changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); endAnimation(); assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1, EPSILON); } @Test public void testAnimateScale_restToRest() { mPreviewBackground.animateToRest(); assertEquals("Scale changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_acceptToRest() { mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR; mPreviewBackground.mIsAccepting = true; mPreviewBackground.invalidate(); mPreviewBackground.animateToRest(); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_acceptToHover() { mPreviewBackground.mScale = ACCEPT_SCALE_FACTOR; mPreviewBackground.mIsAccepting = true; mPreviewBackground.invalidate(); mPreviewBackground.mIsAccepting = false; mPreviewBackground.setHovered(true); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_hoverToAccept() { mPreviewBackground.mScale = HOVER_SCALE; mPreviewBackground.mIsHovered = true; mPreviewBackground.mIsHoveredOrAnimating = true; mPreviewBackground.invalidate(); mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); mPreviewBackground.mIsHovered = false; endAnimation(); assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1, EPSILON); } @Test public void testAnimateScale_midwayToHoverToAccept() { mPreviewBackground.setHovered(true); runAnimationToFraction(0.5f); assertTrue("Scale not changed.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, ACCEPT_SCALE_FACTOR, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); mPreviewBackground.mIsHovered = false; endAnimation(); assertEquals("Scale progress not 1.", mPreviewBackground.getAcceptScaleProgress(), 1, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); } @Test public void testAnimateScale_partWayToAcceptToHover() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(0.25f); assertTrue("Scale not changed part way.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR); mPreviewBackground.mIsAccepting = false; mPreviewBackground.setHovered(true); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_midwayToAcceptEqualsHover() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(0.5f); assertEquals("Scale not changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); mPreviewBackground.mIsAccepting = false; mPreviewBackground.setHovered(true); assertEquals("Scale changed.", mPreviewBackground.mScale, HOVER_SCALE, EPSILON); assertNull("Animator not null.", mPreviewBackground.mScaleAnimator); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_midwayToHoverToRest() { mPreviewBackground.setHovered(true); runAnimationToFraction(0.5f); assertTrue("Scale not changed midway.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < HOVER_SCALE); mPreviewBackground.mIsHovered = false; mPreviewBackground.animateToRest(); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), HOVER_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof PathInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } @Test public void testAnimateScale_midwayToAcceptToRest() { mPreviewBackground.animateToAccept(mCellLayout, 0, 0); runAnimationToFraction(0.5f); assertTrue("Scale not changed.", mPreviewBackground.mScale > REST_SCALE && mPreviewBackground.mScale < ACCEPT_SCALE_FACTOR); mPreviewBackground.animateToRest(); runAnimationToFraction(1f); assertEquals("Scale not changed.", mPreviewBackground.mScale, REST_SCALE, EPSILON); assertEquals("Duration not correct.", mPreviewBackground.mScaleAnimator.getDuration(), CONSUMPTION_ANIMATION_DURATION); assertTrue("Wrong interpolator used.", mPreviewBackground.mScaleAnimator.getInterpolator() instanceof AccelerateDecelerateInterpolator); endAnimation(); assertEquals("Scale progress not 0.", mPreviewBackground.getAcceptScaleProgress(), 0, EPSILON); } private void runAnimationToFraction(float animationFraction) { mPreviewBackground.mScaleAnimator.setCurrentFraction(animationFraction); } private void endAnimation() { mPreviewBackground.mScaleAnimator.end(); } }