Skip to content

Commit

Permalink
feat: use Coil instead of Fresco to load images on the Android platform
Browse files Browse the repository at this point in the history
  • Loading branch information
JimmyDaddy committed Dec 20, 2023
1 parent a178178 commit 032aac0
Show file tree
Hide file tree
Showing 36 changed files with 1,516 additions and 708 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
if: steps.verify-dev-changed-files.outputs.any_changed == 'true'
uses: actions/setup-node@v3
with:
node-version: '16'
node-version: '18'

- name: Install npm dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' && steps.verify-dev-changed-files.outputs.any_changed == 'true'
Expand Down Expand Up @@ -108,14 +108,14 @@ jobs:
uses: actions/setup-node@v3
if: steps.verify-android-changed-files.outputs.any_changed == 'true'
with:
node-version: '16'
node-version: '18'

- name: Set up JDK
uses: actions/setup-java@v3
if: steps.verify-android-changed-files.outputs.any_changed == 'true'
with:
distribution: 'zulu'
java-version: 11
java-version: 17

- name: Install Gradle dependencies
if: steps.cache-gradle.outputs.cache-hit != 'true' && steps.verify-android-changed-files.outputs.any_changed == 'true'
Expand Down Expand Up @@ -201,14 +201,14 @@ jobs:
uses: actions/setup-node@v3
if: steps.verify-android-changed-files.outputs.any_changed == 'true'
with:
node-version: '16'
node-version: '18'

- name: Set up JDK
uses: actions/setup-java@v3
if: steps.verify-android-changed-files.outputs.any_changed == 'true'
with:
distribution: 'zulu'
java-version: 11
java-version: 17

- name: Instrumentation Tests
uses: reactivecircus/android-emulator-runner@v2
Expand Down Expand Up @@ -285,7 +285,7 @@ jobs:
if: steps.verify-iOS-changed-files.outputs.any_changed == 'true'
uses: actions/setup-node@v3
with:
node-version: '16'
node-version: '18'

- name: Install Pods
if: steps.cache-pods.outputs.cache-hit != 'true' && steps.verify-iOS-changed-files.outputs.any_changed == 'true'
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ DerivedData
*.ipa
*.xcuserstate
project.xcworkspace
.xcode.env.local

# Android/IJ
#
Expand Down Expand Up @@ -72,3 +73,6 @@ android/keystores/debug.keystore
lib/

docs/**/*.html

# testing
/coverage
5 changes: 0 additions & 5 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ const options = {
quality: 100,
filename: 'test',
saveFormat: ImageFormat.png,
maxSize: 1000,
};
Marker.markText(options);

