Handle properly number in Jetpack Compose - android-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)
}
}
}

Related

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

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
}
)

Jetpack compose Lazy Column exception: View=androidx.compose.ui.window.PopupLayout not attached to window manager

Sometimes the app crashes with exception "java.lang.IllegalArgumentException: View=androidx.compose.ui.window.PopupLayout{...} not attached to window manager".
I'm only able to reproduce it with testcase and it happens only sometimes.
My test case: list with 3 items, let's call them A, B, C; and test case has basically 3 steps:
Update item A text and remove item B
Update item A text and remove item C
Try to add item C back to list (and sometimes this causes a crash). (The problem only happens when adding item with the same key back that removed one was)
My hypothesis why it happens:
It seems to be happen only when update text A causes to go A text to more lines than it was before. So the recomposition redrew the item A and it will have more height than before
If I run fetchSemanticsNode before step3, then it seems it crashes only when item C is still cached (the node exists but it's not displayed)
Seems like these both conditions needs to be true to error to happen. So I was wondering am I using LazyColumn somehow wrongly or there seems to be bug in Jetpack Compose code?
Below is the full exception and my code and test case to reproduce it.
App is crashing with following exception:
java.lang.IllegalArgumentException: View=androidx.compose.ui.window.PopupLayout{7744d0c V.E...... ......ID 0,0-62,75 #1020002 android:id/content} not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:544)
at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:433)
at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:116)
at androidx.compose.ui.window.PopupLayoutHelperImpl.updateViewLayout(AndroidPopup.android.kt:776)
at androidx.compose.ui.window.PopupLayout.updatePosition(AndroidPopup.android.kt:659)
at androidx.compose.ui.window.PopupLayout.updateParentBounds$ui_release(AndroidPopup.android.kt:626)
at androidx.compose.ui.window.PopupLayout.updateParentLayoutCoordinates(AndroidPopup.android.kt:581)
at androidx.compose.ui.window.AndroidPopup_androidKt$Popup$7.invoke(AndroidPopup.android.kt:316)
at androidx.compose.ui.window.AndroidPopup_androidKt$Popup$7.invoke(AndroidPopup.android.kt:310)
at androidx.compose.ui.layout.OnGloballyPositionedModifierImpl.onGloballyPositioned(OnGloballyPositionedModifier.kt:59)
at androidx.compose.ui.node.LayoutNode.dispatchOnPositionedCallbacks$ui_release(LayoutNode.kt:1149)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:51)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatchHierarchy(OnPositionedDispatcher.kt:55)
at androidx.compose.ui.node.OnPositionedDispatcher.dispatch(OnPositionedDispatcher.kt:44)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.dispatchOnPositionedCallbacks(MeasureAndLayoutDelegate.kt:348)
at androidx.compose.ui.node.MeasureAndLayoutDelegate.dispatchOnPositionedCallbacks$default(MeasureAndLayoutDelegate.kt:344)
at androidx.compose.ui.platform.AndroidComposeView.measureAndLayout(AndroidComposeView.android.kt:761)
at androidx.compose.ui.node.Owner.measureAndLayout$default(Owner.kt:196)
at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:954)
at android.view.View.draw(View.java:22353)
at android.view.View.updateDisplayListIfDirty(View.java:21226)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4500)
at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4473)
at android.view.View.updateDisplayListIfDirty(View.java:21186)
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:559)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:565)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:642)
at android.view.ViewRootImpl.draw(ViewRootImpl.java:4101)
at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3828)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3099)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:731)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
UI
const val LAZY_COLUMN_TEST_TAG = "lazy_column_tag"
const val UNDO_TEST_TAG = "undo_tag"
const val ITEM_FIELD_TEST_TAG = "item_field_tag"
#Composable
fun ScreenInitWrapper(viewModel: MyViewModel = hiltViewModel()) {
viewModel.initModel()
if (viewModel.isLoaded) {
Screen()
}
}
#Composable
fun Screen(viewModel: MyViewModel = hiltViewModel()) {
val items = viewModel.items
val scrollState = rememberLazyListState()
LazyColumn(
state = scrollState,
modifier = Modifier.testTag(LAZY_COLUMN_TEST_TAG)
) {
item {
UndoButton(viewModel)
}
items(items = items, key = { item -> "ITEM_${item.id}" }) { item ->
ItemText(item, viewModel)
}
}
}
#Composable
fun UndoButton(viewModel: MyViewModel) {
IconButton(
onClick = { viewModel.undo() }, enabled = true,
modifier = Modifier.testTag(UNDO_TEST_TAG)
) {
Icon(
imageVector = Icons.Filled.Undo, contentDescription = "undo",
modifier = Modifier.padding(start = 16.dp, end = 16.dp)
)
}
}
#Composable
fun ItemText(item: MyItem, viewModel: MyViewModel) {
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = item.text, selection = item.selection)) }
val textFieldValue = textFieldValueState.copy(text = item.text, selection = item.selection)
TextField(
value = textFieldValue,
onValueChange = {
textFieldValueState = it
if (item.selection != it.selection) {
item.selection = it.selection
}
if (item.text != it.text) {
item.text = it.text
}
},
modifier = Modifier
.onPreviewKeyEvent {
if (isBackspaceClickedAndCursorIsBeginningOfLine(it, textFieldValue)) {
if (item.id != 1) {
viewModel.removeItemAndUpdatePreviousItemText(item)
}
true
} else {
false
}
}
.testTag("${ITEM_FIELD_TEST_TAG}_${item.id}"),
)
}
#OptIn(ExperimentalComposeUiApi::class)
private fun isBackspaceClickedAndCursorIsBeginningOfLine(it: KeyEvent, textFieldValue: TextFieldValue): Boolean {
if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown) {
val currentPosition = textFieldValue.selection.end
if (currentPosition == 0) {
return true
}
}
return false
}
ViewModel
data class MyItem (val id: Int, var text: String, var selection: TextRange = TextRange.Zero)
#HiltViewModel
class MyViewModel : ViewModel() {
var isLoaded: Boolean by mutableStateOf(false)
private set
private var _items = emptyList<MyItem>().toMutableStateList()
var items: List<MyItem> = _items
private set
private val undoStack = ArrayDeque<MyItem>()
fun initModel() {
if (!isLoaded) {
// The names matter
val itemsTemp = listOf(
MyItem(id = 1, text = "some text long enough for item 1"),
MyItem(id = 2, text = "item 2"),
MyItem(id = 3, text = "item 3"),
).toMutableList()
_items.addAll(itemsTemp)
isLoaded = true
}
}
fun removeItemAndUpdatePreviousItemText(deletedItem: MyItem) {
// Update previous item text
val index = items.indexOfFirst { it.id == deletedItem.id }
val previousItemText = items[index - 1]
// I was able to get this exception only when the amount of lines of the previous text changed
previousItemText.text = previousItemText.text + "Some new text to make it more lines"
// Undo
undoStack.addLast(deletedItem)
// Remove value from list
val index = items.indexOfFirst { it.id == item.id }
_items.removeAt(index)
}
fun undo() = viewModelScope.launch {
val deletedItem = undoStack.removeLastOrNull()
if (deletedItem != null) {
_items.add(deletedItem)
}
}
}
InstrumentedTest
#RunWith(AndroidJUnit4::class)
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
#LargeTest
#HiltAndroidTest
class UndoRemoveItemInstrumentedTest {
#get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
#get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()
#Test
undoRemoveItemTest() {
removeItem(2)
removeItem(3)
// First undo causes sometimes failure
composeRule.onNodeWithTag(UNDO_TEST_TAG).performClick()
composeRule.onNodeWithTag(UNDO_TEST_TAG).performClick()
}
#OptIn(ExperimentalTestApi::class)
private fun removeItem(id: Int) {
composeRule.onNodeWithTag(LAZY_COLUMN_TEST_TAG)
.performScrollToKey("ITEM_${id}")
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${id}").performClick()
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${id}")
.performTextInputSelection(TextRange(0))
performBackspaceKeyPress(id)
}
private fun performBackspaceKeyPress(itemId: Int) {
val backspaceDown = android.view.KeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_DEL)
val backspaceUp = android.view.KeyEvent(NativeKeyEvent.ACTION_UP, NativeKeyEvent.KEYCODE_DEL)
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${itemId}").performKeyPress(KeyEvent(backspaceUp))
composeRule.onNodeWithTag("${ITEM_FIELD_TEST_TAG}_${itemId}").performKeyPress(KeyEvent(backspaceDown))
}
}
Seems to be fixed in compose version 1.3.0. At least I'm not able to reproduce it in this version

