Skip to content

Commit 60bd4a5

Browse files
frettclaude
andcommitted
Rename and generalize PinLastItemBottomArrangement to FloatLastItemsToBottomArrangement
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b30899e commit 60bd4a5

5 files changed

Lines changed: 116 additions & 41 deletions

File tree

app/src/main/kotlin/org/cru/godtools/ui/dashboard/PinLastItemBottomArrangement.kt

Lines changed: 0 additions & 23 deletions
This file was deleted.

app/src/main/kotlin/org/cru/godtools/ui/dashboard/lessons/LessonsLayout.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.cru.godtools.ui.dashboard.lessons
22

3-
import androidx.compose.foundation.layout.Arrangement
43
import androidx.compose.foundation.layout.Column
54
import androidx.compose.foundation.layout.Spacer
65
import androidx.compose.foundation.layout.fillMaxHeight
@@ -29,23 +28,18 @@ import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen
2928
import org.cru.godtools.ui.dashboard.LocalizationSettingsBox
3029
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiEvent
3130
import org.cru.godtools.ui.dashboard.lessons.LessonsPresenter.UiState
32-
import org.cru.godtools.ui.dashboard.rememberPinLastItemBottomArrangement
31+
import org.cru.godtools.ui.dashboard.personalization.rememberFloatLastItemsToBottomArrangement
3332
import org.cru.godtools.ui.tools.LessonToolCard
3433

3534
internal val MARGIN_LESSONS_LAYOUT_HORIZONTAL = 16.dp
3635

