I have one Image composable, then I retrieve its boundary box using onGloballyPositioned
listener. When I press a button a new Image is displayed, that have the same resId and initial position and size, so it matches the size of the original Image. And the original image gets hidden, while the copy image changes it locating using absoluteOffset
and its size using the width and height attributes. I am using LaunchedEffect, to generate float values from 0f to 1f, and then use them to change the position and size of the copied image.
Here is the result:
Everything works well, except the fact that there is some flickering, since we hide the original and show the copy image immediately, and probably there is a empty frame, when both images are recomposed at the same time. So the original image is hidden, but the copied image is still not show, so there is a frame where both images are invisible.
Is there a way I can set the the order in which the images are recomposed, so the copied image get its visible state, before the original image is hidden?
I saw that there is way to use key inside columns/rows from here. But I am not so sure it is related.
The other idea I got is to use opacity animation, so there can be a delay, something like
time | Original Image (opacity) | Copy Image (opacity)
-------|---------------------------|-----------------------
0s | 1 | 0
0.2s | 0.75 | 0.25
0.4s | 0.5 | 0.5
0.6s | 0.25 | 0.75
0.8s | 0.0 | 1
Also I know I can use single image to achieve the same effect, but I want to have separate image, that is not part of the compose navigation. So if I transition to another destination, I want the image to be transferred to that destination with smooth animation.
Here is the source code:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.core.TargetBasedAnimation
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.slaviboy.myapplication.ui.theme.MyApplicationTheme
class MainActivity : ComponentActivity() {
val viewModel by viewModels<ViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val left = with(LocalDensity.current) { 200.dp.toPx() }
val top = with(LocalDensity.current) { 300.dp.toPx() }
val width = with(LocalDensity.current) { 100.dp.toPx() }
viewModel.setSharedImageToCoord(Rect(left, top, left + width, top + width))
Box(modifier = Modifier.fillMaxSize()) {
if (!viewModel.isSharedImageVisible.value) {
Image(painter = painterResource(id = viewModel.setSharedImageResId.value),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.width(130.dp)
.height(130.dp)
.onGloballyPositioned { coordinates ->
coordinates.parentCoordinates
?.localBoundingBoxOf(coordinates, false)
?.let {
viewModel.setSharedImageFromCoord(it)
}
})
}
SharedImage(viewModel)
}
Button(onClick = {
viewModel.setIsSharedImageVisible(true)
viewModel.triggerAnimation()
}) {
}
}
}
}
@Composable
fun SharedImage(viewModel: ViewModel) {
var left by remember { mutableStateOf(0f) }
var top by remember { mutableStateOf(0f) }
var width by remember { mutableStateOf(330f) }
val anim = remember {
TargetBasedAnimation(
animationSpec = tween(1700, 0),
typeConverter = Float.VectorConverter,
initialValue = 0f,
targetValue = 1f
)
}
var playTime by remember { mutableStateOf(0L) }
LaunchedEffect(viewModel.triggerAnimation.value) {
val from = viewModel.sharedImageFromCoord.value
val to = viewModel.sharedImageToCoord.value
val fromLeft = from.left
val fromTop = from.top
val fromSize = from.width
val toLeft = to.left
val toTop = to.top
val toSize = to.width
val startTime = withFrameNanos { it }
do {
playTime = withFrameNanos { it } - startTime
val animationValue = anim.getValueFromNanos(playTime)
left = fromLeft + animationValue * (toLeft - fromLeft)
top = fromTop + animationValue * (toTop - fromTop)
width = fromSize + animationValue * (toSize - fromSize)
} while (playTime < anim.durationNanos)
}
if (viewModel.isSharedImageVisible.value) {
Image(
painterResource(id = viewModel.setSharedImageResId.value),
contentDescription = null,
modifier = Modifier
.absoluteOffset {
IntOffset(left.toInt(), top.toInt())
}
.width(
with(LocalDensity.current) { width.toDp() }
)
.height(
with(LocalDensity.current) { width.toDp() }
)
)
}
}
class ViewModel : androidx.lifecycle.ViewModel() {
private val _isSharedImageVisible = mutableStateOf(false)
val isSharedImageVisible: State<Boolean> = _isSharedImageVisible
fun setIsSharedImageVisible(isSharedImageVisible: Boolean) {
_isSharedImageVisible.value = isSharedImageVisible
}
private val _sharedImageFromCoord = mutableStateOf(Rect.Zero)
val sharedImageFromCoord: State<Rect> = _sharedImageFromCoord
fun setSharedImageFromCoord(sharedImageFromCoord: Rect) {
_sharedImageFromCoord.value = sharedImageFromCoord
}
private val _sharedImageToCoord = mutableStateOf(Rect.Zero)
val sharedImageToCoord: State<Rect> = _sharedImageToCoord
fun setSharedImageToCoord(sharedImageToCoord: Rect) {
_sharedImageToCoord.value = sharedImageToCoord
}
private val _setSharedImageResId = mutableStateOf(R.drawable.ic_launcher_background)
val setSharedImageResId: State<Int> = _setSharedImageResId
fun setSharedImageResId(setSharedImageResId: Int) {
_setSharedImageResId.value = setSharedImageResId
}
private val _triggerAnimation = mutableStateOf(false)
val triggerAnimation: State<Boolean> = _triggerAnimation
fun triggerAnimation() {
_triggerAnimation.value = !_triggerAnimation.value
}
}