Skip to content

Commit 7dda34c

Browse files
author
Bořek Leikep
authored
Merge pull request #566 from kiwicom/textfield-focus-glow
Add TextField's focus glow
2 parents 7f51185 + ed06148 commit 7dda34c

11 files changed

+13495
-10244
lines changed

catalog/src/release/generated/baselineProfiles/baseline-prof.txt

+12,799-9,731
Large diffs are not rendered by default.

icons/src/androidMain/baseline-prof.txt

+18-16
Large diffs are not rendered by default.

illustrations/src/androidMain/baseline-prof.txt

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ PLkiwi/orbit/compose/illustrations/Illustrations;->getFastTrack(Landroidx/compos
3737
PLkiwi/orbit/compose/illustrations/Illustrations;->getFastTrackMan(Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
3838
PLkiwi/orbit/compose/illustrations/Illustrations;->getFeedback(Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
3939
PLkiwi/orbit/compose/illustrations/Illustrations;->getFlexibleDates(Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
40+
PLkiwi/orbit/compose/illustrations/Illustrations;->getFlightDisruptions(Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
4041
PLkiwi/orbit/compose/illustrations/Illustrations;->getGroundTransport404(Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
4142
PLkiwi/orbit/compose/illustrations/Illustrations;->getHelp(Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;
4243
PLkiwi/orbit/compose/illustrations/Illustrations;->getImprove(Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/graphics/painter/Painter;

ui/src/androidMain/baseline-prof.txt

+575-461
Large diffs are not rendered by default.

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

+90-24
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
package kiwi.orbit.compose.ui.controls
22

33
import androidx.compose.animation.animateColor
4+
import androidx.compose.animation.core.Transition
5+
import androidx.compose.animation.core.animateDp
46
import androidx.compose.animation.core.tween
57
import androidx.compose.animation.core.updateTransition
68
import androidx.compose.foundation.background
7-
import androidx.compose.foundation.border
89
import androidx.compose.foundation.interaction.MutableInteractionSource
910
import androidx.compose.foundation.interaction.collectIsFocusedAsState
1011
import androidx.compose.foundation.layout.fillMaxWidth
1112
import androidx.compose.foundation.text.BasicTextField
1213
import androidx.compose.foundation.text.KeyboardActions
1314
import androidx.compose.foundation.text.KeyboardOptions
1415
import androidx.compose.runtime.Composable
16+
import androidx.compose.runtime.State
1517
import androidx.compose.runtime.getValue
1618
import androidx.compose.runtime.mutableStateOf
1719
import androidx.compose.runtime.remember
1820
import androidx.compose.runtime.setValue
1921
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.draw.drawWithCache
23+
import androidx.compose.ui.geometry.CornerRadius
24+
import androidx.compose.ui.geometry.Offset
25+
import androidx.compose.ui.geometry.Size
2026
import androidx.compose.ui.graphics.Color
2127
import androidx.compose.ui.graphics.SolidColor
28+
import androidx.compose.ui.graphics.drawscope.Stroke
2229
import androidx.compose.ui.res.stringResource
2330
import androidx.compose.ui.semantics.error
2431
import androidx.compose.ui.semantics.semantics
2532
import androidx.compose.ui.text.input.TextFieldValue
2633
import androidx.compose.ui.text.input.VisualTransformation
34+
import androidx.compose.ui.unit.Dp
2735
import androidx.compose.ui.unit.dp
2836
import kiwi.orbit.compose.icons.Icons
2937
import kiwi.orbit.compose.ui.OrbitTheme
@@ -120,7 +128,7 @@ internal fun TextField(
120128
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
121129
val textFieldValue = textFieldValueState.copy(text = value)
122130

123-
// Reset input text style color to content color norma.
131+
// Reset input text style color to content color normal.
124132
val textStyle = LocalTextStyle.current
125133
val inputTextStyle = textStyle.copy(color = OrbitTheme.colors.content.normal)
126134

@@ -194,33 +202,20 @@ private fun TextFiledDecorationBox(
194202
}
195203

196204
val isFocused = interactionSource.collectIsFocusedAsState().value
197-
val inputState: InputState = when (isFocused) {
198-
true -> when (error != null) {
199-
true -> InputState.FocusedError
200-
false -> InputState.Focused
201-
}
202-
false -> when (error != null) {
203-
true -> InputState.NormalError
204-
false -> InputState.Normal
205-
}
206-
}
205+
val inputState = resolveInputState(isFocused, isError = error != null)
207206

208207
val transition = updateTransition(inputState, "stateTransition")
209-
val borderColor = transition.animateColor(
210-
transitionSpec = { tween(durationMillis = AnimationDuration) },
211-
label = "borderColor",
212-
) {
213-
when (it) {
214-
InputState.Normal -> Color.Transparent
215-
InputState.Focused, InputState.FocusedError -> OrbitTheme.colors.info.normal
216-
InputState.NormalError -> OrbitTheme.colors.critical.normal
217-
}
218-
}
208+
val borderColor by transition.animateBorderColor()
209+
val glowWidth by transition.animateGlowWidth()
219210

220211
FieldContent(
221212
modifier = Modifier
222-
.border(1.dp, borderColor.value, OrbitTheme.shapes.normal)
223-
.background(OrbitTheme.colors.surface.normal, OrbitTheme.shapes.normal),
213+
.background(OrbitTheme.colors.surface.normal, OrbitTheme.shapes.normal)
214+
.borderWithGlow(
215+
provideBorderColor = { borderColor },
216+
provideGlowColor = { borderColor.copy(borderColor.alpha * GlowOpacity) },
217+
provideGlowWidth = { glowWidth },
218+
),
224219
fieldContent = innerTextField,
225220
placeholder = when (textFieldValue.text.isEmpty()) {
226221
true -> placeholder
@@ -243,13 +238,84 @@ private fun TextFiledDecorationBox(
243238
}
244239
}
245240

241+
private fun resolveInputState(isFocused: Boolean, isError: Boolean): InputState =
242+
when (isFocused) {
243+
true -> when (isError) {
244+
true -> InputState.FocusedError
245+
false -> InputState.Focused
246+
}
247+
false -> when (isError) {
248+
true -> InputState.NormalError
249+
false -> InputState.Normal
250+
}
251+
}
252+
253+
@Composable
254+
private fun Transition<InputState>.animateBorderColor(): State<Color> =
255+
this.animateColor(
256+
transitionSpec = { tween(AnimationDuration) },
257+
label = "borderColor",
258+
) {
259+
when (it) {
260+
InputState.Normal -> Color.Transparent
261+
InputState.Focused -> OrbitTheme.colors.info.normal
262+
InputState.NormalError, InputState.FocusedError -> OrbitTheme.colors.critical.normal
263+
}
264+
}
265+
266+
@Composable
267+
private fun Transition<InputState>.animateGlowWidth(): State<Dp> =
268+
this.animateDp(
269+
transitionSpec = { tween(AnimationDuration) },
270+
label = "glowWidth",
271+
) {
272+
when (it) {
273+
InputState.Normal, InputState.NormalError -> 0.dp
274+
InputState.Focused, InputState.FocusedError -> GlowWidth
275+
}
276+
}
277+
278+
private fun Modifier.borderWithGlow(
279+
provideBorderColor: () -> Color,
280+
provideGlowColor: () -> Color,
281+
provideGlowWidth: () -> Dp,
282+
): Modifier = drawWithCache {
283+
val borderColor = provideBorderColor()
284+
val glowColor = provideGlowColor()
285+
val glowWidth = provideGlowWidth()
286+
287+
val cornerSizePx = CornerSize.toPx()
288+
val borderWidthPx = BorderWidth.toPx()
289+
val glowWidthPx = glowWidth.toPx()
290+
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+
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)
300+
301+
onDrawBehind {
302+
drawRoundRect(glowColor, glowTopLeft, glowSize, glowCornerRadius, glowStyle)
303+
drawRoundRect(borderColor, borderTopLeft, borderSize, borderCornerRadius, borderStyle)
304+
}
305+
}
306+
246307
private enum class InputState {
247308
Normal,
248309
NormalError,
249310
Focused,
250311
FocusedError,
251312
}
252313

314+
private val BorderWidth = 2.dp
315+
private val GlowWidth = 2.dp
316+
private val CornerSize = 6.dp
317+
318+
private const val GlowOpacity = 0.1f
253319
private const val AnimationDuration = 150
254320

255321
@OrbitPreviews
Loading
Loading
Loading
Loading

0 commit comments

Comments
 (0)