Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions app/src/main/java/com/getcode/view/components/TextInput.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package com.getcode.view.components

import android.view.ViewTreeObserver
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text2.BasicSecureTextField
import androidx.compose.foundation.text2.BasicTextField2
import androidx.compose.foundation.text2.input.TextFieldLineLimits
import androidx.compose.foundation.text2.input.TextFieldState
import androidx.compose.foundation.text2.input.TextObfuscationMode
import androidx.compose.foundation.text2.input.textAsFlow
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.material.TextFieldColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
import com.getcode.theme.extraSmall
import com.getcode.theme.inputColors
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TextInput(
modifier: Modifier = Modifier,
placeholder: String = "",
minLines: Int = 1,
maxLines: Int = 4,
state: TextFieldState,
onStateChanged: () -> Unit = { },
keyboardActions: KeyboardActions = KeyboardActions(),
keyboardOptions: KeyboardOptions = KeyboardOptions(),
style: TextStyle = CodeTheme.typography.body1,
placeholderStyle: TextStyle = CodeTheme.typography.body1,
shape: Shape = CodeTheme.shapes.extraSmall,
colors: TextFieldColors = inputColors(),
enabled: Boolean = true,
readOnly: Boolean = false,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
scrollState: ScrollState = rememberScrollState(),
) {
val backgroundColor by colors.backgroundColor(enabled = enabled)
val textColor by colors.textColor(enabled = enabled)
val placeholderColor by colors.placeholderColor(enabled = enabled)
BasicTextField2(
modifier = modifier
.background(backgroundColor, shape)
.defaultMinSize(minHeight = 56.dp),
enabled = enabled,
readOnly = readOnly,
state = state,
cursorBrush = SolidColor(colors.cursorColor(isError = false).value),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
textStyle = style.copy(color = textColor),
lineLimits = if (maxLines == 1) {
TextFieldLineLimits.SingleLine
} else {
TextFieldLineLimits.MultiLine(minHeightInLines = minLines, maxHeightInLines = maxLines)
},
decorator = {
DecoratorBox(
state = state,
placeholder = placeholder,
placeholderStyle = placeholderStyle,
placeholderColor = placeholderColor,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
shape = shape,
innerTextField = it
)
},
scrollState = scrollState
)

LaunchedEffect(Unit) {
state.textAsFlow()
.onEach { onStateChanged() }
.launchIn(this)
}

val focusManager = LocalFocusManager.current
val keyboardState by keyboardAsState()
LaunchedEffect(keyboardState) {
if (!keyboardState) {
focusManager.clearFocus(true)
}
}
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SecureTextInput(
modifier: Modifier = Modifier,
placeholder: String = "",
state: TextFieldState,
onStateChanged: () -> Unit = { },
style: TextStyle = CodeTheme.typography.subtitle1,
placeholderStyle: TextStyle = CodeTheme.typography.subtitle1,
shape: Shape = RectangleShape,
colors: TextFieldColors = inputColors(),
enabled: Boolean = true,
leadingIcon: (@Composable () -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
scrollState: ScrollState = rememberScrollState(),
textObfuscationMode: TextObfuscationMode = TextObfuscationMode.RevealLastTyped,
onSubmit: () -> Unit = { },
) {
val backgroundColor by colors.backgroundColor(enabled = enabled)
val textColor by colors.textColor(enabled = enabled)
val placeholderColor by colors.placeholderColor(enabled = enabled)

BasicSecureTextField(
modifier = modifier
.background(backgroundColor, shape)
.defaultMinSize(minHeight = 56.dp),
enabled = enabled,
state = state,
textStyle = style.copy(color = textColor),
textObfuscationMode = textObfuscationMode,
onSubmit = {
if (it == ImeAction.Go) {
onSubmit()
return@BasicSecureTextField true
}
false
},
decorator = {
DecoratorBox(
state = state,
placeholder = placeholder,
placeholderStyle = placeholderStyle,
placeholderColor = placeholderColor,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
shape = shape,
innerTextField = it
)
},
scrollState = scrollState
)

LaunchedEffect(Unit) {
state.textAsFlow()
.onEach { onStateChanged() }
.launchIn(this)
}

val focusManager = LocalFocusManager.current
val keyboardState by keyboardAsState()
LaunchedEffect(keyboardState) {
if (!keyboardState) {
focusManager.clearFocus(true)
}
}
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun DecoratorBox(
state: TextFieldState,
placeholder: String,
placeholderStyle: TextStyle,
placeholderColor: Color,
leadingIcon: (@Composable () -> Unit)?,
trailingIcon: (@Composable () -> Unit)?,
shape: Shape,
innerTextField: @Composable () -> Unit,

) {
Row(
modifier = Modifier
.fillMaxWidth()
.border(
width = CodeTheme.dimens.border,
color = BrandLight,
shape = shape,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.staticGrid.x2)
) {
leadingIcon?.invoke()
Box(
modifier = Modifier
.weight(1f)
.padding(horizontal = CodeTheme.dimens.staticGrid.x2),
contentAlignment = Alignment.CenterStart
) {
innerTextField()
if (state.text.isEmpty() && placeholder.isNotEmpty()) {
Text(
text = placeholder,
style = placeholderStyle.copy(color = placeholderColor),
maxLines = 1,
)
}
}
trailingIcon?.invoke()
}
}

@Composable
fun keyboardAsState(): State<Boolean> {
val keyboardState = remember { mutableStateOf(false) }
val view = LocalView.current
val viewTreeObserver = view.viewTreeObserver
DisposableEffect(viewTreeObserver) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
keyboardState.value = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) ?: true
}
viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
return keyboardState
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package com.getcode.view.main.account.withdraw

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
Expand All @@ -20,21 +22,20 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import com.getcode.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.navigation.screens.WithdrawalArgs
import com.getcode.theme.BrandLight
import com.getcode.theme.CodeTheme
import com.getcode.theme.extraSmall
import com.getcode.theme.green
import com.getcode.theme.inputColors
import com.getcode.util.debugBounds
import com.getcode.view.components.ButtonState
import com.getcode.view.components.CodeButton
import com.getcode.view.components.TextInput

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AccountWithdrawAddress(
viewModel: AccountWithdrawAddressViewModel,
Expand All @@ -43,59 +44,33 @@ fun AccountWithdrawAddress(
val navigator = LocalCodeNavigator.current
val dataState by viewModel.uiFlow.collectAsState()

ConstraintLayout(
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(horizontal = CodeTheme.dimens.inset)
.imePadding()
) {
val (topText, addressField, resolveStatus, pasteButton, nextButton) = createRefs()
Text(
modifier = Modifier.constrainAs(topText) {
start.linkTo(parent.start)
end.linkTo(parent.end)
},
modifier = Modifier
.fillMaxWidth(),
text = stringResource(R.string.subtitle_whereToWithdrawKin),
style = CodeTheme.typography.body1.copy(textAlign = TextAlign.Center),
color = BrandLight
)

OutlinedTextField(
TextInput(
modifier = Modifier
.constrainAs(addressField) {
top.linkTo(topText.bottom)
}
.padding(top = CodeTheme.dimens.grid.x4)
.fillMaxWidth()
.padding(vertical = CodeTheme.dimens.grid.x1),
placeholder = {
Text(
text = stringResource(R.string.subtitle_enterDestinationAddress),
style = CodeTheme.typography.subtitle1.copy(
fontSize = 16.sp,
)
)
},
.padding(vertical = CodeTheme.dimens.grid.x4),
state = dataState.addressText,
maxLines = 1,
placeholder = stringResource(R.string.subtitle_enterDestinationAddress),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
visualTransformation = VisualTransformation.None,
value = dataState.addressText,
onValueChange = { viewModel.setAddress(it) },
textStyle = CodeTheme.typography.subtitle1.copy(
fontSize = 16.sp,
),
singleLine = true,
colors = inputColors(),
shape = CodeTheme.shapes.extraSmall
)

Row(
modifier = Modifier
.constrainAs(resolveStatus) {
top.linkTo(addressField.bottom)
}
.fillMaxWidth()
.padding(vertical = CodeTheme.dimens.grid.x1),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
dataState.isValid?.let { isValid ->
Expand Down Expand Up @@ -129,25 +104,19 @@ fun AccountWithdrawAddress(
}

CodeButton(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = CodeTheme.dimens.inset)
.constrainAs(pasteButton) {
top.linkTo(resolveStatus.bottom)
},
modifier = Modifier.fillMaxWidth()
.padding(top = CodeTheme.dimens.grid.x2),
onClick = { viewModel.pasteAddress() },
enabled = dataState.isPasteEnabled,
text = stringResource(R.string.action_pasteFromClipboard),
buttonState = ButtonState.Filled,
)

Spacer(modifier = Modifier.weight(1f))
CodeButton(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = CodeTheme.dimens.inset)
.constrainAs(nextButton) {
bottom.linkTo(parent.bottom)
},
.padding(bottom = CodeTheme.dimens.inset),
onClick = {
viewModel.onSubmit(navigator, arguments)
},
Expand Down
Loading