Add local SVG load

commit_hash:0b5c855bb8ae2edd151706034d8966cfb7ca3d10
This commit is contained in:
vr-alekseev
2025-05-13 15:09:44 +03:00
parent e70a000af2
commit 2df48b430e
10 changed files with 109 additions and 83 deletions
-1
View File
@@ -992,7 +992,6 @@
"client/android/div-svg/src/main/java/com/yandex/div/svg/SvgCacheManager.kt":"divkit/public/client/android/div-svg/src/main/java/com/yandex/div/svg/SvgCacheManager.kt",
"client/android/div-svg/src/main/java/com/yandex/div/svg/SvgDecoder.kt":"divkit/public/client/android/div-svg/src/main/java/com/yandex/div/svg/SvgDecoder.kt",
"client/android/div-svg/src/main/java/com/yandex/div/svg/SvgDivImageLoader.kt":"divkit/public/client/android/div-svg/src/main/java/com/yandex/div/svg/SvgDivImageLoader.kt",
"client/android/div-svg/src/main/java/com/yandex/div/svg/SvgLoadWrapper.kt":"divkit/public/client/android/div-svg/src/main/java/com/yandex/div/svg/SvgLoadWrapper.kt",
"client/android/div-tests-coverage.gradle":"divkit/public/client/android/div-tests-coverage.gradle",
"client/android/div-tests.gradle":"divkit/public/client/android/div-tests.gradle",
"client/android/div-video/build.gradle":"divkit/public/client/android/div-video/build.gradle",
@@ -1,6 +1,8 @@
package com.yandex.div.svg
import android.content.Context
import android.widget.ImageView
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.core.images.DivImageDownloadCallback
import com.yandex.div.core.images.DivImageLoader
import com.yandex.div.core.images.LoadReference
@@ -12,16 +14,17 @@ import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
internal class SvgDivImageLoader : DivImageLoader {
@InternalApi
public class SvgDivImageLoader(private val context: Context) : DivImageLoader {
private val httpClient = OkHttpClient.Builder().build()
private val coroutineScope = MainScope()
private val svgDecoder = SvgDecoder()
private val svgCacheManager = SvgCacheManager()
override fun hasSvgSupport() = true
override fun hasSvgSupport(): Boolean = true
override fun loadImage(imageUrl: String, callback: DivImageDownloadCallback): LoadReference {
val call = createCall(imageUrl)
val call = createCallOrNull(imageUrl)
val pictureDrawable = svgCacheManager.get(imageUrl)
if (pictureDrawable != null) {
@@ -31,9 +34,12 @@ internal class SvgDivImageLoader : DivImageLoader {
coroutineScope.launch {
withContext(Dispatchers.IO) {
val bytes = runCatching {
call.execute().body?.bytes()
}.getOrNull() ?: return@withContext null
val bytes = if (call == null) {
getImageData(imageUrl)
} else {
downloadImage(call)
} ?: return@withContext null
val drawable = svgDecoder.decode(bytes.inputStream()) ?: return@withContext null
svgCacheManager.set(imageUrl, drawable)
@@ -44,18 +50,21 @@ internal class SvgDivImageLoader : DivImageLoader {
}
return LoadReference {
call.cancel()
call?.cancel()
}
}
override fun loadImage(imageUrl: String, imageView: ImageView): LoadReference {
val call = createCall(imageUrl)
val call = createCallOrNull(imageUrl)
coroutineScope.launch {
withContext(Dispatchers.IO) {
val bytes = runCatching {
call.execute().body?.bytes()
}.getOrNull() ?: return@withContext null
val bytes = if (call == null) {
getImageData(imageUrl)
} else {
downloadImage(call)
} ?: return@withContext null
svgDecoder.decode(bytes.inputStream())
}?.let {
imageView.setImageDrawable(it)
@@ -63,19 +72,34 @@ internal class SvgDivImageLoader : DivImageLoader {
}
return LoadReference {
call.cancel()
call?.cancel()
}
}
override fun loadImageBytes(
imageUrl: String,
callback: DivImageDownloadCallback
) = LoadReference {
): LoadReference = LoadReference {
loadImage(imageUrl, callback)
}
private fun createCall(imageUrl: String) : Call {
private fun createCallOrNull(imageUrl: String) : Call? {
if (!(imageUrl.startsWith("http://") || imageUrl.startsWith("https://"))) {
return null
}
val request = Request.Builder().url(imageUrl).build()
return httpClient.newCall(request)
}
private fun downloadImage(call: Call): ByteArray? = runCatching {
call.execute().body?.bytes()
}.getOrNull()
private fun getImageData(imageUrl: String): ByteArray? {
val assetPath = imageUrl.removePrefix("file:///android_asset/")
val stream = context.applicationContext?.assets?.open(assetPath) ?: return null
stream.use {
return it.readBytes()
}
}
}
@@ -1,51 +0,0 @@
package com.yandex.div.svg
import android.widget.ImageView
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.core.images.DivImageDownloadCallback
import com.yandex.div.core.images.DivImageLoader
import com.yandex.div.core.images.LoadReference
/**
* A wrapper over DivImageLoader.
* Replaces chosen image loader by SVG Image Loader if image loader doesn't support svg images.
*/
@InternalApi
public class SvgLoadWrapper(
private val providedImageLoader: DivImageLoader,
) : DivImageLoader {
private val svgImageLoader: SvgDivImageLoader? = if (!providedImageLoader.hasSvgSupport()) {
SvgDivImageLoader()
} else {
null
}
override fun loadImage(imageUrl: String, callback: DivImageDownloadCallback): LoadReference {
return getProperLoader(imageUrl).loadImage(imageUrl, callback)
}
override fun loadImage(imageUrl: String, imageView: ImageView): LoadReference {
return getProperLoader(imageUrl).loadImage(imageUrl, imageView)
}
override fun loadImageBytes(
imageUrl: String,
callback: DivImageDownloadCallback
): LoadReference {
return getProperLoader(imageUrl).loadImageBytes(imageUrl, callback)
}
private fun getProperLoader(imageUrl: String) : DivImageLoader {
return if (svgImageLoader != null && isSvg(imageUrl)) {
svgImageLoader
} else {
providedImageLoader
}
}
private fun isSvg(imageUrl: String): Boolean {
val queryStartIndex = imageUrl.indexOf('?')
val pathEndIndex = if (queryStartIndex == -1) imageUrl.length else queryStartIndex
return imageUrl.substring(0, pathEndIndex).endsWith(".svg")
}
}
@@ -2,16 +2,17 @@ package com.yandex.div.core;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.yandex.div.core.annotations.InternalApi;
import com.yandex.div.core.annotations.PublicApi;
import com.yandex.div.core.dagger.ExperimentFlag;
import com.yandex.div.core.dagger.Names;
import com.yandex.div.core.downloader.DivDownloader;
import com.yandex.div.core.experiments.Experiment;
import com.yandex.div.core.expression.variables.DivVariableController;
import com.yandex.div.core.expression.variables.GlobalVariableController;
import com.yandex.div.core.extension.DivExtensionHandler;
import com.yandex.div.core.font.DivTypefaceProvider;
import com.yandex.div.core.image.DivImageLoaderWrapper;
import com.yandex.div.core.images.DivImageLoader;
import com.yandex.div.core.player.DivPlayerFactory;
import com.yandex.div.core.player.DivPlayerPreloader;
@@ -29,6 +30,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Named;
/**
* Holds {@link com.yandex.div.core.view2.Div2View} configuration.
* Create instance using {@link Builder} class.
@@ -182,6 +185,7 @@ public class DivConfiguration {
@Provides
@NonNull
@Named(Names.UNWRAPPED_IMAGE_LOADER)
public DivImageLoader getImageLoader() {
return mImageLoader;
}
@@ -711,7 +715,7 @@ public class DivConfiguration {
DivPlayerPreloader divPlayerPreloader =
mDivPlayerPreloader == null ? divPlayerFactory.makePreloader() : mDivPlayerPreloader;
return new DivConfiguration(
new DivImageLoaderWrapper(mImageLoader),
mImageLoader,
mActionHandler == null ? new DivActionHandler() : mActionHandler,
mDiv2Logger == null ? Div2Logger.STUB : mDiv2Logger,
mDivDataChangeListener == null ? DivDataChangeListener.STUB : mDivDataChangeListener,
@@ -4,14 +4,18 @@ import android.content.Context;
import android.os.Build;
import android.renderscript.RenderScript;
import android.view.ContextThemeWrapper;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import com.yandex.div.core.DivCustomContainerViewAdapter;
import com.yandex.div.core.DivPreloader;
import com.yandex.div.core.DivViewDataPreloader;
import com.yandex.div.core.experiments.Experiment;
import com.yandex.div.core.extension.DivExtensionController;
import com.yandex.div.core.font.DivTypefaceProvider;
import com.yandex.div.core.image.DivImageLoaderWrapper;
import com.yandex.div.core.images.DivImageLoader;
import com.yandex.div.core.player.DivPlayerPreloader;
import com.yandex.div.core.resources.ContextThemeWrapperWithResourceCache;
import com.yandex.div.core.view2.DivImagePreloader;
@@ -91,6 +95,16 @@ abstract public class Div2Module {
RenderScript.CREATE_FLAG_NONE, context.getApplicationInfo().targetSdkVersion);
}
@Provides
@DivScope
@NonNull
public static DivImageLoader provideDivImageLoader(
@NonNull @Named(Names.UNWRAPPED_IMAGE_LOADER) DivImageLoader divImageLoader,
@NonNull @Named(Names.CONTEXT) Context context
) {
return new DivImageLoaderWrapper(divImageLoader, context);
}
@Provides
@DivScope
@NonNull
@@ -7,4 +7,5 @@ internal object Names {
const val THEMED_CONTEXT = "themed_context"
const val THEME = "theme"
const val HAS_DEFAULTS = "has_defaults"
const val UNWRAPPED_IMAGE_LOADER = "unwrapped_image_loader"
}
@@ -1,35 +1,50 @@
package com.yandex.div.core.image
import android.content.Context
import android.widget.ImageView
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.core.dagger.DivScope
import com.yandex.div.core.images.DivImageDownloadCallback
import com.yandex.div.core.images.DivImageLoader
import com.yandex.div.core.images.LoadReference
import com.yandex.div.svg.SvgLoadWrapper
import com.yandex.div.internal.util.makeIf
import com.yandex.div.svg.SvgDivImageLoader
import javax.inject.Inject
/**
* Wraps implementation of DivImageLoader from DivConfiguration.
*
* Modifies image_url from layout before providing it to wrapped DivImageLoader.
* A wrapper over DivImageLoader.
* Applies modifiers to image URL and
* replaces chosen image loader by SVG Image Loader if image loader doesn't support svg images.
*/
internal class DivImageLoaderWrapper(
providedImageLoader: DivImageLoader,
): DivImageLoader {
private val imageLoader = SvgLoadWrapper(providedImageLoader)
@DivScope
@InternalApi
class DivImageLoaderWrapper @Inject constructor(
private val providedImageLoader: DivImageLoader,
divContext: Context,
) : DivImageLoader {
private val modifiers: List<DivImageUrlModifier> = listOf(
DivImageAssetUrlModifier(),
)
private val svgImageLoader: SvgDivImageLoader? =
makeIf(!providedImageLoader.hasSvgSupport()) { SvgDivImageLoader(divContext) }
override fun loadImage(imageUrl: String, callback: DivImageDownloadCallback): LoadReference {
return imageLoader.loadImage(getModifiedUrl(imageUrl), callback)
val modifiedUrl = getModifiedUrl(imageUrl)
return getProperLoader(modifiedUrl).loadImage(modifiedUrl, callback)
}
override fun loadImage(imageUrl: String, imageView: ImageView): LoadReference {
return imageLoader.loadImage(getModifiedUrl(imageUrl), imageView)
val modifiedUrl = getModifiedUrl(imageUrl)
return getProperLoader(modifiedUrl).loadImage(modifiedUrl, imageView)
}
override fun loadImageBytes(imageUrl: String, callback: DivImageDownloadCallback): LoadReference {
return imageLoader.loadImageBytes(getModifiedUrl(imageUrl), callback)
override fun loadImageBytes(
imageUrl: String,
callback: DivImageDownloadCallback
): LoadReference {
val modifiedUrl = getModifiedUrl(imageUrl)
return getProperLoader(modifiedUrl).loadImageBytes(modifiedUrl, callback)
}
private fun getModifiedUrl(initialUrl: String): String {
@@ -39,4 +54,18 @@ internal class DivImageLoaderWrapper(
}
return url
}
private fun getProperLoader(imageUrl: String) : DivImageLoader {
return if (svgImageLoader != null && isSvg(imageUrl)) {
svgImageLoader
} else {
providedImageLoader
}
}
private fun isSvg(imageUrl: String): Boolean {
val queryStartIndex = imageUrl.indexOf('?')
val pathEndIndex = if (queryStartIndex < 0) imageUrl.length else queryStartIndex
return imageUrl.substring(0, pathEndIndex).endsWith(".svg")
}
}
@@ -62,3 +62,10 @@ internal class PositiveNumberDelegate<T: Number>(
this.value = if (value.toDouble() <= 0) fallbackValue else value
}
}
/**
* Like [takeIf], but creates object only if needed. Doesn't work with smart casts.
*/
inline fun <T> makeIf(shouldMake: Boolean, constructor: () -> T?): T? {
return if (shouldMake) constructor() else null
}
@@ -31,7 +31,6 @@ import com.yandex.div.shine.DivShineExtensionHandler
import com.yandex.div.shine.DivShineLogger
import com.yandex.div.sizeprovider.DivSizeProviderExtensionHandler
import com.yandex.div.video.ExoDivPlayerFactory
import com.yandex.div.video.ExoPlayerVideoPreloader
import com.yandex.div2.DivAction
import com.yandex.div2.DivData
import com.yandex.div2.DivPatch
@@ -8,9 +8,9 @@ import com.yandex.div.markdown.DivMarkdownExtensionHandler
import com.yandex.div.picasso.PicassoDivImageLoader
import com.yandex.div.rive.OkHttpDivRiveNetworkDelegate
import com.yandex.div.rive.RiveCustomViewAdapter
import com.yandex.divkit.sample.databinding.ActivityMainPageBinding
import com.yandex.div.zoom.DivPinchToZoomConfiguration
import com.yandex.div.zoom.DivPinchToZoomExtensionHandler
import com.yandex.divkit.sample.databinding.ActivityMainPageBinding
import okhttp3.OkHttpClient
class MainPageActivity : AppCompatActivity() {