Empresas
Empregos
  • Sobre nós
  • Soluções
    • Publicação de vagas
      Publique sua vaga e receba candidatos qualificados em 48h.
    • Avaliações de candidatos
      Mais de 500 testes técnicos e psicológicos, mais anti-fraude.
    • Headhunting
      Busca executiva personalizada do início ao fim.
    • Folha de Pagamento + EOR
      Dispersão de folha e EOR em mais de 15 países da LATAM.
  • Preços
  • Empregos

0

399
Visualizações
Android Jetpack Compose Flicker Image Clone

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:

enter image description here

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.

enter image description here

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
    }
}
over 4 years ago · Santiago Trujillo
Responde à pergunta
Encontrar trabalhos remotos

Descubra a nova forma de encontrar um emprego!

melhores empregos
Principais categorias de trabalho
Empresas
Postar vaga Preços Comercial
Jurídico
Termos e Condições Política de privacidade
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomende algumas ofertas para mim
Preciso de ajuda