fix(android): keep body metrics two-column on normal phones
This commit is contained in:
parent
60b08f225a
commit
0e13c3947a
2 changed files with 87 additions and 3 deletions
|
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
|
@ -378,7 +379,7 @@ private fun BodyMetricField(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class BodyMetricItem(
|
internal data class BodyMetricItem(
|
||||||
val label: String,
|
val label: String,
|
||||||
val unit: String,
|
val unit: String,
|
||||||
val value: String,
|
val value: String,
|
||||||
|
|
@ -386,10 +387,19 @@ private data class BodyMetricItem(
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Two columns retain scanability; very narrow accessibility layouts stack. */
|
/** Two columns retain scanability; very narrow accessibility layouts stack. */
|
||||||
|
internal fun bodyMetricColumnCount(availableWidthDp: Float, fontScale: Float): Int {
|
||||||
|
val twoCompactFieldsDp = 2 * 140f + 10f
|
||||||
|
return if (availableWidthDp >= twoCompactFieldsDp && fontScale < 1.5f) 2 else 1
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.BodyMetricGrid(items: List<BodyMetricItem>) {
|
internal fun ColumnScope.BodyMetricGrid(items: List<BodyMetricItem>) {
|
||||||
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||||
val columns = if (maxWidth < 330.dp) 1 else 2
|
/* WHY: this 360 dp device has 312 dp after screen padding; the old
|
||||||
|
* 330 dp threshold therefore stacked fields that need only 140 dp.
|
||||||
|
* CONTRACT: two compact fields plus their 10 dp gutter are preferred
|
||||||
|
* unless width or accessibility font scaling makes them unreadable. */
|
||||||
|
val columns = bodyMetricColumnCount(maxWidth.value, LocalDensity.current.fontScale)
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||||
items.chunked(columns).forEach { rowItems ->
|
items.chunked(columns).forEach { rowItems ->
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.labfytools.trainlog.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.labfytools.trainlog.ui.theme.TrainlogTheme
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
@Config(sdk = [35])
|
||||||
|
class BodyMetricGridTest {
|
||||||
|
@get:Rule val compose = createComposeRule()
|
||||||
|
|
||||||
|
private val items = listOf(
|
||||||
|
BodyMetricItem("POIDS", "kg", "", {}),
|
||||||
|
BodyMetricItem("COU", "cm", "", {}),
|
||||||
|
BodyMetricItem("TOUR DE TAILLE", "cm", "", {}),
|
||||||
|
BodyMetricItem("HANCHES", "cm", "", {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deployedPhoneContentWidthUsesTwoColumns() {
|
||||||
|
// 1080 px / 480 dpi = 360 dp; the screen frame leaves about 312 dp.
|
||||||
|
assertEquals(2, bodyMetricColumnCount(312f, 0.9f))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun genuinelyNarrowWidthStacksFields() {
|
||||||
|
assertEquals(1, bodyMetricColumnCount(280f, 1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun largeAccessibilityTextStacksFields() {
|
||||||
|
assertEquals(1, bodyMetricColumnCount(312f, 1.5f))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deployedPhoneWidthRendersPairedFieldsWithoutClippingUnits() {
|
||||||
|
compose.setContent {
|
||||||
|
TrainlogTheme {
|
||||||
|
Column(Modifier.width(312.dp)) { BodyMetricGrid(items) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val weight = compose.onNodeWithText("POIDS").fetchSemanticsNode().boundsInRoot
|
||||||
|
val neck = compose.onNodeWithText("COU").fetchSemanticsNode().boundsInRoot
|
||||||
|
assertTrue(neck.left > weight.left)
|
||||||
|
assertTrue(kotlin.math.abs(neck.top - weight.top) < 2f)
|
||||||
|
compose.onNodeWithText("kg").fetchSemanticsNode()
|
||||||
|
assertTrue(compose.onAllNodesWithText("cm", useUnmergedTree = true).fetchSemanticsNodes().isNotEmpty())
|
||||||
|
compose.onNodeWithText("TOUR DE TAILLE").fetchSemanticsNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trulyNarrowLayoutStacksPairedFields() {
|
||||||
|
compose.setContent {
|
||||||
|
TrainlogTheme {
|
||||||
|
Column(Modifier.width(280.dp)) { BodyMetricGrid(items.take(2)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val weight = compose.onNodeWithText("POIDS").fetchSemanticsNode().boundsInRoot
|
||||||
|
val neck = compose.onNodeWithText("COU").fetchSemanticsNode().boundsInRoot
|
||||||
|
assertTrue(neck.top > weight.top)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue