Skip to content

Commit

Permalink
Merge pull request #575 from kiwicom/border-expand
Browse files Browse the repository at this point in the history
Rework drawing expanded border
  • Loading branch information
hrach authored Nov 13, 2023
2 parents c044682 + 3cb587e commit 3ff155f
Show file tree
Hide file tree
Showing 18 changed files with 146 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import kiwi.orbit.compose.ui.OrbitTheme
import kiwi.orbit.compose.ui.R
import kiwi.orbit.compose.ui.controls.internal.OrbitPreviews
import kiwi.orbit.compose.ui.controls.internal.Preview
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect

@OptIn(ExperimentalAnimationGraphicsApi::class)
@Composable
Expand Down Expand Up @@ -124,8 +125,6 @@ private fun DrawScope.drawCheckbox(borderColor: Color, backgroundColor: Color, e
val errorShift = if (hasError) 0.5.dp.toPx() else 0f

val checkboxSize = CheckboxSize.toPx()
val checkboxBorderWidth = CheckboxBorderWidth.toPx()
val checkboxBorderHalfWidth = checkboxBorderWidth / 2.0f
val checkboxCornerRadius = CornerRadius(CheckboxCornerRadius.toPx())
drawRoundRect(
color = backgroundColor,
Expand All @@ -134,36 +133,31 @@ private fun DrawScope.drawCheckbox(borderColor: Color, backgroundColor: Color, e
cornerRadius = checkboxCornerRadius,
style = Fill,
)
drawRoundRect(
drawStrokeOutlineRoundRect(
color = borderColor,
topLeft = Offset(checkboxBorderHalfWidth, checkboxBorderHalfWidth),
size = Size(checkboxSize - checkboxBorderWidth, checkboxSize - checkboxBorderWidth),
topLeft = Offset.Zero,
size = Size(CheckboxSize.toPx(), CheckboxSize.toPx()),
cornerRadius = checkboxCornerRadius,
style = Stroke(checkboxBorderWidth),
stroke = Stroke(CheckboxBorderWidth.toPx()),
)
}

private fun DrawScope.drawError(borderColor: Color, shadowColor: Color, alpha: Float) {
if (alpha == 0.0f) return

val shadowRectShift = (ErrorShadowWidth / 2.0f).toPx()
val shadowRectSize = (ErrorShadowSize - ErrorShadowWidth).toPx()
drawRoundRect(
drawStrokeOutlineRoundRect(
color = shadowColor,
topLeft = Offset(-shadowRectShift, -shadowRectShift),
size = Size(shadowRectSize, shadowRectSize),
topLeft = Offset(-ErrorShadowWidth.toPx(), -ErrorShadowWidth.toPx()),
size = Size(ErrorShadowSize.toPx(), ErrorShadowSize.toPx()),
cornerRadius = CornerRadius(ErrorShadowCornerRadius.toPx()),
style = Stroke(ErrorShadowWidth.toPx()),
stroke = Stroke(ErrorShadowWidth.toPx()),
)

val errorRectShift = (CheckboxBorderWidth / 2.0f).toPx()
val errorRectSize = (CheckboxSize - CheckboxBorderWidth).toPx()
drawRoundRect(
drawStrokeOutlineRoundRect(
color = borderColor,
topLeft = Offset(errorRectShift, errorRectShift),
size = Size(errorRectSize, errorRectSize),
topLeft = Offset.Zero,
size = Size(CheckboxSize.toPx(), CheckboxSize.toPx()),
cornerRadius = CornerRadius(CheckboxCornerRadius.toPx()),
style = Stroke(CheckboxBorderWidth.toPx()),
stroke = Stroke(CheckboxBorderWidth.toPx()),
)
}

Expand Down
20 changes: 10 additions & 10 deletions ui/src/androidMain/kotlin/kiwi/orbit/compose/ui/controls/Coupon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
Expand All @@ -30,6 +29,7 @@ import androidx.compose.ui.unit.dp
import kiwi.orbit.compose.ui.OrbitTheme
import kiwi.orbit.compose.ui.controls.internal.Preview
import kiwi.orbit.compose.ui.foundation.ContentEmphasis
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect

/**
* Simple component used for highlighting coupons / promo codes.
Expand Down Expand Up @@ -78,24 +78,24 @@ public fun Coupon(

private fun Modifier.dashedBorder(
color: Color,
strokeWidth: Dp = 1.dp,
strokeLength: Dp = 8.dp,
cornerRadius: Dp = 0.dp,
strokeWidth: Dp,
strokeLength: Dp,
cornerRadius: Dp,
): Modifier = drawWithCache {
val strokeWidthPx = strokeWidth.toPx()
val strokeLengthPx = strokeLength.toPx()

@Suppress("NAME_SHADOWING")
val cornerRadius = CornerRadius(cornerRadius.toPx())

val topLeft = Offset(strokeWidthPx / 2f, strokeWidthPx / 2f)
val size = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
val style = Stroke(
width = strokeWidthPx,
val topLeft = Offset.Zero
val stroke = Stroke(
width = strokeWidth.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(strokeLengthPx, strokeLengthPx)),
)

onDrawBehind { drawRoundRect(color, topLeft, size, cornerRadius, style) }
onDrawBehind {
drawStrokeOutlineRoundRect(color, topLeft, size, cornerRadius, stroke)
}
}

@Preview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
Expand All @@ -44,6 +43,7 @@ import kiwi.orbit.compose.ui.controls.internal.Preview
import kiwi.orbit.compose.ui.foundation.ContentEmphasis
import kiwi.orbit.compose.ui.foundation.LocalContentEmphasis
import kiwi.orbit.compose.ui.foundation.LocalTextStyle
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect

/**
* A segmented switch displaying two options.
Expand Down Expand Up @@ -237,31 +237,32 @@ private fun SelectionOutline(
selectedIndex: Int,
optionsCount: Int,
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val animatedOffset by animateFloatAsState(
targetValue = selectedIndex.toFloat(),
targetValue = when (isRtl) {
false -> selectedIndex.toFloat()
true -> optionsCount - 1 - selectedIndex.toFloat()
},
label = "SegmentedSwitchSelectedOffset",
)
val brushColor = OrbitTheme.colors.info.normal
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val shape = OrbitTheme.shapes.normal
Canvas(
modifier = Modifier
.padding(1.dp)
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
onDraw = {
val rectSize = size.copy(width = size.width / optionsCount)
val divWidth = 2.dp.toPx()
val itemSize = (size.width - optionsCount * divWidth) / optionsCount
val rectSize = size.copy(width = itemSize + 2 * divWidth)
val topLeft = Offset(
x = when (isRtl) {
false -> animatedOffset * rectSize.width
true -> size.width - ((animatedOffset + 1) * rectSize.width)
},
x = animatedOffset * itemSize + (animatedOffset - 1).coerceAtLeast(0f) * divWidth,
y = 0f,
)
drawRoundRect(
brush = SolidColor(brushColor),
drawStrokeOutlineRoundRect(
color = brushColor,
topLeft = topLeft,
size = rectSize,
cornerRadius = CornerRadius(5.dp.toPx(), 5.dp.toPx()),
style = Stroke(width = 2.dp.toPx()),
cornerRadius = CornerRadius(shape.topStart.toPx(rectSize, density = this)),
stroke = Stroke(width = divWidth),
)
},
)
Expand All @@ -276,9 +277,13 @@ internal fun SegmentedSwitchPreview() {
) {
SegmentedSwitchUnselectedPreview()
SegmentedSwitchSelectedPreview()
SegmentedSwitchThreeOptionsUnselectedPreview()
SegmentedSwitchThreeOptionsSelectedPreview()
SegmentedSwitchWithInfoPreview()
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Rtl,
) {
SegmentedSwitchWithInfoPreview()
}
SegmentedSwitchWithErrorPreview()
}
}
Expand Down Expand Up @@ -307,21 +312,6 @@ private fun SegmentedSwitchSelectedPreview() {
)
}

@Composable
private fun SegmentedSwitchThreeOptionsUnselectedPreview() {
var selectedIndex by remember { mutableStateOf<Int?>(null) }
SegmentedSwitch(
options = listOf(
{ Text("Off") },
{ Text("On") },
{ Text("Remote") },
),
selectedIndex = selectedIndex,
onOptionClick = { index -> selectedIndex = index },
label = { Text("Feature") },
)
}

@Composable
private fun SegmentedSwitchThreeOptionsSelectedPreview() {
var selectedIndex by remember { mutableStateOf<Int?>(1) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
Expand Down Expand Up @@ -45,6 +46,8 @@ import kiwi.orbit.compose.ui.controls.internal.OrbitPreviews
import kiwi.orbit.compose.ui.controls.internal.Preview
import kiwi.orbit.compose.ui.foundation.LocalTextStyle
import kiwi.orbit.compose.ui.foundation.ProvideMergedTextStyle
import kiwi.orbit.compose.ui.utils.ExpandedCornerSize
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect

/**
* TextFiled control allowing a text single-line or multi-line input.
Expand Down Expand Up @@ -212,6 +215,7 @@ private fun TextFiledDecorationBox(
modifier = Modifier
.background(OrbitTheme.colors.surface.normal, OrbitTheme.shapes.normal)
.borderWithGlow(
cornerSize = OrbitTheme.shapes.normal.topStart,
provideBorderColor = { borderColor },
provideGlowColor = { borderColor.copy(borderColor.alpha * GlowOpacity) },
provideGlowWidth = { glowWidth },
Expand Down Expand Up @@ -276,6 +280,7 @@ private fun Transition<InputState>.animateGlowWidth(): State<Dp> =
}

private fun Modifier.borderWithGlow(
cornerSize: CornerSize,
provideBorderColor: () -> Color,
provideGlowColor: () -> Color,
provideGlowWidth: () -> Dp,
Expand All @@ -284,23 +289,23 @@ private fun Modifier.borderWithGlow(
val glowColor = provideGlowColor()
val glowWidth = provideGlowWidth()

val cornerSizePx = CornerSize.toPx()
val borderWidthPx = BorderWidth.toPx()
val glowWidthPx = glowWidth.toPx()

val glowTopLeft = Offset(-glowWidthPx / 2f, -glowWidthPx / 2f)
val glowSize = Size(size.width + glowWidthPx, size.height + glowWidthPx)
val glowCornerRadius = CornerRadius(cornerSizePx + glowWidthPx / 2f)
val glowStyle = Stroke(glowWidthPx)
val glowCornerSize = ExpandedCornerSize(cornerSize, extraSize = glowWidth)
val glowCornerRadius = CornerRadius(glowCornerSize.toPx(size, density = this))
val glowTopLeft = Offset(-glowWidthPx, -glowWidthPx)
val glowSize = Size(size.width + glowWidthPx * 2, size.height + glowWidthPx * 2)
val glowStroke = Stroke(glowWidthPx)

val borderTopLeft = Offset(borderWidthPx / 2f, borderWidthPx / 2f)
val borderSize = Size(size.width - borderWidthPx, size.height - borderWidthPx)
val borderCornerRadius = CornerRadius(cornerSizePx - borderWidthPx / 2f)
val borderStyle = Stroke(borderWidthPx)
val borderRadius = CornerRadius(cornerSize.toPx(size, density = this))
val borderTopLeft = Offset.Zero
val borderSize = size
val borderStroke = Stroke(borderWidthPx)

onDrawBehind {
drawRoundRect(glowColor, glowTopLeft, glowSize, glowCornerRadius, glowStyle)
drawRoundRect(borderColor, borderTopLeft, borderSize, borderCornerRadius, borderStyle)
drawStrokeOutlineRoundRect(glowColor, glowTopLeft, glowSize, glowCornerRadius, glowStroke)
drawStrokeOutlineRoundRect(borderColor, borderTopLeft, borderSize, borderRadius, borderStroke)
}
}

Expand All @@ -313,7 +318,6 @@ private enum class InputState {

private val BorderWidth = 2.dp
private val GlowWidth = 2.dp
private val CornerSize = 6.dp

private const val GlowOpacity = 0.1f
private const val AnimationDuration = 150
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kiwi.orbit.compose.ui.utils

import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import kotlin.math.max

/**
* Draws round rect, but the redius is defined by the outer radius of the stroke.
*/
internal fun DrawScope.drawStrokeOutlineRoundRect(
color: Color,
topLeft: Offset,
size: Size,
cornerRadius: CornerRadius,
stroke: Stroke,
alpha: Float = 1.0f,
colorFilter: ColorFilter? = null,
blendMode: BlendMode = DrawScope.DefaultBlendMode,
) {
// Stroked rounded rect with the corner radius
// shrunk by half of the stroke width. This will ensure that the
// outer curvature of the rounded rectangle will have the desired
// corner radius.
drawRoundRect(
color = color,
topLeft = topLeft + Offset(stroke.width / 2f, stroke.width / 2f),
size = Size(size.width - stroke.width, size.height - stroke.width),
cornerRadius = cornerRadius.shrink(stroke.width / 2f),
alpha = alpha,
style = stroke,
colorFilter = colorFilter,
blendMode = blendMode,
)
}

private fun CornerRadius.shrink(value: Float): CornerRadius = CornerRadius(
max(0f, this.x - value),
max(0f, this.y - value),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kiwi.orbit.compose.ui.utils

import androidx.compose.foundation.shape.CornerSize
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp

internal class ExpandedCornerSize(
private val original: CornerSize,
private val extraSize: Dp,
) : CornerSize {
override fun toPx(shapeSize: Size, density: Density): Float {
val originalSize = original.toPx(shapeSize, density)
if (originalSize == 0f) return originalSize
return originalSize + with(density) { extraSize.toPx() }
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3ff155f

Please sign in to comment.