/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.compose.material import androidx.compose.animation.core.TweenSpec import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource 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.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.collect import kotlin.math.roundToInt /** * Material Design switch. * * Switches toggle the state of a single item on or off. * * ![Switches image](https://developer.android.com/images/reference/androidx/compose/material/switches.png) * * @sample androidx.compose.material.samples.SwitchSample * * @param checked whether or not this component is checked * @param onCheckedChange callback to be invoked when Switch is being clicked, * therefore the change of checked state is requested. If null, then this is passive * and relies entirely on a higher-level component to control the "checked" state. * @param modifier Modifier to be applied to the switch layout * @param enabled whether the component is enabled or grayed out * @param interactionSource the [MutableInteractionSource] representing the stream of * [Interaction]s for this Switch. You can create and pass in your own remembered * [MutableInteractionSource] if you want to observe [Interaction]s and customize the * appearance / behavior of this Switch in different [Interaction]s. * @param colors [SwitchColors] that will be used to determine the color of the thumb and track * in different states. See [SwitchDefaults.colors]. */ @Composable @OptIn(ExperimentalMaterialApi::class) fun Switch( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, colors: SwitchColors = SwitchDefaults.colors() ) { val minBound = 0f val maxBound = with(LocalDensity.current) { ThumbPathLength.toPx() } val swipeableState = rememberSwipeableStateFor(checked, onCheckedChange ?: {}, AnimationSpec) val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val toggleableModifier = if (onCheckedChange != null) { Modifier.toggleable( value = checked, onValueChange = onCheckedChange, enabled = enabled, role = Role.Switch, interactionSource = interactionSource, indication = null ) } else { Modifier } Box( modifier .then(toggleableModifier) .swipeable( state = swipeableState, anchors = mapOf(minBound to false, maxBound to true), thresholds = { _, _ -> FractionalThreshold(0.5f) }, orientation = Orientation.Horizontal, enabled = enabled && onCheckedChange != null, reverseDirection = isRtl, interactionSource = interactionSource, resistance = null ) .wrapContentSize(Alignment.Center) .padding(DefaultSwitchPadding) .requiredSize(SwitchWidth, SwitchHeight) ) { SwitchImpl( checked = checked, enabled = enabled, colors = colors, thumbValue = swipeableState.offset, interactionSource = interactionSource ) } } /** * Represents the colors used by a [Switch] in different states * * See [SwitchDefaults.colors] for the default implementation that follows Material * specifications. */ @Stable interface SwitchColors { /** * Represents the color used for the switch's thumb, depending on [enabled] and [checked]. * * @param enabled whether the [Switch] is enabled or not * @param checked whether the [Switch] is checked or not */ @Composable fun thumbColor(enabled: Boolean, checked: Boolean): State /** * Represents the color used for the switch's track, depending on [enabled] and [checked]. * * @param enabled whether the [Switch] is enabled or not * @param checked whether the [Switch] is checked or not */ @Composable fun trackColor(enabled: Boolean, checked: Boolean): State } @Composable private fun BoxScope.SwitchImpl( checked: Boolean, enabled: Boolean, colors: SwitchColors, thumbValue: State, interactionSource: InteractionSource ) { val interactions = remember { mutableStateListOf() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> interactions.add(interaction) is PressInteraction.Release -> interactions.remove(interaction.press) is PressInteraction.Cancel -> interactions.remove(interaction.press) is DragInteraction.Start -> interactions.add(interaction) is DragInteraction.Stop -> interactions.remove(interaction.start) is DragInteraction.Cancel -> interactions.remove(interaction.start) } } } val hasInteraction = interactions.isNotEmpty() val elevation = if (hasInteraction) { ThumbPressedElevation } else { ThumbDefaultElevation } val trackColor by colors.trackColor(enabled, checked) Canvas(Modifier.align(Alignment.Center).fillMaxSize()) { drawTrack(trackColor, TrackWidth.toPx(), TrackStrokeWidth.toPx()) } val thumbColor by colors.thumbColor(enabled, checked) val elevationOverlay = LocalElevationOverlay.current val absoluteElevation = LocalAbsoluteElevation.current + elevation val resolvedThumbColor = if (thumbColor == MaterialTheme.colors.surface && elevationOverlay != null) { elevationOverlay.apply(thumbColor, absoluteElevation) } else { thumbColor } Spacer( Modifier .align(Alignment.CenterStart) .offset { IntOffset(thumbValue.value.roundToInt(), 0) } .indication( interactionSource = interactionSource, indication = rememberRipple(bounded = false, radius = ThumbRippleRadius) ) .requiredSize(ThumbDiameter) .shadow(elevation, CircleShape, clip = false) .background(resolvedThumbColor, CircleShape) ) } private fun DrawScope.drawTrack(trackColor: Color, trackWidth: Float, strokeWidth: Float) { val strokeRadius = strokeWidth / 2 drawLine( trackColor, Offset(strokeRadius, center.y), Offset(trackWidth - strokeRadius, center.y), strokeWidth, StrokeCap.Round ) } internal val TrackWidth = 34.dp internal val TrackStrokeWidth = 14.dp internal val ThumbDiameter = 20.dp private val ThumbRippleRadius = 24.dp private val DefaultSwitchPadding = 2.dp private val SwitchWidth = TrackWidth private val SwitchHeight = ThumbDiameter private val ThumbPathLength = TrackWidth - ThumbDiameter private val AnimationSpec = TweenSpec(durationMillis = 100) private val ThumbDefaultElevation = 1.dp private val ThumbPressedElevation = 6.dp /** * Contains the default values used by [Switch] */ object SwitchDefaults { /** * Creates a [SwitchColors] that represents the different colors used in a [Switch] in * different states. * * @param checkedThumbColor the color used for the thumb when enabled and checked * @param checkedTrackColor the color used for the track when enabled and checked * @param checkedTrackAlpha the alpha applied to [checkedTrackColor] and * [disabledCheckedTrackColor] * @param uncheckedThumbColor the color used for the thumb when enabled and unchecked * @param uncheckedTrackColor the color used for the track when enabled and unchecked * @param uncheckedTrackAlpha the alpha applied to [uncheckedTrackColor] and * [disabledUncheckedTrackColor] * @param disabledCheckedThumbColor the color used for the thumb when disabled and checked * @param disabledCheckedTrackColor the color used for the track when disabled and checked * @param disabledUncheckedThumbColor the color used for the thumb when disabled and unchecked * @param disabledUncheckedTrackColor the color used for the track when disabled and unchecked */ @Composable fun colors( checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant, checkedTrackColor: Color = checkedThumbColor, checkedTrackAlpha: Float = 0.54f, uncheckedThumbColor: Color = MaterialTheme.colors.surface, uncheckedTrackColor: Color = MaterialTheme.colors.onSurface, uncheckedTrackAlpha: Float = 0.38f, disabledCheckedThumbColor: Color = checkedThumbColor .copy(alpha = ContentAlpha.disabled) .compositeOver(MaterialTheme.colors.surface), disabledCheckedTrackColor: Color = checkedTrackColor .copy(alpha = ContentAlpha.disabled) .compositeOver(MaterialTheme.colors.surface), disabledUncheckedThumbColor: Color = uncheckedThumbColor .copy(alpha = ContentAlpha.disabled) .compositeOver(MaterialTheme.colors.surface), disabledUncheckedTrackColor: Color = uncheckedTrackColor .copy(alpha = ContentAlpha.disabled) .compositeOver(MaterialTheme.colors.surface) ): SwitchColors = DefaultSwitchColors( checkedThumbColor = checkedThumbColor, checkedTrackColor = checkedTrackColor.copy(alpha = checkedTrackAlpha), uncheckedThumbColor = uncheckedThumbColor, uncheckedTrackColor = uncheckedTrackColor.copy(alpha = uncheckedTrackAlpha), disabledCheckedThumbColor = disabledCheckedThumbColor, disabledCheckedTrackColor = disabledCheckedTrackColor.copy(alpha = checkedTrackAlpha), disabledUncheckedThumbColor = disabledUncheckedThumbColor, disabledUncheckedTrackColor = disabledUncheckedTrackColor.copy(alpha = uncheckedTrackAlpha) ) } /** * Default [SwitchColors] implementation. */ @Immutable private class DefaultSwitchColors( private val checkedThumbColor: Color, private val checkedTrackColor: Color, private val uncheckedThumbColor: Color, private val uncheckedTrackColor: Color, private val disabledCheckedThumbColor: Color, private val disabledCheckedTrackColor: Color, private val disabledUncheckedThumbColor: Color, private val disabledUncheckedTrackColor: Color ) : SwitchColors { @Composable override fun thumbColor(enabled: Boolean, checked: Boolean): State { return rememberUpdatedState( if (enabled) { if (checked) checkedThumbColor else uncheckedThumbColor } else { if (checked) disabledCheckedThumbColor else disabledUncheckedThumbColor } ) } @Composable override fun trackColor(enabled: Boolean, checked: Boolean): State { return rememberUpdatedState( if (enabled) { if (checked) checkedTrackColor else uncheckedTrackColor } else { if (checked) disabledCheckedTrackColor else disabledUncheckedTrackColor } ) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false other as DefaultSwitchColors if (checkedThumbColor != other.checkedThumbColor) return false if (checkedTrackColor != other.checkedTrackColor) return false if (uncheckedThumbColor != other.uncheckedThumbColor) return false if (uncheckedTrackColor != other.uncheckedTrackColor) return false if (disabledCheckedThumbColor != other.disabledCheckedThumbColor) return false if (disabledCheckedTrackColor != other.disabledCheckedTrackColor) return false if (disabledUncheckedThumbColor != other.disabledUncheckedThumbColor) return false if (disabledUncheckedTrackColor != other.disabledUncheckedTrackColor) return false return true } override fun hashCode(): Int { var result = checkedThumbColor.hashCode() result = 31 * result + checkedTrackColor.hashCode() result = 31 * result + uncheckedThumbColor.hashCode() result = 31 * result + uncheckedTrackColor.hashCode() result = 31 * result + disabledCheckedThumbColor.hashCode() result = 31 * result + disabledCheckedTrackColor.hashCode() result = 31 * result + disabledUncheckedThumbColor.hashCode() result = 31 * result + disabledUncheckedTrackColor.hashCode() return result } }