Skip to content

Commit 3ff155f

Browse files
authored
Merge pull request #575 from kiwicom/border-expand
Rework drawing expanded border
2 parents c044682 + 3cb587e commit 3ff155f

18 files changed

+146
-96
lines changed

ui/src/androidMain/kotlin/kiwi/orbit/compose/ui/controls/Checkbox.kt

+13-19
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import kiwi.orbit.compose.ui.OrbitTheme
3838
import kiwi.orbit.compose.ui.R
3939
import kiwi.orbit.compose.ui.controls.internal.OrbitPreviews
4040
import kiwi.orbit.compose.ui.controls.internal.Preview
41+
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect
4142

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

126127
val checkboxSize = CheckboxSize.toPx()
127-
val checkboxBorderWidth = CheckboxBorderWidth.toPx()
128-
val checkboxBorderHalfWidth = checkboxBorderWidth / 2.0f
129128
val checkboxCornerRadius = CornerRadius(CheckboxCornerRadius.toPx())
130129
drawRoundRect(
131130
color = backgroundColor,
@@ -134,36 +133,31 @@ private fun DrawScope.drawCheckbox(borderColor: Color, backgroundColor: Color, e
134133
cornerRadius = checkboxCornerRadius,
135134
style = Fill,
136135
)
137-
drawRoundRect(
136+
drawStrokeOutlineRoundRect(
138137
color = borderColor,
139-
topLeft = Offset(checkboxBorderHalfWidth, checkboxBorderHalfWidth),
140-
size = Size(checkboxSize - checkboxBorderWidth, checkboxSize - checkboxBorderWidth),
138+
topLeft = Offset.Zero,
139+
size = Size(CheckboxSize.toPx(), CheckboxSize.toPx()),
141140
cornerRadius = checkboxCornerRadius,
142-
style = Stroke(checkboxBorderWidth),
141+
stroke = Stroke(CheckboxBorderWidth.toPx()),
143142
)
144143
}
145144

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

149-
val shadowRectShift = (ErrorShadowWidth / 2.0f).toPx()
150-
val shadowRectSize = (ErrorShadowSize - ErrorShadowWidth).toPx()
151-
drawRoundRect(
148+
drawStrokeOutlineRoundRect(
152149
color = shadowColor,
153-
topLeft = Offset(-shadowRectShift, -shadowRectShift),
154-
size = Size(shadowRectSize, shadowRectSize),
150+
topLeft = Offset(-ErrorShadowWidth.toPx(), -ErrorShadowWidth.toPx()),
151+
size = Size(ErrorShadowSize.toPx(), ErrorShadowSize.toPx()),
155152
cornerRadius = CornerRadius(ErrorShadowCornerRadius.toPx()),
156-
style = Stroke(ErrorShadowWidth.toPx()),
153+
stroke = Stroke(ErrorShadowWidth.toPx()),
157154
)
158-
159-
val errorRectShift = (CheckboxBorderWidth / 2.0f).toPx()
160-
val errorRectSize = (CheckboxSize - CheckboxBorderWidth).toPx()
161-
drawRoundRect(
155+
drawStrokeOutlineRoundRect(
162156
color = borderColor,
163-
topLeft = Offset(errorRectShift, errorRectShift),
164-
size = Size(errorRectSize, errorRectSize),
157+
topLeft = Offset.Zero,
158+
size = Size(CheckboxSize.toPx(), CheckboxSize.toPx()),
165159
cornerRadius = CornerRadius(CheckboxCornerRadius.toPx()),
166-
style = Stroke(CheckboxBorderWidth.toPx()),
160+
stroke = Stroke(CheckboxBorderWidth.toPx()),
167161
)
168162
}
169163

ui/src/androidMain/kotlin/kiwi/orbit/compose/ui/controls/Coupon.kt

+10-10
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import androidx.compose.ui.draw.clip
1818
import androidx.compose.ui.draw.drawWithCache
1919
import androidx.compose.ui.geometry.CornerRadius
2020
import androidx.compose.ui.geometry.Offset
21-
import androidx.compose.ui.geometry.Size
2221
import androidx.compose.ui.graphics.Color
2322
import androidx.compose.ui.graphics.PathEffect
2423
import androidx.compose.ui.graphics.drawscope.Stroke
@@ -30,6 +29,7 @@ import androidx.compose.ui.unit.dp
3029
import kiwi.orbit.compose.ui.OrbitTheme
3130
import kiwi.orbit.compose.ui.controls.internal.Preview
3231
import kiwi.orbit.compose.ui.foundation.ContentEmphasis
32+
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect
3333

3434
/**
3535
* Simple component used for highlighting coupons / promo codes.
@@ -78,24 +78,24 @@ public fun Coupon(
7878

7979
private fun Modifier.dashedBorder(
8080
color: Color,
81-
strokeWidth: Dp = 1.dp,
82-
strokeLength: Dp = 8.dp,
83-
cornerRadius: Dp = 0.dp,
81+
strokeWidth: Dp,
82+
strokeLength: Dp,
83+
cornerRadius: Dp,
8484
): Modifier = drawWithCache {
85-
val strokeWidthPx = strokeWidth.toPx()
8685
val strokeLengthPx = strokeLength.toPx()
8786

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

91-
val topLeft = Offset(strokeWidthPx / 2f, strokeWidthPx / 2f)
92-
val size = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
93-
val style = Stroke(
94-
width = strokeWidthPx,
90+
val topLeft = Offset.Zero
91+
val stroke = Stroke(
92+
width = strokeWidth.toPx(),
9593
pathEffect = PathEffect.dashPathEffect(floatArrayOf(strokeLengthPx, strokeLengthPx)),
9694
)
9795

98-
onDrawBehind { drawRoundRect(color, topLeft, size, cornerRadius, style) }
96+
onDrawBehind {
97+
drawStrokeOutlineRoundRect(color, topLeft, size, cornerRadius, stroke)
98+
}
9999
}
100100

101101
@Preview

ui/src/androidMain/kotlin/kiwi/orbit/compose/ui/controls/SegmentedSwitch.kt

+21-31
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import androidx.compose.ui.Alignment
2828
import androidx.compose.ui.Modifier
2929
import androidx.compose.ui.geometry.CornerRadius
3030
import androidx.compose.ui.geometry.Offset
31-
import androidx.compose.ui.graphics.SolidColor
3231
import androidx.compose.ui.graphics.drawscope.Stroke
3332
import androidx.compose.ui.platform.LocalLayoutDirection
3433
import androidx.compose.ui.semantics.Role
@@ -44,6 +43,7 @@ import kiwi.orbit.compose.ui.controls.internal.Preview
4443
import kiwi.orbit.compose.ui.foundation.ContentEmphasis
4544
import kiwi.orbit.compose.ui.foundation.LocalContentEmphasis
4645
import kiwi.orbit.compose.ui.foundation.LocalTextStyle
46+
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect
4747

4848
/**
4949
* A segmented switch displaying two options.
@@ -237,31 +237,32 @@ private fun SelectionOutline(
237237
selectedIndex: Int,
238238
optionsCount: Int,
239239
) {
240+
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
240241
val animatedOffset by animateFloatAsState(
241-
targetValue = selectedIndex.toFloat(),
242+
targetValue = when (isRtl) {
243+
false -> selectedIndex.toFloat()
244+
true -> optionsCount - 1 - selectedIndex.toFloat()
245+
},
242246
label = "SegmentedSwitchSelectedOffset",
243247
)
244248
val brushColor = OrbitTheme.colors.info.normal
245-
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
249+
val shape = OrbitTheme.shapes.normal
246250
Canvas(
247-
modifier = Modifier
248-
.padding(1.dp)
249-
.fillMaxSize(),
251+
modifier = Modifier.fillMaxSize(),
250252
onDraw = {
251-
val rectSize = size.copy(width = size.width / optionsCount)
253+
val divWidth = 2.dp.toPx()
254+
val itemSize = (size.width - optionsCount * divWidth) / optionsCount
255+
val rectSize = size.copy(width = itemSize + 2 * divWidth)
252256
val topLeft = Offset(
253-
x = when (isRtl) {
254-
false -> animatedOffset * rectSize.width
255-
true -> size.width - ((animatedOffset + 1) * rectSize.width)
256-
},
257+
x = animatedOffset * itemSize + (animatedOffset - 1).coerceAtLeast(0f) * divWidth,
257258
y = 0f,
258259
)
259-
drawRoundRect(
260-
brush = SolidColor(brushColor),
260+
drawStrokeOutlineRoundRect(
261+
color = brushColor,
261262
topLeft = topLeft,
262263
size = rectSize,
263-
cornerRadius = CornerRadius(5.dp.toPx(), 5.dp.toPx()),
264-
style = Stroke(width = 2.dp.toPx()),
264+
cornerRadius = CornerRadius(shape.topStart.toPx(rectSize, density = this)),
265+
stroke = Stroke(width = divWidth),
265266
)
266267
},
267268
)
@@ -276,9 +277,13 @@ internal fun SegmentedSwitchPreview() {
276277
) {
277278
SegmentedSwitchUnselectedPreview()
278279
SegmentedSwitchSelectedPreview()
279-
SegmentedSwitchThreeOptionsUnselectedPreview()
280280
SegmentedSwitchThreeOptionsSelectedPreview()
281281
SegmentedSwitchWithInfoPreview()
282+
CompositionLocalProvider(
283+
LocalLayoutDirection provides LayoutDirection.Rtl,
284+
) {
285+
SegmentedSwitchWithInfoPreview()
286+
}
282287
SegmentedSwitchWithErrorPreview()
283288
}
284289
}
@@ -307,21 +312,6 @@ private fun SegmentedSwitchSelectedPreview() {
307312
)
308313
}
309314

310-
@Composable
311-
private fun SegmentedSwitchThreeOptionsUnselectedPreview() {
312-
var selectedIndex by remember { mutableStateOf<Int?>(null) }
313-
SegmentedSwitch(
314-
options = listOf(
315-
{ Text("Off") },
316-
{ Text("On") },
317-
{ Text("Remote") },
318-
),
319-
selectedIndex = selectedIndex,
320-
onOptionClick = { index -> selectedIndex = index },
321-
label = { Text("Feature") },
322-
)
323-
}
324-
325315
@Composable
326316
private fun SegmentedSwitchThreeOptionsSelectedPreview() {
327317
var selectedIndex by remember { mutableStateOf<Int?>(1) }

ui/src/androidMain/kotlin/kiwi/orbit/compose/ui/controls/TextField.kt

+16-12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.foundation.background
99
import androidx.compose.foundation.interaction.MutableInteractionSource
1010
import androidx.compose.foundation.interaction.collectIsFocusedAsState
1111
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.shape.CornerSize
1213
import androidx.compose.foundation.text.BasicTextField
1314
import androidx.compose.foundation.text.KeyboardActions
1415
import androidx.compose.foundation.text.KeyboardOptions
@@ -45,6 +46,8 @@ import kiwi.orbit.compose.ui.controls.internal.OrbitPreviews
4546
import kiwi.orbit.compose.ui.controls.internal.Preview
4647
import kiwi.orbit.compose.ui.foundation.LocalTextStyle
4748
import kiwi.orbit.compose.ui.foundation.ProvideMergedTextStyle
49+
import kiwi.orbit.compose.ui.utils.ExpandedCornerSize
50+
import kiwi.orbit.compose.ui.utils.drawStrokeOutlineRoundRect
4851

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

278282
private fun Modifier.borderWithGlow(
283+
cornerSize: CornerSize,
279284
provideBorderColor: () -> Color,
280285
provideGlowColor: () -> Color,
281286
provideGlowWidth: () -> Dp,
@@ -284,23 +289,23 @@ private fun Modifier.borderWithGlow(
284289
val glowColor = provideGlowColor()
285290
val glowWidth = provideGlowWidth()
286291

287-
val cornerSizePx = CornerSize.toPx()
288292
val borderWidthPx = BorderWidth.toPx()
289293
val glowWidthPx = glowWidth.toPx()
290294

291-
val glowTopLeft = Offset(-glowWidthPx / 2f, -glowWidthPx / 2f)
292-
val glowSize = Size(size.width + glowWidthPx, size.height + glowWidthPx)
293-
val glowCornerRadius = CornerRadius(cornerSizePx + glowWidthPx / 2f)
294-
val glowStyle = Stroke(glowWidthPx)
295+
val glowCornerSize = ExpandedCornerSize(cornerSize, extraSize = glowWidth)
296+
val glowCornerRadius = CornerRadius(glowCornerSize.toPx(size, density = this))
297+
val glowTopLeft = Offset(-glowWidthPx, -glowWidthPx)
298+
val glowSize = Size(size.width + glowWidthPx * 2, size.height + glowWidthPx * 2)
299+
val glowStroke = Stroke(glowWidthPx)
295300

296-
val borderTopLeft = Offset(borderWidthPx / 2f, borderWidthPx / 2f)
297-
val borderSize = Size(size.width - borderWidthPx, size.height - borderWidthPx)
298-
val borderCornerRadius = CornerRadius(cornerSizePx - borderWidthPx / 2f)
299-
val borderStyle = Stroke(borderWidthPx)
301+
val borderRadius = CornerRadius(cornerSize.toPx(size, density = this))
302+
val borderTopLeft = Offset.Zero
303+
val borderSize = size
304+
val borderStroke = Stroke(borderWidthPx)
300305

301306
onDrawBehind {
302-
drawRoundRect(glowColor, glowTopLeft, glowSize, glowCornerRadius, glowStyle)
303-
drawRoundRect(borderColor, borderTopLeft, borderSize, borderCornerRadius, borderStyle)
307+
drawStrokeOutlineRoundRect(glowColor, glowTopLeft, glowSize, glowCornerRadius, glowStroke)
308+
drawStrokeOutlineRoundRect(borderColor, borderTopLeft, borderSize, borderRadius, borderStroke)
304309
}
305310
}
306311

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

314319
private val BorderWidth = 2.dp
315320
private val GlowWidth = 2.dp
316-
private val CornerSize = 6.dp
317321

318322
private const val GlowOpacity = 0.1f
319323
private const val AnimationDuration = 150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package kiwi.orbit.compose.ui.utils
2+
3+
import androidx.compose.ui.geometry.CornerRadius
4+
import androidx.compose.ui.geometry.Offset
5+
import androidx.compose.ui.geometry.Size
6+
import androidx.compose.ui.graphics.BlendMode
7+
import androidx.compose.ui.graphics.Color
8+
import androidx.compose.ui.graphics.ColorFilter
9+
import androidx.compose.ui.graphics.drawscope.DrawScope
10+
import androidx.compose.ui.graphics.drawscope.Stroke
11+
import kotlin.math.max
12+
13+
/**
14+
* Draws round rect, but the redius is defined by the outer radius of the stroke.
15+
*/
16+
internal fun DrawScope.drawStrokeOutlineRoundRect(
17+
color: Color,
18+
topLeft: Offset,
19+
size: Size,
20+
cornerRadius: CornerRadius,
21+
stroke: Stroke,
22+
alpha: Float = 1.0f,
23+
colorFilter: ColorFilter? = null,
24+
blendMode: BlendMode = DrawScope.DefaultBlendMode,
25+
) {
26+
// Stroked rounded rect with the corner radius
27+
// shrunk by half of the stroke width. This will ensure that the
28+
// outer curvature of the rounded rectangle will have the desired
29+
// corner radius.
30+
drawRoundRect(
31+
color = color,
32+
topLeft = topLeft + Offset(stroke.width / 2f, stroke.width / 2f),
33+
size = Size(size.width - stroke.width, size.height - stroke.width),
34+
cornerRadius = cornerRadius.shrink(stroke.width / 2f),
35+
alpha = alpha,
36+
style = stroke,
37+
colorFilter = colorFilter,
38+
blendMode = blendMode,
39+
)
40+
}
41+
42+
private fun CornerRadius.shrink(value: Float): CornerRadius = CornerRadius(
43+
max(0f, this.x - value),
44+
max(0f, this.y - value),
45+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package kiwi.orbit.compose.ui.utils
2+
3+
import androidx.compose.foundation.shape.CornerSize
4+
import androidx.compose.ui.geometry.Size
5+
import androidx.compose.ui.unit.Density
6+
import androidx.compose.ui.unit.Dp
7+
8+
internal class ExpandedCornerSize(
9+
private val original: CornerSize,
10+
private val extraSize: Dp,
11+
) : CornerSize {
12+
override fun toPx(shapeSize: Size, density: Density): Float {
13+
val originalSize = original.toPx(shapeSize, density)
14+
if (originalSize == 0f) return originalSize
15+
return originalSize + with(density) { extraSize.toPx() }
16+
}
17+
}
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)