1 package expo.modules.image 2 3 import android.view.View 4 import androidx.core.view.doOnDetach 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.image.enums.ContentFit 12 import expo.modules.image.enums.Priority 13 import expo.modules.image.records.CachePolicy 14 import expo.modules.image.records.ContentPosition 15 import expo.modules.image.records.ImageTransition 16 import expo.modules.image.records.SourceMap 17 import expo.modules.kotlin.functions.Queues 18 import expo.modules.kotlin.modules.Module 19 import expo.modules.kotlin.modules.ModuleDefinition 20 import expo.modules.kotlin.views.ViewDefinitionBuilder 21 22 class ExpoImageModule : Module() { 23 override fun definition() = ModuleDefinition { 24 Name("ExpoImage") 25 26 Function("prefetch") { urls: List<String> -> 27 val context = appContext.reactContext ?: return@Function 28 urls.forEach { 29 Glide 30 .with(context) 31 .download(GlideUrl(it)) 32 .submit() 33 } 34 } 35 36 AsyncFunction("clearMemoryCache") { 37 val activity = appContext.currentActivity ?: return@AsyncFunction false 38 Glide.get(activity).clearMemory() 39 return@AsyncFunction true 40 }.runOnQueue(Queues.MAIN) 41 42 AsyncFunction("clearDiskCache") { 43 val activity = appContext.currentActivity ?: return@AsyncFunction false 44 activity.let { 45 Glide.get(activity).clearDiskCache() 46 } 47 48 return@AsyncFunction true 49 } 50 51 View(ExpoImageViewWrapper::class) { 52 Events( 53 "onLoadStart", 54 "onProgress", 55 "onError", 56 "onLoad" 57 ) 58 59 Prop("source") { view: ExpoImageViewWrapper, sources: List<SourceMap>? -> 60 view.sources = sources ?: emptyList() 61 } 62 63 Prop("contentFit") { view: ExpoImageViewWrapper, contentFit: ContentFit? -> 64 view.contentFit = contentFit ?: ContentFit.Cover 65 } 66 67 Prop("placeholderContentFit") { view: ExpoImageViewWrapper, placeholderContentFit: ContentFit? -> 68 view.placeholderContentFit = placeholderContentFit ?: ContentFit.ScaleDown 69 } 70 71 Prop("contentPosition") { view: ExpoImageViewWrapper, contentPosition: ContentPosition? -> 72 view.contentPosition = contentPosition ?: ContentPosition.center 73 } 74 75 Prop("blurRadius") { view: ExpoImageViewWrapper, blurRadius: Int? -> 76 view.blurRadius = blurRadius?.takeIf { it > 0 } 77 } 78 79 Prop("transition") { view: ExpoImageViewWrapper, transition: ImageTransition? -> 80 view.transition = transition 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.borderStyle = borderStyle 128 } 129 130 Prop("backgroundColor") { view: ExpoImageViewWrapper, color: Int? -> 131 view.backgroundColor = color 132 } 133 134 Prop("tintColor") { view: ExpoImageViewWrapper, color: Int? -> 135 view.tintColor = color 136 } 137 138 Prop("placeholder") { view: ExpoImageViewWrapper, placeholder: List<SourceMap>? -> 139 view.placeholders = placeholder ?: emptyList() 140 } 141 142 Prop("accessible") { view: ExpoImageViewWrapper, accessible: Boolean? -> 143 view.accessible = accessible ?: false 144 } 145 146 Prop("accessibilityLabel") { view: ExpoImageViewWrapper, accessibilityLabel: String? -> 147 view.accessibilityLabel = accessibilityLabel 148 } 149 150 Prop("focusable") { view: ExpoImageViewWrapper, isFocusable: Boolean? -> 151 view.isFocusableProp = isFocusable ?: false 152 } 153 154 Prop("priority") { view: ExpoImageViewWrapper, priority: Priority? -> 155 view.priority = priority ?: Priority.NORMAL 156 } 157 158 Prop("cachePolicy") { view: ExpoImageViewWrapper, cachePolicy: CachePolicy? -> 159 view.cachePolicy = cachePolicy ?: CachePolicy.DISK 160 } 161 162 Prop("recyclingKey") { view: ExpoImageViewWrapper, recyclingKey: String? -> 163 view.recyclingKey = recyclingKey 164 } 165 166 Prop("allowDownscaling") { view: ExpoImageViewWrapper, allowDownscaling: Boolean? -> 167 view.allowDownscaling = allowDownscaling ?: true 168 } 169 170 OnViewDidUpdateProps { view: ExpoImageViewWrapper -> 171 view.rerenderIfNeeded() 172 } 173 174 OnViewDestroys { view: ExpoImageViewWrapper -> 175 view.doOnDetach { 176 view.onViewDestroys() 177 } 178 } 179 } 180 } 181 } 182 183 // TODO(@lukmccall): Remove when the same functionality will be defined by the expo-modules-core in SDK 48 184 @Suppress("FunctionName") 185 private inline fun <reified T : View, reified PropType, reified CustomValueType> ViewDefinitionBuilder<T>.PropGroup( 186 vararg props: Pair<String, CustomValueType>, 187 noinline body: (view: T, value: CustomValueType, prop: PropType) -> Unit 188 ) { 189 for ((name, value) in props) { 190 Prop<T, PropType>(name) { view, prop -> body(view, value, prop) } 191 } 192 } 193