From 14217e023a8815be20deea5f57b7c68056235587 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 20 Feb 2026 18:43:37 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Refactor:=20=EA=B3=A0=EC=A0=95=20=EC=83=81?= =?UTF-8?q?=EB=8B=A8=20=EB=B0=94=20StickyHeader=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/template/StickyHeader.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/StickyHeader.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/StickyHeader.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/StickyHeader.kt new file mode 100644 index 00000000..17c5be13 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/StickyHeader.kt @@ -0,0 +1,49 @@ +package com.threegap.bitnagil.presentation.home.component.template + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton + +@Composable +fun StickyHeader( + modifier: Modifier = Modifier, + onHelpClick: () -> Unit, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + BitnagilIcon( + id = R.drawable.ic_logo, + tint = BitnagilTheme.colors.coolGray50, + modifier = Modifier.padding(start = 16.dp), + ) + + Spacer(modifier = Modifier.weight(1f)) + + BitnagilIconButton( + id = R.drawable.ic_help_circle, + onClick = onHelpClick, + paddingValues = PaddingValues(12.dp), + tint = null, + modifier = Modifier.padding(end = 4.dp), + ) + } +} + +@Preview +@Composable +private fun Preview() { + StickyHeader(onHelpClick = {}) +} From 3db2b071013f5344f602364f7ef0ece6a154716a Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sat, 21 Feb 2026 14:00:41 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Refactor:=20CollapsibleHeaderState=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fling 동작과 snap 애니메이션을 처리하여 사용자 경험을 개선합니다. - 헤더 높이 계산 로직을 단순화하고, 화면 높이에 비례하여 동적으로 조절되도록 수정했습니다. - 관련 파일의 패키지 위치를 util에서 model로 변경하여 코드 구조를 개선합니다. --- .../home/model/CollapsibleHeaderState.kt | 120 +++++++++++++ .../home/util/CollapsibleHeaderState.kt | 168 ------------------ 2 files changed, 120 insertions(+), 168 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt new file mode 100644 index 00000000..cad4fa8a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt @@ -0,0 +1,120 @@ +package com.threegap.bitnagil.presentation.home.model + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.platform.WindowInfo +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp + +@Stable +internal class CollapsibleHeaderState( + private val density: Density, + val stickyHeaderHeightDp: Dp, + val expandedHeaderHeightDp: Dp, +) { + private val stickyHeaderHeightPx: Float = with(density) { stickyHeaderHeightDp.toPx() } + + private val expandedHeaderHeightPx: Float = with(density) { expandedHeaderHeightDp.toPx() } + + val collapsedContentOffsetDp: Dp = with(density) { stickyHeaderHeightPx.toDp() + 18.dp } + + var currentHeightPx by mutableFloatStateOf(expandedHeaderHeightPx) + private set + + val expansionProgress: Float + get() = if (expandedHeaderHeightPx > 0f) (currentHeightPx / expandedHeaderHeightPx).coerceIn(0f, 1f) else 1f + + val nestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource) = + if (available.y < 0) consumeDelta(available.y) else Offset.Zero + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource) = + if (available.y > 0) consumeDelta(available.y) else Offset.Zero + + override suspend fun onPreFling(available: Velocity): Velocity { + if (currentHeightPx <= 0f || currentHeightPx >= expandedHeaderHeightPx) return Velocity.Zero + + val collapse = 0f + val expand = expandedHeaderHeightPx + + val target = when { + available.y < -50f -> collapse + available.y > 50f -> expand + else -> if (currentHeightPx - collapse < expand - currentHeightPx) collapse else expand + } + + snapTo(targetHeight = target, velocity = available.y) + + return Velocity(0f, available.y) + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + if (available.y > 0 && currentHeightPx < expandedHeaderHeightPx) { + snapTo(targetHeight = expandedHeaderHeightPx, velocity = available.y) + return Velocity(0f, available.y) + } + + if (available.y < 0 && currentHeightPx > 0f) { + snapTo(targetHeight = 0f, velocity = available.y) + return Velocity(0f, available.y) + } + + return Velocity.Zero + } + } + + private fun consumeDelta(delta: Float): Offset { + val oldHeight = currentHeightPx + currentHeightPx = (oldHeight + delta).coerceIn(0f, expandedHeaderHeightPx) + return Offset(0f, currentHeightPx - oldHeight) + } + + private suspend fun snapTo(targetHeight: Float, velocity: Float) { + if (currentHeightPx == targetHeight) return + + animate( + initialValue = currentHeightPx, + targetValue = targetHeight, + initialVelocity = velocity, + animationSpec = spring( + dampingRatio = Spring.DampingRatioNoBouncy, + stiffness = Spring.StiffnessMediumLow, + ), + ) { value, _ -> + currentHeightPx = value + } + } +} + +@Composable +internal fun rememberCollapsibleHeaderState( + density: Density = LocalDensity.current, + windowInfo: WindowInfo = LocalWindowInfo.current, + stickyHeaderHeight: Dp = 48.dp, + minExpandedHeaderHeight: Dp = 146.dp, +): CollapsibleHeaderState { + return remember(density, windowInfo, minExpandedHeaderHeight, stickyHeaderHeight) { + val screenHeightDp = with(density) { windowInfo.containerSize.height.toDp() } + val expandedHeaderHeightDp = (screenHeightDp * 0.18f).coerceAtLeast(minExpandedHeaderHeight) + + CollapsibleHeaderState( + density = density, + stickyHeaderHeightDp = stickyHeaderHeight, + expandedHeaderHeightDp = expandedHeaderHeightDp, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt deleted file mode 100644 index 979c5df7..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.threegap.bitnagil.presentation.home.util - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.compose.ui.platform.WindowInfo -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -@Stable -data class CollapsibleHeaderState( - val lazyListState: LazyListState, - val nestedScrollConnection: NestedScrollConnection, - val currentHeaderHeight: Dp, - val collapseProgress: Float, - val isFullyCollapsed: Boolean, - val screenHeight: Dp, - val screenWidth: Dp, - val collapsedHeaderHeight: Dp, -) - -@Composable -internal fun rememberCollapsibleHeaderState(): CollapsibleHeaderState { - val windowInfo = LocalWindowInfo.current - val density = LocalDensity.current - - val (screenHeight, screenWidth) = rememberScreenSize(windowInfo, density) - val (expandedHeight, collapsedHeight, collapseRange) = rememberHeaderSizes(screenHeight) - val scrollState = rememberScrollBehavior(expandedHeight, collapsedHeight, collapseRange, density) - - return CollapsibleHeaderState( - lazyListState = scrollState.lazyListState, - nestedScrollConnection = scrollState.nestedScrollConnection, - currentHeaderHeight = scrollState.currentHeaderHeight, - collapseProgress = scrollState.collapseProgress, - isFullyCollapsed = scrollState.isFullyCollapsed, - screenHeight = screenHeight, - screenWidth = screenWidth, - collapsedHeaderHeight = collapsedHeight, - ) -} - -@Composable -private fun rememberScreenSize(windowInfo: WindowInfo, density: Density): Pair { - return remember(windowInfo.containerSize) { - with(density) { - windowInfo.containerSize.height.toDp() to windowInfo.containerSize.width.toDp() - } - } -} - -@Composable -private fun rememberHeaderSizes(screenHeight: Dp): Triple { - return remember(screenHeight) { - val expanded = screenHeight * EXPANDED_HEADER_RATIO - val collapsed = screenHeight * COLLAPSED_HEADER_RATIO - val range = expanded - collapsed - Triple(expanded, collapsed, range) - } -} - -@Stable -private data class ScrollBehaviorState( - val lazyListState: LazyListState, - val nestedScrollConnection: NestedScrollConnection, - val currentHeaderHeight: Dp, - val collapseProgress: Float, - val isFullyCollapsed: Boolean, -) - -@Composable -private fun rememberScrollBehavior( - expandedHeaderHeight: Dp, - collapsedHeaderHeight: Dp, - collapseRange: Dp, - density: Density, -): ScrollBehaviorState { - val lazyListState = rememberLazyListState() - var scrollOffset by remember { mutableFloatStateOf(0f) } - - val collapseRangePx = remember(collapseRange, density) { - with(density) { collapseRange.toPx() } - } - val bufferDistancePx = remember(density) { - with(density) { SCROLL_BUFFER_DISTANCE.toPx() } - } - val maxScrollOffsetPx = remember(collapseRangePx, bufferDistancePx) { - collapseRangePx + bufferDistancePx - } - - val isFullyCollapsed by remember { - derivedStateOf { scrollOffset <= -maxScrollOffsetPx } - } - - val nestedScrollConnection = remember(lazyListState, maxScrollOffsetPx, collapseRangePx, scrollOffset) { - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val deltaY = available.y - - return when { - deltaY < 0 -> { - if (scrollOffset > -maxScrollOffsetPx) { - val newOffset = (scrollOffset + deltaY).coerceAtLeast(-maxScrollOffsetPx) - val consumedOffset = newOffset - scrollOffset - scrollOffset = newOffset - Offset(0f, consumedOffset) - } else { - Offset.Zero - } - } - - deltaY > 0 -> { - if (scrollOffset < 0f && isScrollAtTop(lazyListState)) { - val newOffset = (scrollOffset + deltaY).coerceAtMost(0f) - val consumedOffset = newOffset - scrollOffset - scrollOffset = newOffset - Offset(0f, consumedOffset) - } else { - Offset.Zero - } - } - - else -> Offset.Zero - } - } - } - } - - val currentHeaderHeight by remember { - derivedStateOf { - val progress = (-scrollOffset / collapseRangePx).coerceIn(0f, 1f) - expandedHeaderHeight - (collapseRange * progress) - } - } - - val collapseProgress by remember { - derivedStateOf { - (-scrollOffset / collapseRangePx).coerceIn(0f, 1f) - } - } - - return ScrollBehaviorState( - lazyListState = lazyListState, - nestedScrollConnection = nestedScrollConnection, - currentHeaderHeight = currentHeaderHeight, - collapseProgress = collapseProgress, - isFullyCollapsed = isFullyCollapsed, - ) -} - -private fun isScrollAtTop(lazyListState: LazyListState): Boolean = - lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0 - -private const val EXPANDED_HEADER_RATIO = 225f / 722f -private const val COLLAPSED_HEADER_RATIO = 64f / 722f -private val SCROLL_BUFFER_DISTANCE = 30.dp From 6d71d52ae8841f41d66366d414c7dc499f332eee Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sat, 21 Feb 2026 14:02:03 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Refactor:=20=ED=99=88=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=ED=97=A4=EB=8D=94=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20UI=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CollapsibleHomeHeader를 CollapsibleHeader와 StickyHeader로 분리하여 역할과 책임을 명확히 했습니다. - HomeScreen의 UI 구조를 리팩토링하여 nestedScroll을 최상단 Box에 적용하고, 스크롤에 따른 헤더와 컨텐츠의 레이아웃 및 애니메이션 동작을 개선했습니다. --- .../bitnagil/presentation/home/HomeScreen.kt | 83 ++++++---- .../component/template/CollapsibleHeader.kt | 77 ++++++++++ .../template/CollapsibleHomeHeader.kt | 142 ------------------ 3 files changed, 128 insertions(+), 174 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHeader.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt index 0db4f45f..1eae8747 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt @@ -4,36 +4,44 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.presentation.home.component.template.CollapsibleHomeHeader +import com.threegap.bitnagil.presentation.home.component.template.CollapsibleHeader import com.threegap.bitnagil.presentation.home.component.template.EmptyRoutineView import com.threegap.bitnagil.presentation.home.component.template.RoutineSection +import com.threegap.bitnagil.presentation.home.component.template.StickyHeader import com.threegap.bitnagil.presentation.home.component.template.WeeklyDatePicker import com.threegap.bitnagil.presentation.home.contract.HomeSideEffect import com.threegap.bitnagil.presentation.home.contract.HomeState -import com.threegap.bitnagil.presentation.home.util.rememberCollapsibleHeaderState +import com.threegap.bitnagil.presentation.home.model.DailyEmotionUiModel +import com.threegap.bitnagil.presentation.home.model.rememberCollapsibleHeaderState import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect import java.time.LocalDate +import kotlin.math.pow @Composable fun HomeScreenContainer( @@ -71,6 +79,7 @@ fun HomeScreenContainer( @Composable private fun HomeScreen( uiState: HomeState, + modifier: Modifier = Modifier, onDateSelect: (LocalDate) -> Unit, onPreviousWeekClick: () -> Unit, onNextWeekClick: () -> Unit, @@ -80,18 +89,42 @@ private fun HomeScreen( onRegisterRoutineClick: () -> Unit, onRegisterEmotionClick: () -> Unit, onShowMoreRoutinesClick: () -> Unit, - modifier: Modifier = Modifier, ) { val collapsibleHeaderState = rememberCollapsibleHeaderState() Box( modifier = modifier .fillMaxSize() - .background(BitnagilTheme.colors.coolGray10), + .background(BitnagilTheme.colors.coolGray10) + .statusBarsPadding() + .nestedScroll(collapsibleHeaderState.nestedScrollConnection), ) { - Column { - Spacer(modifier = Modifier.height(collapsibleHeaderState.currentHeaderHeight)) + StickyHeader( + modifier = Modifier + .padding(top = 14.dp) + .height(collapsibleHeaderState.stickyHeaderHeightDp), + onHelpClick = onHelpClick, + ) + + CollapsibleHeader( + modifier = Modifier + .fillMaxWidth() + .padding(top = 80.dp, start = 18.dp, end = 18.dp) + .height(collapsibleHeaderState.expandedHeaderHeightDp) + .graphicsLayer { alpha = collapsibleHeaderState.expansionProgress.pow(3) }, + welcomeMessage = "${uiState.userNickname}${uiState.dailyEmotion.homeMessage}", + dailyEmotion = uiState.dailyEmotion, + onRegisterEmotionClick = onRegisterEmotionClick, + ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = collapsibleHeaderState.collapsedContentOffsetDp) + .graphicsLayer { translationY = collapsibleHeaderState.currentHeightPx } + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .background(color = BitnagilTheme.colors.coolGray99), + ) { WeeklyDatePicker( selectedDate = uiState.selectedDate, weeklyDates = uiState.currentWeeks, @@ -99,29 +132,20 @@ private fun HomeScreen( onDateSelect = onDateSelect, onPreviousWeekClick = onPreviousWeekClick, onNextWeekClick = onNextWeekClick, - modifier = Modifier - .background( - color = BitnagilTheme.colors.coolGray99, - shape = RoundedCornerShape( - topStart = 20.dp, - topEnd = 20.dp, - ), - ), ) if (uiState.selectedDateRoutines.isEmpty()) { EmptyRoutineView( - onRegisterRoutineClick = onRegisterRoutineClick, modifier = Modifier .fillMaxSize() - .background(BitnagilTheme.colors.coolGray99) - .padding(top = 62.dp), + .padding(top = 62.dp) + .verticalScroll(rememberScrollState()), + onRegisterRoutineClick = onRegisterRoutineClick, ) } else { Row( modifier = Modifier .fillMaxWidth() - .background(BitnagilTheme.colors.coolGray99) .padding(start = 16.dp, end = 4.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top, @@ -145,15 +169,13 @@ private fun HomeScreen( LazyColumn( modifier = Modifier .fillMaxSize() - .background(BitnagilTheme.colors.coolGray99) - .nestedScroll(collapsibleHeaderState.nestedScrollConnection) .padding(horizontal = 16.dp), - state = collapsibleHeaderState.lazyListState, verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(bottom = 48.dp), ) { items( items = uiState.selectedDateRoutines, - key = { routine -> "${routine.id}_${uiState.selectedDate}" }, + key = { routine -> routine.id }, ) { routine -> RoutineSection( routine = routine, @@ -166,14 +188,6 @@ private fun HomeScreen( } } } - - CollapsibleHomeHeader( - userName = uiState.userNickname, - dailyEmotion = uiState.dailyEmotion, - collapsibleHeaderState = collapsibleHeaderState, - onHelpClick = onHelpClick, - onRegisterEmotion = onRegisterEmotionClick, - ) } } @@ -181,7 +195,12 @@ private fun HomeScreen( @Composable private fun HomeScreenPreview() { HomeScreen( - uiState = HomeState.INIT, + uiState = HomeState.INIT.copy( + userNickname = "홍길동", + dailyEmotion = DailyEmotionUiModel.INIT.copy( + homeMessage = "님, 오셨군요!\n오늘 기분은 어떤가요?", + ), + ), onDateSelect = {}, onPreviousWeekClick = {}, onNextWeekClick = {}, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHeader.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHeader.kt new file mode 100644 index 00000000..0065c6e0 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHeader.kt @@ -0,0 +1,77 @@ +package com.threegap.bitnagil.presentation.home.component.template + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.presentation.home.component.atom.EmotionRegisterButton +import com.threegap.bitnagil.presentation.home.model.DailyEmotionUiModel + +@Composable +fun CollapsibleHeader( + modifier: Modifier = Modifier, + welcomeMessage: String, + dailyEmotion: DailyEmotionUiModel, + onRegisterEmotionClick: () -> Unit, +) { + val baseImageHeight = 148.dp + val baseImageWidth = 108.dp + + Box(modifier = modifier) { + Column( + modifier = Modifier.align(Alignment.TopStart), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = welcomeMessage, + style = BitnagilTheme.typography.cafe24SsurroundAir.copy( + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), + ), + color = BitnagilTheme.colors.white, + fontWeight = FontWeight.SemiBold, + ) + + EmotionRegisterButton( + onClick = onRegisterEmotionClick, + enabled = !dailyEmotion.hasEmotion, + ) + } + + AsyncImage( + model = dailyEmotion.imageUrl, + modifier = Modifier + .align(Alignment.TopEnd) + .size(baseImageWidth, baseImageHeight), + contentDescription = null, + placeholder = painterResource(R.drawable.default_emotion), + error = painterResource(R.drawable.default_emotion), + ) + } +} + +@Preview +@Composable +private fun CollapsibleHeaderPreview() { + CollapsibleHeader( + modifier = Modifier.fillMaxWidth(), + welcomeMessage = "대현님 오셨군요!\n오늘 기분은 어떤가요?!", + dailyEmotion = DailyEmotionUiModel.INIT, + onRegisterEmotionClick = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt deleted file mode 100644 index 260fffe5..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.threegap.bitnagil.presentation.home.component.template - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import coil3.request.ImageRequest -import coil3.request.crossfade -import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon -import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton -import com.threegap.bitnagil.presentation.home.component.atom.EmotionRegisterButton -import com.threegap.bitnagil.presentation.home.model.DailyEmotionUiModel -import com.threegap.bitnagil.presentation.home.util.CollapsibleHeaderState -import com.threegap.bitnagil.presentation.home.util.rememberCollapsibleHeaderState - -@Composable -fun CollapsibleHomeHeader( - userName: String, - dailyEmotion: DailyEmotionUiModel, - collapsibleHeaderState: CollapsibleHeaderState, - onHelpClick: () -> Unit, - onRegisterEmotion: () -> Unit, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - val alpha by animateFloatAsState( - targetValue = 1f - collapsibleHeaderState.collapseProgress, - animationSpec = tween(durationMillis = 300), - label = "header_alpha", - ) - - Column( - modifier = modifier - .height(collapsibleHeaderState.currentHeaderHeight), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(collapsibleHeaderState.collapsedHeaderHeight) - .statusBarsPadding() - .padding(start = 16.dp, end = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - BitnagilIcon( - id = R.drawable.ic_logo, - tint = BitnagilTheme.colors.coolGray50, - ) - - Spacer(modifier = Modifier.weight(1f)) - - BitnagilIconButton( - id = R.drawable.ic_help_circle, - onClick = onHelpClick, - paddingValues = PaddingValues(12.dp), - tint = null, - ) - } - - if (collapsibleHeaderState.currentHeaderHeight > collapsibleHeaderState.collapsedHeaderHeight) { - Box( - modifier = Modifier - .padding(top = 18.dp) - .height(collapsibleHeaderState.currentHeaderHeight - collapsibleHeaderState.collapsedHeaderHeight) - .alpha(alpha), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .align(Alignment.TopStart), - verticalArrangement = Arrangement.spacedBy(20.dp), - ) { - Text( - text = "$userName${dailyEmotion.homeMessage}", - style = BitnagilTheme.typography.cafe24SsurroundAir, - color = BitnagilTheme.colors.white, - fontWeight = FontWeight.SemiBold, - ) - - EmotionRegisterButton( - onClick = onRegisterEmotion, - enabled = !dailyEmotion.hasEmotion, - ) - } - - AsyncImage( - model = remember(dailyEmotion.imageUrl) { - ImageRequest.Builder(context) - .data(dailyEmotion.imageUrl) - .crossfade(true) - .build() - }, - contentDescription = null, - placeholder = painterResource(R.drawable.default_emotion), - error = painterResource(R.drawable.default_emotion), - contentScale = ContentScale.Fit, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 18.dp) - .aspectRatio(108f / 148f), - ) - } - } - } -} - -@Preview -@Composable -private fun CollapsibleHomeHeaderPreview() { - CollapsibleHomeHeader( - userName = "대현", - dailyEmotion = DailyEmotionUiModel.INIT, - collapsibleHeaderState = rememberCollapsibleHeaderState(), - onHelpClick = {}, - onRegisterEmotion = {}, - ) -} From 172764ba4dd442451c56187def199ddb518403f7 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sat, 21 Feb 2026 14:49:37 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Chore:=20remember=20key=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/home/model/CollapsibleHeaderState.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt index cad4fa8a..f40c986e 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/CollapsibleHeaderState.kt @@ -107,8 +107,9 @@ internal fun rememberCollapsibleHeaderState( stickyHeaderHeight: Dp = 48.dp, minExpandedHeaderHeight: Dp = 146.dp, ): CollapsibleHeaderState { - return remember(density, windowInfo, minExpandedHeaderHeight, stickyHeaderHeight) { - val screenHeightDp = with(density) { windowInfo.containerSize.height.toDp() } + val containerSize = windowInfo.containerSize + return remember(density, containerSize, minExpandedHeaderHeight, stickyHeaderHeight) { + val screenHeightDp = with(density) { containerSize.height.toDp() } val expandedHeaderHeightDp = (screenHeightDp * 0.18f).coerceAtLeast(minExpandedHeaderHeight) CollapsibleHeaderState(