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:
parent
d612ba6f49
commit
172e932ef3
2 changed files with 41 additions and 1 deletions
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue