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