With RecyclerView, I can make some ViewHolder not recyclable (follow some answers in
I want my RecyclerView to not recycle some items).
Can I make LazyColumn to not recompose some items (similar to make RecyclerView don't recycle some ViewHolder)? I have few items in LazyColumn with some big images, it recompose after scrolling down and up so scroll is not smooth.
I met the same problem and use Column instead with a modifier vertical scroll. If you don't want it recycle view, just load all ( few items)
Column(
modifier = Modifier
.constrainAs(listView) {
top.linkTo(
parent.top
)
}
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
list.forEachIndexed { index, itemModel ->
ItemView(itemModel, index) {
// on item click
}
}
Spacer(modifier = Modifier.height(40.dp))
}
I had a similar issue with a composable that is heavily manipulating bitmaps and then draw them on a canvas. You noticed that processing while scrolling the lazyColumn up and down.
To face this issue a stored my manipulated bitmaps in a List<ImageBitmap> as rememberSaveable
val rememberFruits by rememberSaveable(images) {
mutableStateOf(doBitmapOperations(images))
}
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(height)
.constrainAs(circle) {}
) {
rememberFruits
.forEach { createScaledImageBitmap ->
val (image, offset) = createScaledImageBitmap
drawImage(
image = image,
topLeft = offset
)
}
}
My item in the lazyColumn was still recomposed but the heavy bitmap operations were no more executed while scrolling and making scrolling up and down smooth.
hope it helps!
Related
I'm currently trying to recreate a behavior, upon adding a new element to a LazyColumn the items start shifting to the right, in order to represent a Tree and make the elements easier to read.
The mockup in question:
Documentation
Reading through the documentation of Jetpack Compose in Lists and grids I found the following.
Keep in mind that cases where you’re nesting different direction layouts, for example, a scrollable parent Row and a child LazyColumn, are allowed:
Row(
modifier = Modifier.horizontalScroll(scrollState)
) {
LazyColumn {
// ...
}
}
My implementation
Box(Modifier.padding(start = 10.dp)) {
Row(
modifier = Modifier
.horizontalScroll(scrollState)
.border(border = BorderStroke(1.dp, Color.Black))
) {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
for (i in 0..25) {
item {
OptionItem(Modifier.padding(start = (i*20).dp))
}
item {
TaskItem(Modifier.padding(start = (i*10).dp))
}
}
}
}
.
.
.
}
OptionItem represents the element with the dot at the beginning, and TaskItem the other one.
When testing the LazyColumn, it appears as if instead of having a fixed size, the size of the column starts growing just after the elements have gone outside the screen, this causes a strange effect.
As you can see in the GIF, the width of the column starts increasing after the elements no longer fit in the screen.
The Question
I want to prevent this effect from happening, so is there any way I could maintain the width of the column to the maximum all the time?
The reason that applying a simple fillMaxWidth will not work because you are telling a composable to stretch to max, but that is impossible because the view itself can stretch indefinitely since it can be horizontally scrollable. I'm not sure why do you want to prevent this behavior but perhaps maybe you want your views to have some initial width then apply the padding, while maintaining the same width. what you can do in such case is simply give your composables a specific width, or what you can do is to get the width of the box and apply them to your composables by width (i used a text in this case)
val localDensity = LocalDensity.current
var lazyRowWidthDp by remember { mutableStateOf(0.dp) }
Box(
Modifier
.padding(start = 10.dp)
.onGloballyPositioned { layoutCoordinates -> // This function will get called once the layout has been positioned
lazyRowWidthDp =
with(localDensity) { layoutCoordinates.size.width.toDp() } // with Density is required to convert to correct Dp
}
) {
val scrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(scrollState)
) {
items(25) { i ->
Text(
text = "Hello",
modifier = Modifier
.padding(start = (i * 20).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
items(25) { i ->
Text(
text = "World",
modifier = Modifier
.padding(start = (i * 10).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
}
}
Edit:
you can apply horizontal scroll to the lazy column itself and it will scroll in both directions
I got this error message, And I don't get it.
java.lang.IllegalState
Exception: Vertically scrollable component was measured with an
infinity maximum height constraints, which is disallowed. One of the
common reasons is nesting layouts like LazyColumn and
Column(Modifier.verticalScroll()). If you want to add a header before
the list of items please add a header as a separate item() before the
main items() inside the LazyColumn scope. There are could be other
reasons for this to happen: your ComposeView was added into a
LinearLayout with some weight, you applied
Modifier.wrapContentSize(unbounded = true) or wrote a custom layout.
Please try to remove the source of infinit
e constraints in the hierarchy above the scrolling container.
Here's example code.
#Composable
fun SupplementSearchScreen(
onSearch: (String) -> List<Vitamin>,
updateRecentAddedSupplementList: List<Vitamin>
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
) {
item{
SupplementSearchResultCard(someList)
}
}
}
#Composable
fun SupplementSearchScreen(list:List<SomeList>){
ContentCard(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Hello World",
fontSize = 16.sp,
color = Color.Black
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
items(list){ resultItem ->
SearchResultItem(resultItem)
}
}
}
}
What's wrong here?
Lazy column needs fixed height component as parent to achieve lazy behavior,
if add lazy column inside parent with modifier Modifier.verticalScroll() or another Lazy column it will get infinite height it wont be able to decide when to compose and when to dispose an composable, instead use items{} of block of lazy column
in xml also same issue appears if wrap recyclerview inside nested scroll view but in xml instead of showing error it cause ANR as items increases in Recycler view adapter
You can use:
Modifier.height(LocalConfiguration.current.screenHeightDp.dp)
on the first lazy column to get all screen height.
I don't know if something like this will work for you but just to try it out.
You can't directly use LazyColumn inside LazyColumn. Just use LazyColumn with different types like this:
sealed interface Item {
data class SearchCard(val name: String) : Item
data class SearchText(val name: String): Item
}
#Composable
fun SearchScreen(list: List<Item>) {
LazyColumn {
items(list.size) { index ->
when (list[index]) {
is Item.SearchCard -> TODO()
is Item.SearchText -> TODO()
}
}
}
}
Inside a scrollable column I have a horizontal pager with two pages on it. One of them contains a very long list of content, while the other one is much shorter. On the smaller one I have a huge area at the bottom that I can scroll down on with no content, I suspect because of the height of the long list. Also, if I scrolled to the bottom on the longer page, and then swipe to the other page, then I just get a blank screen, and have to scroll far up to the actual content.
How can I limit the user from scrolling down below the actual content on the shorter page?
Column(
modifier = Modifier
.align(Alignment.TopStart)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
someOtherContent() // for example around 400.dp height
HorizontalPager(
state = pagerState,
verticalAlignment = Alignment.Top
) { i ->
when (i) {
0 -> {
Column(
modifier = Modifier.fillMaxWidth().height(450.dp)
) {
... not too much content
}
}
1 -> {
Column(
modifier = Modifier.fillMaxWidth().height(2000.dp)
) {
... many rows of content
}
}
}
}
}
Currently I'm trying to develop a special kind of image carousel which basically has two rows (one at the top with the images, and another one (shorter in width) at the bottom that use names for reference)
The thing is that I need to scroll the top row faster than the bottom one to achieve this with Jetpack Compose (can be done in regular Android with just some scroll listeners)
I was able to achieve scroll the rows simultaneously but they are scrolling at the same speed. I need to scroll the first one twice as fast to achieve the effect I want.
Here's the code I tried.
val scrollState = rememberScrollState()
Row(modifier = Modifier.horizontalScroll(scrollState)) {
Images()
}
Row(modifier = Modifier.horizontalScroll(scrollState)) {
Legend()
}
If you only need one Row to be scrollable, you can create new ScrollState each time.
val scrollState = rememberScrollState()
Row(modifier = Modifier.horizontalScroll(scrollState)) {
repeat(100) {
Text(it.toString())
}
}
Row(
modifier = Modifier.horizontalScroll(
ScrollState(scrollState.value * 2),
enabled = false
)
) {
repeat(200) {
Text(it.toString())
}
}
Note that this solution may not be the best in terms of performance, as it creates a class on each pixel scrolled.
An other solution is to sync them with LaunchedEffect. It'll also allow you both way scrolling synchronization.
#Composable
fun TestScreen(
) {
val scrollState1 = rememberScrollState()
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.horizontalScroll(scrollState1)
) {
repeat(100) {
Text(it.toString())
}
}
val scrollState2 = rememberScrollState()
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.horizontalScroll(scrollState2)
) {
repeat(200) {
Text(it.toString())
}
}
SyncScrollTwoWay(scrollState1, scrollState2, 2f)
}
#Composable
fun SyncScrollTwoWay(scrollState1: ScrollState, scrollState2: ScrollState, multiplier: Float) {
SyncScroll(scrollState1, scrollState2, multiplier)
SyncScroll(scrollState2, scrollState1, 1 / multiplier)
}
#Composable
fun SyncScroll(scrollState1: ScrollState, scrollState2: ScrollState, multiplier: Float) {
if (scrollState1.isScrollInProgress) {
LaunchedEffect(scrollState1.value) {
scrollState2.scrollTo((scrollState1.value * multiplier).roundToInt())
}
}
}
But it also have one downside: there'll be little delay in scrolling, so the second scroll view will always be one frame behind the scrolling one.
In EpoxyRecyclerView with Horizontal LinearLayout there is a Snap to center feature which works like, If i scroll the list with good speed, it keeps on scrolling until it slows down and rests with an item at center. And if I scroll slowly and lift up the finger, then the next item spans/moves to center of screen. One thing you have to understand that, this is not a Pager. Pager automatically snaps the next item only. But I cannot scroll like a free rolling...
You can see this gif as an example
So, I'm looking for such snapping feature in Jetpack Compose. Is this possible yet? If yes, how to achieve this?
You can use this library as well https://github.com/chrisbanes/snapper
https://chrisbanes.github.io/snapper/
val lazyListState = rememberLazyListState()
LazyRow(
state = lazyListState,
flingBehavior = rememberSnapperFlingBehavior(lazyListState),
) {
// content
}
If you use Compose 1.3 you can check the SnapFlingBehavior here
You can try out this library: https://github.com/aakarshrestha/compose-pager-snap-helper
The code will look as follows (using LazyRow for listing the items)
ComposePagerSnapHelper(
width = 320.dp, //required
content = { listState -> //this param is provided by the method itself, add this param below.
LazyRow(
state = listState, //add listState param
) {
items(count = count) { item ->
//Put your Items Composable here
}
}
}
)
Starting with compose 1.3.0 you can use the FlingBehavior that performs snapping of items to a given position:
val state = rememberLazyListState()
LazyRow(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
state = state,
flingBehavior = rememberSnapFlingBehavior(lazyListState = state)
) {
//item content
}