@@ -29,6 +29,7 @@ import androidx.compose.foundation.Canvas
2929import androidx.compose.foundation.ExperimentalFoundationApi
3030import androidx.compose.foundation.background
3131import androidx.compose.foundation.combinedClickable
32+ import androidx.compose.foundation.gestures.detectTapGestures
3233import androidx.compose.foundation.interaction.MutableInteractionSource
3334import androidx.compose.foundation.interaction.PressInteraction
3435import androidx.compose.foundation.interaction.collectIsDraggedAsState
@@ -80,6 +81,7 @@ import androidx.compose.runtime.LaunchedEffect
8081import androidx.compose.runtime.getValue
8182import androidx.compose.runtime.mutableStateOf
8283import androidx.compose.runtime.remember
84+ import androidx.compose.runtime.rememberCoroutineScope
8385import androidx.compose.runtime.setValue
8486import androidx.compose.ui.Alignment
8587import androidx.compose.ui.Modifier
@@ -99,24 +101,19 @@ import androidx.compose.ui.res.painterResource
99101import androidx.compose.ui.res.stringResource
100102import androidx.compose.ui.text.TextStyle
101103import androidx.compose.ui.text.font.FontWeight
104+ import androidx.compose.ui.text.style.TextOverflow
102105import androidx.compose.ui.unit.TextUnit
103106import androidx.compose.ui.unit.dp
104107import androidx.compose.ui.unit.sp
105108import androidx.graphics.shapes.CornerRounding
106109import androidx.graphics.shapes.Morph
107110import androidx.graphics.shapes.RoundedPolygon
108- import androidx.compose.foundation.gestures.detectTapGestures
109- import androidx.compose.material.icons.rounded.Check
110- import androidx.compose.material3.ButtonDefaults
111- import androidx.compose.material3.FilledTonalButton
112- import androidx.compose.runtime.rememberCoroutineScope
113- import androidx.compose.ui.graphics.Shape
114- import kotlinx.coroutines.delay
115- import kotlinx.coroutines.launch
116111import androidx.graphics.shapes.star
117112import androidx.graphics.shapes.toPath
118113import com.better.nothing.music.vizualizer.R
114+ import kotlinx.coroutines.delay
119115import kotlinx.coroutines.flow.collectLatest
116+ import kotlinx.coroutines.launch
120117import kotlin.math.exp
121118import kotlin.math.ln
122119import kotlin.time.Duration.Companion.milliseconds
@@ -320,6 +317,7 @@ fun FlowRowScope.OptionTile(
320317 when (interaction) {
321318 is PressInteraction .Press -> {
322319 pressStartTime = SystemClock .elapsedRealtime()
320+ haptics.performHapticFeedback(HapticFeedbackType .SegmentTick )
323321 isWeightExpanded = true
324322 }
325323 is PressInteraction .Release , is PressInteraction .Cancel -> {
@@ -331,6 +329,7 @@ fun FlowRowScope.OptionTile(
331329 delay(remainingFloorDelay.milliseconds)
332330 }
333331 isWeightExpanded = false
332+ haptics.performHapticFeedback(HapticFeedbackType .SegmentFrequentTick )
334333 }
335334 }
336335 }
@@ -357,7 +356,7 @@ fun FlowRowScope.OptionTile(
357356 val animatedRadius by animateDpAsState(
358357 targetValue = targetRadius,
359358 animationSpec = if (m3eEnabled) {
360- spring(dampingRatio = Spring .DampingRatioMediumBouncy , stiffness = Spring .StiffnessLow )
359+ spring(dampingRatio = Spring .DampingRatioHighBouncy , stiffness = Spring .StiffnessMediumLow )
361360 } else {
362361 spring(stiffness = Spring .StiffnessMedium )
363362 },
@@ -378,7 +377,6 @@ fun FlowRowScope.OptionTile(
378377 Surface (
379378 onClick = if (enabled) {
380379 {
381- haptics.performHapticFeedback(HapticFeedbackType .SegmentTick )
382380 onClick()
383381 }
384382 } else ({}),
@@ -759,8 +757,7 @@ fun <T> ExpressiveSegmentedButtonRow(
759757 selectedItem : T ,
760758 onItemSelection : (T ) -> Unit ,
761759 labelProvider : @Composable (T ) -> String ,
762- modifier : Modifier = Modifier ,
763- iconProvider : @Composable ((T ) -> Unit )? = null // Optional custom icon overrides
760+ modifier : Modifier = Modifier
764761) {
765762 val haptics = LocalHapticFeedback .current
766763 val scope = rememberCoroutineScope()
@@ -772,53 +769,66 @@ fun <T> ExpressiveSegmentedButtonRow(
772769 ) {
773770 items.forEachIndexed { index, item ->
774771 val isSelected = item == selectedItem
775- val buttonShape = rememberExpressiveShape(index = index, count = items.size)
776772
777- // Visual tap bounce tracker
778773 var isPressed by remember { mutableStateOf(false ) }
779774
780- // Spring specs for standard Material 3 expressive/ bouncy physics
775+ // Elastic bouncy spring configuration
781776 val bouncySpec = spring<Float >(
782777 dampingRatio = Spring .DampingRatioHighBouncy ,
783778 stiffness = Spring .StiffnessMediumLow
784779 )
780+ val dpBouncySpec = spring< androidx.compose.ui.unit.Dp > (
781+ dampingRatio = Spring .DampingRatioLowBouncy ,
782+ stiffness = Spring .StiffnessMedium
783+ )
785784
786- // Animate layout expansion weight (1.2f if selected, 1.0f otherwise )
785+ // 1. Dynamic layout expansion weight (grows wider horizontally )
787786 val animatedWeight by animateFloatAsState(
788- targetValue = if (isSelected) 1.2f else 1.0f ,
787+ targetValue = if (isPressed) 0.89f else if ( isSelected) 1.2f else 1.0f ,
789788 animationSpec = bouncySpec,
790789 label = " ExpressiveWeightAnimation"
791790 )
792791
793- // Animate scale factor on click
794- val animatedScale by animateFloatAsState(
795- targetValue = if (isPressed) 0.92f else 1.0f ,
796- animationSpec = bouncySpec,
797- label = " BouncyScaleAnimation"
792+ // 3. Dynamic corner fluid morphing (becomes a pill when selected)
793+ val fullyRounded = 40 .dp
794+ val slightlyRounded = 8 .dp
795+
796+ val targetTopStart = if (isSelected || index == 0 || items.size == 1 ) fullyRounded else slightlyRounded
797+ val targetBottomStart = if (isSelected || index == 0 || items.size == 1 ) fullyRounded else slightlyRounded
798+ val targetTopEnd = if (isSelected || index == items.size - 1 || items.size == 1 ) fullyRounded else slightlyRounded
799+ val targetBottomEnd = if (isSelected || index == items.size - 1 || items.size == 1 ) fullyRounded else slightlyRounded
800+
801+ val topStart by animateDpAsState(targetValue = targetTopStart, animationSpec = dpBouncySpec, label = " TopStart" )
802+ val bottomStart by animateDpAsState(targetValue = targetBottomStart, animationSpec = dpBouncySpec, label = " BottomStart" )
803+ val topEnd by animateDpAsState(targetValue = targetTopEnd, animationSpec = dpBouncySpec, label = " TopEnd" )
804+ val bottomEnd by animateDpAsState(targetValue = targetBottomEnd, animationSpec = dpBouncySpec, label = " BottomEnd" )
805+
806+ val dynamicButtonShape = RoundedCornerShape (
807+ topStart = topStart.coerceAtLeast(0 .dp),
808+ bottomStart = bottomStart.coerceAtLeast(0 .dp),
809+ topEnd = topEnd.coerceAtLeast(0 .dp),
810+ bottomEnd = bottomEnd.coerceAtLeast(0 .dp)
798811 )
799812
800- FilledTonalButton (
801- onClick = {}, // Handled manually via gesture listener
802- shape = buttonShape,
803- colors = ButtonDefaults .filledTonalButtonColors(
804- containerColor = if (isSelected) {
805- MaterialTheme .colorScheme.primaryContainer
806- } else {
807- MaterialTheme .colorScheme.surfaceContainerHighest
808- },
809- contentColor = if (isSelected) {
810- MaterialTheme .colorScheme.onPrimaryContainer
811- } else {
812- MaterialTheme .colorScheme.onSurfaceVariant
813- }
814- ),
813+ val containerColor = if (isSelected) {
814+ MaterialTheme .colorScheme.primary
815+ } else {
816+ MaterialTheme .colorScheme.surfaceContainerHighest
817+ }
818+
819+ val contentColor = if (isSelected) {
820+ MaterialTheme .colorScheme.onPrimary
821+ } else {
822+ MaterialTheme .colorScheme.onSurfaceVariant
823+ }
824+
825+ Surface (
826+ color = containerColor,
827+ contentColor = contentColor,
828+ shape = dynamicButtonShape,
815829 modifier = Modifier
816- .weight(animatedWeight) // Apportion width adaptively based on selection state
817- .graphicsLayer {
818- scaleX = animatedScale
819- scaleY = animatedScale
820- }
821- .pointerInput(item) {
830+ .weight(animatedWeight) // Restored horizontal weight expansion
831+ .pointerInput(item, isSelected) {
822832 detectTapGestures(
823833 onPress = {
824834 val startTime = System .currentTimeMillis()
@@ -828,6 +838,7 @@ fun <T> ExpressiveSegmentedButtonRow(
828838 try {
829839 awaitRelease()
830840 } finally {
841+ haptics.performHapticFeedback(HapticFeedbackType .SegmentFrequentTick )
831842 val elapsedTime = System .currentTimeMillis() - startTime
832843 val remainingTime = 150L - elapsedTime
833844
@@ -836,64 +847,34 @@ fun <T> ExpressiveSegmentedButtonRow(
836847 delay(remainingTime.milliseconds)
837848 }
838849 isPressed = false
839- onItemSelection(item)
850+ if (! isSelected) {
851+ onItemSelection(item)
852+ }
840853 }
841854 }
842855 }
843856 )
844857 }
845858 ) {
846- // Show icon exclusively when the item is active/selected
847- if (isSelected) {
848- if (iconProvider != null ) {
849- iconProvider(item)
850- } else {
851- Icon (
852- imageVector = Icons .Rounded .Check ,
853- contentDescription = " Selected" ,
854- modifier = Modifier .graphicsLayer {
855- // Subtle entry spring synchronization
856- scaleX = animatedScale
857- scaleY = animatedScale
858- }
859- )
860- }
861- Spacer (modifier = Modifier .width(6 .dp))
862- }
859+ Row (
860+ modifier = Modifier .padding(horizontal = 4 .dp, vertical = 10 .dp),
861+ horizontalArrangement = Arrangement .Center ,
862+ verticalAlignment = Alignment .CenterVertically
863+ ) {
864+ // 4. Smooth Icon Slide + Fade Entry
863865
864- Text (
865- text = labelProvider(item),
866- style = MaterialTheme .typography.labelLarge,
867- maxLines = 1
868- )
866+ Text (
867+ text = labelProvider(item),
868+ style = MaterialTheme .typography.labelLarge,
869+ maxLines = 1 ,
870+ overflow = TextOverflow .Ellipsis
871+ )
872+ }
869873 }
870874 }
871875 }
872876}
873877
874- @Composable
875- private fun rememberExpressiveShape (index : Int , count : Int ): Shape {
876- val fullyRounded = 100 .dp
877- val slightlyRounded = 8 .dp
878-
879- return when {
880- count == 1 -> RoundedCornerShape (fullyRounded)
881- index == 0 -> RoundedCornerShape (
882- topStart = fullyRounded,
883- bottomStart = fullyRounded,
884- topEnd = slightlyRounded,
885- bottomEnd = slightlyRounded
886- )
887- index == count - 1 -> RoundedCornerShape (
888- topStart = slightlyRounded,
889- bottomStart = slightlyRounded,
890- topEnd = fullyRounded,
891- bottomEnd = fullyRounded
892- )
893- else -> RoundedCornerShape (slightlyRounded)
894- }
895- }
896-
897878@OptIn(ExperimentalMaterial3Api ::class )
898879@Composable
899880fun ExpressiveSlider (
0 commit comments