Skip to content

Commit

Permalink
Implement MacOSSwitch and innerShadow Modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
shubhamsinghshubham777 committed May 11, 2024
1 parent 8149de7 commit b28b45e
Show file tree
Hide file tree
Showing 7 changed files with 490 additions and 0 deletions.
50 changes: 50 additions & 0 deletions adapt/src/androidMain/kotlin/design/adapt/Utils.android.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
package design.adapt

import android.graphics.BlurMaskFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
Expand Down Expand Up @@ -72,3 +77,48 @@ actual fun Modifier.dropShadow(
}
}
)

actual fun Modifier.innerShadow(
shape: Shape,
color: Color,
blur: Dp,
offsetX: Dp,
offsetY: Dp,
spread: Dp,
): Modifier = then(
drawWithContent {
drawContent()

val rect = Rect(Offset.Zero, size)
val paint = Paint().apply {
this.color = color
this.isAntiAlias = true
}

val shadowOutline = shape.createOutline(size, layoutDirection, this)

drawIntoCanvas { canvas ->

canvas.saveLayer(rect, paint)
canvas.drawOutline(shadowOutline, paint)

val frameworkPaint = paint.asFrameworkPaint()
frameworkPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)

if (blur.toPx() > 0) {
frameworkPaint.maskFilter = BlurMaskFilter(blur.toPx(), BlurMaskFilter.Blur.NORMAL)
}

paint.color = Color.Black

val spreadOffsetX =
offsetX.toPx() + if (offsetX.toPx() < 0) -spread.toPx() else spread.toPx()
val spreadOffsetY =
offsetY.toPx() + if (offsetY.toPx() < 0) -spread.toPx() else spread.toPx()

canvas.translate(spreadOffsetX, spreadOffsetY)
canvas.drawOutline(shadowOutline, paint)
canvas.restore()
}
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2023 Shubham Singh
*
* 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 design.adapt.previews.cupertino

import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import design.adapt.cupertino.MacOSSwitch
import design.adapt.cupertino.MacOSTheme

@Preview
@Composable
private fun MacOSSwitchPreview() {
var checked by remember { mutableStateOf(true) }
MacOSTheme {
MacOSSwitch(
modifier = Modifier.padding(8.dp),
checked = checked,
onCheckedChange = { checked = it },
)
}
}
9 changes: 9 additions & 0 deletions adapt/src/commonMain/kotlin/design/adapt/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ expect fun Modifier.dropShadow(
spread: Dp = 0.dp,
): Modifier

expect fun Modifier.innerShadow(
shape: Shape,
color: Color = Color.Black,
blur: Dp = 0.dp,
offsetX: Dp = 0.dp,
offsetY: Dp = 0.dp,
spread: Dp = 0.dp,
): Modifier

// Ref: https://stackoverflow.com/a/70031663/20325172
@Composable
fun animateHorizontalAlignmentAsState(targetBiasValue: Float): State<BiasAlignment.Horizontal> {
Expand Down
233 changes: 233 additions & 0 deletions adapt/src/commonMain/kotlin/design/adapt/cupertino/MacOSSwitch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* Copyright 2023 Shubham Singh
*
* 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 design.adapt.cupertino

import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
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.derivedStateOf
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.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import design.adapt.animateHorizontalAlignmentAsState
import design.adapt.dropShadow
import design.adapt.innerShadow

@Composable
fun MacOSSwitch(
checked: Boolean,
onCheckedChange: (checked: Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
trackShape: Shape = CircleShape,
thumbShape: Shape = CircleShape,
colors: MacOSSwitchColors = MacOSSwitchDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val density = LocalDensity.current
var dragOffsetX by remember { mutableStateOf<Float?>(null) }

/**
* Gives first preference to whether the user has dragged the thumb over half the width of the
* toggle, but if the user is not dragging, then it prefers the provided [checked] value.
*/
val isDraggedEnoughOrChecked by remember(checked) {
derivedStateOf {
dragOffsetX?.let { safeDragOffsetX ->
val widthPx = with(density) { MacOSSwitchDefaults.SwitchSize.width.toPx() }
return@derivedStateOf safeDragOffsetX > widthPx / 2
}
return@derivedStateOf checked
}
}

val thumbAlignment by animateHorizontalAlignmentAsState(
targetBiasValue = if (isDraggedEnoughOrChecked) 1f else -1f
)

val animatedTrackColor by animateColorAsState(
targetValue = when {
!enabled -> colors.disabledTrackColor
isDraggedEnoughOrChecked -> colors.trackColor
else -> colors.uncheckedTrackColor
},
)

val animatedThumbColor by animateColorAsState(
targetValue = when {
!enabled -> colors.disabledThumbColor
isDraggedEnoughOrChecked -> colors.thumbColor
else -> colors.uncheckedThumbColor
},
)

val animatedThumbBorderColor by animateColorAsState(
targetValue = when {
!enabled -> colors.disabledThumbBorderColor
isDraggedEnoughOrChecked -> colors.thumbBorderColor
else -> colors.uncheckedThumbBorderColor
},
)

Column(
modifier = modifier
.defaultMinSize(
minWidth = MacOSSwitchDefaults.SwitchSize.width,
minHeight = MacOSSwitchDefaults.SwitchSize.height
)
.clip(trackShape)
.switchInnerShadows(shape = trackShape)
.drawBehind {
val outline = trackShape.createOutline(size, layoutDirection, density)
drawOutline(
outline = outline,
color = animatedTrackColor,
)
}
.clickable(
interactionSource = interactionSource,
indication = null,
enabled = enabled,
onClick = { onCheckedChange(!checked) },
)
.pointerInput(enabled) {
if (enabled) {
detectHorizontalDragGestures(
onDragStart = { offset -> dragOffsetX = offset.x },
onHorizontalDrag = { _, delta ->
dragOffsetX?.let { safeOffsetX -> dragOffsetX = safeOffsetX + delta }
},
onDragEnd = {
onCheckedChange(isDraggedEnoughOrChecked)
dragOffsetX = null
},
onDragCancel = {
onCheckedChange(isDraggedEnoughOrChecked)
dragOffsetX = null
},
)
}
},
horizontalAlignment = thumbAlignment,
verticalArrangement = Arrangement.Center,
) {
Box(
modifier = Modifier
.padding(0.5.dp)
.size(MacOSSwitchDefaults.ThumbSize)
.clip(thumbShape)
.dropShadow(
shape = thumbShape,
offsetY = 0.25.dp,
blur = 0.5.dp,
spread = 0.1.dp,
color = Color.Black.copy(alpha = 0.12f),
)
.drawBehind {
val outline = thumbShape.createOutline(size, layoutDirection, density)
drawOutline(
outline = outline,
color = animatedThumbColor,
)
drawOutline(
outline = outline,
color = animatedThumbBorderColor,
style = Stroke(width = 0.5.dp.toPx())
)
}
)
}
}

private fun Modifier.switchInnerShadows(shape: Shape) = then(
Modifier
.innerShadow(
shape = shape,
blur = 2.dp,
spread = 2.dp,
color = Color.Black.copy(alpha = 0.02f),
)
.innerShadow(
shape = shape,
blur = 1.5.dp,
spread = 0.35.dp,
color = Color.Black.copy(alpha = 0.12f),
)
)

@Immutable
data class MacOSSwitchColors(
val trackColor: Color,
val thumbColor: Color,
val thumbBorderColor: Color,
val uncheckedTrackColor: Color,
val uncheckedThumbColor: Color,
val uncheckedThumbBorderColor: Color,
val disabledTrackColor: Color,
val disabledThumbColor: Color,
val disabledThumbBorderColor: Color,
)

object MacOSSwitchDefaults {
val SwitchSize = DpSize(width = 26.dp, height = 15.dp)
val ThumbSize = DpSize(width = 13.dp, height = 13.dp)

@Composable
fun colors(
trackColor: Color = Color(0xFF478CF6),
thumbColor: Color = MacOSTheme.colorScheme.systemWhite,
thumbBorderColor: Color = MacOSTheme.colorScheme.systemBlack.copy(alpha = 0.02f),
uncheckedTrackColor: Color = MacOSTheme.colorScheme.systemBlack.copy(alpha = 0.09f),
uncheckedThumbColor: Color = MacOSTheme.colorScheme.systemWhite,
uncheckedThumbBorderColor: Color = thumbBorderColor,
disabledTrackColor: Color = uncheckedTrackColor,
disabledThumbColor: Color = thumbColor,
disabledThumbBorderColor: Color = thumbBorderColor,
) = MacOSSwitchColors(
trackColor = trackColor,
thumbColor = thumbColor,
thumbBorderColor = thumbBorderColor,
uncheckedTrackColor = uncheckedTrackColor,
uncheckedThumbColor = uncheckedThumbColor,
uncheckedThumbBorderColor = uncheckedThumbBorderColor,
disabledTrackColor = disabledTrackColor,
disabledThumbColor = disabledThumbColor,
disabledThumbBorderColor = disabledThumbBorderColor,
)
}
Loading

0 comments on commit b28b45e

Please sign in to comment.