1+ package com.getcode.view.components
2+
3+ import android.view.ViewTreeObserver
4+ import androidx.compose.foundation.ExperimentalFoundationApi
5+ import androidx.compose.foundation.ScrollState
6+ import androidx.compose.foundation.background
7+ import androidx.compose.foundation.border
8+ import androidx.compose.foundation.layout.Arrangement
9+ import androidx.compose.foundation.layout.Box
10+ import androidx.compose.foundation.layout.Row
11+ import androidx.compose.foundation.layout.defaultMinSize
12+ import androidx.compose.foundation.layout.fillMaxWidth
13+ import androidx.compose.foundation.layout.padding
14+ import androidx.compose.foundation.rememberScrollState
15+ import androidx.compose.foundation.text.KeyboardActions
16+ import androidx.compose.foundation.text.KeyboardOptions
17+ import androidx.compose.foundation.text2.BasicSecureTextField
18+ import androidx.compose.foundation.text2.BasicTextField2
19+ import androidx.compose.foundation.text2.input.TextFieldLineLimits
20+ import androidx.compose.foundation.text2.input.TextFieldState
21+ import androidx.compose.foundation.text2.input.TextObfuscationMode
22+ import androidx.compose.foundation.text2.input.textAsFlow
23+ import androidx.compose.material.ExperimentalMaterialApi
24+ import androidx.compose.material.Text
25+ import androidx.compose.material.TextFieldColors
26+ import androidx.compose.runtime.Composable
27+ import androidx.compose.runtime.DisposableEffect
28+ import androidx.compose.runtime.LaunchedEffect
29+ import androidx.compose.runtime.State
30+ import androidx.compose.runtime.getValue
31+ import androidx.compose.runtime.mutableStateOf
32+ import androidx.compose.runtime.remember
33+ import androidx.compose.ui.Alignment
34+ import androidx.compose.ui.Modifier
35+ import androidx.compose.ui.graphics.Color
36+ import androidx.compose.ui.graphics.RectangleShape
37+ import androidx.compose.ui.graphics.Shape
38+ import androidx.compose.ui.graphics.SolidColor
39+ import androidx.compose.ui.platform.LocalFocusManager
40+ import androidx.compose.ui.platform.LocalView
41+ import androidx.compose.ui.text.TextStyle
42+ import androidx.compose.ui.text.input.ImeAction
43+ import androidx.compose.ui.unit.dp
44+ import androidx.core.view.ViewCompat
45+ import androidx.core.view.WindowInsetsCompat
46+ import com.getcode.theme.BrandLight
47+ import com.getcode.theme.CodeTheme
48+ import com.getcode.theme.extraSmall
49+ import com.getcode.theme.inputColors
50+ import kotlinx.coroutines.flow.launchIn
51+ import kotlinx.coroutines.flow.onEach
52+
53+ @OptIn(ExperimentalFoundationApi ::class )
54+ @Composable
55+ fun TextInput (
56+ modifier : Modifier = Modifier ,
57+ placeholder : String = "",
58+ minLines : Int = 1,
59+ maxLines : Int = 4,
60+ state : TextFieldState ,
61+ onStateChanged : () -> Unit = { },
62+ keyboardActions : KeyboardActions = KeyboardActions (),
63+ keyboardOptions : KeyboardOptions = KeyboardOptions (),
64+ style : TextStyle = CodeTheme .typography.body1,
65+ placeholderStyle : TextStyle = CodeTheme .typography.body1,
66+ shape : Shape = CodeTheme .shapes.extraSmall,
67+ colors : TextFieldColors = inputColors(),
68+ enabled : Boolean = true,
69+ readOnly : Boolean = false,
70+ leadingIcon : (@Composable () -> Unit )? = null,
71+ trailingIcon : (@Composable () -> Unit )? = null,
72+ scrollState : ScrollState = rememberScrollState(),
73+ ) {
74+ val backgroundColor by colors.backgroundColor(enabled = enabled)
75+ val textColor by colors.textColor(enabled = enabled)
76+ val placeholderColor by colors.placeholderColor(enabled = enabled)
77+ BasicTextField2 (
78+ modifier = modifier
79+ .background(backgroundColor, shape)
80+ .defaultMinSize(minHeight = 56 .dp),
81+ enabled = enabled,
82+ readOnly = readOnly,
83+ state = state,
84+ cursorBrush = SolidColor (colors.cursorColor(isError = false ).value),
85+ keyboardOptions = keyboardOptions,
86+ keyboardActions = keyboardActions,
87+ textStyle = style.copy(color = textColor),
88+ lineLimits = if (maxLines == 1 ) {
89+ TextFieldLineLimits .SingleLine
90+ } else {
91+ TextFieldLineLimits .MultiLine (minHeightInLines = minLines, maxHeightInLines = maxLines)
92+ },
93+ decorator = {
94+ DecoratorBox (
95+ state = state,
96+ placeholder = placeholder,
97+ placeholderStyle = placeholderStyle,
98+ placeholderColor = placeholderColor,
99+ leadingIcon = leadingIcon,
100+ trailingIcon = trailingIcon,
101+ shape = shape,
102+ innerTextField = it
103+ )
104+ },
105+ scrollState = scrollState
106+ )
107+
108+ LaunchedEffect (Unit ) {
109+ state.textAsFlow()
110+ .onEach { onStateChanged() }
111+ .launchIn(this )
112+ }
113+
114+ val focusManager = LocalFocusManager .current
115+ val keyboardState by keyboardAsState()
116+ LaunchedEffect (keyboardState) {
117+ if (! keyboardState) {
118+ focusManager.clearFocus(true )
119+ }
120+ }
121+ }
122+
123+ @OptIn(ExperimentalFoundationApi ::class )
124+ @Composable
125+ fun SecureTextInput (
126+ modifier : Modifier = Modifier ,
127+ placeholder : String = "",
128+ state : TextFieldState ,
129+ onStateChanged : () -> Unit = { },
130+ style : TextStyle = CodeTheme .typography.subtitle1,
131+ placeholderStyle : TextStyle = CodeTheme .typography.subtitle1,
132+ shape : Shape = RectangleShape ,
133+ colors : TextFieldColors = inputColors(),
134+ enabled : Boolean = true,
135+ leadingIcon : (@Composable () -> Unit )? = null,
136+ trailingIcon : (@Composable () -> Unit )? = null,
137+ scrollState : ScrollState = rememberScrollState(),
138+ textObfuscationMode : TextObfuscationMode = TextObfuscationMode .RevealLastTyped ,
139+ onSubmit : () -> Unit = { },
140+ ) {
141+ val backgroundColor by colors.backgroundColor(enabled = enabled)
142+ val textColor by colors.textColor(enabled = enabled)
143+ val placeholderColor by colors.placeholderColor(enabled = enabled)
144+
145+ BasicSecureTextField (
146+ modifier = modifier
147+ .background(backgroundColor, shape)
148+ .defaultMinSize(minHeight = 56 .dp),
149+ enabled = enabled,
150+ state = state,
151+ textStyle = style.copy(color = textColor),
152+ textObfuscationMode = textObfuscationMode,
153+ onSubmit = {
154+ if (it == ImeAction .Go ) {
155+ onSubmit()
156+ return @BasicSecureTextField true
157+ }
158+ false
159+ },
160+ decorator = {
161+ DecoratorBox (
162+ state = state,
163+ placeholder = placeholder,
164+ placeholderStyle = placeholderStyle,
165+ placeholderColor = placeholderColor,
166+ leadingIcon = leadingIcon,
167+ trailingIcon = trailingIcon,
168+ shape = shape,
169+ innerTextField = it
170+ )
171+ },
172+ scrollState = scrollState
173+ )
174+
175+ LaunchedEffect (Unit ) {
176+ state.textAsFlow()
177+ .onEach { onStateChanged() }
178+ .launchIn(this )
179+ }
180+
181+ val focusManager = LocalFocusManager .current
182+ val keyboardState by keyboardAsState()
183+ LaunchedEffect (keyboardState) {
184+ if (! keyboardState) {
185+ focusManager.clearFocus(true )
186+ }
187+ }
188+ }
189+
190+ @OptIn(ExperimentalFoundationApi ::class )
191+ @Composable
192+ private fun DecoratorBox (
193+ state : TextFieldState ,
194+ placeholder : String ,
195+ placeholderStyle : TextStyle ,
196+ placeholderColor : Color ,
197+ leadingIcon : (@Composable () -> Unit )? ,
198+ trailingIcon : (@Composable () -> Unit )? ,
199+ shape : Shape ,
200+ innerTextField : @Composable () -> Unit ,
201+
202+ ) {
203+ Row (
204+ modifier = Modifier
205+ .fillMaxWidth()
206+ .border(
207+ width = CodeTheme .dimens.border,
208+ color = BrandLight ,
209+ shape = shape,
210+ ),
211+ verticalAlignment = Alignment .CenterVertically ,
212+ horizontalArrangement = Arrangement .spacedBy(CodeTheme .dimens.staticGrid.x2)
213+ ) {
214+ leadingIcon?.invoke()
215+ Box (
216+ modifier = Modifier
217+ .weight(1f )
218+ .padding(horizontal = CodeTheme .dimens.staticGrid.x2),
219+ contentAlignment = Alignment .CenterStart
220+ ) {
221+ innerTextField()
222+ if (state.text.isEmpty() && placeholder.isNotEmpty()) {
223+ Text (
224+ text = placeholder,
225+ style = placeholderStyle.copy(color = placeholderColor),
226+ maxLines = 1 ,
227+ )
228+ }
229+ }
230+ trailingIcon?.invoke()
231+ }
232+ }
233+
234+ @Composable
235+ fun keyboardAsState (): State <Boolean > {
236+ val keyboardState = remember { mutableStateOf(false ) }
237+ val view = LocalView .current
238+ val viewTreeObserver = view.viewTreeObserver
239+ DisposableEffect (viewTreeObserver) {
240+ val listener = ViewTreeObserver .OnGlobalLayoutListener {
241+ keyboardState.value = ViewCompat .getRootWindowInsets(view)
242+ ?.isVisible(WindowInsetsCompat .Type .ime()) ? : true
243+ }
244+ viewTreeObserver.addOnGlobalLayoutListener(listener)
245+ onDispose { viewTreeObserver.removeOnGlobalLayoutListener(listener) }
246+ }
247+ return keyboardState
248+ }
0 commit comments