From 4a6137e836aeba88a303039400979b5a0f1b008a Mon Sep 17 00:00:00 2001 From: fankesyooni Date: Fri, 10 Nov 2023 01:26:52 +0800 Subject: [PATCH] feat: add Switch --- .../highcapable/flexiui/component/Switch.kt | 200 +++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Switch.kt b/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Switch.kt index 2cfbfd9..dd0e675 100644 --- a/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Switch.kt +++ b/flexiui-core/src/commonMain/kotlin/com/highcapable/flexiui/component/Switch.kt @@ -23,4 +23,202 @@ package com.highcapable.flexiui.component -// TODO: To be implemented \ No newline at end of file +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.highcapable.flexiui.LocalColors +import com.highcapable.flexiui.LocalShapes +import com.highcapable.flexiui.LocalSizes +import com.highcapable.flexiui.interaction.clickable +import com.highcapable.flexiui.utils.borderOrNot +import kotlin.math.roundToInt + +@Immutable +data class SwitchColors( + val thumbColor: Color, + val trackInactive: Color, + val trackActive: Color +) + +data class SwitchStyle( + val thumbDiameter: Dp, + val thumbGain: Float, + val thumbShadowSize: Dp, + val thumbShape: Shape, + val trackShape: Shape, + val thumbBorder: BorderStroke, + val trackBorder: BorderStroke, + val trackWidth: Dp, + val trackHeight: Dp +) + +@Composable +fun Switch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + padding: Dp = Switch.padding, + colors: SwitchColors = Switch.colors, + style: SwitchStyle = Switch.style, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val maxOffset = with(LocalDensity.current) { (style.trackWidth - style.thumbDiameter - padding * 2).toPx() } + val halfWidth = maxOffset / 2 + val hovered by interactionSource.collectIsHoveredAsState() + var dragging by remember { mutableStateOf(false) } + var offsetX by remember { mutableStateOf(0f) } + var distance by remember { mutableStateOf(0f) } + if (!hovered && !dragging) offsetX = if (checked) maxOffset else 0f + val animatedOffsetX by animateFloatAsState(offsetX) + val animatedScale by animateFloatAsState(if (hovered || dragging) style.thumbGain else 1f) + var trackColor by remember { mutableStateOf(colors.trackInactive) } + fun updateTrackColor() { + trackColor = lerp(colors.trackInactive, colors.trackActive, offsetX / maxOffset) + } + updateTrackColor() + val animatedTrackColor by animateColorAsState(trackColor) + val efficientDragging = dragging && distance > 5 + + @Composable + fun Track(content: @Composable RowScope.() -> Unit) { + val sModifier = if (enabled) + modifier.clickable( + interactionSource = interactionSource, + enabled = enabled, + role = Role.Switch + ) { + distance = maxOffset + offsetX = if (checked) 0f else maxOffset + onCheckedChange(!checked) + } + else modifier.alpha(0.5f) + Row( + modifier = sModifier + .background(if (efficientDragging) trackColor else animatedTrackColor, style.trackShape) + .borderOrNot(style.trackBorder, style.trackShape) + .size(style.trackWidth, style.trackHeight) + .padding(start = padding, end = padding), + verticalAlignment = Alignment.CenterVertically, + content = content + ) + } + + @Composable + fun Thumb() { + Box( + modifier = Modifier.size(style.thumbDiameter) + .offset { IntOffset((if (efficientDragging) offsetX else animatedOffsetX).roundToInt(), 0) } + .scale(animatedScale) + .shadow(style.thumbShadowSize, style.thumbShape) + .background(colors.thumbColor, style.thumbShape) + .borderOrNot(style.thumbBorder, style.thumbShape) + .draggable( + enabled = enabled, + orientation = Orientation.Horizontal, + interactionSource = interactionSource, + state = rememberDraggableState { delta -> + offsetX = (offsetX + delta).coerceIn(0f, maxOffset) + updateTrackColor() + }, + onDragStarted = { dragging = true }, + onDragStopped = { + dragging = false + if (offsetX >= halfWidth) { + distance = maxOffset - offsetX + offsetX = maxOffset + onCheckedChange(true) + } else { + distance = offsetX + offsetX = 0f + onCheckedChange(false) + } + } + ) + ) + } + Track { Thumb() } +} + +object Switch { + val padding: Dp + @Composable + @ReadOnlyComposable + get() = DefaultSwitchPadding + val colors: SwitchColors + @Composable + @ReadOnlyComposable + get() = defaultSwitchColors() + val style: SwitchStyle + @Composable + @ReadOnlyComposable + get() = defaultSwitchStyle() +} + +@Composable +@ReadOnlyComposable +private fun defaultSwitchColors() = SwitchColors( + thumbColor = Color.White, + trackInactive = LocalColors.current.themeTertiary, + trackActive = LocalColors.current.themePrimary +) + +@Composable +@ReadOnlyComposable +private fun defaultSwitchStyle() = SwitchStyle( + thumbDiameter = DefaultThumbDiameter, + thumbGain = DefaultThumbGain, + thumbShadowSize = DefaultThumbShadowSize, + thumbShape = CircleShape, + trackShape = LocalShapes.current.tertiary, + thumbBorder = defaultSwitchBorder(), + trackBorder = defaultSwitchBorder(), + trackWidth = DefaultTrackWidth, + trackHeight = DefaultTrackHeight +) + +@Composable +@ReadOnlyComposable +private fun defaultSwitchBorder() = BorderStroke(LocalSizes.current.borderSizeTertiary, LocalColors.current.textPrimary) + +private val DefaultSwitchPadding = 4.dp + +private val DefaultThumbDiameter = 12.dp +private const val DefaultThumbGain = 1.2f +private val DefaultThumbShadowSize = 0.5.dp + +private val DefaultTrackWidth = 40.dp +private val DefaultTrackHeight = 20.dp \ No newline at end of file