Skip to content

Commit eb891d7

Browse files
authored
Merge pull request #122 from compose-fluent/update_bring_into_view
[fluent] Update bring into view
2 parents e2c24ed + 48e9414 commit eb891d7

File tree

2 files changed

+102
-47
lines changed

2 files changed

+102
-47
lines changed

fluent/src/commonMain/kotlin/io/github/composefluent/component/LiteFilter.kt

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@ import androidx.compose.foundation.layout.Row
1414
import androidx.compose.foundation.layout.RowScope
1515
import androidx.compose.foundation.layout.heightIn
1616
import androidx.compose.foundation.layout.padding
17-
import androidx.compose.foundation.relocation.BringIntoViewResponder
18-
import androidx.compose.foundation.relocation.bringIntoViewResponder
1917
import androidx.compose.foundation.rememberScrollState
2018
import androidx.compose.runtime.Composable
2119
import androidx.compose.runtime.Stable
22-
import androidx.compose.runtime.remember
2320
import androidx.compose.runtime.rememberCoroutineScope
2421
import androidx.compose.runtime.snapshots.Snapshot
2522
import androidx.compose.ui.Alignment
@@ -29,7 +26,12 @@ import androidx.compose.ui.geometry.Rect
2926
import androidx.compose.ui.geometry.Size
3027
import androidx.compose.ui.graphics.Outline
3128
import androidx.compose.ui.graphics.Shape
29+
import androidx.compose.ui.layout.LayoutCoordinates
30+
import androidx.compose.ui.node.ModifierNodeElement
31+
import androidx.compose.ui.node.requireLayoutCoordinates
3232
import androidx.compose.ui.platform.LocalDensity
33+
import androidx.compose.ui.relocation.BringIntoViewModifierNode
34+
import androidx.compose.ui.relocation.bringIntoView
3335
import androidx.compose.ui.unit.Density
3436
import androidx.compose.ui.unit.Dp
3537
import androidx.compose.ui.unit.LayoutDirection
@@ -65,7 +67,7 @@ fun LiteFilter(
6567
)
6668
)
6769
.horizontalScroll(state)
68-
.bringIntoViewResponder(remember(state, density) { LiteFilterBringIntoViewResponder(state, density) })
70+
.then(LiteFilterModifierNodeElement(state, density))
6971
.align(Alignment.CenterStart)
7072
) {
7173
content()
@@ -80,7 +82,12 @@ fun LiteFilter(
8082
) {
8183
SubtleButton(
8284
onClick = { scope.launch { state.animateScrollBy(-state.viewportSize / 3f) } },
83-
content = { FontIconSolid8(type = FontIconPrimitive.CaretLeft, contentDescription = null) },
85+
content = {
86+
FontIconSolid8(
87+
type = FontIconPrimitive.CaretLeft,
88+
contentDescription = null
89+
)
90+
},
8491
iconOnly = true
8592
)
8693
}
@@ -93,40 +100,65 @@ fun LiteFilter(
93100
) {
94101
SubtleButton(
95102
onClick = { scope.launch { state.animateScrollBy(state.viewportSize / 3f) } },
96-
content = { FontIconSolid8(type = FontIconPrimitive.CaretRight, contentDescription = null) },
103+
content = {
104+
FontIconSolid8(
105+
type = FontIconPrimitive.CaretRight,
106+
contentDescription = null
107+
)
108+
},
97109
iconOnly = true
98110
)
99111
}
100112
}
101113
}
102114

