@@ -43,7 +43,9 @@ import androidx.compose.material3.Text
4343import androidx.compose.material3.TextButton
4444import androidx.compose.material3.IconButtonDefaults
4545import androidx.compose.runtime.Composable
46+ import androidx.compose.runtime.derivedStateOf
4647import androidx.compose.runtime.getValue
48+ import androidx.compose.runtime.mutableIntStateOf
4749import androidx.compose.runtime.mutableStateOf
4850import androidx.compose.runtime.remember
4951import androidx.compose.runtime.rememberCoroutineScope
@@ -71,9 +73,19 @@ import com.clhs.score.viewmodel.GradesUiState
7173import kotlinx.coroutines.launch
7274import kotlin.math.abs
7375import kotlin.math.roundToInt
76+ import androidx.compose.foundation.layout.offset
77+ import androidx.compose.foundation.lazy.LazyColumn
78+ import androidx.compose.foundation.lazy.rememberLazyListState
79+ import androidx.compose.ui.draw.shadow
80+ import androidx.compose.ui.unit.IntOffset
7481import androidx.compose.foundation.layout.WindowInsets
7582import androidx.compose.foundation.layout.safeDrawing
7683import androidx.compose.foundation.layout.windowInsetsPadding
84+ import androidx.compose.foundation.layout.statusBars
85+ import androidx.compose.foundation.layout.statusBarsPadding
86+ import androidx.compose.foundation.layout.navigationBarsPadding
87+ import androidx.compose.foundation.layout.windowInsetsTopHeight
88+ import androidx.compose.foundation.layout.Spacer
7789
7890@Composable
7991internal fun TrendChart (
@@ -185,18 +197,20 @@ internal fun ScoreSimulatorScreen(
185197 onBack : () -> Unit ,
186198) {
187199 val report = state.report
188- Scaffold (
189- snackbarHost = { SnackbarHost (snackbarHostState) },
190- containerColor = MaterialTheme .colorScheme.surface,
191- ) { padding ->
192- if (report == null ) {
193- Column (modifier = Modifier .padding(padding).padding(16 .dp)) {
200+ if (report == null ) {
201+ SubpageLayout (
202+ onBack = onBack,
203+ snackbarHost = { SnackbarHost (snackbarHostState) },
204+ containerColor = MaterialTheme .colorScheme.surface,
205+ ) {
206+ Column (modifier = Modifier .padding(16 .dp)) {
194207 EmptyAnalysisState (" 尚未載入成績資料。" )
195208 }
196- return @Scaffold
197209 }
210+ return
211+ }
198212
199- val coroutineScope = rememberCoroutineScope()
213+ val coroutineScope = rememberCoroutineScope()
200214
201215 val initialScores = remember(report) {
202216 report.subjects.associate { cleanSubjectName(it.subjectName) to it.scoreValue }
@@ -226,38 +240,83 @@ internal fun ScoreSimulatorScreen(
226240 val allHistory = remember(state.trendReports, state.simulatorHistoryReports) {
227241 state.trendReports + state.simulatorHistoryReports
228242 }
229- val historyMaxMin = remember(allHistory, report) {
230- val map = mutableMapOf< String , Pair < Double , Double >>()
231- report.subjects.forEach { subject ->
232- val key = cleanSubjectName( subject.subjectName)
233- val scores = allHistory.mapNotNull { r -> r.subjects.find { cleanSubjectName(it .subjectName) == key }?.scoreValue }
234- if (scores.isNotEmpty()) {
235- map[key] = scores.min() to scores.max()
236- }
243+
244+ val historyMaxMin = remember(allHistory, report) {
245+ val map = mutableMapOf< String , Pair < Double , Double >>()
246+ report.subjects.forEach { subject ->
247+ val key = cleanSubjectName(subject .subjectName)
248+ val scores = allHistory.mapNotNull { r -> r.subjects.find { cleanSubjectName(it.subjectName) == key }?.scoreValue }
249+ if ( scores.isNotEmpty()) {
250+ map[key] = scores.min() to scores.max()
237251 }
238- map
239252 }
240- val density = LocalDensity .current
241- var summaryCardHeightPx by remember { mutableStateOf(0 ) }
242- val summaryContentTopPadding = if (summaryCardHeightPx > 0 ) {
243- with (density) { summaryCardHeightPx.toDp() } + 16 .dp
244- } else {
245- 156 .dp
253+ map
254+ }
255+
256+ val density = LocalDensity .current
257+ val statusBars = WindowInsets .statusBars
258+ val statusBarHeightPx = remember(density, statusBars) { statusBars.getTop(density) }
259+ val topSpacerHeightPx = remember(density, statusBarHeightPx) { statusBarHeightPx + with (density) { 56 .dp.toPx() }.roundToInt() }
260+
261+ val listState = rememberLazyListState()
262+ var cardHeightPx by remember { mutableIntStateOf(0 ) }
263+
264+ val cardYOffsetPx by remember(listState) {
265+ derivedStateOf {
266+ if (listState.firstVisibleItemIndex == 0 ) {
267+ val offset = listState.firstVisibleItemScrollOffset
268+ maxOf(statusBarHeightPx, topSpacerHeightPx - offset)
269+ } else {
270+ statusBarHeightPx
271+ }
272+ }
273+ }
274+
275+ val isStuck by remember(listState) {
276+ derivedStateOf {
277+ if (listState.firstVisibleItemIndex == 0 ) {
278+ (topSpacerHeightPx - listState.firstVisibleItemScrollOffset) <= statusBarHeightPx
279+ } else {
280+ true
281+ }
246282 }
283+ }
247284
248- Box (
285+ SubpageLayout (
286+ onBack = onBack,
287+ snackbarHost = { SnackbarHost (snackbarHostState) },
288+ containerColor = MaterialTheme .colorScheme.surface,
289+ summaryContent = {
290+ SimulatorSummaryCard (
291+ currentAverage = currentAverage,
292+ currentTotal = currentTotal,
293+ adjustedAverage = adjustedAverage,
294+ adjustedTotal = adjustedTotal,
295+ modifier = Modifier
296+ .offset { IntOffset (0 , cardYOffsetPx) }
297+ .padding(horizontal = 16 .dp)
298+ .fillMaxWidth()
299+ .onGloballyPositioned { coordinates ->
300+ cardHeightPx = coordinates.size.height
301+ }
302+ )
303+ }
304+ ) {
305+ LazyColumn (
249306 modifier = Modifier
250307 .fillMaxSize()
251- .padding(padding),
308+ .padding(horizontal = 16 .dp)
309+ .navigationBarsPadding(),
310+ verticalArrangement = Arrangement .spacedBy(16 .dp),
311+ state = listState,
252312 ) {
253- Column (
254- modifier = Modifier
255- .fillMaxSize()
256- .verticalScroll(rememberScrollState())
257- .padding(horizontal = 16 .dp)
258- .padding(top = summaryContentTopPadding, bottom = 8 .dp),
259- verticalArrangement = Arrangement .spacedBy(16 .dp),
260- ) {
313+ item {
314+ Spacer (modifier = Modifier .height(
315+ with (density) { (topSpacerHeightPx + cardHeightPx).toDp() }
316+ ))
317+ }
318+
319+ item {
261320 Card (
262321 modifier = Modifier .fillMaxWidth(),
263322 shape = RoundedCornerShape (16 .dp),
@@ -336,7 +395,9 @@ internal fun ScoreSimulatorScreen(
336395 }
337396 }
338397 }
398+ }
339399
400+ item {
340401 Card (
341402 modifier = Modifier .fillMaxWidth(),
342403 shape = RoundedCornerShape (16 .dp),
@@ -389,33 +450,8 @@ internal fun ScoreSimulatorScreen(
389450 }
390451 }
391452 }
392-
393- SimulatorSummaryCard (
394- currentAverage = currentAverage,
395- currentTotal = currentTotal,
396- adjustedAverage = adjustedAverage,
397- adjustedTotal = adjustedTotal,
398- modifier = Modifier
399- .padding(horizontal = 16 .dp, vertical = 8 .dp)
400- .onGloballyPositioned { coordinates ->
401- summaryCardHeightPx = coordinates.size.height
402- },
403- )
404-
405- IconButton (
406- onClick = onBack,
407- modifier = Modifier
408- .align(Alignment .TopStart )
409- .padding(16 .dp),
410- colors = IconButtonDefaults .iconButtonColors(
411- containerColor = MaterialTheme .colorScheme.surface.copy(alpha = 0.8f ),
412- contentColor = MaterialTheme .colorScheme.onSurface,
413- ),
414- ) {
415- OutlinedRoundedSymbol (
416- icon = " arrow_back" ,
417- contentDescription = " 返回" ,
418- )
453+ item {
454+ Spacer (modifier = Modifier .height(16 .dp))
419455 }
420456 }
421457 }
@@ -552,7 +588,7 @@ private fun SubjectScoreSlider(
552588 val diff = value - subject.scoreValue
553589 val isAboveMax = historyMax != null && value > historyMax
554590 var lastDispatchedScore by remember(subject.subjectName, value.roundToInt()) {
555- mutableStateOf (value.roundToInt())
591+ mutableIntStateOf (value.roundToInt())
556592 }
557593 fun updateScore (score : Double , haptic : Boolean = false) {
558594 val roundedScore = score.coerceIn(0.0 , 100.0 ).roundToInt()
0 commit comments