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