Merge "Scale Launcher folders on hover." into udc-qpr-dev

This commit is contained in:
Pat Manning
2023-08-25 22:23:20 +00:00
committed by Android (Google) Code Review
3 changed files with 388 additions and 34 deletions

View File

@@ -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;
@@ -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);
}
@@ -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.
*/

View File

@@ -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;
@@ -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;
@@ -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);
@@ -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
@@ -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() {
@@ -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);
mScaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float prog = animation.getAnimatedFraction();
mScale = prog * scale1 + (1 - prog) * scale0;
invalidate();
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 = ValueAnimator.ofFloat(0f, 1.0f);
mScaleAnimator.addUpdateListener(animation -> {
float prog = animation.getAnimatedFraction();
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);
}
}

View File

@@ -0,0 +1,323 @@
/*
* 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();
}
}