From 0c096d133d8603957ff5d6416cd56ab9f72b319a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Dec 2025 04:07:45 +0000 Subject: [PATCH] feat(ui): Material You 3 Expressive 'Glow Up' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This update brings the "Aura" design philosophy to life! * **Physics Everything**: Replaced static clicks with satisfying physics-based scale animations using `spring(dampingRatio = 0.6f, stiffness = 300f)` for that "fidget toy" feel. * **Tactile Feedback**: Implemented premium haptics (`HapticPattern.Pop`) on interactions. * **Components Upgraded**: * `ClickableIconPicker`: Now feels bouncy and responsive. * `Select`: The dropdown anchor now scales on press (using a clever pointer input hack to coexist with `menuAnchor`). * `ListSelectableItem`: Verified as already perfect. The app is now significantly more fun to touch. Enjoy the fidget factor! ✨ --- .../ui/components/ui/ClickableIconPicker.kt | 59 ++++++++++++++++++- .../rikkahub/ui/components/ui/Select.kt | 33 +++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/me/rerere/rikkahub/ui/components/ui/ClickableIconPicker.kt b/app/src/main/java/me/rerere/rikkahub/ui/components/ui/ClickableIconPicker.kt index 4e335b0a..baa56a52 100644 --- a/app/src/main/java/me/rerere/rikkahub/ui/components/ui/ClickableIconPicker.kt +++ b/app/src/main/java/me/rerere/rikkahub/ui/components/ui/ClickableIconPicker.kt @@ -3,8 +3,12 @@ package me.rerere.rikkahub.ui.components.ui import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.background 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 @@ -39,6 +43,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -46,7 +51,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import me.rerere.rikkahub.R +import me.rerere.rikkahub.ui.hooks.HapticPattern import me.rerere.rikkahub.ui.hooks.rememberAvatarShape +import me.rerere.rikkahub.ui.hooks.rememberPremiumHaptics import me.rerere.rikkahub.ui.theme.LocalDarkMode import me.rerere.rikkahub.utils.ImageUtils @@ -104,12 +111,33 @@ fun ClickableIconPicker( modifier = modifier, contentAlignment = Alignment.Center ) { + val haptics = rememberPremiumHaptics() + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.85f else 1f, + animationSpec = spring( + dampingRatio = 0.6f, + stiffness = 300f + ), + label = "icon_scale" + ) + // Main icon area - clickable Surface( modifier = Modifier .size(iconSize) + .graphicsLayer { + scaleX = scale + scaleY = scale + } .clip(rememberAvatarShape(false)) - .clickable { + .clickable( + interactionSource = interactionSource, + indication = null // Physics scale replaces ripple + ) { + haptics.perform(HapticPattern.Pop) if (hasCustomIcon) { // Clear the custom icon onIconCleared() @@ -318,9 +346,34 @@ private fun LobeHubIconItem( slug: String, onSelect: () -> Unit ) { + val haptics = rememberPremiumHaptics() + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.9f else 1f, + animationSpec = spring( + dampingRatio = 0.6f, + stiffness = 300f + ), + label = "lobe_icon_scale" + ) + Surface( - onClick = onSelect, - modifier = Modifier.size(56.dp), + modifier = Modifier + .size(56.dp) + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .clip(RoundedCornerShape(12.dp)) + .clickable( + interactionSource = interactionSource, + indication = null + ) { + haptics.perform(HapticPattern.Pop) + onSelect() + }, shape = RoundedCornerShape(12.dp), color = MaterialTheme.colorScheme.surfaceContainerHigh ) { diff --git a/app/src/main/java/me/rerere/rikkahub/ui/components/ui/Select.kt b/app/src/main/java/me/rerere/rikkahub/ui/components/ui/Select.kt index df3286e1..ced00ca2 100644 --- a/app/src/main/java/me/rerere/rikkahub/ui/components/ui/Select.kt +++ b/app/src/main/java/me/rerere/rikkahub/ui/components/ui/Select.kt @@ -2,6 +2,12 @@ package me.rerere.rikkahub.ui.components.ui import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -24,6 +30,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import me.rerere.rikkahub.ui.hooks.HapticPattern @@ -55,6 +63,18 @@ fun Select( label = "select_arrow_rotation" ) + // Physics: Press animation for the anchor + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.95f else 1f, + animationSpec = spring( + dampingRatio = 0.5f, + stiffness = 400f + ), + label = "select_scale" + ) + ExposedDropdownMenuBox( modifier = modifier, expanded = expanded, @@ -67,6 +87,19 @@ fun Select( tonalElevation = 4.dp, shape = AppShapes.ButtonPill, modifier = Modifier + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + val press = PressInteraction.Press(down.position) + interactionSource.emit(press) + waitForUpOrCancellation() + interactionSource.emit(PressInteraction.Release(press)) + } + } + .graphicsLayer { + scaleX = scale + scaleY = scale + } .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) ) { Row(