How to make the first item in a lazy column clickable and make the rest unclickable

How to make the first item in a lazy column clickable and make the rest unclickable.
for example if I have a list of tasks to complete, enable first item only which is not completed
you have to complete tasks one at a time
LazyColumn(
modifier.padding(top = 40.dp),
) {
itemsIndexed(
items = todayRoute.sortedBy { it.sequence },
) { index, item ->
Row(
modifier = modifier
.fillMaxWidth()
.height(90.dp)
.padding(12.dp)
.clickable(
enabled = item.completed == "0" &&
item.arrived == "0" &&
item.missed == "0"
) {
})
{
}
}
}
Use Hashmap like this
As soon as you get all the item call replaceAll.
When the item is completed, call taskCompleted
class TaskViewModel {
private var _taskStatus = MutableStateFlow<HashMap<String, Boolean>>(hashMapOf())
val taskStatus = _taskStatus.asStateFlow()
fun taskCompleted(item: Task) {
_taskStatus.replace(item.id, true)
}
fun replaceAll(tasks: List<Task>) {
_taskStatus.value.clear()
tasks.forEach {
_taskStatus.value[it.id] = it.completed
}
}
private fun MutableStateFlow<HashMap<String, Boolean>>.replace(key: String, value: Boolean) {
val helper = HashMap<String, Boolean>(this.value)
helper[key] = value
this.value = helper
}
}
On UI, Adjust according to your need
val taskStatus by remember { viewModel.taskStatus }.collectAsState()
LazyColumn {
itemsIndexed(todayRoute) { item, index ->
Row(modifier = Modifier.clickable(
enabled = if (index > 0)
taskStatus[index - 1] ?: false
else
true
)
) {}
}
}

