1

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

Demo in the middle of scrolling

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?

Ostkontentitan
  • 6,930
  • 5
  • 53
  • 71
  • 1
    I answered a question to do similar behavior in a LazyColumn. Please, check if it helps you. https://stackoverflow.com/questions/72140863/itemdecoration-in-jetpack-compose/72143882#72143882 – nglauber Jun 07 '22 at 12:33

3 Answers3

2

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.

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
1

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

enter image description here

Thracian
  • 43,021
  • 16
  • 133
  • 222
1

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:

enter image description here

nglauber
  • 18,674
  • 6
  • 70
  • 75