feat(ui): press feedback + haptics

- pressDip modifier (scale 0.97 press / spring-back, transform-only,
  reduce-motion aware) sharing the row's interaction source with the
  Material ripple
- Library song rows: ripple + dip on tap; verified tap still plays
- Page-settle haptic tick in the tab pager (skipped on first frame)
This commit is contained in:
avi 2026-09-08 10:02:35 -05:00
commit 172e932ef3
2 changed files with 41 additions and 1 deletions

View file

@ -79,6 +79,9 @@ import com.rada.data.repository.SortOrder
import com.rada.ui.theme.AppearanceDialog import com.rada.ui.theme.AppearanceDialog
import com.rada.ui.components.AppHomeButton import com.rada.ui.components.AppHomeButton
import com.rada.ui.components.EqualizerBars import com.rada.ui.components.EqualizerBars
import com.rada.ui.theme.LocalReduceMotion
import com.rada.ui.theme.pressDip
import androidx.compose.foundation.interaction.MutableInteractionSource
import com.rada.ui.theme.LocalTokens import com.rada.ui.theme.LocalTokens
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -463,10 +466,12 @@ private fun SongListItem(
onToggleFavorite: () -> Unit, onToggleFavorite: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
) { ) {
val interaction = remember { MutableInteractionSource() }
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .pressDip(interaction, LocalReduceMotion.current)
.clickable(interactionSource = interaction, indication = androidx.compose.foundation.LocalIndication.current, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {

View file

@ -1,5 +1,13 @@
package com.rada.ui.theme package com.rada.ui.theme
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.CubicBezierEasing
@ -107,3 +115,30 @@ fun metadataExit(reduce: Boolean): ExitTransition =
/** Duration for press/feedback animations, zeroed under reduce motion. */ /** Duration for press/feedback animations, zeroed under reduce motion. */
fun feedbackDuration(reduce: Boolean): Int = if (reduce) 0 else FeedbackMs fun feedbackDuration(reduce: Boolean): Int = if (reduce) 0 else FeedbackMs
// ── Press feedback ────────────────────────────────────────────────────────
// Taps should feel physical before navigation resolves: the row dips
// (scale 0.97) on press and springs back on release. Transform-only, so
// it's cheap; zeroed under Reduce Motion by the caller.
/**
* Press-dip for list rows: scales to 0.97 while pressed. Use on rows that
* already have a clickable/combinedClickable modifier earlier in the chain.
*/
fun Modifier.pressDip(
interactionSource: MutableInteractionSource,
reduce: Boolean,
): Modifier = composed {
if (reduce) return@composed this
val pressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (pressed) 0.97f else 1f,
animationSpec = androidx.compose.animation.core.tween(
durationMillis = if (pressed) 90 else 160,
easing = if (pressed) MotionExit else MotionArrival,
),
label = "pressDip",
)
this.graphicsLayer { scaleX = scale; scaleY = scale }
}