11package com.yapp.breake.presentation.home.screen
22
3+ import androidx.compose.animation.AnimatedVisibility
4+ import androidx.compose.animation.animateContentSize
5+ import androidx.compose.animation.core.LinearEasing
6+ import androidx.compose.animation.core.LinearOutSlowInEasing
7+ import androidx.compose.animation.core.animateDpAsState
8+ import androidx.compose.animation.core.animateFloatAsState
9+ import androidx.compose.animation.core.tween
10+ import androidx.compose.animation.fadeIn
11+ import androidx.compose.animation.fadeOut
12+ import androidx.compose.foundation.Image
13+ import androidx.compose.foundation.LocalOverscrollFactory
14+ import androidx.compose.foundation.background
315import androidx.compose.foundation.layout.Column
416import androidx.compose.foundation.layout.Row
517import androidx.compose.foundation.layout.fillMaxSize
618import androidx.compose.foundation.layout.fillMaxWidth
719import androidx.compose.foundation.layout.padding
8- import androidx.compose.foundation.layout.statusBarsPadding
20+ import androidx.compose.foundation.lazy.LazyColumn
21+ import androidx.compose.foundation.lazy.itemsIndexed
22+ import androidx.compose.foundation.lazy.rememberLazyListState
23+ import androidx.compose.material.icons.Icons
24+ import androidx.compose.material.icons.filled.Add
25+ import androidx.compose.material3.ExperimentalMaterial3Api
26+ import androidx.compose.material3.Icon
27+ import androidx.compose.material3.IconButton
928import androidx.compose.material3.MaterialTheme
29+ import androidx.compose.material3.Scaffold
1030import androidx.compose.material3.Text
31+ import androidx.compose.material3.TopAppBar
32+ import androidx.compose.material3.TopAppBarDefaults
1133import androidx.compose.runtime.Composable
34+ import androidx.compose.runtime.CompositionLocalProvider
35+ import androidx.compose.runtime.derivedStateOf
36+ import androidx.compose.runtime.getValue
37+ import androidx.compose.runtime.remember
1238import androidx.compose.ui.Alignment
1339import androidx.compose.ui.Modifier
40+ import androidx.compose.ui.input.nestedscroll.nestedScroll
41+ import androidx.compose.ui.layout.ContentScale
42+ import androidx.compose.ui.res.painterResource
1443import androidx.compose.ui.res.stringResource
44+ import androidx.compose.ui.text.style.TextAlign
1545import androidx.compose.ui.tooling.preview.Preview
1646import androidx.compose.ui.unit.dp
1747import com.yapp.breake.core.designsystem.component.HorizontalSpacer
1848import com.yapp.breake.core.designsystem.component.VerticalSpacer
1949import com.yapp.breake.core.designsystem.theme.BrakeTheme
50+ import com.yapp.breake.core.designsystem.theme.Gray100
2051import com.yapp.breake.core.designsystem.theme.Gray200
52+ import com.yapp.breake.core.designsystem.theme.Gray900
2153import com.yapp.breake.core.model.app.AppGroup
2254import com.yapp.breake.presentation.home.R
23- import com.yapp.breake.presentation.home.component.AppGroupList
24- import com.yapp.breake.presentation.home.component.ImageTextBox
55+ import com.yapp.breake.presentation.home.component.AppGroupItem
2556
57+ @OptIn(ExperimentalMaterial3Api ::class )
2658@Composable
2759internal fun ListScreen (
2860 appGroups : List <AppGroup >,
2961 onEditClick : (AppGroup ) -> Unit ,
62+ onAddClick : () -> Unit ,
3063) {
31- Column (
32- modifier = Modifier .fillMaxSize(),
33- horizontalAlignment = Alignment .CenterHorizontally ,
34- ) {
35- ImageTextBox (
36- imageRes = R .drawable.img_home_list,
37- text = stringResource(R .string.list_screen_description),
38- modifier = Modifier .statusBarsPadding(),
39- )
40- VerticalSpacer (30 .dp)
41- Row (
42- verticalAlignment = Alignment .Bottom ,
43- modifier = Modifier
44- .fillMaxWidth()
45- .padding(horizontal = 24 .dp),
46- ) {
47- Text (
48- text = stringResource(R .string.group),
49- style = BrakeTheme .typography.subtitle22SB,
50- color = MaterialTheme .colorScheme.onSurface,
51- )
52- HorizontalSpacer (1f )
53- Text (
54- text = stringResource(R .string.group_count_format, appGroups.size),
55- style = BrakeTheme .typography.body12M,
56- color = Gray200 ,
57- )
64+ val scrollBehavior = TopAppBarDefaults .pinnedScrollBehavior()
65+ val listState = rememberLazyListState()
66+ val showTitle by remember {
67+ derivedStateOf {
68+ listState.firstVisibleItemIndex > 0
5869 }
59- VerticalSpacer (16 .dp)
60- AppGroupList (
61- appGroups = appGroups,
62- onEditClick = onEditClick,
63- modifier = Modifier
64- .fillMaxWidth()
65- .padding(horizontal = 16 .dp),
66- )
6770 }
71+ val alpha by animateFloatAsState(
72+ targetValue = if (showTitle) 1f else 0f ,
73+ animationSpec = tween(20 , easing = LinearEasing ),
74+ label = " appbarAlpha" ,
75+ )
76+ val container = Gray900 .copy(alpha = alpha)
77+ val headerKey = " groupsHeader"
78+
79+ Scaffold (
80+ modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection),
81+ topBar = {
82+ TopAppBar (
83+ title = {
84+ AnimatedVisibility (
85+ visible = showTitle,
86+ modifier = Modifier .fillMaxWidth(),
87+ enter = fadeIn(),
88+ exit = fadeOut(),
89+ ) {
90+ Text (
91+ text = stringResource(R .string.list_screen_title),
92+ style = BrakeTheme .typography.subtitle16SB,
93+ color = Gray100 ,
94+ textAlign = TextAlign .Center ,
95+ )
96+ }
97+ },
98+ modifier = Modifier .animateContentSize(),
99+ navigationIcon = {
100+ // 공간만 차지하는 네비게이션 아이콘
101+ HorizontalSpacer (48 .dp)
102+ },
103+ actions = {
104+ IconButton (onClick = onAddClick) {
105+ Icon (
106+ imageVector = Icons .Default .Add ,
107+ contentDescription = stringResource(R .string.add_button_content_description),
108+ )
109+ }
110+ },
111+ scrollBehavior = scrollBehavior,
112+ colors = TopAppBarDefaults .topAppBarColors(
113+ containerColor = container,
114+ scrolledContainerColor = container,
115+ navigationIconContentColor = Gray100 ,
116+ titleContentColor = Gray100 ,
117+ actionIconContentColor = Gray100 ,
118+ ),
119+ )
120+ },
121+ content = { innerPadding ->
122+ CompositionLocalProvider (LocalOverscrollFactory provides null ) {
123+ LazyColumn (
124+ modifier = Modifier
125+ .fillMaxSize()
126+ .padding(bottom = 16 .dp),
127+ state = listState,
128+ horizontalAlignment = Alignment .CenterHorizontally ,
129+ ) {
130+ item {
131+ Column (
132+ modifier = Modifier
133+ .fillMaxWidth()
134+ .padding(horizontal = 16 .dp),
135+ horizontalAlignment = Alignment .CenterHorizontally ,
136+ ) {
137+ Image (
138+ painter = painterResource(id = R .drawable.img_home_list),
139+ contentDescription = null ,
140+ modifier = Modifier .fillMaxWidth(),
141+ contentScale = ContentScale .FillWidth ,
142+ )
143+ VerticalSpacer (12 .dp)
144+ Text (
145+ text = stringResource(R .string.list_screen_description),
146+ style = BrakeTheme .typography.subtitle22SB,
147+ color = Gray100 ,
148+ textAlign = TextAlign .Center ,
149+ )
150+ }
151+ }
152+
153+ stickyHeader(key = headerKey) {
154+ val isPinned by remember {
155+ derivedStateOf {
156+ val info = listState.layoutInfo.visibleItemsInfo
157+ .firstOrNull { it.key == headerKey }
158+ info?.offset == 0
159+ }
160+ }
161+
162+ // 핀일 때만 AppBar 높이 적용, 아니면 0
163+ val topInset = innerPadding.calculateTopPadding()
164+ val spacer by animateDpAsState(
165+ targetValue = if (isPinned) topInset else 36 .dp,
166+ animationSpec = tween(500 , easing = LinearOutSlowInEasing ),
167+ label = " HeaderTopInset" ,
168+ )
169+ Column (
170+ modifier = Modifier .background(container),
171+ ) {
172+ VerticalSpacer (spacer)
173+ Row (
174+ verticalAlignment = Alignment .Bottom ,
175+ modifier = Modifier
176+ .fillMaxWidth()
177+ .padding(top = 4 .dp, bottom = 16 .dp)
178+ .padding(horizontal = 24 .dp),
179+ ) {
180+ Text (
181+ text = stringResource(R .string.group),
182+ style = BrakeTheme .typography.subtitle22SB,
183+ color = MaterialTheme .colorScheme.onSurface,
184+ )
185+ HorizontalSpacer (1f )
186+ Text (
187+ text = stringResource(
188+ R .string.group_count_format,
189+ appGroups.size,
190+ ),
191+ style = BrakeTheme .typography.body12M,
192+ color = Gray200 ,
193+ )
194+ }
195+ }
196+ }
197+
198+ itemsIndexed(
199+ appGroups,
200+ key = { _, appGroup -> appGroup.id },
201+ ) { index, appGroup ->
202+ AppGroupItem (
203+ appGroup = appGroup,
204+ onEditClick = { onEditClick(appGroup) },
205+ modifier = Modifier
206+ .fillMaxWidth()
207+ .padding(horizontal = 16 .dp),
208+ )
209+ if (index != appGroups.lastIndex) {
210+ VerticalSpacer (12 .dp)
211+ }
212+ }
213+ }
214+ }
215+ },
216+ )
68217}
69218
70219@Preview
@@ -74,6 +223,7 @@ private fun ListScreenPreview() {
74223 ListScreen (
75224 appGroups = listOf (AppGroup .sample),
76225 onEditClick = { /* TODO: Handle app group click */ },
226+ onAddClick = { /* Handle add click */ },
77227 )
78228 }
79229}
0 commit comments