The LazyRow LazyListState has the method animateScrollToItem which scrolls to the end of a list item
I want to scroll to the center of the listItem.
I can do that by passing a scrollOffset to the animateScrollToItem, something like listItem.width / 2 , but I was not able to figure out the width of the listItem I want to scroll.
The LazyListState provides the val visibleItemsInfo: List<LazyListItemInfo> list and I can do something like this
suspend fun LazyListState.animateScrollEndCenter(index: Int) {
val itemInfo = this.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
if (itemInfo != null) {
val center = this#animateScrollEndCenter.layoutInfo.viewportEndOffset / 2
val itemCenter = itemInfo.offset + itemInfo.size / 2
this#animateScrollEndCenter.animateScrollBy((itemCenter - center).toFloat())
} else {
this#animateScrollEndCenter.animateScrollToItem(index)
}
}
but visibleItemsInfo is nullable, so it does not have information about the listItem.width, until the listItem is presented on the screen
In the following example, I'm scrolling to the listItem 11
When is on the screen
https://i.stack.imgur.com/k0fk2.gif
When is out of the screen
https://i.stack.imgur.com/P554i.gif
I want to scroll to the center of the LazyRow listItem
Related
I want to display items in a horizontal list using RecyclerView. At a time, only 3 items will be displayed. 1 in the middle and the other 2 on the side, below is an image of what I'm trying to achieve:
I'm using LinearSnapHelper which centers an item all of the time. When an item is moved away from the center I would like the opacity to progessively change from 1f to 0.5f.
Here is the below code which I've written to help:
class CustomRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs) {
private var itemBoundsRect: Rect? = null
init {
itemBoundsRect = Rect()
addOnScrollListener(object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
calculateVisibility()
}
})
}
private fun calculateVisibility() {
val linearLayoutManger: LinearLayoutManager = layoutManager as LinearLayoutManager
val firstVisibleItem = linearLayoutManger.findFirstVisibleItemPosition()
val lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition()
var indexes: MutableList<Int> = mutableListOf()
for (i in firstVisibleItem..lastVisibleItem) {
indexes.add(i)
val item: View = layoutManager?.findViewByPosition(i) ?: continue
item.getGlobalVisibleRect(itemBoundsRect)
var itemSize = layoutManager!!.findViewByPosition(i)!!.width
var visibleSize = 0
if (indexes.size == 1) {
visibleSize = itemBoundsRect!!.right
} else {
visibleSize = itemBoundsRect!!.right - itemBoundsRect!!.left
}
var visibilty = visibleSize * 100 / itemSize
if (visibilty > 0) {
visibilty = 100 - visibilty
}
val viewHolder = findViewHolderForLayoutPosition(i)
viewHolder!!.itemView.alpha = (100 - visibilty).toFloat() / 100f
}
}
}
It doesn't work as expected as the opacity changes at the wrong time. The image below demonstrates this better. I expect the opacity to progressively begin to change when the item edges come out of the red box. However, it only starts when the item reaches the yellow edges.
Is there a way to achieve this effect?
Thank you :)
Your code for calculateVisibility() is looking at global position when looking at the relative position within the RecyclerView is sufficient. Maybe there is more to the code than you posted, but try the following. This code looks at the x position of each visible view and calculates the alpha value as a function of displacement from the center of the RecyclerView. Comments are in the code.
private fun calculateVisibility(recycler: RecyclerView) {
val midRecycler = recycler.width / 2
val linearLayoutManger: LinearLayoutManager = recycler.layoutManager as LinearLayoutManager
val firstVisibleItem = linearLayoutManger.findFirstVisibleItemPosition()
val lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition()
for (i in firstVisibleItem..lastVisibleItem) {
val viewHolder = recycler.findViewHolderForLayoutPosition(i)
viewHolder?.itemView?.apply {
// This is the end of the view in the parent's coordinates
val viewEnd = x + width
// This is the maximum pixels the view can slide left or right until it disappears.
val maxSlide = (midRecycler + width / 2).toFloat()
// Alpha is determined by the percentage of the maximum slide the view has moved.
// This assumes a linear fade but can be adjusted to fade in alternate ways.
alpha = 1f - abs(maxSlide - viewEnd) / maxSlide
Log.d("Applog", String.format("pos=%d alpha=%f", i, alpha))
}
}
}
The foregoing assumes that sizes remain constant.
if you need the center View, you can call
View view = snapHelper.findSnapView(layoutManagaer);
once you have the View, you should be able to get the position on the dataset for that View. For instance using
int pos = adapter.getChildAdapterPosition(view)
And then you can update the center View opacity and invoke
adapter.notifyItemChanged(pos);
I'd like to build a row in Jetpack Compose, with 3 elements, where the first and last elements are "stuck" to either sides, and the middle one stays in the center. The elements are not all the same width. It's possible for the first element to be really long, in which case I would like the middle item to move to the right, as much as possible. The images below hopefully illustrate what I mean:
All elements fit nicely
The first element is long and pushes the middle item to the right
The first element is super long, pushes the middle item all the way to the right and uses an ellipsis if necessary.
Wrapping each element in a Box and setting each weight(1f) helps with the first layout, but it doesn't let the first element to grow if it's long. Maybe I need a custom implementation of a Row Arrangement?
Ok, I managed to get the desired behaviour with a combination of custom implementation of an Arrangement and Modifier.weight.
I recommend you investigate the implementation of Arrangement.SpaceBetween or Arrangement.SpaceEvenly to get the idea.
For simplicity, I'm also assuming we'll always have 3 elements to place within the Row.
First, we create our own implementation of the HorizontalOrVertical interface:
val SpaceBetween3Responsively = object : Arrangement.HorizontalOrVertical {
override val spacing = 0.dp
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
layoutDirection: LayoutDirection,
outPositions: IntArray,
) = if (layoutDirection == LayoutDirection.Ltr) {
placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = false)
} else {
placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = true)
}
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#SpaceBetween3Responsively"
}
The placeResponsivelyBetween method needs to calculate the correct gap sizes between the elements, given their measured widths, and then place the elements with the gaps in-between.
fun placeResponsiveBetween(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
val gapSizes = calculateGapSize(totalSize, size)
var current = 0f
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
// here the element and gap placement happens
current += it.toFloat() + gapSizes[index]
}
}
calculateGapSize has to try and "place" the second/middle item in the centre of the row, if the first element is short enough. Otherwise, set the first gap to 0, and check if there's space for another gap.
private fun calculateGapSize(totalSize: Int, itemSizes: IntArray): List<Int> {
return if (itemSizes.sum() == totalSize) { // the items take up the whole space and there's no space for any gaps
listOf(0, 0, 0)
} else {
val startOf2ndIfInMiddle = totalSize / 2 - itemSizes[1] / 2
val firstGap = Integer.max(startOf2ndIfInMiddle - itemSizes.first(), 0)
val secondGap = totalSize - itemSizes.sum() - firstGap
listOf(firstGap, secondGap, 0)
}
}
Then we can use SpaceBetween3Responsively in our Row! Some code edited out for simplicity
Row(
horizontalArrangement = SpaceBetween3Responsively,
) {
Box(modifier = Modifier.weight(1f, fill = false)) {
Text(text = "Supercalifragilisticexplialidocious",
maxLines = 1,
overflow = TextOverflow.Ellipsis)
}
Box {
// Button
}
Box {
// Icon
}
}
Modifier.weight(1f, fill = false) is important here for the first element - because it's the only one with assigned weight, it forces the other elements to be measured first. This makes sure that if the first element is long, it's truncated/cut to allow enough space for the other two elements (button and icon). This means the correct sizes are passed into placeResponsivelyBetween to be placed with or without gaps. fill = false means that if the element is short, it doesn't have to take up the whole space it's assigned - meaning there's space for the other elements to move closer, letting the Button in the middle.
Et voila!
I have a VerticalPager inside HorizontalPager.
When I scroll VerticalPager down to the Nth page in the 1st page of the HorizontalPager, then scroll to other pages in HorizontalPager, then come back to the 1st page of the HorizontalPager the Nth page I scrolled down to in VerticalPager is saved.
I want always the 1st page of the VerticalPager (not the Nth page I scolled to) to be open whenever HorizontalPager is scrolled.
How can I achieve that?
My code:
val pagerState = rememberPagerState()
HorizontalPager(count = myList.size, state = pagerState) {
idx ->
myList[idx].let { cur ->
val verPagerState = rememberPagerState(initialPage = 0)
VerticalPager(
count = cur.photos.size,
state = verPagerState
) { page ->
}
}
}
Here's how you can scroll it manually when the page is no longer visible.
I use snapshotFlow, which creates a flow that emit values when the state used inside changes.
val verPagerState = rememberPagerState(initialPage = 0)
LaunchedEffect(Unit) {
snapshotFlow {
!pagerState.isScrollInProgress
&& pagerState.currentPage != idx
&& verPagerState.currentPage != 0
}.filter { it }
.collect {
verPagerState.scrollToPage(0)
}
}
I am using a LazyColumn and there are several items in which one of item has a LaunchedEffect which needs to be executed only when the view is visible.
On the other hand, it gets executed as soon as the LazyColumn is rendered.
How to check whether the item is visible and only then execute the LaunchedEffect?
LazyColumn() {
item {Composable1()}
item {Composable2()}
item {Composable3()}
.
.
.
.
item {Composable19()}
item {Composable20()}
}
Lets assume that Composable19() has a Pager implementation and I want to start auto scrolling once the view is visible by using the LaunchedEffect in this way. The auto scroll is happening even though the view is not visible.
LaunchedEffect(pagerState.currentPage) {
//auto scroll logic
}
LazyScrollState has the firstVisibleItemIndex property. The last visible item can be determined by:
val lastIndex: Int? = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
Then you test to see if the list item index you are interested is within the range. For example if you want your effect to launch when list item 5 becomes visible:
val lastIndex: Int = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
LaunchedEffect((lazyListState.firstVisibleItemIndex > 5 ) && ( 5 < lastIndex)) {
Log.i("First visible item", lazyListState.firstVisibleItemIndex.toString())
// Launch your auto scrolling here...
}
LazyColumn(state = lazyListState) {
}
NOTE: For this to work, DON'T use rememberLazyListState. Instead, create an instance of LazyListState in your viewmodel and pass it to your composable.
If you want to know if an item is visible you can use the LazyListState#layoutInfo that contains information about the visible items.
Since you are reading the state you should use derivedStateOf to avoid redundant recompositions and poor performance
To know if the LazyColumn contains an item you can use:
#Composable
private fun LazyListState.containItem(index:Int): Boolean {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
false
} else {
visibleItemsInfo.toMutableList().map { it.index }.contains(index)
}
}
}.value
}
Then you can use:
val state = rememberLazyListState()
LazyColumn(state = state){
//items
}
//Check for a specific item
var isItem2Visible = state.containItem(index = 2)
LaunchedEffect( isItem2Visible){
if (isItem2Visible)
//... item visible do something
else
//... item not visible do something
}
If you want to know all the visible items you can use something similar:
#Composable
private fun LazyListState.visibleItems(): List<Int> {
return remember(this) {
derivedStateOf {
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (layoutInfo.totalItemsCount == 0) {
emptyList()
} else {
visibleItemsInfo.toMutableList().map { it.index }
}
}
}.value
}
I have implemented a "page peek" feature for my ViewPager2:
private fun setViewPager() {
inventoryVp?.apply {
clipToPadding = false // allow full width shown with padding
clipChildren = false // allow left/right item is not clipped
offscreenPageLimit = 2 // make sure left/right item is rendered
}
inventoryVp?.setPadding(Utility.dpToPx(25), 0, Utility.dpToPx(25), 0)
val pageMarginPx = Utility.dpToPx(6)
val marginTransformer = MarginPageTransformer(pageMarginPx)
inventoryVp?.setPageTransformer(marginTransformer)
}
Doing this I am able to view a portion of the previous and next page. But first and last page show a bigger white space because there's no other page in this direction to show.
How can I set a different padding for the first and last page?
I solved it using ItemDecoration.
class CartOOSVPItemDecoration(val marginStart: Int,
val marginEnd: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (parent.getChildAdapterPosition(view) == 0) {
outRect.left = marginStart
}
else if(parent.getChildAdapterPosition(view) == ((parent.adapter?.itemCount ?: 0) - 1)) {
outRect.right = marginEnd
}
}
}
inventoryVp?.addItemDecoration( CartOOSVPItemDecoration(Utility.dpToPx(-9), Utility.dpToPx(-9)))