How to use AnimatedVisibility with nullable values?

I find myself in this situation quite often. I have some value like plates in the sample below, and I want to show/hide it depending on if its null or not. But hiding it always fails since nothing is rendered whenever its null, and the animation just snaps to empty nothingness.
How can I make this work? Id like to keep plates around until the animation finishes.
AnimatedVisibility(
visible = plates != null,
content = {
if (plates != null) {
// Render plates
} else {
// The animation snaps to nothingness, as opposed to animating out
}
})
This is what I ended up doing, and it works as expected!
#Composable
inline fun <T> AnimatedValueVisibility(
value: T?,
enter: EnterTransition = fadeIn(
animationSpec = FloatSpec
) + expandVertically(
animationSpec = SizeSpec,
clip = false
),
exit: ExitTransition = fadeOut(
animationSpec = FloatSpec
) + shrinkVertically(
animationSpec = SizeSpec,
clip = false
),
crossinline content: #Composable (T) -> Unit
) {
val ref = remember {
Ref<T>()
}
ref.value = value ?: ref.value
AnimatedVisibility(
visible = value != null,
enter = enter,
exit = exit,
content = {
ref.value?.let { value ->
content(value)
}
}
)
}
Animation always requires content even when there is nothing to show. Just display a Surface when the content is null (or empty):
AnimatedVisibility(
visible = plates != null,
content = {
if (plates != null) {
// Render plates
} else {
Surface(modifier = Modifier.fillMaxSize()) { }
}
})

How to add text input field in cocos2d.Android cocos sharp?

