Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-12 16:51:32 +03:00
parent 4fc058cd2d
commit a73558501c
16 changed files with 849 additions and 96 deletions

View file

@ -1,71 +1,115 @@
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.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.painterResource
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.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.extensions.stringResourceDefault
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
private class Button {}
@Composable
fun Button(
modifier: Modifier = Modifier,
text: String = "",
textId: Int? = null,
isEnabled: Boolean = true,
modifier: Modifier = Modifier,
leftContent: @Composable RowScope.() -> Unit = {},
rightContent: @Composable RowScope.() -> Unit = {},
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
leadingView: @Composable RowScope.() -> Unit = {},
middleView: @Composable RowScope.() -> Unit = { TextInButton(text = text, textId = textId) },
trailingView: @Composable RowScope.() -> Unit = {},
onClick: () -> Unit,
) {
Button(
modifier = modifier.height(42.dp),
modifier = modifier,
contentPadding = contentPadding,
enabled = isEnabled,
onClick = onClick,
) {
leftContent()
ButtonText(text = textId?.let { stringResource(id = it) } ?: text)
rightContent()
leadingView()
middleView()
trailingView()
}
}
@Composable
fun ButtonText(
text: String,
modifier: Modifier = Modifier
private fun TextInButton(
modifier: Modifier = Modifier,
text: String = "",
textId: Int? = null,
) {
Text(
text,
modifier = modifier,
text = stringResourceDefault(textId, text),
maxLines = 1,
style = TextStyle(
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
)
)
}
@Composable
fun PasteButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
dpSize: DpSize = DpSize(40.dp, 40.dp),
onClick: () -> Unit,
content: @Composable (() -> Unit)? = null
) {
IconButton(
modifier = modifier.size(dpSize),
enabled = enabled,
onClick = onClick,
) {
when (content) {
null -> {
val icon = if (enabled) R.drawable.ic_paste else R.drawable.ic_paste_disabled
Icon(painterResource(id = icon), contentDescription = "Paste")
}
else -> content()
}
}
}
@Preview
@Composable
fun ButtonTest() {
Scaffold {
Button(
"Some button",
onClick = {}
)
Scaffold(
) {
Column(modifier = Modifier.padding(16.dp)) {
PreviewItem("Button") {
Button(text = "Some button") {}
}
PreviewItem("PasteButton") {
PasteButton(onClick = {})
}
}
}
}
@Composable
fun PreviewItem(
name: String,
content: @Composable RowScope.() -> Unit,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
modifier = Modifier.weight(1f),
text = name,
)
content()
}
SpacerH8()
}

View file

