Updated on 2026-08-14
This commit is contained in:
parent
4fc058cd2d
commit
a73558501c
16 changed files with 849 additions and 96 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
|||
import com.tangem.tangem_sdk_new.extensions.dpToPx
|
||||
import com.tangem.tap.common.compose.SpacerS16
|
||||
import com.tangem.tap.common.compose.SpacerS24
|
||||
import com.tangem.tap.common.extensions.compose.toAndroidGraphicsColor
|
||||
import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.google.accompanist.appcompattheme.AppCompatTheme
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.tokens.addCustomToken.compose.AddCustomTokenScreen
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber<AddCustomTokenState> {
|
||||
|
||||
private var state: MutableState<AddCustomTokenState> = mutableStateOf(domainStore.state.addCustomTokensState)
|
||||
|
||||
override fun subscribeToStore() {
|
||||
domainStore.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.addCustomTokensState == newState.addCustomTokensState
|
||||
}.select { it.addCustomTokensState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun newState(state: AddCustomTokenState) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
this.state.value = state
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
view.findViewById<Toolbar>(R.id.toolbar)?.let {
|
||||
it.setTitle(R.string.add_custom_token_title)
|
||||
}
|
||||
|
||||
view.findViewById<ComposeView>(R.id.view_compose)?.setContent {
|
||||
AppCompatTheme(requireContext()) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
AddCustomTokenScreen(state)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// addBackPressHandler(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.domain.DomainError
|
||||
import com.tangem.domain.ErrorConverter
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class CustomTokenErrorConverter(
|
||||
private val context: Context
|
||||
) : ErrorConverter<String> {
|
||||
|
||||
override fun convertError(error: DomainError): String {
|
||||
val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException()
|
||||
|
||||
val resId = when (customTokenError) {
|
||||
AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address
|
||||
AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected
|
||||
AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path
|
||||
AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields
|
||||
else -> null
|
||||
}
|
||||
return resId?.let { context.getString(it) } ?: "Unknown error: ${customTokenError::class.java.simpleName}"
|
||||
}
|
||||
}
|
||||
|
||||
class CustomTokenWarningConverter(
|
||||
private val context: Context
|
||||
) : ErrorConverter<String> {
|
||||
|
||||
override fun convertError(error: DomainError): String {
|
||||
val customTokenWarning = (error as? AddCustomTokenWarning) ?: throw UnsupportedOperationException()
|
||||
|
||||
val resId = when (customTokenWarning) {
|
||||
AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found
|
||||
AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken.compose
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.domain.ErrorConverter
|
||||
import com.tangem.domain.common.form.DataField
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ScreenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ViewStates
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.ComposeDialogManager
|
||||
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
|
||||
import com.tangem.tap.common.compose.SpacerH8
|
||||
import com.tangem.tap.common.compose.keyboardObserverAsState
|
||||
import com.tangem.tap.features.tokens.addCustomToken.CustomTokenErrorConverter
|
||||
import com.tangem.tap.features.tokens.addCustomToken.CustomTokenWarningConverter
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
private class AddCustomTokenScreen {} // for simple search
|
||||
|
||||
@Composable
|
||||
fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>) {
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
backgroundColor = colorResource(id = R.color.backgroundLightGray),
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(bottom = 80.dp)
|
||||
) {
|
||||
item {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
elevation = 4.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
FormFields(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Warnings(state.value.warnings.toList()) }
|
||||
}
|
||||
HangingOverKeyboardView(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
keyboardState = keyboardObserverAsState(),
|
||||
defaultBottomPadding = 30.dp,
|
||||
spaceBetweenKeyboard = 20.dp,
|
||||
) {
|
||||
AddButton(
|
||||
isEnabled = state.value.screenState.addButton.isEnabled
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ComposeDialogManager()
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) })
|
||||
DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } })
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FormFields(state: MutableState<AddCustomTokenState>) {
|
||||
val context = LocalContext.current
|
||||
val errorConverter = remember { CustomTokenErrorConverter(context) }
|
||||
|
||||
state.value.form.fieldList.forEach { field ->
|
||||
val data = ScreenFieldData.fromState(field, state.value, errorConverter)
|
||||
when (field.id) {
|
||||
ContractAddress -> TokenContractAddressView(data)
|
||||
Network -> TokenNetworkView(data)
|
||||
Name -> TokenNameView(data)
|
||||
Symbol -> TokenSymbolView(data)
|
||||
Decimals -> TokenDecimalsView(data)
|
||||
DerivationPath -> TokenDerivationPathView(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_contract_address_input_title,
|
||||
placeholder = "0x0000000000000000",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
isLoading = screenFieldData.viewState.isLoading,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) {
|
||||
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenNameView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_name_input_title,
|
||||
placeholderId = R.string.custom_token_name_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) {
|
||||
domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenNetworkView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
|
||||
val networkField = screenFieldData.field as TokenNetworkField
|
||||
|
||||
TokenNetworkSpinner(
|
||||
title = R.string.custom_token_network_input_title,
|
||||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
itemNameConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) },
|
||||
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSymbolView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_token_symbol_input_title,
|
||||
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) { domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_decimals_input_title,
|
||||
placeholder = "8",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenDerivationPathView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
|
||||
val networkField = screenFieldData.field as TokenDerivationPathField
|
||||
|
||||
TokenNetworkSpinner(
|
||||
title = R.string.custom_token_network_input_title,
|
||||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
itemNameConverter = { AddCustomTokenState.convertDerivationPathName(it, notSelected) },
|
||||
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Warnings(warnings: List<AddCustomTokenWarning>) {
|
||||
if (warnings.isEmpty()) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val warningConverter = remember { CustomTokenWarningConverter(context) }
|
||||
|
||||
Column {
|
||||
warnings.forEachIndexed { index, item ->
|
||||
val modifier = when (index) {
|
||||
0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 16.dp)
|
||||
warnings.lastIndex -> Modifier.padding(16.dp, 16.dp, 16.dp, 16.dp)
|
||||
else -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp)
|
||||
}
|
||||
Surface(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = colorResource(id = R.color.darkGray2),
|
||||
contentColor = colorResource(id = R.color.darkGray3)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = warningConverter.convertError(item),
|
||||
color = colorResource(id = R.color.lightGray0),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class ScreenFieldData(
|
||||
val field: DataField<*>,
|
||||
val error: AddCustomTokenError?,
|
||||
val errorConverter: ErrorConverter<String>,
|
||||
val viewState: ViewStates.TokenField
|
||||
) {
|
||||
companion object {
|
||||
fun fromState(
|
||||
field: DataField<*>,
|
||||
state: AddCustomTokenState,
|
||||
errorConverter: CustomTokenErrorConverter
|
||||
): ScreenFieldData {
|
||||
return ScreenFieldData(
|
||||
field = field,
|
||||
error = state.getError(field.id),
|
||||
errorConverter = errorConverter,
|
||||
viewState = selectField(field.id, state.screenState)
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField {
|
||||
return when (id) {
|
||||
ContractAddress -> screenState.contractAddressField
|
||||
Network -> screenState.network
|
||||
Name -> screenState.name
|
||||
Symbol -> screenState.symbol
|
||||
Decimals -> screenState.decimals
|
||||
DerivationPath -> screenState.derivationPath
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken.compose
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.compose.Button
|
||||
import com.tangem.tap.common.compose.OutlinedSpinner
|
||||
import com.tangem.tap.common.extensions.ValueCallback
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TokenNetworkSpinner(
|
||||
title: Int,
|
||||
itemList: List<Blockchain>,
|
||||
selectedItem: Field.Data<Blockchain>,
|
||||
isEnabled: Boolean = true,
|
||||
itemNameConverter: (Blockchain) -> String,
|
||||
onItemSelected: ValueCallback<Blockchain>,
|
||||
) {
|
||||
|
||||
OutlinedSpinner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
title = stringResource(id = title),
|
||||
itemList = itemList,
|
||||
selectedItem = selectedItem,
|
||||
itemNameConverter = itemNameConverter,
|
||||
isEnabled = isEnabled,
|
||||
onItemSelected = onItemSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddButton(
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean,
|
||||
textId: Int = R.string.common_add,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
textId = textId,
|
||||
isEnabled = isEnabled,
|
||||
modifier = modifier
|
||||
.height(52.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
leadingView = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = "Add",
|
||||
)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken.compose
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.tangem_sdk_new.extensions.pxToDp
|
||||
import com.tangem.tap.common.compose.Keyboard
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun HangingOverKeyboardView(
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardState: State<Keyboard>,
|
||||
defaultBottomPadding: Dp = 0.dp,
|
||||
spaceBetweenKeyboard: Dp = 10.dp,
|
||||
calculateWithActionBarHeight: Boolean = true,
|
||||
content: @Composable() (BoxScope.() -> Unit)
|
||||
) {
|
||||
fun getActionBarHeight(context: Context): Int {
|
||||
val typedValue = TypedValue()
|
||||
return if (context.theme.resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) {
|
||||
val data = typedValue.data
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
TypedValue.complexToDimensionPixelSize(data, displayMetrics)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val calculatedPadding = when (keyboardState.value) {
|
||||
Keyboard.Closed -> defaultBottomPadding
|
||||
is Keyboard.Opened -> {
|
||||
val keyboardHeight = (keyboardState.value as Keyboard.Opened).height
|
||||
val keyboardPadding = context.pxToDp(keyboardHeight.toFloat()).dp
|
||||
if (calculateWithActionBarHeight) {
|
||||
val actionBarHeight = context.pxToDp(getActionBarHeight(context).toFloat()).dp
|
||||
keyboardPadding + spaceBetweenKeyboard - actionBarHeight
|
||||
} else {
|
||||
keyboardPadding + spaceBetweenKeyboard
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Box(modifier.padding(bottom = calculatedPadding)) { content() }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue