From 743d0ad7600fe448782bf9428e608eecfe726248 Mon Sep 17 00:00:00 2001 From: Hepolise Date: Thu, 18 Sep 2025 22:38:25 +0300 Subject: [PATCH] feat(settings): extract action icon into separate component with animation and feedback - Added ActionIconButton with scale and alpha animation on press - Preserved card size when action button is hidden - Increased bottom padding for visual balance - Removed default ripple indication --- .../ui/screen/SettingsScreen.kt | 98 ++++++++++++++++--- 1 file changed, 85 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt index 52860b2..22dbbd5 100644 --- a/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/ru/hepolise/volumekeytrackcontrol/ui/screen/SettingsScreen.kt @@ -7,12 +7,17 @@ import android.content.res.Configuration import android.os.Vibrator import android.widget.Toast import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -20,8 +25,10 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -55,10 +62,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString.Builder import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle @@ -68,6 +80,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withLink import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.content.edit import androidx.core.net.toUri @@ -297,8 +310,9 @@ fun SettingsCard( ), ) { Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Row( @@ -309,23 +323,81 @@ fun SettingsCard( Spacer(Modifier.width(12.dp)) Text(title, style = MaterialTheme.typography.titleMedium) Spacer(Modifier.weight(1f)) - AnimatedVisibility( - visible = showAction, - enter = fadeIn() + slideInHorizontally(initialOffsetX = { it / 2 }), - exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it / 2 }) - ) { - Icon( - imageVector = actionIcon ?: Icons.Default.Edit, - contentDescription = null, - modifier = Modifier.clickable(onClick = { onActionClick?.invoke() }) - ) - } + ActionIconButton( + imageVector = actionIcon ?: Icons.Default.Edit, + contentDescription = null, + onClick = { onActionClick?.invoke() }, + visible = showAction + ) } content() } } } +@Composable +fun ActionIconButton( + imageVector: ImageVector, + contentDescription: String?, + onClick: () -> Unit, + modifier: Modifier = Modifier, + iconSize: Dp = 22.dp, + containerSize: Dp = 40.dp, + visible: Boolean = true +) { + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + + val scale by animateFloatAsState( + targetValue = if (pressed) 0.8f else 1f, + label = "actionIconScale" + ) + val alpha by animateFloatAsState( + targetValue = if (pressed) 0.6f else 1f, + label = "actionIconAlpha" + ) + val color by animateColorAsState( + targetValue = if (pressed) + MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) + else + MaterialTheme.colorScheme.onSurface, + label = "actionIconColor" + ) + + Box( + modifier = modifier + .size(containerSize) + .clip(CircleShape) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + enabled = visible + ) + .semantics { role = Role.Button }, + contentAlignment = Alignment.Center + ) { + AnimatedVisibility( + visible = visible, + enter = fadeIn() + slideInHorizontally(initialOffsetX = { it / 2 }), + exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it / 2 }) + ) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + tint = color, + modifier = Modifier + .size(iconSize) + .graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + } + ) + } + } +} + @Composable fun ModuleIsEnabled(launchedCount: Int) { Row(