diff --git a/android/src/main/java/com/fastsquircle/FastSquircleView.java b/android/src/main/java/com/fastsquircle/FastSquircleView.java index 1e8363d..c5029fb 100644 --- a/android/src/main/java/com/fastsquircle/FastSquircleView.java +++ b/android/src/main/java/com/fastsquircle/FastSquircleView.java @@ -12,6 +12,7 @@ import com.facebook.react.common.annotations.UnstableReactNativeAPI; import com.facebook.react.uimanager.drawable.BackgroundDrawable; +import com.facebook.react.uimanager.drawable.BackgroundImageDrawable; import com.facebook.react.uimanager.drawable.BorderDrawable; import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable; import com.facebook.react.uimanager.drawable.OutlineDrawable; @@ -19,6 +20,7 @@ import com.facebook.react.uimanager.style.Overflow; import com.facebook.react.views.view.ReactViewGroup; import com.fastsquircle.drawables.SquircleBackgroundDrawable; +import com.fastsquircle.drawables.SquircleBackgroundImageDrawable; import com.fastsquircle.drawables.SquircleBorderDrawable; import com.fastsquircle.drawables.SquircleOutlineDrawable; import com.fastsquircle.drawables.SquircleOutsetShadowDrawable; @@ -31,6 +33,7 @@ public class FastSquircleView extends ReactViewGroup { private float cornerSmoothing = 0.0f; private SquircleBackgroundDrawable squircleBackgroundDrawable; + private SquircleBackgroundImageDrawable squircleBackgroundImageDrawable; private SquircleBorderDrawable squircleBorderDrawable; private SquircleOutlineDrawable squircleOutlineDrawable; @@ -95,6 +98,7 @@ public void draw(@NonNull Canvas canvas) { } int backgroundDrawableIndex = -1; + int backgroundImageDrawableIndex = -1; int borderDrawableIndex = -1; int outlineDrawableIndex = -1; for (int i = 0; i < layerDrawable.getNumberOfLayers(); i++) { @@ -104,6 +108,10 @@ public void draw(@NonNull Canvas canvas) { backgroundDrawableIndex = i; } + if (backgroundImageDrawableIndex < 0 && layer instanceof BackgroundImageDrawable) { + backgroundImageDrawableIndex = i; + } + if (borderDrawableIndex < 0 && layer instanceof BorderDrawable) { borderDrawableIndex = i; } @@ -123,6 +131,16 @@ public void draw(@NonNull Canvas canvas) { } } + BackgroundImageDrawable backgroundImageDrawable = null; + if (backgroundImageDrawableIndex >= 0) { + backgroundImageDrawable = (BackgroundImageDrawable) layerDrawable.getDrawable(backgroundImageDrawableIndex); + if (this.squircleBackgroundImageDrawable == null) { + this.squircleBackgroundImageDrawable = new SquircleBackgroundImageDrawable(backgroundImageDrawable, this.cornerSmoothing); + } else { + this.squircleBackgroundImageDrawable.setBase(backgroundImageDrawable); + } + } + BorderDrawable borderDrawable = null; if (borderDrawableIndex >= 0) { borderDrawable = (BorderDrawable) layerDrawable.getDrawable(borderDrawableIndex); @@ -146,12 +164,14 @@ public void draw(@NonNull Canvas canvas) { } if (backgroundDrawableIndex >= 0) layerDrawable.setDrawable(backgroundDrawableIndex, this.squircleBackgroundDrawable); + if (backgroundImageDrawableIndex >= 0) layerDrawable.setDrawable(backgroundImageDrawableIndex, this.squircleBackgroundImageDrawable); if (borderDrawableIndex >= 0) layerDrawable.setDrawable(borderDrawableIndex, this.squircleBorderDrawable); if (outlineDrawableIndex >= 0) layerDrawable.setDrawable(outlineDrawableIndex, this.squircleOutlineDrawable); super.draw(canvas); if (backgroundDrawableIndex >= 0) layerDrawable.setDrawable(backgroundDrawableIndex, backgroundDrawable); + if (backgroundImageDrawableIndex >= 0) layerDrawable.setDrawable(backgroundImageDrawableIndex, backgroundImageDrawable); if (borderDrawableIndex >= 0) layerDrawable.setDrawable(borderDrawableIndex, borderDrawable); if (outlineDrawableIndex >= 0) layerDrawable.setDrawable(outlineDrawableIndex, outlineDrawable); } @@ -164,6 +184,10 @@ public void setCornerSmoothing(float cornerSmoothing) { this.squircleBackgroundDrawable.setCornerSmoothing(cornerSmoothing); } + if (this.squircleBackgroundImageDrawable != null) { + this.squircleBackgroundImageDrawable.setCornerSmoothing(cornerSmoothing); + } + if (this.squircleBorderDrawable != null) { this.squircleBorderDrawable.setCornerSmoothing(cornerSmoothing); } diff --git a/android/src/main/java/com/fastsquircle/drawables/SquircleBackgroundImageDrawable.java b/android/src/main/java/com/fastsquircle/drawables/SquircleBackgroundImageDrawable.java new file mode 100644 index 0000000..cdbefa1 --- /dev/null +++ b/android/src/main/java/com/fastsquircle/drawables/SquircleBackgroundImageDrawable.java @@ -0,0 +1,114 @@ +package com.fastsquircle.drawables; + +import android.graphics.Canvas; +import android.graphics.Rect; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.drawable.BackgroundImageDrawable; +import com.fastsquircle.utils.SquirclePathCalculator; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Wraps BackgroundImageDrawable (RN 0.83+) to render background images/gradients + * with squircle corners instead of regular rounded rectangles. + */ +public class SquircleBackgroundImageDrawable extends ComposedDrawable { + + private BackgroundImageDrawable base; + private float cornerSmoothing; + + public SquircleBackgroundImageDrawable(BackgroundImageDrawable base, float cornerSmoothing) { + super(base); + this.base = base; + this.cornerSmoothing = cornerSmoothing; + } + + public void setBase(BackgroundImageDrawable base) { + super.updateBase(base); + this.base = base; + } + + public void setCornerSmoothing(float cornerSmoothing) { + this.cornerSmoothing = cornerSmoothing; + } + + @Override + public void draw(@NonNull Canvas canvas) { + var borderRadius = base.getBorderRadius(); + if (borderRadius == null) { + base.draw(canvas); + return; + } + + var context = getContext(); + if (context == null) { + base.draw(canvas); + return; + } + + var computedBorderRadius = borderRadius.resolve( + getLayoutDirection(), + context, + PixelUtil.toDIPFromPixel(getBounds().width()), + PixelUtil.toDIPFromPixel(getBounds().height()) + ); + + if (computedBorderRadius == null || !computedBorderRadius.hasRoundedBorders()) { + base.draw(canvas); + return; + } + + // Create squircle path and clip to it, then let base draw + canvas.save(); + + var bounds = getBounds(); + var squirclePath = SquirclePathCalculator.getPath( + computedBorderRadius, + bounds.width(), + bounds.height(), + cornerSmoothing + ); + + // Clip to the squircle path + canvas.clipPath(squirclePath); + + // Let the base drawable draw itself (which will draw the gradient) + // but it will be clipped to our squircle path + base.draw(canvas); + + canvas.restore(); + } + + @Nullable + private android.content.Context getContext() { + try { + Field field = BackgroundImageDrawable.class.getDeclaredField("context"); + field.setAccessible(true); + Object fieldValue = field.get(base); + field.setAccessible(false); + return (android.content.Context) fieldValue; + } catch (NoSuchFieldException | IllegalAccessException | ClassCastException ignored) { + } + return null; + } + + @Override + protected void onBoundsChange(@NonNull Rect bounds) { + super.onBoundsChange(bounds); + if (base == null) return; + + try { + Class clazz = base.getClass(); + Method privateMethod = clazz.getDeclaredMethod("onBoundsChange", Rect.class); + privateMethod.setAccessible(true); + privateMethod.invoke(base, bounds); + } catch (Exception ignored) { + } + } +} + diff --git a/ios/FastSquircleBoxShadow_81orMore.mm b/ios/FastSquircleBoxShadow_81orMore.mm index c96b43c..d10e077 100644 --- a/ios/FastSquircleBoxShadow_81orMore.mm +++ b/ios/FastSquircleBoxShadow_81orMore.mm @@ -65,8 +65,11 @@ static CGColorRef colorRefFromSharedColor(const SharedColor &color) { CALayer *shadowLayer = initBoxShadowLayer(shadow, layerSize); + // Calculate adjusted corner radii for the spread distance + RCTCornerRadii adjustedCornerRadii = cornerRadiiForBoxShadow(cornerRadii, shadow.spreadDistance); + const RCTCornerInsets shadowRectCornerInsets = - RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, shadow.spreadDistance), UIEdgeInsetsZero); + RCTGetCornerInsets(adjustedCornerRadii, UIEdgeInsetsZero); CGRect shadowRect = CGRectInset(shadowLayer.bounds, -shadow.spreadDistance, -shadow.spreadDistance); shadowRect = CGRectOffset(shadowRect, shadow.offsetX, shadow.offsetY); @@ -74,13 +77,12 @@ static CGColorRef colorRefFromSharedColor(const SharedColor &color) CGFloat width = shadowRect.size.width; CGFloat height = shadowRect.size.height; - NSLog(@"width: %f, height: %f", width, height); - + // Use the ADJUSTED corner radii for the shadow path SquircleParams *squircleParams = [[SquircleParams alloc] initWithCornerSmoothing:cornerSmoothing width:@(width) height:@(height)]; - squircleParams.topLeftCornerRadius = @(fmax(cornerRadii.topLeftVertical, cornerRadii.topLeftHorizontal)); - squircleParams.topRightCornerRadius = @(fmax(cornerRadii.topRightVertical, cornerRadii.topRightHorizontal)); - squircleParams.bottomLeftCornerRadius = @(fmax(cornerRadii.bottomLeftVertical, cornerRadii.bottomLeftHorizontal)); - squircleParams.bottomRightCornerRadius = @(fmax(cornerRadii.bottomRightVertical, cornerRadii.bottomRightHorizontal)); + squircleParams.topLeftCornerRadius = @(fmax(adjustedCornerRadii.topLeftVertical, adjustedCornerRadii.topLeftHorizontal)); + squircleParams.topRightCornerRadius = @(fmax(adjustedCornerRadii.topRightVertical, adjustedCornerRadii.topRightHorizontal)); + squircleParams.bottomLeftCornerRadius = @(fmax(adjustedCornerRadii.bottomLeftVertical, adjustedCornerRadii.bottomLeftHorizontal)); + squircleParams.bottomRightCornerRadius = @(fmax(adjustedCornerRadii.bottomRightVertical, adjustedCornerRadii.bottomRightHorizontal)); UIBezierPath *squirclePath = [SquirclePathGenerator getSquirclePath:squircleParams]; CGAffineTransform translation = CGAffineTransformMakeTranslation(shadowRect.origin.x, shadowRect.origin.y); @@ -97,9 +99,14 @@ static CGColorRef colorRefFromSharedColor(const SharedColor &color) CGRect maskRect = CGRectInset(shadowRect, -2 * (shadow.blurRadius + 1), -2 * (shadow.blurRadius + 1)); CGPathAddRect(path, NULL, maskRect); - // Add the squircle path as a hole (using original layer bounds for the squircle) + // Add the squircle path as a hole (using original layer bounds and ORIGINAL corner radii) squircleParams.width = @(shadowLayer.bounds.size.width); squircleParams.height = @(shadowLayer.bounds.size.height); + // Reset to original corner radii for the cutout (not adjusted) + squircleParams.topLeftCornerRadius = @(fmax(cornerRadii.topLeftVertical, cornerRadii.topLeftHorizontal)); + squircleParams.topRightCornerRadius = @(fmax(cornerRadii.topRightVertical, cornerRadii.topRightHorizontal)); + squircleParams.bottomLeftCornerRadius = @(fmax(cornerRadii.bottomLeftVertical, cornerRadii.bottomLeftHorizontal)); + squircleParams.bottomRightCornerRadius = @(fmax(cornerRadii.bottomRightVertical, cornerRadii.bottomRightHorizontal)); UIBezierPath *squircleLayerPath = [SquirclePathGenerator getSquirclePath:squircleParams]; CGPathRef layerPath = CGPathCreateCopy(squircleLayerPath.CGPath); diff --git a/ios/FastSquircleView.mm b/ios/FastSquircleView.mm index 71f25c4..a5c0c9f 100644 --- a/ios/FastSquircleView.mm +++ b/ios/FastSquircleView.mm @@ -170,6 +170,9 @@ - (void)invalidateLayer Ivar backgroundColorLayerIvar = class_getInstanceVariable([RCTViewComponentView class], "_backgroundColorLayer"); CALayer *backgroundColorLayer = object_getIvar(self, backgroundColorLayerIvar); + Ivar backgroundImageLayersIvar = class_getInstanceVariable([RCTViewComponentView class], "_backgroundImageLayers"); + NSArray *backgroundImageLayers = object_getIvar(self, backgroundImageLayersIvar); + Ivar borderLayerIvar = class_getInstanceVariable([RCTViewComponentView class], "_borderLayer"); CALayer *borderLayer = object_getIvar(self, borderLayerIvar); @@ -227,6 +230,16 @@ - (void)invalidateLayer [_squircleBackgroundLayer removeAllAnimations]; } + // background image layers (gradients, images, etc.) + if (backgroundImageLayers && backgroundImageLayers.count > 0) { + for (CALayer *backgroundImageLayer in backgroundImageLayers) { + CAShapeLayer *backgroundImageMaskLayer = [[CAShapeLayer alloc] init]; + backgroundImageMaskLayer.path = squirclePath.CGPath; + backgroundImageLayer.mask = backgroundImageMaskLayer; + [backgroundImageLayer removeAllAnimations]; + } + } + // border if (borderLayer) { if (_squircleBorderLayer) {