From a320dfa0cff57f9b312f73f49f1a5b41b1a1b968 Mon Sep 17 00:00:00 2001 From: Dung Nguyen <158302785+DungxND@users.noreply.github.com> Date: Sat, 15 Aug 2026 09:51:57 +0700 Subject: [PATCH 1/2] feat(help-dialog): add scroll direction arrows --- .../ui/screens/main/EffectSections.kt | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt b/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt index 6da3916..4abe6d2 100644 --- a/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt +++ b/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt @@ -2,7 +2,10 @@ package com.llsl.viper4android.ui.screens.main import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -21,6 +24,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.TrendingUp import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.AspectRatio import androidx.compose.material.icons.filled.BlurCircular import androidx.compose.material.icons.filled.BlurOn @@ -204,9 +209,63 @@ fun EffectSection( LocalWindowInfo.current.containerSize.height .toDp() / 2 } - Column( - modifier = Modifier.heightIn(max = maxHeight).verticalScroll(rememberScrollState()), - ) { RichText(text = stringResource(descriptionRes)) } + val scrollState = rememberScrollState() + // Show bottom ↓ only when resting at the very top and there is content below + val showBottomArrow = scrollState.value == 0 && scrollState.maxValue > 0 + // Show top ↑ only when resting at the very bottom and there is content above + val showTopArrow = scrollState.maxValue > 0 && scrollState.value == scrollState.maxValue + val dialogColor = MaterialTheme.colorScheme.surfaceContainerHigh + Box(modifier = Modifier.heightIn(max = maxHeight)) { + Column( + modifier = Modifier.verticalScroll(scrollState), + ) { RichText(text = stringResource(descriptionRes)) } + // ↑ at top edge — user has reached the bottom, hint they can scroll back up + AnimatedVisibility( + visible = showTopArrow, + modifier = Modifier.align(Alignment.TopCenter), + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = + Modifier + .fillMaxWidth() + .height(28.dp) + .background(dialogColor), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowUp, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } + } + // ↓ at bottom edge — user is at the top, hint there is more below + AnimatedVisibility( + visible = showBottomArrow, + modifier = Modifier.align(Alignment.BottomCenter), + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = + Modifier + .fillMaxWidth() + .height(28.dp) + .background(dialogColor), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } + } + } }, confirmButton = { TextButton(onClick = { showHelpDialog = false }) { From 2bd6c3dd12d8ba10c370434ff8c927c74fa5b681 Mon Sep 17 00:00:00 2001 From: Dung Nguyen <158302785+DungxND@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:02:09 +0700 Subject: [PATCH 2/2] refactor: DRY the arrow hint --- .../ui/screens/main/EffectSections.kt | 77 ++++++++----------- 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt b/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt index 4abe6d2..ebcf750 100644 --- a/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt +++ b/app/src/main/java/com/llsl/viper4android/ui/screens/main/EffectSections.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -71,6 +72,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalDensity @@ -100,6 +102,33 @@ private fun rawToDb(raw: Number): Double = 20.0 * log10(raw.toDouble() / 100.0) private fun dbToRaw(db: Double): Int = (10.0.pow(db / 20.0) * 100.0).roundToInt() +@Composable +private fun BoxScope.ScrollArrowHint( + visible: Boolean, + alignment: Alignment, + icon: ImageVector, + color: Color, +) { + AnimatedVisibility( + visible = visible, + modifier = Modifier.align(alignment), + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier.fillMaxWidth().height(28.dp).background(color), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } + } +} + @OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable fun EffectSection( @@ -219,52 +248,8 @@ fun EffectSection( Column( modifier = Modifier.verticalScroll(scrollState), ) { RichText(text = stringResource(descriptionRes)) } - // ↑ at top edge — user has reached the bottom, hint they can scroll back up - AnimatedVisibility( - visible = showTopArrow, - modifier = Modifier.align(Alignment.TopCenter), - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = - Modifier - .fillMaxWidth() - .height(28.dp) - .background(dialogColor), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } - } - // ↓ at bottom edge — user is at the top, hint there is more below - AnimatedVisibility( - visible = showBottomArrow, - modifier = Modifier.align(Alignment.BottomCenter), - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = - Modifier - .fillMaxWidth() - .height(28.dp) - .background(dialogColor), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp), - ) - } - } + ScrollArrowHint(showTopArrow, Alignment.TopCenter, Icons.Default.KeyboardArrowUp, dialogColor) + ScrollArrowHint(showBottomArrow, Alignment.BottomCenter, Icons.Default.KeyboardArrowDown, dialogColor) } }, confirmButton = {