-
Notifications
You must be signed in to change notification settings - Fork 4
/
FillableLoader.kt
123 lines (105 loc) · 3.83 KB
/
FillableLoader.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package dev.jorgecastillo.fillableloader
import androidx.animation.LinearOutSlowInEasing
import androidx.compose.Composable
import androidx.compose.State
import androidx.compose.dispatch.withFrameMillis
import androidx.compose.launchInComposition
import androidx.compose.state
import androidx.lifecycle.whenStarted
import androidx.ui.core.LifecycleOwnerAmbient
import androidx.ui.core.Modifier
import androidx.ui.foundation.Canvas
import androidx.ui.geometry.Size
import androidx.ui.graphics.Color
import androidx.ui.graphics.drawscope.*
import androidx.ui.graphics.vector.PathParser
import androidx.ui.layout.fillMaxSize
import kotlin.math.max
import kotlin.math.min
val animationEasing = LinearOutSlowInEasing
@Composable
fun FillableLoader(
originalVectorSize: Size,
strokeColor: Color = Color.DarkGray,
fillColor: Color = Color.Magenta,
strokeDurationMillis: Long = 2000,
fillDurationMillis: Long = 8000,
) {
val state = animationTimeMillis(strokeDurationMillis, fillDurationMillis)
fun DrawScope.drawStroke(elapsedTime: Long) {
val strokePercent = max(0f, min(1f, elapsedTime * 1f / strokeDurationMillis))
val nodesToDraw = (animationEasing(strokePercent) * catPathNodes().size).toInt()
val path = PathParser().addPathNodes(
catPathNodes().take(nodesToDraw)
).toPath()
this.drawPath(path, strokeColor, style = Stroke(2f))
}
fun DrawScope.drawFilling(elapsedTime: Long) {
// Is stoke completely drawn, we can start drawing the filling.
if (elapsedTime > strokeDurationMillis) {
val fillPercent =
max(0f, min(1f, (elapsedTime - strokeDurationMillis) / fillDurationMillis.toFloat()))
waveClip(fillPercent, originalVectorSize, 128) {
drawPath(catPath(), fillColor, style = Fill)
}
}
}
Canvas(modifier = Modifier.fillMaxSize()) {
val originalCanvasWidth = size.width
val originalCanvasHeight = size.height
val scaleFactor = min(
originalCanvasWidth / originalVectorSize.width,
originalCanvasHeight / originalVectorSize.height
)
scale(
scaleX = scaleFactor,
scaleY = scaleFactor
) {
translate(
left = originalCanvasWidth / 2f - originalVectorSize.width / 2f,
top = originalCanvasHeight / 2f - originalVectorSize.height / 2f
) {
val elapsedTime = state.value.elapsedTime
drawStroke(elapsedTime)
drawFilling(elapsedTime)
}
}
}
}
/**
* Returns a [State] holding a local animation time in milliseconds and the current [AnimationPhase]
* The local animation time always starts at `0L` and stops updating when the call
* leaves the composition. The animation phase starts as [AnimationPhase.STROKE_STARTED], since
* stroke always renders first, and gets updated according to the elapsed time.
*/
@Composable
private fun animationTimeMillis(
strokeDrawingDuration: Long,
fillDrawingDuration: Long
): State<AnimationState> {
fun keepDrawing(elapsedTime: Long): Boolean =
elapsedTime < (strokeDrawingDuration + fillDrawingDuration)
val state = state { AnimationState(AnimationPhase.STROKE_STARTED, 0L) }
val lifecycleOwner = LifecycleOwnerAmbient.current
launchInComposition {
val startTime = withFrameMillis { it }
lifecycleOwner.whenStarted {
while (true) {
withFrameMillis { frameTime ->
val elapsedTime = frameTime - startTime
if (!keepDrawing(elapsedTime)) {
state.value = state.value.copy(animationPhase = AnimationPhase.FINISHED)
}
if (elapsedTime > strokeDrawingDuration) {
if (state.value.animationPhase < AnimationPhase.FILL_STARTED) {
state.value =
state.value.copy(animationPhase = AnimationPhase.FILL_STARTED)
}
}
state.value = state.value.copy(elapsedTime = frameTime - startTime)
}
}
}
}
return state
}