Jetpack Compose - How to manipulate the paste behaviour in a TextField - android-jetpack-compose

I want to be able to manipulate the paste behaviour of an TextField, something along the lines of -
override fun onPaste(pastedText: String){
}
Like how an EditText has
#Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
switch (item.getItemId()) {
case R.id.paste:
break;
}
return true;
}

I thought of a workaround. When the paste happens, your value usually typically changes by more than 1 symbol, so maybe something like this will work. I know it is hacky, and I would rather write this as a comment, but comment limits will not let me describe it completely.
TextField(
value = textValue,
onValueChange = { newValue ->
textValue = if (newValue.text.length > 1) {
doSomething()
newValue
} else {
newValue
}
}
)
UPD:
Oh I forgot that you can set up a modifier!
TextField(
value = textValue,
onValueChange = {...},
modifier = Modifier
.onKeyEvent { event: KeyEvent ->
if (
event.type == KeyEventType.KeyDown
&& event.key == Key.Paste
) {
// DO SOMETHING
return#onKeyEvent true
}
false
}
)

Related

How to properly set DataStore for storing boolean value

I want to set a simple switch that'll save a boolean value and if then block in my function.
Currently I have this in my DataStore:
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("userToken")
private val AutoRestartSystemUI = booleanPreferencesKey("AutoRestartSystemUIValue")
}
var getAutoRestartSystemUIValue: Flow<Boolean> = context.dataStore.data.map { preferences ->
(preferences[AutoRestartSystemUI] ?: "") as Boolean
}
suspend fun setAutoRestartSystemUI(value: Boolean) {
context.dataStore.edit { preferences ->
preferences[AutoRestartSystemUI] = value
}
}
}
and
Button(onClick = {
// if [pro.themed.manager.UserStore(context).getAutoRestartSystemUIValue = true] ()
CoroutineScope(Dispatchers.IO).launch {
UserStore(context).setAutoRestartSystemUI(false)
}
}) {
Text(text = UserStore(context).getAutoRestartSystemUIValue.collectAsState(
initial = ""
).toString())
}
in my activity. I have generally no idea of what I should do next and for some weird reason instead of showing value in a text (temp solution for testing) i have
How do i simplify datastore? How do I properly implement switch that'll make it = !it? How to set default value?

Handle properly number in Jetpack Compose

How can I properly handle number in text component in jetpack compose (with MVVM pattern)
Please note that the price can be null or have a value (maybe 0 btw)
I have a poor implementation for now, I changed the keyboard like this :
OutlinedTextField(
value = if (vm.viewState.collectAsState().value.price != null) vm.viewState.collectAsState().value.price.toString() else "",
onValueChange = { vm.onProductPriceChange(it) },
label = { Text(stringResource(id = R.string.price)) },
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = true,
keyboardType = KeyboardType.Number
),
)
and for onValueChange :
fun onProductPriceChange(it: Any) {
if (it.toString() == "") {
_viewState.value = _viewState.value.copy(price = null)
} else {
try
{
_viewState.value = _viewState.value.copy(price = it.toString().toDouble())
}
catch (e: NumberFormatException)
{ // dismiss the bad entries
}
}
}
there can be multiple bad output of the user for example write 22..0 (I dismissed them which is a workaround acceptable)
but there are bad behaviour, when you want to write 10, it will convert it to 10.0. it is not huge but it has backwards
when you delete number in the EditText, 10.0 will become 10.0 and then 100.0 and then 10.0 and finally 1.0. btw it is impossible to go back to the null value (for this case, I can consider 0.0 = no value)
I saw that VisualTransformation (https://medium.com/google-developer-experts/hands-on-jetpack-compose-visualtransformation-to-create-a-phone-number-formatter-99b0347fc4f6) could handle my case but the documentation seems complicated
class DoubleVisualTransformation : VisualTransformation {
override fun filter(str: AnnotatedString): TransformedText {
val strNullDouble = str.text.toBigDecimalOrNull()
var transformedString: String
if (str.text == "" || strNullDouble == null)
return TransformedText(AnnotatedString(""), OffsetMapping.Identity)
else if (strNullDouble.toDouble() % 1 == 0.0 && str.text.last() != '.')
transformedString = strNullDouble.toInt().toString()
else
transformedString = str.text
return TransformedText(
text = AnnotatedString(transformedString),
offsetMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return offset
}
override fun transformedToOriginal(offset: Int): Int {
return offset
}
}
)
}
}
how can I improve the behavior ?
What about not returning a double to your TextField but just the String?
fun onProductPriceChange(it: String) {
if (it == "") {
_viewState.value = _viewState.value.copy(price = null)
} else {
if (it.toDoubleOrNull() != null) {
_viewState.value = _viewState.value.copy(price = it)
}
}
}