3736
@Composable
3837
@CircuitInject(LessonsScreen::class, SingletonComponent::class)
3938
internal fun LessonsLayout(state: UiState, modifier: Modifier = Modifier) {
40-
val pinLastItem = rememberPinLastItemBottomArrangement()
41-
val verticalArrangement = if (state.mode == UiState.Mode.PERSONALIZATION) {
42-
pinLastItem
43-
} else {
44-
Arrangement.Top
45-
}
46-
4739
LazyColumn(
48-
verticalArrangement = verticalArrangement,
40+
verticalArrangement = rememberFloatLastItemsToBottomArrangement(
41+
numToFloat = if (state.mode == UiState.Mode.PERSONALIZATION) 1 else 0
42+
),
4943
modifier = modifier.fillMaxHeight()
5044
) {
5145
if (state.isPersonalizationEnabled) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.cru.godtools.ui.dashboard.personalization
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.ui.unit.Density
7+
8+
@Composable
9+
internal fun rememberFloatLastItemsToBottomArrangement(numToFloat: Int) =
10+
remember(numToFloat) { FloatLastItemsToBottomArrangement(numToFloat) }
11+
12+
internal class FloatLastItemsToBottomArrangement(val numToFloat: Int) : Arrangement.Vertical {
13+
override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
14+
var currentOffset = 0
15+
sizes.forEachIndexed { index, size ->
16+
outPositions[index] = currentOffset
17+
currentOffset += size
18+
}
19+
20+
if (currentOffset < totalSize && numToFloat > 0) {
21+
currentOffset = totalSize
22+
sizes.takeLast(numToFloat).reversed().forEachIndexed { index, size ->
23+
currentOffset -= size
24+
outPositions[sizes.lastIndex - index] = currentOffset
25+
}
26+
}
27+
}
28+
}

app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsLayout.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import org.cru.godtools.R
3232
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen
3333
import org.cru.godtools.ui.banner.Banners
3434
import org.cru.godtools.ui.dashboard.LocalizationSettingsBox
35-
import org.cru.godtools.ui.dashboard.rememberPinLastItemBottomArrangement
35+
import org.cru.godtools.ui.dashboard.personalization.rememberFloatLastItemsToBottomArrangement
3636
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiEvent
3737
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState
3838
import org.cru.godtools.ui.tools.SquareToolCard
@@ -49,16 +49,12 @@ internal fun ToolsLayout(state: UiState, modifier: Modifier = Modifier) {
4949

5050
val columnState = rememberLazyListState()
5151
LaunchedEffect(state.banner?.type) { if (state.banner != null) columnState.animateScrollToItem(0) }
52-
val pinLastItem = rememberPinLastItemBottomArrangement()
53-
val verticalArrangement = if (state.mode == UiState.Mode.PERSONALIZATION) {
54-
pinLastItem
55-
} else {
56-
Arrangement.Top
57-
}
5852

5953
LazyColumn(
6054
state = columnState,
61-
verticalArrangement = verticalArrangement,
55+
verticalArrangement = rememberFloatLastItemsToBottomArrangement(
56+
numToFloat = if (state.mode == UiState.Mode.PERSONALIZATION) 1 else 0
57+
),
6258
modifier = modifier.fillMaxHeight()
6359
) {
6460
if (!state.dataLoaded) return@LazyColumn
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package org.cru.godtools.ui.dashboard.personalization
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.ui.unit.Density
5+
import kotlin.test.Test
6+
import kotlin.test.assertContentEquals
7+
8+
class FloatLastItemsToBottomArrangementTest {
9+
private val density = Density(1f)
10+
11+
private fun Arrangement.Vertical.testArrange(totalSize: Int, vararg sizes: Int): IntArray {
12+
val outPositions = IntArray(sizes.size)
13+
with(density) { arrange(totalSize, sizes, outPositions) }
14+
return outPositions
15+
}
16+
17+
// region numToFloat = 0
18+
@Test
19+
fun `numToFloat=0 - items positioned sequentially from top`() {
20+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 0)
21+
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(500, 100, 100, 100))
22+
}
23+
// endregion numToFloat = 0
24+
25+
// region numToFloat = 1
26+
@Test
27+
fun `numToFloat=1 - last item floats to bottom when content fits`() {
28+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
29+
assertContentEquals(intArrayOf(0, 100, 400), arrangement.testArrange(500, 100, 100, 100))
30+
}
31+
32+
@Test
33+
fun `numToFloat=1 - no floating when content overflows container`() {
34+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
35+
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(200, 100, 100, 100))
36+
}
37+
38+
@Test
39+
fun `numToFloat=1 - single item floats to bottom`() {
40+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
41+
assertContentEquals(intArrayOf(400), arrangement.testArrange(500, 100))
42+
}
43+
// endregion numToFloat = 1
44+
45+
// region numToFloat = 2
46+
@Test
47+
fun `numToFloat=2 - last 2 items float to bottom when content fits`() {
48+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 2)
49+
assertContentEquals(intArrayOf(0, 300, 400), arrangement.testArrange(500, 100, 100, 100))
50+
}
51+
52+
@Test
53+
fun `numToFloat=2 - no floating when content exactly fills container`() {
54+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 2)
55+
assertContentEquals(intArrayOf(0, 100, 200), arrangement.testArrange(300, 100, 100, 100))
56+
}
57+
// endregion numToFloat = 2
58+
59+
// region numToFloat exceeds item count
60+
@Test
61+
fun `numToFloat exceeds item count - all items float to bottom`() {
62+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 5)
63+
assertContentEquals(intArrayOf(200, 300, 400), arrangement.testArrange(500, 100, 100, 100))
64+
}
65+
// endregion numToFloat exceeds item count
66+
67+
// region edge cases
68+
@Test
69+
fun `empty item list`() {
70+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
71+
assertContentEquals(intArrayOf(), arrangement.testArrange(500))
72+
}
73+
74+
@Test
75+
fun `variable item sizes - floating item positioned correctly`() {
76+
val arrangement = FloatLastItemsToBottomArrangement(numToFloat = 1)
77+
assertContentEquals(intArrayOf(0, 50, 350), arrangement.testArrange(500, 50, 200, 150))
78+
}
79+
// endregion edge cases
80+
}

0 commit comments

Comments
 (0)