Loading libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveBoundsCalculator.kt +112 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.wm.shell.common import android.graphics.PointF import android.graphics.Rect import android.graphics.RectF import android.util.MathUtils.min /** * Utility class for calculating bounds during multi-display drag operations. Loading @@ -26,6 +27,8 @@ import android.graphics.RectF * This class provides helper functions to perform bounds calculation during window drag. */ object MultiDisplayDragMoveBoundsCalculator { private const val OVERHANG_DP = 96 /** * Calculates the global DP bounds of a window being dragged across displays. * Loading Loading @@ -67,6 +70,115 @@ object MultiDisplayDragMoveBoundsCalculator { return RectF(currentLeftDp, currentTopDp, currentRightDp, currentBottomDp) } /** * Adjusts window bounds to fit within the visible area of a display, including a small * "overhang" margin. * * For resizable windows, the bounds are simply intersected with the overhang region. For * non-resizable windows, it scales the window down, preserving its aspect ratio, to fit within * the allowed area. * * @param originalBounds The window's current bounds in screen pixel coordinates. * @param displayLayout The layout of the display where the bounds need to be constrained. * @param isResizeable True if the window can be resized, false otherwise. * @param pointerX The pointer's horizontal position in the display's local pixel coordinates, * used as a scaling anchor. * @return The adjusted bounds in screen pixel coordinates. */ @JvmStatic fun constrainBoundsForDisplay( originalBounds: Rect, displayLayout: DisplayLayout?, isResizeable: Boolean, pointerX: Float, ): Rect { if (displayLayout == null) { return originalBounds } // Define the allowed screen area, including a small overhang margin. val displayBoundsOverhang = Rect(0, 0, displayLayout.width(), displayLayout.height()) val overhang = displayLayout.dpToPx(OVERHANG_DP).toInt() displayBoundsOverhang.inset(-overhang, -overhang) if (displayBoundsOverhang.contains(originalBounds)) { return originalBounds } val intersectBounds = Rect() intersectBounds.setIntersect(displayBoundsOverhang, originalBounds) // For resizable windows, we employ a logic similar to window trimming. if (isResizeable) { return Rect(intersectBounds) } // For non-resizable windows, scale the window down to make sure all edges within overhang. if ( originalBounds.width() <= 0 || originalBounds.height() <= 0 || intersectBounds.width() <= 0 || intersectBounds.height() <= 0 ) { return intersectBounds } val scaleFactorHorizontal = intersectBounds.width().toFloat() / originalBounds.width() val scaleFactorVertical = intersectBounds.height().toFloat() / originalBounds.height() val scaleFactor = min(scaleFactorHorizontal, scaleFactorVertical) val isLeftCornerIn = displayBoundsOverhang.contains(originalBounds.left, originalBounds.top) val isRightCornerIn = displayBoundsOverhang.contains(originalBounds.right, originalBounds.top) if (isLeftCornerIn && isRightCornerIn) { // Case 1: Both top corners are on-screen. Anchor to the pointer's horizontal position. return scaleWithHorizontalOrigin(originalBounds, scaleFactor, pointerX) } else if (isLeftCornerIn) { // Case 2: Only the top-left corner is on-screen. Anchor to that corner. return scaleWithHorizontalOrigin( originalBounds, scaleFactor, originalBounds.left.toFloat(), ) } else if (isRightCornerIn) { // Case 3: Only the top-right corner is on-screen. Anchor to that corner. return scaleWithHorizontalOrigin( originalBounds, scaleFactor, originalBounds.right.toFloat(), ) } // Case 4: Both top corners are off-screen. if (scaleFactorHorizontal > scaleFactorVertical) { // The height is the limiting factor. We can still safely anchor to the pointer's // horizontal position while scaling to fit vertically. return scaleWithHorizontalOrigin(originalBounds, scaleFactorVertical, pointerX) } // The width is the limiting factor. To prevent anchoring to a potentially far-off-screen // point, we force the window's width to match the allowed display width, and then scales // the height proportionally to maintain the aspect ratio. return Rect( displayBoundsOverhang.left, originalBounds.top, displayBoundsOverhang.right, originalBounds.top + (originalBounds.height() * scaleFactorHorizontal).toInt(), ) } /** * Scales a Rect from a horizontal anchor point, keeping the top edge fixed. */ private fun scaleWithHorizontalOrigin( originalBounds: Rect, scaleFactor: Float, originX: Float, ): Rect { val height = (originalBounds.height() * scaleFactor).toInt() val left = (originX + (originalBounds.left - originX) * scaleFactor).toInt() val right = (originX + (originalBounds.right - originX) * scaleFactor).toInt() return Rect(left, originalBounds.top, right, originalBounds.top + height) } /** * Converts global DP bounds to local pixel bounds for a specific display. * Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +16 −1 Original line number Diff line number Diff line Loading @@ -101,6 +101,7 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent import com.android.wm.shell.common.RemoteCallable Loading Loading @@ -4670,10 +4671,24 @@ class DesktopTasksController( displayAreaInfo != null if (isCrossDisplayDrag) { val constrainedBounds = if ( DesktopExperienceFlags.ENABLE_SHRINK_WINDOW_BOUNDS_AFTER_DRAG.isTrue() ) { MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( destinationBounds, displayController.getDisplayLayout(newDisplayId), taskInfo.isResizeable, inputCoordinate.x, ) } else { Rect(destinationBounds) } moveToDisplay( taskInfo, newDisplayId, destinationBounds, constrainedBounds, dragToDisplayTransitionHandler, ) } else { Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveBoundsCalculatorTest.kt +150 −1 Original line number Diff line number Diff line Loading @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.common import android.content.res.Configuration Loading Loading @@ -81,4 +80,154 @@ class MultiDisplayDragMoveBoundsCalculatorTest : ShellTestCase() { val expectedBoundsPx = Rect(100, 1300, 400, 1500) assertEquals(expectedBoundsPx, actualBoundsPx) } @Test fun constrainBoundsForDisplay_nullDisplayLayout_returnsOriginalBounds() { val originalBounds = Rect(10, 20, 110, 120) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout = null, isResizeable = false, pointerX = 50f, ) assertEquals(originalBounds, result) } @Test fun constrainBoundsForDisplay_fullyContained_returnsOriginalBounds() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // Fits well within DISPLAY + overhang val originalBounds = Rect(10, 20, 110, 120) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = true, pointerX = 50f, ) assertEquals(originalBounds, result) } @Test fun constrainBoundsForDisplay_resizableExceedsOverhang_returnsIntersection() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // Window bounds exceeds right and bottom of overhang val originalBounds = Rect(10, 20, 250, 400) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = true, pointerX = 10f, ) val expectedBounds = Rect(10, 20, 124, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableTopEdgeContained_scaleBasedOnPointer() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=50, height=100, exceeding bottom. val originalBounds = Rect(10, 254, 60, 354) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 40f, ) // width=35, height=70 val expectedBounds = Rect(19, 254, 54, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableTopLeftCornerInside_scaleBasedOnTopLeftCorner() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=88, height=100, exceeding right. val originalBounds = Rect(80, 100, 168, 200) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 100f, ) // width=44, height=50 val expectedBounds = Rect(80, 100, 124, 150) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableTopRightCornerInside_scaleBasedOnTopRightCorner() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=106, height=200, exceeding left and bottom. val originalBounds = Rect(-30, 224, 76, 424) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 100f, ) // width=53, height=100 val expectedBounds = Rect(23, 224, 76, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableBothTopCornersOutsideHeightLimited_scaleBasedOnPointer() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=180, height=200, exceeding left, right and bottom. val originalBounds = Rect(-30, 224, 150, 424) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 40f, ) // width=90, height=100 val expectedBounds = Rect(5, 224, 95, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableBothTopCornersOutsideWidthLimited_scaleToFitDisplay() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=296, height=100, exceeding left, right and bottom. val originalBounds = Rect(-30, 54, 266, 154) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 40f, ) // width=148, height=50 val expectedBounds = Rect(-24, 54, 124, 104) assertEquals(expectedBounds, result) } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayTestUtil.kt +4 −0 Original line number Diff line number Diff line Loading @@ -42,6 +42,10 @@ object MultiDisplayTestUtil { fun getSpyDisplayLayout(resources: Resources): DisplayLayout { val displayInfo = DisplayInfo() displayInfo.logicalDensityDpi = dpi displayInfo.logicalWidth = (bounds.width() * dpi / DisplayMetrics.DENSITY_DEFAULT).toInt() displayInfo.logicalHeight = (bounds.height() * dpi / DisplayMetrics.DENSITY_DEFAULT).toInt() val displayLayout = spy(DisplayLayout(displayInfo, resources, true, true)) displayLayout.setGlobalBoundsDp(bounds) return displayLayout Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveBoundsCalculator.kt +112 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package com.android.wm.shell.common import android.graphics.PointF import android.graphics.Rect import android.graphics.RectF import android.util.MathUtils.min /** * Utility class for calculating bounds during multi-display drag operations. Loading @@ -26,6 +27,8 @@ import android.graphics.RectF * This class provides helper functions to perform bounds calculation during window drag. */ object MultiDisplayDragMoveBoundsCalculator { private const val OVERHANG_DP = 96 /** * Calculates the global DP bounds of a window being dragged across displays. * Loading Loading @@ -67,6 +70,115 @@ object MultiDisplayDragMoveBoundsCalculator { return RectF(currentLeftDp, currentTopDp, currentRightDp, currentBottomDp) } /** * Adjusts window bounds to fit within the visible area of a display, including a small * "overhang" margin. * * For resizable windows, the bounds are simply intersected with the overhang region. For * non-resizable windows, it scales the window down, preserving its aspect ratio, to fit within * the allowed area. * * @param originalBounds The window's current bounds in screen pixel coordinates. * @param displayLayout The layout of the display where the bounds need to be constrained. * @param isResizeable True if the window can be resized, false otherwise. * @param pointerX The pointer's horizontal position in the display's local pixel coordinates, * used as a scaling anchor. * @return The adjusted bounds in screen pixel coordinates. */ @JvmStatic fun constrainBoundsForDisplay( originalBounds: Rect, displayLayout: DisplayLayout?, isResizeable: Boolean, pointerX: Float, ): Rect { if (displayLayout == null) { return originalBounds } // Define the allowed screen area, including a small overhang margin. val displayBoundsOverhang = Rect(0, 0, displayLayout.width(), displayLayout.height()) val overhang = displayLayout.dpToPx(OVERHANG_DP).toInt() displayBoundsOverhang.inset(-overhang, -overhang) if (displayBoundsOverhang.contains(originalBounds)) { return originalBounds } val intersectBounds = Rect() intersectBounds.setIntersect(displayBoundsOverhang, originalBounds) // For resizable windows, we employ a logic similar to window trimming. if (isResizeable) { return Rect(intersectBounds) } // For non-resizable windows, scale the window down to make sure all edges within overhang. if ( originalBounds.width() <= 0 || originalBounds.height() <= 0 || intersectBounds.width() <= 0 || intersectBounds.height() <= 0 ) { return intersectBounds } val scaleFactorHorizontal = intersectBounds.width().toFloat() / originalBounds.width() val scaleFactorVertical = intersectBounds.height().toFloat() / originalBounds.height() val scaleFactor = min(scaleFactorHorizontal, scaleFactorVertical) val isLeftCornerIn = displayBoundsOverhang.contains(originalBounds.left, originalBounds.top) val isRightCornerIn = displayBoundsOverhang.contains(originalBounds.right, originalBounds.top) if (isLeftCornerIn && isRightCornerIn) { // Case 1: Both top corners are on-screen. Anchor to the pointer's horizontal position. return scaleWithHorizontalOrigin(originalBounds, scaleFactor, pointerX) } else if (isLeftCornerIn) { // Case 2: Only the top-left corner is on-screen. Anchor to that corner. return scaleWithHorizontalOrigin( originalBounds, scaleFactor, originalBounds.left.toFloat(), ) } else if (isRightCornerIn) { // Case 3: Only the top-right corner is on-screen. Anchor to that corner. return scaleWithHorizontalOrigin( originalBounds, scaleFactor, originalBounds.right.toFloat(), ) } // Case 4: Both top corners are off-screen. if (scaleFactorHorizontal > scaleFactorVertical) { // The height is the limiting factor. We can still safely anchor to the pointer's // horizontal position while scaling to fit vertically. return scaleWithHorizontalOrigin(originalBounds, scaleFactorVertical, pointerX) } // The width is the limiting factor. To prevent anchoring to a potentially far-off-screen // point, we force the window's width to match the allowed display width, and then scales // the height proportionally to maintain the aspect ratio. return Rect( displayBoundsOverhang.left, originalBounds.top, displayBoundsOverhang.right, originalBounds.top + (originalBounds.height() * scaleFactorHorizontal).toInt(), ) } /** * Scales a Rect from a horizontal anchor point, keeping the top edge fixed. */ private fun scaleWithHorizontalOrigin( originalBounds: Rect, scaleFactor: Float, originX: Float, ): Rect { val height = (originalBounds.height() * scaleFactor).toInt() val left = (originX + (originalBounds.left - originX) * scaleFactor).toInt() val right = (originX + (originalBounds.right - originX) * scaleFactor).toInt() return Rect(left, originalBounds.top, right, originalBounds.top + height) } /** * Converts global DP bounds to local pixel bounds for a specific display. * Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +16 −1 Original line number Diff line number Diff line Loading @@ -101,6 +101,7 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.HomeIntentProvider import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent import com.android.wm.shell.common.RemoteCallable Loading Loading @@ -4670,10 +4671,24 @@ class DesktopTasksController( displayAreaInfo != null if (isCrossDisplayDrag) { val constrainedBounds = if ( DesktopExperienceFlags.ENABLE_SHRINK_WINDOW_BOUNDS_AFTER_DRAG.isTrue() ) { MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( destinationBounds, displayController.getDisplayLayout(newDisplayId), taskInfo.isResizeable, inputCoordinate.x, ) } else { Rect(destinationBounds) } moveToDisplay( taskInfo, newDisplayId, destinationBounds, constrainedBounds, dragToDisplayTransitionHandler, ) } else { Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveBoundsCalculatorTest.kt +150 −1 Original line number Diff line number Diff line Loading @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.common import android.content.res.Configuration Loading Loading @@ -81,4 +80,154 @@ class MultiDisplayDragMoveBoundsCalculatorTest : ShellTestCase() { val expectedBoundsPx = Rect(100, 1300, 400, 1500) assertEquals(expectedBoundsPx, actualBoundsPx) } @Test fun constrainBoundsForDisplay_nullDisplayLayout_returnsOriginalBounds() { val originalBounds = Rect(10, 20, 110, 120) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout = null, isResizeable = false, pointerX = 50f, ) assertEquals(originalBounds, result) } @Test fun constrainBoundsForDisplay_fullyContained_returnsOriginalBounds() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // Fits well within DISPLAY + overhang val originalBounds = Rect(10, 20, 110, 120) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = true, pointerX = 50f, ) assertEquals(originalBounds, result) } @Test fun constrainBoundsForDisplay_resizableExceedsOverhang_returnsIntersection() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // Window bounds exceeds right and bottom of overhang val originalBounds = Rect(10, 20, 250, 400) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = true, pointerX = 10f, ) val expectedBounds = Rect(10, 20, 124, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableTopEdgeContained_scaleBasedOnPointer() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=50, height=100, exceeding bottom. val originalBounds = Rect(10, 254, 60, 354) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 40f, ) // width=35, height=70 val expectedBounds = Rect(19, 254, 54, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableTopLeftCornerInside_scaleBasedOnTopLeftCorner() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=88, height=100, exceeding right. val originalBounds = Rect(80, 100, 168, 200) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 100f, ) // width=44, height=50 val expectedBounds = Rect(80, 100, 124, 150) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableTopRightCornerInside_scaleBasedOnTopRightCorner() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=106, height=200, exceeding left and bottom. val originalBounds = Rect(-30, 224, 76, 424) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 100f, ) // width=53, height=100 val expectedBounds = Rect(23, 224, 76, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableBothTopCornersOutsideHeightLimited_scaleBasedOnPointer() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=180, height=200, exceeding left, right and bottom. val originalBounds = Rect(-30, 224, 150, 424) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 40f, ) // width=90, height=100 val expectedBounds = Rect(5, 224, 95, 324) assertEquals(expectedBounds, result) } @Test fun constrainBoundsForDisplay_nonResizableBothTopCornersOutsideWidthLimited_scaleToFitDisplay() { // displayBounds: (0, 0, 100, 300), displayBoundsOverhang: (-24, -24, 124, 324) val displayLayout = TestDisplay.DISPLAY_3.getSpyDisplayLayout(resources.resources) // width=296, height=100, exceeding left, right and bottom. val originalBounds = Rect(-30, 54, 266, 154) val result = MultiDisplayDragMoveBoundsCalculator.constrainBoundsForDisplay( originalBounds, displayLayout, isResizeable = false, pointerX = 40f, ) // width=148, height=50 val expectedBounds = Rect(-24, 54, 124, 104) assertEquals(expectedBounds, result) } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayTestUtil.kt +4 −0 Original line number Diff line number Diff line Loading @@ -42,6 +42,10 @@ object MultiDisplayTestUtil { fun getSpyDisplayLayout(resources: Resources): DisplayLayout { val displayInfo = DisplayInfo() displayInfo.logicalDensityDpi = dpi displayInfo.logicalWidth = (bounds.width() * dpi / DisplayMetrics.DENSITY_DEFAULT).toInt() displayInfo.logicalHeight = (bounds.height() * dpi / DisplayMetrics.DENSITY_DEFAULT).toInt() val displayLayout = spy(DisplayLayout(displayInfo, resources, true, true)) displayLayout.setGlobalBoundsDp(bounds) return displayLayout Loading