Skip to content

Commit 3db2b07

Browse files
committed
Refactor: CollapsibleHeaderState 구현 변경
- Fling 동작과 snap 애니메이션을 처리하여 사용자 경험을 개선합니다. - 헤더 높이 계산 로직을 단순화하고, 화면 높이에 비례하여 동적으로 조절되도록 수정했습니다. - 관련 파일의 패키지 위치를 util에서 model로 변경하여 코드 구조를 개선합니다.
1 parent 14217e0 commit 3db2b07

2 files changed

Lines changed: 120 additions & 168 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.threegap.bitnagil.presentation.home.model
2+
3+
import androidx.compose.animation.core.Spring
4+
import androidx.compose.animation.core.animate
5+
import androidx.compose.animation.core.spring
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.Stable
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableFloatStateOf
10+
import androidx.compose.runtime.remember
11+
import androidx.compose.runtime.setValue
12+
import androidx.compose.ui.geometry.Offset
13+
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
14+
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
15+
import androidx.compose.ui.platform.LocalDensity
16+
import androidx.compose.ui.platform.LocalWindowInfo
17+
import androidx.compose.ui.platform.WindowInfo
18+
import androidx.compose.ui.unit.Density
19+
import androidx.compose.ui.unit.Dp
20+
import androidx.compose.ui.unit.Velocity
21+
import androidx.compose.ui.unit.dp
22+
23+
@Stable
24+
internal class CollapsibleHeaderState(
25+
private val density: Density,
26+
val stickyHeaderHeightDp: Dp,
27+
val expandedHeaderHeightDp: Dp,
28+
) {
29+
private val stickyHeaderHeightPx: Float = with(density) { stickyHeaderHeightDp.toPx() }
30+
31+
private val expandedHeaderHeightPx: Float = with(density) { expandedHeaderHeightDp.toPx() }
32+
33+
val collapsedContentOffsetDp: Dp = with(density) { stickyHeaderHeightPx.toDp() + 18.dp }
34+
35+
var currentHeightPx by mutableFloatStateOf(expandedHeaderHeightPx)
36+
private set
37+
38+
val expansionProgress: Float
39+
get() = if (expandedHeaderHeightPx > 0f) (currentHeightPx / expandedHeaderHeightPx).coerceIn(0f, 1f) else 1f
40+
41+
val nestedScrollConnection = object : NestedScrollConnection {
42+
override fun onPreScroll(available: Offset, source: NestedScrollSource) =
43+
if (available.y < 0) consumeDelta(available.y) else Offset.Zero
44+
45+
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource) =
46+
if (available.y > 0) consumeDelta(available.y) else Offset.Zero
47+
48+
override suspend fun onPreFling(available: Velocity): Velocity {
49+
if (currentHeightPx <= 0f || currentHeightPx >= expandedHeaderHeightPx) return Velocity.Zero
50+
51+
val collapse = 0f
52+
val expand = expandedHeaderHeightPx
53+
54+
val target = when {
55+
available.y < -50f -> collapse
56+
available.y > 50f -> expand
57+
else -> if (currentHeightPx - collapse < expand - currentHeightPx) collapse else expand
58+
}
59+
60+
snapTo(targetHeight = target, velocity = available.y)
61+
62+
return Velocity(0f, available.y)
63+
}
64+
65+
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
66+
if (available.y > 0 && currentHeightPx < expandedHeaderHeightPx) {
67+
snapTo(targetHeight = expandedHeaderHeightPx, velocity = available.y)
68+
return Velocity(0f, available.y)
69+
}
70+
71+
if (available.y < 0 && currentHeightPx > 0f) {
72+
snapTo(targetHeight = 0f, velocity = available.y)
73+
return Velocity(0f, available.y)
74+
}
75+
76+
return Velocity.Zero
77+
}
78+
}
79+
80+
private fun consumeDelta(delta: Float): Offset {
81+
val oldHeight = currentHeightPx
82+
currentHeightPx = (oldHeight + delta).coerceIn(0f, expandedHeaderHeightPx)
83+
return Offset(0f, currentHeightPx - oldHeight)
84+
}
85+
86+
private suspend fun snapTo(targetHeight: Float, velocity: Float) {
87+
if (currentHeightPx == targetHeight) return
88+
89+
animate(
90+
initialValue = currentHeightPx,
91+
targetValue = targetHeight,
92+
initialVelocity = velocity,
93+
animationSpec = spring(
94+
dampingRatio = Spring.DampingRatioNoBouncy,
95+
stiffness = Spring.StiffnessMediumLow,
96+
),
97+
) { value, _ ->
98+
currentHeightPx = value
99+
}
100+
}
101+
}
102+
103+
@Composable
104+
internal fun rememberCollapsibleHeaderState(
105+
density: Density = LocalDensity.current,
106+
windowInfo: WindowInfo = LocalWindowInfo.current,
107+
stickyHeaderHeight: Dp = 48.dp,
108+
minExpandedHeaderHeight: Dp = 146.dp,
109+
): CollapsibleHeaderState {
110+
return remember(density, windowInfo, minExpandedHeaderHeight, stickyHeaderHeight) {
111+
val screenHeightDp = with(density) { windowInfo.containerSize.height.toDp() }
112+
val expandedHeaderHeightDp = (screenHeightDp * 0.18f).coerceAtLeast(minExpandedHeaderHeight)
113+
114+
CollapsibleHeaderState(
115+
density = density,
116+
stickyHeaderHeightDp = stickyHeaderHeight,
117+
expandedHeaderHeightDp = expandedHeaderHeightDp,
118+
)
119+
}
120+
}

presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt

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

0 commit comments

Comments
 (0)