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

Commit a808e7ea authored by Sebastián Franco's avatar Sebastián Franco Committed by Android (Google) Code Review
Browse files

Merge "Moving ReorderPreviewAnimation into it's own file and rewrite to Kotlin." into main

parents 6d4fe47e f7654252
Loading
Loading
Loading
Loading
+9 −146
Original line number Diff line number Diff line
@@ -16,18 +16,13 @@

package com.android.launcher3;

import static android.animation.ValueAnimator.areAnimatorsEnabled;

import static com.android.app.animation.Interpolators.DECELERATE_1_5;
import static com.android.launcher3.LauncherState.EDIT_MODE;
import static com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON;
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET;
import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_PREVIEW_OFFSET;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -70,6 +65,7 @@ import com.android.launcher3.celllayout.DelegatedCellDrawing;
import com.android.launcher3.celllayout.ItemConfiguration;
import com.android.launcher3.celllayout.ReorderAlgorithm;
import com.android.launcher3.celllayout.ReorderParameters;
import com.android.launcher3.celllayout.ReorderPreviewAnimation;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.PreviewBackground;
@@ -188,7 +184,7 @@ public class CellLayout extends ViewGroup {

    @ContainerType private final int mContainerType;

    private final float mChildScale = 1f;
    public static final float DEFAULT_SCALE = 1f;

    public static final int MODE_SHOW_REORDER_HINT = 0;
    public static final int MODE_DRAG_OVER = 1;
@@ -198,8 +194,8 @@ public class CellLayout extends ViewGroup {
    private static final boolean DESTRUCTIVE_REORDER = false;
    private static final boolean DEBUG_VISUALIZE_OCCUPIED = false;

    private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f;
    private static final int REORDER_ANIMATION_DURATION = 150;
    public static final float REORDER_PREVIEW_MAGNITUDE = 0.12f;
    public static final int REORDER_ANIMATION_DURATION = 150;
    @Thunk final float mReorderPreviewAnimationMagnitude;

    private final ArrayList<View> mIntersectingViews = new ArrayList<>();
@@ -761,8 +757,8 @@ public class CellLayout extends ViewGroup {
            bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
        }

        child.setScaleX(mChildScale);
        child.setScaleY(mChildScale);
        child.setScaleX(DEFAULT_SCALE);
        child.setScaleY(DEFAULT_SCALE);

        // Generate an id for each view, this assumes we have at most 256x256 cells
        // per workspace screen
@@ -1438,147 +1434,14 @@ public class CellLayout extends ViewGroup {

            CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
            if (c != null && !skip && (child instanceof Reorderable)) {
                ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child,
                        mode, lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY);
                ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode,
                        lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY,
                        mReorderPreviewAnimationMagnitude, this, mShakeAnimators);
                rha.animate();
            }
        }
    }

    // Class which represents the reorder preview animations. These animations show that an item is
    // in a temporary state, and hint at where the item will return to.
    class ReorderPreviewAnimation<T extends View & Reorderable> implements
            ValueAnimator.AnimatorUpdateListener {
        final T child;
        float finalDeltaX;
        float finalDeltaY;
        float initDeltaX;
        float initDeltaY;
        final float finalScale;
        float initScale;
        private static final int PREVIEW_DURATION = 300;
        private static final int HINT_DURATION = Workspace.REORDER_TIMEOUT;

        private static final float CHILD_DIVIDEND = 4.0f;

        public static final int MODE_HINT = 0;
        public static final int MODE_PREVIEW = 1;

        ValueAnimator mAnimator;

        ReorderPreviewAnimation(View childView, int mode, int cellX0, int cellY0,
                int cellX1, int cellY1, int spanX, int spanY) {
            regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
            final int x0 = mTmpPoint[0];
            final int y0 = mTmpPoint[1];
            regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint);
            final int x1 = mTmpPoint[0];
            final int y1 = mTmpPoint[1];
            final int dX = x1 - x0;
            final int dY = y1 - y0;

            this.child = (T) childView;
            finalDeltaX = 0;
            finalDeltaY = 0;

            MultiTranslateDelegate mtd = child.getTranslateDelegate();
            initDeltaX = mtd.getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).getValue();
            initDeltaY = mtd.getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).getValue();
            initScale = child.getReorderBounceScale();
            finalScale = mChildScale - (CHILD_DIVIDEND / child.getWidth()) * initScale;

            mAnimator = ObjectAnimator.ofFloat(0, 1);
            mAnimator.addUpdateListener(this);

            // Animations are disabled in power save mode, causing the repeated animation to jump
            // spastically between beginning and end states. Since this looks bad, we don't repeat
            // the animation in power save mode.
            if (areAnimatorsEnabled() && mode == MODE_PREVIEW) {
                mAnimator.setRepeatCount(ValueAnimator.INFINITE);
                mAnimator.setRepeatMode(ValueAnimator.REVERSE);
            }

            mAnimator.setDuration(mode == MODE_HINT ? HINT_DURATION : PREVIEW_DURATION);
            mAnimator.setStartDelay((int) (Math.random() * 60));

            int dir = mode == MODE_HINT ? -1 : 1;
            if (dX == dY && dX == 0) {
            } else {
                if (dY == 0) {
                    finalDeltaX = -dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
                } else if (dX == 0) {
                    finalDeltaY = -dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
                } else {
                    double angle = Math.atan( (float) (dY) / dX);
                    finalDeltaX = (int) (-dir * Math.signum(dX)
                            * Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
                    finalDeltaY = (int) (-dir * Math.signum(dY)
                            * Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
                }
            }
        }

        void setInitialAnimationValuesToBaseline() {
            initScale = mChildScale;
            initDeltaX = 0;
            initDeltaY = 0;
        }

        void animate() {
            boolean noMovement = (finalDeltaX == 0) && (finalDeltaY == 0);

            if (mShakeAnimators.containsKey(child)) {
                ReorderPreviewAnimation oldAnimation = mShakeAnimators.get(child);
                mShakeAnimators.remove(child);

                if (noMovement) {
                    // A previous animation for this item exists, and no new animation will exist.
                    // Finish the old animation smoothly.
                    oldAnimation.finishAnimation();
                    return;
                } else {
                    // A previous animation for this item exists, and a new one will exist. Stop
                    // the old animation in its tracks, and proceed with the new one.
                    oldAnimation.cancel();
                }
            }
            if (noMovement) {
                return;
            }

            mShakeAnimators.put(child, this);
            mAnimator.start();
        }

        @Override
        public void onAnimationUpdate(ValueAnimator updatedAnimation) {
            float progress = (float) updatedAnimation.getAnimatedValue();
            child.getTranslateDelegate().setTranslation(
                    INDEX_REORDER_BOUNCE_OFFSET,
                    /* dx = */ progress * finalDeltaX + (1 - progress) * initDeltaX,
                    /* dy = */ progress * finalDeltaY + (1 - progress) * initDeltaY
            );
            child.setReorderBounceScale(progress * finalScale + (1 - progress) * initScale);
        }

        private void cancel() {
            mAnimator.cancel();
        }

        /**
         * Smoothly returns the item to its baseline position / scale
         */
        @Thunk void finishAnimation() {
            mAnimator.cancel();
            setInitialAnimationValuesToBaseline();
            mAnimator = ObjectAnimator.ofFloat((Float) mAnimator.getAnimatedValue(), 0);
            mAnimator.addUpdateListener(this);
            mAnimator.setInterpolator(DECELERATE_1_5);
            mAnimator.setDuration(REORDER_ANIMATION_DURATION);
            mAnimator.start();
        }
    }

    private void completeAndClearReorderPreviewAnimations() {
        for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
            a.finishAnimation();
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.celllayout

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.animation.ValueAnimator.areAnimatorsEnabled
import android.util.ArrayMap
import android.view.View
import com.android.app.animation.Interpolators.DECELERATE_1_5
import com.android.launcher3.CellLayout
import com.android.launcher3.CellLayout.REORDER_ANIMATION_DURATION
import com.android.launcher3.Reorderable
import com.android.launcher3.Workspace
import com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET
import com.android.launcher3.util.Thunk
import kotlin.math.abs
import kotlin.math.atan
import kotlin.math.cos
import kotlin.math.sign
import kotlin.math.sin

/**
 * Class which represents the reorder preview animations. These animations show that an item is in a
 * temporary state, and hint at where the item will return to.
 */
class ReorderPreviewAnimation<T>(
    val child: T,
    // If the mode is MODE_HINT it will only move one period and stop, it then will be going
    // backwards to the initial position, otherwise it will oscillate.
    val mode: Int,
    cellX0: Int,
    cellY0: Int,
    cellX1: Int,
    cellY1: Int,
    spanX: Int,
    spanY: Int,
    reorderMagnitude: Float,
    cellLayout: CellLayout,
    private val shakeAnimators: ArrayMap<Reorderable, ReorderPreviewAnimation<T>>
) : ValueAnimator.AnimatorUpdateListener where T : View, T : Reorderable {

    private var finalDeltaX = 0f
    private var finalDeltaY = 0f
    private var initDeltaX =
        child.getTranslateDelegate().getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).value
    private var initDeltaY =
        child.getTranslateDelegate().getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).value
    private var initScale = child.getReorderBounceScale()
    private val finalScale = CellLayout.DEFAULT_SCALE - CHILD_DIVIDEND / child.width * initScale

    private val dir = if (mode == MODE_HINT) -1 else 1
    var animator: ValueAnimator =
        ObjectAnimator.ofFloat(0f, 1f).also {
            it.addUpdateListener(this)
            it.setDuration((if (mode == MODE_HINT) HINT_DURATION else PREVIEW_DURATION).toLong())
            it.startDelay = (Math.random() * 60).toLong()
            // Animations are disabled in power save mode, causing the repeated animation to jump
            // spastically between beginning and end states. Since this looks bad, we don't repeat
            // the animation in power save mode.
            if (areAnimatorsEnabled() && mode == MODE_PREVIEW) {
                it.repeatCount = ValueAnimator.INFINITE
                it.repeatMode = ValueAnimator.REVERSE
            }
        }

    init {
        val tmpRes = intArrayOf(0, 0)
        cellLayout.regionToCenterPoint(cellX0, cellY0, spanX, spanY, tmpRes)
        val (x0, y0) = tmpRes
        cellLayout.regionToCenterPoint(cellX1, cellY1, spanX, spanY, tmpRes)
        val (x1, y1) = tmpRes
        val dX = x1 - x0
        val dY = y1 - y0

        if (dX != 0 || dY != 0) {
            if (dY == 0) {
                finalDeltaX = -dir * sign(dX.toFloat()) * reorderMagnitude
            } else if (dX == 0) {
                finalDeltaY = -dir * sign(dY.toFloat()) * reorderMagnitude
            } else {
                val angle = atan((dY.toFloat() / dX))
                finalDeltaX = (-dir * sign(dX.toFloat()) * abs(cos(angle) * reorderMagnitude))
                finalDeltaY = (-dir * sign(dY.toFloat()) * abs(sin(angle) * reorderMagnitude))
            }
        }
    }

    private fun setInitialAnimationValuesToBaseline() {
        initScale = CellLayout.DEFAULT_SCALE
        initDeltaX = 0f
        initDeltaY = 0f
    }

    fun animate() {
        val noMovement = finalDeltaX == 0f && finalDeltaY == 0f
        if (shakeAnimators.containsKey(child)) {
            val oldAnimation: ReorderPreviewAnimation<T>? = shakeAnimators.remove(child)
            if (noMovement) {
                // A previous animation for this item exists, and no new animation will exist.
                // Finish the old animation smoothly.
                oldAnimation!!.finishAnimation()
                return
            } else {
                // A previous animation for this item exists, and a new one will exist. Stop
                // the old animation in its tracks, and proceed with the new one.
                oldAnimation!!.cancel()
            }
        }
        if (noMovement) {
            return
        }
        shakeAnimators[child] = this
        animator.start()
    }

    override fun onAnimationUpdate(updatedAnimation: ValueAnimator) {
        val progress = updatedAnimation.animatedValue as Float
        child
            .getTranslateDelegate()
            .setTranslation(
                INDEX_REORDER_BOUNCE_OFFSET,
                /* x = */ progress * finalDeltaX + (1 - progress) * initDeltaX,
                /* y = */ progress * finalDeltaY + (1 - progress) * initDeltaY
            )
        child.setReorderBounceScale(progress * finalScale + (1 - progress) * initScale)
    }

    private fun cancel() {
        animator.cancel()
    }

    /** Smoothly returns the item to its baseline position / scale */
    @Thunk
    fun finishAnimation() {
        animator.cancel()
        setInitialAnimationValuesToBaseline()
        animator = ObjectAnimator.ofFloat((animator.animatedValue as Float), 0f)
        animator.addUpdateListener(this)
        animator.interpolator = DECELERATE_1_5
        animator.setDuration(REORDER_ANIMATION_DURATION.toLong())
        animator.start()
    }

    companion object {
        const val PREVIEW_DURATION = 300
        const val HINT_DURATION = Workspace.REORDER_TIMEOUT
        private const val CHILD_DIVIDEND = 4.0f
        const val MODE_HINT = 0
        const val MODE_PREVIEW = 1
    }
}
+5 −47
Original line number Diff line number Diff line
@@ -29,8 +29,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.MultipageCellLayout;
import com.android.launcher3.celllayout.board.CellLayoutBoard;
import com.android.launcher3.celllayout.board.IconPoint;
@@ -41,8 +39,7 @@ import com.android.launcher3.celllayout.testgenerator.RandomMultiBoardGenerator;
import com.android.launcher3.util.ActivityContextWrapper;
import com.android.launcher3.views.DoubleShadowBubbleTextView;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@@ -70,7 +67,8 @@ public class ReorderAlgorithmUnitTest {
    private static final int TOTAL_OF_CASES_GENERATED = 300;
    private Context mApplicationContext;

    private int mPrevNumColumns, mPrevNumRows;
    @Rule
    public UnitTestCellLayoutBuilderRule mCellLayoutBuilder = new UnitTestCellLayoutBuilderRule();

    /**
     * This test reads existing test cases and makes sure the CellLayout produces the same
@@ -144,34 +142,10 @@ public class ReorderAlgorithmUnitTest {
                (CellLayoutLayoutParams) cell.getLayoutParams(), true);
    }

    public CellLayout createCellLayout(int width, int height, boolean isMulti) {
        Context c = mApplicationContext;
        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
        // modify the device profile.
        dp.inv.numColumns = isMulti ? width / 2 : width;
        dp.inv.numRows = height;
        dp.cellLayoutBorderSpacePx = new Point(0, 0);

        CellLayout cl = isMulti ? new MultipageCellLayout(getWrappedContext(c, dp))
                : new CellLayout(getWrappedContext(c, dp));
        // I put a very large number for width and height so that all the items can fit, it doesn't
        // need to be exact, just bigger than the sum of cell border
        cl.measure(View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY));
        return cl;
    }

    private Context getWrappedContext(Context context, DeviceProfile dp) {
        return new ActivityContextWrapper(context) {
            public DeviceProfile getDeviceProfile() {
                return dp;
            }
        };
    }

    public ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX,
            int spanY, int minSpanX, int minSpanY, boolean isMulti) {
        CellLayout cl = createCellLayout(board.getWidth(), board.getHeight(), isMulti);
        CellLayout cl = mCellLayoutBuilder.createCellLayout(board.getWidth(), board.getHeight(),
                isMulti);

        // The views have to be sorted or the result can vary
        board.getIcons()
@@ -249,22 +223,6 @@ public class ReorderAlgorithmUnitTest {
        }
    }

    @Before
    public void storePreviousValues() {
        Context c = new ActivityContextWrapper(getApplicationContext());
        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
        mPrevNumColumns = dp.inv.numColumns;
        mPrevNumRows = dp.inv.numRows;
    }

    @After
    public void restorePreviousValues() {
        Context c = new ActivityContextWrapper(getApplicationContext());
        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
        dp.inv.numColumns = mPrevNumColumns;
        dp.inv.numRows = mPrevNumRows;
    }

    private ReorderAlgorithmUnitTestCase generateRandomTestCase(
            RandomBoardGenerator boardGenerator) {
        ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase();
+201 −0

File added.

Preview size limit exceeded, changes collapsed.

+84 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.celllayout

import android.content.Context
import android.graphics.Point
import android.view.View
import androidx.test.core.app.ApplicationProvider
import com.android.launcher3.CellLayout
import com.android.launcher3.DeviceProfile
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.MultipageCellLayout
import com.android.launcher3.util.ActivityContextWrapper
import org.junit.rules.TestWatcher
import org.junit.runner.Description

/**
 * Create CellLayouts to be used in Unit testing and make sure to set the DeviceProfile back to
 * normal.
 */
class UnitTestCellLayoutBuilderRule : TestWatcher() {

    private var prevNumColumns = 0
    private var prevNumRows = 0

    private val applicationContext =
        ActivityContextWrapper(ApplicationProvider.getApplicationContext())

    override fun starting(description: Description?) {
        val dp = getDeviceProfile()
        prevNumColumns = dp.inv.numColumns
        prevNumRows = dp.inv.numRows
    }

    override fun finished(description: Description?) {
        val dp = getDeviceProfile()
        dp.inv.numColumns = prevNumColumns
        dp.inv.numRows = prevNumRows
    }

    fun createCellLayout(width: Int, height: Int, isMulti: Boolean): CellLayout {
        val dp = getDeviceProfile()
        // modify the device profile.
        dp.inv.numColumns = if (isMulti) width / 2 else width
        dp.inv.numRows = height
        dp.cellLayoutBorderSpacePx = Point(0, 0)
        val cl =
            if (isMulti) MultipageCellLayout(getWrappedContext(applicationContext, dp))
            else CellLayout(getWrappedContext(applicationContext, dp))
        // I put a very large number for width and height so that all the items can fit, it doesn't
        // need to be exact, just bigger than the sum of cell border
        cl.measure(
            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY)
        )
        return cl
    }

    private fun getDeviceProfile(): DeviceProfile =
        InvariantDeviceProfile.INSTANCE[applicationContext].getDeviceProfile(applicationContext)
            .copy(applicationContext)

    private fun getWrappedContext(context: Context, dp: DeviceProfile): Context {
        return object : ActivityContextWrapper(context) {
            override fun getDeviceProfile(): DeviceProfile {
                return dp
            }
        }
    }
}