• Jobs
  • About Us
  • professionals
    • Home
    • Jobs
    • Courses and challenges
  • business
    • Home
    • Post vacancy
    • Our process
    • Pricing
    • Assessments
    • Payroll
    • Blog
    • Sales
    • Salary Calculator

0

278
Views
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 3 years ago · Santiago Trujillo
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Our process Sales
Legal
Terms and conditions Privacy policy
© 2025 PeakU Inc. All Rights Reserved.

Andres GPT

Recommend me some offers
I have an error