Files
lawnchair/wmshell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
2024-11-02 10:55:28 +08:00

671 lines
27 KiB
Kotlin

/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.bubbles
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.res.Resources
import android.graphics.Insets
import android.graphics.PointF
import android.graphics.Rect
import android.os.UserHandle
import android.view.WindowManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
import com.android.wm.shell.common.bubbles.BubbleBarLocation
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
/** Tests operations and the resulting state managed by [BubblePositioner]. */
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubblePositionerTest {
private lateinit var positioner: BubblePositioner
private val context = ApplicationProvider.getApplicationContext<Context>()
private val resources: Resources
get() = context.resources
private val defaultDeviceConfig =
DeviceConfig(
windowBounds = Rect(0, 0, 1000, 2000),
isLargeScreen = false,
isSmallTablet = false,
isLandscape = false,
isRtl = false,
insets = Insets.of(0, 0, 0, 0)
)
@Before
fun setUp() {
ProtoLog.REQUIRE_PROTOLOGTOOL = false
val windowManager = context.getSystemService(WindowManager::class.java)
positioner = BubblePositioner(context, windowManager)
}
@Test
fun testUpdate() {
val insets = Insets.of(10, 20, 5, 15)
val screenBounds = Rect(0, 0, 1000, 1200)
val availableRect = Rect(screenBounds)
availableRect.inset(insets)
positioner.update(defaultDeviceConfig.copy(insets = insets, windowBounds = screenBounds))
assertThat(positioner.availableRect).isEqualTo(availableRect)
assertThat(positioner.isLandscape).isFalse()
assertThat(positioner.isLargeScreen).isFalse()
assertThat(positioner.insets).isEqualTo(insets)
}
@Test
fun testShowBubblesVertically_phonePortrait() {
positioner.update(defaultDeviceConfig)
assertThat(positioner.showBubblesVertically()).isFalse()
}
@Test
fun testShowBubblesVertically_phoneLandscape() {
positioner.update(defaultDeviceConfig.copy(isLandscape = true))
assertThat(positioner.isLandscape).isTrue()
assertThat(positioner.showBubblesVertically()).isTrue()
}
@Test
fun testShowBubblesVertically_tablet() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
assertThat(positioner.showBubblesVertically()).isTrue()
}
/** If a resting position hasn't been set, calling it will return the default position. */
@Test
fun testGetRestingPosition_returnsDefaultPosition() {
positioner.update(defaultDeviceConfig)
val restingPosition = positioner.getRestingPosition()
val defaultPosition = positioner.defaultStartPosition
assertThat(restingPosition).isEqualTo(defaultPosition)
}
/** If a resting position has been set, it'll return that instead of the default position. */
@Test
fun testGetRestingPosition_returnsRestingPosition() {
positioner.update(defaultDeviceConfig)
val restingPosition = PointF(100f, 100f)
positioner.restingPosition = restingPosition
assertThat(positioner.getRestingPosition()).isEqualTo(restingPosition)
}
/** Test that the default resting position on phone is in upper left. */
@Test
fun testGetRestingPosition_bubble_onPhone() {
positioner.update(defaultDeviceConfig)
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
val restingPosition = positioner.getRestingPosition()
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
}
@Test
fun testGetRestingPosition_bubble_onPhone_RTL() {
positioner.update(defaultDeviceConfig.copy(isRtl = true))
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
val restingPosition = positioner.getRestingPosition()
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
}
/** Test that the default resting position on tablet is middle left. */
@Test
fun testGetRestingPosition_chatBubble_onTablet() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
val restingPosition = positioner.getRestingPosition()
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.left)
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
}
@Test
fun testGetRestingPosition_chatBubble_onTablet_RTL() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
val restingPosition = positioner.getRestingPosition()
assertThat(restingPosition.x).isEqualTo(allowableStackRegion.right)
assertThat(restingPosition.y).isEqualTo(defaultYPosition)
}
/** Test that the default resting position on tablet is middle right. */
@Test
fun testGetDefaultPosition_appBubble_onTablet() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
assertThat(startPosition.x).isEqualTo(allowableStackRegion.right)
assertThat(startPosition.y).isEqualTo(defaultYPosition)
}
@Test
fun testGetRestingPosition_appBubble_onTablet_RTL() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
assertThat(startPosition.x).isEqualTo(allowableStackRegion.left)
assertThat(startPosition.y).isEqualTo(defaultYPosition)
}
@Test
fun testGetRestingPosition_afterBoundsChange() {
positioner.update(
defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
)
// Set the resting position to the right side
var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY())
positioner.restingPosition = restingPosition
// Now make the device smaller
positioner.update(
defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
)
// Check the resting position is on the correct side
allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right)
}
@Test
fun testHasUserModifiedDefaultPosition_false() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
positioner.restingPosition = positioner.defaultStartPosition
assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
}
@Test
fun testHasUserModifiedDefaultPosition_true() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
positioner.restingPosition = PointF(0f, 100f)
assertThat(positioner.hasUserModifiedDefaultPosition()).isTrue()
}
@Test
fun testBubbleBarExpandedViewHeightAndWidth() {
val deviceConfig =
defaultDeviceConfig.copy(
// portrait orientation
isLandscape = false,
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.setShowingInBubbleBar(true)
positioner.update(deviceConfig)
positioner.bubbleBarTopOnScreen = 2500
val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680
val expandedViewVerticalSpacing =
resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
val expectedHeight =
spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing
val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width)
assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
}
@Test
fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() {
val screenWidth = 300
val deviceConfig =
defaultDeviceConfig.copy(
// portrait orientation
isLandscape = false,
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, screenWidth, 2600)
)
positioner.setShowingInBubbleBar(true)
positioner.update(deviceConfig)
positioner.bubbleBarTopOnScreen = 2500
val spaceBetweenTopInsetAndBubbleBarInLandscape = 180
val expandedViewSpacing =
resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing
val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing
assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
}
@Test
fun testGetExpandedViewHeight_max() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(MAX_HEIGHT)
}
@Test
fun testGetExpandedViewHeight_customHeight_valid() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val minHeight =
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
val bubble =
Bubble(
"key",
ShortcutInfo.Builder(context, "id").build(),
minHeight + 100 /* desiredHeight */,
0 /* desiredHeightResId */,
"title",
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
directExecutor()
) {}
// Ensure the height is the same as the desired value
assertThat(positioner.getExpandedViewHeight(bubble))
.isEqualTo(bubble.getDesiredHeight(context))
}
@Test
fun testGetExpandedViewHeight_customHeight_tooSmall() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val bubble =
Bubble(
"key",
ShortcutInfo.Builder(context, "id").build(),
10 /* desiredHeight */,
0 /* desiredHeightResId */,
"title",
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
directExecutor()
) {}
// Ensure the height is the same as the desired value
val minHeight =
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_default_height)
assertThat(positioner.getExpandedViewHeight(bubble)).isEqualTo(minHeight)
}
@Test
fun testGetMaxExpandedViewHeight_onLargeTablet() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val manageButtonHeight =
context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
val expandedViewPadding =
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
val expectedHeight =
1800 - 2 * 20 - manageButtonHeight - pointerWidth - expandedViewPadding * 2
assertThat(positioner.getMaxExpandedViewHeight(false /* isOverflow */))
.isEqualTo(expectedHeight)
}
@Test
fun testAreBubblesBottomAligned_largeScreen_true() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
assertThat(positioner.areBubblesBottomAligned()).isTrue()
}
@Test
fun testAreBubblesBottomAligned_largeScreen_landscape_false() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
isLandscape = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
assertThat(positioner.areBubblesBottomAligned()).isFalse()
}
@Test
fun testAreBubblesBottomAligned_smallTablet_false() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
isSmallTablet = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
assertThat(positioner.areBubblesBottomAligned()).isFalse()
}
@Test
fun testAreBubblesBottomAligned_phone_false() {
val deviceConfig =
defaultDeviceConfig.copy(
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
assertThat(positioner.areBubblesBottomAligned()).isFalse()
}
@Test
fun testExpandedViewY_phoneLandscape() {
val deviceConfig =
defaultDeviceConfig.copy(
isLandscape = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
// This bubble will have max height so it'll always be top aligned
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
.isEqualTo(positioner.getExpandedViewYTopAligned())
}
@Test
fun testExpandedViewY_phonePortrait() {
val deviceConfig =
defaultDeviceConfig.copy(
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
// Always top aligned in phone portrait
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
.isEqualTo(positioner.getExpandedViewYTopAligned())
}
@Test
fun testExpandedViewY_smallTabletLandscape() {
val deviceConfig =
defaultDeviceConfig.copy(
isSmallTablet = true,
isLandscape = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
// This bubble will have max height which is always top aligned on small tablets
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
.isEqualTo(positioner.getExpandedViewYTopAligned())
}
@Test
fun testExpandedViewY_smallTabletPortrait() {
val deviceConfig =
defaultDeviceConfig.copy(
isSmallTablet = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
// This bubble will have max height which is always top aligned on small tablets
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
.isEqualTo(positioner.getExpandedViewYTopAligned())
}
@Test
fun testExpandedViewY_largeScreenLandscape() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
isLandscape = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
// This bubble will have max height which is always top aligned on landscape, large tablet
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
.isEqualTo(positioner.getExpandedViewYTopAligned())
}
@Test
fun testExpandedViewY_largeScreenPortrait() {
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
insets = Insets.of(10, 20, 5, 15),
windowBounds = Rect(0, 0, 1800, 2600)
)
positioner.update(deviceConfig)
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), null, directExecutor())
val manageButtonHeight =
context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_height)
val manageButtonPlusMargin =
manageButtonHeight +
2 * context.resources.getDimensionPixelSize(R.dimen.bubble_manage_button_margin)
val pointerWidth = context.resources.getDimensionPixelSize(R.dimen.bubble_pointer_width)
val expectedExpandedViewY =
positioner.availableRect.bottom -
manageButtonPlusMargin -
positioner.getExpandedViewHeightForLargeScreen() -
pointerWidth
// Bubbles are bottom aligned on portrait, large tablet
assertThat(positioner.getExpandedViewY(bubble, 0f /* bubblePosition */))
.isEqualTo(expectedExpandedViewY)
}
@Test
fun testGetTaskViewContentWidth_onLeft() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
val paddings =
positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
assertThat(taskViewWidth)
.isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
fun testGetTaskViewContentWidth_onRight() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
val paddings =
positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
assertThat(taskViewWidth)
.isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
fun testIsBubbleBarOnLeft_defaultsToRight() {
positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
assertThat(positioner.isBubbleBarOnLeft).isFalse()
// Check that left and right return expected position
positioner.bubbleBarLocation = BubbleBarLocation.LEFT
assertThat(positioner.isBubbleBarOnLeft).isTrue()
positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
assertThat(positioner.isBubbleBarOnLeft).isFalse()
}
@Test
fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() {
positioner.update(defaultDeviceConfig.copy(isRtl = true))
positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
assertThat(positioner.isBubbleBarOnLeft).isTrue()
// Check that left and right return expected position
positioner.bubbleBarLocation = BubbleBarLocation.LEFT
assertThat(positioner.isBubbleBarOnLeft).isTrue()
positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
assertThat(positioner.isBubbleBarOnLeft).isFalse()
}
@Test
fun testGetBubbleBarExpandedViewBounds_onLeft() {
testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
}
@Test
fun testGetBubbleBarExpandedViewBounds_onRight() {
testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
}
@Test
fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
}
@Test
fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
}
private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
positioner.setShowingInBubbleBar(true)
val windowBounds = Rect(0, 0, 2000, 2600)
val insets = Insets.of(10, 20, 5, 15)
val deviceConfig =
defaultDeviceConfig.copy(
isLargeScreen = true,
isLandscape = true,
insets = insets,
windowBounds = windowBounds
)
positioner.update(deviceConfig)
val bubbleBarHeight = 100
positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight
val expandedViewPadding =
context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
val left: Int
val right: Int
if (onLeft) {
// Pin to the left, calculate right
left = deviceConfig.insets.left + expandedViewPadding
right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
} else {
// Pin to the right, calculate left
right =
deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
}
// Above the bubble bar
val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding
// Calculate right and top based on size
val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
val expectedBounds = Rect(left, top, right, bottom)
val bounds = Rect()
positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
assertThat(bounds).isEqualTo(expectedBounds)
}
private val defaultYPosition: Float
/**
* Calculates the Y position bubbles should be placed based on the config. Based on the
* calculations in [BubblePositioner.getDefaultStartPosition] and
* [BubbleStackView.RelativeStackPosition].
*/
get() {
val isTablet = positioner.isLargeScreen
// On tablet the position is centered, on phone it is an offset from the top.
val desiredY =
if (isTablet) {
positioner.screenRect.height() / 2f - positioner.bubbleSize / 2f
} else {
context.resources
.getDimensionPixelOffset(R.dimen.bubble_stack_starting_offset_y)
.toFloat()
}
// Since we're visually centering the bubbles on tablet, use total screen height rather
// than the available height.
val height =
if (isTablet) {
positioner.screenRect.height()
} else {
positioner.availableRect.height()
}
val offsetPercent = (desiredY / height).coerceIn(0f, 1f)
val allowableStackRegion =
positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
}
}