mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(radar): keep the full-screen circle width share in small windows
The radar circle is a square, so it is limited by the shorter side of the card it is drawn in: the small-window layout gave the radar card a height of ~156dp in a 411x450 window, which shrank the circle to ~36% of the page width while the full-screen layout shows ~84%. - measure the real window with BoxWithConstraints instead of trusting the window size class, which can still report the full screen while the app runs in a split/floating window - when the classic 1:1 split would give the radar less than 75% of the page width, use a fill layout: the radar takes the largest square that fits, a usable pager block is reserved under it while that keeps the circle at its full-screen share, otherwise the pager collapses into a 48dp translucent tab strip with a tappable panel - keep the normal two-row top area in the vertical layout: TimerRow and NextPassRow both carry weight(1f) internally, so putting them in a single row splits that row into two 50% blocks - extract the geometry into pure Dp functions (useFillRadarLayout, radarFillSizes) covered by unit tests
This commit is contained in:
1 parent
800494b978
commit
e9f58f76f6
3 files changed
+372
-24
No files matched your search
@@ -10,4 +10,6 @@ dependencies {
|
||||
implementation(project(":feature:mutual"))
|
||||
implementation(project(":feature:cw"))
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
}
|
||||
testImplementation(libs.test.junit4)
|
||||
testImplementation(libs.test.coroutines)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
@@ -35,6 +36,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -45,19 +47,24 @@ import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.keepScreenOn
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -88,8 +95,84 @@ private enum class RadarPage(val title: String) {
|
||||
Sstv("SSTV")
|
||||
}
|
||||
|
||||
/** Compact pager strip height in split-screen / small windows. */
|
||||
private val COMPACT_PAGER_HEIGHT = 132.dp
|
||||
/**
|
||||
* Always-visible translucent pager tab strip drawn over the radar in small windows.
|
||||
*/
|
||||
private val COMPACT_TAB_STRIP_HEIGHT = 48.dp
|
||||
|
||||
/**
|
||||
* Smallest pager block worth reserving below the radar before falling back to the
|
||||
* overlaid tab strip + panel. Below this the pager would be unusable anyway.
|
||||
*/
|
||||
private val COMPACT_MIN_PAGER_HEIGHT = 96.dp
|
||||
|
||||
/**
|
||||
* Vertical chrome the classic layout spends outside the radar: two 48dp top bar rows
|
||||
* plus three 6dp row gaps (bar/bar/radar/pager).
|
||||
*/
|
||||
private val CLASSIC_VERTICAL_CHROME = 114.dp
|
||||
|
||||
/**
|
||||
* The radar circle is 0.95 x min(cardWidth, cardHeight), so a card that is wider than it
|
||||
* is tall always shows a small circle with empty bands on the sides. Full screen the radar
|
||||
* card ends up ~0.88 x cardWidth tall (circle ~84% of the page width). Once the classic
|
||||
* 1:1 split on a vertical window would give the radar less than this share of its own
|
||||
* width, the fill layout takes over so the circle keeps the full-screen width proportion
|
||||
* instead of shrinking with the window height.
|
||||
*/
|
||||
private const val FILL_RADAR_SPLIT_RATIO = 0.75f
|
||||
|
||||
/**
|
||||
* Smallest radar side (as a share of the page width) worth keeping a reserved pager block for.
|
||||
* 0.88 x 0.95 = ~0.84 = the circle's share of the page width full screen, so the pager is only
|
||||
* preserved while reserving it costs the circle nothing against the full-screen proportion.
|
||||
*/
|
||||
private const val MIN_RADAR_SHARE_WITH_PAGER = 0.88f
|
||||
|
||||
/** Vertical gap kept between the stacked cards. */
|
||||
private val ROW_GAP = 6.dp
|
||||
|
||||
/**
|
||||
* True when a vertical window is too short for the classic 1:1 split to keep the radar circle
|
||||
* at its full-screen share of the page width. The split gives the radar
|
||||
* `(height - chrome) / 2`, so a short window caps the circle by that height instead of by the
|
||||
* page width.
|
||||
*/
|
||||
internal fun useFillRadarLayout(isVertical: Boolean, maxWidth: Dp, maxHeight: Dp): Boolean =
|
||||
isVertical && (maxHeight - CLASSIC_VERTICAL_CHROME) / 2 < maxWidth * FILL_RADAR_SPLIT_RATIO
|
||||
|
||||
/**
|
||||
* Geometry of the fill layout: the square the radar keeps for itself (largest square that fits,
|
||||
* so the circle spans the page width like it does full screen) and the space left for the pager.
|
||||
* When that space is too small to be usable the pager is overlaid on the plot instead.
|
||||
*/
|
||||
internal data class RadarFillSizes(
|
||||
val radarSide: Dp,
|
||||
val pagerSpace: Dp,
|
||||
val pagerOverlaid: Boolean
|
||||
)
|
||||
|
||||
internal fun radarFillSizes(maxWidth: Dp, maxHeight: Dp): RadarFillSizes {
|
||||
// Preferred: keep a usable pager block under the radar square. That is only worth it while
|
||||
// the circle still reaches its full-screen share of the page width.
|
||||
val radarWithPager = minOf(maxWidth, maxHeight - COMPACT_MIN_PAGER_HEIGHT - ROW_GAP)
|
||||
if (radarWithPager >= maxWidth * MIN_RADAR_SHARE_WITH_PAGER) {
|
||||
return RadarFillSizes(
|
||||
radarSide = radarWithPager,
|
||||
pagerSpace = maxHeight - radarWithPager - ROW_GAP,
|
||||
pagerOverlaid = false
|
||||
)
|
||||
}
|
||||
// Window too short for both: the radar takes the largest square it can and the pager is
|
||||
// folded into the overlaid tab strip.
|
||||
val radarSide = minOf(maxWidth, maxHeight)
|
||||
val pagerSpace = maxHeight - radarSide - ROW_GAP
|
||||
return RadarFillSizes(
|
||||
radarSide = radarSide,
|
||||
pagerSpace = pagerSpace,
|
||||
pagerOverlaid = pagerSpace < COMPACT_MIN_PAGER_HEIGHT
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadarDestination(navigateUp: () -> Unit) {
|
||||
@@ -187,16 +270,21 @@ private fun RadarScreen(
|
||||
// size in split-screen / multi-window, so the compact branch never
|
||||
// triggered there. maxHeight/maxWidth are the real window bounds.
|
||||
val isVertical = maxWidth < 600.dp
|
||||
// In split-screen / small windows the vertical space is scarce: let the
|
||||
// radar card fill ALL remaining space so the square plot is as large as
|
||||
// possible, and give the pager a compact fixed strip — its pages are all
|
||||
// scrollable, so nothing gets clipped. Full-screen layouts keep the 1:1 split.
|
||||
val compact = isVertical && maxHeight < 480.dp
|
||||
// The classic vertical layout gives the radar a 1:1 share of the height while also
|
||||
// paying for two top bar rows. In split-screen / small windows that leaves a card that
|
||||
// is wider than it is tall, so the circle is capped by the height and stops filling the
|
||||
// page width. Below the full-screen proportion the fill layout takes over: one top bar
|
||||
// row, the radar keeps the largest square it can, the pager yields (reserved block when
|
||||
// the window is tall enough, overlaid tab strip + panel when it is not).
|
||||
val fillRadar = useFillRadarLayout(isVertical, maxWidth, maxHeight)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
if (isVertical) {
|
||||
// Normal vertical top area: the timer card keeps its own row and the pass card
|
||||
// gets a full-width row of its own. Never merge them into one row — both cards
|
||||
// carry weight(1f), so side by side they split the width into two halves.
|
||||
TopBar {
|
||||
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos)
|
||||
@@ -212,9 +300,16 @@ private fun RadarScreen(
|
||||
}
|
||||
}
|
||||
if (isVertical) {
|
||||
if (compact) {
|
||||
RadarCard(uiState, trackB, trackBPosition, Modifier.weight(1f))
|
||||
PagerCard(uiState, onAction, logViewModel, requestMicPermission, Modifier.height(COMPACT_PAGER_HEIGHT))
|
||||
if (fillRadar) {
|
||||
RadarFillArea(
|
||||
uiState = uiState,
|
||||
trackB = trackB,
|
||||
trackBPosition = trackBPosition,
|
||||
onAction = onAction,
|
||||
logViewModel = logViewModel,
|
||||
requestMicPermission = requestMicPermission,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
} else {
|
||||
RadarCard(uiState, trackB, trackBPosition, Modifier.weight(1f))
|
||||
PagerCard(uiState, onAction, logViewModel, requestMicPermission, Modifier.weight(1f))
|
||||
@@ -229,18 +324,118 @@ private fun RadarScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Radar area for split-screen / small windows. The radar takes the largest square the area
|
||||
* can hold so the circle spans the same share of the width it does full screen, and the
|
||||
* pager yields: a real block below the square when there is room, otherwise a translucent
|
||||
* tab strip overlaid on the plot that opens the pager panel on top of it.
|
||||
*/
|
||||
@Composable
|
||||
private fun PagerCard(
|
||||
private fun RadarFillArea(
|
||||
uiState: RadarState,
|
||||
trackB: List<OrbitalPos>,
|
||||
trackBPosition: OrbitalPos?,
|
||||
onAction: (RadarAction) -> Unit,
|
||||
logViewModel: LogViewModel,
|
||||
requestMicPermission: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
|
||||
val sizes = radarFillSizes(maxWidth, maxHeight)
|
||||
// Hoisted out of the nested Box scope: the BoxWithConstraints receiver is not
|
||||
// implicitly reachable inside it.
|
||||
val pagerPanelHeight = maxHeight * 0.72f
|
||||
if (!sizes.pagerOverlaid) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(ROW_GAP)
|
||||
) {
|
||||
RadarCard(uiState, trackB, trackBPosition, Modifier.height(sizes.radarSide).fillMaxWidth())
|
||||
PagerCard(uiState, onAction, logViewModel, requestMicPermission, Modifier.height(sizes.pagerSpace))
|
||||
}
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
RadarCard(uiState, trackB, trackBPosition, Modifier.fillMaxSize(), overlayStrip = true)
|
||||
CompactPagerOverlay(
|
||||
uiState = uiState,
|
||||
onAction = onAction,
|
||||
logViewModel = logViewModel,
|
||||
requestMicPermission = requestMicPermission,
|
||||
panelHeight = pagerPanelHeight,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsed pager for short windows: a translucent row of tab names over the bottom of the
|
||||
* radar plot; picking one opens the full pager panel on top of the plot, collapsible again
|
||||
* with the chevron in its tab row.
|
||||
*/
|
||||
@Composable
|
||||
private fun CompactPagerOverlay(
|
||||
uiState: RadarState,
|
||||
onAction: (RadarAction) -> Unit,
|
||||
logViewModel: LogViewModel,
|
||||
requestMicPermission: () -> Unit,
|
||||
panelHeight: Dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val pages = rememberRadarPages(uiState)
|
||||
var openPage by remember { mutableStateOf<RadarPage?>(null) }
|
||||
val currentPage = openPage
|
||||
if (currentPage == null) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.88f),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(COMPACT_TAB_STRIP_HEIGHT)
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
|
||||
pages.forEach { page ->
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clickable { openPage = page }
|
||||
) {
|
||||
Text(
|
||||
text = page.title,
|
||||
fontSize = 13.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PagerCard(
|
||||
uiState = uiState,
|
||||
onAction = onAction,
|
||||
logViewModel = logViewModel,
|
||||
requestMicPermission = requestMicPermission,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(panelHeight),
|
||||
startPage = currentPage,
|
||||
onCollapse = { openPage = null }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Page list of the pager card, shared by the inline pager and the compact overlay panel. */
|
||||
@Composable
|
||||
private fun rememberRadarPages(uiState: RadarState): List<RadarPage> {
|
||||
val hasCalculatorPage = remember(uiState.transceivers.transmitters) {
|
||||
uiState.transceivers.transmitters.any(DopplerFrequencyCalculator::isNamedLinearTransponder)
|
||||
}
|
||||
val pages = remember(hasCalculatorPage) {
|
||||
return remember(hasCalculatorPage) {
|
||||
buildList {
|
||||
add(RadarPage.Transceivers)
|
||||
if (hasCalculatorPage) add(RadarPage.Calculator)
|
||||
@@ -248,7 +443,23 @@ private fun PagerCard(
|
||||
add(RadarPage.Sstv)
|
||||
}
|
||||
}
|
||||
val pagerState = rememberPagerState(pageCount = { pages.size })
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PagerCard(
|
||||
uiState: RadarState,
|
||||
onAction: (RadarAction) -> Unit,
|
||||
logViewModel: LogViewModel,
|
||||
requestMicPermission: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
startPage: RadarPage? = null,
|
||||
onCollapse: (() -> Unit)? = null
|
||||
) {
|
||||
val pages = rememberRadarPages(uiState)
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = startPage?.let { pages.indexOf(it) }?.coerceAtLeast(0) ?: 0,
|
||||
pageCount = { pages.size }
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(pages.size) {
|
||||
@@ -259,12 +470,26 @@ private fun PagerCard(
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
val selectedTabIndex = pagerState.currentPage.coerceIn(0, pages.lastIndex)
|
||||
PrimaryTabRow(selectedTabIndex = selectedTabIndex) {
|
||||
pages.forEachIndexed { index, page ->
|
||||
Tab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
text = { Text(text = page.title, maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
modifier = if (onCollapse != null) Modifier.weight(1f) else Modifier
|
||||
) {
|
||||
pages.forEachIndexed { index, page ->
|
||||
Tab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
text = { Text(text = page.title, maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
}
|
||||
}
|
||||
onCollapse?.let { collapse ->
|
||||
Text(
|
||||
text = "▼",
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.clickable { collapse() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -309,7 +534,8 @@ private fun RadarCard(
|
||||
uiState: RadarState,
|
||||
trackB: List<OrbitalPos> = emptyList(),
|
||||
trackBPosition: OrbitalPos? = null,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
overlayStrip: Boolean = false
|
||||
) {
|
||||
val satellitePos = uiState.orbitalPos
|
||||
val shouldAnimateBorder = satellitePos?.aboveHorizon == true && satellitePos.eclipsed
|
||||
@@ -352,19 +578,25 @@ private fun RadarCard(
|
||||
sunPosition = uiState.sunPosition,
|
||||
moonPosition = uiState.moonPosition,
|
||||
)
|
||||
PositionOverlay(position)
|
||||
PositionOverlay(position, overlayStrip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PositionOverlay(position: OrbitalPos) {
|
||||
private fun PositionOverlay(position: OrbitalPos, overlayStrip: Boolean = false) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp)
|
||||
// Keep the bottom labels clear of the overlaid pager tab strip in small windows.
|
||||
.padding(
|
||||
start = 6.dp,
|
||||
end = 6.dp,
|
||||
top = 4.dp,
|
||||
bottom = if (overlayStrip) COMPACT_TAB_STRIP_HEIGHT + 4.dp else 4.dp
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Small-window radar sizing.
|
||||
*
|
||||
* The plotted circle is `0.95 x min(cardWidth, cardHeight)`, so a radar card that is wider than
|
||||
* it is tall caps the circle by its height and shows empty bands on the sides. Full screen the
|
||||
* classic 1:1 split leaves the radar card ~0.88 x cardWidth tall, i.e. the circle spans ~84% of
|
||||
* the page width. These tests pin the split-screen / small-window layout to the same width share
|
||||
* instead of letting the circle shrink with the window height.
|
||||
*/
|
||||
class RadarFillLayoutTest {
|
||||
|
||||
/** Circle diameter as a share of the page width (radar canvas rule: 0.95 * min(width, height)). */
|
||||
private fun circleShare(width: Dp, height: Dp, sizes: RadarFillSizes): Float {
|
||||
val availableHeight = if (sizes.pagerOverlaid) height else sizes.radarSide
|
||||
return 0.95f * min(width.value, availableHeight.value) / width.value
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullScreenVerticalKeepsClassicSplit() {
|
||||
// 411 x 867dp full screen: the 1:1 split already gives the circle ~87% of the width,
|
||||
// so the full-screen layout must not change.
|
||||
assertFalse(useFillRadarLayout(isVertical = true, maxWidth = 411.dp, maxHeight = 867.dp))
|
||||
assertFalse(useFillRadarLayout(isVertical = true, maxWidth = 393.dp, maxHeight = 804.dp))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun splitScreenAndSmallWindowsUseFillLayout() {
|
||||
assertTrue(useFillRadarLayout(true, 411.dp, 426.dp)) // portrait split-screen half
|
||||
assertTrue(useFillRadarLayout(true, 411.dp, 500.dp)) // floating small window
|
||||
assertTrue(useFillRadarLayout(true, 411.dp, 676.dp)) // 70/30 split
|
||||
assertTrue(useFillRadarLayout(true, 393.dp, 560.dp))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wideWindowsKeepSideBySideLayout() {
|
||||
assertFalse(useFillRadarLayout(isVertical = false, maxWidth = 700.dp, maxHeight = 400.dp))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fillLayoutKeepsCircleAtFullScreenWidthShare() {
|
||||
// Previously these rendered the circle at ~36% of the page width.
|
||||
val cases = listOf(
|
||||
411.dp to 426.dp,
|
||||
411.dp to 500.dp,
|
||||
393.dp to 560.dp,
|
||||
411.dp to 676.dp
|
||||
)
|
||||
cases.forEach { (width, height) ->
|
||||
val share = circleShare(width, height, radarFillSizes(width, height))
|
||||
assertTrue("circle is only ${share * 100}% of the page width at ${width.value}x${height.value}",
|
||||
share >= 0.84f)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun classicSplitWouldHaveSqueezedTheCircle() {
|
||||
// Regression guard documenting why the fill layout exists: the old compact branch left
|
||||
// a 411 x 156dp card, i.e. a circle at 36% of the page width, in the same split window.
|
||||
val classicCompactCardHeight = 426.dp - 96.dp - 132.dp - 18.dp
|
||||
val classicCompactShare = 0.95f * classicCompactCardHeight.value / 411f
|
||||
assertTrue(classicCompactShare < 0.45f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pagerIsOverlaidOnlyWhenAReservedBlockWouldBeUseless() {
|
||||
val shortWindow = radarFillSizes(411.dp, 426.dp)
|
||||
assertTrue(shortWindow.pagerOverlaid)
|
||||
assertTrue(shortWindow.radarSide == 411.dp)
|
||||
|
||||
val tallWindow = radarFillSizes(411.dp, 676.dp)
|
||||
assertFalse(tallWindow.pagerOverlaid)
|
||||
assertEquals(411f, tallWindow.radarSide.value, 0.01f)
|
||||
assertEquals(259f, tallWindow.pagerSpace.value, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pagerBlockIsKeptWhileTheCircleStaysAtTheFullScreenShare() {
|
||||
// 411 x 468dp: reserving 96dp for the pager still leaves the circle at ~85% of the page
|
||||
// width, so the pager stays as a real block instead of folding into the overlay strip.
|
||||
val sizes = radarFillSizes(411.dp, 468.dp)
|
||||
assertFalse(sizes.pagerOverlaid)
|
||||
assertEquals(366f, sizes.radarSide.value, 0.01f)
|
||||
assertEquals(96f, sizes.pagerSpace.value, 0.01f)
|
||||
val share = circleShare(411.dp, 468.dp, sizes)
|
||||
assertTrue("circle is only ${share * 100}% of the page width", share >= 0.84f)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user