Jetpack Compose - ModalBottomSheet comes up with soft keyboard - kotlin

I have a problem with ModalBottomSheet and it's on my work computer so I can't record it to you right now. So basically, after I give focus to one of my TextFields, my keyboard comes up and pushes all the content upwards so I can see the TextField that I'm writing to. When I'm hiding my keyboard I can see that my ModalBottomSheet hides too, but I never set it to come up.
So if you are familiar with this bug, please let me know your solutions.

My coworker, so he inserted a boolean that checks if keyboard is up or not and if it is, dont put ap modal bottom sheet.

You can use this method until this problem is fixed with an additional update.
You can use LaunchedEffect for this. Here is an example for you.
The important thing here is to disable the ModalBottomSheetDialog when the keyboard is opened and re-enable it half a second after the keyboard is closed.
You can trigger the required function by assigning a value to this variable when the keyboard is turned on, and then changing and checking this value when the keyboard is closed.
/*Change this value to "keyboard_on" when the keyboard is turned on and "keyboard_off" when the keyboard is closed again. You can give different names for different usage areas. That's why we're using a string, not a Boolean.*/
var taskCodeValue = remember { mutableStateOf("keyboard_off") }
var sheetOpener by remember { mutableStateOf(true) }
if (taskCodeValue.value == "keyboard_off"){
LaunchedEffect(taskCodeValue.value == "keyboard_off"){
delay(500)
sheetOpener = true
}
}else {
sheetOpener = false
}
/*
By adding the Scaffold, which includes ModalBottomSheet and other compose
elements, into a box, we enable them to work independently of each other.
*/
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
content = {}
)
if (sheetOpener){
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {}
) {}
}
}

Related

Kotlin button listener

