From fcf3237c595956aac041dcd74e79590506ac8961 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Apr 2022 19:08:34 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Button.kt | 69 ++++++++ .../common/compose/ComposableTextDebouncer.kt | 26 +++ .../tap/common/compose/OutlinedSpinner.kt | 78 +++++++++ .../common/compose/OutlinedTextFieldWidget.kt | 153 ++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/Button.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt new file mode 100644 index 0000000000..dc5d278520 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -0,0 +1,69 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.height +import androidx.compose.material.Button +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** +[REDACTED_AUTHOR] + */ +private class Button {} + +@Composable +fun Button( + text: String = "", + textId: Int? = null, + modifier: Modifier = Modifier, + leftContent: @Composable RowScope.() -> Unit = {}, + rightContent: @Composable RowScope.() -> Unit = {}, + onClick: () -> Unit, +) { + Button( + modifier = modifier.height(42.dp), + onClick = onClick, + ) { + leftContent() + ButtonText(text = textId?.let { stringResource(id = it) } ?: text) + rightContent() + } +} + +@Composable +fun ButtonText( + text: String, + modifier: Modifier = Modifier +) { + Text( + text, + modifier = modifier, + maxLines = 1, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + ) + ) +} + +@Preview +@Composable +fun ButtonTest() { + Scaffold { + Button( + "Some button", + onClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt new file mode 100644 index 0000000000..7420dce292 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.common.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.tangem.domain.common.ValueDebouncer + +/** +[REDACTED_AUTHOR] + * This is an empty compose view. It just remember the ValueDebouncer inside of itself. + */ +@Composable +fun ComposableTextDebouncer( + text: String, + debounce: Long = 400, + onTextChanged: (String) -> Unit +): ValueDebouncer { + val rTextDebounce = remember { + mutableStateOf(ValueDebouncer(text, debounce) { changedValue -> + changedValue?.let { onTextChanged(it) } + }) + } + rTextDebounce.value.value = text + + return rTextDebounce.value +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt new file mode 100644 index 0000000000..7be01ec1d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt @@ -0,0 +1,78 @@ +package com.tangem.tap.common.compose + +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.VoidCallback +import com.tangem.tap.common.extensions.ValueCallback + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun OutlinedSpinner( + title: String, + itemList: List, + selectedItem: T, + onItemSelected: ValueCallback, + modifier: Modifier = Modifier, + itemNameConverter: (T) -> String = { it.toString() }, + onClose: VoidCallback = {} +) { + val rSelectedItem = remember { mutableStateOf(selectedItem) } + val rIsExpanded = remember { mutableStateOf(false) } + + val onItemSelectedInternal: (T) -> Unit = { + rSelectedItem.value = it + rIsExpanded.value = false + onItemSelected(it) + } + val onDismissRequest = { + rIsExpanded.value = false + onClose() + } + + ExposedDropdownMenuBox( + expanded = rIsExpanded.value, + onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, + ) { + OutlinedTextField( + modifier = modifier, + readOnly = true, + value = itemNameConverter(rSelectedItem.value), + onValueChange = {}, + label = { Text(title) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, + ) + ExposedDropdownMenu( + expanded = rIsExpanded.value, + onDismissRequest = onDismissRequest, + ) { + itemList.forEach { item -> + DropdownMenuItem( + onClick = { onItemSelectedInternal(item) } + ) { + Text(itemNameConverter(item)) + } + } + } + } +} + +@Preview +@Composable +fun TestSpinnerPreview(){ + Scaffold() { + OutlinedSpinner( + title = "Blockchain name", + itemList = listOf(Blockchain.values()), + selectedItem = Blockchain.Avalanche, + onItemSelected = {}, + ) + } +} diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt new file mode 100644 index 0000000000..8ed5cbb2d9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -0,0 +1,153 @@ +package com.tangem.tap.common.compose + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.domain.common.DomainError +import com.tangem.domain.common.ErrorConverter + +/** +[REDACTED_AUTHOR] + */ +private class OutlinedTextFieldWidget + +@Composable +fun OutlinedTextFieldWidget( + text: String, + modifier: Modifier = Modifier, + labelId: Int? = null, + label: String = "", + placeholderId: Int? = null, + placeholder: String = "", + isEnabled: Boolean = true, + error: DomainError? = null, + errorConverter: ErrorConverter? = null, + debounceTextChanges: Long = 400, + onTextChanged: (String) -> Unit, +) { + val placeholder = placeholderId?.let { stringResource(id = it) } ?: placeholder + val label = labelId?.let { stringResource(id = it) } ?: label + + val rTextValue = remember { mutableStateOf(text) } + val textDebouncer = ComposableTextDebouncer(text, debounceTextChanges, onTextChanged) + + Column( + modifier = modifier.animateContentSize(), + ) { + OutlinedTextField( + value = rTextValue.value, + onValueChange = { + rTextValue.value = it + textDebouncer.emmit(it) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(label) }, + placeholder = { Text(placeholder) }, + singleLine = true, + enabled = isEnabled, + isError = error != null, + ) + errorConverter?.let { TextFieldErrorWidget(error, it) } + } +} + +@Composable +fun TextFieldErrorWidget( + error: DomainError? = null, + errorConverter: ErrorConverter, +) { + AnimatedVisibility( + visible = error != null, + enter = fadeIn() + slideInVertically(), + exit = slideOutVertically() + fadeOut(), + ) { + ErrorView( + errorConverter.convertError(error!!), + style = TextStyle( + fontSize = 14.sp + ) + ) + } +} + +@Composable +fun ErrorView( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current +) { + Text( + text, + color = MaterialTheme.colors.error, + modifier = modifier, + style = style + ) +} + + +@Preview +@Composable +fun OutlinedTextFieldWithErrorTest() { + val converter = remember { + object : ErrorConverter { + override fun convertError(error: DomainError): String { + return "Hello, i'am the error: ${error::class.java.simpleName}" + } + + } + } + + class SimpleError( + override val code: Int = 1, + override val message: String = "Error message", + override val data: Any? = null, + ) : DomainError + + val modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + Scaffold( + ) { + Column( + ) { + OutlinedTextFieldWidget( + modifier = modifier, + text = "", + label = "First label", + placeholder = "1 placeholder", + error = null, + errorConverter = converter, + onTextChanged = {}, + ) + OutlinedTextFieldWidget( + modifier = modifier, + text = "First", + label = "First label", + placeholder = "1 placeholder", + error = null, + errorConverter = converter, + onTextChanged = {}, + ) + OutlinedTextFieldWidget( + modifier = modifier, + text = "First", + label = "First label", + placeholder = "1 placeholder", + error = SimpleError(), + errorConverter = converter, + onTextChanged = {}, + ) + } + } +} \ No newline at end of file