I am trying to get CCTextFieldTTF to work in cocos sharp with Xamarin for an android application. But can't get hold of this for the life of me. Could not find any documentation on cocos sharp API either. Does anyone know how to use this class to render a text area in an android application? The reason I am asking is in a xamarin forum I saw someone saying that this does not work in the API yet. Any help would be highly appreciated. Thanks in advance.
I have this working in android
Here is the sample code:
Create a node to track the textfield
CCTextField trackNode;
protected CCTextField TrackNode
{
get { return trackNode; }
set
{
if (value == null)
{
if (trackNode != null)
{
DetachListeners();
trackNode = value;
return;
}
}
if (trackNode != value)
{
DetachListeners();
}
trackNode = value;
AttachListeners();
}
}
//create the actual input textfield
var textField = new CCTextField(string.Empty, "Somefont", 25, CCLabelFormat.SystemFont);
textField.IsColorModifiedByOpacity = false;
textField.Color = new CCColor3B(Theme.TextWhite);
textField.BeginEditing += OnBeginEditing;
textField.EndEditing += OnEndEditing;
textField.Position = new CCPoint (0, 0);
textField.Dimensions = new CCSize(VisibleBoundsWorldspace.Size.Width - (160 * sx), vPadding);
textField.PlaceHolderTextColor = Theme.TextYellow;
textField.PlaceHolderText = Constants.TextHighScoreEnterNamePlaceholder;
textField.AutoEdit = true;
textField.HorizontalAlignment = CCTextAlignment.Center;
textField.VerticalAlignment = CCVerticalTextAlignment.Center;
TrackNode = textField;
TrackNode.Position = pos;
AddChild(textField);
// Register Touch Event
var touchListener = new CCEventListenerTouchOneByOne();
touchListener.OnTouchBegan = OnTouchBegan;
touchListener.OnTouchEnded = OnTouchEnded;
AddEventListener(touchListener);
// The events
bool OnTouchBegan(CCTouch pTouch, CCEvent touchEvent)
{
beginPosition = pTouch.Location;
return true;
}
void OnTouchEnded(CCTouch pTouch, CCEvent touchEvent)
{
if (trackNode == null)
{
return;
}
var endPos = pTouch.Location;
if (trackNode.BoundingBox.ContainsPoint(beginPosition) && trackNode.BoundingBox.ContainsPoint(endPos))
{
OnClickTrackNode(true);
}
else
{
OnClickTrackNode(false);
}
}
public void OnClickTrackNode(bool bClicked)
{
if (bClicked && TrackNode != null)
{
if (!isKeyboardShown)
{
isKeyboardShown = true;
TrackNode.Edit();
}
}
else
{
if (TrackNode != null)
{
TrackNode.EndEdit();
}
}
}
private void OnEndEditing(object sender, ref string text, ref bool canceled)
{
//((CCNode)sender).RunAction(scrollDown);
Console.WriteLine("OnEndEditing text {0}", text);
}
private void OnBeginEditing(object sender, ref string text, ref bool canceled)
{
//((CCNode)sender).RunAction(scrollUp);
Console.WriteLine("OnBeginEditing text {0}", text);
}
void AttachListeners()
{
// Attach our listeners.
var imeImplementation = trackNode.TextFieldIMEImplementation;
imeImplementation.KeyboardDidHide += OnKeyboardDidHide;
imeImplementation.KeyboardDidShow += OnKeyboardDidShow;
imeImplementation.KeyboardWillHide += OnKeyboardWillHide;
imeImplementation.KeyboardWillShow += OnKeyboardWillShow;
imeImplementation.InsertText += InsertText;
}
void DetachListeners()
{
if (TrackNode != null)
{
// Remember to remove our event listeners.
var imeImplementation = TrackNode.TextFieldIMEImplementation;
imeImplementation.KeyboardDidHide -= OnKeyboardDidHide;
imeImplementation.KeyboardDidShow -= OnKeyboardDidShow;
imeImplementation.KeyboardWillHide -= OnKeyboardWillHide;
imeImplementation.KeyboardWillShow -= OnKeyboardWillShow;
imeImplementation.InsertText -= InsertText;
}
}
This is all taken from the link below but needed a bit of additional work to get it working on each platform.
https://github.com/mono/cocos-sharp-samples/tree/master/TextField

Resources