I want to increase the value of i every time the button is clicked
I've tried this code but it's not working.
val textview = findViewById<TextView>(R.id.texttest)
var i = 10
bookbutton.setOnClickListener {
i++
}
textview.text = "$i"
You have to set the text inside the listener:
bookbutton.setOnClickListener {
i++
textview.text = "$i"
}
Your listener is updating the value of i — but that's not having any visible effect because by then it's too late: the text shown in your textview has already been set.
Let's review the order of events:
Your code runs. That creates a text view and a variable, sets a listener on the button, and sets the text in the text view.
At some later point(s), the user might click on the button. That calls the listener, which updates the variable.
So while your code sets the listener, the listener does not run until later. It might not run at all, or it might run many times, depending on what the user does, but it won't run before you set the text in the text view.
So you need some way to update the text when you update the variable. The simplest way is to do explicitly it in the listener, e.g.:
bookbutton.setOnClickListener {
textview.text = "${++i}"
}
(There are other ways — for example, some UI frameworks provide ways to ‘bind’ variables to screen fields so that this sort of update happens automatically. But they tend to be a lot more complex; there's nothing wrong with the simple solution.)

How to clear focus of BasicTextField upon clicking somewhere else in Compose Multiplatform?

I have a BasicTextField in Jetbrains Compose Multiplatform for desktop. When I click on it, the TextField gets focus and becomes editable. However, when I click somewhere else in my application, the focus is not lost and the field is still editable as if I just clicked on it.
I know this behavior is normal and intended. Nonetheless, I want to the TextField to become unfocused when the user clicks somewhere else, regardless of it being a clickable or non-clickable composable.
How do I achieve this?
This is one way I've done it in the past.
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val interactionSource = remember { MutableInteractionSource() }
Then I made my parent layout clickable.
Box(modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = null // this gets rid of the ripple effect
) {
keyboardController?.hide()
focusManager.clearFocus(true)
}

Jetpack Compose: scrolling to the bottom of list on event

I have a composable representing list of results:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
coroutineScope.launch {
listState.animateScrollToItem(results.size)
}
}
}
}
Expected behaviour: The list smoothly scrolls to the last item whenever a new item is added
Actual behaviour: All is good, but whenever I manually scroll fast through the list, it is also automatically put on the bottom. Also, the scrolling is not smooth.
Your code gives the following error:
Calls to launch should happen inside a LaunchedEffect and not composition
You should not ignore it by calling the side effect directly from the Composable function. See side effects documentation for more details.
You can use LaunchedEffect instead (as this error suggests). By providing results.size as a key, you guarantee that this will be called only once when your list size changes:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val listState = rememberLazyListState()
LaunchedEffect(results.size) {
listState.animateScrollToItem(results.size)
}
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
Philip's solution will work for you. However, I'm posting this to ensure that you understand why
A.) The scroll was not smooth
B.) The list gets scrolled to the bottom when you scroll through it fast enough.
Explanation for A.)
It is because you are using animateScollTo. I've experienced issues with this method if called too often,
Explanation for this lies in how Lazy scrollers handle their children internally. You see, Lazy scrollers, as you might know, are meant to display only a small window of a large dataset to the user. To achieve this, it uses internal caching. So, the items on the screen, AND a couple of items to the top and bottom of the current window are cached.
Now, since in your code, you are making a call to animateScrollTo(size) inside the Composable's body (the items scope), the code will essentially be executed upon every composition.
Hence, on every recomposition, there is an active clash between the animateScrollTo method, and the users touch input. When the user scrolls past in a not-so-fast manner, this is what happens - user presses down, gently scrolls, then lifts up the finger. Now, remember this, for as long as the finger is actually pressed down, they animateScrollTo will seem to have no effect (because the user is actively holding a position on the scroller, so it won't be scrolled past it by the system). Hence, while the user is scrolling, some items ahead of the list are cached, but the animateScrollTo does not work. Then, because the motion is slow enough, the distance the scroller travels because of inertia is not a problem, since the list already has enough cached items to show for the distance. That also explains the second problem.
B.)
When you are scrolling through the list FAST enough, the exact same thing as the above case (the slow-scroll) happens. Only, this time the inertia carries the list too forward for the scroller to be handled based on the internal cache, and hence there is active recomposition. However, now since there is no active user input (they have lifted their finger off the screen), it actually does animate to the bottom, since their is no clash here for the animateScrollTo method.
For as long as your finger is pressed, no matter how fast you scroll, it won't scroll to the bottom (test that!)
Now to the solution of the actual problem. Philip your answer is brilliant. The only thing is that it might not work if the developer has an item remove implementation as well. Since only the size of the list is monitored, it will scroll to end when an item is added OR deleted. To counteract that, we would actually need some sort of reference value. So, either you can implement something of your own to provide you with a Boolean variable that actually confirms whether an item has been ADDED, or you could just use something like this.
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
//Right here, create a variable to remember the current size
val currentSize by rememberSaveable { mutableStateOf (results.size) }
//Now, extract a Boolean to be used as a key for LaunchedEffect
var isItemAdded by mutableStateO(results.size > currentSize)
LaunchedEffect (isItemAdded){ //Won't be called upon item deletion
if(isItemAdded){
listState.animateScrollToItem(results.size)
currentSize = results.size
}
}
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
This should ensure the proper behaviour. Of course, let me know if there is anything else, happy to help.
Pretty obvious. Why are you calling:
listState.animateScrollToItem(results.size) inside your LazyList? Of course you're going to get extremely bad performance. You shouldn't be messing around with scrolling when items are being rendered. Get rid of this line of code.

talkback not focusing by default on any view on start of inner fragment

