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.bridge.ReadableMap 8 import com.facebook.react.uimanager.PixelUtil 9 import com.facebook.react.uimanager.Spacing 10 import com.facebook.react.uimanager.ViewProps 11 import com.facebook.yoga.YogaConstants 12 import expo.modules.core.errors.ModuleDestroyedException 13 import expo.modules.image.enums.ImageResizeMode 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: ReadableMap? -> 67 view.sourceMap = sourceMap 68 } 69 70 Prop("resizeMode") { view: ExpoImageViewWrapper, stringValue: String -> 71 val resizeMode = ImageResizeMode.fromStringValue(stringValue) 72 view.resizeMode = resizeMode 73 } 74 75 Prop("blurRadius") { view: ExpoImageViewWrapper, blurRadius: Int -> 76 view.blurRadius = blurRadius 77 } 78 79 Prop("fadeDuration") { view: ExpoImageViewWrapper, fadeDuration: Int -> 80 view.fadeDuration = fadeDuration 81 } 82 83 PropGroup( 84 ViewProps.BORDER_RADIUS to 0, 85 ViewProps.BORDER_TOP_LEFT_RADIUS to 1, 86 ViewProps.BORDER_TOP_RIGHT_RADIUS to 2, 87 ViewProps.BORDER_BOTTOM_RIGHT_RADIUS to 3, 88 ViewProps.BORDER_BOTTOM_LEFT_RADIUS to 4, 89 ViewProps.BORDER_TOP_START_RADIUS to 5, 90 ViewProps.BORDER_TOP_END_RADIUS to 6, 91 ViewProps.BORDER_BOTTOM_START_RADIUS to 7, 92 ViewProps.BORDER_BOTTOM_END_RADIUS to 8 93 ) { view: ExpoImageViewWrapper, index: Int, borderRadius: Float? -> 94 val radius = makeYogaUndefinedIfNegative(borderRadius ?: YogaConstants.UNDEFINED) 95 view.setBorderRadius(index, radius) 96 } 97 98 PropGroup( 99 ViewProps.BORDER_WIDTH to Spacing.ALL, 100 ViewProps.BORDER_LEFT_WIDTH to Spacing.LEFT, 101 ViewProps.BORDER_RIGHT_WIDTH to Spacing.RIGHT, 102 ViewProps.BORDER_TOP_WIDTH to Spacing.TOP, 103 ViewProps.BORDER_BOTTOM_WIDTH to Spacing.BOTTOM, 104 ViewProps.BORDER_START_WIDTH to Spacing.START, 105 ViewProps.BORDER_END_WIDTH to Spacing.END 106 ) { view: ExpoImageViewWrapper, index: Int, width: Float? -> 107 val pixelWidth = makeYogaUndefinedIfNegative(width ?: YogaConstants.UNDEFINED) 108 .ifYogaDefinedUse(PixelUtil::toPixelFromDIP) 109 view.setBorderWidth(index, pixelWidth) 110 } 111 112 PropGroup( 113 ViewProps.BORDER_COLOR to Spacing.ALL, 114 ViewProps.BORDER_LEFT_COLOR to Spacing.LEFT, 115 ViewProps.BORDER_RIGHT_COLOR to Spacing.RIGHT, 116 ViewProps.BORDER_TOP_COLOR to Spacing.TOP, 117 ViewProps.BORDER_BOTTOM_COLOR to Spacing.BOTTOM, 118 ViewProps.BORDER_START_COLOR to Spacing.START, 119 ViewProps.BORDER_END_COLOR to Spacing.END 120 ) { view: ExpoImageViewWrapper, index: Int, color: Int? -> 121 val rgbComponent = if (color == null) YogaConstants.UNDEFINED else (color and 0x00FFFFFF).toFloat() 122 val alphaComponent = if (color == null) YogaConstants.UNDEFINED else (color ushr 24).toFloat() 123 view.setBorderColor(index, rgbComponent, alphaComponent) 124 } 125 126 Prop("borderStyle") { view: ExpoImageViewWrapper, borderStyle: String? -> 127 view.setBorderStyle(borderStyle) 128 } 129 130 Prop("tintColor") { view: ExpoImageViewWrapper, color: Int? -> 131 view.setTintColor(color) 132 } 133 134 Prop("defaultSource") { view: ExpoImageViewWrapper, defaultSource: ReadableMap? -> 135 view.defaultSourceMap = defaultSource 136 } 137 138 Prop("accessible") { view: ExpoImageViewWrapper, accessible: Boolean -> 139 view.isFocusable = accessible 140 } 141 142 OnViewDidUpdateProps { view: ExpoImageViewWrapper -> 143 view.onAfterUpdateTransaction() 144 } 145 146 OnViewDestroys { view: ExpoImageViewWrapper -> 147 view.onDrop() 148 } 149 } 150 } 151 } 152 153 // TODO(@lukmccall): Remove when the same functionality will be defined by the expo-modules-core in SDK 48 154 private inline fun <reified T : View, reified PropType, reified CustomValueType> ViewDefinitionBuilder<T>.PropGroup( 155 vararg props: Pair<String, CustomValueType>, 156 noinline body: (view: T, value: CustomValueType, prop: PropType) -> Unit 157 ) { 158 for ((name, value) in props) { 159 Prop<T, PropType>(name) { view, prop -> body(view, value, prop) } 160 } 161 } 162