diff --git a/README.md b/README.md index 864bd8e..bc3ac4f 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ await clearCache(); | cachePolicy | string | memory | The cache policy of the image | | transitionDuration | number | 0.75 (iOS) Android (100) | The transition duration of the image | | borderRadius | number | 0 | border radius of image | +| borderTopLeftRadius | number | 0 | top left border radius of image (Android only) | +| borderTopRightRadius | number | 0 | top right border radius of image (Android only) | +| borderBottomLeftRadius | number | 0 | bottom left border radius of image (Android only) | +| borderBottomRightRadius | number | 0 | bottom right border radius of image (Android only) | | failureImage | string | | If the image fails to download this will be set (blurhash, thumbhash, base64) | | progressiveLoadingEnabled | boolean | false | Progressively load images (iOS only) | | onError | function | | The function to call when an error occurs. The error is passed as the first argument of the function | diff --git a/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt b/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt index 9f45882..5b2fa1a 100644 --- a/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt +++ b/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt @@ -5,6 +5,9 @@ import android.graphics.BitmapFactory import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.Outline +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Path import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.Base64 @@ -29,6 +32,18 @@ import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.events.RCTEventEmitter + data class BorderRadii( + val uniform: Double, + val topLeft: Double, + val topRight: Double, + val bottomLeft: Double, + val bottomRight: Double, + ) { + fun sum(): Double { + return uniform + topLeft + topRight + bottomLeft + bottomRight; + } + } + @Suppress("unused") class FasterImageModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName(): String = "FasterImageModule" @@ -72,15 +87,22 @@ import com.facebook.react.uimanager.events.RCTEventEmitter val thumbHash = options.getString("thumbhash") val resizeMode = options.getString("resizeMode") val transitionDuration = if (options.hasKey("transitionDuration")) options.getInt("transitionDuration") else 100 - val borderRadius = if (options.hasKey("borderRadius")) options.getDouble("borderRadius") else 0.0 val cachePolicy = options.getString("cachePolicy") val failureImage = options.getString("failureImage") val grayscale = if (options.hasKey("grayscale")) options.getDouble("grayscale") else 0.0 val allowHardware = if (options.hasKey("allowHardware")) options.getBoolean("allowHardware") else true val headers = options.getMap("headers") - if (borderRadius != 0.0) { - setViewBorderRadius(view, borderRadius.toInt()) + val borderRadii = BorderRadii( + uniform = if (options.hasKey("borderRadius")) options.getDouble("borderRadius") else 0.0, + topLeft = if (options.hasKey("borderTopLeftRadius")) options.getDouble("borderTopLeftRadius") else 0.0, + topRight = if (options.hasKey("borderTopRightRadius")) options.getDouble("borderTopRightRadius") else 0.0, + bottomLeft = if (options.hasKey("borderBottomLeftRadius")) options.getDouble("borderBottomLeftRadius") else 0.0, + bottomRight = if (options.hasKey("borderBottomRightRadius")) options.getDouble("borderBottomRightRadius") else 0.0, + ) + + if (borderRadii.sum() != 0.0) { + setViewBorderRadius(view, borderRadii) } if (RESIZE_MODE.containsKey(resizeMode)) { @@ -160,11 +182,37 @@ import com.facebook.react.uimanager.events.RCTEventEmitter imageLoader.enqueue(request) } - private fun setViewBorderRadius(view: AppCompatImageView, borderRadius: Int) { + private fun setViewBorderRadius(view: AppCompatImageView, borderRadii: BorderRadii) { view.clipToOutline = true view.outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { - outline.setRoundRect(0, 0, view.width, view.height, borderRadius.toFloat()) + val width = view.width + val height = view.height + val nonUniformRadiiSum = borderRadii.sum() - borderRadii.uniform + + if (nonUniformRadiiSum == 0.0 || nonUniformRadiiSum == borderRadii.uniform) { + outline.setRoundRect(0, 0, width, height, borderRadii.uniform.toFloat()) + return + } + + val radii = floatArrayOf( + borderRadii.topLeft.toFloat(), borderRadii.topLeft.toFloat(), + borderRadii.topRight.toFloat(), borderRadii.topRight.toFloat(), + borderRadii.bottomRight.toFloat(), borderRadii.bottomRight.toFloat(), + borderRadii.bottomLeft.toFloat(), borderRadii.bottomLeft.toFloat(), + ) + + val rect = Rect(0, 0, width, height) + + val path = Path().apply { + addRoundRect( + RectF(rect), + radii, + Path.Direction.CW + ) + } + + outline.setPath(path) } } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 4094c89..dcb64a7 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -62,8 +62,10 @@ export default function App() { }} source={{ transitionDuration: 0.3, - borderRadius: - Platform.OS === 'android' ? size * 2 : (size - 16) / 2, + borderTopLeftRadius: + Platform.OS === 'android' ? size : (size - 16) / 2, + borderBottomRightRadius: + Platform.OS === 'android' ? size : (size - 16) / 2, cachePolicy: 'discWithCacheControl', showActivityIndicator: true, base64Placeholder: @@ -83,7 +85,8 @@ const styles = StyleSheet.create({ image: { width: size - 16, height: size - 16, - borderRadius: (size - 16) / 2, + borderTopLeftRadius: (size - 16) / 2, + borderBottomRightRadius: (size - 16) / 2, overflow: 'hidden', backgroundColor: 'white', }, diff --git a/src/index.tsx b/src/index.tsx index d152edc..48de6c2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -39,6 +39,10 @@ export type AndroidImageResizeMode = * @property {boolean} [progressiveLoadingEnabled] - Enable progressive loading, defaults to false * @property {('memory' | 'discWithCacheControl' | 'discNoCacheControl')} [cachePolicy] - Cache [policy](https://kean-docs.github.io/nuke/documentation/nuke/imagepipeline), defaults to 'memory'. 'discWithCacheControl' will cache the image in the disc and use the cache control headers to determine if the image should be re-fetched. 'discNoCacheControl' will cache the image in the disc and never re-fetch it. * @property {number} [borderRadius] - Border radius of the image + * @property {number} [borderTopLeftRadius] - Top left border radius of the image + * @property {number} [borderTopRightRadius] - Top right border radius of the image + * @property {number} [borderBottomLeftRadius] - Bottom left border radius of the image + * @property {number} [borderBottomRightRadius] - Bottom right border radius of the image * @property {number} [grayscale] - Grayscale value of the image, 0-1 * @property {boolean} [allowHardware] - Allow hardware rendering, defaults to true (Android only) */ @@ -49,6 +53,10 @@ export type ImageOptions = { thumbhash?: string; resizeMode?: IOSImageResizeMode | AndroidImageResizeMode; borderRadius?: number; + borderTopLeftRadius?: number; + borderTopRightRadius?: number; + borderBottomLeftRadius?: number; + borderBottomRightRadius?: number; showActivityIndicator?: boolean; transitionDuration?: number; cachePolicy?: 'memory' | 'discWithCacheControl' | 'discNoCacheControl';