1 package expo.modules.image 2 3 import android.util.Log 4 import android.view.View 5 import com.bumptech.glide.Glide 6 import com.bumptech.glide.load.model.GlideUrl 7 import com.facebook.react.uimanager.PixelUtil 8 import com.facebook.react.uimanager.Spacing 9 import com.facebook.react.uimanager.ViewProps 10 import com.facebook.yoga.YogaConstants 11 import expo.modules.core.errors.ModuleDestroyedException 12 import expo.modules.image.enums.ImageResizeMode 13 import expo.modules.image.records.SourceMap 14 import expo.modules.kotlin.Promise 15 import expo.modules.kotlin.exception.Exceptions 16 import expo.modules.kotlin.modules.Module 17 import expo.modules.kotlin.modules.ModuleDefinition 18 import expo.modules.kotlin.views.ViewDefinitionBuilder 19 import kotlinx.coroutines.CoroutineScope 20 import kotlinx.coroutines.Dispatchers 21 import kotlinx.coroutines.cancel 22 import kotlinx.coroutines.launch 23 24 class ExpoImageModule : Module() { 25 private val moduleCoroutineScope = CoroutineScope(Dispatchers.IO) 26 27 override fun definition() = ModuleDefinition { 28 Name("ExpoImage") 29 30 AsyncFunction("prefetch") { url: String, promise: Promise -> 31 val context = appContext.reactContext ?: throw Exceptions.ReactContextLost() 32 moduleCoroutineScope.launch { 33 try { 34 val glideUrl = GlideUrl(url) 35 val result = Glide.with(context) 36 .download(glideUrl) 37 .submit() 38 .awaitGet() 39 if (result != null) { 40 promise.resolve(null) 41 } else { 42 promise.reject(ImagePrefetchFailure("cannot download $url")) 43 } 44 } catch (e: Exception) { 45 promise.reject(ImagePrefetchFailure(e.message ?: e.toString())) 46 } 47 } 48 } 49 50 OnDestroy { 51 try { 52 moduleCoroutineScope.cancel(ModuleDestroyedException()) 53 } catch (e: IllegalStateException) { 54 Log.w("ExpoImageModule", "No coroutines to cancel") 55 } 56 } 57 58 View(ExpoImageViewWrapper::class) { 59 Events( 60 "onLoadStart", 61 "onProgress", 62 "onError", 63 "onLoad" 64 ) 65 66 Prop("source") { view: ExpoImageViewWrapper, sourceMap: SourceMap? -> 67 view.imageView.sourceMap = sourceMap 68 } 69 70 Prop("resizeMode") { view: ExpoImageViewWrapper, resizeMode: ImageResizeMode -> 71 view.imageView.resizeMode = resizeMode 72 } 73 74 Prop("blurRadius") { view: ExpoImageViewWrapper, blurRadius: Int -> 75 view.imageView.blurRadius = blurRadius 76 } 77 78 Prop("fadeDuration") { view: ExpoImageViewWrapper, fadeDuration: Int -> 79 view.imageView.fadeDuration = fadeDuration 80 } 81 82 PropGroup( 83 ViewProps.BORDER_RADIUS to 0, 84 ViewProps.BORDER_TOP_LEFT_RADIUS to 1, 85 ViewProps.BORDER_TOP_RIGHT_RADIUS to 2, 86 ViewProps.BORDER_BOTTOM_RIGHT_RADIUS to 3, 87 ViewProps.BORDER_BOTTOM_LEFT_RADIUS to 4, 88 ViewProps.BORDER_TOP_START_RADIUS to 5, 89 ViewProps.BORDER_TOP_END_RADIUS to 6, 90 ViewProps.BORDER_BOTTOM_START_RADIUS to 7, 91 ViewProps.BORDER_BOTTOM_END_RADIUS to 8 92 ) { view: ExpoImageViewWrapper, index: Int, borderRadius: Float? -> 93 val radius = makeYogaUndefinedIfNegative(borderRadius ?: YogaConstants.UNDEFINED) 94 view.imageView.setBorderRadius(index, radius) 95 } 96 97 PropGroup( 98 ViewProps.BORDER_WIDTH to Spacing.ALL, 99 ViewProps.BORDER_LEFT_WIDTH to Spacing.LEFT, 100 ViewProps.BORDER_RIGHT_WIDTH to Spacing.RIGHT, 101 ViewProps.BORDER_TOP_WIDTH to Spacing.TOP, 102 ViewProps.BORDER_BOTTOM_WIDTH to Spacing.BOTTOM, 103 ViewProps.BORDER_START_WIDTH to Spacing.START, 104 ViewProps.BORDER_END_WIDTH to Spacing.END 105 ) { view: ExpoImageViewWrapper, index: Int, width: Float? -> 106 val pixelWidth = makeYogaUndefinedIfNegative(width ?: YogaConstants.UNDEFINED) 107 .ifYogaDefinedUse(PixelUtil::toPixelFromDIP) 108 view.imageView.setBorderWidth(index, pixelWidth) 109 } 110 111 PropGroup( 112 ViewProps.BORDER_COLOR to Spacing.ALL, 113 ViewProps.BORDER_LEFT_COLOR to Spacing.LEFT, 114 ViewProps.BORDER_RIGHT_COLOR to Spacing.RIGHT, 115 ViewProps.BORDER_TOP_COLOR to Spacing.TOP, 116 ViewProps.BORDER_BOTTOM_COLOR to Spacing.BOTTOM, 117 ViewProps.BORDER_START_COLOR to Spacing.START, 118 ViewProps.BORDER_END_COLOR to Spacing.END 119 ) { view: ExpoImageViewWrapper, index: Int, color: Int? -> 120 val rgbComponent = if (color == null) YogaConstants.UNDEFINED else (color and 0x00FFFFFF).toFloat() 121 val alphaComponent = if (color == null) YogaConstants.UNDEFINED else (color ushr 24).toFloat() 122 view.imageView.setBorderColor(index, rgbComponent, alphaComponent) 123 } 124 125 Prop("borderStyle") { view: ExpoImageViewWrapper, borderStyle: String? -> 126 view.imageView.setBorderStyle(borderStyle) 127 } 128 129 Prop("tintColor") { view: ExpoImageViewWrapper, color: Int? -> 130 view.imageView.setTintColor(color) 131 } 132 133 Prop("defaultSource") { view: ExpoImageViewWrapper, defaultSource: SourceMap? -> 134 view.imageView.defaultSourceMap = defaultSource 135 } 136 137 Prop("accessible") { view: ExpoImageViewWrapper, accessible: Boolean -> 138 view.imageView.isFocusable = accessible 139 } 140 141 OnViewDidUpdateProps { view: ExpoImageViewWrapper -> 142 view.imageView.onAfterUpdateTransaction() 143 } 144 145 OnViewDestroys { view: ExpoImageViewWrapper -> 146 view.imageView.onDrop() 147 } 148 } 149 } 150 } 151 152 // TODO(@lukmccall): Remove when the same functionality will be defined by the expo-modules-core in SDK 48 153 private inline fun <reified T : View, reified PropType, reified CustomValueType> ViewDefinitionBuilder<T>.PropGroup( 154 vararg props: Pair<String, CustomValueType>, 155 noinline body: (view: T, value: CustomValueType, prop: PropType) -> Unit 156 ) { 157 for ((name, value) in props) { 158 Prop<T, PropType>(name) { view, prop -> body(view, value, prop) } 159 } 160 } 161