Skip to content

Commit cd6fccb

Browse files
authored
feat(core): multiple css box-shadows support (#10777)
1 parent cce8a22 commit cd6fccb

File tree

16 files changed

+291
-245
lines changed

16 files changed

+291
-245
lines changed

apps/toolbox/src/pages/box-shadow.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ export class BoxShadowModel extends Observable {
1010
private _selectedBackgroundType: string;
1111
private _selectedBorderType: string;
1212
private _selectedAnimation: string;
13-
private _boxShadow: string = '0 0 2 2 rgba(200, 0, 0, 0.4)';
14-
// private _boxShadow: string = '5 5 1 1 rgba(255, 0, 0, .9)';
15-
// private _boxShadow: string = '5 5 5 10 rgba(255, 0, 0, .9)';
13+
private _boxShadow: string = '-5 -5 5 5 rgba(200, 0, 0, 0.5), 5 5 5 5 rgba(0, 99, 0, 0.5)';
1614

1715
background: string;
1816
borderColor: string;
@@ -136,7 +134,7 @@ export class BoxShadowModel extends Observable {
136134
view.animate({
137135
width: originalWidth,
138136
duration: animationDuration,
139-
})
137+
}),
140138
)
141139
.catch((err) => {
142140
console.error('animation error', err);
@@ -152,7 +150,7 @@ export class BoxShadowModel extends Observable {
152150
view.animate({
153151
height: originalHeight,
154152
duration: animationDuration,
155-
})
153+
}),
156154
)
157155
.catch((err) => {
158156
console.error('animation error', err);
@@ -173,7 +171,7 @@ export class BoxShadowModel extends Observable {
173171
rotate: 0,
174172
translate: { x: 0, y: 0 },
175173
duration: 500,
176-
})
174+
}),
177175
)
178176
.catch((err) => {
179177
console.error('animation error', err);

packages/core/ui/core/view/index.android.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,17 +1507,23 @@ export class View extends ViewCommon {
15071507
}
15081508
}
15091509

1510-
protected _drawBoxShadow(boxShadow: BoxShadow) {
1510+
protected _drawBoxShadow(boxShadows: BoxShadow[]) {
15111511
const nativeView = this.nativeViewProtected;
1512-
const config = {
1513-
shadowColor: boxShadow.color.android,
1514-
cornerRadius: Length.toDevicePixels(this.borderRadius as CoreTypes.LengthType, 0.0),
1515-
spreadRadius: boxShadow.spreadRadius,
1516-
blurRadius: boxShadow.blurRadius,
1517-
offsetX: boxShadow.offsetX,
1518-
offsetY: boxShadow.offsetY,
1519-
};
1520-
org.nativescript.widgets.Utils.drawBoxShadow(nativeView, JSON.stringify(config));
1512+
const valueCount = 6;
1513+
const nativeArray: number[] = Array.create('int', boxShadows.length * valueCount);
1514+
1515+
for (let i = 0, length = boxShadows.length; i < length; i++) {
1516+
const boxShadow = boxShadows[i];
1517+
const nativeIndex = i * valueCount;
1518+
1519+
nativeArray[nativeIndex + 0] = boxShadow.color.android;
1520+
nativeArray[nativeIndex + 1] = boxShadow.spreadRadius;
1521+
nativeArray[nativeIndex + 2] = boxShadow.blurRadius;
1522+
nativeArray[nativeIndex + 3] = boxShadow.offsetX;
1523+
nativeArray[nativeIndex + 4] = boxShadow.offsetY;
1524+
nativeArray[nativeIndex + 5] = boxShadow.inset ? 1 : 0;
1525+
}
1526+
org.nativescript.widgets.Utils.drawBoxShadow(nativeView, nativeArray);
15211527
}
15221528

15231529
_redrawNativeBackground(value: android.graphics.drawable.Drawable | Background): void {
@@ -1566,15 +1572,15 @@ export class View extends ViewCommon {
15661572
// prettier-ignore
15671573
const onlyColor = !background.hasBorderWidth()
15681574
&& !background.hasBorderRadius()
1569-
&& !background.hasBoxShadow()
1575+
&& !background.hasBoxShadows()
15701576
&& !background.clipPath
15711577
&& !background.image
15721578
&& !!background.color;
15731579

15741580
this._applyBackground(background, isBorderDrawable, onlyColor, drawable);
15751581

1576-
if (background.hasBoxShadow()) {
1577-
this._drawBoxShadow(background.getBoxShadow());
1582+
if (background.hasBoxShadows()) {
1583+
this._drawBoxShadow(background.getBoxShadows());
15781584
}
15791585

15801586
// TODO: Can we move BorderWidths as separate native setter?

packages/core/ui/core/view/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ export abstract class View extends ViewCommon {
384384
*
385385
* @nsProperty
386386
*/
387-
boxShadow: string | ShadowCSSValues;
387+
boxShadow: string | ShadowCSSValues[];
388388

389389
/**
390390
* Gets or sets the minimum width the view may grow to.

packages/core/ui/core/view/index.ios.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export class View extends ViewCommon {
393393
}
394394

395395
const background = this.style.backgroundInternal;
396-
const backgroundDependsOnSize = (background.image && background.image !== 'none') || background.clipPath || !background.hasUniformBorder() || background.hasBorderRadius() || background.hasBoxShadow();
396+
const backgroundDependsOnSize = (background.image && background.image !== 'none') || background.clipPath || !background.hasUniformBorder() || background.hasBorderRadius() || background.hasBoxShadows();
397397

398398
if (this._nativeBackgroundState === 'invalid' || (this._nativeBackgroundState === 'drawn' && backgroundDependsOnSize)) {
399399
this._redrawNativeBackground(background);

packages/core/ui/core/view/view-common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -724,10 +724,10 @@ export abstract class ViewCommon extends ViewBase {
724724
this.style.backgroundRepeat = value;
725725
}
726726

727-
get boxShadow(): ShadowCSSValues {
727+
get boxShadow(): string | ShadowCSSValues[] {
728728
return this.style.boxShadow;
729729
}
730-
set boxShadow(value: ShadowCSSValues) {
730+
set boxShadow(value: string | ShadowCSSValues[]) {
731731
this.style.boxShadow = value;
732732
}
733733

packages/core/ui/styling/background-common.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class Background {
4040
public borderBottomLeftRadius = 0;
4141
public borderBottomRightRadius = 0;
4242
public clipPath: string | ClipPathFunction;
43-
public boxShadow: BoxShadow;
43+
public boxShadows: BoxShadow[];
4444
public clearFlags: number = BackgroundClearFlags.NONE;
4545

4646
private clone(): Background {
@@ -64,7 +64,7 @@ export class Background {
6464
clone.borderBottomRightRadius = this.borderBottomRightRadius;
6565
clone.borderBottomLeftRadius = this.borderBottomLeftRadius;
6666
clone.clipPath = this.clipPath;
67-
clone.boxShadow = this.boxShadow;
67+
clone.boxShadows = this.boxShadows;
6868
clone.clearFlags = this.clearFlags;
6969

7070
return clone;
@@ -199,10 +199,10 @@ export class Background {
199199
return clone;
200200
}
201201

202-
public withBoxShadow(value: BoxShadow): Background {
202+
public withBoxShadows(value: BoxShadow[]): Background {
203203
const clone = this.clone();
204-
clone.boxShadow = value;
205-
if (!value) {
204+
clone.boxShadows = value;
205+
if (!value?.length) {
206206
clone.clearFlags |= BackgroundClearFlags.CLEAR_BOX_SHADOW;
207207
}
208208

@@ -317,12 +317,12 @@ export class Background {
317317
return 0;
318318
}
319319

320-
public hasBoxShadow(): boolean {
321-
return !!this.boxShadow;
320+
public hasBoxShadows(): boolean {
321+
return this.boxShadows?.length > 0;
322322
}
323323

324-
public getBoxShadow(): BoxShadow {
325-
return this.boxShadow;
324+
public getBoxShadows(): BoxShadow[] {
325+
return this.boxShadows;
326326
}
327327

328328
public toString(): string {

packages/core/ui/styling/background.d.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export enum CacheMode {
3434
// public borderBottomRightRadius: number;
3535
// public borderBottomLeftRadius: number;
3636
// public clipPath: string | ClipPathFunction;
37-
// public boxShadow: string | BoxShadow;
3837
// public clearFlags: number;
3938

4039
// public withColor(value: Color): Background;
@@ -72,8 +71,6 @@ export enum CacheMode {
7271
// public getUniformBorderColor(): Color;
7372
// public getUniformBorderWidth(): number;
7473
// public getUniformBorderRadius(): number;
75-
// public hasBoxShadow(): boolean;
76-
// public getBoxShadow(): BoxShadow;
7774
// }
7875

7976
export namespace ios {

packages/core/ui/styling/background.ios.ts

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export namespace ios {
103103
layer.mask.path = generateClipPath(view, layer.bounds);
104104
}
105105

106-
if (background.hasBoxShadow()) {
106+
if (background.hasBoxShadows()) {
107107
drawBoxShadow(view);
108108
needsLayerAdjustmentOnScroll = true;
109109
}
@@ -214,12 +214,10 @@ export namespace ios {
214214
callback(generatePatternImage(bitmap, view, flip));
215215
}
216216

217-
export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any } {
217+
export function generateShadowLayerPaths(view: View, boxShadow: BoxShadow, bounds: CGRect): { maskPath: any; shadowPath: any } {
218218
const background = view.style.backgroundInternal;
219219
const nativeView = <NativeScriptUIView>view.nativeViewProtected;
220220
const layer = nativeView.layer;
221-
222-
const boxShadow: BoxShadow = background.getBoxShadow();
223221
const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
224222

225223
const { width, height } = bounds.size;
@@ -1080,65 +1078,56 @@ function drawBoxShadow(view: View): void {
10801078
}
10811079

10821080
const bounds = nativeView.bounds;
1083-
const boxShadow: BoxShadow = background.getBoxShadow();
1081+
const boxShadows: BoxShadow[] = background.getBoxShadows();
10841082

10851083
// Initialize outer shadows
1086-
let outerShadowContainerLayer: CALayer;
1084+
let shadowGroupLayer: CALayer;
1085+
10871086
if (nativeView.outerShadowContainerLayer) {
1088-
outerShadowContainerLayer = nativeView.outerShadowContainerLayer;
1087+
shadowGroupLayer = nativeView.outerShadowContainerLayer;
10891088
} else {
1090-
outerShadowContainerLayer = CALayer.new();
1091-
1092-
// TODO: Make this dynamic when views get support for multiple shadows
1093-
const shadowLayer = CALayer.new();
1094-
// This mask is necessary to maintain transparent background
1095-
const maskLayer = CAShapeLayer.new();
1096-
maskLayer.fillRule = kCAFillRuleEvenOdd;
1097-
1098-
shadowLayer.mask = maskLayer;
1099-
outerShadowContainerLayer.addSublayer(shadowLayer);
1089+
shadowGroupLayer = CALayer.new();
11001090

11011091
// Instead of nesting it, add shadow container layer underneath view so that it's not affected by border masking
1102-
layer.superlayer.insertSublayerBelow(outerShadowContainerLayer, layer);
1103-
nativeView.outerShadowContainerLayer = outerShadowContainerLayer;
1092+
layer.superlayer.insertSublayerBelow(shadowGroupLayer, layer);
1093+
nativeView.outerShadowContainerLayer = shadowGroupLayer;
11041094
}
11051095

1106-
// Apply clip path to shadow
1107-
if (nativeView.maskType === iosViewUtils.LayerMask.CLIP_PATH && layer.mask instanceof CAShapeLayer) {
1108-
if (!outerShadowContainerLayer.mask) {
1109-
outerShadowContainerLayer.mask = CAShapeLayer.new();
1096+
// Check if the number of shadow layers is correct
1097+
if (!shadowGroupLayer.sublayers || boxShadows.length > shadowGroupLayer.sublayers.count) {
1098+
const length = shadowGroupLayer.sublayers ? boxShadows.length - shadowGroupLayer.sublayers.count : boxShadows.length;
1099+
1100+
for (let i = 0; i < length; i++) {
1101+
const shadowLayer = CALayer.new();
1102+
// This mask is necessary to maintain transparent background
1103+
const maskLayer = CAShapeLayer.new();
1104+
1105+
maskLayer.fillRule = kCAFillRuleEvenOdd;
1106+
shadowLayer.mask = maskLayer;
1107+
shadowGroupLayer.addSublayer(shadowLayer);
11101108
}
1111-
if (outerShadowContainerLayer.mask instanceof CAShapeLayer) {
1112-
outerShadowContainerLayer.mask.path = layer.mask.path;
1109+
} else if (shadowGroupLayer.sublayers && boxShadows.length < shadowGroupLayer.sublayers.count) {
1110+
for (let i = 0, length = shadowGroupLayer.sublayers.count - boxShadows.length; i < length; i++) {
1111+
shadowGroupLayer.sublayers[i].removeFromSuperlayer();
11131112
}
11141113
}
11151114

1116-
outerShadowContainerLayer.bounds = bounds;
1117-
outerShadowContainerLayer.transform = layer.transform;
1118-
outerShadowContainerLayer.anchorPoint = layer.anchorPoint;
1119-
outerShadowContainerLayer.position = nativeView.center;
1120-
outerShadowContainerLayer.zPosition = layer.zPosition;
1121-
1122-
// Inherit view visibility values
1123-
outerShadowContainerLayer.opacity = layer.opacity;
1124-
outerShadowContainerLayer.hidden = layer.hidden;
1125-
1126-
const outerShadowLayers = outerShadowContainerLayer.sublayers;
1127-
if (outerShadowLayers?.count) {
1128-
for (let i = 0, count = outerShadowLayers.count; i < count; i++) {
1129-
const shadowLayer = outerShadowLayers[i];
1115+
if (shadowGroupLayer.sublayers?.count) {
1116+
for (let i = 0, count = shadowGroupLayer.sublayers.count; i < count; i++) {
1117+
const shadowLayer = shadowGroupLayer.sublayers[i];
1118+
const boxShadow = boxShadows[i];
11301119
const shadowRadius = layout.toDeviceIndependentPixels(boxShadow.blurRadius);
1131-
const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
11321120
const offsetX = layout.toDeviceIndependentPixels(boxShadow.offsetX);
11331121
const offsetY = layout.toDeviceIndependentPixels(boxShadow.offsetY);
1134-
const { maskPath, shadowPath } = ios.generateShadowLayerPaths(view, bounds);
1122+
const { maskPath, shadowPath } = ios.generateShadowLayerPaths(view, boxShadow, bounds);
11351123

11361124
shadowLayer.allowsEdgeAntialiasing = true;
11371125
shadowLayer.contentsScale = Screen.mainScreen.scale;
11381126

11391127
// Shadow opacity is handled on the shadow's color instance
11401128
shadowLayer.shadowOpacity = boxShadow.color?.a ? boxShadow.color.a / 255 : 1;
1141-
shadowLayer.shadowRadius = shadowRadius;
1129+
// Use this multiplier to imitate CSS shadow blur
1130+
shadowLayer.shadowRadius = shadowRadius * 0.5;
11421131
shadowLayer.shadowColor = boxShadow.color?.ios?.CGColor;
11431132
shadowLayer.shadowOffset = CGSizeMake(offsetX, offsetY);
11441133

@@ -1151,6 +1140,26 @@ function drawBoxShadow(view: View): void {
11511140
}
11521141
}
11531142
}
1143+
1144+
// Apply clip path to shadow
1145+
if (nativeView.maskType === iosViewUtils.LayerMask.CLIP_PATH && layer.mask instanceof CAShapeLayer) {
1146+
if (!shadowGroupLayer.mask) {
1147+
shadowGroupLayer.mask = CAShapeLayer.new();
1148+
}
1149+
if (shadowGroupLayer.mask instanceof CAShapeLayer) {
1150+
shadowGroupLayer.mask.path = layer.mask.path;
1151+
}
1152+
}
1153+
1154+
shadowGroupLayer.bounds = bounds;
1155+
shadowGroupLayer.transform = layer.transform;
1156+
shadowGroupLayer.anchorPoint = layer.anchorPoint;
1157+
shadowGroupLayer.position = nativeView.center;
1158+
shadowGroupLayer.zPosition = layer.zPosition;
1159+
1160+
// Inherit view visibility values
1161+
shadowGroupLayer.opacity = layer.opacity;
1162+
shadowGroupLayer.hidden = layer.hidden;
11541163
}
11551164

11561165
function clearBoxShadow(nativeView: NativeScriptUIView) {

packages/core/ui/styling/css-utils.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Trace } from '../../trace';
21
import { CoreTypes } from '../../core-types';
2+
import { Trace } from '../../trace';
33
import { Length } from './length-shared';
44

55
export function cleanupImportantFlags(value: unknown, propertyName: string) {
@@ -18,9 +18,14 @@ export function cleanupImportantFlags(value: unknown, propertyName: string) {
1818
}
1919

2020
/**
21-
* Matches whitespace except if the whitespace is contained in parenthesis - ex. rgb(a), hsl color.
21+
* Matches whitespace except if the whitespace is contained in parenthesis - e.g. rgb(a), hsl color.
22+
*/
23+
const WHITE_SPACE_RE = /\s(?![^(]*\))/;
24+
25+
/**
26+
* Matches comma except if the comma is contained in parenthesis - e.g. rgb(a, b, c).
2227
*/
23-
const PARTS_RE = /\s(?![^(]*\))/;
28+
const COMMA_RE = /,(?![^(]*\))/;
2429

2530
/**
2631
* Matches a Length value with or without a unit
@@ -32,12 +37,22 @@ const LENGTH_RE = /^-?[0-9]+[a-zA-Z%]*?$/;
3237
*/
3338
const isLength = (v) => v === '0' || LENGTH_RE.test(v);
3439

40+
export function parseCSSCommaSeparatedListOfValues(value: string): string[] {
41+
const values: string[] = [];
42+
43+
if (!value) {
44+
return [];
45+
}
46+
47+
return value.split(COMMA_RE);
48+
}
49+
3550
export function parseCSSShorthand(value: string): {
3651
values: Array<CoreTypes.LengthType>;
3752
color: string;
3853
inset: boolean;
3954
} {
40-
const parts = value.trim().split(PARTS_RE);
55+
const parts = value.trim().split(WHITE_SPACE_RE);
4156
const first = parts[0];
4257

4358
if (['', 'none', 'unset'].includes(first)) {

0 commit comments

Comments
 (0)