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("placeholderContentFit") { view: ExpoImageViewWrapper, placeholderContentFit: ContentFit? ->
67         view.placeholderContentFit = placeholderContentFit ?: ContentFit.ScaleDown
68       }
69 
70       Prop("contentPosition") { view: ExpoImageViewWrapper, contentPosition: ContentPosition? ->
71         view.contentPosition = contentPosition ?: ContentPosition.center
72       }
73 
74       Prop("blurRadius") { view: ExpoImageViewWrapper, blurRadius: Int? ->
75         view.blurRadius = blurRadius?.takeIf { it > 0 }
76       }
77 
78       Prop("transition") { view: ExpoImageViewWrapper, transition: ImageTransition? ->
79         view.transition = transition
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.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.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.setBorderColor(index, rgbComponent, alphaComponent)
123       }
124 
125       Prop("borderStyle") { view: ExpoImageViewWrapper, borderStyle: String? ->
126         view.borderStyle = borderStyle
127       }
128 
129       Prop("backgroundColor") { view: ExpoImageViewWrapper, color: Int? ->
130         view.backgroundColor = color
131       }
132 
133       Prop("tintColor") { view: ExpoImageViewWrapper, color: Int? ->
134         view.tintColor = color
135       }
136 
137       Prop("placeholder") { view: ExpoImageViewWrapper, placeholder: List<SourceMap>? ->
138         view.placeholders = placeholder ?: emptyList()
139       }
140 
141       Prop("accessible") { view: ExpoImageViewWrapper, accessible: Boolean? ->
142         view.accessible = accessible ?: false
143       }
144 
145       Prop("accessibilityLabel") { view: ExpoImageViewWrapper, accessibilityLabel: String? ->
146         view.accessibilityLabel = accessibilityLabel
147       }
148 
149       Prop("focusable") { view: ExpoImageViewWrapper, isFocusable: Boolean? ->
150         view.isFocusableProp = isFocusable ?: false
151       }
152 
153       Prop("priority") { view: ExpoImageViewWrapper, priority: Priority? ->
154         view.priority = priority ?: Priority.NORMAL
155       }
156 
157       Prop("cachePolicy") { view: ExpoImageViewWrapper, cachePolicy: CachePolicy? ->
158         view.cachePolicy = cachePolicy ?: CachePolicy.DISK
159       }
160 
161       OnViewDidUpdateProps { view: ExpoImageViewWrapper ->
162         view.rerenderIfNeeded()
163       }
164 
165       OnViewDestroys { view: ExpoImageViewWrapper ->
166         view.onViewDestroys()
167       }
168     }
169   }
170 }
171 
172 // TODO(@lukmccall): Remove when the same functionality will be defined by the expo-modules-core in SDK 48
173 @Suppress("FunctionName")
174 private inline fun <reified T : View, reified PropType, reified CustomValueType> ViewDefinitionBuilder<T>.PropGroup(
175   vararg props: Pair<String, CustomValueType>,
176   noinline body: (view: T, value: CustomValueType, prop: PropType) -> Unit
177 ) {
178   for ((name, value) in props) {
179     Prop<T, PropType>(name) { view, prop -> body(view, value, prop) }
180   }
181 }
182