I have a composable not setting button text as expected; wondering why. Have a reproducible example

this started as a new compose project
with the following code the intent is to change the text to the picked time. The code is commented where the behavior occurs
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TimeCardButton(id = 1, symbol ="In", enabled=true,modifier = Modifier) { entry ->
Log.d("click", "$entry result")
}
}
}
}
}
}
data class TimeCardEntry(val id: Int = -1, var entry: String = "")
#Composable
fun TimeCardButton(
id: Int,
symbol: String,
enabled: Boolean = false,
modifier: Modifier,
onValueChange: (TimeCardEntry) -> Unit = {},
) {
// Value for storing time as a string
val timeState = remember {
mutableStateOf(TimeCardEntry(id, symbol))
}
val validState = remember {
timeState.value.entry.trim().isNotEmpty()
}
val mTime = remember { mutableStateOf(symbol) }
if (enabled) {
// Fetching local context
val mContext = LocalContext.current
// Declaring and initializing a calendar
val mCalendar = Calendar.getInstance()
val mHour = mCalendar[Calendar.HOUR_OF_DAY]
val mMinute = mCalendar[Calendar.MINUTE]
// Creating a TimePicker dialog
val mTimePickerDialog = TimePickerDialog(
mContext,
{ _, mHour: Int, mMinute: Int ->
timeState.value.entry = "$mHour:$mMinute"
mTime.value = "$mHour:$mMinute"
onValueChange(timeState.value)
}, mHour, mMinute, false
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
TextButton(onClick = { mTimePickerDialog.show() }.also {
Log.d("click", "id $id clicked!") }) {
Column() {
// if I use just this it works [in changes to the time picked]
//Text(text = mTime.value)
// if i use both of these BOTH are set when the date picker is invoked
// if I just use the second one alone, the text never changes
Text(text = timeState.value.entry)
}
}
}
} else {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
Text(text = symbol, color =
MaterialTheme.colors.onBackground)
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyApplicationTestTheme {
}
}
First of all how to fix it:
Your problem basically is this. The easiest way to fix it would be to reassign the whole value of TimeState, not just entry by calling
timeState.value = timeState.value.copy(entry = "$mHour:$mMinute")
The reason it doesn't work with only the second one is that the change of a property doesn't trigger recomposition, even if the variable containing it is a mutableState. To fix (as outlined in the answers to the question linked above) this you either have to reassign the whole variable or make the parameter you want to observe observable (for example changing the String to State<String>)
PS: if you use by with mutableStateOf (i.e. val timeState = remember { mutableStateOf(TimeCardEntry(id, symbol)) }) you don't have to use .value every time. I find that a lot cleaner and more readable

LazyColumn does not scroll if using TextFields as child

#Composable
fun init() {
LazyColumn(Modifier.fillMaxSize()) {
for (i in 0..10) {
item { Box(Modifier.padding(15.dp)) { TextField("Hallo$i", modifier = Modifier.fillMaxWidth(), onValueChange = {}) } }
}
}
}
If i have something simple as this list with textfields
then the textfields will not let me scroll down the column.
Only works if i scroll down next to the textfields.
Tried also with readonly/disabled textfield.
is there a way to overcome this behaviour?
maybe a way to disable focus on textfield if scrolled?
I am using jetbrains-compose for desktop version (0.5.0-build245)
but can also be the same as in the jetpack-compose for android (did not try)
for the moment because i don't find any other solution i will use this workaround
using a invisible box above the text field and change the state accordingly
#Composable
fun init() {
LazyColumn(Modifier.fillMaxSize()) {
for (i in 0..10) {
item {
val isfocused = remember { mutableStateOf(false) }
val focusRequester = FocusRequester()
Box(Modifier.padding(15.dp)) {
TextField("Hallo$i", modifier = Modifier.fillMaxWidth().focusRequester(focusRequester).onFocusChanged {
isfocused.value = it.isFocused
}, onValueChange = {})
if (!isfocused.value)
Box(
modifier = Modifier
.matchParentSize()
.alpha(0f)
.clickable(onClick = {
isfocused.value = true
focusRequester.requestFocus()
}),
)
}
}
}
}
}

Jetpack Compose - Scroll to focused composable in Column

I have UI like this:
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize(1F)
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
) {
TextField(...)
// multiple textfields
TextField(
//...
modifier = Modifier.focusOrder(countryFocus).onFocusChanged {
if(it == FocusState.Active) {
// scroll to this textfield
}
},
)
}
I have multiple TextFields in this column and when one of them is focused I want to scroll Column to it. There is a method in scrollState scrollState.smoothScrollTo(0f) but I have no idea how to get a focused TextField position.
Update:
It seems that I've found a working solution. I've used onGloballyPositioned and it works. But I'm not sure if it the best way of solving this.
var scrollToPosition = 0.0F
TextField(
modifier = Modifier
.focusOrder(countryFocus)
.onGloballyPositioned { coordinates ->
scrollToPosition = scrollState.value + coordinates.positionInRoot().y
}
.onFocusChanged {
if (it == FocusState.Active) {
scope.launch {
scrollState.smoothScrollTo(scrollToPosition)
}
}
}
)
There is a new thing in compose called RelocationRequester. That solved the problem for me. I have something like this inside of my custom TextField.
val focused = source.collectIsFocusedAsState()
val relocationRequester = remember { RelocationRequester() }
val ime = LocalWindowInsets.current.ime
if (ime.isVisible && focused.value) {
relocationRequester.bringIntoView()
}
Also you can use BringIntoViewRequester
//
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val coroutineScope = rememberCoroutineScope()
//--------
TextField( ..., modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)
.onFocusEvent {
if (it.isFocused) {
coroutineScope.launch {
bringIntoViewRequester.bringIntoView()
}
}
}
It seems that using LazyColumn and LazyListState.animateScrollToItem() instead of Column could be a good option for your case.
Reference: https://developer.android.com/jetpack/compose/lists#control-scroll-position
By the way, thank you for the information about onGloballyPositioned() modifier. I was finding a solution for normal Column case. It saved me a lot of time!
Here's some code I used to make sure that the fields in my form were not cut off by the keyboard:
From: stack overflow - detect when keyboard is open
enum class Keyboard {
Opened, Closed
}
#Composable
fun keyboardAsState(): State<Keyboard> {
val keyboardState = 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
} else {
Keyboard.Closed
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
}
and then in my composable:
val scrollState = rememberScrollState()
val scope = rememberCoroutineScope()
val isKeyboardOpen by keyboardAsState()
if (isKeyboardOpen == Keyboard.Opened) {
val view = LocalView.current
val screenHeight = view.rootView.height
scope.launch { scrollState.scrollTo((screenHeight * 2)) }
}
Surface(modifier = Modifier
.fillMaxHeight()
.verticalScroll(scrollState),
) {
//Rest of your Composables, Columns, Rows, TextFields, Buttons
//add this so the screen can scroll up and keyboard cannot cover the form fields - Important!
/*************************************************/
if (isKeyboardOpen == Keyboard.Opened) {
Spacer(modifier = Modifier.height(140.dp))
}
}
Hope it helps someone. I was using:
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
val view = LocalView.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
Surface(modifier.bringIntoViewRequester(bringIntoViewRequester)) {
///////////rest of my composables
}
But this did not work.

Resources