[Compose] Add support for images
diff --git a/.idea/misc.xml b/.idea/misc.xml index c3085c1..d72f78c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml
@@ -26,6 +26,8 @@ <entry key="../../../../layout/compose-model-1615661124216.xml" value="0.28631756756756754" /> <entry key="../../../../layout/compose-model-1615661338631.xml" value="1.0" /> <entry key="../../../../layout/compose-model-1615661344364.xml" value="0.28607594936708863" /> + <entry key="../../../../layout/custom_preview.xml" value="1.0" /> + <entry key="issue-repro/src/main/res/layout/issue_repro_activity.xml" value="0.2545289855072464" /> </map> </option> </component>
diff --git a/.idea/modules.xml b/.idea/modules.xml index 2590511..ec33ca5 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml
@@ -4,6 +4,7 @@ <modules> <module fileurl="file://$PROJECT_DIR$/.idea/lottie-android.iml" filepath="$PROJECT_DIR$/.idea/lottie-android.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/issue-repro/lottie-android.issue-repro.iml" filepath="$PROJECT_DIR$/.idea/modules/issue-repro/lottie-android.issue-repro.iml" /> + <module fileurl="file://$PROJECT_DIR$/.idea/modules/issue-repro-compose/lottie-android.issue-repro-compose.iml" filepath="$PROJECT_DIR$/.idea/modules/issue-repro-compose/lottie-android.issue-repro-compose.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/lottie/lottie-android.lottie.iml" filepath="$PROJECT_DIR$/.idea/modules/lottie/lottie-android.lottie.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/lottie-compose/lottie-android.lottie-compose.iml" filepath="$PROJECT_DIR$/.idea/modules/lottie-compose/lottie-android.lottie-compose.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/modules/sample/lottie-android.sample.iml" filepath="$PROJECT_DIR$/.idea/modules/sample/lottie-android.sample.iml" />
diff --git a/issue-repro-compose/build.gradle b/issue-repro-compose/build.gradle new file mode 100755 index 0000000..72f8700 --- /dev/null +++ b/issue-repro-compose/build.gradle
@@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 30 + defaultConfig { + applicationId "com.airbnb.lottie.issues.compose" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion composeVersion + kotlinCompilerVersion kotlinVersion + } +} + +dependencies { + implementation project(':lottie-compose') + implementation 'androidx.appcompat:appcompat:1.3.0-beta01' + implementation 'androidx.activity:activity-compose:1.3.0-alpha04' + implementation "androidx.compose.ui:ui:$composeVersion" + implementation "androidx.compose.material:material:$composeVersion" + implementation "androidx.compose.material:material-icons-extended:$composeVersion" + implementation "androidx.compose.ui:ui-tooling:$composeVersion" +}
diff --git a/issue-repro-compose/src/main/AndroidManifest.xml b/issue-repro-compose/src/main/AndroidManifest.xml new file mode 100755 index 0000000..3a75e49 --- /dev/null +++ b/issue-repro-compose/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.airbnb.lottie.issues.compose"> + + <application + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:theme="@style/Theme.AppCompat.Light.NoActionBar"> + <activity android:name=".ComposeIssueReproActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> \ No newline at end of file
diff --git a/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt new file mode 100755 index 0000000..4e55a5d --- /dev/null +++ b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
@@ -0,0 +1,19 @@ +package com.airbnb.lottie.issues.compose + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable + +class ComposeIssueReproActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Content() + } + } + + @Composable + fun Content() { + } +}
diff --git a/issue-repro-compose/src/main/res/mipmap-hdpi/ic_launcher.png b/issue-repro-compose/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000..898f3ed --- /dev/null +++ b/issue-repro-compose/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differ
diff --git a/issue-repro-compose/src/main/res/mipmap-mdpi/ic_launcher.png b/issue-repro-compose/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000..64ba76f --- /dev/null +++ b/issue-repro-compose/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differ
diff --git a/issue-repro-compose/src/main/res/mipmap-xhdpi/ic_launcher.png b/issue-repro-compose/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000..e5ed465 --- /dev/null +++ b/issue-repro-compose/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differ
diff --git a/issue-repro-compose/src/main/res/mipmap-xxhdpi/ic_launcher.png b/issue-repro-compose/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..b0907ca --- /dev/null +++ b/issue-repro-compose/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differ
diff --git a/issue-repro-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/issue-repro-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..2c18de9 --- /dev/null +++ b/issue-repro-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differ
diff --git a/issue-repro-compose/src/main/res/values/strings.xml b/issue-repro-compose/src/main/res/values/strings.xml new file mode 100755 index 0000000..dd8717f --- /dev/null +++ b/issue-repro-compose/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@ +<resources> + <string name="app_name">Lottie Issue</string> +</resources>
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/ImageAssetDelegateSetter.kt b/lottie-compose/src/main/java/com/airbnb/lottie/ImageAssetDelegateSetter.kt new file mode 100644 index 0000000..222e45d --- /dev/null +++ b/lottie-compose/src/main/java/com/airbnb/lottie/ImageAssetDelegateSetter.kt
@@ -0,0 +1,10 @@ +package com.airbnb.lottie + +import com.airbnb.lottie.manager.ImageAssetManager + +/** + * Proxy for this internal API. + */ +internal fun LottieDrawable.setImageAssetManager(imageAssetManager: ImageAssetManager?) { + this.setImageAssetManager(imageAssetManager) +} \ No newline at end of file
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt index a242575..57feaa7 100644 --- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt +++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
@@ -13,6 +13,8 @@ import com.airbnb.lottie.LottieComposition import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieDrawable +import com.airbnb.lottie.manager.ImageAssetManager +import com.airbnb.lottie.setImageAssetManager import java.io.FileInputStream import java.util.concurrent.TimeUnit import java.util.zip.ZipInputStream @@ -80,6 +82,16 @@ modifier: Modifier = Modifier, ) { val drawable = remember { LottieDrawable() } + var imageAssetManager: ImageAssetManager? by remember { mutableStateOf(null) } + + if (composition?.hasImages() == true) { + val context = LocalContext.current + LaunchedEffect(context, composition, state.imageAssetsFolder, state.imageAssetDelegate) { + imageAssetManager = ImageAssetManager(context, state.imageAssetsFolder, state.imageAssetDelegate, composition.images) + } + } else { + imageAssetManager = null + } SideEffect { drawable.composition = composition @@ -119,10 +131,8 @@ .maintainAspectRatio(composition) ) { drawIntoCanvas { canvas -> - drawable.progress = state.progress - drawable.setOutlineMasksAndMattes(state.outlineMasksAndMattes) - drawable.isApplyingOpacityToLayersEnabled = state.applyOpacityToLayers - drawable.enableMergePathsForKitKatAndAbove(state.enableMergePaths) + state.applyTo(drawable) + drawable.setImageAssetManager(imageAssetManager) withTransform({ scale(size.width / composition.bounds.width().toFloat(), size.height / composition.bounds.height().toFloat(), Offset.Zero) }) {
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt index b410285..6801bd0 100644 --- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt +++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimationState.kt
@@ -1,10 +1,8 @@ package com.airbnb.lottie.compose -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* +import com.airbnb.lottie.ImageAssetDelegate +import com.airbnb.lottie.LottieDrawable /** * Create a [LottieAnimationState] and remember it @@ -13,13 +11,18 @@ * @param repeatCount Initial value for [LottieAnimationState.repeatCount] * @param initialProgress Initial value for [LottieAnimationState.progress] * @param enableMergePaths Initial value for [LottieAnimationState.enableMergePaths] + * @param imageAssetsFolder Initial value for [LottieAnimationState.imageAssetsFolder] + * @param imageAssetDelegate Initial value for [LottieAnimationState.imageAssetDelegate] */ @Composable fun rememberLottieAnimationState( autoPlay: Boolean = true, repeatCount: Int = 0, initialProgress: Float = 0f, - enableMergePaths: Boolean = true + enableMergePaths: Boolean = true, + imageAssetsFolder: String? = null, + imageAssetDelegate: ImageAssetDelegate? = null, + ): LottieAnimationState { // Use rememberSavedInstanceState so you can pause/resume animations return remember(repeatCount, autoPlay) { @@ -27,7 +30,9 @@ isPlaying = autoPlay, repeatCount = repeatCount, initialProgress = initialProgress, - enableMergePaths = enableMergePaths + enableMergePaths = enableMergePaths, + imageAssetsFolder = imageAssetsFolder, + imageAssetDelegate = imageAssetDelegate, ) } } @@ -39,6 +44,8 @@ * @param repeatCount Initial value for [repeatCount] * @param initialProgress Initial value for [progress] * @param enableMergePaths Initial value for [enableMergePaths] + * @param imageAssetsFolder Initial value for [LottieAnimationState.imageAssetsFolder] + * @param imageAssetDelegate Initial value for [LottieAnimationState.imageAssetDelegate] * * @see rememberLottieAnimationState */ @@ -46,7 +53,9 @@ isPlaying: Boolean, repeatCount: Int = 0, initialProgress: Float = 0f, - enableMergePaths: Boolean = true + enableMergePaths: Boolean = true, + imageAssetsFolder: String? = null, + imageAssetDelegate: ImageAssetDelegate? = null, ) { var progress by mutableStateOf(initialProgress) @@ -100,6 +109,35 @@ */ var enableMergePaths by mutableStateOf(enableMergePaths) + /** + * If you use image assets, you must explicitly specify the folder in assets/ in which they are + * located because bodymovin uses the name filenames across all compositions (img_#). + * Do NOT rename the images themselves. + * <p> + * If your images are located in src/main/assets/airbnb_loader/ then set this to "airbnb_loader". + * <p> + * <p> + * Be wary if you are using many images, however. Lottie is designed to work with vector shapes + * from After Effects. If your images look like they could be represented with vector shapes, + * see if it is possible to convert them to shape layers and re-export your animation. Check + * the documentation at https://airbnb.io/lottie for more information about importing shapes from + * Sketch or Illustrator to avoid this. + */ + var imageAssetsFolder by mutableStateOf(imageAssetsFolder) + + /** + * Use this if you can't bundle images with your app. This may be useful if you download the + * animations from the network or have the images saved to an SD Card. In that case, Lottie + * will defer the loading of the bitmap to this delegate. + * <p> + * Be wary if you are using many images, however. Lottie is designed to work with vector shapes + * from After Effects. If your images look like they could be represented with vector shapes, + * see if it is possible to convert them to shape layers and re-export your animation. Check + * the documentation at https://airbnb.io/lottie for more information about importing shapes from + * Sketch or Illustrator to avoid this. + */ + var imageAssetDelegate by mutableStateOf(imageAssetDelegate) + internal fun updateFrame(frame: Int) { _frame.value = frame } @@ -107,4 +145,11 @@ fun toggleIsPlaying() { isPlaying = !isPlaying } + + internal fun applyTo(drawable: LottieDrawable) { + drawable.progress = progress + drawable.setOutlineMasksAndMattes(outlineMasksAndMattes) + drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers + drawable.enableMergePathsForKitKatAndAbove(enableMergePaths) + } } \ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java index 8bfecd9..14ff5ac 100644 --- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java +++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -23,6 +23,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; import com.airbnb.lottie.manager.FontAssetManager; import com.airbnb.lottie.manager.ImageAssetManager; @@ -75,6 +76,15 @@ } } }; + + /** + * ImageAssetManager created externally. By Compose, for example. + */ + @Nullable + private ImageAssetManager imageAssetManagerOverride; + /** + * ImageAssetManager created automatically by Lottie for views. + */ @Nullable private ImageAssetManager imageAssetManager; @Nullable @@ -1095,7 +1105,18 @@ return null; } + /** + * Use by Lottie internally when outside of a normal View tree such as for Jetpack Compose. + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + void setImageAssetManager(@Nullable ImageAssetManager imageAssetManager) { + this.imageAssetManagerOverride = imageAssetManager; + } + private ImageAssetManager getImageAssetManager() { + if (imageAssetManagerOverride != null) { + return imageAssetManagerOverride; + } if (getCallback() == null) { // We can't get a bitmap since we can't get a Context from the callback. return null;
diff --git a/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java b/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java index 7182b70..833cf61 100644 --- a/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java +++ b/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java
@@ -24,16 +24,16 @@ private static final Object bitmapHashLock = new Object(); private final Context context; - private String imagesFolder; + private final String imagesFolder; @Nullable private ImageAssetDelegate delegate; private final Map<String, LottieImageAsset> imageAssets; public ImageAssetManager(Drawable.Callback callback, String imagesFolder, ImageAssetDelegate delegate, Map<String, LottieImageAsset> imageAssets) { - this.imagesFolder = imagesFolder; - if (!TextUtils.isEmpty(imagesFolder) && - this.imagesFolder.charAt(this.imagesFolder.length() - 1) != '/') { - this.imagesFolder += '/'; + if (!TextUtils.isEmpty(imagesFolder) && imagesFolder.charAt(imagesFolder.length() - 1) != '/') { + this.imagesFolder = imagesFolder + '/'; + } else { + this.imagesFolder = imagesFolder; } if (!(callback instanceof View)) { @@ -48,6 +48,17 @@ setDelegate(delegate); } + public ImageAssetManager(Context context, String imagesFolder, ImageAssetDelegate delegate, Map<String, LottieImageAsset> imageAssets) { + this.context = context; + if (!TextUtils.isEmpty(imagesFolder) && imagesFolder.charAt(imagesFolder.length() - 1) != '/') { + this.imagesFolder = imagesFolder + '/'; + } else { + this.imagesFolder = imagesFolder; + } + this.imageAssets = imageAssets; + setDelegate(delegate); + } + public void setDelegate(@Nullable ImageAssetDelegate assetDelegate) { this.delegate = assetDelegate; }
diff --git a/sample-compose/build.gradle b/sample-compose/build.gradle index 6c57c64..8b6535d 100644 --- a/sample-compose/build.gradle +++ b/sample-compose/build.gradle
@@ -56,20 +56,20 @@ implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.activity:activity-ktx:1.2.1' - implementation 'androidx.activity:activity-compose:1.3.0-alpha03' + implementation 'androidx.activity:activity-compose:1.3.0-alpha04' implementation 'androidx.appcompat:appcompat:1.3.0-beta01' implementation 'com.google.android.material:material:1.3.0' implementation "androidx.compose.ui:ui:$composeVersion" implementation "androidx.compose.material:material:$composeVersion" implementation "androidx.compose.material:material-icons-extended:$composeVersion" implementation "androidx.compose.ui:ui-tooling:$composeVersion" - implementation "androidx.navigation:navigation-compose:1.0.0-alpha08" + implementation "androidx.navigation:navigation-compose:1.0.0-alpha09" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0' implementation "androidx.navigation:navigation-ui-ktx:2.3.4" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' implementation "com.google.dagger:dagger:$daggerVersion" @@ -77,7 +77,7 @@ implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' - implementation "dev.chrisbanes.accompanist:accompanist-coil:0.6.0" + implementation "dev.chrisbanes.accompanist:accompanist-coil:0.6.1" implementation 'com.airbnb.android:mavericks:2.1.0' implementation 'com.airbnb.android:mavericks-compose:2.1.0-alpha01'
diff --git a/settings.gradle b/settings.gradle index 428b963..a895a44 100644 --- a/settings.gradle +++ b/settings.gradle
@@ -3,3 +3,4 @@ include ':sample' include ':sample-compose' include ':issue-repro' +include ':issue-repro-compose'