feat: add Switch

This commit is contained in:
2023-11-10 01:26:52 +08:00
parent d1c1a8eafe
commit 4a6137e836

View File

@@ -23,4 +23,202 @@
package com.highcapable.flexiui.component
// TODO: To be implemented
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