Reputation: 7020
I am trying to create a parallax effect with a wide image lets say: https://placekitten.com/2000/400
On top of it i show a LazyRow with items. Whilst the user goes through those i would like to offset the image so that it 'moves along' slowly with the items.
The image should basically FillHeight and align to the Start so that it can move left to right.
The calculation part of the offset is done and works as it should. So does overlaying the lazy row. Now displaying the image properly is where i struggle.
I tried variations of this:
Image(
modifier = Modifier
.height(BG_IMAGE_HEIGHT)
.graphicsLayer {
translationX = -parallaxOffset
},
painter = painter,
contentDescription = "",
alignment = Alignment.CenterStart,
contentScale = ContentScale.FillHeight
)
Unfortunately though the rendered image is chopped off at the end of the initially visible portion so when the image moves there is just empty space coming up.
DEMO
As you can see while going through the list white space appears on the right instead of the remaining image.
How do i do this properly?
Upvotes: 2
Views: 962
Reputation: 24044
I'm leaving my solution here...
@Composable
private fun ListBg(
firstVisibleIndex: Int,
totalVisibleItems: Int,
firstVisibleItemOffset: Int,
itemsCount: Int,
itemWidth: Dp,
maxWidth: Dp
) {
val density = LocalDensity.current
val firstItemOffsetDp = with(density) { firstVisibleItemOffset.toDp() }
val hasNoScroll = itemsCount <= totalVisibleItems
val totalWidth = if (hasNoScroll) maxWidth else maxWidth * 2
val scrollableBgWidth = if (hasNoScroll) maxWidth else totalWidth - maxWidth
val scrollStep = scrollableBgWidth / itemsCount
val firstVisibleScrollPercentage = firstItemOffsetDp.value / itemWidth.value
val xOffset =
if (hasNoScroll) 0.dp else -(scrollStep * firstVisibleIndex) - (scrollStep * firstVisibleScrollPercentage)
Box(
Modifier
.wrapContentWidth(unbounded = true, align = Alignment.Start)
.offset { IntOffset(x = xOffset.roundToPx(), y = 0) }
) {
Image(
painter = rememberAsyncImagePainter(
model = "https://placekitten.com/2000/400",
contentScale = ContentScale.FillWidth,
),
contentDescription = null,
alignment = Alignment.TopCenter,
modifier = Modifier
.height(232.dp)
.width(totalWidth)
)
}
}
@Composable
fun ListWithParallaxImageScreen() {
val lazyListState = rememberLazyListState()
val firstVisibleIndex by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex
}
}
val totalVisibleItems by remember {
derivedStateOf {
lazyListState.layoutInfo.visibleItemsInfo.size
}
}
val firstVisibleItemOffset by remember {
derivedStateOf {
lazyListState.firstVisibleItemScrollOffset
}
}
val itemsCount = 10
val itemWidth = 300.dp
val itemPadding = 16.dp
BoxWithConstraints(Modifier.fillMaxSize()) {
ListBg(
firstVisibleIndex,
totalVisibleItems,
firstVisibleItemOffset,
itemsCount,
itemWidth + (itemPadding * 2),
maxWidth
)
LazyRow(state = lazyListState, modifier = Modifier.fillMaxSize()) {
items(itemsCount) {
Card(
backgroundColor = Color.LightGray.copy(alpha = .5f),
modifier = Modifier
.padding(itemPadding)
.width(itemWidth)
.height(200.dp)
) {
Text(
text = "Item $it",
Modifier
.padding(horizontal = 16.dp, vertical = 6.dp)
)
}
}
}
}
}
Here is the result:
Upvotes: 1
Reputation: 67443
You can do it by drawing image to Canvas
and setting srcOffset
to set which section of the image should be drawn and dstOffset
to where it should be drawn in canvas of drawImage
function
@Composable
private fun MyComposable() {
Column {
var parallaxOffset by remember { mutableStateOf(0f) }
Spacer(modifier = Modifier.height(100.dp))
Slider(
value = parallaxOffset, onValueChange = {
parallaxOffset = it
},
valueRange = 0f..1500f
)
val imageBitmap = ImageBitmap.imageResource(id = R.drawable.kitty)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.border(2.dp, Color.Red)
) {
val canvasWidth = size.width.toInt()
val canvasHeight = size.height.toInt()
val imageHeight = imageBitmap.height
val imageWidth = imageBitmap.width
drawImage(
image = imageBitmap,
srcOffset = IntOffset(
parallaxOffset.toInt().coerceAtMost(kotlin.math.abs(canvasWidth - imageWidth)),
0
),
dstOffset = IntOffset(0, kotlin.math.abs(imageHeight - canvasHeight) /2)
)
}
}
}
Result
Upvotes: 2
Reputation: 88457
Image
is too smart and doesn't draw anything beyond the bounds. translationX
doesn't change the bound but only moves the view.
Here's how you can draw it manually:
val painter = painterResource(id = R.drawable.my_image_1)
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(BG_IMAGE_HEIGHT)
) {
translate(
left = -parallaxOffset,
) {
with(painter) {
draw(Size(width = painter.intrinsicSize.aspectRatio * size.height, height = size.height))
}
}
}
I don't see your code that calculates parallaxOffset
, but just in case, I suggest you watch this video to get the best performance.
Upvotes: 2