11package dev.bnorm.storyboard.text.magic
22
33import androidx.compose.animation.*
4- import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis
5- import androidx.compose.animation.core.Transition
6- import androidx.compose.animation.core.createChildTransition
7- import androidx.compose.animation.core.tween
8- import androidx.compose.animation.core.updateTransition
4+ import androidx.compose.animation.core.*
95import androidx.compose.foundation.layout.Column
106import androidx.compose.foundation.layout.Row
117import androidx.compose.material.Text
@@ -15,39 +11,62 @@ import androidx.compose.runtime.remember
1511import androidx.compose.ui.Alignment
1612import androidx.compose.ui.Modifier
1713import androidx.compose.ui.text.AnnotatedString
14+ import kotlin.jvm.JvmName
15+
16+ const val DefaultMoveDurationMillis = 300
17+ const val DefaultFadeDurationMillis = 300
18+ const val DefaultDelayDurationMillis = 0
1819
1920@Composable
2021fun MagicText (
2122 text : AnnotatedString ,
2223 modifier : Modifier = Modifier ,
23- // Used to time FadeOut -> Move -> FadeIn animations.
24- moveDurationMillis : Int = DefaultDurationMillis ,
25- fadeDurationMillis : Int = moveDurationMillis / 2,
24+ // Used to time FadeOut --(Delay)-> Move --(Delay)-> FadeIn animations.
25+ moveDurationMillis : Int = DefaultMoveDurationMillis ,
26+ fadeDurationMillis : Int = DefaultFadeDurationMillis ,
27+ delayDurationMillis : Int = DefaultDelayDurationMillis ,
2628) {
2729 val words = remember(text) { text.toWords() }
2830 val transition = updateTransition(words)
29- MagicText (transition, modifier, moveDurationMillis, fadeDurationMillis)
31+ MagicText (transition, modifier, moveDurationMillis, fadeDurationMillis, delayDurationMillis)
32+ }
33+
34+ @Composable
35+ @JvmName(" MagicTextAnnotatedString" )
36+ fun MagicText (
37+ transition : Transition <AnnotatedString >,
38+ modifier : Modifier = Modifier ,
39+ // Used to time FadeOut --(Delay)-> Move --(Delay)-> FadeIn animations.
40+ moveDurationMillis : Int = DefaultMoveDurationMillis ,
41+ fadeDurationMillis : Int = DefaultFadeDurationMillis ,
42+ delayDurationMillis : Int = DefaultDelayDurationMillis ,
43+ ) {
44+ val worlds = transition.createChildTransition { remember(it) { it.toWords() } }
45+ MagicText (worlds, modifier, moveDurationMillis, fadeDurationMillis, delayDurationMillis)
3046}
3147
3248@Composable
3349fun MagicText (
3450 tokenizedText : List <AnnotatedString >,
3551 modifier : Modifier = Modifier ,
36- // Used to time FadeOut -> Move -> FadeIn animations.
37- moveDurationMillis : Int = DefaultDurationMillis ,
38- fadeDurationMillis : Int = moveDurationMillis / 2,
52+ // Used to time FadeOut --(Delay)-> Move --(Delay)-> FadeIn animations.
53+ moveDurationMillis : Int = DefaultMoveDurationMillis ,
54+ fadeDurationMillis : Int = DefaultFadeDurationMillis ,
55+ delayDurationMillis : Int = DefaultDelayDurationMillis ,
3956) {
4057 val transition = updateTransition(tokenizedText)
41- MagicText (transition, modifier, moveDurationMillis, fadeDurationMillis)
58+ MagicText (transition, modifier, moveDurationMillis, fadeDurationMillis, delayDurationMillis )
4259}
4360
4461@Composable
62+ @JvmName(" MagicTextList" )
4563fun MagicText (
4664 transition : Transition <List <AnnotatedString >>,
4765 modifier : Modifier = Modifier ,
48- // Used to time FadeOut -> Move -> FadeIn animations.
49- moveDurationMillis : Int = DefaultDurationMillis ,
50- fadeDurationMillis : Int = moveDurationMillis / 2,
66+ // Used to time FadeOut --(Delay)-> Move --(Delay)-> FadeIn animations.
67+ moveDurationMillis : Int = DefaultMoveDurationMillis ,
68+ fadeDurationMillis : Int = DefaultFadeDurationMillis ,
69+ delayDurationMillis : Int = DefaultDelayDurationMillis ,
5170) {
5271 // Keyed on current and target state, so a new transition is created with each new segment.
5372 // This allows re-rendering of the previous text with the new transition keys.
@@ -59,20 +78,20 @@ fun MagicText(
5978 // TODO should we be caching these maps for repeated transitions?
6079 val sharedText = remember {
6180 when (currentState == targetState) {
62- true -> mapOf (currentState to currentState.map { SharedText (it) })
81+ true -> mapOf (currentState to currentState.map { SharedText (it) }.flatMap { toLines(it) } )
6382
6483 false -> {
6584 val (current, target) = findShared(currentState, targetState)
6685 mapOf (
67- currentState to current, // Re-render the previous text, split up based on diff with the next text.
68- targetState to target, // Render the next text, split up based on diff with the previous text.
86+ currentState to current.flatMap { toLines(it) } , // Re-render the previous text, split up based on diff with the next text.
87+ targetState to target.flatMap { toLines(it) } , // Render the next text, split up based on diff with the previous text.
6988 )
7089 }
7190 }
7291 }
7392
7493 val child = transition.createChildTransition { text -> sharedText.getValue(text) }
75- MagicTextInternal (child, modifier, fadeDurationMillis, moveDurationMillis)
94+ MagicTextInternal (child, modifier, fadeDurationMillis, moveDurationMillis, delayDurationMillis )
7695 }
7796}
7897
@@ -81,9 +100,13 @@ fun MagicText(
81100private fun MagicTextInternal (
82101 transition : Transition <List <SharedText >>,
83102 modifier : Modifier ,
84- fadeDuration : Int ,
85- moveDuration : Int ,
103+ fadeDuration : Int , // Millis
104+ moveDuration : Int , // Millis
105+ delayDuration : Int , // Millis
86106) {
107+ val moveDelay = delayDuration + fadeDuration
108+ val fadeInDelay = 2 * delayDuration + fadeDuration + moveDuration
109+
87110 SharedTransitionLayout {
88111 transition.AnimatedContent (
89112 modifier = modifier,
@@ -95,19 +118,19 @@ private fun MagicTextInternal(
95118 return when {
96119 // Text is completely different and should fade in and out.
97120 key == null -> Modifier .animateEnterExit(
98- enter = fadeIn(tween(fadeDuration, delayMillis = moveDuration + fadeDuration )),
99- exit = fadeOut(tween(fadeDuration)),
121+ enter = fadeIn(tween(fadeDuration, delayMillis = fadeInDelay, easing = EaseInCubic )),
122+ exit = fadeOut(tween(fadeDuration, easing = EaseOutCubic )),
100123 )
101124
102125 // Text content is the same, but the styling may be different,
103126 // so move and cross-fade.
104127 crossFade -> Modifier .sharedBounds(
105128 rememberSharedContentState(key),
106129 animatedVisibilityScope = this @AnimatedContent,
107- enter = fadeIn(tween(moveDuration, delayMillis = fadeDuration )),
108- exit = fadeOut(tween(moveDuration, delayMillis = fadeDuration )),
130+ enter = fadeIn(tween(moveDuration, delayMillis = moveDelay, easing = EaseInOut )),
131+ exit = fadeOut(tween(moveDuration, delayMillis = moveDelay, easing = EaseInOut )),
109132 boundsTransform = { _, _ ->
110- tween(moveDuration, delayMillis = fadeDuration )
133+ tween(moveDuration, delayMillis = moveDelay, easing = EaseInOut )
111134 },
112135 )
113136
@@ -116,14 +139,14 @@ private fun MagicTextInternal(
116139 rememberSharedContentState(key),
117140 animatedVisibilityScope = this @AnimatedContent,
118141 boundsTransform = { _, _ ->
119- tween(moveDuration, delayMillis = fadeDuration )
142+ tween(moveDuration, delayMillis = moveDelay, easing = EaseInOut )
120143 },
121144 )
122145 }
123146 }
124147
125148 Column (horizontalAlignment = Alignment .Start ) {
126- val iterator = parts.flatMap { toLines(it) }. iterator()
149+ val iterator = parts.iterator()
127150 while (iterator.hasNext()) {
128151 Row {
129152 var itemCount = 0
0 commit comments