Expand Down Expand Up @@ -154,7 +153,6 @@ const options = {
quality: 100,
filename: 'test',
saveFormat: ImageFormat.png,
maxSize: 1000,
};
Marker.markText(options);
```
Expand Down Expand Up @@ -206,7 +204,6 @@ const options = {
quality: 100,
filename: 'test',
saveFormat: ImageFormat.png,
maxSize: 1000,
};
ImageMarker.markText(options);

Expand Down Expand Up @@ -268,7 +265,6 @@ const options = {
quality: 100,
filename: 'test',
saveFormat: ImageFormat.png,
maxSize: 1000,
};
ImageMarker.markText(options);

Expand Down Expand Up @@ -316,7 +312,6 @@ const options = {
quality: 100,
filename: 'test',
saveFormat: ImageFormat.png,
maxSize: 1000,
};
Marker.markText(options);

Expand Down
17 changes: 13 additions & 4 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ buildscript {
mavenCentral()
}

dependencies {
classpath "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0"
classpath "com.android.tools.build:gradle:7.2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
if (project == rootProject) {
repositories {
google()
mavenCentral()
}
} else {
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
}

Expand Down Expand Up @@ -76,6 +82,9 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
testImplementation "org.mockito:mockito-core:3.+"
implementation "io.coil-kt:coil:2.5.0"
implementation "io.coil-kt:coil-svg:2.5.0"
implementation "io.coil-kt:coil-gif:2.5.0"
}

if (isNewArchitectureEnabled()) {
Expand Down
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ImageMarker_kotlinVersion=1.7.0
ImageMarker_kotlinVersion=1.8.0
ImageMarker_minSdkVersion=24
ImageMarker_targetSdkVersion=31
ImageMarker_compileSdkVersion=31
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ class ImageMarkerManager(private val context: ReactApplicationContext) : ReactCo
Log.d(IMAGE_MARKER_TAG, "src: " + markOpts.backgroundImage.src.toString())
GlobalScope.launch(Dispatchers.Main) {
try {
val bitmaps = ImageLoader(context, markOpts.maxSize).loadImages(
val bitmaps = MarkerImageLoader(context, markOpts.maxSize).loadImages(
listOf(
markOpts.backgroundImage,
)
Expand Down Expand Up @@ -256,7 +256,7 @@ class ImageMarkerManager(private val context: ReactApplicationContext) : ReactCo
val concatenatedArray = listOf(
markOpts.backgroundImage,
).plus(markers)
val bitmaps = ImageLoader(context, markOpts.maxSize).loadImages(
val bitmaps = MarkerImageLoader(context, markOpts.maxSize).loadImages(
concatenatedArray
)
val bg = bitmaps[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,79 +5,78 @@ import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import androidx.annotation.RequiresApi
import com.facebook.common.internal.Supplier
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.cache.MemoryCacheParams
import com.facebook.imagepipeline.common.ResizeOptions
import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import androidx.core.graphics.drawable.toBitmap
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.modules.systeminfo.ReactNativeVersion
import com.jimmydaddy.imagemarker.base.Constants.IMAGE_MARKER_TAG
import com.jimmydaddy.imagemarker.base.ErrorCode
import com.jimmydaddy.imagemarker.base.ImageOptions
import com.jimmydaddy.imagemarker.base.MarkerError
import com.jimmydaddy.imagemarker.base.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.concurrent.Executors

class ImageLoader(private val context: ReactApplicationContext, private val maxSize: Int) {
class MarkerImageLoader(private val context: ReactApplicationContext, private val maxSize: Int) {

init {
if (maxSize > 0) {
setMaxBitmapSize(maxSize)
private var imageLoader: ImageLoader = ImageLoader.Builder(context)
.components {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(SvgDecoder.Factory())
}
}
.allowHardware(false)
.build()
private val resources: Resources
get() = context.resources

@RequiresApi(Build.VERSION_CODES.N)
suspend fun loadImages(images: List<ImageOptions>): List<Bitmap?> = withContext(Dispatchers.IO) {

val deferredList = images.map { img ->
async {
try {
val isFrescoImg = isFrescoImg(img.uri)
Log.d(IMAGE_MARKER_TAG, "isFrescoImg: " + isFrescoImg(img.uri))
if (isFrescoImg) {
val isCoilImg = isCoilImg(img.uri)
Log.d(IMAGE_MARKER_TAG, "isCoilImg: $isCoilImg")
if (isCoilImg) {
val future = CompletableFuture<Bitmap?>()
var imageRequest = ImageRequest.fromUri(img.uri)
var request = ImageRequest.Builder(context)
.data(img.uri)
if (img.src != null && img.src.width > 0 && img.src.height > 0) {
val options: ResizeOptions? = ResizeOptions(img.src.width, img.src.height)
imageRequest = ImageRequestBuilder.fromRequest(imageRequest).setResizeOptions(options).build()
request = request.size(img.src.width, img.src.height)
Log.d(IMAGE_MARKER_TAG, "src.width: " + img.src.width + " src.height: " + img.src.height)
}
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, null)
val executor: Executor = Executors.newSingleThreadExecutor()
dataSource.subscribe(object : BaseBitmapDataSubscriber() {
public override fun onNewResultImpl(bitmap: Bitmap?) {
if (bitmap != null) {
val bg = ImageProcess.scaleBitmap(bitmap, img.scale)
future.complete(bg)
} else {
imageLoader.enqueue(request.target (
onStart = { _ ->
// Handle the placeholder drawable.
Log.d(IMAGE_MARKER_TAG, "start to load image: " + img.uri)
},
onSuccess = { result ->
val bitmap = result.toBitmap()
val bg = ImageProcess.scaleBitmap(bitmap, img.scale)
if (bg == null) {
future.completeExceptionally(MarkerError(ErrorCode.LOAD_IMAGE_FAILED,
"Can't retrieve the file from the src: " + img.uri))
"Can't retrieve the file from the src: " + img.uri))
}
}

override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
future.complete(bg)
},
onError = { _ ->
future.completeExceptionally(MarkerError(ErrorCode.LOAD_IMAGE_FAILED,
"Can't retrieve the file from the src: " + img.uri))
}
}, executor)
).build())
return@async future.get()

} else {
val resId = getDrawableResourceByName(img.uri)
Log.d(IMAGE_MARKER_TAG, "resId: $resId")
Expand Down Expand Up @@ -111,7 +110,7 @@ class ImageLoader(private val context: ReactApplicationContext, private val maxS
deferredList.awaitAll()
}

private fun isFrescoImg(uri: String?): Boolean {
private fun isCoilImg(uri: String?): Boolean {
// val base64Pattern =
// "^data:(image|img)/(bmp|jpg|png|tif|gif|pcx|tga|exif|fpx|svg|psd|cdr|pcd|dxf|ufo|eps|ai|raw|WMF|webp);base64,(([[A-Za-z0-9+/])*\\s\\S*)*"
return uri!!.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("file://") || uri.startsWith(
Expand All @@ -128,29 +127,4 @@ class ImageLoader(private val context: ReactApplicationContext, private val maxS
)
}

private fun setMaxBitmapSize(maxSize: Int) {
val major = Utils.getStringSafe("major", ReactNativeVersion.VERSION)
val minor = Utils.getStringSafe("minor", ReactNativeVersion.VERSION)
val patch = Utils.getStringSafe("patch", ReactNativeVersion.VERSION)
if (Integer.valueOf(major.toString()) >= 0 && Integer.valueOf(minor.toString()) >= 60 && Integer.valueOf(
patch.toString()
) >= 0
) {
val bitmapMemoryCacheParamsSupplier = Supplier<MemoryCacheParams> {
MemoryCacheParams(
maxSize, // max cache entry size
Integer.MAX_VALUE, // max cache entries
maxSize, // max cache size
Integer.MAX_VALUE, // max cache eviction size
Integer.MAX_VALUE // max cache eviction count
)
}

val config = ImagePipelineConfig.newBuilder(context)
.setBitmapMemoryCacheParamsSupplier(bitmapMemoryCacheParamsSupplier)
.build()
Fresco.initialize(context, config)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ package com.jimmydaddy.imagemarker.base
import com.facebook.react.bridge.ReadableMap

data class WatermarkImageOptions(val options: ReadableMap?) {
var imageOption: ImageOptions
var x: String?
var y: String?
var positionEnum: PositionEnum?
lateinit var imageOption: ImageOptions
var x: String? = null
var y: String? = null
var positionEnum: PositionEnum? = null

init {
imageOption = options?.let { ImageOptions(it) }!!
val positionOptions =
if (null != options.getMap("position")) options.getMap("position") else null
x = if (positionOptions!!.hasKey("X")) Utils.handleDynamicToString(positionOptions.getDynamic("X")) else null
y = if (positionOptions.hasKey("Y")) Utils.handleDynamicToString(positionOptions.getDynamic("Y")) else null
positionEnum =
if (null != positionOptions.getString("position")) PositionEnum.getPosition(
positionOptions.getString("position")
) else null
if (options != null) {
imageOption = ImageOptions(options)
val positionOptions =
if (null != options.getMap("position")) options.getMap("position") else null
x =
if (positionOptions!!.hasKey("X")) Utils.handleDynamicToString(positionOptions.getDynamic("X")) else null
y =
if (positionOptions.hasKey("Y")) Utils.handleDynamicToString(positionOptions.getDynamic("Y")) else null
positionEnum =
if (null != positionOptions.getString("position")) PositionEnum.getPosition(
positionOptions.getString("position")
) else null
}
}

constructor(watermarkImage: ImageOptions, x: String?, y: String?, position: PositionEnum?) : this(null) {
Expand Down
4 changes: 4 additions & 0 deletions example/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: '@react-native',
};
6 changes: 3 additions & 3 deletions example/Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
source 'https://rubygems.org'

# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby '>= 2.6.10'

gem 'cocoapods', '>= 1.11.3'
ruby ">= 2.6.10"
gem 'cocoapods', '~> 1.13'
gem 'activesupport', '>= 6.1.7.3', '< 7.1.0'
Loading

0 comments on commit 032aac0

Please sign in to comment.