diff --git a/.gitignore b/.gitignore index 4c3073c..e79d8c7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ xcuserdata *.pgp *.psd kotlins-js-store -.kotlin \ No newline at end of file +.kotlin +**.log \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index fea85fb..e755abf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,6 @@ @file:Suppress("DSL_SCOPE_VIOLATION") -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi - plugins { - id("root.publication") alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.android.application).apply(false) @@ -11,11 +8,11 @@ plugins { alias(libs.plugins.compose).apply(false) alias(libs.plugins.composeCompiler).apply(false) alias(libs.plugins.serialization).apply(false) + alias(libs.plugins.atomicfu).apply(false) } buildscript { dependencies { - classpath(libs.gp.atomicfu) classpath(libs.nexus.publish) } } diff --git a/compottie-network-core/build.gradle.kts b/compottie-network-core/build.gradle.kts index 88fa96e..8bb3a81 100644 --- a/compottie-network-core/build.gradle.kts +++ b/compottie-network-core/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("kotlinx-atomicfu") + alias(libs.plugins.atomicfu) } kotlin { diff --git a/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DiskLruCache.kt b/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DiskLruCache.kt index d38b48f..ba8ebdb 100644 --- a/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DiskLruCache.kt +++ b/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DiskLruCache.kt @@ -7,7 +7,6 @@ import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @@ -124,9 +123,7 @@ internal class DiskLruCache( private val journalFileTmp = directory / JOURNAL_FILE_TMP private val journalFileBackup = directory / JOURNAL_FILE_BACKUP private val lruEntries = LinkedHashMap() - private val cleanupScope = - @OptIn(ExperimentalCoroutinesApi::class) - CoroutineScope(SupervisorJob() + cleanupDispatcher.limitedParallelism(1)) + private val cleanupScope = CoroutineScope(SupervisorJob() + cleanupDispatcher.limitedParallelism(1)) private val lock = SynchronizedObject() private var size = 0L private var operationsSinceRewrite = 0 diff --git a/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/Network.kt b/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/Network.kt index cbd4357..564d193 100644 --- a/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/Network.kt +++ b/compottie-network-core/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/Network.kt @@ -3,9 +3,6 @@ package io.github.alexzhirkevich.compottie import kotlinx.coroutines.withContext import okio.Path -@OptIn(InternalCompottieApi::class) -private val NetworkLock = MapMutex() - @OptIn(InternalCompottieApi::class) internal suspend fun networkLoad( request : suspend (url: String) -> ByteArray, @@ -13,31 +10,29 @@ internal suspend fun networkLoad( url: String ): Pair { return withContext(Compottie.ioDispatcher()) { - NetworkLock.withLock(url) { + try { try { - try { - cacheStrategy.load(url)?.let { - return@withLock cacheStrategy.path(url) to it - } - } catch (_: Throwable) { + cacheStrategy.load(url)?.let { + return@withContext cacheStrategy.path(url) to it } + } catch (_: Throwable) { + } - val bytes = request(url) + val bytes = request(url) - try { - cacheStrategy.save(url, bytes)?.let { - return@withLock it to bytes - } - } catch (e: Throwable) { - Compottie.logger?.error( - "${this::class.simpleName} failed to cache downloaded asset", - e - ) + try { + cacheStrategy.save(url, bytes)?.let { + return@withContext it to bytes } - null to bytes - } catch (t: Throwable) { - null to null + } catch (e: Throwable) { + Compottie.logger?.error( + "${this::class.simpleName} failed to cache downloaded asset", + e + ) } + null to bytes + } catch (t: Throwable) { + null to null } } } \ No newline at end of file diff --git a/compottie-network/build.gradle.kts b/compottie-network/build.gradle.kts index 7382497..2912b8a 100644 --- a/compottie-network/build.gradle.kts +++ b/compottie-network/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("kotlinx-atomicfu") + alias(libs.plugins.atomicfu) } kotlin { diff --git a/compottie-network/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DefaultHttpClient.kt b/compottie-network/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DefaultHttpClient.kt index aff8486..a2027f3 100644 --- a/compottie-network/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DefaultHttpClient.kt +++ b/compottie-network/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/DefaultHttpClient.kt @@ -2,16 +2,19 @@ package io.github.alexzhirkevich.compottie import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpRequestRetry +import io.ktor.client.plugins.HttpTimeout import io.ktor.client.request.get import io.ktor.client.statement.bodyAsBytes -import io.ktor.client.statement.bodyAsChannel import io.ktor.utils.io.InternalAPI -import io.ktor.utils.io.toByteArray internal val DefaultHttpClient by lazy { HttpClient { followRedirects = true expectSuccess = true + install(HttpTimeout){ + requestTimeoutMillis = 15_000 + connectTimeoutMillis = 15_000 + } install(HttpRequestRetry) { maxRetries = 2 constantDelay(1000, 500) diff --git a/compottie/build.gradle.kts b/compottie/build.gradle.kts index db6c415..6f3363f 100644 --- a/compottie/build.gradle.kts +++ b/compottie/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.compose.ExperimentalComposeLibrary plugins { alias(libs.plugins.serialization) - id("kotlinx-atomicfu") + alias(libs.plugins.atomicfu) } kotlin { diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottieComposition.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottieComposition.kt index 4224b67..8a850b9 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottieComposition.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottieComposition.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlin.time.Duration import kotlin.time.Duration.Companion.microseconds -import kotlin.time.measureTime /** @@ -104,10 +103,9 @@ public fun rememberLottieComposition( } LaunchedEffect(result) { + try { - val composition = withContext(Compottie.ioDispatcher()) { - spec.load() - } + val composition = withContext(Compottie.ioDispatcher()) { spec.load() } result.complete(composition) } catch (c: CancellationException) { result.completeExceptionally(c) diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottiePainter.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottiePainter.kt index 2483fa1..ce516ef 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottiePainter.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LottiePainter.kt @@ -6,10 +6,12 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.scale @@ -87,22 +89,22 @@ public fun rememberLottiePainter( null -> null } - val painter by produceState( - EmptyPainter, - composition, - dp != null + val copy = dp != null + + val painter by produceState( + null, composition, copy ) { if (composition != null) { val assets = async(Compottie.ioDispatcher()) { - composition.loadAssets(assetsManager ?: EmptyAssetsManager, true) + composition.loadAssets(assetsManager ?: EmptyAssetsManager, copy) } val fonts = async(Compottie.ioDispatcher()) { composition.loadFonts(fontManager ?: EmptyFontManager) } value = LottiePainter( - composition = composition.deepCopy(), - progress = { updatedProgress() }, + composition = if (copy) composition.deepCopy() else composition, + progress = updatedProgress::invoke, dynamicProperties = dp, clipTextToBoundingBoxes = clipTextToBoundingBoxes, fontFamilyResolver = fontFamilyResolver, @@ -126,7 +128,7 @@ public fun rememberLottiePainter( enableMergePaths, enableExpressions ) { - (painter as? LottiePainter)?.let { + painter?.let { it.enableMergePaths = enableMergePaths it.enableExpressions = enableExpressions it.applyOpacityToLayers = applyOpacityToLayers @@ -136,14 +138,13 @@ public fun rememberLottiePainter( } } - LaunchedEffect( - painter, - dp - ) { - (painter as? LottiePainter)?.setDynamicProperties(dp) + LaunchedEffect(painter, dp) { + painter?.setDynamicProperties(dp) } - return painter + return remember { + LateInitPainter { painter } + } } /** @@ -186,7 +187,7 @@ public fun rememberLottiePainter( return rememberLottiePainter( composition = composition, - progress = { progress.value }, + progress = progress::value, assetsManager = assetsManager, fontManager = fontManager, dynamicProperties = dynamicProperties, @@ -222,9 +223,17 @@ public suspend fun LottiePainter( enableMergePaths: Boolean = false, enableExpressions: Boolean = true, ) : Painter = coroutineScope { + + val dp = when (dynamicProperties) { + is DynamicCompositionProvider -> dynamicProperties + null -> null + } + + val copy = dp != null + val assets = async(Compottie.ioDispatcher()) { assetsManager?.let { - composition.loadAssets(it, true) + composition.loadAssets(it, copy) } } val fonts = async(Compottie.ioDispatcher()) { @@ -234,12 +243,9 @@ public suspend fun LottiePainter( } LottiePainter( - composition = composition.deepCopy(), + composition = if (copy) composition.deepCopy() else composition, progress = progress, - dynamicProperties = when (dynamicProperties) { - is DynamicCompositionProvider -> dynamicProperties - null -> null - }, + dynamicProperties = dp, clipTextToBoundingBoxes = clipTextToBoundingBoxes, enableTextGrouping = enableTextGrouping, fontFamilyResolver = makeFontFamilyResolver(), @@ -255,12 +261,31 @@ public suspend fun LottiePainter( internal expect fun makeFontFamilyResolver() : FontFamily.Resolver internal expect fun mockFontFamilyResolver() : FontFamily.Resolver -private object EmptyPainter : Painter() { +private class LateInitPainter( + val painter : () -> LottiePainter? +) : Painter() { + private var alpha by mutableStateOf(1f) + private var colorFilter by mutableStateOf(null) + + override val intrinsicSize: Size by derivedStateOf { + painter()?.intrinsicSize ?: Size(1f,1f) + } + + override fun applyAlpha(alpha: Float): Boolean { + this.alpha = alpha + return true + } - override val intrinsicSize: Size = Size(1f,1f) + override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { + this.colorFilter = colorFilter + return true + } override fun DrawScope.onDraw() { + painter()?.run { + draw(size, alpha, colorFilter) + } } } @@ -332,7 +357,7 @@ private class LottiePainter( var enableMergePaths: Boolean by animationState::enableMergePaths var enableExpressions: Boolean by animationState::enableExpressions - override fun applyAlpha(alpha: Float): Boolean { + public override fun applyAlpha(alpha: Float): Boolean { if (alpha !in 0f..1f) return false diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LruMap.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LruMap.kt index 9fb0d37..835ad4d 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LruMap.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/LruMap.kt @@ -2,39 +2,36 @@ package io.github.alexzhirkevich.compottie import kotlinx.atomicfu.locks.SynchronizedObject import kotlinx.atomicfu.locks.synchronized -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock internal class LruMap( private val delegate : LinkedHashMap = LinkedHashMap(), private val limit : () -> Int, -) : MutableMap by delegate { +) : SynchronizedObject(), MutableMap by delegate { @OptIn(InternalCompottieApi::class) - private val suspendGetOrPutMutex = MapMutex() - private val lock = SynchronizedObject() + private val suspendGetOrPutMutex = MultiOwnerMutex() - override fun put(key: Any, value: T): T? = synchronized(lock) { + override fun put(key: Any, value: T): T? = synchronized(this) { putRaw(key, value) } - override fun clear() = synchronized(lock) { + override fun clear() = synchronized(this) { clearRaw() } - override fun putAll(from: Map) = synchronized(lock) { + override fun putAll(from: Map) = synchronized(this) { putAllRaw(from) } - override fun remove(key: Any): T? = synchronized(lock) { + override fun remove(key: Any): T? = synchronized(this) { removeRaw(key) } - override fun get(key: Any): T? = synchronized(lock) { + override fun get(key: Any): T? = synchronized(this) { getRaw(key) } - fun getOrPut(key: Any?, put: () -> T): T = synchronized(lock) { + fun getOrPut(key: Any?, put: () -> T): T = synchronized(this) { if (key == null) return put() @@ -55,7 +52,7 @@ internal class LruMap( private fun putRaw(key: Any, value: T): T? { val cacheLimit = limit() - if (cacheLimit < 1){ + if (cacheLimit < 1) { clearRaw() } else { while (cacheLimit < size) { @@ -79,5 +76,6 @@ internal class LruMap( } private fun removeRaw(key: Any): T? = delegate.remove(key) + private fun clearRaw() = delegate.clear() } \ No newline at end of file diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/MapMutex.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/MultiOwnerMutex.kt similarity index 54% rename from compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/MapMutex.kt rename to compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/MultiOwnerMutex.kt index 46d74b2..2179909 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/MapMutex.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/MultiOwnerMutex.kt @@ -1,33 +1,31 @@ package io.github.alexzhirkevich.compottie -import kotlinx.atomicfu.locks.SynchronizedObject -import kotlinx.atomicfu.locks.synchronized -import kotlinx.coroutines.delay +import kotlinx.atomicfu.locks.reentrantLock +import kotlinx.atomicfu.locks.withLock import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -private class MutexCount( +private class RefCountMutex( val mutex: Mutex = Mutex(), var waiters : Int = 0 ) @InternalCompottieApi -public class MapMutex { +public class MultiOwnerMutex { - private val lock = SynchronizedObject() - private val mutex = mutableMapOf() + private val lock = reentrantLock() + private val mutex = mutableMapOf() public suspend fun withLock(key: Any, action: suspend () -> T): T { - val keyLock = synchronized(lock) { - mutex.getOrPut(key) { MutexCount() }.also { it.waiters++ } + val keyLock = lock.withLock { + mutex.getOrPut(key) { RefCountMutex() }.also { it.waiters++ } } - Mutex().lock() return try { keyLock.mutex.withLock(key) { action() } } finally { - synchronized(lock) { + lock.withLock { if (--keyLock.waiters <= 0) { mutex.remove(key) } diff --git a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/content/ContentGroupImpl.kt b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/content/ContentGroupImpl.kt index 60994df..bfa3354 100644 --- a/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/content/ContentGroupImpl.kt +++ b/compottie/src/commonMain/kotlin/io/github/alexzhirkevich/compottie/internal/content/ContentGroupImpl.kt @@ -1,7 +1,6 @@ package io.github.alexzhirkevich.compottie.internal.content import androidx.compose.ui.geometry.MutableRect -import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.graphics.Paint import androidx.compose.ui.graphics.Path @@ -11,7 +10,7 @@ import io.github.alexzhirkevich.compottie.internal.AnimationState import io.github.alexzhirkevich.compottie.internal.animation.AnimatedTransform import io.github.alexzhirkevich.compottie.internal.animation.interpolatedNorm import io.github.alexzhirkevich.compottie.internal.platform.addPath -import io.github.alexzhirkevich.compottie.internal.utils.fastReset +import io.github.alexzhirkevich.compottie.internal.platform.saveLayer import io.github.alexzhirkevich.compottie.internal.utils.fastSetFrom import io.github.alexzhirkevich.compottie.internal.utils.preConcat import io.github.alexzhirkevich.compottie.internal.utils.union @@ -83,7 +82,7 @@ internal class ContentGroupImpl( offscreenRect.set(0f,0f,0f,0f) getBounds(drawScope, matrix, true, state, offscreenRect) offscreenPaint.alpha = layerAlpha - canvas.saveLayer(offscreenRect.toRect(), offscreenPaint) + canvas.saveLayer(offscreenRect, offscreenPaint) } val childAlpha = if (isRenderingWithOffScreen) 1f else layerAlpha diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa7ee18..633ecbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ agp = "8.1.4" okio = "3.9.1" serialization="1.6.3" ktor="3.0.0" -atomicfu="0.23.2" +atomicfu="0.26.0" coroutines="1.9.0" nexus-publish="2.0.0" coil="3.0.0-alpha10" @@ -24,7 +24,6 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } ktor-client-ios = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } -gp-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfu" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } nexus-publish = { module = "io.github.gradle-nexus.publish-plugin:io.github.gradle-nexus.publish-plugin.gradle.plugin", version.ref = "nexus-publish" } kotlin-gp = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -33,6 +32,7 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } [plugins] +atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" } compose = { id = "org.jetbrains.compose", version.ref = "compose" } kotlin-multiplatform = { id ="org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } android-application = { id = "com.android.application", version.ref = "agp" }