1 package expo.modules.image
2 
3 import android.view.View
4 import com.bumptech.glide.Glide
5 import com.bumptech.glide.load.model.GlideUrl
6 import com.facebook.react.uimanager.PixelUtil
7 import com.facebook.react.uimanager.Spacing
8 import com.facebook.react.uimanager.ViewProps
9 import com.facebook.yoga.YogaConstants
10 import expo.modules.image.enums.ContentFit
11 import expo.modules.image.enums.Priority
12 import expo.modules.image.records.CachePolicy
13 import expo.modules.image.records.ContentPosition
14 import expo.modules.image.records.SourceMap
15 import expo.modules.kotlin.functions.Queues
16 import expo.modules.kotlin.modules.Module
17 import expo.modules.kotlin.modules.ModuleDefinition
18 import expo.modules.kotlin.views.ViewDefinitionBuilder
19 
20 class ExpoImageModule : Module() {
21   override fun definition() = ModuleDefinition {
22     Name("ExpoImage")
23 
24     Function("prefetch") { urls: List<String> ->
25       val context = appContext.reactContext ?: return@Function
26       urls.forEach {
27         Glide
28           .with(context)
29           .download(GlideUrl(it))
30           .submit()
31       }
32     }
33 
34     AsyncFunction("clearMemoryCache") {
35       val activity = appContext.currentActivity ?: return@AsyncFunction false
36       Glide.get(activity).clearMemory()
37       return@AsyncFunction true
38     }.runOnQueue(Queues.MAIN)
39 
40     AsyncFunction("clearDiskCache") {
41       val activity = appContext.currentActivity ?: return@AsyncFunction false
42       activity.let {
43         Glide.get(activity).clearDiskCache()
44       }
45 
46       return@AsyncFunction true
47     }
48 
49     View(ExpoImageViewWrapper::class) {
50       Events(
51         "onLoadStart",
52         "onProgress",
53         "onError",
54         "onLoad"
55       )
56 
57       Prop("source") { view: ExpoImageViewWrapper, sources: List<SourceMap>? ->
58         view.imageView.sources = sources ?: emptyList()
59       }
60 
61       Prop("contentFit") { view: ExpoImageViewWrapper, contentFit: ContentFit? ->
62         view.imageView.contentFit = contentFit ?: ContentFit.Cover
63       }
64 
65       Prop("contentPosition") { view: ExpoImageViewWrapper, contentPosition: ContentPosition? ->
66         view.imageView.contentPosition = contentPosition ?: ContentPosition.center
67       }
68 
69       Prop("blurRadius") { view: ExpoImageViewWrapper, blurRadius: Int ->
70         view.imageView.blurRadius = blurRadius
71       }
72 
73       Prop("fadeDuration") { view: ExpoImageViewWrapper, fadeDuration: Int ->
74         view.imageView.fadeDuration = fadeDuration
75       }
76 
77       PropGroup(
78         ViewProps.BORDER_RADIUS to 0,
79         ViewProps.BORDER_TOP_LEFT_RADIUS to 1,
80         ViewProps.BORDER_TOP_RIGHT_RADIUS to 2,
81         ViewProps.BORDER_BOTTOM_RIGHT_RADIUS to 3,
82         ViewProps.BORDER_BOTTOM_LEFT_RADIUS to 4,
83         ViewProps.BORDER_TOP_START_RADIUS to 5,
84         ViewProps.BORDER_TOP_END_RADIUS to 6,
85         ViewProps.BORDER_BOTTOM_START_RADIUS to 7,
86         ViewProps.BORDER_BOTTOM_END_RADIUS to 8
87       ) { view: ExpoImageViewWrapper, index: Int, borderRadius: Float? ->
88         val radius = makeYogaUndefinedIfNegative(borderRadius ?: YogaConstants.UNDEFINED)
89         view.imageView.setBorderRadius(index, radius)
90       }
91 
92       PropGroup(
93         ViewProps.BORDER_WIDTH to Spacing.ALL,
94         ViewProps.BORDER_LEFT_WIDTH to Spacing.LEFT,
95         ViewProps.BORDER_RIGHT_WIDTH to Spacing.RIGHT,
96         ViewProps.BORDER_TOP_WIDTH to Spacing.TOP,
97         ViewProps.BORDER_BOTTOM_WIDTH to Spacing.BOTTOM,
98         ViewProps.BORDER_START_WIDTH to Spacing.START,
99         ViewProps.BORDER_END_WIDTH to Spacing.END
100       ) { view: ExpoImageViewWrapper, index: Int, width: Float? ->
101         val pixelWidth = makeYogaUndefinedIfNegative(width ?: YogaConstants.UNDEFINED)
102           .ifYogaDefinedUse(PixelUtil::toPixelFromDIP)
103         view.imageView.setBorderWidth(index, pixelWidth)
104       }
105 
106       PropGroup(
107         ViewProps.BORDER_COLOR to Spacing.ALL,
108         ViewProps.BORDER_LEFT_COLOR to Spacing.LEFT,
109         ViewProps.BORDER_RIGHT_COLOR to Spacing.RIGHT,
110         ViewProps.BORDER_TOP_COLOR to Spacing.TOP,
111         ViewProps.BORDER_BOTTOM_COLOR to Spacing.BOTTOM,
112         ViewProps.BORDER_START_COLOR to Spacing.START,
113         ViewProps.BORDER_END_COLOR to Spacing.END
114       ) { view: ExpoImageViewWrapper, index: Int, color: Int? ->
115         val rgbComponent = if (color == null) YogaConstants.UNDEFINED else (color and 0x00FFFFFF).toFloat()
116         val alphaComponent = if (color == null) YogaConstants.UNDEFINED else (color ushr 24).toFloat()
117         view.imageView.setBorderColor(index, rgbComponent, alphaComponent)
118       }
119 
120       Prop("borderStyle") { view: ExpoImageViewWrapper, borderStyle: String? ->
121         view.imageView.setBorderStyle(borderStyle)
122       }
123 
124       Prop("backgroundColor") { view: ExpoImageViewWrapper, color: Int? ->
125         view.imageView.setBackgroundColor(color)
126       }
127 
128       Prop("tintColor") { view: ExpoImageViewWrapper, color: Int? ->
129         view.imageView.setTintColor(color)
130       }
131 
132       Prop("placeholder") { view: ExpoImageViewWrapper, placeholder: List<SourceMap>? ->
133         view.imageView.placeholders = placeholder ?: emptyList()
134       }
135 
136       Prop("accessible") { view: ExpoImageViewWrapper, accessible: Boolean ->
137         view.imageView.isFocusable = accessible
138       }
139 
140       Prop("priority") { view: ExpoImageViewWrapper, priority: Priority? ->
141         view.imageView.priority = priority ?: Priority.NORMAL
142       }
143 
144       Prop("cachePolicy") { view: ExpoImageViewWrapper, cachePolicy: CachePolicy? ->
145         view.imageView.cachePolicy = cachePolicy ?: CachePolicy.DISK
146       }
147 
148       OnViewDidUpdateProps { view: ExpoImageViewWrapper ->
149         view.imageView.onAfterUpdateTransaction()
150       }
151 
152       OnViewDestroys { view: ExpoImageViewWrapper ->
153         view.imageView.onDrop()
154       }
155     }
156   }
157 }
158 
159 // TODO(@lukmccall): Remove when the same functionality will be defined by the expo-modules-core in SDK 48
160 @Suppress("FunctionName")
161 private inline fun <reified T : View, reified PropType, reified CustomValueType> ViewDefinitionBuilder<T>.PropGroup(
162   vararg props: Pair<String, CustomValueType>,
163   noinline body: (view: T, value: CustomValueType, prop: PropType) -> Unit
164 ) {
165   for ((name, value) in props) {
166     Prop<T, PropType>(name) { view, prop -> body(view, value, prop) }
167   }
168 }
169