From fb07d8923e7fabfa0b6d185e5870db028258404e Mon Sep 17 00:00:00 2001 From: Voemp Date: Sun, 4 May 2025 23:38:19 +0800 Subject: [PATCH 1/3] library: Add 'Sink' and 'Tilt' press feedback effects to Card --- docs/components/card.md | 23 +++ docs/guide/utils.md | 76 ++++++++++ docs/zh_CN/components/card.md | 39 ++++- docs/zh_CN/guide/utils.md | 76 ++++++++++ .../kotlin/component/OtherComponent.kt | 84 ++++++++--- .../top/yukonga/miuix/kmp/basic/Card.kt | 126 +++++++++++++++- .../yukonga/miuix/kmp/utils/PressFeedback.kt | 142 ++++++++++++++++++ 7 files changed, 534 insertions(+), 32 deletions(-) create mode 100644 miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/PressFeedback.kt diff --git a/docs/components/card.md b/docs/components/card.md index 6eadc12e..806e083e 100644 --- a/docs/components/card.md +++ b/docs/components/card.md @@ -28,6 +28,10 @@ Card { | cornerRadius | Dp | Card corner radius | CardDefaults.CornerRadius | No | | insideMargin | PaddingValues | Card inner padding | CardDefaults.InsideMargin | No | | color | Color | Card background color | CardDefaults.DefaultColor() | No | +| pressFeedbackType | PressFeedbackType | The type of feedback when the card is pressed | PressFeedbackType.None | No | +| showIndication | Boolean? | Whether to show indication on interaction | false | No | +| onClick | (() -> Unit)? | Callback invoked when the card is clicked | null | No | +| onLongPress | (() -> Unit)? | Callback invoked when the card is long pressed | null | No | | content | @Composable ColumnScope.() -> Unit | Composable function for card content area | - | Yes | ### CardDefaults Object @@ -111,3 +115,22 @@ LazyColumn { } } ``` + +### Interactive Card + +```kotlin +Card( + modifier = Modifier.padding(16.dp), + pressFeedbackType = PressFeedbackType.Sink, + showIndication = true, + onClick = { /* Handle click event */ }, + onLongPress = { /* Handle long press event */ } +) { + Text("Interactive Card") +} +``` + +In this example: +- `pressFeedbackType = PressFeedbackType.Sink` adds a sink animation when pressing the card. +- `showIndication = true` enables visual indication during interactions. +- `onClick` and `onLongPress` define the respective callbacks. \ No newline at end of file diff --git a/docs/guide/utils.md b/docs/guide/utils.md index b738aa58..cd50534d 100644 --- a/docs/guide/utils.md +++ b/docs/guide/utils.md @@ -105,6 +105,82 @@ LazyColumn( * `springDamp`: Float, defines the spring damping for the rebound animation. Higher values result in less oscillation. Defaults to `1f`. * `isEnabled`: A lambda expression returning a Boolean, used to dynamically control whether the overscroll effect is enabled. By default, it is enabled only on Android and iOS platforms. +## Press Feedback Effects + +Miuix provides visual feedback effects to enhance user interaction experience, improving operability through tactile-like responses. + +### Sink Effect + +The `pressSink` modifier applies a "sink" visual feedback when the component is pressed, by smoothly scaling down the component. + +```kotlin +val interactionSource = remember { MutableInteractionSource() } + +Box( + modifier = Modifier + .clickable(interactionSource = interactionSource, indication = null) + .pressSink(interactionSource) + .background(Color.Blue) + .size(100.dp) +) +``` + +### Tilt Effect + +The `pressTilt` modifier applies a "tilt" effect based on the position where the user pressed the component. The tilt direction is determined by touch offset, giving the effect that one corner "sinks" while the other "static". + +```kotlin +val interactionSource = remember { MutableInteractionSource() } + +Box( + modifier = Modifier + .clickable(interactionSource = interactionSource, indication = null) + .pressTilt(interactionSource) + .background(Color.Green) + .size(100.dp) +) +``` + +### Prerequisites for Triggering Press Feedback + +Press feedback effects require detecting `interactionSource.collectIsPressedAsState()` to be triggered. + +You can use responsive modifiers like `Modifier.clickable()` to add `PressInteraction.Press` to the `interactionSource` and trigger press feedback effects. + +However, it's recommended to use the method below to add `PressInteraction.Press` to the `interactionSource` for faster response triggering of press feedback effects. + +```kotlin +val interactionSource = remember { MutableInteractionSource() } + +Box( + modifier = Modifier + .pointerInput(Unit) { + awaitEachGesture { + val pressInteraction: PressInteraction.Press + awaitFirstDown().also { + pressInteraction = PressInteraction.Press(it.position) + interactionSource.tryEmit(pressInteraction) + } + if (waitForUpOrCancellation() == null) { + interactionSource.tryEmit(PressInteraction.Cancel(pressInteraction)) + } else { + interactionSource.tryEmit(PressInteraction.Release(pressInteraction)) + } + } + } +) +``` + +### Press Feedback Type (`PressFeedbackType`) + +The `PressFeedbackType` enum defines different types of visual feedback that can be applied when the component is pressed. + +| Type | Description | +|------|-------------| +| None | No visual feedback | +| Sink | Applies a sink effect, where the component scales down slightly when pressed | +| Tilt | Applies a tilt effect, where the component tilts slightly based on the touch position | + ## Smooth Rounded Corners (SmoothRoundedCornerShape) `SmoothRoundedCornerShape` provides smoother rounded corners compared to the standard `RoundedCornerShape`. diff --git a/docs/zh_CN/components/card.md b/docs/zh_CN/components/card.md index 4ad966f8..15bde45a 100644 --- a/docs/zh_CN/components/card.md +++ b/docs/zh_CN/components/card.md @@ -22,13 +22,17 @@ Card { ### Card 属性 -| 属性名 | 类型 | 说明 | 默认值 | 是否必须 | -| ------------ | ---------------------------------- | ------------------------ | --------------------------- | -------- | -| modifier | Modifier | 应用于卡片的修饰符 | Modifier | 否 | -| cornerRadius | Dp | 卡片圆角半径 | CardDefaults.CornerRadius | 否 | -| insideMargin | PaddingValues | 卡片内部边距 | CardDefaults.InsideMargin | 否 | -| color | Color | 卡片背景颜色 | CardDefaults.DefaultColor() | 否 | -| content | @Composable ColumnScope.() -> Unit | 卡片内容区域的可组合函数 | - | 是 | +| 属性名 | 类型 | 说明 | 默认值 | 是否必须 | +| ---------------- | ---------------------------------- | ------------------------ | --------------------------- | -------- | +| modifier | Modifier | 应用于卡片的修饰符 | Modifier | 否 | +| cornerRadius | Dp | 卡片圆角半径 | CardDefaults.CornerRadius | 否 | +| insideMargin | PaddingValues | 卡片内部边距 | CardDefaults.InsideMargin | 否 | +| color | Color | 卡片背景颜色 | CardDefaults.DefaultColor() | 否 | +| pressFeedbackType| PressFeedbackType | 按压反馈类型 | PressFeedbackType.None | 否 | +| showIndication | Boolean? | 是否显示点击指示效果 | false | 否 | +| onClick | (() -> Unit)? | 点击事件回调 | null | 否 | +| onLongPress | (() -> Unit)? | 长按事件回调 | null | 否 | +| content | @Composable ColumnScope.() -> Unit | 卡片内容区域的可组合函数 | - | 是 | ### CardDefaults 对象 @@ -89,7 +93,7 @@ Card( TextButton( text = "确定", colors = ButtonDefaults.textButtonColorsPrimary(), // 使用主题颜色 - onClick = { /* 处理取消事件 */ } + onClick = { /* 处理确认事件 */ } ) } } @@ -111,3 +115,22 @@ LazyColumn { } } ``` + +### 可交互的卡片 + +```kotlin +Card( + modifier = Modifier.padding(16.dp), + pressFeedbackType = PressFeedbackType.Sink, + showIndication = true, + onClick = { /* 处理点击事件 */ }, + onLongPress = { /* 处理长按事件 */ } +) { + Text("可交互的卡片") +} +``` + +在这个示例中: +- `pressFeedbackType = PressFeedbackType.Sink` 表示按下时会有一个下沉动画。 +- `showIndication = true` 表示启用交互时的视觉反馈。 +- `onClick` 和 `onLongPress` 分别定义了点击和长按的回调。 \ No newline at end of file diff --git a/docs/zh_CN/guide/utils.md b/docs/zh_CN/guide/utils.md index 1bf77ac9..7cee5e8d 100644 --- a/docs/zh_CN/guide/utils.md +++ b/docs/zh_CN/guide/utils.md @@ -106,6 +106,82 @@ LazyColumn( * `springDamp`: 浮点数,定义回弹动画的弹簧阻尼。值越高,振荡越小。默认为 `1f`。 * `isEnabled`: 一个返回布尔值的 Lambda 表达式,用于动态控制是否启用越界回弹效果。默认情况下,仅在 Android 和 iOS 平台上启用。 +## 按压反馈效果 (PressFeedback) + +Miuix 提供了视觉反馈效果来增强用户交互体验,通过类似触觉的响应提升操作感。 + +### 下沉效果 + +`pressSink` 修饰符会在组件被按下时应用一种“下沉”视觉效果,通过平滑缩放组件实现。 + +```kotlin +val interactionSource = remember { MutableInteractionSource() } + +Box( + modifier = Modifier + .clickable(interactionSource = interactionSource, indication = null) + .pressSink(interactionSource) + .background(Color.Blue) + .size(100.dp) +) +``` + +### 倾斜效果 + +`pressTilt` 修饰符会根据用户按压组件的位置应用一种“倾斜”效果。倾斜方向由触摸偏移决定,使一角“下沉”而另一角保持“静止”。 + +```kotlin +val interactionSource = remember { MutableInteractionSource() } + +Box( + modifier = Modifier + .clickable(interactionSource = interactionSource, indication = null) + .pressTilt(interactionSource) + .background(Color.Green) + .size(100.dp) +) +``` + +### 触发按压反馈效果的前提 + +按压反馈效果需要检测 `interactionSource.collectIsPressedAsState()` 以触发。 + +可以使用 `Modifier.clickable()` 等响应式修饰符来为 `interactionSource` 添加 `PressInteraction.Press` 以触发按压反馈效果。 + +但更推荐使用下面的方法来为 `interactionSource` 添加 `PressInteraction.Press` 以获得更快响应的触发按压反馈效果。 + +```kotlin +val interactionSource = remember { MutableInteractionSource() } + +Box( + modifier = Modifier + .pointerInput(Unit) { + awaitEachGesture { + val pressInteraction: PressInteraction.Press + awaitFirstDown().also { + pressInteraction = PressInteraction.Press(it.position) + interactionSource.tryEmit(pressInteraction) + } + if (waitForUpOrCancellation() == null) { + interactionSource.tryEmit(PressInteraction.Cancel(pressInteraction)) + } else { + interactionSource.tryEmit(PressInteraction.Release(pressInteraction)) + } + } + } +) +``` + +### 按压反馈类型 (PressFeedbackType) + +`PressFeedbackType` 枚举定义了组件被按下时可以应用的不同类型的视觉反馈。 + +| 类型 | 说明 | +|------|-------------| +| None | 无视觉反馈 | +| Sink | 应用下沉效果,组件在按下时轻微缩小 | +| Tilt | 应用倾斜效果,组件根据触摸位置轻微倾斜 | + ## 平滑圆角 (SmoothRoundedCornerShape) `SmoothRoundedCornerShape` 提供了比标准 `RoundedCornerShape` 更加平滑的圆角效果。 diff --git a/example/src/commonMain/kotlin/component/OtherComponent.kt b/example/src/commonMain/kotlin/component/OtherComponent.kt index edf545ee..238eb8b5 100644 --- a/example/src/commonMain/kotlin/component/OtherComponent.kt +++ b/example/src/commonMain/kotlin/component/OtherComponent.kt @@ -28,7 +28,9 @@ 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.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue @@ -91,6 +93,7 @@ import top.yukonga.miuix.kmp.icon.icons.useful.Unlike import top.yukonga.miuix.kmp.icon.icons.useful.Unstick import top.yukonga.miuix.kmp.icon.icons.useful.Update import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape import kotlin.math.round @@ -100,6 +103,7 @@ fun OtherComponent(padding: PaddingValues) { var submitButtonText by remember { mutableStateOf("Submit") } var clickCount by remember { mutableStateOf(0) } var submitClickCount by remember { mutableStateOf(0) } + val hapticFeedback = LocalHapticFeedback.current val focusManager = LocalFocusManager.current var text1 by remember { mutableStateOf("") } var text2 by remember { mutableStateOf(TextFieldValue("")) } @@ -378,15 +382,15 @@ fun OtherComponent(padding: PaddingValues) { ) { Text( text = "Selected Color:\nRGBA: " + - "${(selectedColor.red * 255).toInt()}," + - "${(selectedColor.green * 255).toInt()}," + - "${(selectedColor.blue * 255).toInt()}," + - "${(round(selectedColor.alpha * 100) / 100.0)}" + - "\nHEX: #" + - (selectedColor.alpha * 255).toInt().toString(16).padStart(2, '0').uppercase() + - (selectedColor.red * 255).toInt().toString(16).padStart(2, '0').uppercase() + - (selectedColor.green * 255).toInt().toString(16).padStart(2, '0').uppercase() + - (selectedColor.blue * 255).toInt().toString(16).padStart(2, '0').uppercase(), + "${(selectedColor.red * 255).toInt()}," + + "${(selectedColor.green * 255).toInt()}," + + "${(selectedColor.blue * 255).toInt()}," + + "${(round(selectedColor.alpha * 100) / 100.0)}" + + "\nHEX: #" + + (selectedColor.alpha * 255).toInt().toString(16).padStart(2, '0').uppercase() + + (selectedColor.red * 255).toInt().toString(16).padStart(2, '0').uppercase() + + (selectedColor.green * 255).toInt().toString(16).padStart(2, '0').uppercase() + + (selectedColor.blue * 255).toInt().toString(16).padStart(2, '0').uppercase(), modifier = Modifier.weight(1f) ) Spacer(Modifier.width(12.dp)) @@ -415,7 +419,10 @@ fun OtherComponent(padding: PaddingValues) { .padding(horizontal = 12.dp) .padding(bottom = 12.dp), color = MiuixTheme.colorScheme.primaryVariant, - insideMargin = PaddingValues(16.dp) + insideMargin = PaddingValues(16.dp), + pressFeedbackType = PressFeedbackType.None, + showIndication = true, + onClick = { } ) { Text( color = MiuixTheme.colorScheme.onPrimary, @@ -423,19 +430,62 @@ fun OtherComponent(padding: PaddingValues) { fontSize = 19.sp, fontWeight = FontWeight.SemiBold ) + Text( + color = MiuixTheme.colorScheme.onPrimaryVariant, + text = "ShowIndication: true", + fontSize = 17.sp, + fontWeight = FontWeight.Normal + ) } - Card( + Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) - .padding(bottom = 12.dp + padding.calculateBottomPadding()), - insideMargin = PaddingValues(16.dp) + .padding(bottom = 12.dp + padding.calculateBottomPadding() + 80.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - Text( - color = MiuixTheme.colorScheme.onSurface, - text = "Card\nCardCard\nCardCardCard", - style = MiuixTheme.textStyles.paragraph + Card( + modifier = Modifier + .weight(1f), + insideMargin = PaddingValues(16.dp), + pressFeedbackType = PressFeedbackType.Sink, + showIndication = true, + onClick = { }, + content = { + Text( + color = MiuixTheme.colorScheme.onSurface, + text = "Card", + fontSize = 18.sp, + fontWeight = FontWeight.Medium + ) + Text( + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + text = "PressFeedback\nType: Sink", + style = MiuixTheme.textStyles.paragraph + ) + } + ) + + Card( + modifier = Modifier + .weight(1f), + insideMargin = PaddingValues(16.dp), + pressFeedbackType = PressFeedbackType.Tilt, + onLongPress = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) }, + content = { + Text( + color = MiuixTheme.colorScheme.onSurface, + text = "Card", + fontSize = 18.sp, + fontWeight = FontWeight.Medium + ) + Text( + color = MiuixTheme.colorScheme.onSurfaceVariantSummary, + text = "PressFeedback\nType: Tilt", + style = MiuixTheme.textStyles.paragraph + ) + } ) } } \ No newline at end of file diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt index 98a6a270..04925a74 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Card.kt @@ -1,10 +1,20 @@ package top.yukonga.miuix.kmp.basic +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -13,12 +23,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.utils.PressFeedbackType import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape +import top.yukonga.miuix.kmp.utils.pressSink +import top.yukonga.miuix.kmp.utils.pressTilt /** * A [Card] component with Miuix style. @@ -33,12 +47,114 @@ import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape * @param content The [Composable] content of the [Card]. */ @Composable +fun Card( + modifier: Modifier = Modifier, + color: Color = CardDefaults.DefaultColor(), + cornerRadius: Dp = CardDefaults.CornerRadius, + insideMargin: PaddingValues = CardDefaults.InsideMargin, + content: @Composable ColumnScope.() -> Unit +) { + BasicCard( + modifier = modifier, + cornerRadius = cornerRadius, + color = color + ) { + Column( + modifier = Modifier.padding(insideMargin), + content = content + ) + } +} + +/** + * A [Card] component with Miuix style. + * Card contain contain content and actions that relate information about a subject. + * + * This [Card] handles input events + * + * @param modifier The modifier to be applied to the [Card]. + * @param cornerRadius The corner radius of the [Card]. + * @param insideMargin The margin inside the [Card]. + * @param color The color of the [Card]. + * @param pressFeedbackType The press feedback type of the [Card]. + * @param showIndication Whether to show indication of the [Card]. + * @param onClick The callback to be invoked when the [Card] is clicked. + * @param onLongPress The callback to be invoked when the [Card] is long pressed. + * @param content The [Composable] content of the [Card]. + */ +@Composable fun Card( modifier: Modifier = Modifier, cornerRadius: Dp = CardDefaults.CornerRadius, insideMargin: PaddingValues = CardDefaults.InsideMargin, color: Color = CardDefaults.DefaultColor(), + pressFeedbackType: PressFeedbackType = PressFeedbackType.None, + showIndication: Boolean? = false, + onClick: (() -> Unit)? = null, + onLongPress: (() -> Unit)? = null, content: @Composable ColumnScope.() -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + + val pressModifier = when (pressFeedbackType) { + PressFeedbackType.None -> Modifier + PressFeedbackType.Sink -> Modifier.pressSink(interactionSource) + PressFeedbackType.Tilt -> Modifier.pressTilt(interactionSource) + } + + BasicCard( + modifier = modifier + .pointerInput(Unit) { + detectTapGestures( + onTap = { onClick?.invoke() }, + onLongPress = { onLongPress?.invoke() } + ) + } + .pointerInput(Unit) { + awaitEachGesture { + val pressInteraction: PressInteraction.Press + awaitFirstDown().also { + pressInteraction = PressInteraction.Press(it.position) + interactionSource.tryEmit(pressInteraction) + } + if (waitForUpOrCancellation() == null) { + interactionSource.tryEmit(PressInteraction.Cancel(pressInteraction)) + } else { + interactionSource.tryEmit(PressInteraction.Release(pressInteraction)) + } + } + } + .then(pressModifier), + cornerRadius = cornerRadius, + color = color + ) { + Column( + modifier = Modifier + .indication( + interactionSource = interactionSource, + indication = if (showIndication == true) LocalIndication.current else null + ) + .fillMaxSize() + .padding(insideMargin), + content = content + ) + } +} + +/** + * A [BasicCard] component. + * + * @param modifier The modifier to be applied to the [BasicCard]. + * @param color The color of the [BasicCard]. + * @param cornerRadius The corner radius of the [BasicCard]. + * @param content The [Composable] content of the [BasicCard]. + */ +@Composable +private fun BasicCard( + modifier: Modifier = Modifier, + color: Color = CardDefaults.DefaultColor(), + cornerRadius: Dp = CardDefaults.CornerRadius, + content: @Composable BoxScope.() -> Unit ) { val shape = remember { derivedStateOf { SmoothRoundedCornerShape(cornerRadius) } } Box( @@ -48,13 +164,9 @@ fun Card( } .background(color = color, shape = shape.value) .clip(RoundedCornerShape(cornerRadius)), // For touch feedback, there is a problem when using SmoothRoundedCornerShape. - propagateMinConstraints = true - ) { - Column( - modifier = Modifier.padding(insideMargin), - content = content - ) - } + propagateMinConstraints = true, + content = content + ) } object CardDefaults { diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/PressFeedback.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/PressFeedback.kt new file mode 100644 index 00000000..f1cf06ca --- /dev/null +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/PressFeedback.kt @@ -0,0 +1,142 @@ +package top.yukonga.miuix.kmp.utils + +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.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import top.yukonga.miuix.kmp.utils.PressFeedbackType.None +import top.yukonga.miuix.kmp.utils.PressFeedbackType.Sink +import top.yukonga.miuix.kmp.utils.PressFeedbackType.Tilt + +/** + * Default sink amount for the press sink effect. + */ +internal const val SinkAmount: Float = 0.94f + +/** + * Default tilt amount for the press tilt effect. + */ +internal const val TiltAmount: Float = 8f + +/** + * Default damping ratio for the press feedback spring animations. + */ +internal const val DampingRatio: Float = 0.6f + +/** + * Default stiffness for the press feedback spring animations. + */ +internal const val Stiffness: Float = 400f + +/** + * Applies a "sink" visual feedback when the component is pressed, + * by scaling the component down smoothly using animation. + * + * @param interactionSource An external [MutableInteractionSource] to observe press state. + * @param sinkAmount The target scale when pressed (less than 1f). + * @param dampingRatio The damping ratio for the spring animation. + * @param stiffness The stiffness of the spring animation. + */ +fun Modifier.pressSink( + interactionSource: MutableInteractionSource, + sinkAmount: Float = SinkAmount, + dampingRatio: Float = DampingRatio, + stiffness: Float = Stiffness +): Modifier = composed { + val isPressed by interactionSource.collectIsPressedAsState() + + val animationSpec = spring(dampingRatio = dampingRatio, stiffness = stiffness) + + val scale by animateFloatAsState( + targetValue = if (isPressed) sinkAmount else 1f, + animationSpec = animationSpec, + label = "pressSinkScale" + ) + + this.scale(scale) +} + +/** + * Applies a "tilt" effect based on the position where the user pressed the component. + * The tilt direction is determined by touch offset, + * giving the effect that one corner "sinks" while the other "static". + * + * @param interactionSource An external [MutableInteractionSource] to observe press state. + * @param tiltAmount Maximum rotation (in degrees) to apply along X and Y axes. + * @param dampingRatio The damping ratio for the tilt spring animation. + * @param stiffness The stiffness of the tilt spring animation. + */ +fun Modifier.pressTilt( + interactionSource: MutableInteractionSource, + tiltAmount: Float = TiltAmount, + dampingRatio: Float = DampingRatio, + stiffness: Float = Stiffness +): Modifier = composed { + val isPressed by interactionSource.collectIsPressedAsState() + + val animationSpec = spring(dampingRatio = dampingRatio, stiffness = stiffness) + + var targetX by remember { mutableStateOf(0f) } + var targetY by remember { mutableStateOf(0f) } + + val tiltX by animateFloatAsState( + targetValue = if (isPressed) targetX else 0f, + animationSpec = animationSpec, + label = "pressTiltX" + ) + val tiltY by animateFloatAsState( + targetValue = if (isPressed) targetY else 0f, + animationSpec = animationSpec, + label = "pressTiltY" + ) + var transformOrigin by remember { mutableStateOf(TransformOrigin.Center) } + + this + .graphicsLayer { + rotationX = tiltX + rotationY = tiltY + cameraDistance = 12 * density + this.transformOrigin = transformOrigin + } + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown() + val w = size.width + val h = size.height + val offset = down.position + + transformOrigin = TransformOrigin( + pivotFractionX = if (offset.x < w / 2) 1f else 0f, + pivotFractionY = if (offset.y < h / 2) 1f else 0f + ) + + targetX = if (offset.y < h / 2) tiltAmount else -tiltAmount + targetY = if (offset.x < w / 2) -tiltAmount else tiltAmount + } + } +} + +/** + * The type of visual feedback to apply when the component is pressed. + * + * @property None No visual feedback. + * @property Sink Sink effect, where the component scales down slightly when pressed. + * @property Tilt Tilt effect, where the component tilts slightly based on the touch position. + */ +enum class PressFeedbackType { + None, + Sink, + Tilt +} From fa3bcd510d15e157c875c59e64ae3c9aa388ff5e Mon Sep 17 00:00:00 2001 From: YuKongA <70465933+YuKongA@users.noreply.github.com> Date: Mon, 5 May 2025 23:17:07 +0800 Subject: [PATCH 2/3] 1 --- docs/components/card.md | 58 +++++++++++++++++------------------ docs/zh_CN/components/card.md | 43 +++++++++++++------------- 2 files changed, 51 insertions(+), 50 deletions(-) diff --git a/docs/components/card.md b/docs/components/card.md index 806e083e..61a073cc 100644 --- a/docs/components/card.md +++ b/docs/components/card.md @@ -1,16 +1,17 @@ # Card -`Card` is a basic container component in Miuix, used to hold related content and actions. It provides a card container with Miuix style, suitable for scenarios such as information display and content grouping. +`Card` is a basic container component in Miuix, used to hold related content and actions. It provides a card container with Miuix style, suitable for scenarios such as information display and content grouping. Supports both static display and interactive modes. ## Import ```kotlin import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.utils.PressFeedbackType // If using interactive card ``` ## Basic Usage -The Card component can be used to wrap and organize content: +The Card component can be used to wrap and organize content (static card): ```kotlin Card { @@ -22,17 +23,21 @@ Card { ### Card Properties -| Property Name | Type | Description | Default Value | Required | -| ------------- | ---------------------------------- | ------------------------ | --------------------------- | -------- | -| modifier | Modifier | Modifier applied to card | Modifier | No | -| cornerRadius | Dp | Card corner radius | CardDefaults.CornerRadius | No | -| insideMargin | PaddingValues | Card inner padding | CardDefaults.InsideMargin | No | -| color | Color | Card background color | CardDefaults.DefaultColor() | No | -| pressFeedbackType | PressFeedbackType | The type of feedback when the card is pressed | PressFeedbackType.None | No | -| showIndication | Boolean? | Whether to show indication on interaction | false | No | -| onClick | (() -> Unit)? | Callback invoked when the card is clicked | null | No | -| onLongPress | (() -> Unit)? | Callback invoked when the card is long pressed | null | No | -| content | @Composable ColumnScope.() -> Unit | Composable function for card content area | - | Yes | +| Property Name | Type | Description | Default Value | Required | Applies To | +| ----------------- | ---------------------------------- | ----------------------------------------- | --------------------------- | -------- | ----------- | +| modifier | Modifier | Modifier applied to the card | Modifier | No | All | +| cornerRadius | Dp | Card corner radius | CardDefaults.CornerRadius | No | All | +| insideMargin | PaddingValues | Card inner padding | CardDefaults.InsideMargin | No | All | +| color | Color | Card background color | CardDefaults.DefaultColor() | No | All | +| pressFeedbackType | PressFeedbackType | Feedback type when pressed | PressFeedbackType.None | No | Interactive | +| showIndication | Boolean? | Show indication on interaction | false | No | Interactive | +| onClick | (() -> Unit)? | Callback when clicked | null | No | Interactive | +| onLongPress | (() -> Unit)? | Callback when long pressed | null | No | Interactive | +| content | @Composable ColumnScope.() -> Unit | Composable function for card content area | - | Yes | All | + +::: warning +Some properties are only available when creating an interactive card! +::: ### CardDefaults Object @@ -40,16 +45,16 @@ The CardDefaults object provides default values and color configurations for the #### Constants -| Constant Name | Type | Description | Default Value | -| ------------- | ------------- | ------------------ | --------------------- | -| CornerRadius | Dp | Card corner radius | 16.dp | -| InsideMargin | PaddingValues | Card inner padding | PaddingValues(0.dp) | +| Constant Name | Type | Description | Default Value | +| ------------- | ------------- | ------------------ | ------------------- | +| CornerRadius | Dp | Card corner radius | 16.dp | +| InsideMargin | PaddingValues | Card inner padding | PaddingValues(0.dp) | #### Methods -| Method Name | Type | Description | -| -------------- | ----- | ------------------------- | -| DefaultColor() | Color | Creates the default color for the card | +| Method Name | Type | Description | +| -------------- | ----- | ----------------------------------------- | +| DefaultColor() | Color | The default background color for the card | ## Advanced Usage @@ -121,16 +126,11 @@ LazyColumn { ```kotlin Card( modifier = Modifier.padding(16.dp), - pressFeedbackType = PressFeedbackType.Sink, - showIndication = true, - onClick = { /* Handle click event */ }, - onLongPress = { /* Handle long press event */ } + pressFeedbackType = PressFeedbackType.Sink, // Set press feedback to sink effect + showIndication = true, // Show indication on click + onClick = {/* Handle click event */ }, + onLongPress = {/* Handle long press event */ } ) { Text("Interactive Card") } ``` - -In this example: -- `pressFeedbackType = PressFeedbackType.Sink` adds a sink animation when pressing the card. -- `showIndication = true` enables visual indication during interactions. -- `onClick` and `onLongPress` define the respective callbacks. \ No newline at end of file diff --git a/docs/zh_CN/components/card.md b/docs/zh_CN/components/card.md index 15bde45a..6f2e8657 100644 --- a/docs/zh_CN/components/card.md +++ b/docs/zh_CN/components/card.md @@ -1,16 +1,17 @@ # Card -`Card` 是 Miuix 中的基础容器组件,用于承载相关内容和操作。它提供了具有 Miuix 风格的卡片容器,适用于信息展示、内容分组等场景。 +`Card` 是 Miuix 中的基础容器组件,用于承载相关内容和操作。它提供了具有 Miuix 风格的卡片容器,适用于信息展示、内容分组等场景。支持静态显示和交互式两种模式。 ## 引入 ```kotlin import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.utils.PressFeedbackType // 如果使用交互式卡片 ``` ## 基本用法 -Card 组件可以用于包装和组织内容: +Card 组件可以用于包装和组织内容(静态卡片): ```kotlin Card { @@ -22,17 +23,22 @@ Card { ### Card 属性 -| 属性名 | 类型 | 说明 | 默认值 | 是否必须 | -| ---------------- | ---------------------------------- | ------------------------ | --------------------------- | -------- | -| modifier | Modifier | 应用于卡片的修饰符 | Modifier | 否 | -| cornerRadius | Dp | 卡片圆角半径 | CardDefaults.CornerRadius | 否 | -| insideMargin | PaddingValues | 卡片内部边距 | CardDefaults.InsideMargin | 否 | -| color | Color | 卡片背景颜色 | CardDefaults.DefaultColor() | 否 | -| pressFeedbackType| PressFeedbackType | 按压反馈类型 | PressFeedbackType.None | 否 | -| showIndication | Boolean? | 是否显示点击指示效果 | false | 否 | -| onClick | (() -> Unit)? | 点击事件回调 | null | 否 | -| onLongPress | (() -> Unit)? | 长按事件回调 | null | 否 | -| content | @Composable ColumnScope.() -> Unit | 卡片内容区域的可组合函数 | - | 是 | + +| 属性名 | 类型 | 说明 | 默认值 | 是否必须 | 适用范围 | +| ----------------- | ---------------------------------- | ------------------------ | --------------------------- | -------- | -------- | +| modifier | Modifier | 应用于卡片的修饰符 | Modifier | 否 | 所有 | +| cornerRadius | Dp | 卡片圆角半径 | CardDefaults.CornerRadius | 否 | 所有 | +| insideMargin | PaddingValues | 卡片内部边距 | CardDefaults.InsideMargin | 否 | 所有 | +| color | Color | 卡片背景颜色 | CardDefaults.DefaultColor() | 否 | 所有 | +| pressFeedbackType | PressFeedbackType | 按压反馈类型 | PressFeedbackType.None | 否 | 交互式 | +| showIndication | Boolean? | 显示点击指示效果 | false | 否 | 交互式 | +| onClick | (() -> Unit)? | 点击事件回调 | null | 否 | 交互式 | +| onLongPress | (() -> Unit)? | 长按事件回调 | null | 否 | 交互式 | +| content | @Composable ColumnScope.() -> Unit | 卡片内容区域的可组合函数 | - | 是 | 所有 | + +::: warning 注意 +部分属性仅在创建可交互的卡片时可用! +::: ### CardDefaults 对象 @@ -49,7 +55,7 @@ CardDefaults 对象提供了卡片组件的默认值和颜色配置。 | 方法名 | 类型 | 说明 | | -------------- | ----- | ------------------ | -| DefaultColor() | Color | 创建卡片的默认颜色 | +| DefaultColor() | Color | 卡片的默认背景颜色 | ## 进阶用法 @@ -121,16 +127,11 @@ LazyColumn { ```kotlin Card( modifier = Modifier.padding(16.dp), - pressFeedbackType = PressFeedbackType.Sink, - showIndication = true, + pressFeedbackType = PressFeedbackType.Sink, // 设置按压反馈为下沉动画效果 + showIndication = true, // 显示点击时的视觉反馈效果 onClick = { /* 处理点击事件 */ }, onLongPress = { /* 处理长按事件 */ } ) { Text("可交互的卡片") } ``` - -在这个示例中: -- `pressFeedbackType = PressFeedbackType.Sink` 表示按下时会有一个下沉动画。 -- `showIndication = true` 表示启用交互时的视觉反馈。 -- `onClick` 和 `onLongPress` 分别定义了点击和长按的回调。 \ No newline at end of file From 2b3011b96cf8df3173e209d57ddc726deee2fcdf Mon Sep 17 00:00:00 2001 From: YuKongA <70465933+YuKongA@users.noreply.github.com> Date: Mon, 5 May 2025 23:21:08 +0800 Subject: [PATCH 3/3] 2 --- example/src/commonMain/kotlin/UITest.kt | 2 +- .../kotlin/component/OtherComponent.kt | 26 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/example/src/commonMain/kotlin/UITest.kt b/example/src/commonMain/kotlin/UITest.kt index ffd56f5a..752c59d3 100644 --- a/example/src/commonMain/kotlin/UITest.kt +++ b/example/src/commonMain/kotlin/UITest.kt @@ -89,7 +89,7 @@ data class UIState( val showFloatingToolbar: Boolean = false, val floatingToolbarPosition: Int = 1, val floatingToolbarOrientation: Int = 1, - val showFloatingActionButton: Boolean = true, + val showFloatingActionButton: Boolean = false, val floatingActionButtonPosition: Int = 2, val enablePageUserScroll: Boolean = false, val isTopPopupExpanded: Boolean = false diff --git a/example/src/commonMain/kotlin/component/OtherComponent.kt b/example/src/commonMain/kotlin/component/OtherComponent.kt index 238eb8b5..f583ee1b 100644 --- a/example/src/commonMain/kotlin/component/OtherComponent.kt +++ b/example/src/commonMain/kotlin/component/OtherComponent.kt @@ -382,15 +382,15 @@ fun OtherComponent(padding: PaddingValues) { ) { Text( text = "Selected Color:\nRGBA: " + - "${(selectedColor.red * 255).toInt()}," + - "${(selectedColor.green * 255).toInt()}," + - "${(selectedColor.blue * 255).toInt()}," + - "${(round(selectedColor.alpha * 100) / 100.0)}" + - "\nHEX: #" + - (selectedColor.alpha * 255).toInt().toString(16).padStart(2, '0').uppercase() + - (selectedColor.red * 255).toInt().toString(16).padStart(2, '0').uppercase() + - (selectedColor.green * 255).toInt().toString(16).padStart(2, '0').uppercase() + - (selectedColor.blue * 255).toInt().toString(16).padStart(2, '0').uppercase(), + "${(selectedColor.red * 255).toInt()}," + + "${(selectedColor.green * 255).toInt()}," + + "${(selectedColor.blue * 255).toInt()}," + + "${(round(selectedColor.alpha * 100) / 100.0)}" + + "\nHEX: #" + + (selectedColor.alpha * 255).toInt().toString(16).padStart(2, '0').uppercase() + + (selectedColor.red * 255).toInt().toString(16).padStart(2, '0').uppercase() + + (selectedColor.green * 255).toInt().toString(16).padStart(2, '0').uppercase() + + (selectedColor.blue * 255).toInt().toString(16).padStart(2, '0').uppercase(), modifier = Modifier.weight(1f) ) Spacer(Modifier.width(12.dp)) @@ -442,12 +442,11 @@ fun OtherComponent(padding: PaddingValues) { modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) - .padding(bottom = 12.dp + padding.calculateBottomPadding() + 80.dp), + .padding(bottom = 12.dp + padding.calculateBottomPadding()), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Card( - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), insideMargin = PaddingValues(16.dp), pressFeedbackType = PressFeedbackType.Sink, showIndication = true, @@ -468,8 +467,7 @@ fun OtherComponent(padding: PaddingValues) { ) Card( - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), insideMargin = PaddingValues(16.dp), pressFeedbackType = PressFeedbackType.Tilt, onLongPress = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) },