103-
@OptIn(ExperimentalFoundationApi::class)
104-
private class LiteFilterBringIntoViewResponder(
115+
private data class LiteFilterModifierNodeElement(
105116
private val state: ScrollState,
106-
density: Density,
107-
): BringIntoViewResponder {
108-
val startSize = with(density) { 44.dp.toPx() }
109-
val endSize = startSize
117+
private val density: Density,
118+
) : ModifierNodeElement<LiteFilterBringIntoViewModifierNode>() {
119+
override fun create(): LiteFilterBringIntoViewModifierNode {
120+
return LiteFilterBringIntoViewModifierNode(
121+
state = state,
122+
density = density
123+
)
124+
}
110125

111-
override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
126+
override fun update(node: LiteFilterBringIntoViewModifierNode) {
127+
node.state = state
128+
node.density = density
129+
}
130+
}
131+
132+
@OptIn(ExperimentalFoundationApi::class)
133+
private class LiteFilterBringIntoViewModifierNode(
134+
var state: ScrollState,
135+
var density: Density,
136+
) : Modifier.Node(), BringIntoViewModifierNode {
112137

113-
override fun calculateRectForParent(localRect: Rect): Rect {
114-
return Snapshot.withoutReadObservation {
115-
when {
138+
override suspend fun bringIntoView(
139+
childCoordinates: LayoutCoordinates,
140+
boundsProvider: () -> Rect?
141+
) {
142+
Snapshot.withoutReadObservation {
143+
if (!childCoordinates.isAttached || !isAttached) return@withoutReadObservation
144+
val localRect = requireLayoutCoordinates().localBoundingBoxOf(childCoordinates)
145+
val startSize = with(density) { 44.dp.toPx() }
146+
val endSize = startSize
147+
val targetRect = when {
116148
state.canScrollForward && state.viewportSize - localRect.right - state.value < endSize -> {
117-
return localRect.copy(
149+
localRect.copy(
118150
right = localRect.right + endSize
119151
)
120152
}
121153

122154
state.canScrollBackward && localRect.left < state.value + startSize -> {
123-
return localRect.copy(
155+
localRect.copy(
124156
left = localRect.left - startSize
125157
)
126158
}
127-
128159
else -> localRect
129160
}
161+
bringIntoView { targetRect }
130162
}
131163
}
132164
}

fluent/src/commonMain/kotlin/io/github/composefluent/component/TabView.kt

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import androidx.compose.foundation.lazy.LazyListScope
2626
import androidx.compose.foundation.lazy.LazyListState
2727
import androidx.compose.foundation.lazy.LazyRow
2828
import androidx.compose.foundation.lazy.rememberLazyListState
29-
import androidx.compose.foundation.relocation.BringIntoViewResponder
30-
import androidx.compose.foundation.relocation.bringIntoViewResponder
3129
import androidx.compose.foundation.shape.RoundedCornerShape
3230
import androidx.compose.runtime.Composable
3331
import androidx.compose.runtime.CompositionLocalProvider
@@ -42,7 +40,6 @@ import androidx.compose.runtime.mutableStateMapOf
4240
import androidx.compose.runtime.mutableStateOf
4341
import androidx.compose.runtime.remember
4442
import androidx.compose.runtime.rememberCoroutineScope
45-
import androidx.compose.runtime.rememberUpdatedState
4643
import androidx.compose.runtime.setValue
4744
import androidx.compose.runtime.snapshotFlow
4845
import androidx.compose.runtime.snapshots.Snapshot
@@ -60,11 +57,17 @@ import androidx.compose.ui.graphics.RectangleShape
6057
import androidx.compose.ui.graphics.Shape
6158
import androidx.compose.ui.graphics.SolidColor
6259
import androidx.compose.ui.graphics.drawscope.Stroke
60+
import androidx.compose.ui.layout.LayoutCoordinates
6361
import androidx.compose.ui.layout.boundsInParent
6462
import androidx.compose.ui.layout.onGloballyPositioned
6563
import androidx.compose.ui.layout.onSizeChanged
64+
import androidx.compose.ui.node.DelegatingNode
65+
import androidx.compose.ui.node.ModifierNodeElement
66+
import androidx.compose.ui.node.requireLayoutCoordinates
6667
import androidx.compose.ui.platform.LocalDensity
6768
import androidx.compose.ui.platform.LocalLayoutDirection
69+
import androidx.compose.ui.relocation.BringIntoViewModifierNode
70+
import androidx.compose.ui.relocation.bringIntoView
6871
import androidx.compose.ui.unit.Density
6972
import androidx.compose.ui.unit.Dp
7073
import androidx.compose.ui.unit.DpSize
@@ -129,7 +132,14 @@ fun TabRow(
129132

130133
val selectedItem = remember {
131134
derivedStateOf {
132-
state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == selectedKey() }
135+
val currentKey = selectedKey()
136+
state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == currentKey }
137+
}
138+
}
139+
val selectedItemScrollOffset = remember(selectedItem, state) {
140+
derivedStateOf {
141+
val selectedItem = selectedItem.value ?: return@derivedStateOf 0
142+
selectedItem.offset + state.firstVisibleItemScrollOffset - state.firstVisibleItemScrollOffset
133143
}
134144
}
135145
Box(modifier = Modifier
@@ -140,7 +150,8 @@ fun TabRow(
140150
borderColor = borderColor,
141151
containerWidth = { containerWidth.value },
142152
rowRect = { rowRect.value },
143-
selectedItem = { selectedItem.value }
153+
selectedItem = { selectedItem.value },
154+
selectedItemScrollOffset = { selectedItemScrollOffset.value }
144155
)
145156
)
146157
Box(modifier = Modifier.widthIn(padding)) {
@@ -277,7 +288,6 @@ fun TabItem(
277288
val direction = LocalLayoutDirection.current
278289
val color = colors.schemeFor(targetInteractionSource.collectVisualState(false))
279290
val density = LocalDensity.current
280-
val selectedValue = rememberUpdatedState(selected)
281291
val bottomRadius = FluentTheme.cornerRadius.control
282292
val topRadius = FluentTheme.cornerRadius.overlay
283293
Box(
@@ -299,11 +309,10 @@ fun TabItem(
299309
Modifier
300310
}
301311
)
302-
.bringIntoViewResponder(
303-
remember(density, selectedValue, bottomRadius) {
304-
TabItemBringIntoViewResponder(density, bottomRadius) { selectedValue.value }
305-
}
306-
)
312+
.then(TabItemBringIntoViewModifierNodeElement(
313+
density = density,
314+
bottomRadius = bottomRadius
315+
))
307316
.clickable(
308317
indication = null,
309318
interactionSource = targetInteractionSource
@@ -798,6 +807,7 @@ private fun Modifier.drawTabRowBorder(
798807
containerWidth: () -> Int,
799808
rowRect: () -> Rect,
800809
selectedItem: () -> LazyListItemInfo?,
810+
selectedItemScrollOffset: () -> Int,
801811
) = drawWithCache {
802812
val path = Path()
803813
val strokeSizePx = StrokeSize.toPx()
@@ -809,7 +819,7 @@ private fun Modifier.drawTabRowBorder(
809819
val itemPadding = bottomRadius.toPx()
810820
if (currentItem != null) {
811821
val rowRectValue = rowRect()
812-
val currentItemOffset = (rowRectValue.left + currentItem.offset)
822+
val currentItemOffset = (rowRectValue.left + selectedItemScrollOffset())
813823
lineTo(
814824
(currentItemOffset).coerceIn(
815825
rowRectValue.left,
@@ -885,27 +895,40 @@ private fun Modifier.drawTabViewItemBorder(
885895
}
886896
}
887897

888-
@OptIn(ExperimentalFoundationApi::class)
889-
@Stable
890-
private class TabItemBringIntoViewResponder(
891-
density: Density,
892-
bottomRadius: Dp,
893-
val selected: () -> Boolean,
894-
) : BringIntoViewResponder {
895-
val paddingSize = with(density) { bottomRadius.toPx() }
898+
private data class TabItemBringIntoViewModifierNodeElement(
899+
val density: Density,
900+
val bottomRadius: Dp,
901+
): ModifierNodeElement<TabItemBringIntoViewModifierNode>() {
902+
override fun create(): TabItemBringIntoViewModifierNode {
903+
return TabItemBringIntoViewModifierNode(density, bottomRadius)
904+
}
896905

897-
override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
906+
override fun update(node: TabItemBringIntoViewModifierNode) {
907+
node.density = density
908+
node.bottomRadius = bottomRadius
909+
}
910+
}
898911

899-
override fun calculateRectForParent(localRect: Rect): Rect {
900-
return Snapshot.withoutReadObservation {
901-
if (selected()) {
902-
localRect.copy(
912+
@OptIn(ExperimentalFoundationApi::class)
913+
@Stable
914+
private class TabItemBringIntoViewModifierNode(
915+
var density: Density,
916+
var bottomRadius: Dp,
917+
) : BringIntoViewModifierNode, DelegatingNode() {
918+
919+
override suspend fun bringIntoView(
920+
childCoordinates: LayoutCoordinates,
921+
boundsProvider: () -> Rect?
922+
) {
923+
Snapshot.withoutReadObservation {
924+
if (!childCoordinates.isAttached || !isAttached) return@withoutReadObservation
925+
val localRect = requireLayoutCoordinates().localBoundingBoxOf(childCoordinates)
926+
val paddingSize = with(density) { bottomRadius.toPx() }
927+
val targetRect = localRect.copy(
903928
left = localRect.left - paddingSize,
904929
right = localRect.right + paddingSize
905930
)
906-
} else {
907-
localRect
908-
}
931+
bringIntoView { targetRect }
909932
}
910933
}
911934
}

0 commit comments

Comments
 (0)