@ -1,26 +1,24 @@
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
import com.tangem.domain.common.util.ValueDebouncer
/**
[REDACTED_AUTHOR]
* This is an empty compose view. It just remember the ValueDebouncer inside of itself.
*/
@Composable
fun ComposableTextDebouncer(
text: String,
fun <T> valueDebouncerAsState(
debounce: Long = 400,
onTextChanged: (String) -> Unit
): ValueDebouncer<String> {
val rTextDebounce = remember {
mutableStateOf(ValueDebouncer(text, debounce) { changedValue ->
changedValue?.let { onTextChanged(it) }
})
onValueChanged: (T) -> Unit
): ValueDebouncer<T> {
return remember {
ValueDebouncer(
debounce = debounce,
onValueChanged = { changedValue ->
changedValue?.let { onValueChanged(it) }
},
)
}
rTextDebounce.value.value = text
return rTextDebounce.value
}

View file

@ -0,0 +1,125 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.domain.DomainDialog
import com.tangem.domain.DomainStateDialog
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import org.rekotlin.StoreSubscriber
@Composable
fun ComposeDialogManager() {
val dialogSate = remember { mutableStateOf<DomainStateDialog?>(null) }
val subscriber = remember {
object : StoreSubscriber<DomainGlobalState> {
override fun newState(state: DomainGlobalState) {
dialogSate.value = state.dialog
}
}
}
ShowTheDialog(dialogSate)
LaunchedEffect(key1 = Unit, block = {
domainStore.subscribe(subscriber) { state ->
state.skipRepeats { oldState, newState ->
oldState.globalState == newState.globalState
}.select { it.globalState }
}
})
DisposableEffect(key1 = Unit, effect = {
onDispose { domainStore.unsubscribe(subscriber) }
})
}
@Composable
fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
if (dialogState.value == null) return
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
when (val dialog = dialogState.value) {
is DomainDialog.SelectTokenDialog -> {
SimpleDialog(
title = "Select a token",
items = dialog.items,
itemNameConverter = dialog.itemNameConverter,
onSelect = dialog.onSelect,
onDismissRequest = onDismissRequest
)
}
}
}
/**
* Dialog with single item selection
*/
@Composable
fun <T> SimpleDialog(
title: String,
items: List<T>,
itemNameConverter: (T) -> String,
onSelect: (T) -> Unit,
onDismissRequest: () -> Unit
) {
Dialog(
properties = DialogProperties(false, false),
onDismissRequest = { }
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp)
) {
Column(
modifier = Modifier.padding(22.dp)
) {
Text(
text = title,
style = LocalTextStyle.provides(
TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
).value
)
SpacerH16()
LazyColumn() {
items(items) { item ->
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable {
onSelect(item)
onDismissRequest()
},
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = itemNameConverter(item),
)
}
}
}
}
}
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.tap.common.compose
import android.graphics.Rect
import android.view.ViewTreeObserver
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView
/**
[REDACTED_AUTHOR]
*/
@Composable
fun keyboardObserverAsState(): State<Keyboard> {
val keyboardState: MutableState<Keyboard> = remember { mutableStateOf(Keyboard.Closed) }
val view = LocalView.current
DisposableEffect(view) {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
Keyboard.Opened(keypadHeight)
} else {
Keyboard.Closed
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
}
sealed class Keyboard {
data class Opened(val height: Int) : Keyboard()
object Closed : Keyboard()
}

View file

@ -1,11 +1,16 @@
package com.tangem.tap.common.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
/**
[REDACTED_AUTHOR]
@ -22,4 +27,15 @@ fun ErrorView(
modifier = modifier,
style = style
)
}
@Preview
@Composable
fun ErrorViewTest() {
Scaffold(
) {
Box(Modifier.padding(16.dp)) {
ErrorView(text = "Some error description")
}
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.runtime.Composable
import timber.log.Timber
/**
* Simple logger for all recompositions
*/
@Composable
fun LogSideEffect(message: String) {
Timber.d("SideEffect: $message")
}

View file

@ -8,24 +8,31 @@ 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.domain.common.form.Field
import com.tangem.tap.common.extensions.ValueCallback
/**
[REDACTED_AUTHOR]
*/
private class OutlinedSpinner
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun <T> OutlinedSpinner(
modifier: Modifier = Modifier,
title: String,
itemList: List<T>,
selectedItem: T,
selectedItem: Field.Data<T>,
onItemSelected: ValueCallback<T>,
modifier: Modifier = Modifier,
itemNameConverter: (T) -> String = { it.toString() },
isEnabled: Boolean = true,
onClose: VoidCallback = {}
) {
val rSelectedItem = remember { mutableStateOf(selectedItem) }
val rIsExpanded = remember { mutableStateOf(false) }
val rSelectedItem = remember { mutableStateOf(selectedItem.value) }
if (!selectedItem.isUserInput) {
rSelectedItem.value = selectedItem.value
}
val onItemSelectedInternal: (T) -> Unit = {
rSelectedItem.value = it
@ -44,6 +51,7 @@ fun <T> OutlinedSpinner(
OutlinedTextField(
modifier = modifier,
readOnly = true,
enabled = isEnabled,
value = itemNameConverter(rSelectedItem.value),
onValueChange = {},
label = { Text(title) },
@ -66,12 +74,12 @@ fun <T> OutlinedSpinner(
@Preview
@Composable
fun TestSpinnerPreview(){
fun TestSpinnerPreview() {
Scaffold() {
OutlinedSpinner(
title = "Blockchain name",
itemList = listOf(Blockchain.values()),
selectedItem = Blockchain.Avalanche,
selectedItem = Field.Data(Blockchain.Avalanche),
onItemSelected = {},
)
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
@ -14,14 +15,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
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
import com.tangem.domain.DomainError
import com.tangem.domain.ErrorConverter
import com.tangem.domain.common.form.Field
import com.tangem.tap.common.compose.extensions.stringResourceDefault
/**
[REDACTED_AUTHOR]
@ -30,8 +32,8 @@ private class OutlinedTextFieldWidget
@Composable
fun OutlinedTextFieldWidget(
text: String,
modifier: Modifier = Modifier,
textFieldData: Field.Data<String>,
labelId: Int? = null,
label: String = "",
placeholderId: Int? = null,
@ -44,62 +46,66 @@ fun OutlinedTextFieldWidget(
errorConverter: ErrorConverter<String>? = null,
debounceTextChanges: Long = 400,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
onTextChanged: (String) -> Unit,
) {
if (!isVisible) return
val placeholder = placeholderId?.let { stringResource(id = it) } ?: placeholder
val label = labelId?.let { stringResource(id = it) } ?: label
Column(
modifier = modifier.animateContentSize(),
) {
OutlinedProgressTextField(
text = text,
modifier = modifier,
label = label,
placeholder = placeholder,
textFieldData = textFieldData,
label = stringResourceDefault(labelId, label),
placeholder = stringResourceDefault(placeholderId, placeholder),
trailingIcon = trailingIcon,
isEnabled = isEnabled,
isLoading = isLoading,
error = error,
debounceTextChanges = debounceTextChanges,
debounce = debounceTextChanges,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
onTextChanged = onTextChanged
)
errorConverter?.let { TextFieldErrorWidget(error, it) }
errorConverter?.let { AnimatedErrorView(error, it) }
}
}
@Composable
private fun OutlinedProgressTextField(
text: String,
modifier: Modifier = Modifier,
textFieldData: Field.Data<String>,
label: String = "",
placeholder: String = "",
isEnabled: Boolean = true,
isLoading: Boolean = false,
error: DomainError? = null,
debounceTextChanges: Long = 400,
debounce: Long = 400,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
trailingIcon: @Composable (() -> Unit)? = null,
onTextChanged: (String) -> Unit,
) {
val rTextValue = remember { mutableStateOf(text) }
val textDebouncer = ComposableTextDebouncer(text, debounceTextChanges, onTextChanged)
val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged)
val rText = remember { mutableStateOf(textFieldData.value) }
// add ability to paste text from state
if (rTextValue.value != text) rTextValue.value = text
fun updateFieldValueAndEmmit(value: String){
rText.value = value
rTextDebouncer.emmit(value)
}
// This action came from redux. Update the field value and send a new event as if from the user
if (!textFieldData.isUserInput) {
updateFieldValueAndEmmit(textFieldData.value)
}
Box {
OutlinedTextField(
value = rTextValue.value,
onValueChange = {
// immediately change text for the OutlinedTextField
rTextValue.value = it
textDebouncer.emmit(it)
},
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth(),
value = rText.value,
onValueChange = ::updateFieldValueAndEmmit,
keyboardOptions = keyboardOptions,
label = { Text(label) },
placeholder = { Text(placeholder) },
trailingIcon = trailingIcon,
@ -120,7 +126,7 @@ private fun OutlinedProgressTextField(
}
@Composable
private fun TextFieldErrorWidget(
private fun AnimatedErrorView(
error: DomainError? = null,
errorConverter: ErrorConverter<String>,
) {
@ -162,41 +168,37 @@ fun OutlinedTextFieldWithErrorTest() {
) {
OutlinedTextFieldWidget(
modifier = modifier,
text = "",
textFieldData = Field.Data(""),
label = "First label",
placeholder = "1 placeholder",
error = null,
errorConverter = converter,
onTextChanged = {},
)
) {}
OutlinedTextFieldWidget(
modifier = modifier,
text = "First",
textFieldData = Field.Data("First"),
label = "First label",
placeholder = "1 placeholder",
error = null,
errorConverter = converter,
onTextChanged = {},
)
) {}
OutlinedTextFieldWidget(
modifier = modifier,
text = "First",
textFieldData = Field.Data("First"),
label = "First label",
placeholder = "1 placeholder",
isLoading = true,
error = null,
errorConverter = converter,
onTextChanged = {},
)
) {}
OutlinedTextFieldWidget(
modifier = modifier,
text = "First",
textFieldData = Field.Data("First"),
label = "First label",
placeholder = "1 placeholder",
error = SimpleError(),
errorConverter = converter,
onTextChanged = {},
)
) {}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.common.extensions.compose
package com.tangem.tap.common.compose.extensions
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb

View file

@ -0,0 +1,21 @@
package com.tangem.tap.common.compose.extensions
import android.content.res.Resources
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
/**
[REDACTED_AUTHOR]
*/
@Composable
fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String {
val resources = LocalContext.current.resources
return try {
resources.getString(requireNotNull(id))
} catch (ex: Resources.NotFoundException) {
default
} catch (ex: IllegalArgumentException) {
default
}
}