1
1
package kiwi.orbit.compose.ui.controls
2
2
3
3
import androidx.compose.animation.animateColor
4
+ import androidx.compose.animation.core.Transition
5
+ import androidx.compose.animation.core.animateDp
4
6
import androidx.compose.animation.core.tween
5
7
import androidx.compose.animation.core.updateTransition
6
8
import androidx.compose.foundation.background
7
- import androidx.compose.foundation.border
8
9
import androidx.compose.foundation.interaction.MutableInteractionSource
9
10
import androidx.compose.foundation.interaction.collectIsFocusedAsState
10
11
import androidx.compose.foundation.layout.fillMaxWidth
11
12
import androidx.compose.foundation.text.BasicTextField
12
13
import androidx.compose.foundation.text.KeyboardActions
13
14
import androidx.compose.foundation.text.KeyboardOptions
14
15
import androidx.compose.runtime.Composable
16
+ import androidx.compose.runtime.State
15
17
import androidx.compose.runtime.getValue
16
18
import androidx.compose.runtime.mutableStateOf
17
19
import androidx.compose.runtime.remember
18
20
import androidx.compose.runtime.setValue
19
21
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
20
26
import androidx.compose.ui.graphics.Color
21
27
import androidx.compose.ui.graphics.SolidColor
28
+ import androidx.compose.ui.graphics.drawscope.Stroke
22
29
import androidx.compose.ui.res.stringResource
23
30
import androidx.compose.ui.semantics.error
24
31
import androidx.compose.ui.semantics.semantics
25
32
import androidx.compose.ui.text.input.TextFieldValue
26
33
import androidx.compose.ui.text.input.VisualTransformation
34
+ import androidx.compose.ui.unit.Dp
27
35
import androidx.compose.ui.unit.dp
28
36
import kiwi.orbit.compose.icons.Icons
29
37
import kiwi.orbit.compose.ui.OrbitTheme
@@ -120,7 +128,7 @@ internal fun TextField(
120
128
var textFieldValueState by remember { mutableStateOf(TextFieldValue (text = value)) }
121
129
val textFieldValue = textFieldValueState.copy(text = value)
122
130
123
- // Reset input text style color to content color norma .
131
+ // Reset input text style color to content color normal .
124
132
val textStyle = LocalTextStyle .current
125
133
val inputTextStyle = textStyle.copy(color = OrbitTheme .colors.content.normal)
126
134
@@ -194,33 +202,20 @@ private fun TextFiledDecorationBox(
194
202
}
195
203
196
204
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 )
207
206
208
207
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()
219
210
220
211
FieldContent (
221
212
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
+ ),
224
219
fieldContent = innerTextField,
225
220
placeholder = when (textFieldValue.text.isEmpty()) {
226
221
true -> placeholder
@@ -243,13 +238,84 @@ private fun TextFiledDecorationBox(
243
238
}
244
239
}
245
240
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
+
246
307
private enum class InputState {
247
308
Normal ,
248
309
NormalError ,
249
310
Focused ,
250
311
FocusedError ,
251
312
}
252
313
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
253
319
private const val AnimationDuration = 150
254
320
255
321
@OrbitPreviews
0 commit comments