Loading src/com/android/launcher3/CellLayout.java +9 −146 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; Loading @@ -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<>(); Loading Loading @@ -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 Loading Loading @@ -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(); Loading src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt 0 → 100644 +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 } } tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java +5 −47 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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() Loading Loading @@ -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(); Loading tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt 0 → 100644 +201 −0 File added.Preview size limit exceeded, changes collapsed. Show changes tests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt 0 → 100644 +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 } } } } Loading
src/com/android/launcher3/CellLayout.java +9 −146 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; Loading @@ -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<>(); Loading Loading @@ -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 Loading Loading @@ -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(); Loading
src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt 0 → 100644 +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 } }
tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java +5 −47 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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() Loading Loading @@ -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(); Loading
tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt 0 → 100644 +201 −0 File added.Preview size limit exceeded, changes collapsed. Show changes
tests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt 0 → 100644 +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 } } } }