• 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: (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.setImageAssetDelegate { BitmapFactory.decodeResource(context.resources, R.drawable.airbnb) }
148     filmStripView.setFontAssetDelegate(object : FontAssetDelegate() {
149         override fun getFontPath(fontFamily: String?): String {
150             return "fonts/Roboto.ttf"
151         }
152     })
153     callback?.invoke(filmStripView)
154     val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
155     filmStripView.measure(spec, spec)
156     filmStripView.layout(0, 0, filmStripView.measuredWidth, filmStripView.measuredHeight)
157     val bitmap = bitmapPool.acquire(filmStripView.width, filmStripView.height)
158     val canvas = Canvas(bitmap)
159     filmStripView.setComposition(composition, name)
160     canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
161     withContext(Dispatchers.Main) {
162         log("Drawing $name")
163         filmStripView.draw(canvas)
164     }
165     filmStripViewPool.release(filmStripView)
166     LottieCompositionCache.getInstance().clear()
167     snapshotter.record(bitmap, name, variant)
168     bitmapPool.release(bitmap)
169 }
170 
171 /**
172  * Use this to signal that the composition is not ready to be snapshot yet.
173  * This use useful if you are using things like `rememberLottieComposition` which parses a composition asynchronously.
174  */
<lambda>null175 val LocalSnapshotReady = compositionLocalOf { MutableStateFlow<Boolean?>(true) }
176 
SnapshotTestCaseContextnull177 fun SnapshotTestCaseContext.loadCompositionFromAssetsSync(fileName: String): LottieComposition {
178     return LottieCompositionFactory.fromAssetSync(context, fileName).value!!
179 }
180 
181 suspend fun SnapshotTestCaseContext.snapshotComposable(
182     name: String,
183     variant: String = "default",
184     renderHardwareAndSoftware: Boolean = false,
185     content: @Composable (RenderMode) -> Unit,
186 ) = withContext(Dispatchers.Default) {
187     log("Snapshotting $name")
188     val composeView = ComposeView(context)
189     composeView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
190     val readyFlow = MutableStateFlow<Boolean?>(null)
<lambda>null191     composeView.setContent {
192         CompositionLocalProvider(LocalSnapshotReady provides readyFlow) {
193             content(RenderMode.SOFTWARE)
194         }
195         val readyFlowValue by readyFlow.collectAsState()
196         LaunchedEffect(readyFlowValue) {
197             if (readyFlowValue == null) {
198                 readyFlow.value = true
199             }
200         }
201     }
activitynull202     onActivity { activity ->
203         activity.binding.content.addView(composeView)
204     }
<lambda>null205     readyFlow.first { it == true }
206     composeView.awaitFrame()
207     log("Drawing $name - Software")
208     var bitmap = bitmapPool.acquire(composeView.width, composeView.height)
209     var canvas = Canvas(bitmap)
<lambda>null210     withContext(Dispatchers.Main) {
211         composeView.draw(canvas)
212     }
213     snapshotter.record(bitmap, name, if (renderHardwareAndSoftware) "$variant - Software" else variant)
214     bitmapPool.release(bitmap)
215 
216     if (renderHardwareAndSoftware) {
217         readyFlow.value = null
<lambda>null218         composeView.setContent {
219             CompositionLocalProvider(LocalSnapshotReady provides readyFlow) {
220                 content(RenderMode.HARDWARE)
221             }
222             val readyFlowValue by readyFlow.collectAsState()
223             LaunchedEffect(readyFlowValue) {
224                 if (readyFlowValue == null) {
225                     readyFlow.value = true
226                 }
227             }
228         }
<lambda>null229         readyFlow.first { it == true }
230         composeView.awaitFrame()
231         log("Drawing $name - Software")
232         bitmap = bitmapPool.acquire(composeView.width, composeView.height)
233         canvas = Canvas(bitmap)
<lambda>null234         withContext(Dispatchers.Main) {
235             composeView.draw(canvas)
236         }
237         snapshotter.record(bitmap, name, if (renderHardwareAndSoftware) "$variant - Hardware" else variant)
238         bitmapPool.release(bitmap)
239     }
240 
activitynull241     onActivity { activity ->
242         activity.binding.content.removeView(composeView)
243     }
244 
245     LottieCompositionCache.getInstance().clear()
246 }
247 
awaitFramenull248 private suspend fun View.awaitFrame() {
249     suspendCancellableCoroutine<Unit> { cont ->
250         post {
251             cont.resume(Unit)
252         }
253     }
254 }