Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +17 −15 Original line number Diff line number Diff line Loading @@ -55,18 +55,20 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.mechanics.spec.BreakpointKey; import com.android.mechanics.spec.InputDirection; import com.android.mechanics.view.DistanceGestureContext; import com.android.mechanics.view.ViewMotionValue; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.desktopmode.DesktopState; import com.google.android.msdl.data.model.MSDLToken; import java.util.Objects; /** * Divider for multi window splits. */ Loading Loading @@ -99,7 +101,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { // Calculation classes for "magnetic snap" user-controlled movement private DistanceGestureContext mDistanceGestureContext; private ViewMotionValue mViewMotionValue; private BreakpointKey mCurrentHapticBreakpoint; @Nullable private Integer mLastHoveredOverSnapPosition; /** * This is not the visible bounds you see on screen, but the actual behind-the-scenes window Loading Loading @@ -193,7 +195,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { return true; } DividerSnapAlgorithm.SnapTarget nextTarget = null; SnapTarget nextTarget = null; DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; if (action == R.id.action_move_tl_full) { nextTarget = snapAlgorithm.getDismissEndTarget(); Loading Loading @@ -379,20 +381,20 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDistanceGestureContext, mSplitLayout.mDividerSnapAlgorithm.getMotionSpec(), "dividerView::pos" /* label */); mLastHoveredOverSnapPosition = mSplitLayout.calculateCurrentSnapPosition(); mViewMotionValue.addUpdateCallback(viewMotionValue -> { // Whenever MotionValue updates (from user moving the divider): // - Place divider in its new position placeDivider((int) viewMotionValue.getOutput()); // Each MotionValue "segment" has two "breakpoints", one on each end. // We can uniquely identify each segment by just one of its breakpoints, // so we arbitrarily listen for changes to the "min-side" breakpoint // to determine when the user has moved the onto a new segment (i.e. // moved the divider from the "free-drag" segment to the "snapped" // segment, or vice versa). We play a haptic when this happens. if (!viewMotionValue.getSegmentKey().getMinBreakpoint() .equals(mCurrentHapticBreakpoint)) { // - Play a haptic if entering a magnetic zone Integer currentlyHoveredOverSnapZone = viewMotionValue.get( MagneticDividerUtils.getSNAP_POSITION_KEY()); if (currentlyHoveredOverSnapZone != null && !Objects.equals( currentlyHoveredOverSnapZone, mLastHoveredOverSnapPosition)) { playHapticClick(); } mCurrentHapticBreakpoint = viewMotionValue.getSegmentKey().getMinBreakpoint(); // - Update the last-hovered-over snap zone mLastHoveredOverSnapPosition = currentlyHoveredOverSnapZone; }); } } Loading Loading @@ -423,7 +425,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity(); final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos; final DividerSnapAlgorithm.SnapTarget snapTarget = final SnapTarget snapTarget = mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */); mSplitLayout.snapToTarget(position, snapTarget); mMoving = false; Loading Loading @@ -467,7 +469,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } mDistanceGestureContext = null; mViewMotionValue = null; mCurrentHapticBreakpoint = null; mLastHoveredOverSnapPosition = null; } private void setTouching() { Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/split/MagneticDividerUtils.kt +121 −43 Original line number Diff line number Diff line Loading @@ -17,16 +17,14 @@ package com.android.wm.shell.common.split import android.content.res.Resources import com.android.mechanics.spec.Breakpoint import com.android.mechanics.spec.Breakpoint.Companion.maxLimit import com.android.mechanics.spec.Breakpoint.Companion.minLimit import com.android.mechanics.spec.BreakpointKey import com.android.mechanics.spec.DirectionalMotionSpec import com.android.mechanics.spec.Guarantee import androidx.compose.ui.unit.dp import com.android.mechanics.spec.Mapping import com.android.mechanics.spec.MotionSpec import com.android.mechanics.spring.SpringParameters.Companion.Snap import com.android.wm.shell.common.pip.PipUtils import com.android.mechanics.spec.SemanticKey import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec import com.android.mechanics.spec.with import com.android.mechanics.spring.SpringParameters import com.android.mechanics.view.standardViewMotionBuilderContext import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget /** Loading @@ -36,48 +34,128 @@ import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget class MagneticDividerUtils { companion object { /** * The size of the "snap zone" (a zone around the snap point that attracts the divider.) * In dp. * 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 const val MAGNETIC_ZONE_SIZE = 30f private val DEFAULT_MAGNETIC_ATTACH_THRESHOLD = 56.dp /** The minimum spacing between snap zones, to prevent overlap on smaller displays. */ private val MINIMUM_SPACE_BETWEEN_SNAP_ZONES = 4.dp /** The stiffness of the magnetic snap effect. */ private const val ATTACH_STIFFNESS = 850f /** 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 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. */ @JvmStatic val SNAP_POSITION_KEY = SemanticKey<Int?>() /** * Create a MotionSpec that has "snap zones" for each of the SnapTargets provided. */ @JvmStatic fun generateMotionSpec(targets: List<SnapTarget>, res: Resources): MotionSpec { val breakpoints: MutableList<Breakpoint> = ArrayList() val mappings: MutableList<Mapping> = ArrayList() // 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. // First, get the position of the left-most (or top-most) dismiss point. val topLeftDismissTarget = targets.first() val topLeftDismissPosition = topLeftDismissTarget.position.toFloat() // 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( initialMapping = Mapping.Fixed(topLeftDismissPosition), semantics = listOf(SNAP_POSITION_KEY with null), 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. // Add the "min" breakpoint, the "max" breakpoint, and 2 breakpoints for each snap point // (for a total of n breakpoints). // Add n-1 mappings that go between the breakpoints breakpoints.add(minLimit) mappings.add(Mapping.Identity) for (i in targets.indices) { val t: SnapTarget = targets[i] val halfZoneSizePx = PipUtils.dpToPx(MAGNETIC_ZONE_SIZE, res.displayMetrics) / 2f val startOfZone = t.position - halfZoneSizePx val endOfZone = t.position + halfZoneSizePx // 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) val startOfMagneticZone = Breakpoint( BreakpointKey("snapzone$i::start", startOfZone), startOfZone, Snap, Guarantee.None // 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) ) val endOfMagneticZone = Breakpoint( BreakpointKey("snapzone$i::end", endOfZone), endOfZone, Snap, Guarantee.None // 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): for (i in (1 until targets.size - 1)) { val target = targets[i] val targetPosition = target.position.toFloat() // 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. semantics = listOf(SNAP_POSITION_KEY with target.snapPosition), delta = snapThreshold * (1 - ATTACH_DETACH_SCALE), fraction = ATTACH_DETACH_SCALE, ) breakpoints.add(startOfMagneticZone) mappings.add(Mapping.Fixed(t.position.toFloat())) breakpoints.add(endOfMagneticZone) mappings.add(Mapping.Identity) // Create another identity zone. identity( breakpoint = targetPosition + snapThreshold, semantics = listOf(SNAP_POSITION_KEY with null) ) } breakpoints.add(maxLimit) return MotionSpec(DirectionalMotionSpec(breakpoints, mappings)) // 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 with null) ) } } ) } } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/MagneticDividerUtilsTests.java +20 −42 Original line number Diff line number Diff line Loading @@ -16,75 +16,53 @@ package com.android.wm.shell.common.split; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; 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 static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.content.res.Resources; import android.util.DisplayMetrics; 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.pip.PipUtils; import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoSession; import java.util.List; @RunWith(AndroidJUnit4.class) public class MagneticDividerUtilsTests { private MockitoSession mMockitoSession; private final List<SnapTarget> mTargets = List.of( new SnapTarget(0, SNAP_TO_START_AND_DISMISS), new SnapTarget(100, SNAP_TO_2_10_90), new SnapTarget(500, SNAP_TO_2_50_50), new SnapTarget(900, SNAP_TO_2_90_10), new SnapTarget(1000, SNAP_TO_END_AND_DISMISS) ); @Mock Resources mResources; @Mock DisplayMetrics mDisplayMetrics; Resources mResources; @Before public void setup() { mMockitoSession = mockitoSession() .initMocks(this) .mockStatic(PipUtils.class) .startMocking(); } @After public void tearDown() { mMockitoSession.finishMocking(); mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources(); } @Test public void generateMotionSpec_producesCorrectNumberOfBreakpointsAndMappings() { when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics); when(PipUtils.dpToPx(anyFloat(), eq(mDisplayMetrics))).thenReturn(30); public void generateMotionSpec_worksOnThisDeviceWithoutCrashing() { int longEdge = Math.max( mResources.getDisplayMetrics().heightPixels, mResources.getDisplayMetrics().widthPixels ); MotionSpec motionSpec = MagneticDividerUtils.generateMotionSpec(mTargets, mResources); 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) ); // Expect 12 breakpoints: the "min" breakpoint, the "max" breakpoint, and 2 breakpoints for // each of the 5 snap points. assertEquals(12, motionSpec.getMaxDirection().getBreakpoints().size()); // Expect 11 mappings, that go between the breakpoints. assertEquals(11, motionSpec.getMaxDirection().getMappings().size()); // 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); } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +17 −15 Original line number Diff line number Diff line Loading @@ -55,18 +55,20 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.mechanics.spec.BreakpointKey; import com.android.mechanics.spec.InputDirection; import com.android.mechanics.view.DistanceGestureContext; import com.android.mechanics.view.ViewMotionValue; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.desktopmode.DesktopState; import com.google.android.msdl.data.model.MSDLToken; import java.util.Objects; /** * Divider for multi window splits. */ Loading Loading @@ -99,7 +101,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { // Calculation classes for "magnetic snap" user-controlled movement private DistanceGestureContext mDistanceGestureContext; private ViewMotionValue mViewMotionValue; private BreakpointKey mCurrentHapticBreakpoint; @Nullable private Integer mLastHoveredOverSnapPosition; /** * This is not the visible bounds you see on screen, but the actual behind-the-scenes window Loading Loading @@ -193,7 +195,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { return true; } DividerSnapAlgorithm.SnapTarget nextTarget = null; SnapTarget nextTarget = null; DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; if (action == R.id.action_move_tl_full) { nextTarget = snapAlgorithm.getDismissEndTarget(); Loading Loading @@ -379,20 +381,20 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mDistanceGestureContext, mSplitLayout.mDividerSnapAlgorithm.getMotionSpec(), "dividerView::pos" /* label */); mLastHoveredOverSnapPosition = mSplitLayout.calculateCurrentSnapPosition(); mViewMotionValue.addUpdateCallback(viewMotionValue -> { // Whenever MotionValue updates (from user moving the divider): // - Place divider in its new position placeDivider((int) viewMotionValue.getOutput()); // Each MotionValue "segment" has two "breakpoints", one on each end. // We can uniquely identify each segment by just one of its breakpoints, // so we arbitrarily listen for changes to the "min-side" breakpoint // to determine when the user has moved the onto a new segment (i.e. // moved the divider from the "free-drag" segment to the "snapped" // segment, or vice versa). We play a haptic when this happens. if (!viewMotionValue.getSegmentKey().getMinBreakpoint() .equals(mCurrentHapticBreakpoint)) { // - Play a haptic if entering a magnetic zone Integer currentlyHoveredOverSnapZone = viewMotionValue.get( MagneticDividerUtils.getSNAP_POSITION_KEY()); if (currentlyHoveredOverSnapZone != null && !Objects.equals( currentlyHoveredOverSnapZone, mLastHoveredOverSnapPosition)) { playHapticClick(); } mCurrentHapticBreakpoint = viewMotionValue.getSegmentKey().getMinBreakpoint(); // - Update the last-hovered-over snap zone mLastHoveredOverSnapPosition = currentlyHoveredOverSnapZone; }); } } Loading Loading @@ -423,7 +425,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { ? mVelocityTracker.getXVelocity() : mVelocityTracker.getYVelocity(); final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos; final DividerSnapAlgorithm.SnapTarget snapTarget = final SnapTarget snapTarget = mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */); mSplitLayout.snapToTarget(position, snapTarget); mMoving = false; Loading Loading @@ -467,7 +469,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } mDistanceGestureContext = null; mViewMotionValue = null; mCurrentHapticBreakpoint = null; mLastHoveredOverSnapPosition = null; } private void setTouching() { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/split/MagneticDividerUtils.kt +121 −43 Original line number Diff line number Diff line Loading @@ -17,16 +17,14 @@ package com.android.wm.shell.common.split import android.content.res.Resources import com.android.mechanics.spec.Breakpoint import com.android.mechanics.spec.Breakpoint.Companion.maxLimit import com.android.mechanics.spec.Breakpoint.Companion.minLimit import com.android.mechanics.spec.BreakpointKey import com.android.mechanics.spec.DirectionalMotionSpec import com.android.mechanics.spec.Guarantee import androidx.compose.ui.unit.dp import com.android.mechanics.spec.Mapping import com.android.mechanics.spec.MotionSpec import com.android.mechanics.spring.SpringParameters.Companion.Snap import com.android.wm.shell.common.pip.PipUtils import com.android.mechanics.spec.SemanticKey import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec import com.android.mechanics.spec.with import com.android.mechanics.spring.SpringParameters import com.android.mechanics.view.standardViewMotionBuilderContext import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget /** Loading @@ -36,48 +34,128 @@ import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget class MagneticDividerUtils { companion object { /** * The size of the "snap zone" (a zone around the snap point that attracts the divider.) * In dp. * 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 const val MAGNETIC_ZONE_SIZE = 30f private val DEFAULT_MAGNETIC_ATTACH_THRESHOLD = 56.dp /** The minimum spacing between snap zones, to prevent overlap on smaller displays. */ private val MINIMUM_SPACE_BETWEEN_SNAP_ZONES = 4.dp /** The stiffness of the magnetic snap effect. */ private const val ATTACH_STIFFNESS = 850f /** 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 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. */ @JvmStatic val SNAP_POSITION_KEY = SemanticKey<Int?>() /** * Create a MotionSpec that has "snap zones" for each of the SnapTargets provided. */ @JvmStatic fun generateMotionSpec(targets: List<SnapTarget>, res: Resources): MotionSpec { val breakpoints: MutableList<Breakpoint> = ArrayList() val mappings: MutableList<Mapping> = ArrayList() // 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. // First, get the position of the left-most (or top-most) dismiss point. val topLeftDismissTarget = targets.first() val topLeftDismissPosition = topLeftDismissTarget.position.toFloat() // 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( initialMapping = Mapping.Fixed(topLeftDismissPosition), semantics = listOf(SNAP_POSITION_KEY with null), 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. // Add the "min" breakpoint, the "max" breakpoint, and 2 breakpoints for each snap point // (for a total of n breakpoints). // Add n-1 mappings that go between the breakpoints breakpoints.add(minLimit) mappings.add(Mapping.Identity) for (i in targets.indices) { val t: SnapTarget = targets[i] val halfZoneSizePx = PipUtils.dpToPx(MAGNETIC_ZONE_SIZE, res.displayMetrics) / 2f val startOfZone = t.position - halfZoneSizePx val endOfZone = t.position + halfZoneSizePx // 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) val startOfMagneticZone = Breakpoint( BreakpointKey("snapzone$i::start", startOfZone), startOfZone, Snap, Guarantee.None // 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) ) val endOfMagneticZone = Breakpoint( BreakpointKey("snapzone$i::end", endOfZone), endOfZone, Snap, Guarantee.None // 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): for (i in (1 until targets.size - 1)) { val target = targets[i] val targetPosition = target.position.toFloat() // 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. semantics = listOf(SNAP_POSITION_KEY with target.snapPosition), delta = snapThreshold * (1 - ATTACH_DETACH_SCALE), fraction = ATTACH_DETACH_SCALE, ) breakpoints.add(startOfMagneticZone) mappings.add(Mapping.Fixed(t.position.toFloat())) breakpoints.add(endOfMagneticZone) mappings.add(Mapping.Identity) // Create another identity zone. identity( breakpoint = targetPosition + snapThreshold, semantics = listOf(SNAP_POSITION_KEY with null) ) } breakpoints.add(maxLimit) return MotionSpec(DirectionalMotionSpec(breakpoints, mappings)) // 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 with null) ) } } ) } } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/MagneticDividerUtilsTests.java +20 −42 Original line number Diff line number Diff line Loading @@ -16,75 +16,53 @@ package com.android.wm.shell.common.split; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; 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 static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.content.res.Resources; import android.util.DisplayMetrics; 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.pip.PipUtils; import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoSession; import java.util.List; @RunWith(AndroidJUnit4.class) public class MagneticDividerUtilsTests { private MockitoSession mMockitoSession; private final List<SnapTarget> mTargets = List.of( new SnapTarget(0, SNAP_TO_START_AND_DISMISS), new SnapTarget(100, SNAP_TO_2_10_90), new SnapTarget(500, SNAP_TO_2_50_50), new SnapTarget(900, SNAP_TO_2_90_10), new SnapTarget(1000, SNAP_TO_END_AND_DISMISS) ); @Mock Resources mResources; @Mock DisplayMetrics mDisplayMetrics; Resources mResources; @Before public void setup() { mMockitoSession = mockitoSession() .initMocks(this) .mockStatic(PipUtils.class) .startMocking(); } @After public void tearDown() { mMockitoSession.finishMocking(); mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources(); } @Test public void generateMotionSpec_producesCorrectNumberOfBreakpointsAndMappings() { when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics); when(PipUtils.dpToPx(anyFloat(), eq(mDisplayMetrics))).thenReturn(30); public void generateMotionSpec_worksOnThisDeviceWithoutCrashing() { int longEdge = Math.max( mResources.getDisplayMetrics().heightPixels, mResources.getDisplayMetrics().widthPixels ); MotionSpec motionSpec = MagneticDividerUtils.generateMotionSpec(mTargets, mResources); 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) ); // Expect 12 breakpoints: the "min" breakpoint, the "max" breakpoint, and 2 breakpoints for // each of the 5 snap points. assertEquals(12, motionSpec.getMaxDirection().getBreakpoints().size()); // Expect 11 mappings, that go between the breakpoints. assertEquals(11, motionSpec.getMaxDirection().getMappings().size()); // 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); } }