I am using accessibility talkback functionality and I am facing one problem I have one bottom navigation in the parent activity and from the setting tab I am opening another fragment(inner fragment) using .add but the inner fragment view not getting focus by default
I also tried with . replace but it's not focusing by default on fragment creation.
open fragment code
val details = DetailsFragment.newInstance();
getSupportFragmentManager().setupForAccessibility()
getSupportFragmentManager().beginTransaction().add(android.R.id.content, details).commit()
and I used this extension function to not get focus on the previous fragment from this source
fun FragmentManager.setupForAccessibility() {
addOnBackStackChangedListener {
val lastFragmentWithView = fragments.lastOrNull { it.view != null }
for (fragment in fragments) {
if (fragment == lastFragmentWithView) {
fragment.view?.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_YES
} else {
fragment.view?.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
}
}
}
}
in normal I show that at the start of the first fragment it's focusing top first Textview and speaking automatic but in the inner fragment it's not focusing by default so what should I do to get focus by default on the first view by default
I already try
android:focusable="true"
android:focusableInTouchMode="true"
and request focus but it's not working
Please suggest me any help would be highly appriciated
I've had the best luck using
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
on the view you want to focus, or maybe
Handler().postDelayed({
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}, someDelayMillis)
to let the system do whatever it does, and then jump in after a moment and force a focus change.
But this might be considered bad accessibility, if it's interfering with the usual navigation, and that might be why it's so hard to get focus working consistently! It might be better to just announce the new fragment (with something like view.announceForAccessibility("new fragment")) and let the user start navigating from the top. It depends on your app, it's your call
Also you probably want to use replace for your fragment instead of add, if you add a fragment on top of an old one, TalkBack can get stuck looking at the "invisible" views on the old fragment
this is my improved code that extends from #cactustictacs
//target to specific a view
binding.getRoot().postDelayed(new Runnable() {
#Override
public void run() {
binding.textView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
}, 300);
full: https://github.com/dotrinh-PM/AndroidTalkback

Dynamically setting passwordMask in Titanium

Since Titanium doesn't allow you to manually change the hintText colour of a textfield, I have to set hintText manually. Because of this, I have to dynamically change the passwordMask setting on one of fields I'm using.
However, I'm getting weird behaviour and I can't tell if I'm doing something wrong, or if it's a bug in Titanium.
So, here's my markup:
<TextField id="password" onFocus="passwordFocusEvent" onReturn="passwordReturnEvent" onBlur="passwordBlurEvent" value="password"></TextField>
And some of my controller code:
function passwordFocusEvent(e) {
slideViewUp();
if (e.value === 'password') {
e.source.setPasswordMask(true);
e.source.value = '';
}
}
function passwordBlurEvent(e) {
if (!e.value) {
e.source.setPasswordMask(false);
e.source.value = 'password';
}
}
function passwordReturnEvent(e) {
slideViewDown();
passwordBlurEvent(e);
}
What happens is bizarre. When I focus on the password field, it remains plain text. I enter some text, then click off to another field, stays as plain text.
I click back to the password field, it's STILL plain text.
Now here's the weirdness. Up to this point, I would just assume it's not working. However, when I click off this second time, the passwordMask is set.
Major WTF.
I even tried targeting the field directly using $.password.passwordMask = true; but same thing.
Unfortunately, you cant do this. According to the docs on Ti.UI.TextField in the fine print;
Note: on iOS, passwordMask must be specified when this text field is created.
Its not all bad news though, there are a couple ways you can approach this, one option is to make the password mask yourself, by listening to the change event:
var theStoredPassword = '';
$.password.addEventListener('change', function(e) {
var newpass = e.source.value;
if(newpass.length < theStoredPassword.length) {
// Character deleted from end
theStoredPassword = theStoredPassword.substring(0, theStoredPassword.length-1);
} else {
// Character added to end
theStoredPassword += newpass.substring(newpass.length-1);
}
// Mask the text with unicode ● BLACK CIRCLE, 25CF
$.password.value = new Array(newpass.length + 1).join('●');
});
Another option, would be to have two text fields and swap them out whenever the user focuses the password field, the top one would have the custom hinttext, the bottom one would be passwordMasked. In fact thats probably way easier than what I just coded up. :-)