How to detect focus move direction on focus change? - kotlin

I have a Composable Row that has some click listeners:
val action = { ... }
Row(Modifier.clickable(action) {
IconButton({ /* other, unrelated action */}) {}
Text("This isn't clickable")
Checkbox({ /* something that calls action() on toggle */ })
}
When tabbing through this UI, the focus goes to the IconButton, then the Checkbox, then the Row. I want it to skip the row. I've implemented that by adding to the Row modifier:
val manager = LocalFocusManager.current
Row(Modifier.clickable(action).onFocusChanged {
if (it.isFocused) manager.moveFocus(FocusDirection.Next)
}) { /* same content */ }
... which works when moving forward, but not when moving backward (using Shift-Tab). And of course that's because of the FocusDirection.Next, which should instead be Previous when moving backward. But how do I detect that? The focus event doesn't have a direction property.
Update
I tried doing this by manually detecting if shift is pressed, which feels more like a hack than a solution:
val keys = LocalWindowInfo.current.keyboardModifiers
/* in onFocusChanged */
manager.moveFocus(if (keys.isShiftPressed) FocusDirection.Previous else FocusDirection.Next)
.. and also, it doesn't work. Calling manager.moveFocus(FocusDirection.Previous) if shift is pressed causes an infinite loop and application crash, presumably because it's setting the focus back to where it came from.

Related

Composable State from Jetpack Compose execute these two states once or twice

I got a two states for handling a dynamic pop up screen component
var showPopUpScreen by remember { viewModel.popUpScreenIsOpen }
var popUpType by remember { viewModel.popUpScreenType }
but when I change the value of these mutableState-values when opening the pop up component
like this:
fun OpenPopUpScreen(type: BasePopUpScreen) {
popUpScreenType.value = type
popUpScreenIsOpen.value = true
}
will this composable function get executed twice (which is performance heavy) or will it be smart enough to know that these values are set at once so execute my pop up render function only once?
Extra code info:
fun LiveTrainingScreen(viewModel: LiveTrainingViewModel = viewModel()) {
// lots of code and then:
var showPopUpScreen by remember { viewModel.popUpScreenIsOpen }
var popUpType by remember { viewModel.popUpScreenType }
//pop up container
if(showPopUpScreen) {
Row(modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.6f))
.zIndex(11f), verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
DyanmicPopUpScreenLiveTraining(popUpScreenTypeInfo = popUpType, viewModel = viewModel)
} // pop up main column
} // end pop up screen row
} // end if pop up screen
}
I believe the recomposition starts right after both have changed as the compose guide says:
"Recomposition is optimistic and may be canceled." [source: https://developer.android.com/jetpack/compose/mental-model]
in which means the recomposition will be canceled as the other parameter is assigned and you will see the change in state in UI in means of both values.
However, it is a better approach to save the UI state inside a data class and remember the data class directly. that way, you change both variables and the composition resets as the data class changes. plus, rather than remembering the data class, hoist the state in a ViewModel and you will good to go.
I think compose is smart enough to identify the changes and react on it.
As per your question
once you set first value it will start changes compose views which are dependent on it
And suppose considering complex view previous recomposition process is going on, after setting second value previous recomposition will get cancelled and compose will recompose your screen with updated both values.
So effectively we can Recomposition will happen once only.

How to retrigger focusRequester.requestFocus() when coming back to a composable?

I have a MyBasicTextField in a composable to request user input:
#Composable
fun MyBasicTextField() {
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember{ FocusRequester() }
BasicTextField(
modifier = Modifier
.focusRequester(focusRequester),
keyboardActions = keyboardActions ?: KeyboardActions(onAny = { keyboardController?.hide() }),
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
The keyboard automatically slides in when showing this composable, always.
But wherever MyBasicTextField is used:
I tap on a LinkifiedText to leave and open a browser to show link
I tap BACK
and come back to previous MyBasicTextField screen, the keyboard is not shown
also the focusRequester.requestFocus() is not triggered again when coming back
How can I solve my issue?
Create a top-level variable in your activity, then modify it from within the onStart overridden method. Use that variable as the key for LaunchedEffect in place of Unit. That variable basically keeps track of when the user enters the app.
var userIn by mutableStateOf (true)
In your Composable,
Launched effect(userIn){
if(userIn && isKeyboardShown){
...
}
}
Boring,
You can use my answer here as well, but instead of incrmenting the launchKey every time it's called, only increment the launchKey once user clicks on the browser link, that way it will not pop up during other re-compositions.

Optimise Kotlin code for button clicked state

I want to change the button background when the button clicked, the function is work by using this code
bank1.setOnClickListener {
bank1.setBackgroundResource(R.drawable.selected_btn_border_blue_bg);
bank2.setBackgroundResource(R.drawable.default_option_border_bg);
bank3.setBackgroundResource(R.drawable.default_option_border_bg);
bank4.setBackgroundResource(R.drawable.default_option_border_bg);
}
bank2.setOnClickListener {
bank2.setBackgroundResource(R.drawable.selected_btn_border_blue_bg);
bank1.setBackgroundResource(R.drawable.default_option_border_bg);
bank3.setBackgroundResource(R.drawable.default_option_border_bg);
bank4.setBackgroundResource(R.drawable.default_option_border_bg);
}
bank3.setOnClickListener {
bank3.setBackgroundResource(R.drawable.selected_btn_border_blue_bg);
bank2.setBackgroundResource(R.drawable.default_option_border_bg);
bank1.setBackgroundResource(R.drawable.default_option_border_bg);
bank4.setBackgroundResource(R.drawable.default_option_border_bg);
}
bank4.setOnClickListener {
bank4.setBackgroundResource(R.drawable.selected_btn_border_blue_bg);
bank2.setBackgroundResource(R.drawable.default_option_border_bg);
bank3.setBackgroundResource(R.drawable.default_option_border_bg);
bank1.setBackgroundResource(R.drawable.default_option_border_bg);
}
But it kinda hardcoded, and make it to so many lines, any way to make the code shorter?
I would keep a variable that keeps track of the selected one like
private var selectedBank: View? = null
And then do
arrayOf(bank1, bank2, bank3, bank4).forEach {
it.setOnClickListener {
selectedBank?.setBackgroundResource(R.drawable.default_option_border_bg)
selectedBank = it
it.setBackgroundResource(R.drawable.selected_btn_border_blue_bg)
}
}
you only need to deselected the previous selected one

Why Is Vue Data Element Changing When I Only Set It Once?

I have created a component that allows users to select objects from a list and put them into another "selected list" lets say. So there are 100 items in the main list and however many in the users selected list. They can of course remove and add items as they want.
There are two buttons at the bottom of the modal. Cancel and Update. Where Cancel forgets anything they have added or removed (essentially an Undo of when they popped up the modal) and Update technically does nothing because when you are adding and removing, I am actually updating the real selected list.
So my data has these two properties (among others of course):
originalSelections: [],
selectedMedications: [],
There is a method that only gets called one time to set the selectedMedications property to whatever the current state of originalSelections is. I have a console.log to prove I am not running this method more than once and it NEVER gets hit again.
console.log('Post Initing');
let Selected = [];
for (let med of this.value.OtherNotFromEpic) {
let match = this.otherMedications.find(x => { return x.value === med });
if (match) {
Selected.push(JSON.parse(JSON.stringify(match)));
this.originalSelections.push(JSON.parse(JSON.stringify(match)));
this.selectedMedications.push(JSON.parse(JSON.stringify(match)));
}
}
I was baffled at why the selectedMedications was changing, so I added a watch to let me know if it was:
watch: {
originalSelections(newValue) {
console.log(' ** Original Selection Changed', this.init, newValue);
},
},
The Cancel method is as follows:
cancel() {
$('#' + this.modalID).modal('hide');
console.log('Cancel: this.originalSelections', JSON.parse(JSON.stringify(this.originalSelections)));
this.selectedMedications = this.originalSelections;
this.$nextTick(() => {
console.log(' Cancelled: this.selectedMedications', JSON.parse(JSON.stringify(this.selectedMedications)));
});
},
You can see that I am setting selectedMedications to whatever it was originally.
The odd thing is... This WORKS 100% the first time I bring up the modal. So I pop up the modal, remove an item, cancel the modal and it brings back the item I removed. Perfect!
If I bring the modal up again, remove that same item or any other item, cancel the modal and that item stays removed and the watch is actually hit. I have NO other place in the code where originalMedications = … or originalMedications.push or even originalMedications.slice ever happen. And it is my understand that the JSON.parse(JSON.stringify(match)) code I use to set it is making a new object and not a reference in any way.
Another odd thing I have found is that if I refresh the page, open the modal, cancel out without doing any adding or removing. Then I bring up the modal again and try either add or remove, then cancel the modal, those items do not revert back to the originalMedications state because originalMedications is the same as selectedMedications. Aargh!
So HOW can a property get altered when I never do anything to it after the initial setting of it?
In the cancel method, when below direct assignment is done here after both data is referring to same (since its array). So any time after the first cancel both originalSelections and selectedMedications array would have same reference and hence same data.
this.selectedMedications = this.originalSelections;
instead better to use concat as below? This will create a copy of originalSelections and assign that to selectedMedications. So any operations on selectedMedications will not affect the originalSelections.
this.selectedMedications = [].concat(this.originalSelections);

Back button on first level of nestedlist

I have a nestedList with a few levels that appears when the user presses a button on the screen. When the nestedList appears, there is no back button (because we're at the top of the tree, so understandably there is nowhere to go back to), whereas tapping on items in the list takes you to screens with a back button.
I'd like to add a back button to the first screen. I have managed to do this, but not without adding the same back button to every sublist in the nestedList - this has the effect of 1 back button at the top level, and 2 back buttons (one to take you out of the nestledList completely, and one to take you up a level) at every subsequent level.
Can anyone help me figure out how to have 1 back button on each screen, including the top level to close the list?
Many thanks
PS a nasty workaround that I'm using at the moment is to have a "close" button in the top right of every screen instead.
I don't know how comfortable you are with the inner workings of Sencha Touch so how you go about doing this is up to you--
The back button is there, hidden, when the nested list is shown (created in the initComponent function with hidden: true), and then onBackTap, onItemTap and setActivePath will all call syncToolbar near the end of their functions which is where the back button is hidden when you are at a depth of 0.
So there are 2 places you need to do something about, first is initComponent which is easy-- just implement initComponent in your nestedList, call the superclass' initComponent and then set the backButton visible
var myNestedList = new Ext.NestedList({
...,
initComponent: function() {
myNestedList.superclass.initComponent.call(this);
this.backButton.setVisible(true);
},
...
});
That takes care of showing it intially.. how you care to deal with fixing syncToolbar is up to you. You can use Ext.override, you can straight up copy and paste the whole syncToolbar function into your nestedList object which would also override it or you could do what you're told never to do and just edit the sencha-touch.js file directly. However you decide to do it, what you're looking to change is
syncToolbar: function(card) {
...
backToggleMth = (depth !== 0) ? 'show' : 'hide';
if (backBtn) {
backBtn[backToggleMth]();
if (parentNode) {
backBtn.setText(backBtnText);
}
}
... };
You can either change backToggleMth to = 'show' or just delete the if (backBtn {...} all together.
In the Sencha Touch 2.2 I had to use a different code:
Ext.create('Ext.NestedList', {
...,
listeners: {
initialize: function(obj) {
obj.getBackButton().show();
},
back: function(obj, node, lastActiveList, detailCardActive) {
switch(node.getDepth()) {
case 1:
obj.getBackButton().show();
break;
case 0:
alert("wohooooooo!");
break;
}
}
}
}