https://kotlinlang.org logo
#compose
Title
# compose
c

Colton Idle

08/04/2021, 4:21 AM
How can I place an image on the screen with a concrete height (140.dp in this case) which then affects the width of the image, but I want to offset the image alllllll the way on the left. Right now I hardcoded it to -200.dp and it works great on my pixel5, but running it on the emulator, it doesn't get pushed off the screen entirely. Do I have to try to measure the width somehow after height is applied and set offset that way?
Copy code
Box() {
    Image(
        modifier = Modifier.height(140.dp).offset(x = (-200).dp),
        painter = painterResource(id = com.myapp.myresources.R.drawable.catdog),
        contentDescription = null)
}
m

ms

08/04/2021, 4:23 AM
set
contentAlignment
to Start for Box, that may help
c

Colton Idle

08/04/2021, 4:25 AM
For reference... this is what I'm trying to achieve as the start position of my image (circle)
a

Albert Chang

08/04/2021, 4:36 AM
I think using a custom layout or a layout modifier would be the simplest way. You just measure the image and do
placeable.place(-placeable.width, 0)
.
1
c

Colton Idle

08/04/2021, 4:50 AM
Gotcha. I was looking towards using offset because I'm going to animate this offset from off of the screen to on screen, bit by bit. In that case would you say a custom layout would still be the way to go?
Basically I'm looking to animate it from off the screen... to on & off the screen, to middle of the screen every time the user presses a continue button at the bottom of the screen.
a

Albert Chang

08/04/2021, 4:52 AM
You can animate the offset the same way.
Copy code
val offset = remember { Animatable(0) }
// In layout block
placeable.place(offset.value - placeable.width, 0)
c

Colton Idle

