• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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