<lambda>null1 package com.airbnb.lottie.snapshots
2
3 import android.content.Context
4 import android.graphics.BitmapFactory
5 import android.graphics.Canvas
6 import android.graphics.Color
7 import android.graphics.PorterDuff
8 import android.util.Log
9 import android.view.View
10 import android.view.ViewGroup
11 import android.widget.FrameLayout
12 import android.widget.ImageView
13 import android.widget.LinearLayout
14 import androidx.compose.runtime.Composable
15 import androidx.compose.runtime.CompositionLocalProvider
16 import androidx.compose.runtime.LaunchedEffect
17 import androidx.compose.runtime.collectAsState
18 import androidx.compose.runtime.compositionLocalOf
19 import androidx.compose.runtime.getValue
20 import androidx.compose.ui.platform.ComposeView
21 import com.airbnb.lottie.FontAssetDelegate
22 import com.airbnb.lottie.LottieAnimationView
23 import com.airbnb.lottie.LottieComposition
24 import com.airbnb.lottie.LottieCompositionFactory
25 import com.airbnb.lottie.LottieDrawable
26 import com.airbnb.lottie.RenderMode
27 import com.airbnb.lottie.model.LottieCompositionCache
28 import com.airbnb.lottie.snapshots.utils.BitmapPool
29 import com.airbnb.lottie.snapshots.utils.HappoSnapshotter
30 import com.airbnb.lottie.snapshots.utils.ObjectPool
31 import kotlinx.coroutines.Dispatchers
32 import kotlinx.coroutines.flow.MutableStateFlow
33 import kotlinx.coroutines.flow.first
34 import kotlinx.coroutines.suspendCancellableCoroutine
35 import kotlinx.coroutines.withContext
36 import kotlin.coroutines.resume
37
38 /**
39 * Set of properties that are available to all [SnapshotTestCase] runs.
40 */
41 interface SnapshotTestCaseContext {
42 val context: Context
43 val snapshotter: HappoSnapshotter
44 val bitmapPool: BitmapPool
45 val animationViewPool: ObjectPool<LottieAnimationView>
46 val filmStripViewPool: ObjectPool<FilmStripView>
47 fun onActivity(callback: (SnapshotTestActivity) -> Unit)
48 }
49
50 @Suppress("unused")
SnapshotTestCaseContextnull51 fun SnapshotTestCaseContext.log(message: String) {
52 Log.d("LottieTestCase", message)
53 }
54
withDrawablenull55 suspend fun SnapshotTestCaseContext.withDrawable(
56 assetName: String,
57 snapshotName: String,
58 snapshotVariant: String,
59 callback: suspend (LottieDrawable) -> Unit,
60 ) {
61 val result = LottieCompositionFactory.fromAssetSync(context, assetName)
62 val composition = result.value ?: throw IllegalArgumentException("Unable to parse $assetName.", result.exception)
63 val drawable = LottieDrawable()
64 drawable.composition = composition
65 callback(drawable)
66 val bitmap = bitmapPool.acquire(drawable.intrinsicWidth, drawable.intrinsicHeight)
67 val canvas = Canvas(bitmap)
68 log("Drawing $assetName")
69 drawable.draw(canvas)
70 snapshotter.record(bitmap, snapshotName, snapshotVariant)
71 LottieCompositionCache.getInstance().clear()
72 bitmapPool.release(bitmap)
73 }
74
withAnimationViewnull75 suspend fun SnapshotTestCaseContext.withAnimationView(
76 assetName: String,
77 snapshotName: String = assetName,
78 snapshotVariant: String = "default",
79 widthPx: Int = context.resources.displayMetrics.widthPixels,
80 heightPx: Int = context.resources.displayMetrics.heightPixels,
81 renderHardwareAndSoftware: Boolean = false,
82 callback: (LottieAnimationView) -> Unit,
83 ) {
84 val result = LottieCompositionFactory.fromAssetSync(context, assetName)
85 val composition = result.value ?: throw IllegalArgumentException("Unable to parse $assetName.", result.exception)
86 val animationView = animationViewPool.acquire()
87 animationView.setComposition(composition)
88 animationView.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
89 animationView.scaleType = ImageView.ScaleType.FIT_CENTER
90 callback(animationView)
91 val animationViewContainer = animationView.parent as ViewGroup
92 val widthSpec = View.MeasureSpec.makeMeasureSpec(
93 widthPx,
94 View.MeasureSpec.EXACTLY,
95 )
96 val heightSpec: Int = View.MeasureSpec.makeMeasureSpec(
97 heightPx,
98 View.MeasureSpec.EXACTLY,
99 )
100 animationViewContainer.measure(widthSpec, heightSpec)
101 animationViewContainer.layout(0, 0, animationViewContainer.measuredWidth, animationViewContainer.measuredHeight)
102 val bitmap = bitmapPool.acquire(animationView.width, animationView.height)
103 val canvas = Canvas(bitmap)
104 if (renderHardwareAndSoftware) {
105 log("Drawing $assetName - hardware")
106 val renderMode = animationView.renderMode
107 animationView.renderMode = RenderMode.HARDWARE
108 animationView.draw(canvas)
109 snapshotter.record(bitmap, snapshotName, "$snapshotVariant - Hardware")
110
111 bitmap.eraseColor(0)
112 animationView.renderMode = RenderMode.SOFTWARE
113 animationView.draw(canvas)
114 animationViewPool.release(animationView)
115 snapshotter.record(bitmap, snapshotName, "$snapshotVariant - Software")
116 animationView.renderMode = renderMode
117 } else {
118 log("Drawing $assetName")
119 animationView.draw(canvas)
120 animationViewPool.release(animationView)
121 snapshotter.record(bitmap, snapshotName, snapshotVariant)
122 }
123 bitmapPool.release(bitmap)
124 }
125
withFilmStripViewnull126 suspend fun SnapshotTestCaseContext.withFilmStripView(
127 assetName: String,
128 snapshotName: String = assetName,
129 snapshotVariant: String = "default",
130 callback: (FilmStripView) -> Unit,
131 ) {
132 val result = LottieCompositionFactory.fromAssetSync(context, assetName)
133 val composition = result.value ?: throw IllegalArgumentException("Unable to parse $assetName.", result.exception)
134 snapshotComposition(snapshotName, snapshotVariant, composition, callback)
135 }
136
snapshotCompositionnull137 suspend fun SnapshotTestCaseContext.snapshotComposition(
138 name: String,
139 variant: String = "default",
140 composition: LottieComposition,
141 callback: ((FilmStripView) -> Unit)? = null,
142 ) = withContext(Dispatchers.Default) {
143 log("Snapshotting $name")
144 val filmStripView = filmStripViewPool.acquire()
145 filmStripView.setOutlineMasksAndMattes(false)
146 filmStripView.setApplyingOpacityToLayersEnabled(false)
147 filmStripView.setUseCompositionFrameRate(false)
148 filmStripView.setImageAssetDelegate { BitmapFactory.decodeResource(context.resources, R.drawable.airbnb) }
149 if (composition.characters.isEmpty) {
150 filmStripView.setFontAssetDelegate(object : FontAssetDelegate() {
151 override fun getFontPath(fontFamily: String?, fontStyle: String?, fontName: String?): String {
152 return "fonts/Roboto.ttf"
153 }
154 })
155 }
156 callback?.invoke(filmStripView)
157 val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
158 filmStripView.measure(spec, spec)
159 filmStripView.layout(0, 0, filmStripView.measuredWidth, filmStripView.measuredHeight)
160 val bitmap = bitmapPool.acquire(filmStripView.width, filmStripView.height)
161 val canvas = Canvas(bitmap)
162 filmStripView.setComposition(composition, name)
163 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
164 withContext(Dispatchers.Main) {
165 log("Drawing $name")
166 filmStripView.draw(canvas)
167 }
168 filmStripViewPool.release(filmStripView)
169 LottieCompositionCache.getInstance().clear()
170 snapshotter.record(bitmap, name, variant)
171 bitmapPool.release(bitmap)
172 }
173
174 /**
175 * Use this to signal that the composition is not ready to be snapshot yet.
176 * This use useful if you are using things like `rememberLottieComposition` which parses a composition asynchronously.
177 */
<lambda>null178 val LocalSnapshotReady = compositionLocalOf { MutableStateFlow<Boolean?>(true) }
179
SnapshotTestCaseContextnull180 fun SnapshotTestCaseContext.loadCompositionFromAssetsSync(fileName: String): LottieComposition {
181 return LottieCompositionFactory.fromAssetSync(context, fileName).value!!
182 }
183
184 suspend fun SnapshotTestCaseContext.snapshotComposable(
185 name: String,
186 variant: String = "default",
187 renderHardwareAndSoftware: Boolean = false,
188 content: @Composable (RenderMode) -> Unit,
189 ) = withContext(Dispatchers.Default) {
190 log("Snapshotting $name")
191 val composeView = ComposeView(context)
192 composeView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
193 val readyFlow = MutableStateFlow<Boolean?>(null)
<lambda>null194 composeView.setContent {
195 CompositionLocalProvider(LocalSnapshotReady provides readyFlow) {
196 content(RenderMode.SOFTWARE)
197 }
198 val readyFlowValue by readyFlow.collectAsState()
199 LaunchedEffect(readyFlowValue) {
200 if (readyFlowValue == null) {
201 readyFlow.value = true
202 }
203 }
204 }
activitynull205 onActivity { activity ->
206 activity.binding.content.addView(composeView)
207 }
<lambda>null208 readyFlow.first { it == true }
209 composeView.awaitFrame()
210 log("Drawing $name - Software")
211 var bitmap = bitmapPool.acquire(composeView.width, composeView.height)
212 var canvas = Canvas(bitmap)
<lambda>null213 withContext(Dispatchers.Main) {
214 composeView.draw(canvas)
215 }
216 snapshotter.record(bitmap, name, if (renderHardwareAndSoftware) "$variant - Software" else variant)
217 bitmapPool.release(bitmap)
218
219 if (renderHardwareAndSoftware) {
220 readyFlow.value = null
<lambda>null221 composeView.setContent {
222 CompositionLocalProvider(LocalSnapshotReady provides readyFlow) {
223 content(RenderMode.HARDWARE)
224 }
225 val readyFlowValue by readyFlow.collectAsState()
226 LaunchedEffect(readyFlowValue) {
227 if (readyFlowValue == null) {
228 readyFlow.value = true
229 }
230 }
231 }
<lambda>null232 readyFlow.first { it == true }
233 composeView.awaitFrame()
234 log("Drawing $name - Software")
235 bitmap = bitmapPool.acquire(composeView.width, composeView.height)
236 canvas = Canvas(bitmap)
<lambda>null237 withContext(Dispatchers.Main) {
238 composeView.draw(canvas)
239 }
240 snapshotter.record(bitmap, name, if (renderHardwareAndSoftware) "$variant - Hardware" else variant)
241 bitmapPool.release(bitmap)
242 }
243
activitynull244 onActivity { activity ->
245 activity.binding.content.removeView(composeView)
246 }
247
248 LottieCompositionCache.getInstance().clear()
249 }
250
awaitFramenull251 private suspend fun View.awaitFrame() {
252 suspendCancellableCoroutine<Unit> { cont ->
253 post {
254 cont.resume(Unit)
255 }
256 }
257 }
258