08/04/2021, 4:53 AM
Thanks. I will try that. Cheers
Cool. I think I got something usable.
Copy code
private fun Modifier.animatedXAxisPeak(offset: Float) = layout { measurable, constraints ->
    val placeable = measurable.measure(constraints)

    layout(placeable.width, placeable.height) {
        placeable.place(x = ((offset - (placeable.width)).toInt()), y = 0)
    }
}
I haven't figured out how to do the animated offset yet. I thought this would work, but I'll keep trying.
Copy code
Box() {
    val offset = remember { Animatable(0F) }

    if (currentPage == 1) {
        offset.targetValue = 50F
    } else if (currentPage == 2) {
        offset.targetValue = 100F
    }

    Image(
        modifier =
            Modifier.height(140.dp)
                .animatedXAxisPeak(offset = offset.value),
a

Albert Chang

08/04/2021, 5:14 AM
You should use
animateTo()
on the
Animatable
. Also if your animation is state-based you may want to use
animateFloatAsState
.
n

Nader Jawad

08/04/2021, 5:20 AM
You should be able to pass an
Alignment
parameter to the
Image
composable with a value of -1, 0 for the horizontal and vertical alignment respectively. This will align the image to the left side within the bounds of the Image composable. An alignment of 0,0 means to center the contents of the drawn image within the composable bounds. Alignment is intended to be used for positioning or parallax effects within the Image composable.
c

Colton Idle

08/04/2021, 5:22 AM
Oooh. Interesting. That could work as well as I very much want this to be a percentage based thing. I will try that now Nader. @Albert Chang I was able to get it to move/animate successfully with your help though.
Copy code
Box {
    val offset = remember { Animatable(0F) }

    val scope = rememberCoroutineScope()

    if (currentPage == 1) {
        scope.launch { offset.animateTo(50F) }
    } else if (currentPage == 2) {
        scope.launch { offset.animateTo(100F) }
    }

    Image(
        modifier = Modifier.height(140.dp).animatedXAxisPeak(offset.value),
The only issue is the "animateTo" calls are just guesses of mine. I'm going to see if the alignment param makes things easier.
@Nader Jawad how do you rec passing in an alignment param?
Copy code
Image(
    alignment = Alignment(-1, 0),
?
m

ms

08/04/2021, 5:28 AM
@Colton Idle try
BiasAlignment
n

Nader Jawad

08/04/2021, 5:30 AM
@Colton Idle that's correct.
Alignment(-1, 0)
as you had in your snippet
c

Colton Idle

08/04/2021, 5:31 AM
Hm. Maybe that api changed Nader as that doesn't compile.
n

Nader Jawad

08/04/2021, 5:31 AM
Since the alignment parameters are just floats you should be able to animate them using animatable
Sorry that got renamed since the last time I looked at it. It should be
BiasAlignment
as @ms suggested
Alignment
is the interface and
BiasAlignment
is an implementation of that interface
👍 1
c

Colton Idle

08/04/2021, 5:38 AM
Hm
Copy code
Image(
    alignment = BiasAlignment(-1F, 0F),
    modifier = Modifier.height(140.dp),
but the image still showed up dead center 🤔
n

Nader Jawad

08/04/2021, 5:47 AM
You will probably also need to change the ContentScale parameter configured on the Image composable. By default it is set to
ContentScale.Fit
which will automatically scale the image to fit within the bounds of the composable which won't end up needing to handle any alignment. As you mentioned earlier that the height is to be fixed and the image is expected to be wider than the height, you could use
ContentScale.FillHeight
instead. This will ensure the image fills the height of the Image composable but will crop the width of the image being drawn. This is positioned by the alignment parameter you have provided as well.
a

Albert Chang

08/04/2021, 5:49 AM
@Nader Jawad Is it possible to align the image completely out of the bounds using
BiasAlignment
?
n

Nader Jawad

08/04/2021, 5:52 AM
I believe we intentionally did not bound the BiasAlignment API to prevent rendering completely out of bounds. I'm not by my computer at the moment to verify
So it should be possible
c

Colton Idle

08/04/2021, 5:53 AM
This also did not work.
Copy code
Image(
    alignment = BiasAlignment(-1F, 0F),
    contentScale = ContentScale.FillHeight,
    modifier = Modifier.height(140.dp)
I will keep trying different content scales...
a

Albert Chang

08/04/2021, 5:55 AM
@Nader Jawad Yeah I think the image can be rendered out of bounds but the problem is what bias you should use when you want it be completely out of bounds. I think the bias value can only be calculated based on image width and available space.
n

Nader Jawad

08/04/2021, 5:56 AM
@Colton Idle What is the size of the image asset that you are trying to render? If the contents of the image are smaller than the dimensions given to the composable, there is nothing to align as it can fit within the composable bounds so there isn't a need to pan the content. Might be worth experimenting with a smaller composable height to verify.
m

ms

08/04/2021, 5:56 AM
I think the Image us taking required size, try to make it take full width so that it can align itself
n

Nader Jawad

08/04/2021, 5:57 AM
@Albert Chang the bias value is always normalized and based off of the size of the image itself
c

Colton Idle

08/04/2021, 5:57 AM
The image is actually this. lol And it's technically wider than the device. But I'm trying to have the picture not be on the screen, and every step of a 8 step flow I move it across the screen until it fully dissapears on the other side. 😄
n

Nader Jawad

08/04/2021, 5:59 AM
-1 will left align the image so you would end up seeing the "cat" of catdog (wow that's a throwback, I remember watching that show as a kid). Might be worth experimenting with values smaller than -1 (ex. -2 etc.) to get the desired positioning. Also might be worthwhile to share the full composable source you're working with if you can.
👍 1
c

Colton Idle

08/04/2021, 5:59 AM
I set a height on the image in dp, because I want the width to auto scale correctly (which works), but now I want to place it off the screen, and then move it from the left of the screen to the right (off the screen as well), hence why percentages would work nicely.
Copy code
Box() {
    Image(
        alignment = BiasAlignment(-1F, 0F),
        contentScale = ContentScale.FillHeight,
        modifier = Modifier.height(140.dp),
        painter = painterResource(id = com.myapp.myresources.R.drawable.catdog),
        contentDescription = null)
}
This is what I have currently, and it seems like it's just smack in the middle of the screen. I will try with -2 etc, as Nader suggested.
a

Albert Chang

08/04/2021, 6:04 AM
@Nader Jawad What I wanted to say is that I guess you can use some values to put the image at some point left of bounds but I don't think it's possible to put it just out of bounds (the behavior of
placeable.place(-placeable.width, 0)
).
c

Colton Idle

08/04/2021, 6:05 AM
Interesting. 10f almost works. As in... the dog is almost completely off the left edge of the screen. Can see a bit of his purple nose. Weird. I thought for sure the float here would be percantage based out of 100, but at this point idk what's going on.
Okay. This "works". Again, a little hacky because 10f isn't perfectly off the screen. I will need to play around to see what 100% off the screen is and then test on multiple devices to make sure it holds true on them.
Copy code
Box {
    val offset = remember { Animatable(10F) }

    val scope = rememberCoroutineScope()

    when (currentPage) {
        0 -> scope.launch { offset.animateTo(10F) }
        1 -> scope.launch { offset.animateTo(7F) }
        2 -> scope.launch { offset.animateTo(5F) }
        3 -> scope.launch { offset.animateTo(2F) }
        4 -> scope.launch { offset.animateTo(-5F) }
        5 -> scope.launch { offset.animateTo(-8F) }
    }

    Image(
        alignment = BiasAlignment(offset.value, 0F),
        contentScale = ContentScale.FillHeight,
        modifier = Modifier.height(140.dp)
n

Nader Jawad

08/04/2021, 6:10 AM
@Albert Chang in this case, the Alignment parameter is not used as part of layout, but rather translates the drawscope to draw the image within the previously sized bounds. The Alignment parameter only affect drawing of the painter provided to the Image composable but not the sizing
c

Colton Idle

08/04/2021, 6:12 AM
Yeah, on my physical device, I need to change the 10 to a 60 to get the same effect. I guess I can't use this method. edit: unless this is a bug with biasAlignment. I guess I could file a bug?
n

Nader Jawad

08/04/2021, 6:15 AM
I don't think this is an issue with BiasAlignment per se. It starts by calculating the center point of the size of the image within the bounds of the composable and shifts to the left or right based on values from -1 to 1. So -1 ends up translating the contents right by 50% of the image size. Would have to work backwards to calculate the bias for it to be fully off screen
a

Albert Chang

08/04/2021, 6:23 AM
By the way you can make the offset percent-based using the layout way. Something like this should work:
Copy code
val offsetPercent by animateFloatAsState(
    targetValue = when (currentPage) {
        0 -> 0f
        1 -> 0.2f
        2 -> 0.4f
        3 -> 0.6f
        4 -> 0.8f
        else -> 1f
    }
)
Image(
    painter = painterResource(id = com.myapp.myresources.R.drawable.catdog),
    contentDescription = null,
    modifier = Modifier
        .height(140.dp)
        .layout { measurable, constraints ->
            val placeable = measurable.measure(constraints)
            layout(constraints.maxWidth, placeable.height) {
                placeable.place(
                    x = lerp(-placeable.width, constraints.maxWidth, offsetPercent),
                    y = 0
                )
            }
        }
)
c

Colton Idle

08/04/2021, 6:25 AM
@Albert Chang which lerp import did you use?
a

Albert Chang

08/04/2021, 6:27 AM
androidx.compose.ui.util.lerp
in
androidx.compose.ui:ui-util
. Or you can just copy the function:
Copy code
fun lerp(start: Float, stop: Float, fraction: Float): Float {
    return (1 - fraction) * start + fraction * stop
}
c

Colton Idle

08/04/2021, 6:32 AM
Thanks. That almost works. The face of the cat (left side of image) got chopped off though. Interesting. Need to get some sleep. Stuff like this makes me feel like I still don't understand compose anims at all, but I'm sure I'll get it eventually.
Another small note. Going alberts approach also pins the image to the bottom of the bounding box throwing off my spacing/padding on top of the image. /shruggie
a

Albert Chang

08/04/2021, 6:50 AM
Ok so your image is wider than the screen. In that case you might want to measure it with a infinite width instead:
measurable.measure(constraints.copy(maxWidth = Constraints.Infinity))
.
n

Nader Jawad

08/04/2021, 7:08 AM
I think the simplest approach will be to create your own alignment implementation that will give you the most fine grained control with the given image size and the Image composables dimensions. Something like the following that you configure on the Image composable's alignment property:
Copy code
val offscreenAlignment = object : Alignment {
            override fun align(
                size: IntSize,
                space: IntSize,
                layoutDirection: LayoutDirection
            ): IntOffset {
                return IntOffset(-size.width, 0)
            }
        }
👍 1
Note you don't want to change any of the layout properties, but rather you are defining how to position the png that you are drawing within the bounds/layout of the composable itself. You're effectively "panning" the png within the composable so changing constraints/layout here is not going to generate the desired effect
c

Colton Idle

08/04/2021, 3:38 PM
Ah. Thank you both @Albert Chang your solution worked and Nader your latest solution worked as well! I'm able to easily go the percentage route with both solutions, so now catdog is happily walking through the screen. Thank you again for dealing with this little exercise. I learned a ton from you all!
👍 2