Skip to content

Commit

Permalink
feat(Android): add support for non-uniform border radii
Browse files Browse the repository at this point in the history
Introduced the ability to set non-uniform border radii for Android images.
This feature allows developers to specify different border radius values for each corner of a image, enhancing design flexibility.
  • Loading branch information
Florian Vogt committed May 25, 2024
1 parent 0a3c4c7 commit 684e7a0
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 8 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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',
},
Expand Down
8 changes: 8 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand All @@ -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';
Expand Down

0 comments on commit 684e7a0

Please sign in to comment.