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