blob: ed5c80eaf399609e3846a8cbdc1715bed030111f [file] [log] [blame]
package com.airbnb.lottie
import android.content.Context
import android.content.res.Resources
import android.graphics.*
import android.graphics.drawable.ColorDrawable
import android.os.Environment
import android.util.Log
import android.util.TypedValue
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import com.airbnb.happo.SnapshotProvider
import com.airbnb.lottie.model.KeyPath
import com.airbnb.lottie.value.*
import java.io.File
import java.io.IOException
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
private class SnapshotInfo(val canvas: Canvas, val progress: Float)
class LottieSnapshotProvider internal constructor(private val context: Context) : SnapshotProvider() {
private val queue = LinkedBlockingQueue<Runnable>()
private val executor = ThreadPoolExecutor(CORES, CORES, 15, TimeUnit.MINUTES, queue)
// Bitmap to return from an ImageAssetDelegate to make testing animations with images easier.
private val dummyBitmap: Bitmap = BitmapFactory.decodeResource(context.resources, com.airbnb.lottie.samples.R.drawable.airbnb)
private val renderBitmap: Bitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
private val clearPaint = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
private val renderCanvas = Canvas(renderBitmap)
private val snapshotInfos: List<SnapshotInfo> by lazy {
ArrayList<SnapshotInfo>(25).apply {
for (row in 0..4) {
for (col in 0..4) {
val matrix = Matrix()
matrix.setRectToRect(
RectF(0f, 0f, 1000f, 1000f),
RectF(col * 200f, row * 200f, col * 200f + 200f, row * 200f + 200f),
Matrix.ScaleToFit.CENTER
)
val canvas = Canvas(renderBitmap)
canvas.matrix = matrix
canvas.scale(0.2f, 0.2f, 0f, 0f)
canvas.translate(col * 1000f, row * 1000f)
add(SnapshotInfo(canvas, 0.04f * (row * 5f + col)))
}
}
}
}
private var remainingTasks = 0
override fun beginSnapshotting() {
Log.d(L.TAG, "beginSnapshotting")
try {
snapshotAssets(context.assets.list(""))
val tests = context.assets.list("Tests")
for (i in tests!!.indices) {
tests[i] = "Tests/" + tests[i]
}
snapshotAssets(tests)
val lottiefiles = context.assets.list("lottiefiles")
for (i in lottiefiles!!.indices) {
lottiefiles[i] = "lottiefiles/" + lottiefiles[i]
}
snapshotAssets(lottiefiles)
} catch (e: IOException) {
onError(e)
}
testFrameBoundary()
testFrameBoundary2()
testScaleTypes()
testDynamicProperties()
testSwitchingToDrawableAndBack()
testStartEndFrameWithStartEndProgress()
testUrl()
}
private fun snapshotAssets(animations: Array<String>?) {
val dir = File(Environment.getExternalStorageDirectory().toString() + "/Snapshots")
dir.mkdirs()
for (file in dir.listFiles()) {
file.delete()
}
for (animation in animations!!) {
if (!animation.contains(".json") && !animation.contains(".zip")) {
continue
}
remainingTasks += 1
Log.d(L.TAG, "Enqueueing $animation")
executor.execute {
runAnimation(animation)
decrementAndCompleteIfDone()
}
}
}
private fun runAnimation(name: String) {
Log.d(L.TAG, "Running $name")
val result = LottieCompositionFactory.fromAssetSync(context, name)
if (result.exception != null) throw IllegalStateException(result.exception)
val composition = result.value ?: return
val bounds = composition.bounds
val width = bounds.width()
val height = bounds.height()
val displayMetrics = Resources.getSystem().displayMetrics
if (width > 4 * displayMetrics.widthPixels || height > 4 * displayMetrics.heightPixels) {
Log.d("Happo", name + " is too large. Skipping (" + width + "x" + height + ")")
return
}
drawComposition(composition, name)
}
private fun drawComposition(composition: LottieComposition, name: String) {
Log.d(L.TAG, "Drawing $name")
val drawable = LottieDrawable()
drawable.composition = composition
drawable.setImageAssetDelegate { dummyBitmap }
drawable.scale = maxOf(composition.bounds.width(), composition.bounds.height()) / 200f
val strokeWidth = 7f
val outlinePaint = Paint().apply {
style = Paint.Style.STROKE
color = Color.RED
this.strokeWidth = strokeWidth
}
clearBitmap()
snapshotInfos.forEach {
drawable.progress = it.progress
drawable.draw(it.canvas)
it.canvas.drawRect(strokeWidth, strokeWidth, it.canvas.width - strokeWidth, it.canvas.height - strokeWidth, outlinePaint)
}
recordSnapshot(renderBitmap, "android", name, "Main")
}
override fun stopSnapshotting() {
queue.clear()
}
private fun decrementAndCompleteIfDone() {
remainingTasks--
Log.d(L.TAG, "There are $remainingTasks tasks left.")
Log.d("Happo", "There are $remainingTasks remaining tasks.")
if (remainingTasks < 0) {
throw IllegalStateException("Remaining tasks cannot be negative.")
}
if (remainingTasks == 0) {
onComplete()
}
}
private fun testScaleTypes() {
val composition = LottieComposition.Factory.fromFileSync(
context, "LottieLogo1.json")
var params = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
testScaleTypeView(context, composition, "Wrap Content", params, null, null)
params = FrameLayout.LayoutParams(dpToPx(300), dpToPx(300))
testScaleTypeView(context, composition, "300x300 @4x", params, null, 4f)
params = FrameLayout.LayoutParams(dpToPx(300), dpToPx(300))
testScaleTypeView(context, composition, "300x300 centerCrop", params,
ImageView.ScaleType.CENTER_CROP, null)
params = FrameLayout.LayoutParams(dpToPx(300), dpToPx(300))
testScaleTypeView(context, composition, "300x300 centerInside", params,
ImageView.ScaleType.CENTER_INSIDE, null)
params = FrameLayout.LayoutParams(dpToPx(300), dpToPx(300))
testScaleTypeView(context, composition, "300x300 centerInside @2x", params,
ImageView.ScaleType.CENTER_INSIDE, 2f)
params = FrameLayout.LayoutParams(dpToPx(300), dpToPx(300))
testScaleTypeView(context, composition, "300x300 centerCrop @2x", params,
ImageView.ScaleType.CENTER_CROP, 2f)
params = FrameLayout.LayoutParams(dpToPx(300), dpToPx(300))
testScaleTypeView(context, composition, "300x300 @2x", params, null, 2f)
params = FrameLayout.LayoutParams(dpToPx(600), dpToPx(300))
testScaleTypeView(context, composition, "600x300 centerInside", params,
ImageView.ScaleType.CENTER_INSIDE, null)
params = FrameLayout.LayoutParams(dpToPx(300), dpToPx(600))
testScaleTypeView(context, composition, "300x600 centerInside", params,
ImageView.ScaleType.CENTER_INSIDE, null)
params = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
testScaleTypeView(context, composition, "Match Parent", params, null, null)
}
private fun testScaleTypeView(context: Context, composition: LottieComposition?,
name: String, params: FrameLayout.LayoutParams, scaleType: ImageView.ScaleType?,
scale: Float?) {
val container = FrameLayout(context)
val animationView = LottieAnimationView(context)
animationView.setComposition(composition!!)
animationView.progress = 1f
if (scaleType != null) {
animationView.scaleType = scaleType
}
if (scale != null) {
animationView.scale = scale
}
container.addView(animationView, params)
recordSnapshot(container, 1080, "android", "Scale Types", name, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
}
private fun testFrameBoundary() {
val animationView = LottieAnimationView(context)
val composition = LottieComposition.Factory.fromFileSync(context, "Tests/Frame.json")
animationView.setComposition(composition!!)
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
animationView.frame = 16
recordSnapshot(animationView, 1080, "android", "Frame Boundary", "Frame 16 Red", params)
animationView.frame = 17
recordSnapshot(animationView, 1080, "android", "Frame Boundary", "Frame 17 Blue", params)
animationView.frame = 50
recordSnapshot(animationView, 1080, "android", "Frame Boundary", "Frame 50 Blue", params)
animationView.frame = 51
recordSnapshot(animationView, 1080, "android", "Frame Boundary", "Frame 51 Green", params)
}
private fun testFrameBoundary2() {
val animationView = LottieAnimationView(context)
val composition = LottieComposition.Factory.fromFileSync(context, "Tests/RGB.json")
animationView.setComposition(composition!!)
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
animationView.frame = 0
recordSnapshot(animationView, 1080, "android", "Frame Boundary", "Frame 0 Red", params)
animationView.frame = 1
recordSnapshot(animationView, 1080, "android", "Frame Boundary", "Frame 1 Green", params)
animationView.frame = 2
recordSnapshot(animationView, 1080, "android", "Frame Boundary", "Frame 2 Blue", params)
}
private fun testDynamicProperties() {
testDynamicProperty(
"Fill color (Green)",
KeyPath("Shape Layer 1", "Rectangle", "Fill 1"),
LottieProperty.COLOR,
LottieValueCallback(Color.GREEN))
testDynamicProperty(
"Fill color (Yellow)",
KeyPath("Shape Layer 1", "Rectangle", "Fill 1"),
LottieProperty.COLOR,
LottieValueCallback(Color.YELLOW))
testDynamicProperty(
"Fill opacity",
KeyPath("Shape Layer 1", "Rectangle", "Fill 1"),
LottieProperty.OPACITY,
LottieValueCallback(50))
testDynamicProperty(
"Stroke color",
KeyPath("Shape Layer 1", "Rectangle", "Stroke 1"),
LottieProperty.STROKE_COLOR,
LottieValueCallback(Color.GREEN))
testDynamicProperty(
"Stroke width",
KeyPath("Shape Layer 1", "Rectangle", "Stroke 1"),
LottieProperty.STROKE_WIDTH,
LottieRelativeFloatValueCallback(50f))
testDynamicProperty(
"Stroke opacity",
KeyPath("Shape Layer 1", "Rectangle", "Stroke 1"),
LottieProperty.OPACITY,
LottieValueCallback(50))
testDynamicProperty(
"Transform anchor point",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_ANCHOR_POINT,
LottieRelativePointValueCallback(PointF(20f, 20f)))
testDynamicProperty(
"Transform position",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_POSITION,
LottieRelativePointValueCallback(PointF(20f, 20f)))
testDynamicProperty(
"Transform position (relative)",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_POSITION,
LottieRelativePointValueCallback(PointF(20f, 20f)))
testDynamicProperty(
"Transform opacity",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_OPACITY,
LottieValueCallback(50))
testDynamicProperty(
"Transform rotation",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_ROTATION,
LottieValueCallback(45f))
testDynamicProperty(
"Transform scale",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_SCALE,
LottieValueCallback(ScaleXY(0.5f, 0.5f)))
testDynamicProperty(
"Ellipse position",
KeyPath("Shape Layer 1", "Ellipse", "Ellipse Path 1"),
LottieProperty.POSITION,
LottieRelativePointValueCallback(PointF(20f, 20f)))
testDynamicProperty(
"Ellipse size",
KeyPath("Shape Layer 1", "Ellipse", "Ellipse Path 1"),
LottieProperty.ELLIPSE_SIZE,
LottieRelativePointValueCallback(PointF(40f, 60f)))
testDynamicProperty(
"Star points",
KeyPath("Shape Layer 1", "Star", "Polystar Path 1"),
LottieProperty.POLYSTAR_POINTS,
LottieValueCallback(8f))
testDynamicProperty(
"Star rotation",
KeyPath("Shape Layer 1", "Star", "Polystar Path 1"),
LottieProperty.POLYSTAR_ROTATION,
LottieValueCallback(10f))
testDynamicProperty(
"Star position",
KeyPath("Shape Layer 1", "Star", "Polystar Path 1"),
LottieProperty.POSITION,
LottieRelativePointValueCallback(PointF(20f, 20f)))
testDynamicProperty(
"Star inner radius",
KeyPath("Shape Layer 1", "Star", "Polystar Path 1"),
LottieProperty.POLYSTAR_INNER_RADIUS,
LottieValueCallback(10f))
testDynamicProperty(
"Star inner roundedness",
KeyPath("Shape Layer 1", "Star", "Polystar Path 1"),
LottieProperty.POLYSTAR_INNER_ROUNDEDNESS,
LottieValueCallback(100f))
testDynamicProperty(
"Star outer radius",
KeyPath("Shape Layer 1", "Star", "Polystar Path 1"),
LottieProperty.POLYSTAR_OUTER_RADIUS,
LottieValueCallback(60f))
testDynamicProperty(
"Star outer roundedness",
KeyPath("Shape Layer 1", "Star", "Polystar Path 1"),
LottieProperty.POLYSTAR_OUTER_ROUNDEDNESS,
LottieValueCallback(100f))
testDynamicProperty(
"Polygon points",
KeyPath("Shape Layer 1", "Polygon", "Polystar Path 1"),
LottieProperty.POLYSTAR_POINTS,
LottieValueCallback(8f))
testDynamicProperty(
"Polygon rotation",
KeyPath("Shape Layer 1", "Polygon", "Polystar Path 1"),
LottieProperty.POLYSTAR_ROTATION,
LottieValueCallback(10f))
testDynamicProperty(
"Polygon position",
KeyPath("Shape Layer 1", "Polygon", "Polystar Path 1"),
LottieProperty.POSITION,
LottieRelativePointValueCallback(PointF(20f, 20f)))
testDynamicProperty(
"Polygon radius",
KeyPath("Shape Layer 1", "Polygon", "Polystar Path 1"),
LottieProperty.POLYSTAR_OUTER_RADIUS,
LottieRelativeFloatValueCallback(60f))
testDynamicProperty(
"Polygon roundedness",
KeyPath("Shape Layer 1", "Polygon", "Polystar Path 1"),
LottieProperty.POLYSTAR_OUTER_ROUNDEDNESS,
LottieValueCallback(100f))
testDynamicProperty(
"Repeater transform position",
KeyPath("Shape Layer 1", "Repeater Shape", "Repeater 1"),
LottieProperty.TRANSFORM_POSITION,
LottieRelativePointValueCallback(PointF(100f, 100f)))
testDynamicProperty(
"Repeater transform start opacity",
KeyPath("Shape Layer 1", "Repeater Shape", "Repeater 1"),
LottieProperty.TRANSFORM_START_OPACITY,
LottieValueCallback(25f))
testDynamicProperty(
"Repeater transform end opacity",
KeyPath("Shape Layer 1", "Repeater Shape", "Repeater 1"),
LottieProperty.TRANSFORM_END_OPACITY,
LottieValueCallback(25f))
testDynamicProperty(
"Repeater transform rotation",
KeyPath("Shape Layer 1", "Repeater Shape", "Repeater 1"),
LottieProperty.TRANSFORM_ROTATION,
LottieValueCallback(45f))
testDynamicProperty(
"Repeater transform scale",
KeyPath("Shape Layer 1", "Repeater Shape", "Repeater 1"),
LottieProperty.TRANSFORM_SCALE,
LottieValueCallback(ScaleXY(2f, 2f)))
testDynamicProperty(
"Time remapping",
KeyPath("Circle 1"),
LottieProperty.TIME_REMAP,
LottieValueCallback(1f))
testDynamicProperty(
"Color Filter",
KeyPath("**"),
LottieProperty.COLOR_FILTER,
LottieValueCallback<ColorFilter>(SimpleColorFilter(Color.GREEN)))
val blueColorFilter = LottieValueCallback<ColorFilter>(SimpleColorFilter(Color.GREEN))
val animationView = LottieAnimationView(context)
val composition = LottieComposition.Factory.fromFileSync(context, "Tests/Shapes.json")
animationView.setComposition(composition!!)
animationView.progress = 0f
val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
animationView.addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER, blueColorFilter)
recordSnapshot(animationView, 1080, "android", "Dynamic Properties", "Color Filter before blue", params)
blueColorFilter.setValue(SimpleColorFilter(Color.BLUE))
recordSnapshot(animationView, 1080, "android", "Dynamic Properties", "Color Filter after blue", params)
testDynamicProperty(
"Null Color Filter",
KeyPath("**"),
LottieProperty.COLOR_FILTER,
LottieValueCallback<ColorFilter>(null))
testDynamicProperty(
"Opacity interpolation (0)",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_OPACITY,
LottieInterpolatedIntegerValue(10, 100),
0f)
testDynamicProperty(
"Opacity interpolation (0.5)",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_OPACITY,
LottieInterpolatedIntegerValue(10, 100),
0.5f)
testDynamicProperty(
"Opacity interpolation (1)",
KeyPath("Shape Layer 1", "Rectangle"),
LottieProperty.TRANSFORM_OPACITY,
LottieInterpolatedIntegerValue(10, 100),
1f)
}
private fun <T> testDynamicProperty(
name: String, keyPath: KeyPath, property: T, callback: LottieValueCallback<T>) {
testDynamicProperty(name, keyPath, property, callback, 0f)
}
private fun <T> testDynamicProperty(
name: String, keyPath: KeyPath, property: T, callback: LottieValueCallback<T>, progress: Float) {
val animationView = LottieAnimationView(context)
val composition = LottieComposition.Factory.fromFileSync(context, "Tests/Shapes.json")
animationView.setComposition(composition!!)
animationView.progress = progress
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
animationView.addValueCallback(keyPath, property, callback)
recordSnapshot(animationView, 1080, "android", "Dynamic Properties", name, params)
}
private fun testSwitchingToDrawableAndBack() {
val composition = LottieComposition.Factory.fromFileSync(context, "Tests/Shapes.json")
val view = LottieAnimationView(context)
view.setComposition(composition!!)
view.setImageDrawable(ColorDrawable(Color.RED))
view.setComposition(composition)
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
recordSnapshot(view, 1080, "android", "Reset Animation", "Drawable and back", params)
}
private fun testStartEndFrameWithStartEndProgress() {
var composition = LottieComposition.Factory.fromFileSync(context, "Tests/StartEndFrame.json")
var view = LottieAnimationView(context)
view.setComposition(composition!!)
val params = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
view.setMinProgress(0f)
view.progress = 0f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "minProgress 0", params)
view.setMinProgress(0.25f)
view.progress = 0f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "minProgress 0.25", params)
view.setMinProgress(0.75f)
view.progress = 0f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "minProgress 0.75", params)
view.setMinProgress(1f)
view.progress = 0f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "minProgress 1", params)
view.setMaxProgress(0f)
view.progress = 1f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "maxProgress 0", params)
view.setMaxProgress(0.25f)
view.progress = 1f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "maxProgress 0.25", params)
view.setMaxProgress(0.75f)
view.progress = 1f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "maxProgress 0.75", params)
view.setMaxProgress(1f)
view.progress = 1f
recordSnapshot(view, 1080, "android", "MinMaxFrame", "maxProgress 1", params)
composition = LottieComposition.Factory.fromFileSync(context, "Tests/EndFrame.json")
view = LottieAnimationView(context)
view.setComposition(composition!!)
view.frame = 29
recordSnapshot(view, 1080, "android", "EndFrame", "End Frame (red)", params)
view.frame = 30
recordSnapshot(view, 1080, "android", "EndFrame", "End Frame (blue)", params)
}
private fun testUrl() {
val composition = LottieCompositionFactory.fromUrlSync(context, "https://www.lottiefiles.com/download/427").value ?: return
drawComposition(composition, "GiftBox from LottieFiles URL (427)")
}
private fun dpToPx(dp: Int): Int {
val resources = context.resources
return TypedValue.applyDimension(1, dp.toFloat(), resources.displayMetrics).toInt()
}
private fun clearBitmap() {
renderCanvas.drawRect(0f, 0f, renderBitmap.width.toFloat(), renderBitmap.height.toFloat(), clearPaint)
}
companion object {
private val CORES = 1 //Runtime.getRuntime().availableProcessors();
}
}