Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9ab2e761 authored by Pat Manning's avatar Pat Manning Committed by Automerger Merge Worker
Browse files

Merge "Scale Launcher folders on hover." into udc-qpr-dev am: 0ff8103c

parents a5e28a3a 0ff8103c
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -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.
     */
+55 −33
Original line number Diff line number Diff line
@@ -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);
        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);
    }
}
+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();
    }
}