Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions android/src/main/java/com/fastsquircle/FastSquircleView.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

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;
import com.facebook.react.uimanager.drawable.OutsetBoxShadowDrawable;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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++) {
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
}
}
}

23 changes: 15 additions & 8 deletions ios/FastSquircleBoxShadow_81orMore.mm
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,24 @@ 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);

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);
Expand All @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions ios/FastSquircleView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<CALayer *> *backgroundImageLayers = object_getIvar(self, backgroundImageLayersIvar);

Ivar borderLayerIvar = class_getInstanceVariable([RCTViewComponentView class], "_borderLayer");
CALayer *borderLayer = object_getIvar(self, borderLayerIvar);

Expand Down Expand Up @@ -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) {
Expand Down