mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Outset box shadow impl
Summary: tisa. Algo is * Draw offscreen rect the size of the outset shadow (so accounting for spread) * Set g state to cast shadow in proper place ON screen * Clear out region in view The rects need to be offscreen for the following reasons * We need to account for spread radius, and CGContext shadows do not have support for this. So the only way to create a bigger shadow is the create a path that is the same size as the shadow we want * We cannot just position this rect onscreen with no offset (so the shadow is casting directly under it) since the blur will look unnatural * Offscreen means we do not see filled shadow rect but we do see the shadow it casts by offsetting it in the proper location Changelog: [Internal] Reviewed By: lenaic Differential Revision: D58881588 fbshipit-source-id: 2ea1b8945a3b9f182c4fb11668ac91a0ae7846ca
This commit is contained in:
committed by
Facebook GitHub Bot
parent
24d7cc15db
commit
a3cf1624d0
+20
@@ -13,6 +13,7 @@
|
||||
|
||||
#import <React/RCTAssert.h>
|
||||
#import <React/RCTBorderDrawing.h>
|
||||
#import <React/RCTBoxShadow.h>
|
||||
#import <React/RCTConversions.h>
|
||||
#import <React/RCTLocalizedString.h>
|
||||
#import <react/renderer/components/view/ViewComponentDescriptor.h>
|
||||
@@ -29,6 +30,7 @@ using namespace facebook::react;
|
||||
@implementation RCTViewComponentView {
|
||||
UIColor *_backgroundColor;
|
||||
__weak CALayer *_borderLayer;
|
||||
CALayer *_boxShadowLayer;
|
||||
CALayer *_filterLayer;
|
||||
BOOL _needsInvalidateLayer;
|
||||
BOOL _isJSResponder;
|
||||
@@ -394,6 +396,11 @@ using namespace facebook::react;
|
||||
_needsInvalidateLayer = YES;
|
||||
}
|
||||
|
||||
// `boxShadow`
|
||||
if (oldViewProps.boxShadow != newViewProps.boxShadow) {
|
||||
_needsInvalidateLayer = YES;
|
||||
}
|
||||
|
||||
_needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer;
|
||||
|
||||
_props = std::static_pointer_cast<const ViewProps>(props);
|
||||
@@ -761,6 +768,19 @@ static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
|
||||
_filterLayer.zPosition = CGFLOAT_MAX;
|
||||
[self.layer addSublayer:_filterLayer];
|
||||
}
|
||||
|
||||
_boxShadowLayer = nil;
|
||||
if (!_props->boxShadow.empty()) {
|
||||
_boxShadowLayer = [CALayer layer];
|
||||
[self.layer addSublayer:_boxShadowLayer];
|
||||
_boxShadowLayer.zPosition = CGFLOAT_MIN;
|
||||
_boxShadowLayer.frame = RCTGetBoundingRect(_props->boxShadow, self.layer.frame.size);
|
||||
|
||||
UIImage *boxShadowImage =
|
||||
RCTGetBoxShadowImage(_props->boxShadow, RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), layer);
|
||||
|
||||
_boxShadowLayer.contents = (id)boxShadowImage.CGImage;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Accessibility
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import <vector>
|
||||
|
||||
#import <React/RCTBorderDrawing.h>
|
||||
#import <React/RCTDefines.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <react/renderer/graphics/BoxShadow.h>
|
||||
|
||||
RCT_EXTERN UIImage *
|
||||
RCTGetBoxShadowImage(std::vector<facebook::react::BoxShadow> shadows, RCTCornerRadii cornerRadii, CALayer *layer);
|
||||
|
||||
RCT_EXTERN CGRect
|
||||
RCTGetBoxShadowRect(CGFloat offsetX, CGFloat offsetY, CGFloat blurRadius, CGFloat spreadRadius, CGSize layerSize);
|
||||
|
||||
RCT_EXTERN CGRect RCTGetBoundingRect(std::vector<facebook::react::BoxShadow> boxShadows, CGSize layerSize);
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#import "RCTBoxShadow.h"
|
||||
|
||||
#import <CoreImage/CoreImage.h>
|
||||
#import <React/RCTConversions.h>
|
||||
|
||||
#import <math.h>
|
||||
|
||||
using namespace facebook::react;
|
||||
// See https://drafts.csswg.org/css-backgrounds/#shadow-shape
|
||||
static CGFloat adjustedCornerRadius(CGFloat cornerRadius, CGFloat spreadRadius)
|
||||
{
|
||||
CGFloat adjustment = spreadRadius;
|
||||
if (cornerRadius < spreadRadius) {
|
||||
adjustment *= 1 + pow((cornerRadius / spreadRadius) - 1, 3);
|
||||
}
|
||||
|
||||
return cornerRadius + adjustment;
|
||||
}
|
||||
|
||||
static RCTCornerRadii cornerRadiiForBoxShadow(RCTCornerRadii cornerRadii, CGFloat spreadRadius)
|
||||
{
|
||||
return {
|
||||
adjustedCornerRadius(cornerRadii.topLeft, spreadRadius),
|
||||
adjustedCornerRadius(cornerRadii.topRight, spreadRadius),
|
||||
adjustedCornerRadius(cornerRadii.bottomLeft, spreadRadius),
|
||||
adjustedCornerRadius(cornerRadii.bottomRight, spreadRadius)};
|
||||
}
|
||||
|
||||
CGRect RCTGetBoxShadowRect(CGFloat offsetX, CGFloat offsetY, CGFloat blurRadius, CGFloat spreadRadius, CGSize layerSize)
|
||||
{
|
||||
return CGRectMake(
|
||||
offsetX - spreadRadius - blurRadius,
|
||||
offsetY - spreadRadius - blurRadius,
|
||||
layerSize.width + 2 * (spreadRadius + blurRadius),
|
||||
layerSize.height + 2 * (spreadRadius + blurRadius));
|
||||
}
|
||||
|
||||
// Returns the smallest CGRect that will contain all shadows and the layer itself.
|
||||
// The origin represents the location of this box relative to the layer the shadows
|
||||
// are attached to.
|
||||
CGRect RCTGetBoundingRect(std::vector<BoxShadow> boxShadows, CGSize layerSize)
|
||||
{
|
||||
CGFloat smallestX = 0;
|
||||
CGFloat smallestY = 0;
|
||||
CGFloat largestX = layerSize.width;
|
||||
CGFloat largestY = layerSize.height;
|
||||
for (const auto &boxShadow : boxShadows) {
|
||||
CGFloat negativeXExtent = boxShadow.offsetX - boxShadow.spreadRadius - boxShadow.blurRadius;
|
||||
smallestX = MIN(smallestX, negativeXExtent);
|
||||
CGFloat negativeYExtent = boxShadow.offsetY - boxShadow.spreadRadius - boxShadow.blurRadius;
|
||||
smallestY = MIN(smallestY, negativeYExtent);
|
||||
CGFloat positiveXExtent = boxShadow.offsetX + boxShadow.spreadRadius + boxShadow.blurRadius + layerSize.width;
|
||||
largestX = MAX(largestX, positiveXExtent);
|
||||
CGFloat positiveYExtent = boxShadow.offsetY + boxShadow.spreadRadius + boxShadow.blurRadius + layerSize.height;
|
||||
largestY = MAX(largestY, positiveYExtent);
|
||||
}
|
||||
|
||||
return CGRectMake(smallestX, smallestY, largestX - smallestX, largestY - smallestY);
|
||||
}
|
||||
|
||||
UIImage *RCTGetBoxShadowImage(std::vector<BoxShadow> shadows, RCTCornerRadii cornerRadii, CALayer *layer)
|
||||
{
|
||||
CGRect boundingRect = RCTGetBoundingRect(shadows, layer.bounds.size);
|
||||
UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat];
|
||||
UIGraphicsImageRenderer *const renderer = [[UIGraphicsImageRenderer alloc] initWithSize:boundingRect.size
|
||||
format:rendererFormat];
|
||||
// Core graphics has support for shadows that looks similar to web and are very
|
||||
// fast to apply. The only issue is that this shadow does not take a spread
|
||||
// radius like on web. To get around this, we draw the shadow rect (the rect
|
||||
// that casts the shadow, not the shadow itself) offscreen. This shadow rect
|
||||
// is correctly sized to account for spread radius. Then, when setting the
|
||||
// shadow itself, we modify the offsetX/Y to account for the fact that our
|
||||
// shadow rect is offscreen and position it where it needs to be in the image.
|
||||
UIImage *const boxShadowImage = [renderer imageWithActions:^(
|
||||
UIGraphicsImageRendererContext *_Nonnull rendererContext) {
|
||||
const CGContextRef context = rendererContext.CGContext;
|
||||
// Reverse iterator as shadows are stacked back to front
|
||||
for (auto it = shadows.rbegin(); it != shadows.rend(); ++it) {
|
||||
CGFloat offsetX = it->offsetX;
|
||||
CGFloat offsetY = it->offsetY;
|
||||
CGFloat blurRadius = it->blurRadius;
|
||||
CGFloat spreadRadius = it->spreadRadius;
|
||||
CGColorRef color = RCTUIColorFromSharedColor(it->color).CGColor;
|
||||
|
||||
// First, define the shadow rect. This is the rect that will be filled
|
||||
// and _cast_ the shadow. As a result, the size does not incorporate
|
||||
// the blur radius since this rect is not the shadow itself.
|
||||
const RCTCornerInsets shadowRectCornerInsets =
|
||||
RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, spreadRadius), UIEdgeInsetsZero);
|
||||
CGSize shadowRectSize =
|
||||
CGSizeMake(layer.bounds.size.width + 2 * spreadRadius, layer.bounds.size.height + 2 * spreadRadius);
|
||||
// Ensure this is drawn offscreen and will not show in the image
|
||||
CGRect shadowRect = CGRectMake(-shadowRectSize.width, 0, shadowRectSize.width, shadowRectSize.height);
|
||||
CGPathRef shadowRectPath = RCTPathCreateWithRoundedRect(shadowRect, shadowRectCornerInsets, nil);
|
||||
|
||||
// Second, set the shadow as graphics state so that when we fill our
|
||||
// shadow rect it will actually cast a shadow. The offset of this
|
||||
// shadow needs to compensate for the fact that the shadow rect is
|
||||
// offscreen. Additionally, we take away the spread radius since spread
|
||||
// grows in all directions but the origin of our shadow rect is just
|
||||
// the negative width, which accounts for 2*spread radius.
|
||||
CGContextSetShadowWithColor(
|
||||
context,
|
||||
CGSizeMake(
|
||||
offsetX - boundingRect.origin.x - spreadRadius - shadowRect.origin.x,
|
||||
offsetY - boundingRect.origin.y - spreadRadius),
|
||||
blurRadius,
|
||||
color);
|
||||
|
||||
// Third, the Core Graphics functions to actually draw the shadow rect
|
||||
// and thus the shadow itself.
|
||||
CGContextAddPath(context, shadowRectPath);
|
||||
// Color here does not matter, we just need something that has 1 for
|
||||
// alpha so that the shadow is visible. The rect is purposely rendered
|
||||
// outside of the context so it should not be visible.
|
||||
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
|
||||
CGContextFillPath(context);
|
||||
|
||||
CGPathRelease(shadowRectPath);
|
||||
}
|
||||
// Lastly, clear out the region inside the view so that the shadows do
|
||||
// not cover its content
|
||||
const RCTCornerInsets layerCornerInsets =
|
||||
RCTGetCornerInsets(cornerRadiiForBoxShadow(cornerRadii, 0), UIEdgeInsetsZero);
|
||||
CGPathRef shadowPathAlignedWithLayer = RCTPathCreateWithRoundedRect(
|
||||
CGRectMake(-boundingRect.origin.x, -boundingRect.origin.y, layer.bounds.size.width, layer.bounds.size.height),
|
||||
layerCornerInsets,
|
||||
nil);
|
||||
CGContextAddPath(context, shadowPathAlignedWithLayer);
|
||||
CGContextSetBlendMode(context, kCGBlendModeClear);
|
||||
CGContextFillPath(context);
|
||||
|
||||
CGPathRelease(shadowPathAlignedWithLayer);
|
||||
}];
|
||||
|
||||
return boxShadowImage;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ void ViewShadowNode::initialize() noexcept {
|
||||
|
||||
bool formsView = formsStackingContext ||
|
||||
isColorMeaningful(viewProps.backgroundColor) || hasBorder() ||
|
||||
!viewProps.testId.empty() ||
|
||||
!viewProps.testId.empty() || !viewProps.boxShadow.empty() ||
|
||||
HostPlatformViewTraitsInitializer::formsView(viewProps);
|
||||
|
||||
if (formsView) {
|
||||
|
||||
Reference in New Issue
Block a user