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

Commit beb6ecfb authored by Mike Schneider's avatar Mike Schneider
Browse files

Kotlin-ify MagneticDividerUtils(-Tests)

- Make MagneticDividerUtils an object
- Use the MotionBuilderContext as a scope for the generateMotionSpec
- Convert test to Kotlin

This is done to (a) reduce nesting and thus increase readability, and
(b) to facilitate writing tests in the next CL. It removes the need to work around a device specific Resources object

This is a pure mechanic refactoring

Test: MagneticDividerUtilsTests
Bug: 383631946
Flag: com.android.wm.shell.enable_magnetic_split_divider
Change-Id: I0684d1e9d5db5125d6a47a4f1fecfef8c5e9c5c7
parent 3f0aebd9
Loading
Loading
Loading
Loading
+127 −131
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec
import com.android.mechanics.spec.with
import com.android.mechanics.spring.SpringParameters
@@ -31,11 +32,10 @@ import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget
 * Utility class used to create a framework that enables the divider to snap magnetically to snap
 * points while the user is dragging it.
 */
class MagneticDividerUtils {
    companion object {
object MagneticDividerUtils {
    /**
         * When the user moves the divider towards or away from a snap point, a magnetic spring
         * movement and haptic will take place at this distance.
     * When the user moves the divider towards or away from a snap point, a magnetic spring movement
     * and haptic will take place at this distance.
     */
    private val DEFAULT_MAGNETIC_ATTACH_THRESHOLD = 56.dp
    /** The minimum spacing between snap zones, to prevent overlap on smaller displays. */
@@ -45,90 +45,88 @@ class MagneticDividerUtils {
    /** The damping ratio of the magnetic snap effect. */
    private const val ATTACH_DAMPING_RATIO = 0.95f
    /** The spring used for the magnetic snap effect. */
        private val MagneticSpring = SpringParameters(
            stiffness = ATTACH_STIFFNESS,
            dampingRatio = ATTACH_DAMPING_RATIO
        )
        /**
         * When inside the magnetic snap zone, the divider's movement is reduced by this amount.
         */
    private val MagneticSpring =
        SpringParameters(stiffness = ATTACH_STIFFNESS, dampingRatio = ATTACH_DAMPING_RATIO)
    /** When inside the magnetic snap zone, the divider's movement is reduced by this amount. */
    private const val ATTACH_DETACH_SCALE = 0.5f
    /**
         * A key that can be passed into a MotionValue to retrieve the SnapPosition associated
         * with the current drag.
     * A key that can be passed into a MotionValue to retrieve the SnapPosition associated with the
     * current drag.
     */
    @JvmStatic val SNAP_POSITION_KEY = SemanticKey<Int?>()

    /**
         * Key used for identity regions which don't have drop zones associated with them.
         * Need to keep this key separate for the SemanticKeys we create with null values as it
         * seems like this overwrites the semantics created with real snapTarget values
     * Key used for identity regions which don't have drop zones associated with them. Need to keep
     * this key separate for the SemanticKeys we create with null values as it seems like this
     * overwrites the semantics created with real snapTarget values
     */
    @JvmStatic private val SNAP_POSITION_KEY_IDENTITY = SemanticKey<Int?>()

    /**
     * Create a MotionSpec that has "snap zones" for each of the SnapTargets provided.
     *
     * NOTE: This exists for Java/View interoperability only
     */
    @JvmStatic
        fun generateMotionSpec(targets: List<SnapTarget>, res: Resources): MotionSpec {
            // Create a new MotionSpec object and return it. A MotionSpec is composed of at least
            // one DirectionalMotionSpec, so below we will create a DirectionalMotionSpec and pass
            // it as the single argument to the MotionSpec constructor.
            return MotionSpec(
                // To do that, we first create a "context" object, which gives access to a
                // DirectionalMotionSpec builder and some convenience functions, like for converting
                // dp > px.
                with(standardViewMotionBuilderContext(res.displayMetrics.density)) {
                    // Inside of this "with" block, we can write code freely -- the final evaluated
                    // value of this block will be the value of the final expression we write. See
                    // Kotlin docs for more details.
    fun generateMotionSpec(targets: List<SnapTarget>, resources: Resources): MotionSpec {
        return with(standardViewMotionBuilderContext(resources.displayMetrics.density)) {
            generateMotionSpec(targets)
        }
    }

    /** Create a MotionSpec that has "snap zones" for each of the SnapTargets provided. */
    fun MotionBuilderContext.generateMotionSpec(targets: List<SnapTarget>): MotionSpec {
        // First, get the position of the left-most (or top-most) dismiss point.
        val topLeftDismissTarget = targets.first()
        val topLeftDismissPosition = topLeftDismissTarget.position.toFloat()

        return MotionSpec(

            // Create a DirectionalMotionSpec using a pre-set builder method. We choose the
                    // "spatialDirectionalMotionSpec", which is meant for "spatial" movement (as
                    // opposed to "effects" movement).
            // "spatialDirectionalMotionSpec", which is meant for "spatial" movement (as opposed to
            // "effects" movement).
            spatialDirectionalMotionSpec(
                initialMapping = Mapping.Fixed(topLeftDismissPosition),
                semantics = listOf(SNAP_POSITION_KEY_IDENTITY with null),
                        defaultSpring = MagneticSpring
                defaultSpring = MagneticSpring,
            ) {
                // NOTE: This block is a trailing lambda passed in as the "init" parameter.

                        // A DirectionalMotionSpec is essentially a number line from -infinity to
                        // infinity, with instructions on how to interpret the value at each point.
                        // We create each individual segment below to fill out our number line.
                // A DirectionalMotionSpec is essentially a number line from -infinity to infinity,
                // with instructions on how to interpret the value at each point. We create each
                // individual segment below to fill out our number line.

                        // Start by finding the smallest span between two targets and setting an
                        // appropriate magnetic snap threshold.
                        val smallestSpanBetweenTargets = targets.zipWithNext { t1, t2 ->
                            t2.position.toFloat() - t1.position.toFloat()
                        }.reduce { minSoFar, currentDiff ->
                            kotlin.math.min(minSoFar, currentDiff)
                        }
                        val availableSpaceForSnapZone = (smallestSpanBetweenTargets -
                                MINIMUM_SPACE_BETWEEN_SNAP_ZONES.toPx()) / 2f
                        val snapThreshold = kotlin.math.min(
                            DEFAULT_MAGNETIC_ATTACH_THRESHOLD.toPx(), availableSpaceForSnapZone)

                        // Our first breakpoint is located at topLeftDismissPosition. On the right
                        // side of this breakpoint, we'll use the "identity" instruction, which
                        // means values won't be converted.
                // Start by finding the smallest span between two targets and setting an appropriate
                // magnetic snap threshold.
                val smallestSpanBetweenTargets =
                    targets
                        .zipWithNext { t1, t2 -> t2.position.toFloat() - t1.position.toFloat() }
                        .reduce { minSoFar, currentDiff -> kotlin.math.min(minSoFar, currentDiff) }
                val availableSpaceForSnapZone =
                    (smallestSpanBetweenTargets - MINIMUM_SPACE_BETWEEN_SNAP_ZONES.toPx()) / 2f
                val snapThreshold =
                    kotlin.math.min(
                        DEFAULT_MAGNETIC_ATTACH_THRESHOLD.toPx(),
                        availableSpaceForSnapZone,
                    )

                // Our first breakpoint is located at topLeftDismissPosition. On the right side of
                // this breakpoint, we'll use the "identity" instruction, which means values won't
                // be converted.
                identity(
                    breakpoint = topLeftDismissPosition,
                            semantics = listOf(SNAP_POSITION_KEY with null)
                    semantics = listOf(SNAP_POSITION_KEY with null),
                )

                // We continue creating alternating zones of "identity" and
                        // "fractionalInputFromCurrent", which will give us the behavior we're
                        // looking for, where the divider can be dragged along normally in some
                        // areas (the identity zones) and resists the user's movement in some areas
                        // (the fractionalInputFromCurrent zones). The targets have to be created in
                        // ascending order.

                        // Iterating from the second target to the second-last target (EXCLUDING the
                        // first and last):
                // "fractionalInputFromCurrent", which will give us the behavior we're looking for,
                // where the divider can be dragged along normally in some areas (the identity
                // zones) and resists the user's movement in some areas (the
                // fractionalInputFromCurrent zones). The targets have to be created in ascending
                // order.

                // Iterating from the second target to the second-last target (EXCLUDING the first
                // and last):
                for (i in (1 until targets.size - 1)) {
                    val target = targets[i]
                    val targetPosition = target.position.toFloat()
@@ -136,9 +134,9 @@ class MagneticDividerUtils {
                    // Create a fractionalInputFromCurrent zone.
                    fractionalInputFromCurrent(
                        breakpoint = targetPosition - snapThreshold,
                                // With every magnetic segment, we also pass in the associated
                                // snapPosition as a "semantic association", so we can later query
                                // the MotionValue for it.
                        // With every magnetic segment, we also pass in the associated snapPosition
                        // as a "semantic association", so we can later query the MotionValue for
                        // it.
                        semantics = listOf(SNAP_POSITION_KEY with target.snapPosition),
                        delta = snapThreshold * (1 - ATTACH_DETACH_SCALE),
                        fraction = ATTACH_DETACH_SCALE,
@@ -147,22 +145,20 @@ class MagneticDividerUtils {
                    // Create another identity zone.
                    identity(
                        breakpoint = targetPosition + snapThreshold,
                                semantics = listOf(SNAP_POSITION_KEY_IDENTITY with null)
                        semantics = listOf(SNAP_POSITION_KEY_IDENTITY with null),
                    )
                }

                        // Finally, create one last fixedValue zone, from the bottom/right
                        // dismiss point to infinity.
                // Finally, create one last fixedValue zone, from the bottom/right dismiss point to
                // infinity.
                val bottomRightDismissTarget = targets.last()
                val bottomRightDismissPosition = bottomRightDismissTarget.position.toFloat()
                fixedValue(
                    breakpoint = bottomRightDismissPosition,
                    value = bottomRightDismissPosition,
                            semantics = listOf(SNAP_POSITION_KEY_IDENTITY with null)
                    semantics = listOf(SNAP_POSITION_KEY_IDENTITY with null),
                )
            }
                }
        )
    }
}
}
+56 −0
Original line number Diff line number Diff line
@@ -13,56 +13,44 @@
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.wm.shell.common.split

package com.android.wm.shell.common.split;
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget
import com.android.wm.shell.common.split.MagneticDividerUtils.generateMotionSpec
import com.android.wm.shell.shared.split.SplitScreenConstants
import com.google.common.truth.Truth.assertThat
import kotlin.math.max
import org.junit.Test
import org.junit.runner.RunWith

import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS;

import android.content.res.Resources;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.mechanics.spec.MotionSpec;
import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget;

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

import java.util.List;

@RunWith(AndroidJUnit4.class)
public class MagneticDividerUtilsTests {
    Resources mResources;

    @Before
    public void setup() {
        mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources();
    }
@RunWith(AndroidJUnit4::class)
class MagneticDividerUtilsTests {

    @Test
    public void generateMotionSpec_worksOnThisDeviceWithoutCrashing() {
        int longEdge = Math.max(
                mResources.getDisplayMetrics().heightPixels,
                mResources.getDisplayMetrics().widthPixels
        );

        List<SnapTarget> mTargets = List.of(
                new SnapTarget(0, SNAP_TO_START_AND_DISMISS),
                new SnapTarget(longEdge / 10, SNAP_TO_2_10_90),
                new SnapTarget(longEdge / 2, SNAP_TO_2_50_50),
                new SnapTarget(longEdge - (longEdge / 10), SNAP_TO_2_90_10),
                new SnapTarget(longEdge, SNAP_TO_END_AND_DISMISS)
        );
    fun generateMotionSpec_worksOnThisDeviceWithoutCrashing() {
        // Retrieve long edge and motion builder context (density) from this device.
        val resources = InstrumentationRegistry.getInstrumentation().context.resources
        val longEdge =
            max(
                    resources.displayMetrics.heightPixels.toDouble(),
                    resources.displayMetrics.widthPixels.toDouble(),
                )
                .toInt()

        val targets =
            listOf(
                SnapTarget(0, SplitScreenConstants.SNAP_TO_START_AND_DISMISS),
                SnapTarget(longEdge / 10, SplitScreenConstants.SNAP_TO_2_10_90),
                SnapTarget(longEdge / 2, SplitScreenConstants.SNAP_TO_2_50_50),
                SnapTarget(longEdge - (longEdge / 10), SplitScreenConstants.SNAP_TO_2_90_10),
                SnapTarget(longEdge, SplitScreenConstants.SNAP_TO_END_AND_DISMISS),
            )

        // Check that a MotionSpec gets created without crashing. A crash can happen if the dp
        // values set MagneticDividerUtils are large enough that the snap zones overlap on smaller
        // screens.
        MotionSpec motionSpec = MagneticDividerUtils.generateMotionSpec(mTargets, mResources);
        assertThat(generateMotionSpec(targets, resources)).isNotNull()
    }
}