Skip to content

During composition a full asset animation is played instead of a set segment #99

@kpovilaitis

Description

@kpovilaitis

Hello, I have a use case where I have an app tour/overview animation showing to the user.

This is a 1 big animation and it is divided by multiple segments (pages of each feature). During composition (screen open) I want to set the 1st page segment on the animation and play only that segment. Then wait until user clicks on page 2.

However I discovered an issue that DotLottie plays the full animation during composition if you call controller.play() in a LaunchedEffect() . I use it to set the segment and start it, but it just plays the whole thing.

Weirdly enough, the issue is solved if I add delay(100) to my LaunchedEffect.
Can this be cause by DotLottieController.shouldPlayOnInit?

Compose code:

@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongParameterList", "LongMethod")
@Composable
fun TourScreen(
    state: TourViewModelState,
    onBack: () -> Unit,
    onPrevious: () -> Unit,
    onNext: () -> Unit,
    onSkipTour: () -> Unit,
    modifier: Modifier = Modifier,
) {
    var currentPage by remember { mutableIntStateOf(state.currentPage) }
    val pagerState = rememberPagerState { state.pageCount }
    val controller = remember { DotLottieController() }
    LaunchedEffect(Unit) {
        delay(100)
        val segment = getSegment(state.currentPage)
        controller.setSegment(
            firstFrame = segment.first,
            lastFrame = segment.second,
        )
        controller.setPlayMode(Mode.FORWARD)
        controller.play()
    }
    LaunchedEffect(state.currentPage) {
        if (currentPage < state.currentPage) {
            controller.stop()
            val segment = getSegment(state.currentPage)
            controller.setSegment(segment.first, segment.second)
            controller.setPlayMode(Mode.FORWARD)
            controller.play()
        }
        if (currentPage > state.currentPage) {
            controller.stop()
            val segment = getSegment(currentPage)
            controller.setSegment(segment.first, segment.second)
            controller.setPlayMode(Mode.REVERSE)
            controller.play()
        }
        currentPage = state.currentPage
        pagerState.animateScrollToPage(currentPage)
    }
    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.targetPage }
            .distinctUntilChanged()
            .collect { newPage ->
                when {
                    newPage > currentPage -> {
                        onNext()
                    }
                    newPage < currentPage -> {
                        onPrevious()
                    }
                }
            }
    }

    Scaffold(
        modifier = modifier
            .testTag("tour_layout")
            .systemBarsPadding()
            .semantics {
                testTagsAsResourceId = true
            },
        topBar = {
            TopAppBar(
                navigationIcon = {
                    if (state.isBackActionVisible) {
                        IconButton(
                            onClick = onBack,
                            modifier = Modifier.testTag("tour_back"),
                            analyticsTag = "Back",
                        ) {
                            Icon(
                                painter = painterResource(UICoreR.drawable.ic_arrow_back),
                                tint = MaterialTheme.colorScheme.onSurface,
                                contentDescription = stringResource(id = UICoreR.string.back_button_description),
                            )
                        }
                    }
                },
            )
        },
        content = { contentPadding ->
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .fillMaxSize()
                    .imePadding()
                    .padding(contentPadding),
            ) {
                DotLottieAnimation(
                    controller = controller,
                    source = DotLottieSource.Asset("anim/app_tour/app_tour.lottie"),
                    modifier = Modifier
                        .fillMaxWidth()
                        .weight(1f),
                )
                HorizontalPager(
                    state = pagerState,
                    modifier = Modifier
                        .height(186.dp),
                    pageContent = { index ->
                        Column(
                            horizontalAlignment = Alignment.CenterHorizontally,
                            modifier = Modifier
                                .fillMaxSize(),
                        ) {
                            Text(
                                text = stringResource(getTitleResId(index)),
                                style = MaterialTheme.typography.headlineMedium,
                                textAlign = TextAlign.Center,
                                modifier = Modifier.padding(
                                    all = MaterialTheme.spaceDimensions.medium,
                                ),
                            )
                            Text(
                                text = stringResource(getDescriptionResId(index)),
                                style = MaterialTheme.typography.bodyLarge,
                                textAlign = TextAlign.Center,
                                modifier = Modifier
                                    .padding(
                                        horizontal = MaterialTheme.spaceDimensions.medium,
                                    ),
                            )
                        }
                    },
                )
                Row(
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .padding(horizontal = MaterialTheme.spaceDimensions.medium),
                ) {
                    OutlinedIconButton(
                        shape = Shapes.extraLarge,
                        enabled = state.isPreviousEnabled,
                        onClick = onPrevious,
                        content = {
                            Icon(
                                imageVector = Icons.Default.ChevronLeft,
                                contentDescription = null,
                            )
                        },
                    )
                    Spacer(Modifier.weight(1f))
                    PageIndicator(
                        pagesCount = state.pageCount,
                        currentPageIndex = state.currentPage,
                        modifier = Modifier
                            .semantics { currentPageIndex = currentPage },
                    )
                    Spacer(Modifier.weight(1f))
                    OutlinedIconButton(
                        enabled = state.isNextEnabled,
                        shape = Shapes.extraLarge,
                        onClick = onNext,
                        content = {
                            Icon(
                                imageVector = Icons.Default.ChevronRight,
                                contentDescription = null,
                            )
                        },
                    )
                }
                OutlinedButton(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(MaterialTheme.spaceDimensions.medium)
                        .testTag("tour_skip_tour"),
                    label = stringResource(R.string.tour_skip_tour),
                    onClick = onSkipTour,
                    isSmall = true,
                )
            }
        },
    )
}

This is a fixed version for illustration purposes:

trimmed.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions