Merge "Allow drag/drop of display blocks" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
4e78151345
@@ -33,6 +33,7 @@ import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
|
|||||||
import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT
|
import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT
|
||||||
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
|
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
@@ -161,9 +162,41 @@ class TopologyScale(
|
|||||||
|
|
||||||
const val PREFERENCE_KEY = "display_topology_preference"
|
const val PREFERENCE_KEY = "display_topology_preference"
|
||||||
|
|
||||||
/** dp of padding on each side of a display block. */
|
/** Padding in pane coordinate pixels on each side of a display block. */
|
||||||
const val BLOCK_PADDING = 2
|
const val BLOCK_PADDING = 2
|
||||||
|
|
||||||
|
/** Represents a draggable block in the topology pane. */
|
||||||
|
class DisplayBlock(context : Context) : Button(context) {
|
||||||
|
init {
|
||||||
|
isScrollContainer = false
|
||||||
|
isVerticalScrollBarEnabled = false
|
||||||
|
isHorizontalScrollBarEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets position of the block given unpadded coordinates. */
|
||||||
|
fun place(topLeft : Point) {
|
||||||
|
x = (topLeft.x + BLOCK_PADDING).toFloat()
|
||||||
|
y = (topLeft.y + BLOCK_PADDING).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
val unpaddedX : Int
|
||||||
|
get() = (x - BLOCK_PADDING).toInt()
|
||||||
|
|
||||||
|
val unpaddedY : Int
|
||||||
|
get() = (y - BLOCK_PADDING).toInt()
|
||||||
|
|
||||||
|
/** Sets position and size of the block given unpadded bounds. */
|
||||||
|
fun placeAndSize(bounds : RectF, scale : TopologyScale) {
|
||||||
|
val topLeft = scale.displayToPaneCoor(PointF(bounds.left, bounds.top))
|
||||||
|
val bottomRight = scale.displayToPaneCoor(PointF(bounds.right, bounds.bottom))
|
||||||
|
val layout = layoutParams
|
||||||
|
layout.width = bottomRight.x - topLeft.x - BLOCK_PADDING * 2
|
||||||
|
layout.height = bottomRight.y - topLeft.y - BLOCK_PADDING * 2
|
||||||
|
layoutParams = layout
|
||||||
|
place(topLeft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DisplayTopologyPreference allows the user to change the display topology
|
* DisplayTopologyPreference allows the user to change the display topology
|
||||||
* when there is one or more extended display attached.
|
* when there is one or more extended display attached.
|
||||||
@@ -190,7 +223,7 @@ class DisplayTopologyPreference(context : Context)
|
|||||||
|
|
||||||
key = PREFERENCE_KEY
|
key = PREFERENCE_KEY
|
||||||
|
|
||||||
injector = Injector()
|
injector = Injector(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||||
@@ -223,74 +256,157 @@ class DisplayTopologyPreference(context : Context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Injector {
|
open class Injector(val context : Context) {
|
||||||
open fun displayTopology(context : Context) : DisplayTopology? {
|
/**
|
||||||
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
* Lazy property for Display Manager, to prevent eagerly getting the service in unit tests.
|
||||||
return displayManager.displayTopology
|
*/
|
||||||
|
private val displayManager : DisplayManager by lazy {
|
||||||
|
context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun wallpaper(context : Context) : Drawable {
|
open var displayTopology : DisplayTopology?
|
||||||
return WallpaperManager.getInstance(context).drawable ?: ColorDrawable(Color.BLACK)
|
get() = displayManager.displayTopology
|
||||||
}
|
set(value) { displayManager.displayTopology = value }
|
||||||
|
|
||||||
|
open val wallpaper : Drawable
|
||||||
|
get() = WallpaperManager.getInstance(context).drawable ?: ColorDrawable(Color.BLACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calcAbsRects(
|
/**
|
||||||
dest : MutableMap<Int, RectF>, n : DisplayTopology.TreeNode, x : Float, y : Float) {
|
* Holds information about the current system topology.
|
||||||
dest.put(n.displayId, RectF(x, y, x + n.width, y + n.height))
|
* @param positions list of displays comprised of the display ID and position
|
||||||
|
*/
|
||||||
|
private data class TopologyInfo(
|
||||||
|
val topology: DisplayTopology, val scaling: TopologyScale,
|
||||||
|
val positions: List<Pair<Int, RectF>>)
|
||||||
|
|
||||||
for (c in n.children) {
|
/**
|
||||||
val (xoff, yoff) = when (c.position) {
|
* Holds information about the current drag operation.
|
||||||
POSITION_LEFT -> Pair(-c.width, +c.offset)
|
* @param stationaryDisps ID and position of displays that are not moving
|
||||||
POSITION_RIGHT -> Pair(+n.width, +c.offset)
|
* @param display View that is currently being dragged
|
||||||
POSITION_TOP -> Pair(+c.offset, -c.height)
|
* @param displayId ID of display being dragged
|
||||||
POSITION_BOTTOM -> Pair(+c.offset, +n.height)
|
* @param displayWidth width of display being dragged in actual (not View) coordinates
|
||||||
else -> throw IllegalStateException("invalid position for display: ${c}")
|
* @param displayHeight height of display being dragged in actual (not View) coordinates
|
||||||
}
|
* @param dragOffsetX difference between event rawX coordinate and X of the display in the pane
|
||||||
calcAbsRects(dest, c, x + xoff, y + yoff)
|
* @param dragOffsetY difference between event rawY coordinate and Y of the display in the pane
|
||||||
|
*/
|
||||||
|
private data class BlockDrag(
|
||||||
|
val stationaryDisps : List<Pair<Int, RectF>>,
|
||||||
|
val display: DisplayBlock, val displayId: Int,
|
||||||
|
val displayWidth: Float, val displayHeight: Float,
|
||||||
|
val dragOffsetX: Float, val dragOffsetY: Float)
|
||||||
|
|
||||||
|
private var mTopologyInfo : TopologyInfo? = null
|
||||||
|
private var mDrag : BlockDrag? = null
|
||||||
|
|
||||||
|
@VisibleForTesting fun refreshPane() {
|
||||||
|
val recycleableBlocks = ArrayDeque<DisplayBlock>()
|
||||||
|
for (i in 0..mPaneContent.childCount-1) {
|
||||||
|
recycleableBlocks.add(mPaneContent.getChildAt(i) as DisplayBlock)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshPane() {
|
val topology = injector.displayTopology
|
||||||
mPaneContent.removeAllViews()
|
if (topology == null) {
|
||||||
|
|
||||||
val root = injector.displayTopology(context)?.root
|
|
||||||
if (root == null) {
|
|
||||||
// This occurs when no topology is active.
|
// This occurs when no topology is active.
|
||||||
// TODO(b/352648432): show main display or mirrored displays rather than an empty pane.
|
// TODO(b/352648432): show main display or mirrored displays rather than an empty pane.
|
||||||
mTopologyHint.text = ""
|
mTopologyHint.text = ""
|
||||||
|
mPaneContent.removeAllViews()
|
||||||
|
mTopologyInfo = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mTopologyHint.text = context.getString(R.string.external_display_topology_hint)
|
mTopologyHint.text = context.getString(R.string.external_display_topology_hint)
|
||||||
|
|
||||||
val blocksPos = buildMap { calcAbsRects(this, root, x = 0f, y = 0f) }
|
val blocksPos = buildList {
|
||||||
|
val bounds = topology.absoluteBounds
|
||||||
|
(0..bounds.size()-1).forEach {
|
||||||
|
add(Pair(bounds.keyAt(it), bounds.valueAt(it)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val scaling = TopologyScale(
|
val scaling = TopologyScale(
|
||||||
mPaneContent.width, minEdgeLength = 60, maxBlockRatio = 0.12f, blocksPos.values)
|
mPaneContent.width, minEdgeLength = 60, maxBlockRatio = 0.12f,
|
||||||
|
blocksPos.map { it.second }.toList())
|
||||||
mPaneHolder.layoutParams.let {
|
mPaneHolder.layoutParams.let {
|
||||||
if (it.height != scaling.paneHeight) {
|
if (it.height != scaling.paneHeight) {
|
||||||
it.height = scaling.paneHeight
|
it.height = scaling.paneHeight
|
||||||
mPaneHolder.layoutParams = it
|
mPaneHolder.layoutParams = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val wallpaper = injector.wallpaper(context)
|
|
||||||
blocksPos.values.forEach { p ->
|
blocksPos.forEach { (id, pos) ->
|
||||||
Button(context).apply {
|
val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(context).apply {
|
||||||
isScrollContainer = false
|
// We need a separate wallpaper Drawable for each display block, since each needs to
|
||||||
isVerticalScrollBarEnabled = false
|
// be drawn at a separate size.
|
||||||
isHorizontalScrollBarEnabled = false
|
background = injector.wallpaper
|
||||||
background = wallpaper
|
|
||||||
val topLeft = scaling.displayToPaneCoor(PointF(p.left, p.top))
|
|
||||||
val bottomRight = scaling.displayToPaneCoor(PointF(p.right, p.bottom))
|
|
||||||
|
|
||||||
mPaneContent.addView(this)
|
mPaneContent.addView(this)
|
||||||
|
}
|
||||||
|
|
||||||
val layout = layoutParams
|
block.placeAndSize(pos, scaling)
|
||||||
layout.width = bottomRight.x - topLeft.x - BLOCK_PADDING * 2
|
block.setOnTouchListener { view, ev ->
|
||||||
layout.height = bottomRight.y - topLeft.y - BLOCK_PADDING * 2
|
when (ev.actionMasked) {
|
||||||
layoutParams = layout
|
MotionEvent.ACTION_DOWN -> onBlockTouchDown(id, pos, block, ev)
|
||||||
x = (topLeft.x + BLOCK_PADDING).toFloat()
|
MotionEvent.ACTION_MOVE -> onBlockTouchMove(ev)
|
||||||
y = (topLeft.y + BLOCK_PADDING).toFloat()
|
MotionEvent.ACTION_UP -> onBlockTouchUp()
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
mPaneContent.removeViews(blocksPos.size, recycleableBlocks.size)
|
||||||
|
|
||||||
|
mTopologyInfo = TopologyInfo(topology, scaling, blocksPos)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBlockTouchDown(
|
||||||
|
displayId: Int, displayPos: RectF, block: DisplayBlock, ev: MotionEvent): Boolean {
|
||||||
|
val stationaryDisps = (mTopologyInfo ?: return false)
|
||||||
|
.positions.filter { it.first != displayId }
|
||||||
|
|
||||||
|
// We have to use rawX and rawY for the coordinates since the view receiving the event is
|
||||||
|
// also the view that is moving. We need coordinates relative to something that isn't
|
||||||
|
// moving, and the raw coordinates are relative to the screen.
|
||||||
|
mDrag = BlockDrag(
|
||||||
|
stationaryDisps.toList(), block, displayId, displayPos.width(), displayPos.height(),
|
||||||
|
ev.rawX - block.unpaddedX, ev.rawY - block.unpaddedY)
|
||||||
|
|
||||||
|
// Prevents a container of this view from intercepting the touch events in the case the
|
||||||
|
// pointer moves outside of the display block or the pane.
|
||||||
|
mPaneContent.requestDisallowInterceptTouchEvent(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBlockTouchMove(ev: MotionEvent): Boolean {
|
||||||
|
val drag = mDrag ?: return false
|
||||||
|
val topology = mTopologyInfo ?: return false
|
||||||
|
val dispDragCoor = topology.scaling.paneToDisplayCoor(Point(
|
||||||
|
(ev.rawX - drag.dragOffsetX).toInt(),
|
||||||
|
(ev.rawY - drag.dragOffsetY).toInt()))
|
||||||
|
val dispDragRect = RectF(
|
||||||
|
dispDragCoor.x, dispDragCoor.y,
|
||||||
|
dispDragCoor.x + drag.displayWidth, dispDragCoor.y + drag.displayHeight)
|
||||||
|
val snapRect = clampPosition(drag.stationaryDisps.map { it.second }, dispDragRect)
|
||||||
|
|
||||||
|
drag.display.place(topology.scaling.displayToPaneCoor(PointF(snapRect.left, snapRect.top)))
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onBlockTouchUp(): Boolean {
|
||||||
|
val drag = mDrag ?: return false
|
||||||
|
val topology = mTopologyInfo ?: return false
|
||||||
|
mPaneContent.requestDisallowInterceptTouchEvent(false)
|
||||||
|
|
||||||
|
val newCoor = topology.scaling.paneToDisplayCoor(
|
||||||
|
Point(drag.display.unpaddedX, drag.display.unpaddedY))
|
||||||
|
val newTopology = topology.topology.copy()
|
||||||
|
val newPositions = drag.stationaryDisps.map { (id, pos) -> id to PointF(pos.left, pos.top) }
|
||||||
|
.plus(drag.displayId to newCoor)
|
||||||
|
|
||||||
|
val arr = hashMapOf(*newPositions.toTypedArray())
|
||||||
|
newTopology.rearrange(arr)
|
||||||
|
injector.displayTopology = newTopology
|
||||||
|
|
||||||
|
refreshPane()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.android.settings.connecteddevice.display
|
package com.android.settings.connecteddevice.display
|
||||||
|
|
||||||
|
import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM
|
||||||
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
|
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -23,10 +24,12 @@ import android.graphics.Color
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.hardware.display.DisplayTopology
|
import android.hardware.display.DisplayTopology
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.preference.PreferenceViewHolder
|
import androidx.preference.PreferenceViewHolder
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.core.view.MotionEventBuilder
|
||||||
|
|
||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
@@ -39,7 +42,7 @@ import org.robolectric.RobolectricTestRunner
|
|||||||
class DisplayTopologyPreferenceTest {
|
class DisplayTopologyPreferenceTest {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
val preference = DisplayTopologyPreference(context)
|
val preference = DisplayTopologyPreference(context)
|
||||||
val injector = TestInjector()
|
val injector = TestInjector(context)
|
||||||
val rootView = View.inflate(context, preference.layoutResource, /*parent=*/ null)
|
val rootView = View.inflate(context, preference.layoutResource, /*parent=*/ null)
|
||||||
val holder = PreferenceViewHolder.createInstanceForTests(rootView)
|
val holder = PreferenceViewHolder.createInstanceForTests(rootView)
|
||||||
val wallpaper = ColorDrawable(Color.MAGENTA)
|
val wallpaper = ColorDrawable(Color.MAGENTA)
|
||||||
@@ -50,13 +53,16 @@ class DisplayTopologyPreferenceTest {
|
|||||||
preference.onBindViewHolder(holder)
|
preference.onBindViewHolder(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestInjector : DisplayTopologyPreference.Injector() {
|
class TestInjector(context : Context) : DisplayTopologyPreference.Injector(context) {
|
||||||
var topology : DisplayTopology? = null
|
var topology : DisplayTopology? = null
|
||||||
var systemWallpaper : Drawable? = null
|
var systemWallpaper : Drawable? = null
|
||||||
|
|
||||||
override fun displayTopology(context : Context) : DisplayTopology? { return topology }
|
override var displayTopology : DisplayTopology?
|
||||||
|
get() = topology
|
||||||
|
set(value) { topology = value }
|
||||||
|
|
||||||
override fun wallpaper(context : Context) : Drawable { return systemWallpaper!! }
|
override val wallpaper : Drawable
|
||||||
|
get() = systemWallpaper!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -70,8 +76,16 @@ class DisplayTopologyPreferenceTest {
|
|||||||
assertThat(preference.mTopologyHint.text).isEqualTo("")
|
assertThat(preference.mTopologyHint.text).isEqualTo("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private fun getPaneChildren(): List<DisplayBlock> =
|
||||||
fun twoDisplaysGenerateBlocks() {
|
(0..preference.mPaneContent.childCount-1)
|
||||||
|
.map { preference.mPaneContent.getChildAt(it) as DisplayBlock }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a simple topology in the pane with two displays. Returns the left-hand display and
|
||||||
|
* right-hand display in order in a list. The right-hand display is the root.
|
||||||
|
*/
|
||||||
|
fun setupTwoDisplays(): List<DisplayBlock> {
|
||||||
val child = DisplayTopology.TreeNode(
|
val child = DisplayTopology.TreeNode(
|
||||||
/* displayId= */ 42, /* width= */ 100f, /* height= */ 80f,
|
/* displayId= */ 42, /* width= */ 100f, /* height= */ 80f,
|
||||||
POSITION_LEFT, /* offset= */ 42f)
|
POSITION_LEFT, /* offset= */ 42f)
|
||||||
@@ -93,15 +107,19 @@ class DisplayTopologyPreferenceTest {
|
|||||||
preference.onAttached()
|
preference.onAttached()
|
||||||
preference.onGlobalLayout()
|
preference.onGlobalLayout()
|
||||||
|
|
||||||
assertThat(preference.mPaneContent.childCount).isEqualTo(2)
|
val paneChildren = getPaneChildren()
|
||||||
val block0 = preference.mPaneContent.getChildAt(0)
|
assertThat(paneChildren).hasSize(2)
|
||||||
val block1 = preference.mPaneContent.getChildAt(1)
|
|
||||||
|
|
||||||
// Block of child display is on the left.
|
// Block of child display is on the left.
|
||||||
val (childBlock, rootBlock) = if (block0.x < block1.x)
|
return if (paneChildren[0].x < paneChildren[1].x)
|
||||||
listOf(block0, block1)
|
paneChildren
|
||||||
else
|
else
|
||||||
listOf(block1, block0)
|
paneChildren.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun twoDisplaysGenerateBlocks() {
|
||||||
|
val (childBlock, rootBlock) = setupTwoDisplays()
|
||||||
|
|
||||||
// After accounting for padding, child should be half the length of root in each dimension.
|
// After accounting for padding, child should be half the length of root in each dimension.
|
||||||
assertThat(childBlock.layoutParams.width + BLOCK_PADDING)
|
assertThat(childBlock.layoutParams.width + BLOCK_PADDING)
|
||||||
@@ -109,12 +127,96 @@ class DisplayTopologyPreferenceTest {
|
|||||||
assertThat(childBlock.layoutParams.height + BLOCK_PADDING)
|
assertThat(childBlock.layoutParams.height + BLOCK_PADDING)
|
||||||
.isEqualTo(rootBlock.layoutParams.height / 2)
|
.isEqualTo(rootBlock.layoutParams.height / 2)
|
||||||
assertThat(childBlock.y).isGreaterThan(rootBlock.y)
|
assertThat(childBlock.y).isGreaterThan(rootBlock.y)
|
||||||
assertThat(block0.background).isEqualTo(wallpaper)
|
assertThat(childBlock.background).isEqualTo(wallpaper)
|
||||||
assertThat(block1.background).isEqualTo(wallpaper)
|
assertThat(rootBlock.background).isEqualTo(wallpaper)
|
||||||
assertThat(rootBlock.x - BLOCK_PADDING * 2)
|
assertThat(rootBlock.x - BLOCK_PADDING * 2)
|
||||||
.isEqualTo(childBlock.x + childBlock.layoutParams.width)
|
.isEqualTo(childBlock.x + childBlock.layoutParams.width)
|
||||||
|
|
||||||
assertThat(preference.mTopologyHint.text)
|
assertThat(preference.mTopologyHint.text)
|
||||||
.isEqualTo(context.getString(R.string.external_display_topology_hint))
|
.isEqualTo(context.getString(R.string.external_display_topology_hint))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dragDisplayDownward() {
|
||||||
|
val (leftBlock, rightBlock) = setupTwoDisplays()
|
||||||
|
|
||||||
|
val downEvent = MotionEventBuilder.newBuilder()
|
||||||
|
.setPointer(0f, 0f)
|
||||||
|
.setAction(MotionEvent.ACTION_DOWN)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Move the left block half of its height downward. This is 40 pixels in display
|
||||||
|
// coordinates. The original offset is 42, so the new offset will be 42 + 40.
|
||||||
|
val moveEvent = MotionEventBuilder.newBuilder()
|
||||||
|
.setAction(MotionEvent.ACTION_MOVE)
|
||||||
|
.setPointer(0f, leftBlock.layoutParams.height / 2f + BLOCK_PADDING)
|
||||||
|
.build()
|
||||||
|
val upEvent = MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build()
|
||||||
|
|
||||||
|
leftBlock.dispatchTouchEvent(downEvent)
|
||||||
|
leftBlock.dispatchTouchEvent(moveEvent)
|
||||||
|
leftBlock.dispatchTouchEvent(upEvent)
|
||||||
|
|
||||||
|
val rootChildren = injector.topology!!.root!!.children
|
||||||
|
assertThat(rootChildren).hasSize(1)
|
||||||
|
val child = rootChildren[0]
|
||||||
|
assertThat(child.position).isEqualTo(POSITION_LEFT)
|
||||||
|
assertThat(child.offset).isWithin(1f).of(82f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dragRootDisplayToNewSide() {
|
||||||
|
val (leftBlock, rightBlock) = setupTwoDisplays()
|
||||||
|
|
||||||
|
val downEvent = MotionEventBuilder.newBuilder()
|
||||||
|
.setAction(MotionEvent.ACTION_DOWN)
|
||||||
|
.setPointer(0f, 0f)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Move the right block left and upward. We won't move it into exactly the correct position,
|
||||||
|
// relying on the clamp algorithm to choose the correct side and offset.
|
||||||
|
val moveEvent = MotionEventBuilder.newBuilder()
|
||||||
|
.setAction(MotionEvent.ACTION_MOVE)
|
||||||
|
.setPointer(
|
||||||
|
-leftBlock.layoutParams.width - 2f * BLOCK_PADDING,
|
||||||
|
-leftBlock.layoutParams.height / 2f)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val upEvent = MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_UP).build()
|
||||||
|
|
||||||
|
assertThat(leftBlock.y).isGreaterThan(rightBlock.y)
|
||||||
|
|
||||||
|
rightBlock.dispatchTouchEvent(downEvent)
|
||||||
|
rightBlock.dispatchTouchEvent(moveEvent)
|
||||||
|
rightBlock.dispatchTouchEvent(upEvent)
|
||||||
|
|
||||||
|
val rootChildren = injector.topology!!.root!!.children
|
||||||
|
assertThat(rootChildren).hasSize(1)
|
||||||
|
val child = rootChildren[0]
|
||||||
|
assertThat(child.position).isEqualTo(POSITION_BOTTOM)
|
||||||
|
assertThat(child.offset).isWithin(1f).of(0f)
|
||||||
|
|
||||||
|
// After rearranging blocks, the original block views should still be present.
|
||||||
|
val paneChildren = getPaneChildren()
|
||||||
|
assertThat(paneChildren.indexOf(leftBlock)).isNotEqualTo(-1)
|
||||||
|
assertThat(paneChildren.indexOf(rightBlock)).isNotEqualTo(-1)
|
||||||
|
|
||||||
|
// Left edge of both blocks should be aligned after dragging.
|
||||||
|
assertThat(paneChildren[0].x)
|
||||||
|
.isWithin(1f)
|
||||||
|
.of(paneChildren[1].x)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun keepOriginalViewsWhenAddingMore() {
|
||||||
|
setupTwoDisplays()
|
||||||
|
val childrenBefore = getPaneChildren()
|
||||||
|
injector.topology!!.addDisplay(/* displayId= */ 101, 320f, 240f)
|
||||||
|
preference.refreshPane()
|
||||||
|
val childrenAfter = getPaneChildren()
|
||||||
|
|
||||||
|
assertThat(childrenBefore).hasSize(2)
|
||||||
|
assertThat(childrenAfter).hasSize(3)
|
||||||
|
assertThat(childrenAfter.subList(0, 2)).isEqualTo(childrenBefore)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user