Jetpack Compose FAB hide on scroll behavior - kotlin

In the XML-based UI, FAB had a field to override the custom scroll behavior. Since there is no such parameter in the function in Compose FAB implementation, it became a little unclear how to implement the hide on scroll behavior.
I use Scaffold with the nestedScroll modifier in order for my СollapsingAppBar to work. I assumed at first that it was necessary to create an object inherited from the NestedScrollConnection interface. And also connect it with the nestedScroll modifier to Scaffold. But unfortunately, as I understand it, it is impossible to connect several scrollBehavior objects to one Scaffold at once.
val scrollBehavior = TopAppBarScrollBehavior()
val scrollBehavior1 = FloatingActionButtonScrollBehavior()
Scaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
//this one won't work
.nestedScroll(scrollBehavior1.nestedScrollConnection)
...

According to nestedScroll documentation it takes 2 arguments
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
): Modifier
NestedScrollDispatcher is needed if a component is able to receive and react to the drag/fling events. I think you have to implement NestedScrollDispatcher to coordinate FAB's visibility with scroll.

Related

Jetpack compose: Top App Bar pinnedScrollBehavior not changing color on scroll from text focus events

Okay, so this is a bit of a specific issue, but hopefully someone understands what's happening:
I'm using a Jetpack Compose Material 3 CenterAlignedTopAppBar, with TopAppBarDefaults.pinnedScrollBehavior to make it so that the color of the appbar changes when I scroll down on nested content.
It works! In most cases. However, one of my screens has a large text field, that when clicked causes the content to scroll down by itself (to focus on the text field)—and that seems to confuse the appbar, which doesn't change color. It will only change color if I manually scroll up and down.
Relevant code:
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
contentWindowInsets = EmptyWindowInsets,
modifier = Modifier
.fillMaxHeight()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
// just a wrapper around CenterAlignTopAppBar
StandardTopAppBar(scrollBehavior = scrollBehavior)
},
content = { innerPadding ->
// I've also tried with LazyColumn and see the same behavior
Column(
Modifier
.padding(innerPadding)
.padding(start = 10.dp, end = 10.dp)
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
tl;dr; manually scrolling changes the appbar color, but scrolling caused by clicking into a text field that scrolls into view does not. Any idea how I could fix this?
Probably not quite what you are looking for unfortunately, but I had a similar problem. My TopAppBar color was also stuck, but when navigating between different screens. I fixed it by assigning the TopAppBar a different scrollBehaviour depending on the screen.
val scrollBehavior1 = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val scrollBehavior2 = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
In TopAppBar arguments:
scrollBehavior =
when (currentRoute) {
Screens.ExampleScreen1.route -> scrollBehavior1
Screens.ExampleScreen2.route -> scrollBehavior2
else -> null
}
Hopefully this helps you in one way or another.

Why scrollable modifier doesn't scroll view content?

I am trying to get scrolling to work on a Column where the number of entries may exceed the height of the window.
I'm currently using Compose 1.1.0-rc03 and at the moment I'm only trying to get it working on desktop.
I reduced the problem to this:
#Composable
fun App() {
val optionsScrollState = rememberScrollState()
Row(modifier = Modifier.fillMaxSize()) {
// Left column
Column(
modifier = Modifier
.scrollable(optionsScrollState, Orientation.Vertical)
.width(240.dp)
.fillMaxHeight()
) {
(1..100).forEach { i -> Text("Row $i") }
}
}
}
But this doesn't scroll, or at least, not with the mousewheel. Maybe there's another way to scroll that isn't immediately apparent to me.
How do I make this work?
The docs on scrollable say that I might have to manage the state myself. So is using rememberScrollState() not enough?
I found some existing questions about disabling scrolling on columns, but they were always talking about LazyColumn which I'm not using here.
You're using a wrong modifier. From documentation:
The scrollable modifier differs from the scroll modifiers in that scrollable detects the scroll gestures, but does not offset its contents.
If you're interested how Modifier.scrollable should be used, you can find an example in the same documentation chapter.
You can use Modifier.verticalScroll instead, which will give you the expected behavior.
Also consider switching to LazyColumn, which already has built-in scrolling as well as lazy cell loading.

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.

How to change the fonts of all items on recyclerview runtime

I wanted to change font family of items on a recycler view every time I click a button.
So I coded like below.
rbAritaBuri = view.findViewById(R.id.rb_aritaBuri)
rbCafe24 = view.findViewById(R.id.rb_cafe24SurroundAir)
rbAritaBuri.setOnClickListener {
rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/arita_buri.otf")
}
rbCafe24.setOnClickListener {
rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/cafe24_surround_air.ttf")
}
But it changes only the font family of the first item of the recycler view.
Is there a way to change fonts of them all together runtime? And please tell me why the code I wrote doesn't work right.
Thank you.
If I were in your position, I would:
Put your font changing calls inside of onBindViewHolder(). If you have to, you could put a bool in there like buttonClicked and link its value to your buttons.
Come up with a good way to force a call to onBindViewHolder(). Sometimes notifyDataSetChanged() is enough. But in some cases, you might have to remove the adapter by setting it to null and then reset the adapter to its original value.
Place that logic from step 2 inside of your buttons' onClick()s.
Edit:
What I mean is, create a var inside the class with the most exterior scope, so outside of oncreate().
var textChoice=""
Now use your buttons to change that var.
rbAritaBuri.setOnClickListener {
textChoice="fonts/arita_buri.otf"
}
Now inside your onBindViewHolder(), make the font switch.
when (fontChoice){
"fonts/arita_buri.otf"->{ rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/arita_buri.otf")}
//and so on and so forth for all of your fonts
Now when you want to show the change, call notifyDatasetChanged(). I think maybe the best place to do that would be inside of your buttons. So maybe you'd actually have:
rbAritaBuri.setOnClickListener {
textChoice="fonts/arita_buri.otf"
<The name of your recyclerview adapter>.notifyDatasetChanged()
}
Here is how I solved it, thanks to D. Kupra:
class SampleWorkAdapter(private val context: Context) :
RecyclerView.Adapter<SampleWorkAdapter.ViewHolder>() {
var selectedFont = EditTextActivity.HAMBAK_SNOW
First, I assigned the default font Hambak_snow to selectedFont, type String.
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
fun changeFont(font: String) {
CustomFontHelper.setCustomFont(content, font, itemView.context)
} ...
}
Then I wrote a function to be called on onBindViewHolder to change font-family of textview, using custom typeface. https://stackoverflow.com/a/16648457/15096801 This post helped a lot.
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
...
viewHolder.changeFont(selectedFont)
...
}
Now, replaceFont will be called when the variable selectedFont get changed and adapter.notifyDatasetChanged() is called on an activity, like this:
rbMapoFlowerIsland.setOnClickListener {
sampleWorkAdapter.selectedFont = EditTextActivity.MAPO_FLOWER
sampleWorkAdapter.notifyDataSetChanged()
}