I want to have the total scroll position in px in LazyColumn.
The column itself has a ScrollState which has a value (Described in the document as the current scroll position value in pixels), But LazyColumn ScrollState hasn't this field, I want the exact equivalent of Column ScrollState scroll position value for LazyColumn, How I can achieve that with the help of LazyScrollState?
The reason it exists in Column is because it knows about all the items (rows) when it's being generated.
But LazyColumn only displays a portion of the total views at any given moment, and when you scroll, it continuously re-generates the views on the screen (plus a couple above and/or below the visible area), so it never actually calculates the total height of all the rows. If you wanted to get the scroll position, you would have to manually calculate it. If the height of each row is the same, it'll work pretty well. But if the heights are different, then you won't be able to get the exact scroll position (your calculations will fluctuate depending on the height of the rows that are currently displayed). But here's what you have to do to calculate it yourself:
Calculate the total size of all the items
val totalItems = lazyListState.layoutInfo.totalItemsCount
val itemLengthInPx = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull()?.size ?: 0
val totalLengthInPx = totalItems * itemLengthInPx
Calculate the current scroll position
val scrollPos = (lazyListState.firstVisibleItemIndex * itemLengthInPx) / totalLengthInPx
But as I mentioned earlier, this calculation depends on the height of each item (itemLengthInPx), and it works perfectly if it's the same for all the views. But you can see how it'll fluctuate if each view has a different height
The scroll amount can be obtained, no calculation required, in the items themselves, by attaching an onGloballyPosition{ it.positionInParent()} modifier to one or more items.
Then, the items can do what they need to do with their own scroll position, such as offsetting some screen-drawing y coordinate.
Or, if you need the scroll offset in the parent LazyColumn, you could have one item (perhaps an item with zero height, though I've not tested that) at the very top of your list of items, that reports back its position to the parent (perhaps by updating a mutableState that was passed to the item by the parent) whenever it moves.
I had the same need and onGloballyPosition{ it.positionInParent()} addressed it very nicely.
You can get it by firstVisibleItemScrollOffset.
('androidx.activity:activity-compose:1.3.1')
val listState = rememberLazyListState()
val itemHeight = with(LocalDensity.current) { 80.dp.toPx() } // Your item height
val scrollPos = listState.firstVisibleItemIndex * itemHeight + listState.firstVisibleItemScrollOffset
Text(text = "scroll: $scrollPos")
LazyColumn(state = listState) {
// Your items here
}
Also, you can set the scroll position to listState, like this:
LaunchedEffect(key1 = "Key") {
delay(1000) // To show scroll explicitly, set the delay
listState.scrollBy(itemHeight * 2)
}
Related
I have a Card as an expandable item in LazyColumn, the initial height I want is 50dp, and the max is WrapContaintHeight, I'm trying playing with Modifiers to set the min and the max, here is what I did
.height(50dp)
.heightIn(min = 50dp, max = WrapContaintHeight)
But it always previews the card height like WrapContaintHeight as the initial height, what I want is the initial height to be 50dp, and when I expand it by clicking on the card to be WrapContaintHeight, I already have a code to control the expand state
you need to create a boolean variable for click to item.
.height(if(isFullSize) 100.dp else 50.dp)
I have recently been trying to implement a calendar day view (see picture) with items laying on top of a recycler view
Example of part of the calendar view
Currently I calculate the offset the item needs to have from the top of the recycler view with this bit of code
val dpPerMinute = 24f / 15f
val viewOffset = 12f
initialOffset = (startTime * dpPerMinute + viewOffset + 4f).dpToPx().toInt()
The background item height is 24dp and is equal to 15 minutes, viewOffset is just to offset the view to the center of the itemHeight and the 4 comes from padding
Whenever I scroll in the view I update the position of the item with this
val pos = -offset + initialOffset
view?.translationY = pos.toFloat()
Offset is the recycler views current scroll Y and initialOffset is calculated above
This works great on my Samsung, everything lines up perfectly. But then comes the problem, I tested it on another work phone (as we do, a OnePlus Nord) and now everything isn't lining up right. The offset becomes more off the further down it is.
Could this be a problem with converting dp to pixels on different phones? Has anyone else encountered the same problem as me with other phones and if that is the case how did you solve it?
The function converting dp to px looks like following:
#JvmOverloads #Dimension(unit = Dimension.PX) fun Number.dpToPx(
metrics: DisplayMetrics = Resources.getSystem().displayMetrics
): Float {
return toFloat() * metrics.density
}
In case anyone else has a similar problem, problem was dpToPx() not rounding the number correctly. I simply just rounded the converted number after it was calculated, solved the problem
I have a Lazy column like so
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(gridMultiple(i = 15) * data.size)
.padding(vertical = gridMultiple(i = 2)),
verticalArrangement = Arrangement.spacedBy(gridMultiple(i = 2)),
userScrollEnabled = false
) {
items(data.size) { index ->
val item = data[index]
ListItem(
item,
editButtonClick = { id -> onEditClick(id) }
)
}
}
and it is encapsulated in an interop composable XML element like so:
<androidx.compose.ui.platform.ComposeView
android:id="#+id/informationList"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
The catch is that the informationList is inside of a ScrollView (XML), and I want the lazy column list to be fully expanded within the scroll view. But I do not know the height that the elements need to be, so I am doing this math here:
.height(gridMultiple(i = 15) * data.size)
But it uses either too much space or not enough. So either elements get cut off, or they have a lot of empty space at the end. I want to use exactly as much space as the LazyColumn needs. If I set the height to be wrapping, I get an exception that the lazy column has an infinite possible height and must have a height specified. There must be a way to do this. I know that the max amount of elements that can ever be in the lazy column is limited to about 10.
That will defeat the whole purpose of LazyColumn. The lazy part just won't work when you use it like that. You could just go with normal Column and it would make no difference at all in terms of performance. Especially with 10 items, with such number the LazyColumn is huge overkill. Bonus for your case - Column supports wrapContentHeight().
TLDL - just use Column with wrapContentHeight() and it should be just fine
I'm using a horizontal RecyclerView with PagerSnapHelper to make it look like a ViewPager. It's displaying CardViews.
My question is, how can I make it show a small peek of the edge of the next and previous card? The user needs to see a little of those cards so they understand intuitively that they need to swipe horizontally so they can view other cards.
I'll also note that the current card always needs to be centered, even for the first one, which would not have a previous card peeking on the left. Also, the design requirements are out of my control; I need to peek, I can't use dot indicators or anything else.
I could use a LinearSnapHelper and make the width of the cards smaller, but then 1) the first item will be left-aligned instead of centered, since there's no card peeking on the left side and 2) how much of each card displays would vary based on the width of the phone.
This seems like it should be a common and simple task, so I hope I'm missing something obvious to make it happen.
After some trial and error, I found a solution. Fortunately, it wasn't as complicated as I thought, although not as clean as I hoped.
So start with a RecyclerView and its Adapter, use a LinearLayoutManager with a Horizontal orientation, add in a PagerSnapHelper and so on... then to fix the specific issue in this question I made some adjustments to the adapter:
private var orientation: Int? = null
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
orientation = (recyclerView.layoutManager as LinearLayoutManager).orientation
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Kludge to adjust margins for horizontal, ViewPager style RecyclerView
if (orientation != LinearLayout.VERTICAL) {
holder.itemView.layoutParams = (holder.itemView.layoutParams as RecyclerView.LayoutParams).apply {
val displayMetrics = DisplayMetrics()
baseActivity.windowManager.defaultDisplay.getMetrics(displayMetrics)
// To show the edge of the next/previous card on the screen, we'll adjust the width of our MATCH_PARENT card to make
// it just slightly smaller than the screen. That way, no matter the size of the screen, the card will fill most of
// it and show a hint of the next cards.
val widthSubtraction = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40f, displayMetrics).toInt()
width = displayMetrics.widthPixels - widthSubtraction
// We always want the spot card centered. But the RecyclerView will left-align the first card and right-align the
// last card, since there's no card peeking on that size. We'll adjust the margins in those two places to pad it out
// so those cards appear centered.
// Theoretically we SHOULD be able to just use half of the amount we shrank the card by, but for some reason that's
// not quite right, so I'm adding a fudge factor developed via trial and error to make it look better.
val fudgeFactor = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6f, displayMetrics).toInt()
val endAdjustment = (widthSubtraction / 2) - fudgeFactor
marginStart = if (position == 0) endAdjustment else 0
marginEnd = if (position == (itemCount - 1)) endAdjustment else 0
}
}
}
Of course, you'll want to change 40f and 6f to the appropriate values for your use case.
In case you're wondering, I have the orientation check because in my case, the same adapter is used for two different RecyclerViews. One is a simple vertical list. The other is horizontal and functions like a ViewPager, which greatly increased the complexity.
I have a customlist in which I overload the getView function and I need to know the y position of the current item that is being drawn. And I need it in relation to the entire list and not just the ones visible on screen. How can I find this out?
Edit: The Items have different heights.
Since you want the one relative to the top of the list (not the top of the visible screen, you can calculate it using:
int yPos = view.getHeight() * position;
Edit:
Different heights? That probably hurts performance, since the recycler isn't helping much.
I don't think there's a magic method for this case. Realistically what you have to do is keep an array of list position and y offsets, so for instance
// Item at position 0, top starts at 0, height is 15
pos[0][0] = 0
pos[0][1] = 15
// Next item starts one pixel down from previous item's top, is 10 pixels tall
pos[1][0] = 16
pos[1][1] = 10
And just populate from within getView. Note that this solution tracks individual height as well as absolute offset for each item- since the previous item's position + height is required to determine the next item's position.