diff --git a/apps/toolbox/src/pages/box-shadow.ts b/apps/toolbox/src/pages/box-shadow.ts
index 5de8dd7b1..f9c156a9e 100644
--- a/apps/toolbox/src/pages/box-shadow.ts
+++ b/apps/toolbox/src/pages/box-shadow.ts
@@ -15,6 +15,7 @@ export class BoxShadowModel extends Observable {
// private _boxShadow: string = '5 5 5 10 rgba(255, 0, 0, .9)';
background: string;
+ borderColor: string;
borderWidth: number;
borderRadius: number;
appliedBoxShadow: string;
@@ -72,18 +73,23 @@ export class BoxShadowModel extends Observable {
this.notifyPropertyChange('selectedBorderType', value);
switch (value) {
case 'solid':
- this.borderWidth = this.borderWidth ? 0 : 2;
+ this.borderWidth = this.borderWidth ? 0 : 5;
break;
case 'rounded':
this.borderRadius = this.borderRadius ? 0 : 10;
break;
+ case 'colorful':
+ this.borderColor = this.borderColor ? null : 'green blue pink yellow';
+ break;
case 'none':
+ this.borderColor = null;
this.borderRadius = 0;
this.borderWidth = 0;
break;
default:
break;
}
+ this.notifyPropertyChange('borderColor', this.borderColor);
this.notifyPropertyChange('borderRadius', this.borderRadius);
this.notifyPropertyChange('borderWidth', this.borderWidth);
}
diff --git a/apps/toolbox/src/pages/box-shadow.xml b/apps/toolbox/src/pages/box-shadow.xml
index f19aea34b..6724ea0d4 100644
--- a/apps/toolbox/src/pages/box-shadow.xml
+++ b/apps/toolbox/src/pages/box-shadow.xml
@@ -9,129 +9,48 @@
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
@@ -139,61 +58,46 @@
-
-
-
+
+
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
-
-
+
+
+
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/apps/ui/src/scroll-view/scrolling-and-sizing-page.css b/apps/ui/src/scroll-view/scrolling-and-sizing-page.css
index 14689fa9f..352b6aea0 100644
--- a/apps/ui/src/scroll-view/scrolling-and-sizing-page.css
+++ b/apps/ui/src/scroll-view/scrolling-and-sizing-page.css
@@ -42,6 +42,14 @@ ScrollView {
border-left-width: 20;
}
+.gradient {
+ background: linear-gradient(to bottom, orangered, green, lightblue);
+}
+
+.shadow {
+ box-shadow: inset 0 0 5 5 #000000;
+}
+
.body {
font-size: 11;
}
diff --git a/apps/ui/src/scroll-view/scrolling-and-sizing-page.xml b/apps/ui/src/scroll-view/scrolling-and-sizing-page.xml
index b9fe7ed4a..064f5d8e3 100644
--- a/apps/ui/src/scroll-view/scrolling-and-sizing-page.xml
+++ b/apps/ui/src/scroll-view/scrolling-and-sizing-page.xml
@@ -6,7 +6,7 @@
-
+
@@ -17,7 +17,7 @@
-
+
@@ -28,7 +28,7 @@
-
+
@@ -39,7 +39,7 @@
-
+
@@ -50,7 +50,7 @@
-
+
@@ -61,5 +61,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/ui/src/text-view/scrolling-and-sizing-page.css b/apps/ui/src/text-view/scrolling-and-sizing-page.css
index 9a74612cd..2cbc719a4 100644
--- a/apps/ui/src/text-view/scrolling-and-sizing-page.css
+++ b/apps/ui/src/text-view/scrolling-and-sizing-page.css
@@ -42,6 +42,14 @@ TextView {
border-left-width: 20;
}
+.gradient {
+ background: linear-gradient(to bottom, orangered, green, lightblue);
+}
+
+.shadow {
+ box-shadow: inset 0 0 5 5 #000000;
+}
+
.body {
font-size: 11;
}
diff --git a/apps/ui/src/text-view/scrolling-and-sizing-page.xml b/apps/ui/src/text-view/scrolling-and-sizing-page.xml
index cfebcb4f5..be32029ca 100644
--- a/apps/ui/src/text-view/scrolling-and-sizing-page.xml
+++ b/apps/ui/src/text-view/scrolling-and-sizing-page.xml
@@ -6,27 +6,39 @@
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/ui/animation/index.ios.ts b/packages/core/ui/animation/index.ios.ts
index c238a7e40..38e2a00ac 100644
--- a/packages/core/ui/animation/index.ios.ts
+++ b/packages/core/ui/animation/index.ios.ts
@@ -6,6 +6,8 @@ import { View } from '../core/view';
import { AnimationBase, Properties, CubicBezierAnimationCurve } from './animation-common';
import { Trace } from '../../trace';
import { opacityProperty, backgroundColorProperty, rotateProperty, rotateXProperty, rotateYProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, heightProperty, widthProperty, PercentLength } from '../styling/style-properties';
+import { ios as iosBackground } from '../styling/background';
+import { ios as iosViewUtils, NativeScriptUIView } from '../utils';
import { ios as iosHelper } from '../../utils/native-helper';
@@ -214,7 +216,6 @@ export class Animation extends AnimationBase {
public cancel(): void {
if (!this.isPlaying) {
Trace.write('Animation is not currently playing.', Trace.categories.Animation, Trace.messageType.warn);
-
return;
}
@@ -222,8 +223,54 @@ export class Animation extends AnimationBase {
for (let i = 0; i < this._mergedPropertyAnimations.length; i++) {
const propertyAnimation = this._mergedPropertyAnimations[i];
if (propertyAnimation) {
- if (propertyAnimation.target?.nativeViewProtected?.layer) {
- propertyAnimation.target.nativeViewProtected.layer.removeAllAnimations();
+ if (propertyAnimation.target?.nativeViewProtected) {
+ const nativeView: NativeScriptUIView = propertyAnimation.target.nativeViewProtected;
+ if (nativeView.layer.mask) {
+ nativeView.layer.mask.removeAllAnimations();
+ }
+ nativeView.layer.removeAllAnimations();
+
+ // Gradient background animations
+ if (nativeView.gradientLayer) {
+ nativeView.gradientLayer.removeAllAnimations();
+ }
+
+ // Border animations
+ if (nativeView.borderLayer) {
+ if (nativeView.borderLayer.mask) {
+ nativeView.borderLayer.mask.removeAllAnimations();
+ }
+
+ const borderLayers = nativeView.borderLayer.sublayers;
+ if (borderLayers?.count) {
+ for (let i = 0, count = borderLayers.count; i < count; i++) {
+ borderLayers[i].removeAllAnimations();
+ }
+ }
+
+ nativeView.borderLayer.removeAllAnimations();
+ }
+
+ // Shadow animations
+ if (nativeView.outerShadowContainerLayer) {
+ if (nativeView.outerShadowContainerLayer.mask) {
+ nativeView.outerShadowContainerLayer.mask.removeAllAnimations();
+ }
+
+ const outerShadowLayers = nativeView.outerShadowContainerLayer.sublayers;
+ if (outerShadowLayers?.count) {
+ for (let i = 0, count = outerShadowLayers.count; i < count; i++) {
+ const shadowLayer = outerShadowLayers[i];
+
+ if (shadowLayer.mask) {
+ shadowLayer.mask.removeAllAnimations();
+ }
+ shadowLayer.removeAllAnimations();
+ }
+ }
+
+ nativeView.outerShadowContainerLayer.removeAllAnimations();
+ }
}
if (propertyAnimation._propertyResetCallback) {
propertyAnimation._propertyResetCallback(propertyAnimation._originalValue, this._valueSource);
@@ -262,14 +309,14 @@ export class Animation extends AnimationBase {
private static _getNativeAnimationArguments(animation: PropertyAnimationInfo, valueSource: 'animation' | 'keyframe'): AnimationInfo {
const view = animation.target;
const style = view.style;
- const nativeView = view.nativeViewProtected;
+ const nativeView: NativeScriptUIView = view.nativeViewProtected;
const parent = view.parent as View;
let propertyNameToAnimate = animation.property;
let subPropertyNameToAnimate;
let toValue = animation.value;
let fromValue;
- if (nativeView?.layer) {
+ if (nativeView) {
const setLocal = valueSource === 'animation';
switch (animation.property) {
@@ -279,10 +326,11 @@ export class Animation extends AnimationBase {
style[setLocal ? backgroundColorProperty.name : backgroundColorProperty.keyframe] = value;
};
fromValue = nativeView.layer.backgroundColor;
+
if (nativeView instanceof UILabel) {
nativeView.setValueForKey(UIColor.clearColor, 'backgroundColor');
}
- toValue = toValue.CGColor;
+ toValue = toValue?.ios?.CGColor;
break;
case Properties.opacity:
animation._originalValue = view.opacity;
@@ -395,7 +443,7 @@ export class Animation extends AnimationBase {
const extentY = isHeight ? asNumber : currentBounds.size.height;
fromValue = NSValue.valueWithCGRect(currentBounds);
toValue = NSValue.valueWithCGRect(CGRectMake(currentBounds.origin.x, currentBounds.origin.y, extentX, extentY));
- animation._originalValue = view.height;
+ animation._originalValue = view[isHeight ? 'height' : 'width'];
animation._propertyResetCallback = (value, valueSource) => {
const prop = isHeight ? heightProperty : widthProperty;
style[setLocal ? prop.name : prop.keyframe] = value;
@@ -438,7 +486,7 @@ export class Animation extends AnimationBase {
}
private static _createNativeAnimation(propertyAnimations: Array, index: number, playSequentially: boolean, args: AnimationInfo, animation: PropertyAnimation, valueSource: 'animation' | 'keyframe', finishedCallback: (cancelled?: boolean) => void) {
- const nativeView = animation.target.nativeViewProtected;
+ const nativeView: NativeScriptUIView = animation.target.nativeViewProtected;
let nativeAnimation;
@@ -450,8 +498,16 @@ export class Animation extends AnimationBase {
const animationDelegate = AnimationDelegateImpl.initWithFinishedCallback(finishedCallback, animation, valueSource);
nativeAnimation.setValueForKey(animationDelegate, 'delegate');
- if (nativeView?.layer) {
+ if (nativeView) {
nativeView.layer.addAnimationForKey(nativeAnimation, args.propertyNameToAnimate);
+ if (args.propertyNameToAnimate === 'bounds') {
+ this.animateNestedLayerSizeUsingBasicAnimation(nativeView, args.toValue.CGRectValue, animation, args, nativeAnimation);
+ }
+
+ // Shadow layers do not inherit from animating view layer
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.addAnimationForKey(nativeAnimation, args.propertyNameToAnimate);
+ }
}
let callback = undefined;
if (index + 1 < propertyAnimations.length) {
@@ -512,7 +568,7 @@ export class Animation extends AnimationBase {
}
private static _createNativeSpringAnimation(propertyAnimations: Array, index: number, playSequentially: boolean, args: AnimationInfo, animation: PropertyAnimationInfo, valueSource: 'animation' | 'keyframe', finishedCallback: (cancelled?: boolean) => void) {
- const nativeView = animation.target.nativeViewProtected;
+ const nativeView: NativeScriptUIView = animation.target.nativeViewProtected;
let callback = undefined;
let nextAnimation;
@@ -552,6 +608,10 @@ export class Animation extends AnimationBase {
case Properties.width:
animation._originalValue = animation.target[animation.property];
nativeView.layer.setValueForKey(args.toValue, args.propertyNameToAnimate);
+
+ // Resize background during animation
+ iosBackground.drawBackgroundVisualEffects(animation.target);
+
animation._propertyResetCallback = function (value) {
animation.target[animation.property] = value;
};
@@ -559,6 +619,12 @@ export class Animation extends AnimationBase {
case _transform:
animation._originalValue = nativeView.layer.transform;
nativeView.layer.setValueForKey(args.toValue, args.propertyNameToAnimate);
+
+ // Shadow layers do not inherit from animating view layer
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.setValueForKey(args.toValue, args.propertyNameToAnimate);
+ }
+
animation._propertyResetCallback = function (value) {
nativeView.layer.transform = value;
};
@@ -687,6 +753,182 @@ export class Animation extends AnimationBase {
return result;
}
+
+ private static animateNestedLayerSizeUsingBasicAnimation(nativeView: NativeScriptUIView, bounds: CGRect, animation: PropertyAnimation, args: AnimationInfo, nativeAnimation: CABasicAnimation) {
+ const view: View = animation.target;
+
+ // Gradient background animation
+ if (nativeView.gradientLayer) {
+ nativeView.gradientLayer.addAnimationForKey(nativeAnimation, 'bounds');
+ }
+
+ let clipPath; // This is also used for animating shadow
+
+ // Clipping mask animation
+ if (nativeView.layer.mask instanceof CAShapeLayer) {
+ let toValue;
+
+ if (nativeView.maskType === iosViewUtils.LayerMask.BORDER) {
+ toValue = iosBackground.generateNonUniformBorderOuterClipRoundedPath(view, bounds);
+ } else if (nativeView.maskType === iosViewUtils.LayerMask.CLIP_PATH) {
+ clipPath = iosBackground.generateClipPath(view, bounds);
+ toValue = clipPath;
+ } else {
+ Trace.write('Unknown mask on animating view: ' + view, Trace.categories.Animation, Trace.messageType.info);
+ }
+
+ if (toValue) {
+ nativeView.layer.mask.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'path',
+ fromValue: nativeView.layer.mask.path,
+ toValue,
+ },
+ animation
+ ),
+ 'path'
+ );
+ }
+ }
+
+ // Border animations (uniform and non-uniform)
+ if (nativeView.hasNonUniformBorder) {
+ if (nativeView.borderLayer) {
+ const innerClipPath = iosBackground.generateNonUniformBorderInnerClipRoundedPath(animation.target, bounds);
+
+ if (nativeView.hasNonUniformBorderColor) {
+ const borderMask = nativeView.borderLayer.mask;
+ if (borderMask instanceof CAShapeLayer) {
+ borderMask.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'path',
+ fromValue: borderMask.path,
+ toValue: innerClipPath,
+ },
+ animation
+ ),
+ 'path'
+ );
+ }
+
+ const borderLayers = nativeView.borderLayer.sublayers;
+ if (borderLayers?.count) {
+ const paths = iosBackground.generateNonUniformMultiColorBorderRoundedPaths(animation.target, bounds);
+
+ for (let i = 0, count = borderLayers.count; i < count; i++) {
+ const layer = nativeView.borderLayer.sublayers[i];
+ if (layer instanceof CAShapeLayer) {
+ layer.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'path',
+ fromValue: layer.path,
+ toValue: paths[i],
+ },
+ animation
+ ),
+ 'path'
+ );
+ }
+ }
+ }
+ } else {
+ nativeView.borderLayer.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'path',
+ fromValue: nativeView.borderLayer.path,
+ toValue: innerClipPath,
+ },
+ animation
+ ),
+ 'path'
+ );
+ }
+ }
+ } else {
+ // TODO: Animate border width when borders get support for percentage values
+ // Uniform corner radius also relies on view size
+ if (nativeView.layer.cornerRadius) {
+ nativeView.layer.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'cornerRadius',
+ fromValue: nativeView.layer.cornerRadius,
+ toValue: iosBackground.getUniformBorderRadius(animation.target, bounds),
+ },
+ animation
+ ),
+ 'cornerRadius'
+ );
+ }
+ }
+
+ // Shadow layers do not inherit from animating view layer
+ if (nativeView.outerShadowContainerLayer) {
+ const shadowClipMask = nativeView.outerShadowContainerLayer.mask;
+
+ // This is for animating view clip path on shadow
+ if (clipPath && shadowClipMask instanceof CAShapeLayer) {
+ shadowClipMask.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'path',
+ fromValue: shadowClipMask.path,
+ toValue: clipPath,
+ },
+ animation
+ ),
+ 'path'
+ );
+ }
+
+ const outerShadowLayers = nativeView.outerShadowContainerLayer.sublayers;
+ if (outerShadowLayers?.count) {
+ const { maskPath, shadowPath } = iosBackground.generateShadowLayerPaths(view, bounds);
+
+ for (let i = 0, count = outerShadowLayers.count; i < count; i++) {
+ const shadowLayer = outerShadowLayers[i];
+
+ shadowLayer.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'shadowPath',
+ fromValue: shadowLayer.shadowPath,
+ toValue: shadowPath,
+ },
+ animation
+ ),
+ 'shadowPath'
+ );
+
+ if (shadowLayer.mask instanceof CAShapeLayer) {
+ shadowLayer.mask.addAnimationForKey(
+ this._createBasicAnimation(
+ {
+ ...args,
+ propertyNameToAnimate: 'path',
+ fromValue: shadowLayer.mask.path,
+ toValue: maskPath,
+ },
+ animation
+ ),
+ 'path'
+ );
+ }
+ }
+ }
+ }
+ }
}
export function _getTransformMismatchErrorMessage(view: View): string {
diff --git a/packages/core/ui/button/index.ios.ts b/packages/core/ui/button/index.ios.ts
index 1c049de8e..b67e8ca69 100644
--- a/packages/core/ui/button/index.ios.ts
+++ b/packages/core/ui/button/index.ios.ts
@@ -1,7 +1,7 @@
import { ControlStateChangeListener } from '../core/control-state-change';
import { ButtonBase } from './button-common';
import { View, PseudoClassHandler } from '../core/view';
-import { backgroundColorProperty, borderTopWidthProperty, borderRightWidthProperty, borderBottomWidthProperty, borderLeftWidthProperty, paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty } from '../styling/style-properties';
+import { borderTopWidthProperty, borderRightWidthProperty, borderBottomWidthProperty, borderLeftWidthProperty, paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty } from '../styling/style-properties';
import { textAlignmentProperty, whiteSpaceProperty, textOverflowProperty } from '../text-base';
import { layout } from '../../utils';
import { CoreTypes } from '../../core-types';
@@ -56,14 +56,6 @@ export class Button extends ButtonBase {
}
}
- [backgroundColorProperty.getDefault](): UIColor {
- return this.nativeViewProtected.backgroundColor;
- }
-
- [backgroundColorProperty.setNative](value: UIColor | Color) {
- this.nativeViewProtected.backgroundColor = value instanceof Color ? value.ios : value;
- }
-
[borderTopWidthProperty.getDefault](): CoreTypes.LengthType {
return {
value: this.nativeViewProtected.contentEdgeInsets.top,
diff --git a/packages/core/ui/core/view-base/index.ts b/packages/core/ui/core/view-base/index.ts
index 02772c567..8ed2bdc1a 100644
--- a/packages/core/ui/core/view-base/index.ts
+++ b/packages/core/ui/core/view-base/index.ts
@@ -957,6 +957,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
* Clean up references to the native view.
*/
public disposeNativeView() {
+ // Unset those values so that view will check for resize after being removed and re-added to view-tree
+ this._oldLeft = 0;
+ this._oldTop = 0;
+ this._oldRight = 0;
+ this._oldBottom = 0;
+
this.notify({
eventName: ViewBase.disposeNativeViewEvent,
object: this,
diff --git a/packages/core/ui/core/view/index.android.ts b/packages/core/ui/core/view/index.android.ts
index 86f8406c2..04fbf6938 100644
--- a/packages/core/ui/core/view/index.android.ts
+++ b/packages/core/ui/core/view/index.android.ts
@@ -24,7 +24,7 @@ import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilit
import { AccessibilityLiveRegion, AccessibilityRole, AndroidAccessibilityEvent, isAccessibilityServiceEnabled, sendAccessibilityEvent, updateAccessibilityProperties, updateContentDescription, AccessibilityState } from '../../../accessibility';
import * as Utils from '../../../utils';
import { SDK_VERSION } from '../../../utils/constants';
-import { CSSShadow } from '../../styling/css-shadow';
+import { BoxShadow } from '../../styling/box-shadow';
import { _setAndroidFragmentTransitions, _getAnimatedEntries, _updateTransitions, _reverseTransitions, _clearEntry, _clearFragment, addNativeTransitionListener } from '../../frame/fragment.transitions';
export * from './view-common';
@@ -1150,15 +1150,15 @@ export class View extends ViewCommon {
}
}
- protected _drawBoxShadow(boxShadow: CSSShadow) {
+ protected _drawBoxShadow(boxShadow: BoxShadow) {
const nativeView = this.nativeViewProtected;
const config = {
shadowColor: boxShadow.color.android,
cornerRadius: Length.toDevicePixels(this.borderRadius as CoreTypes.LengthType, 0.0),
- spreadRadius: Length.toDevicePixels(boxShadow.spreadRadius, 0.0),
- blurRadius: Length.toDevicePixels(boxShadow.blurRadius, 0.0),
- offsetX: Length.toDevicePixels(boxShadow.offsetX, 0.0),
- offsetY: Length.toDevicePixels(boxShadow.offsetY, 0.0),
+ spreadRadius: boxShadow.spreadRadius,
+ blurRadius: boxShadow.blurRadius,
+ offsetX: boxShadow.offsetX,
+ offsetY: boxShadow.offsetY,
};
org.nativescript.widgets.Utils.drawBoxShadow(nativeView, JSON.stringify(config));
}
diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts
index 583249209..c462b8e1c 100644
--- a/packages/core/ui/core/view/index.d.ts
+++ b/packages/core/ui/core/view/index.d.ts
@@ -7,7 +7,7 @@ import { GestureTypes, GesturesObserver } from '../../gestures';
import { LinearGradient } from '../../styling/linear-gradient';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AccessibilityEventOptions } from '../../../accessibility/accessibility-types';
import { CoreTypes } from '../../../core-types';
-import { CSSShadow } from '../../styling/css-shadow';
+import { ShadowCSSValues } from '../../styling/css-shadow';
import { ViewCommon } from './view-common';
export * from './view-common';
@@ -332,7 +332,7 @@ export abstract class View extends ViewCommon {
/**
* Gets or sets the box shadow of the view.
*/
- boxShadow: string | CSSShadow;
+ boxShadow: string | ShadowCSSValues;
/**
* Gets or sets the minimum width the view may grow to.
diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts
index 6aa6a451a..03ef018a4 100644
--- a/packages/core/ui/core/view/index.ios.ts
+++ b/packages/core/ui/core/view/index.ios.ts
@@ -8,8 +8,8 @@ import { Trace } from '../../../trace';
import { layout, iOSNativeHelper } from '../../../utils';
import { isNumber } from '../../../utils/types';
import { IOSHelper } from './view-helper';
-import { ios as iosBackground, Background } from '../../styling/background';
-import { perspectiveProperty, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty, clipPathProperty } from '../../styling/style-properties';
+import { ios as iosBackground, Background, BackgroundClearFlags } from '../../styling/background';
+import { perspectiveProperty, visibilityProperty, opacityProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, zIndexProperty, backgroundInternalProperty } from '../../styling/style-properties';
import { profile } from '../../../profiling';
import { accessibilityEnabledProperty, accessibilityHiddenProperty, accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityLanguageProperty, accessibilityLiveRegionProperty, accessibilityMediaSessionProperty, accessibilityRoleProperty, accessibilityStateProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { IOSPostAccessibilityNotificationType, isAccessibilityServiceEnabled, updateAccessibilityProperties, AccessibilityEventOptions, AccessibilityRole, AccessibilityState } from '../../../accessibility';
@@ -17,6 +17,7 @@ import { CoreTypes } from '../../../core-types';
import type { ModalTransition } from '../../transition/modal-transition';
import { SharedTransition } from '../../transition/shared-transition';
import { GestureStateTypes, PanGestureEventData } from '../../gestures';
+import { NativeScriptUIView } from '../../utils';
export * from './view-common';
// helpers (these are okay re-exported here)
@@ -72,6 +73,12 @@ export class View extends ViewCommon implements ViewDefinition {
this._isLaidOut = false;
this._hasTransform = false;
this._hasPendingTransform = false;
+
+ // Make sure shadows get removed
+ this.style.backgroundInternal.clearFlags |= BackgroundClearFlags.CLEAR_BOX_SHADOW;
+
+ // Perform background cleanup
+ iosBackground.clearBackgroundVisualEffects(this);
}
public requestLayout(): void {
@@ -116,7 +123,8 @@ export class View extends ViewCommon implements ViewDefinition {
this.layoutNativeView(left, top, right, bottom);
}
- if (boundsChanged || (this._privateFlags & PFLAG_LAYOUT_REQUIRED) === PFLAG_LAYOUT_REQUIRED) {
+ const needsLayout = boundsChanged || (this._privateFlags & PFLAG_LAYOUT_REQUIRED) === PFLAG_LAYOUT_REQUIRED;
+ if (needsLayout) {
let position = { left, top, right, bottom };
if (this.nativeViewProtected && majorVersion > 10) {
// on iOS 11+ it is possible to have a changed layout frame due to safe area insets
@@ -129,7 +137,7 @@ export class View extends ViewCommon implements ViewDefinition {
this._privateFlags &= ~PFLAG_LAYOUT_REQUIRED;
}
- this.updateBackground(sizeChanged);
+ this.updateBackground(sizeChanged, needsLayout);
if (this._hasPendingTransform) {
this.updateNativeTransform();
this._hasPendingTransform = false;
@@ -137,12 +145,36 @@ export class View extends ViewCommon implements ViewDefinition {
this._privateFlags &= ~PFLAG_FORCE_LAYOUT;
}
- private updateBackground(sizeChanged: boolean): void {
+ private updateBackground(sizeChanged: boolean, needsLayout: boolean): void {
if (sizeChanged) {
this._onSizeChanged();
} else if (this._nativeBackgroundState === 'invalid') {
const background = this.style.backgroundInternal;
this._redrawNativeBackground(background);
+ } else {
+ // Update layers that don't belong to view's layer (e.g. shadow layers)
+ if (needsLayout) {
+ this.layoutOuterShadows();
+ }
+ }
+ }
+
+ private layoutOuterShadows(): void {
+ const nativeView: NativeScriptUIView = this.nativeViewProtected;
+ if (nativeView) {
+ const frame = nativeView.frame;
+ const needsUpdate: boolean = nativeView.outerShadowContainerLayer != null;
+
+ if (needsUpdate) {
+ CATransaction.setDisableActions(true);
+
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.bounds = nativeView.bounds;
+ nativeView.outerShadowContainerLayer.position = CGPointMake(frame.origin.x + frame.size.width / 2, frame.origin.y + frame.size.height / 2);
+ }
+
+ CATransaction.setDisableActions(false);
+ }
}
}
@@ -272,8 +304,8 @@ export class View extends ViewCommon implements ViewDefinition {
this._privateFlags &= ~PFLAG_FORCE_LAYOUT;
this.setMeasuredDimension(width, height);
- const { sizeChanged } = this._setCurrentLayoutBounds(left, top, right, bottom);
- this.updateBackground(sizeChanged);
+ const { boundsChanged, sizeChanged } = this._setCurrentLayoutBounds(left, top, right, bottom);
+ this.updateBackground(sizeChanged, boundsChanged);
this._privateFlags &= ~PFLAG_LAYOUT_REQUIRED;
}
@@ -365,16 +397,11 @@ export class View extends ViewCommon implements ViewDefinition {
}
const background = this.style.backgroundInternal;
- const backgroundDependsOnSize = (background.image && background.image !== 'none') || !background.hasUniformBorder() || background.hasBorderRadius();
+ const backgroundDependsOnSize = (background.image && background.image !== 'none') || background.clipPath || !background.hasUniformBorder() || background.hasBorderRadius() || background.hasBoxShadow();
if (this._nativeBackgroundState === 'invalid' || (this._nativeBackgroundState === 'drawn' && backgroundDependsOnSize)) {
this._redrawNativeBackground(background);
}
-
- const clipPath = this.style.clipPath;
- if (clipPath !== '' && this[clipPathProperty.setNative]) {
- this[clipPathProperty.setNative](clipPath);
- }
}
public updateNativeTransform() {
@@ -386,6 +413,7 @@ export class View extends ViewCommon implements ViewDefinition {
const scaleX = this.scaleX || 1e-6;
const scaleY = this.scaleY || 1e-6;
const perspective = this.perspective || 300;
+ const nativeView: NativeScriptUIView = this.nativeViewProtected;
let transform = new CATransform3D(CATransform3DIdentity);
@@ -397,13 +425,24 @@ export class View extends ViewCommon implements ViewDefinition {
transform = CATransform3DTranslate(transform, this.translateX, this.translateY, 0);
transform = iOSNativeHelper.applyRotateTransform(transform, this.rotateX, this.rotateY, this.rotate);
transform = CATransform3DScale(transform, scaleX, scaleY, 1);
- if (!CATransform3DEqualToTransform(this.nativeViewProtected.layer.transform, transform)) {
+
+ const needsTransform: boolean = !CATransform3DEqualToTransform(this.nativeViewProtected.layer.transform, transform) || (nativeView.outerShadowContainerLayer && !CATransform3DEqualToTransform(nativeView.outerShadowContainerLayer.transform, transform));
+
+ if (needsTransform) {
const updateSuspended = this._isPresentationLayerUpdateSuspended();
if (!updateSuspended) {
CATransaction.begin();
}
+ // Disable CALayer animatable property changes
+ CATransaction.setDisableActions(true);
+
this.nativeViewProtected.layer.transform = transform;
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.transform = transform;
+ }
this._hasTransform = this.nativeViewProtected && !CATransform3DEqualToTransform(this.nativeViewProtected.transform3D, CATransform3DIdentity);
+
+ CATransaction.setDisableActions(false);
if (!updateSuspended) {
CATransaction.commit();
}
@@ -721,29 +760,45 @@ export class View extends ViewCommon implements ViewDefinition {
return this.nativeViewProtected.hidden ? CoreTypes.Visibility.collapse : CoreTypes.Visibility.visible;
}
[visibilityProperty.setNative](value: CoreTypes.VisibilityType) {
+ const nativeView: NativeScriptUIView = this.nativeViewProtected;
+
switch (value) {
case CoreTypes.Visibility.visible:
- this.nativeViewProtected.hidden = false;
+ nativeView.hidden = false;
break;
case CoreTypes.Visibility.hidden:
case CoreTypes.Visibility.collapse:
- this.nativeViewProtected.hidden = true;
+ nativeView.hidden = true;
break;
default:
throw new Error(`Invalid visibility value: ${value}. Valid values are: "${CoreTypes.Visibility.visible}", "${CoreTypes.Visibility.hidden}", "${CoreTypes.Visibility.collapse}".`);
}
+
+ // Apply visibility value to shadows as well
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.hidden = nativeView.hidden;
+ }
}
[opacityProperty.getDefault](): number {
return this.nativeViewProtected.alpha;
}
[opacityProperty.setNative](value: number) {
- const nativeView = this.nativeViewProtected;
+ const nativeView: NativeScriptUIView = this.nativeViewProtected;
const updateSuspended = this._isPresentationLayerUpdateSuspended();
if (!updateSuspended) {
CATransaction.begin();
}
+ // Disable CALayer animatable property changes
+ CATransaction.setDisableActions(true);
+
nativeView.alpha = value;
+ // Apply opacity value to shadows as well
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.opacity = value;
+ }
+
+ CATransaction.setDisableActions(false);
if (!updateSuspended) {
CATransaction.commit();
}
@@ -899,18 +954,22 @@ export class View extends ViewCommon implements ViewDefinition {
if (!updateSuspended) {
CATransaction.begin();
}
- const view = this.nativeViewProtected;
- if (view) {
+ // Disable CALayer animatable property changes
+ CATransaction.setDisableActions(true);
+
+ const nativeView = this.nativeViewProtected;
+ if (nativeView) {
if (value instanceof UIColor) {
- view.backgroundColor = value;
+ nativeView.backgroundColor = value;
} else {
iosBackground.createBackgroundUIColor(this, (color: UIColor) => {
- view.backgroundColor = color;
+ nativeView.backgroundColor = color;
});
this._setNativeClipToBounds();
}
}
+ CATransaction.setDisableActions(false);
if (!updateSuspended) {
CATransaction.commit();
}
@@ -922,7 +981,7 @@ export class View extends ViewCommon implements ViewDefinition {
const view = this.nativeViewProtected;
if (view) {
const backgroundInternal = this.style.backgroundInternal;
- view.clipsToBounds = (view instanceof UIScrollView || backgroundInternal.hasBorderWidth() || backgroundInternal.hasBorderRadius()) && !backgroundInternal.hasBoxShadow();
+ view.clipsToBounds = view instanceof UIScrollView || backgroundInternal.hasBorderWidth() || backgroundInternal.hasBorderRadius();
}
}
diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts
index 601f579a6..58adce944 100644
--- a/packages/core/ui/core/view/view-common.ts
+++ b/packages/core/ui/core/view/view-common.ts
@@ -26,7 +26,7 @@ import * as am from '../../animation';
import { AccessibilityEventOptions, AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait } from '../../../accessibility/accessibility-types';
import { accessibilityHintProperty, accessibilityIdentifierProperty, accessibilityLabelProperty, accessibilityValueProperty, accessibilityIgnoresInvertColorsProperty } from '../../../accessibility/accessibility-properties';
import { accessibilityBlurEvent, accessibilityFocusChangedEvent, accessibilityFocusEvent, accessibilityPerformEscapeEvent, getCurrentFontScale } from '../../../accessibility';
-import { CSSShadow } from '../../styling/css-shadow';
+import { ShadowCSSValues } from '../../styling/css-shadow';
import { SharedTransition, SharedTransitionInteractiveOptions } from '../../transition/shared-transition';
// helpers (these are okay re-exported here)
@@ -646,10 +646,10 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.style.backgroundRepeat = value;
}
- get boxShadow(): CSSShadow {
+ get boxShadow(): ShadowCSSValues {
return this.style.boxShadow;
}
- set boxShadow(value: CSSShadow) {
+ set boxShadow(value: ShadowCSSValues) {
this.style.boxShadow = value;
}
diff --git a/packages/core/ui/label/index.ios.ts b/packages/core/ui/label/index.ios.ts
index 680e06d6f..bf35b3269 100644
--- a/packages/core/ui/label/index.ios.ts
+++ b/packages/core/ui/label/index.ios.ts
@@ -150,14 +150,17 @@ export class Label extends TextBase implements LabelDefinition {
_redrawNativeBackground(value: UIColor | Background): void {
if (value instanceof Background) {
- ios.createBackgroundUIColor(
- this,
- (color: UIColor) => {
- const cgColor = color ? color.CGColor : null;
- this.nativeTextViewProtected.layer.backgroundColor = cgColor;
- },
- true
- );
+ const nativeView = this.nativeTextViewProtected;
+ if (nativeView) {
+ ios.createBackgroundUIColor(
+ this,
+ (color: UIColor) => {
+ const cgColor = color ? color.CGColor : null;
+ nativeView.layer.backgroundColor = cgColor;
+ },
+ true
+ );
+ }
}
this._setNativeClipToBounds();
diff --git a/packages/core/ui/layouts/root-layout/index.ios.ts b/packages/core/ui/layouts/root-layout/index.ios.ts
index 461963bbb..b96f92960 100644
--- a/packages/core/ui/layouts/root-layout/index.ios.ts
+++ b/packages/core/ui/layouts/root-layout/index.ios.ts
@@ -41,7 +41,10 @@ export class RootLayout extends RootLayoutBase {
if (options.color !== this._currentGradient) {
this._currentGradient = options.color;
const parsedGradient = parseLinearGradient(options.color);
- this._gradientLayer = iosViewUtils.drawGradient(view.nativeViewProtected, LinearGradient.parse(parsedGradient.value), 0);
+
+ this._gradientLayer = CAGradientLayer.new();
+ iosViewUtils.drawGradient(view.nativeViewProtected, this._gradientLayer, LinearGradient.parse(parsedGradient.value));
+ view.nativeViewProtected.layer.insertSublayerAtIndex(this._gradientLayer, 0);
}
}
UIView.animateWithDurationAnimationsCompletion(
@@ -92,7 +95,10 @@ export class RootLayout extends RootLayoutBase {
protected _cleanupPlatformShadeCover(): void {
this._currentGradient = null;
- this._gradientLayer = null;
+ if (this._gradientLayer != null) {
+ this._gradientLayer.removeFromSuperlayer();
+ this._gradientLayer = null;
+ }
}
private _applyAnimationProperties(view: View, shadeCoverAnimation: TransitionAnimation): void {
diff --git a/packages/core/ui/list-picker/index.ios.ts b/packages/core/ui/list-picker/index.ios.ts
index 6d6455d24..fbb11fbaf 100644
--- a/packages/core/ui/list-picker/index.ios.ts
+++ b/packages/core/ui/list-picker/index.ios.ts
@@ -1,6 +1,6 @@
import { ListPickerBase, selectedIndexProperty, itemsProperty, ItemsSource } from './list-picker-common';
import { Color } from '../../color';
-import { backgroundColorProperty, colorProperty } from '../styling/style-properties';
+import { colorProperty } from '../styling/style-properties';
import { profile } from '../../profiling';
export * from './list-picker-common';
@@ -52,13 +52,6 @@ export class ListPicker extends ListPickerBase {
selectedIndexProperty.coerce(this);
}
- [backgroundColorProperty.getDefault](): UIColor {
- return this.ios.backgroundColor;
- }
- [backgroundColorProperty.setNative](value: UIColor | Color) {
- this.ios.backgroundColor = value instanceof Color ? value.ios : value;
- }
-
[colorProperty.getDefault](): UIColor {
return this.ios.tintColor;
}
diff --git a/packages/core/ui/styling/background-common.ts b/packages/core/ui/styling/background-common.ts
index 10929e553..4d52da76c 100644
--- a/packages/core/ui/styling/background-common.ts
+++ b/packages/core/ui/styling/background-common.ts
@@ -2,7 +2,7 @@ import { CoreTypes } from '../../core-types';
import { LinearGradient } from './linear-gradient';
// Types.
import { Color } from '../../color';
-import { CSSShadow } from './css-shadow';
+import { BoxShadow } from './box-shadow';
/**
* Flags used to hint the background handler if it has to clear a specific property
@@ -40,7 +40,7 @@ export class Background {
public borderBottomLeftRadius = 0;
public borderBottomRightRadius = 0;
public clipPath: string;
- public boxShadow: CSSShadow;
+ public boxShadow: BoxShadow;
public clearFlags: number = BackgroundClearFlags.NONE;
private clone(): Background {
@@ -199,7 +199,7 @@ export class Background {
return clone;
}
- public withBoxShadow(value: CSSShadow): Background {
+ public withBoxShadow(value: BoxShadow): Background {
const clone = this.clone();
clone.boxShadow = value;
if (!value) {
@@ -266,6 +266,10 @@ export class Background {
return this.borderTopLeftRadius > 0 || this.borderTopRightRadius > 0 || this.borderBottomRightRadius > 0 || this.borderBottomLeftRadius > 0;
}
+ public hasBorder(): boolean {
+ return (this.hasBorderColor() && this.hasBorderWidth()) || this.hasBorderRadius();
+ }
+
public hasUniformBorderColor(): boolean {
return Color.equals(this.borderTopColor, this.borderRightColor) && Color.equals(this.borderTopColor, this.borderBottomColor) && Color.equals(this.borderTopColor, this.borderLeftColor);
}
@@ -310,7 +314,7 @@ export class Background {
return !!this.boxShadow;
}
- public getBoxShadow(): CSSShadow {
+ public getBoxShadow(): BoxShadow {
return this.boxShadow;
}
diff --git a/packages/core/ui/styling/background.d.ts b/packages/core/ui/styling/background.d.ts
index 797a2dc14..086d64a94 100644
--- a/packages/core/ui/styling/background.d.ts
+++ b/packages/core/ui/styling/background.d.ts
@@ -2,7 +2,7 @@ import { Color } from '../../color';
import { View } from '../core/view';
import { BackgroundRepeat } from '../../css/parser';
import { LinearGradient } from '../styling/linear-gradient';
-import { CSSShadow } from './css-shadow';
+import { BoxShadow } from './box-shadow';
export * from './background-common';
@@ -32,7 +32,7 @@ export enum CacheMode {
// public borderBottomRightRadius: number;
// public borderBottomLeftRadius: number;
// public clipPath: string;
-// public boxShadow: string | CSSShadow;
+// public boxShadow: string | BoxShadow;
// public clearFlags: number;
// public withColor(value: Color): Background;
@@ -53,7 +53,7 @@ export enum CacheMode {
// public withBorderBottomRightRadius(value: number): Background;
// public withBorderBottomLeftRadius(value: number): Background;
// public withClipPath(value: string): Background;
-// public withBoxShadow(value: CSSShadow): Background;
+// public withBoxShadow(value: BoxShadow): Background;
// public isEmpty(): boolean;
@@ -62,6 +62,7 @@ export enum CacheMode {
// public hasBorderColor(): boolean;
// public hasBorderWidth(): boolean;
// public hasBorderRadius(): boolean;
+// public hasBorder(): boolean;
// public hasUniformBorderColor(): boolean;
// public hasUniformBorderWidth(): boolean;
// public hasUniformBorderRadius(): boolean;
@@ -70,11 +71,19 @@ export enum CacheMode {
// public getUniformBorderWidth(): number;
// public getUniformBorderRadius(): number;
// public hasBoxShadow(): boolean;
-// public getBoxShadow(): CSSShadow;
+// public getBoxShadow(): BoxShadow;
// }
export namespace ios {
export function createBackgroundUIColor(view: View, callback: (uiColor: any /* UIColor */) => void, flip?: boolean): void;
+ export function drawBackgroundVisualEffects(view: View): void;
+ export function clearBackgroundVisualEffects(view: View): void;
+ export function generateClipPath(view: View, bounds: CGRect): any;
+ export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any };
+ export function getUniformBorderRadius(view: View, bounds: CGRect): number;
+ export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: CGRect): any;
+ export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: CGRect): any;
+ export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: CGRect): Array;
}
export namespace ad {
diff --git a/packages/core/ui/styling/background.ios.ts b/packages/core/ui/styling/background.ios.ts
index 8aeba7e9c..a6e2756b6 100644
--- a/packages/core/ui/styling/background.ios.ts
+++ b/packages/core/ui/styling/background.ios.ts
@@ -4,24 +4,43 @@ import { Background as BackgroundDefinition } from './background';
import { View, Point } from '../core/view';
import { LinearGradient } from './linear-gradient';
import { Color } from '../../color';
-import { iOSNativeHelper, isDataURI, isFileOrResourcePath, layout } from '../../utils';
+import { Screen } from '../../platform';
+import { isDataURI, isFileOrResourcePath, layout } from '../../utils';
+import { extendPointsToTargetY } from '../../utils/number-utils';
import { ios as iosViewUtils, NativeScriptUIView } from '../utils';
import { ImageSource } from '../../image-source';
import { CSSValue, parse as cssParse } from '../../css-value';
-import { CSSShadow } from './css-shadow';
+import { BoxShadow } from './box-shadow';
import { Length } from './style-properties';
import { BackgroundClearFlags } from './background-common';
export * from './background-common';
-interface Rect {
- left: number;
+interface Position {
top: number;
right: number;
bottom: number;
+ left: number;
+}
+
+interface BackgroundDrawParams {
+ repeatX: boolean;
+ repeatY: boolean;
+ posX: number;
+ posY: number;
+ sizeX?: number;
+ sizeY?: number;
+}
+
+interface CappedOuterRadii {
+ topLeft: number;
+ topRight: number;
+ bottomLeft: number;
+ bottomRight: number;
}
const clearCGColor = UIColor.clearColor.CGColor;
+const uriPattern = /url\(('|")(.*?)\1\)/;
const symbolUrl = Symbol('backgroundImageUrl');
export enum CacheMode {
@@ -37,63 +56,305 @@ export namespace ios {
return;
}
- if (background.clearFlags & BackgroundClearFlags.CLEAR_BOX_SHADOW) {
- // clear box shadow if it has been removed!
- view.setProperty('clipToBounds', true);
- clearBoxShadow(nativeView);
- }
+ // Unset this in case another layer handles background color (e.g. gradient)
+ nativeView.layer.backgroundColor = null;
- if (nativeView.hasNonUniformBorder) {
- unsubscribeFromScrollNotifications(view);
- clearNonUniformBorders(nativeView);
- }
+ // Cleanup of previous values
+ clearBackgroundVisualEffects(view);
- iosViewUtils.clearGradient(nativeView);
+ // Borders, shadows, etc
+ drawBackgroundVisualEffects(view);
+
+ if (!background.image) {
+ callback(background?.color?.ios);
+ } else {
+ if (!(background.image instanceof LinearGradient)) {
+ setUIColorFromImage(view, nativeView, callback, flip);
+ }
+ }
+ }
+
+ export function drawBackgroundVisualEffects(view: View): void {
+ const background = view.style.backgroundInternal;
+ const nativeView = view.nativeViewProtected;
+ const layer: CALayer = nativeView.layer;
+
+ let needsLayerAdjustmentOnScroll = false;
+
+ // Add new gradient layer or update existing one
if (background.image instanceof LinearGradient) {
- iosViewUtils.drawGradient(nativeView, background.image);
+ if (!nativeView.gradientLayer) {
+ nativeView.gradientLayer = CAGradientLayer.new();
+ layer.insertSublayerAtIndex(nativeView.gradientLayer, 0);
+ }
+ iosViewUtils.drawGradient(nativeView, nativeView.gradientLayer, background.image);
+ needsLayerAdjustmentOnScroll = true;
}
- const hasNonUniformBorderWidths = background.hasBorderWidth() && !background.hasUniformBorder();
- const hasNonUniformBorderRadiuses = background.hasBorderRadius() && !background.hasUniformBorderRadius();
- if (background.hasUniformBorderColor() && (hasNonUniformBorderWidths || hasNonUniformBorderRadiuses)) {
- drawUniformColorNonUniformBorders(nativeView, background);
- subscribeForScrollNotifications(view);
- } else if (background.hasUniformBorder()) {
- const layer = nativeView.layer;
+ // Initialize clipping mask (usually for clip-path and non-uniform rounded borders)
+ maskLayerIfNeeded(nativeView, background);
+
+ if (background.hasUniformBorder()) {
const borderColor = background.getUniformBorderColor();
- layer.borderColor = !borderColor ? undefined : borderColor.ios.CGColor;
+ layer.borderColor = borderColor?.ios?.CGColor;
layer.borderWidth = layout.toDeviceIndependentPixels(background.getUniformBorderWidth());
- const renderSize = view.getActualSize() || { width: 0, height: 0 };
- const cornerRadius = layout.toDeviceIndependentPixels(background.getUniformBorderRadius());
- layer.cornerRadius = Math.min(Math.min(renderSize.width / 2, renderSize.height / 2), cornerRadius);
+ layer.cornerRadius = getUniformBorderRadius(view, layer.bounds);
} else {
- drawNoRadiusNonUniformBorders(nativeView, background);
- subscribeForScrollNotifications(view);
+ drawNonUniformBorders(nativeView, background);
+ needsLayerAdjustmentOnScroll = true;
}
- // Clip-path should be called after borders are applied.
- // It will eventually move them to different layer if uniform.
- if (background.clipPath) {
- drawClipPath(nativeView, background);
- }
-
- if (!background.image || background.image instanceof LinearGradient) {
- const uiColor = background.color ? background.color.ios : undefined;
- callback(uiColor);
- } else {
- setUIColorFromImage(view, nativeView, callback, flip);
+ // Clip-path should be called after borders are applied
+ if (nativeView.maskType === iosViewUtils.LayerMask.CLIP_PATH && layer.mask instanceof CAShapeLayer) {
+ layer.mask.path = generateClipPath(view, layer.bounds);
}
if (background.hasBoxShadow()) {
- drawBoxShadow(nativeView, view, background.getBoxShadow(), background);
+ drawBoxShadow(view);
+ needsLayerAdjustmentOnScroll = true;
}
- // reset clear flags
+ if (needsLayerAdjustmentOnScroll) {
+ registerAdjustLayersOnScrollListener(view);
+ }
+ }
+
+ export function clearBackgroundVisualEffects(view: View): void {
+ const nativeView: NativeScriptUIView = view.nativeViewProtected;
+ if (!nativeView) {
+ return;
+ }
+
+ const background: BackgroundDefinition = view.style.backgroundInternal;
+ const hasGradientBackground: boolean = background.image && background.image instanceof LinearGradient;
+
+ // Remove mask if there is no clip path or non-uniform border with radius
+ let needsMask;
+ switch (nativeView.maskType) {
+ case iosViewUtils.LayerMask.BORDER:
+ needsMask = !background.hasUniformBorder() && background.hasBorderRadius();
+ break;
+ case iosViewUtils.LayerMask.CLIP_PATH:
+ needsMask = !!background.clipPath;
+ break;
+ default:
+ needsMask = false;
+ break;
+ }
+
+ if (!needsMask) {
+ clearLayerMask(nativeView, background);
+ }
+
+ // Clear box shadow if it's no longer needed
+ if (background.clearFlags & BackgroundClearFlags.CLEAR_BOX_SHADOW) {
+ clearBoxShadow(nativeView);
+ }
+
+ // Non-uniform borders cleanup
+ if (nativeView.hasNonUniformBorder) {
+ if (nativeView.hasNonUniformBorderColor && background.hasUniformBorderColor()) {
+ clearNonUniformColorBorders(nativeView);
+ }
+
+ if (background.hasUniformBorder()) {
+ clearNonUniformBorders(nativeView);
+ }
+ }
+
+ if (nativeView.gradientLayer && !hasGradientBackground) {
+ nativeView.gradientLayer.removeFromSuperlayer();
+ nativeView.gradientLayer = null;
+ }
+
+ // Force unset scroll listener
+ unregisterAdjustLayersOnScrollListener(view);
+
+ // Reset clear flags
background.clearFlags = BackgroundClearFlags.NONE;
}
+
+ export function generateShadowLayerPaths(view: View, bounds: CGRect): { maskPath: any; shadowPath: any } {
+ const background = view.style.backgroundInternal;
+ const nativeView = view.nativeViewProtected;
+ const layer = nativeView.layer;
+
+ const boxShadow: BoxShadow = background.getBoxShadow();
+ const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
+
+ const { width, height } = bounds.size;
+
+ let innerPath, shadowPath;
+
+ // Generate more detailed paths if view has border radius
+ if (background.hasBorderRadius()) {
+ if (background.hasUniformBorder()) {
+ const cornerRadius = layer.cornerRadius;
+ const cappedRadius = getBorderCapRadius(cornerRadius, width / 2, height / 2);
+ const cappedOuterRadii: CappedOuterRadii = {
+ topLeft: cappedRadius,
+ topRight: cappedRadius,
+ bottomLeft: cappedRadius,
+ bottomRight: cappedRadius,
+ };
+ const cappedOuterRadiiWithSpread: CappedOuterRadii = {
+ topLeft: cappedRadius + spreadRadius,
+ topRight: cappedRadius + spreadRadius,
+ bottomLeft: cappedRadius + spreadRadius,
+ bottomRight: cappedRadius + spreadRadius,
+ };
+
+ innerPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadii);
+ shadowPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadiiWithSpread, spreadRadius);
+ } else {
+ const outerTopLeftRadius = layout.toDeviceIndependentPixels(background.borderTopLeftRadius);
+ const outerTopRightRadius = layout.toDeviceIndependentPixels(background.borderTopRightRadius);
+ const outerBottomRightRadius = layout.toDeviceIndependentPixels(background.borderBottomRightRadius);
+ const outerBottomLeftRadius = layout.toDeviceIndependentPixels(background.borderBottomLeftRadius);
+
+ const topRadii = outerTopLeftRadius + outerTopRightRadius;
+ const rightRadii = outerTopRightRadius + outerBottomRightRadius;
+ const bottomRadii = outerBottomRightRadius + outerBottomLeftRadius;
+ const leftRadii = outerBottomLeftRadius + outerTopLeftRadius;
+ const cappedOuterRadii: CappedOuterRadii = {
+ topLeft: getBorderCapRadius(outerTopLeftRadius, (outerTopLeftRadius / topRadii) * width, (outerTopLeftRadius / leftRadii) * height),
+ topRight: getBorderCapRadius(outerTopRightRadius, (outerTopRightRadius / topRadii) * width, (outerTopRightRadius / rightRadii) * height),
+ bottomLeft: getBorderCapRadius(outerBottomLeftRadius, (outerBottomLeftRadius / bottomRadii) * width, (outerBottomLeftRadius / leftRadii) * height),
+ bottomRight: getBorderCapRadius(outerBottomRightRadius, (outerBottomRightRadius / bottomRadii) * width, (outerBottomRightRadius / rightRadii) * height),
+ };
+
+ // Add spread radius to corners that actually have radius as shadow has grown larger
+ // than view itself and needs to be rounded accordingly
+ const cappedOuterRadiiWithSpread: CappedOuterRadii = {
+ topLeft: cappedOuterRadii.topLeft > 0 ? cappedOuterRadii.topLeft + spreadRadius : cappedOuterRadii.topLeft,
+ topRight: cappedOuterRadii.topRight > 0 ? cappedOuterRadii.topRight + spreadRadius : cappedOuterRadii.topRight,
+ bottomLeft: cappedOuterRadii.bottomLeft > 0 ? cappedOuterRadii.bottomLeft + spreadRadius : cappedOuterRadii.bottomLeft,
+ bottomRight: cappedOuterRadii.bottomRight > 0 ? cappedOuterRadii.bottomRight + spreadRadius : cappedOuterRadii.bottomRight,
+ };
+
+ innerPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadii);
+ shadowPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadiiWithSpread, spreadRadius);
+ }
+ } else {
+ innerPath = CGPathCreateWithRect(bounds, null);
+ shadowPath = CGPathCreateWithRect(CGRectInset(bounds, -spreadRadius, -spreadRadius), null);
+ }
+
+ return {
+ maskPath: generateShadowMaskPath(bounds, boxShadow, innerPath),
+ shadowPath,
+ };
+ }
+
+ export function generateClipPath(view: View, bounds: CGRect): UIBezierPath {
+ const background = view.style.backgroundInternal;
+ const { origin, size } = bounds;
+
+ const position = {
+ left: origin.x,
+ top: origin.y,
+ bottom: size.height,
+ right: size.width,
+ };
+
+ if (position.right === 0 || position.bottom === 0) {
+ return;
+ }
+
+ let path: UIBezierPath;
+ const clipPath = background.clipPath;
+
+ const functionName: string = clipPath.substring(0, clipPath.indexOf('('));
+ const value: string = clipPath.replace(`${functionName}(`, '').replace(')', '');
+
+ switch (functionName) {
+ case 'rect':
+ path = rectPath(value, position);
+ break;
+
+ case 'inset':
+ path = insetPath(value, position);
+ break;
+
+ case 'circle':
+ path = circlePath(value, position);
+ break;
+
+ case 'ellipse':
+ path = ellipsePath(value, position);
+ break;
+
+ case 'polygon':
+ path = polygonPath(value, position);
+ break;
+ }
+ return path;
+ }
+
+ export function getUniformBorderRadius(view: View, bounds: CGRect): number {
+ const background = view.style.backgroundInternal;
+ const { width, height } = bounds.size;
+ const cornerRadius = layout.toDeviceIndependentPixels(background.getUniformBorderRadius());
+
+ return Math.min(Math.min(width / 2, height / 2), cornerRadius);
+ }
+
+ export function generateNonUniformBorderInnerClipRoundedPath(view: View, bounds: CGRect): any {
+ const background = view.style.backgroundInternal;
+ const nativeView = view.nativeViewProtected;
+
+ const cappedOuterRadii = calculateNonUniformBorderCappedRadii(bounds, background);
+ return generateNonUniformBorderInnerClipPath(bounds, background, cappedOuterRadii);
+ }
+
+ export function generateNonUniformBorderOuterClipRoundedPath(view: View, bounds: CGRect): any {
+ const background = view.style.backgroundInternal;
+ const nativeView = view.nativeViewProtected;
+
+ const cappedOuterRadii = calculateNonUniformBorderCappedRadii(bounds, background);
+ return generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadii);
+ }
+
+ export function generateNonUniformMultiColorBorderRoundedPaths(view: View, bounds: CGRect): Array {
+ const background = view.style.backgroundInternal;
+ const nativeView = view.nativeViewProtected;
+
+ const cappedOuterRadii = calculateNonUniformBorderCappedRadii(bounds, background);
+ return generateNonUniformMultiColorBorderPaths(bounds, background, cappedOuterRadii);
+ }
}
-function onScroll(this: void, args: ScrollEventData): void {
+function maskLayerIfNeeded(nativeView: NativeScriptUIView, background: BackgroundDefinition) {
+ const layer: CALayer = nativeView.layer;
+
+ // Check if layer should be masked
+ if (!(layer.mask instanceof CAShapeLayer)) {
+ // Since layers can only accept up to a single mask at a time, clip path is given more priority
+ if (background.clipPath) {
+ nativeView.maskType = iosViewUtils.LayerMask.CLIP_PATH;
+ } else if (!background.hasUniformBorder() && background.hasBorderRadius()) {
+ nativeView.maskType = iosViewUtils.LayerMask.BORDER;
+ } else {
+ nativeView.maskType = null;
+ }
+
+ if (nativeView.maskType != null) {
+ nativeView.originalMask = layer.mask;
+ layer.mask = CAShapeLayer.new();
+ }
+ }
+}
+
+function clearLayerMask(nativeView: NativeScriptUIView, background: BackgroundDefinition) {
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.mask = null;
+ }
+ nativeView.layer.mask = nativeView.originalMask;
+ nativeView.originalMask = null;
+ nativeView.maskType = null;
+}
+
+function onBackgroundViewScroll(args: ScrollEventData): void {
const view = args.object;
const nativeView = view.nativeViewProtected;
if (nativeView instanceof UIScrollView) {
@@ -102,71 +363,70 @@ function onScroll(this: void, args: ScrollEventData): void {
}
function adjustLayersForScrollView(nativeView: UIScrollView & NativeScriptUIView) {
- const layer = nativeView.borderLayer;
- if (layer instanceof CALayer) {
- // Compensates with transition for the background layers for scrolling in ScrollView based controls.
- CATransaction.begin();
- CATransaction.setValueForKey(kCFBooleanTrue, kCATransactionDisableActions);
- const offset = nativeView.contentOffset;
- const transform = {
- a: 1,
- b: 0,
- c: 0,
- d: 1,
- tx: offset.x,
- ty: offset.y,
- };
- layer.setAffineTransform(transform);
- if (nativeView.layer.mask) {
- nativeView.layer.mask.setAffineTransform(transform);
- }
- CATransaction.commit();
+ // Compensates with transition for the background layers for scrolling in ScrollView based controls.
+ CATransaction.begin();
+ CATransaction.setDisableActions(true);
+ const offset = nativeView.contentOffset;
+ const transform = {
+ a: 1,
+ b: 0,
+ c: 0,
+ d: 1,
+ tx: offset.x,
+ ty: offset.y,
+ };
+
+ if (nativeView.layer.mask) {
+ nativeView.layer.mask.setAffineTransform(transform);
+ }
+
+ // Nested layers
+ if (nativeView.gradientLayer) {
+ nativeView.gradientLayer.setAffineTransform(transform);
+ }
+ if (nativeView.borderLayer) {
+ nativeView.borderLayer.setAffineTransform(transform);
+ }
+ if (nativeView.outerShadowContainerLayer) {
+ // Update bounds of shadow layer as it belongs to parent view
+ nativeView.outerShadowContainerLayer.bounds = nativeView.bounds;
+ nativeView.outerShadowContainerLayer.setAffineTransform(transform);
+ }
+
+ CATransaction.setDisableActions(false);
+ CATransaction.commit();
+}
+
+function unregisterAdjustLayersOnScrollListener(view: View) {
+ if (view.nativeViewProtected instanceof UIScrollView) {
+ view.off('scroll', onBackgroundViewScroll);
}
}
-function unsubscribeFromScrollNotifications(view: View) {
+function registerAdjustLayersOnScrollListener(view: View) {
if (view.nativeViewProtected instanceof UIScrollView) {
- view.off('scroll', onScroll);
- }
-}
-
-function subscribeForScrollNotifications(view: View) {
- if (view.nativeViewProtected instanceof UIScrollView) {
- view.on('scroll', onScroll);
+ view.off('scroll', onBackgroundViewScroll);
+ view.on('scroll', onBackgroundViewScroll);
adjustLayersForScrollView(view.nativeViewProtected);
}
}
+function clearNonUniformColorBorders(nativeView: NativeScriptUIView): void {
+ if (nativeView.borderLayer) {
+ nativeView.borderLayer.mask = null;
+ nativeView.borderLayer.sublayers = null;
+ }
+ nativeView.hasNonUniformBorderColor = false;
+}
+
function clearNonUniformBorders(nativeView: NativeScriptUIView): void {
if (nativeView.borderLayer) {
nativeView.borderLayer.removeFromSuperlayer();
+ nativeView.borderLayer = null;
}
-
- if (nativeView.hasBorderMask) {
- nativeView.layer.mask = nativeView.borderOriginalMask;
- nativeView.hasBorderMask = false;
- nativeView.borderOriginalMask = null;
- }
-
- if (nativeView.topBorderLayer) {
- nativeView.topBorderLayer.removeFromSuperlayer();
- }
-
- if (nativeView.rightBorderLayer) {
- nativeView.rightBorderLayer.removeFromSuperlayer();
- }
-
- if (nativeView.bottomBorderLayer) {
- nativeView.bottomBorderLayer.removeFromSuperlayer();
- }
-
- if (nativeView.leftBorderLayer) {
- nativeView.leftBorderLayer.removeFromSuperlayer();
- }
+ nativeView.hasNonUniformBorder = false;
}
-const pattern = /url\(('|")(.*?)\1\)/;
-
function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const frame = nativeView.frame;
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
@@ -179,7 +439,7 @@ function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor:
const background = style.backgroundInternal;
let imageUri = background.image as string;
if (imageUri) {
- const match = imageUri.match(pattern);
+ const match = imageUri.match(uriPattern);
if (match && match[2]) {
imageUri = match[2];
}
@@ -209,15 +469,6 @@ function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor:
uiColorFromImage(bitmap, view, callback, flip);
}
-interface BackgroundDrawParams {
- repeatX: boolean;
- repeatY: boolean;
- posX: number;
- posY: number;
- sizeX?: number;
- sizeY?: number;
-}
-
function parsePosition(pos: string): { x: CSSValue; y: CSSValue } {
const values = cssParse(pos);
if (values.length === 2) {
@@ -365,14 +616,14 @@ function getDrawParams(this: void, image: UIImage, background: BackgroundDefinit
function uiColorFromImage(img: UIImage, view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const background = view.style.backgroundInternal;
+ const nativeView: NativeScriptUIView = view.nativeViewProtected;
- if (!img || !view.nativeViewProtected) {
+ if (!img || !nativeView) {
callback(background.color && background.color.ios);
return;
}
- const nativeView = view.nativeViewProtected as UIView;
const frame = nativeView.frame;
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height;
@@ -449,35 +700,13 @@ function cssValueToDeviceIndependentPixels(source: string, total: number): numbe
}
}
-function drawUniformColorNonUniformBorders(nativeView: NativeScriptUIView, background: BackgroundDefinition) {
- const layer = nativeView.layer;
- layer.backgroundColor = undefined;
- layer.borderColor = undefined;
- layer.borderWidth = 0;
- layer.cornerRadius = 0;
+function getBorderCapRadius(a: number, b: number, c: number): number {
+ return a && Math.min(a, Math.min(b, c));
+}
- const { width, height } = layer.bounds.size;
- const { x, y } = layer.bounds.origin;
-
- const left = x;
- const top = y;
- const right = x + width;
- const bottom = y + height;
-
- const { min, max } = Math;
-
- const borderTopWidth = max(0, layout.toDeviceIndependentPixels(background.borderTopWidth));
- const borderRightWidth = max(0, layout.toDeviceIndependentPixels(background.borderRightWidth));
- const borderBottomWidth = max(0, layout.toDeviceIndependentPixels(background.borderBottomWidth));
- const borderLeftWidth = max(0, layout.toDeviceIndependentPixels(background.borderLeftWidth));
-
- const borderVWidth = borderTopWidth + borderBottomWidth;
- const borderHWidth = borderLeftWidth + borderRightWidth;
-
- const cappedBorderTopWidth = borderTopWidth && borderTopWidth * min(1, height / borderVWidth);
- const cappedBorderRightWidth = borderRightWidth && borderRightWidth * min(1, width / borderHWidth);
- const cappedBorderBottomWidth = borderBottomWidth && borderBottomWidth * min(1, height / borderVWidth);
- const cappedBorderLeftWidth = borderLeftWidth && borderLeftWidth * min(1, width / borderHWidth);
+function calculateNonUniformBorderCappedRadii(bounds: CGRect, background: BackgroundDefinition): CappedOuterRadii {
+ const { width, height } = bounds.size;
+ const { x, y } = bounds.origin;
const outerTopLeftRadius = layout.toDeviceIndependentPixels(background.borderTopLeftRadius);
const outerTopRightRadius = layout.toDeviceIndependentPixels(background.borderTopRightRadius);
@@ -489,371 +718,496 @@ function drawUniformColorNonUniformBorders(nativeView: NativeScriptUIView, backg
const bottomRadii = outerBottomRightRadius + outerBottomLeftRadius;
const leftRadii = outerBottomLeftRadius + outerTopLeftRadius;
- function capRadius(a: number, b: number, c: number): number {
- return a && Math.min(a, Math.min(b, c));
- }
-
- const cappedOuterTopLeftRadius = capRadius(outerTopLeftRadius, (outerTopLeftRadius / topRadii) * width, (outerTopLeftRadius / leftRadii) * height);
- const cappedOuterTopRightRadius = capRadius(outerTopRightRadius, (outerTopRightRadius / topRadii) * width, (outerTopRightRadius / rightRadii) * height);
- const cappedOuterBottomRightRadius = capRadius(outerBottomRightRadius, (outerBottomRightRadius / bottomRadii) * width, (outerBottomRightRadius / rightRadii) * height);
- const cappedOuterBottomLeftRadius = capRadius(outerBottomLeftRadius, (outerBottomLeftRadius / bottomRadii) * width, (outerBottomLeftRadius / leftRadii) * height);
-
- // Outer contour
- const clipPath = CGPathCreateMutable();
- CGPathMoveToPoint(clipPath, null, left + cappedOuterTopLeftRadius, top);
- CGPathAddArcToPoint(clipPath, null, right, top, right, top + cappedOuterTopRightRadius, cappedOuterTopRightRadius);
- CGPathAddArcToPoint(clipPath, null, right, bottom, right - cappedOuterBottomRightRadius, bottom, cappedOuterBottomRightRadius);
- CGPathAddArcToPoint(clipPath, null, left, bottom, left, bottom - cappedOuterBottomLeftRadius, cappedOuterBottomLeftRadius);
- CGPathAddArcToPoint(clipPath, null, left, top, left + cappedOuterTopLeftRadius, top, cappedOuterTopLeftRadius);
- CGPathCloseSubpath(clipPath);
-
- nativeView.borderOriginalMask = layer.mask;
- const clipShapeLayer = CAShapeLayer.layer();
- clipShapeLayer.path = clipPath;
- layer.mask = clipShapeLayer;
- nativeView.hasBorderMask = true;
-
- if (cappedBorderLeftWidth > 0 || cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0 || cappedBorderBottomWidth > 0) {
- const borderPath = CGPathCreateMutable();
- CGPathAddRect(borderPath, null, CGRectMake(left, top, width, height));
-
- // Inner contour
- if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
- CGPathMoveToPoint(borderPath, null, left + cappedOuterTopLeftRadius, top + cappedBorderTopWidth);
- } else {
- CGPathMoveToPoint(borderPath, null, left, top);
- }
-
- if (cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0) {
- const innerTopRightWRadius = max(0, cappedOuterTopRightRadius - cappedBorderRightWidth);
- const innerTopRightHRadius = max(0, cappedOuterTopRightRadius - cappedBorderTopWidth);
- const innerTopRightMaxRadius = max(innerTopRightWRadius, innerTopRightHRadius);
- const innerTopRightTransform: any = CGAffineTransformMake(innerTopRightMaxRadius && innerTopRightWRadius / innerTopRightMaxRadius, 0, 0, innerTopRightMaxRadius && innerTopRightHRadius / innerTopRightMaxRadius, right - cappedBorderRightWidth - innerTopRightWRadius, top + cappedBorderTopWidth + innerTopRightHRadius);
- CGPathAddArc(borderPath, innerTopRightTransform, 0, 0, innerTopRightMaxRadius, (Math.PI * 3) / 2, 0, false);
- } else {
- CGPathMoveToPoint(borderPath, null, right, top);
- }
-
- if (cappedBorderBottomWidth > 0 || cappedBorderRightWidth > 0) {
- const innerBottomRightWRadius = max(0, cappedOuterBottomRightRadius - cappedBorderRightWidth);
- const innerBottomRightHRadius = max(0, cappedOuterBottomRightRadius - cappedBorderBottomWidth);
- const innerBottomRightMaxRadius = max(innerBottomRightWRadius, innerBottomRightHRadius);
- const innerBottomRightTransform: any = CGAffineTransformMake(innerBottomRightMaxRadius && innerBottomRightWRadius / innerBottomRightMaxRadius, 0, 0, innerBottomRightMaxRadius && innerBottomRightHRadius / innerBottomRightMaxRadius, right - cappedBorderRightWidth - innerBottomRightWRadius, bottom - cappedBorderBottomWidth - innerBottomRightHRadius);
- CGPathAddArc(borderPath, innerBottomRightTransform, 0, 0, innerBottomRightMaxRadius, 0, Math.PI / 2, false);
- } else {
- CGPathAddLineToPoint(borderPath, null, right, bottom);
- }
-
- if (cappedBorderBottomWidth > 0 || cappedBorderLeftWidth > 0) {
- const innerBottomLeftWRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderLeftWidth);
- const innerBottomLeftHRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderBottomWidth);
- const innerBottomLeftMaxRadius = max(innerBottomLeftWRadius, innerBottomLeftHRadius);
- const innerBottomLeftTransform: any = CGAffineTransformMake(innerBottomLeftMaxRadius && innerBottomLeftWRadius / innerBottomLeftMaxRadius, 0, 0, innerBottomLeftMaxRadius && innerBottomLeftHRadius / innerBottomLeftMaxRadius, left + cappedBorderLeftWidth + innerBottomLeftWRadius, bottom - cappedBorderBottomWidth - innerBottomLeftHRadius);
- CGPathAddArc(borderPath, innerBottomLeftTransform, 0, 0, innerBottomLeftMaxRadius, Math.PI / 2, Math.PI, false);
- } else {
- CGPathAddLineToPoint(borderPath, null, left, bottom);
- }
-
- if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
- const innerTopLeftWRadius = max(0, cappedOuterTopLeftRadius - cappedBorderLeftWidth);
- const innerTopLeftHRadius = max(0, cappedOuterTopLeftRadius - cappedBorderTopWidth);
- const innerTopLeftMaxRadius = max(innerTopLeftWRadius, innerTopLeftHRadius);
- const innerTopLeftTransform: any = CGAffineTransformMake(innerTopLeftMaxRadius && innerTopLeftWRadius / innerTopLeftMaxRadius, 0, 0, innerTopLeftMaxRadius && innerTopLeftHRadius / innerTopLeftMaxRadius, left + cappedBorderLeftWidth + innerTopLeftWRadius, top + cappedBorderTopWidth + innerTopLeftHRadius);
- CGPathAddArc(borderPath, innerTopLeftTransform, 0, 0, innerTopLeftMaxRadius, Math.PI, (Math.PI * 3) / 2, false);
- } else {
- CGPathAddLineToPoint(borderPath, null, left, top);
- }
-
- CGPathCloseSubpath(borderPath);
-
- const borderLayer = CAShapeLayer.layer();
- borderLayer.fillColor = (background.borderTopColor && background.borderTopColor.ios.CGColor) || UIColor.blackColor.CGColor;
- borderLayer.fillRule = kCAFillRuleEvenOdd;
- borderLayer.path = borderPath;
- layer.addSublayer(borderLayer);
- nativeView.borderLayer = borderLayer;
- }
-
- nativeView.hasNonUniformBorder = true;
-}
-
-function drawNoRadiusNonUniformBorders(nativeView: NativeScriptUIView, background: BackgroundDefinition) {
- const borderLayer = CALayer.layer();
- nativeView.layer.addSublayer(borderLayer);
- nativeView.borderLayer = borderLayer;
-
- borderLayer.borderColor = undefined;
- borderLayer.borderWidth = 0;
- borderLayer.cornerRadius = 0;
-
- const layerBounds = nativeView.layer.bounds;
- const layerOrigin = layerBounds.origin;
- const layerSize = layerBounds.size;
-
- const nativeViewLayerBounds = {
- left: layerOrigin.x,
- top: layerOrigin.y,
- bottom: layerSize.height,
- right: layerSize.width,
+ const cappedOuterRadii: CappedOuterRadii = {
+ topLeft: getBorderCapRadius(outerTopLeftRadius, (outerTopLeftRadius / topRadii) * width, (outerTopLeftRadius / leftRadii) * height),
+ topRight: getBorderCapRadius(outerTopRightRadius, (outerTopRightRadius / topRadii) * width, (outerTopRightRadius / rightRadii) * height),
+ bottomLeft: getBorderCapRadius(outerBottomLeftRadius, (outerBottomLeftRadius / bottomRadii) * width, (outerBottomLeftRadius / leftRadii) * height),
+ bottomRight: getBorderCapRadius(outerBottomRightRadius, (outerBottomRightRadius / bottomRadii) * width, (outerBottomRightRadius / rightRadii) * height),
};
- const top = layout.toDeviceIndependentPixels(background.borderTopWidth);
- const right = layout.toDeviceIndependentPixels(background.borderRightWidth);
- const bottom = layout.toDeviceIndependentPixels(background.borderBottomWidth);
- const left = layout.toDeviceIndependentPixels(background.borderLeftWidth);
+ return cappedOuterRadii;
+}
+
+function drawNonUniformBorders(nativeView: NativeScriptUIView, background: BackgroundDefinition): void {
+ const layer: CALayer = nativeView.layer;
+ const layerBounds = layer.bounds;
+
+ layer.borderColor = null;
+ layer.borderWidth = 0;
+ layer.cornerRadius = 0;
+
+ const cappedOuterRadii = calculateNonUniformBorderCappedRadii(layerBounds, background);
+ if (nativeView.maskType === iosViewUtils.LayerMask.BORDER && layer.mask instanceof CAShapeLayer) {
+ layer.mask.path = generateNonUniformBorderOuterClipPath(layerBounds, cappedOuterRadii);
+ }
+
+ if (background.hasBorderWidth()) {
+ if (!nativeView.hasNonUniformBorder) {
+ nativeView.borderLayer = CAShapeLayer.new();
+ nativeView.borderLayer.fillRule = kCAFillRuleEvenOdd;
+ layer.addSublayer(nativeView.borderLayer);
+ nativeView.hasNonUniformBorder = true;
+ }
+
+ if (background.hasUniformBorderColor()) {
+ nativeView.borderLayer.fillColor = background.borderTopColor?.ios?.CGColor || UIColor.blackColor.CGColor;
+ nativeView.borderLayer.path = generateNonUniformBorderInnerClipPath(layerBounds, background, cappedOuterRadii);
+ } else {
+ // Non-uniform borders need more layers in order to display multiple colors at the same time
+ let borderTopLayer, borderRightLayer, borderBottomLayer, borderLeftLayer;
+
+ if (!nativeView.hasNonUniformBorderColor) {
+ const maskLayer = CAShapeLayer.new();
+ maskLayer.fillRule = kCAFillRuleEvenOdd;
+ nativeView.borderLayer.mask = maskLayer;
+
+ borderTopLayer = CAShapeLayer.new();
+ borderRightLayer = CAShapeLayer.new();
+ borderBottomLayer = CAShapeLayer.new();
+ borderLeftLayer = CAShapeLayer.new();
+
+ nativeView.borderLayer.addSublayer(borderTopLayer);
+ nativeView.borderLayer.addSublayer(borderRightLayer);
+ nativeView.borderLayer.addSublayer(borderBottomLayer);
+ nativeView.borderLayer.addSublayer(borderLeftLayer);
+
+ nativeView.hasNonUniformBorderColor = true;
+ } else {
+ borderTopLayer = nativeView.borderLayer.sublayers[0];
+ borderRightLayer = nativeView.borderLayer.sublayers[1];
+ borderBottomLayer = nativeView.borderLayer.sublayers[2];
+ borderLeftLayer = nativeView.borderLayer.sublayers[3];
+ }
+
+ const paths = generateNonUniformMultiColorBorderPaths(layerBounds, background, cappedOuterRadii);
+
+ borderTopLayer.fillColor = background.borderTopColor?.ios?.CGColor || UIColor.blackColor.CGColor;
+ borderTopLayer.path = paths[0];
+ borderRightLayer.fillColor = background.borderRightColor?.ios?.CGColor || UIColor.blackColor.CGColor;
+ borderRightLayer.path = paths[1];
+ borderBottomLayer.fillColor = background.borderBottomColor?.ios?.CGColor || UIColor.blackColor.CGColor;
+ borderBottomLayer.path = paths[2];
+ borderLeftLayer.fillColor = background.borderLeftColor?.ios?.CGColor || UIColor.blackColor.CGColor;
+ borderLeftLayer.path = paths[3];
+
+ // Clip inner area to create borders
+ if (nativeView.borderLayer.mask instanceof CAShapeLayer) {
+ nativeView.borderLayer.mask.path = generateNonUniformBorderInnerClipPath(layerBounds, background, cappedOuterRadii);
+ }
+ }
+ }
+}
+
+function calculateInnerBorderClipRadius(radius: number, insetX: number, insetY: number): { xRadius: number; yRadius: number; maxRadius: number } {
+ const innerXRadius = Math.max(0, radius - insetX);
+ const innerYRadius = Math.max(0, radius - insetY);
+ const innerMaxRadius = Math.max(innerXRadius, innerYRadius);
+
+ return {
+ xRadius: innerXRadius,
+ yRadius: innerYRadius,
+ maxRadius: innerMaxRadius,
+ };
+}
+
+/**
+ * Generates a path that represents the rounded view area.
+ *
+ * @param bounds
+ * @param cappedRadii
+ * @param offset
+ * @returns
+ */
+export function generateNonUniformBorderOuterClipPath(bounds: CGRect, cappedRadii: CappedOuterRadii, offset: number = 0): any {
+ const { width, height } = bounds.size;
+ const { x, y } = bounds.origin;
+
+ const left = x - offset;
+ const top = y - offset;
+ const right = x + width + offset;
+ const bottom = y + height + offset;
+
+ const clipPath = CGPathCreateMutable();
+
+ CGPathMoveToPoint(clipPath, null, left + cappedRadii.topLeft, top);
+ CGPathAddArcToPoint(clipPath, null, right, top, right, top + cappedRadii.topRight, cappedRadii.topRight);
+ CGPathAddArcToPoint(clipPath, null, right, bottom, right - cappedRadii.bottomRight, bottom, cappedRadii.bottomRight);
+ CGPathAddArcToPoint(clipPath, null, left, bottom, left, bottom - cappedRadii.bottomLeft, cappedRadii.bottomLeft);
+ CGPathAddArcToPoint(clipPath, null, left, top, left + cappedRadii.topLeft, top, cappedRadii.topLeft);
+ CGPathCloseSubpath(clipPath);
+
+ return clipPath;
+}
+
+/**
+ * Generates a path that represents the area inside borders.
+ *
+ * @param bounds
+ * @param background
+ * @param cappedOuterRadii
+ * @returns
+ */
+function generateNonUniformBorderInnerClipPath(bounds: CGRect, background: BackgroundDefinition, cappedOuterRadii: CappedOuterRadii): any {
+ const { width, height } = bounds.size;
+ const { x, y } = bounds.origin;
+
+ const position: Position = {
+ left: x,
+ top: y,
+ bottom: y + height,
+ right: x + width,
+ };
+
+ const borderTopWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderTopWidth));
+ const borderRightWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderRightWidth));
+ const borderBottomWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderBottomWidth));
+ const borderLeftWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderLeftWidth));
+
+ const borderVWidth = borderTopWidth + borderBottomWidth;
+ const borderHWidth = borderLeftWidth + borderRightWidth;
+
+ const cappedBorderTopWidth = borderTopWidth && borderTopWidth * Math.min(1, height / borderVWidth);
+ const cappedBorderRightWidth = borderRightWidth && borderRightWidth * Math.min(1, width / borderHWidth);
+ const cappedBorderBottomWidth = borderBottomWidth && borderBottomWidth * Math.min(1, height / borderVWidth);
+ const cappedBorderLeftWidth = borderLeftWidth && borderLeftWidth * Math.min(1, width / borderHWidth);
+
+ const clipPath = CGPathCreateMutable();
+ CGPathAddRect(clipPath, null, CGRectMake(x, y, width, height));
+
+ // Inner clip paths
+ if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
+ CGPathMoveToPoint(clipPath, null, position.left + cappedOuterRadii.topLeft, position.top + cappedBorderTopWidth);
+ } else {
+ CGPathMoveToPoint(clipPath, null, position.left, position.top);
+ }
+
+ if (cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0) {
+ const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.topRight, cappedBorderRightWidth, cappedBorderTopWidth);
+ const innerTopRightTransform: any = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.right - cappedBorderRightWidth - xRadius, position.top + cappedBorderTopWidth + yRadius);
+ CGPathAddArc(clipPath, innerTopRightTransform, 0, 0, maxRadius, (Math.PI * 3) / 2, 0, false);
+ } else {
+ CGPathAddLineToPoint(clipPath, null, position.right, position.top);
+ }
+
+ if (cappedBorderBottomWidth > 0 || cappedBorderRightWidth > 0) {
+ const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.bottomRight, cappedBorderRightWidth, cappedBorderBottomWidth);
+ const innerBottomRightTransform: any = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.right - cappedBorderRightWidth - xRadius, position.bottom - cappedBorderBottomWidth - yRadius);
+ CGPathAddArc(clipPath, innerBottomRightTransform, 0, 0, maxRadius, 0, Math.PI / 2, false);
+ } else {
+ CGPathAddLineToPoint(clipPath, null, position.right, position.bottom);
+ }
+
+ if (cappedBorderBottomWidth > 0 || cappedBorderLeftWidth > 0) {
+ const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.bottomLeft, cappedBorderLeftWidth, cappedBorderBottomWidth);
+ const innerBottomLeftTransform: any = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.left + cappedBorderLeftWidth + xRadius, position.bottom - cappedBorderBottomWidth - yRadius);
+ CGPathAddArc(clipPath, innerBottomLeftTransform, 0, 0, maxRadius, Math.PI / 2, Math.PI, false);
+ } else {
+ CGPathAddLineToPoint(clipPath, null, position.left, position.bottom);
+ }
+
+ if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
+ const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.topLeft, cappedBorderLeftWidth, cappedBorderTopWidth);
+ const innerTopLeftTransform: any = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.left + cappedBorderLeftWidth + xRadius, position.top + cappedBorderTopWidth + yRadius);
+ CGPathAddArc(clipPath, innerTopLeftTransform, 0, 0, maxRadius, Math.PI, (Math.PI * 3) / 2, false);
+ } else {
+ CGPathAddLineToPoint(clipPath, null, position.left, position.top);
+ }
+
+ CGPathCloseSubpath(clipPath);
+ return clipPath;
+}
+
+/**
+ * Generates paths for visualizing borders with different color per side.
+ * This is achieved by extending all borders enough to consume entire view size and
+ * use an inner path along with even-odd fill rule to render borders according to their corresponding width.
+ *
+ * @param bounds
+ * @param background
+ * @param cappedOuterRadii
+ * @returns
+ */
+function generateNonUniformMultiColorBorderPaths(bounds: CGRect, background: BackgroundDefinition, cappedOuterRadii: CappedOuterRadii): Array {
+ const { width, height } = bounds.size;
+ const { x, y } = bounds.origin;
+
+ const position: Position = {
+ left: x,
+ top: y,
+ bottom: y + height,
+ right: x + width,
+ };
+
+ const topWidth: number = layout.toDeviceIndependentPixels(background.borderTopWidth);
+ const rightWidth: number = layout.toDeviceIndependentPixels(background.borderRightWidth);
+ const bottomWidth: number = layout.toDeviceIndependentPixels(background.borderBottomWidth);
+ const leftWidth: number = layout.toDeviceIndependentPixels(background.borderLeftWidth);
+
+ // These values have 1 as fallback in order to handler borders with zero values
+ const safeTopWidth: number = Math.max(topWidth, 1);
+ const safeRightWidth: number = Math.max(rightWidth, 1);
+ const safeBottomWidth: number = Math.max(bottomWidth, 1);
+ const safeLeftWidth: number = Math.max(leftWidth, 1);
+
+ const paths = new Array(4);
const lto: Point = {
- x: nativeViewLayerBounds.left,
- y: nativeViewLayerBounds.top,
+ x: position.left,
+ y: position.top,
}; // left-top-outside
const lti: Point = {
- x: nativeViewLayerBounds.left + left,
- y: nativeViewLayerBounds.top + top,
+ x: position.left + safeLeftWidth,
+ y: position.top + safeTopWidth,
}; // left-top-inside
const rto: Point = {
- x: nativeViewLayerBounds.right,
- y: nativeViewLayerBounds.top,
+ x: position.right,
+ y: position.top,
}; // right-top-outside
const rti: Point = {
- x: nativeViewLayerBounds.right - right,
- y: nativeViewLayerBounds.top + top,
+ x: position.right - safeRightWidth,
+ y: position.top + safeTopWidth,
}; // right-top-inside
const rbo: Point = {
- x: nativeViewLayerBounds.right,
- y: nativeViewLayerBounds.bottom,
+ x: position.right,
+ y: position.bottom,
}; // right-bottom-outside
const rbi: Point = {
- x: nativeViewLayerBounds.right - right,
- y: nativeViewLayerBounds.bottom - bottom,
+ x: position.right - safeRightWidth,
+ y: position.bottom - safeBottomWidth,
}; // right-bottom-inside
const lbo: Point = {
- x: nativeViewLayerBounds.left,
- y: nativeViewLayerBounds.bottom,
+ x: position.left,
+ y: position.bottom,
}; // left-bottom-outside
const lbi: Point = {
- x: nativeViewLayerBounds.left + left,
- y: nativeViewLayerBounds.bottom - bottom,
+ x: position.left + safeLeftWidth,
+ y: position.bottom - safeBottomWidth,
}; // left-bottom-inside
- let hasNonUniformBorder: boolean;
+ const centerX: number = position.right / 2;
+ const centerY: number = position.bottom / 2;
+
+ // These values help calculate the size that each border shape should consume
+ const averageHorizontalBorderWidth: number = Math.max((leftWidth + rightWidth) / 2, 1);
+ const averageVerticalBorderWidth: number = Math.max((topWidth + bottomWidth) / 2, 1);
+ const viewRatioMultiplier: number = width > 0 && height > 0 ? width / height : 1;
const borderTopColor = background.borderTopColor;
- if (top > 0 && borderTopColor && borderTopColor.ios) {
+ const borderRightColor = background.borderRightColor;
+ const borderBottomColor = background.borderBottomColor;
+ const borderLeftColor = background.borderLeftColor;
+
+ let borderTopY: number = centerY * (safeTopWidth / averageHorizontalBorderWidth) * viewRatioMultiplier;
+ let borderRightX: number = position.right - (centerX * (safeRightWidth / averageVerticalBorderWidth)) / viewRatioMultiplier;
+ let borderBottomY: number = position.bottom - centerY * (safeBottomWidth / averageHorizontalBorderWidth) * viewRatioMultiplier;
+ let borderLeftX: number = (centerX * (safeLeftWidth / averageVerticalBorderWidth)) / viewRatioMultiplier;
+
+ // Adjust border triangle width in case of borders colliding between each other or borders being less than 4
+ const hasHorizontalIntersection: boolean = borderLeftX > borderRightX;
+ const hasVerticalIntersection: boolean = borderTopY > borderBottomY;
+ if (hasVerticalIntersection) {
+ borderTopY = extendPointsToTargetY(lto.y, lto.x, lti.y, lti.x, borderLeftX);
+ borderBottomY = extendPointsToTargetY(lbo.y, lbo.x, lbi.y, lbi.x, borderLeftX);
+ } else if (hasHorizontalIntersection) {
+ borderLeftX = extendPointsToTargetY(lto.x, lto.y, lti.x, lti.y, borderTopY);
+ borderRightX = extendPointsToTargetY(rto.x, rto.y, rti.x, rti.y, borderTopY);
+ }
+
+ if (topWidth > 0 && borderTopColor?.ios) {
const topBorderPath = CGPathCreateMutable();
+ const borderTopLeftX: number = extendPointsToTargetY(lto.x, lto.y, lti.x, lti.y, borderTopY);
+ const borderTopRightX: number = extendPointsToTargetY(rto.x, rto.y, rti.x, rti.y, borderTopY);
+
CGPathMoveToPoint(topBorderPath, null, lto.x, lto.y);
CGPathAddLineToPoint(topBorderPath, null, rto.x, rto.y);
- CGPathAddLineToPoint(topBorderPath, null, rti.x, rti.y);
- CGPathAddLineToPoint(topBorderPath, null, lti.x, lti.y);
+ CGPathAddLineToPoint(topBorderPath, null, borderTopRightX, borderTopY);
+ if (borderTopRightX !== borderTopLeftX) {
+ CGPathAddLineToPoint(topBorderPath, null, borderTopLeftX, borderTopY);
+ }
CGPathAddLineToPoint(topBorderPath, null, lto.x, lto.y);
- const topBorderLayer = CAShapeLayer.layer();
- topBorderLayer.fillColor = background.borderTopColor.ios.CGColor;
- topBorderLayer.path = topBorderPath;
-
- borderLayer.addSublayer(topBorderLayer);
- nativeView.topBorderLayer = topBorderLayer;
- hasNonUniformBorder = true;
+ paths[0] = topBorderPath;
}
-
- const borderRightColor = background.borderRightColor;
- if (right > 0 && borderRightColor && borderRightColor.ios) {
+ if (rightWidth > 0 && borderRightColor?.ios) {
const rightBorderPath = CGPathCreateMutable();
+ const borderRightBottomY: number = extendPointsToTargetY(rbo.y, rbo.x, rbi.y, rbi.x, borderRightX);
+ const borderRightTopY: number = extendPointsToTargetY(rto.y, rto.x, rti.y, rti.x, borderRightX);
+
CGPathMoveToPoint(rightBorderPath, null, rto.x, rto.y);
CGPathAddLineToPoint(rightBorderPath, null, rbo.x, rbo.y);
- CGPathAddLineToPoint(rightBorderPath, null, rbi.x, rbi.y);
- CGPathAddLineToPoint(rightBorderPath, null, rti.x, rti.y);
+ CGPathAddLineToPoint(rightBorderPath, null, borderRightX, borderRightBottomY);
+ if (borderRightBottomY !== borderRightTopY) {
+ CGPathAddLineToPoint(rightBorderPath, null, borderRightX, borderRightTopY);
+ }
CGPathAddLineToPoint(rightBorderPath, null, rto.x, rto.y);
- const rightBorderLayer = CAShapeLayer.layer();
- rightBorderLayer.fillColor = background.borderRightColor.ios.CGColor;
- rightBorderLayer.path = rightBorderPath;
-
- borderLayer.addSublayer(rightBorderLayer);
- nativeView.rightBorderLayer = rightBorderLayer;
- hasNonUniformBorder = true;
+ paths[1] = rightBorderPath;
}
-
- const borderBottomColor = background.borderBottomColor;
- if (bottom > 0 && borderBottomColor && borderBottomColor.ios) {
+ if (bottomWidth > 0 && borderBottomColor?.ios) {
const bottomBorderPath = CGPathCreateMutable();
+ const borderBottomLeftX: number = extendPointsToTargetY(lbo.x, lbo.y, lbi.x, lbi.y, borderBottomY);
+ const borderBottomRightX: number = extendPointsToTargetY(rbo.x, rbo.y, rbi.x, rbi.y, borderBottomY);
+
CGPathMoveToPoint(bottomBorderPath, null, rbo.x, rbo.y);
CGPathAddLineToPoint(bottomBorderPath, null, lbo.x, lbo.y);
- CGPathAddLineToPoint(bottomBorderPath, null, lbi.x, lbi.y);
- CGPathAddLineToPoint(bottomBorderPath, null, rbi.x, rbi.y);
+ CGPathAddLineToPoint(bottomBorderPath, null, borderBottomLeftX, borderBottomY);
+ if (borderBottomLeftX !== borderBottomRightX) {
+ CGPathAddLineToPoint(bottomBorderPath, null, borderBottomRightX, borderBottomY);
+ }
CGPathAddLineToPoint(bottomBorderPath, null, rbo.x, rbo.y);
- const bottomBorderLayer = CAShapeLayer.layer();
- bottomBorderLayer.fillColor = background.borderBottomColor.ios.CGColor;
- bottomBorderLayer.path = bottomBorderPath;
-
- borderLayer.addSublayer(bottomBorderLayer);
- nativeView.bottomBorderLayer = bottomBorderLayer;
- hasNonUniformBorder = true;
+ paths[2] = bottomBorderPath;
}
-
- const borderLeftColor = background.borderLeftColor;
- if (left > 0 && borderLeftColor && borderLeftColor.ios) {
+ if (leftWidth > 0 && borderLeftColor?.ios) {
const leftBorderPath = CGPathCreateMutable();
+ const borderLeftTopY: number = extendPointsToTargetY(lto.y, lto.x, lti.y, lti.x, borderLeftX);
+ const borderLeftBottomY: number = extendPointsToTargetY(lbo.y, lbo.x, lbi.y, lbi.x, borderLeftX);
+
CGPathMoveToPoint(leftBorderPath, null, lbo.x, lbo.y);
CGPathAddLineToPoint(leftBorderPath, null, lto.x, lto.y);
- CGPathAddLineToPoint(leftBorderPath, null, lti.x, lti.y);
- CGPathAddLineToPoint(leftBorderPath, null, lbi.x, lbi.y);
+ CGPathAddLineToPoint(leftBorderPath, null, borderLeftX, borderLeftTopY);
+ if (borderLeftTopY !== borderLeftBottomY) {
+ CGPathAddLineToPoint(leftBorderPath, null, borderLeftX, borderLeftBottomY);
+ }
CGPathAddLineToPoint(leftBorderPath, null, lbo.x, lbo.y);
- const leftBorderLayer = CAShapeLayer.layer();
- leftBorderLayer.fillColor = background.borderLeftColor.ios.CGColor;
- leftBorderLayer.path = leftBorderPath;
-
- borderLayer.addSublayer(leftBorderLayer);
- nativeView.leftBorderLayer = leftBorderLayer;
- hasNonUniformBorder = true;
+ paths[3] = leftBorderPath;
}
- nativeView.hasNonUniformBorder = hasNonUniformBorder;
+ return paths;
}
-// TODO: use sublayer if its applied to a layout
-function drawBoxShadow(nativeView: NativeScriptUIView, view: View, boxShadow: CSSShadow, background: BackgroundDefinition, useSubLayer: boolean = false) {
- const layer: CALayer = iOSNativeHelper.getShadowLayer(nativeView, 'ns-box-shadow');
-
- layer.masksToBounds = false;
- nativeView.clipsToBounds = false;
-
- // this is required (if not, shadow will get cutoff at parent's dimensions)
- // nativeView.clipsToBounds doesn't work
- view.setProperty('clipToBounds', false);
-
- if (!background.color?.a) {
- // add white background if view has a transparent background
- layer.backgroundColor = UIColor.whiteColor.CGColor;
- }
- // shadow opacity is handled on the shadow's color instance
- layer.shadowOpacity = boxShadow.color?.a ? boxShadow.color?.a / 255 : 1;
- layer.shadowRadius = Length.toDevicePixels(boxShadow.blurRadius, 0.0);
- layer.shadowColor = boxShadow.color.ios.CGColor;
-
- // prettier-ignore
- layer.shadowOffset = CGSizeMake(
- Length.toDevicePixels(boxShadow.offsetX, 0.0),
- Length.toDevicePixels(boxShadow.offsetY, 0.0)
- );
-
- // this should match the view's border radius
- let cornerRadius: number;
- if (typeof view.style.borderRadius !== 'number') {
- cornerRadius = Length.toDevicePixels(view.style.borderRadius, 0.0);
- } else {
- cornerRadius = view.style.borderRadius;
- }
-
- // apply spreadRadius by expanding shadow layer bounds
- // prettier-ignore
- const bounds = CGRectInset(nativeView.bounds,
- -Length.toDevicePixels(boxShadow.spreadRadius, 0.0),
- -Length.toDevicePixels(boxShadow.spreadRadius, 0.0)
- );
-
- // This has the nice glow with box shadow of 0,0
- layer.shadowPath = UIBezierPath.bezierPathWithRoundedRectCornerRadius(bounds, cornerRadius).CGPath;
-}
-
-function clearBoxShadow(nativeView: NativeScriptUIView) {
- nativeView.clipsToBounds = true;
- const layer: CALayer = iOSNativeHelper.getShadowLayer(nativeView, 'ns-box-shadow', false);
- if (!layer) {
- return;
- }
- layer.masksToBounds = true;
- layer.shadowOffset = CGSizeMake(0, 0);
- layer.shadowColor = UIColor.clearColor.CGColor;
- layer.cornerRadius = 0.0;
- layer.shadowRadius = 0.0;
- layer.shadowOpacity = 0.0;
-}
-
-function drawClipPath(nativeView: UIView, background: BackgroundDefinition) {
+function drawBoxShadow(view: View): void {
+ const background = view.style.backgroundInternal;
+ const nativeView = view.nativeViewProtected;
const layer = nativeView.layer;
- const layerBounds = layer.bounds;
- const layerOrigin = layerBounds.origin;
- const layerSize = layerBounds.size;
- const bounds = {
- left: layerOrigin.x,
- top: layerOrigin.y,
- bottom: layerSize.height,
- right: layerSize.width,
- };
-
- if (bounds.right === 0 || bounds.bottom === 0) {
+ // There is no parent to add shadow to
+ if (!layer.superlayer) {
return;
}
- let path: UIBezierPath;
- const clipPath = background.clipPath;
+ const bounds = nativeView.bounds;
+ const viewFrame = nativeView.frame;
+ const boxShadow: BoxShadow = background.getBoxShadow();
- const functionName = clipPath.substring(0, clipPath.indexOf('('));
- const value = clipPath.replace(`${functionName}(`, '').replace(')', '');
+ // Initialize outer shadows
+ let outerShadowContainerLayer: CALayer;
+ if (nativeView.outerShadowContainerLayer) {
+ outerShadowContainerLayer = nativeView.outerShadowContainerLayer;
+ } else {
+ outerShadowContainerLayer = CALayer.new();
- switch (functionName) {
- case 'rect':
- path = rectPath(value, bounds);
- break;
+ // TODO: Make this dynamic when views get support for multiple shadows
+ const shadowLayer = CALayer.new();
+ // This mask is necessary to maintain transparent background
+ const maskLayer = CAShapeLayer.new();
+ maskLayer.fillRule = kCAFillRuleEvenOdd;
- case 'inset':
- path = insetPath(value, bounds);
- break;
+ shadowLayer.mask = maskLayer;
+ outerShadowContainerLayer.addSublayer(shadowLayer);
- case 'circle':
- path = circlePath(value, bounds);
- break;
-
- case 'ellipse':
- path = ellipsePath(value, bounds);
- break;
-
- case 'polygon':
- path = polygonPath(value, bounds);
- break;
+ // Instead of nesting it, add shadow container layer underneath view so that it's not affected by border masking
+ layer.superlayer.insertSublayerBelow(outerShadowContainerLayer, layer);
+ nativeView.outerShadowContainerLayer = outerShadowContainerLayer;
}
- if (path) {
- const shape = CAShapeLayer.layer();
- shape.path = path;
- layer.mask = shape;
- nativeView.clipsToBounds = true;
+ // Apply clip path to shadow
+ if (nativeView.maskType === iosViewUtils.LayerMask.CLIP_PATH && layer.mask instanceof CAShapeLayer) {
+ if (!outerShadowContainerLayer.mask) {
+ outerShadowContainerLayer.mask = CAShapeLayer.new();
+ }
+ if (outerShadowContainerLayer.mask instanceof CAShapeLayer) {
+ outerShadowContainerLayer.mask.path = layer.mask.path;
+ }
+ }
- const borderWidth = background.getUniformBorderWidth();
- const borderColor = background.getUniformBorderColor();
+ // Since shadow layer is added to view layer's superlayer, we have to be more specific about shadow layer position
+ outerShadowContainerLayer.bounds = bounds;
+ outerShadowContainerLayer.position = CGPointMake(viewFrame.origin.x + viewFrame.size.width / 2, viewFrame.origin.y + viewFrame.size.height / 2);
- if (borderWidth > 0 && borderColor instanceof Color) {
- const borderLayer = CAShapeLayer.layer();
- borderLayer.path = path;
- borderLayer.lineWidth = borderWidth * 2;
- borderLayer.strokeColor = borderColor.ios.CGColor;
- borderLayer.fillColor = clearCGColor;
- borderLayer.frame = nativeView.bounds;
+ // Inherit view visibility values
+ outerShadowContainerLayer.opacity = layer.opacity;
+ outerShadowContainerLayer.hidden = layer.hidden;
- layer.borderColor = undefined;
- layer.borderWidth = 0;
- layer.addSublayer(borderLayer);
+ const outerShadowLayers = outerShadowContainerLayer.sublayers;
+ if (outerShadowLayers?.count) {
+ for (let i = 0, count = outerShadowLayers.count; i < count; i++) {
+ const shadowLayer = outerShadowLayers[i];
+ const shadowRadius = layout.toDeviceIndependentPixels(boxShadow.blurRadius);
+ const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
+ const offsetX = layout.toDeviceIndependentPixels(boxShadow.offsetX);
+ const offsetY = layout.toDeviceIndependentPixels(boxShadow.offsetY);
+ const { maskPath, shadowPath } = ios.generateShadowLayerPaths(view, bounds);
+
+ shadowLayer.allowsEdgeAntialiasing = true;
+ shadowLayer.contentsScale = Screen.mainScreen.scale;
+
+ // Shadow opacity is handled on the shadow's color instance
+ shadowLayer.shadowOpacity = boxShadow.color?.a ? boxShadow.color.a / 255 : 1;
+ shadowLayer.shadowRadius = shadowRadius;
+ shadowLayer.shadowColor = boxShadow.color?.ios?.CGColor;
+ shadowLayer.shadowOffset = CGSizeMake(offsetX, offsetY);
+
+ // Apply spread radius by expanding shadow layer bounds (this has a nice glow with radii set to 0)
+ shadowLayer.shadowPath = shadowPath;
+
+ // A mask that ensures that view maintains transparent background
+ if (shadowLayer.mask instanceof CAShapeLayer) {
+ shadowLayer.mask.path = maskPath;
+ }
}
}
}
-function rectPath(value: string, bounds: Rect): UIBezierPath {
+function clearBoxShadow(nativeView: NativeScriptUIView) {
+ if (nativeView.outerShadowContainerLayer) {
+ nativeView.outerShadowContainerLayer.removeFromSuperlayer();
+ nativeView.outerShadowContainerLayer = null;
+ }
+}
+
+/**
+ * Creates a mask that ensures no shadow will be displayed underneath transparent backgrounds.
+ *
+ * @param bounds
+ * @param boxShadow
+ * @param bordersClipPath
+ * @returns
+ */
+function generateShadowMaskPath(bounds: CGRect, boxShadow: BoxShadow, innerClipPath: any): any {
+ const shadowRadius = layout.toDeviceIndependentPixels(boxShadow.blurRadius);
+ const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
+ const offsetX = layout.toDeviceIndependentPixels(boxShadow.offsetX);
+ const offsetY = layout.toDeviceIndependentPixels(boxShadow.offsetY);
+
+ // This value has to be large enough to avoid clipping shadow halo effect
+ const outerRectRadius: number = shadowRadius * 3 + spreadRadius;
+
+ const maskPath = CGPathCreateMutable();
+ // Proper clip position and size
+ const outerRect = CGRectOffset(CGRectInset(bounds, -outerRectRadius, -outerRectRadius), offsetX, offsetY);
+
+ CGPathAddPath(maskPath, null, innerClipPath);
+ CGPathAddRect(maskPath, null, outerRect);
+
+ return maskPath;
+}
+
+function rectPath(value: string, position: Position): UIBezierPath {
const arr = value.split(/[\s]+/);
- const top = cssValueToDeviceIndependentPixels(arr[0], bounds.top);
- const right = cssValueToDeviceIndependentPixels(arr[1], bounds.right);
- const bottom = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
- const left = cssValueToDeviceIndependentPixels(arr[3], bounds.left);
+ const top = cssValueToDeviceIndependentPixels(arr[0], position.top);
+ const right = cssValueToDeviceIndependentPixels(arr[1], position.right);
+ const bottom = cssValueToDeviceIndependentPixels(arr[2], position.bottom);
+ const left = cssValueToDeviceIndependentPixels(arr[3], position.left);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
-function insetPath(value: string, bounds: Rect): UIBezierPath {
+function insetPath(value: string, position: Position): UIBezierPath {
const arr = value.split(/[\s]+/);
let topString: string;
@@ -876,30 +1230,30 @@ function insetPath(value: string, bounds: Rect): UIBezierPath {
leftString = arr[3];
}
- const top = cssValueToDeviceIndependentPixels(topString, bounds.bottom);
- const right = cssValueToDeviceIndependentPixels('100%', bounds.right) - cssValueToDeviceIndependentPixels(rightString, bounds.right);
- const bottom = cssValueToDeviceIndependentPixels('100%', bounds.bottom) - cssValueToDeviceIndependentPixels(bottomString, bounds.bottom);
- const left = cssValueToDeviceIndependentPixels(leftString, bounds.right);
+ const top = cssValueToDeviceIndependentPixels(topString, position.bottom);
+ const right = cssValueToDeviceIndependentPixels('100%', position.right) - cssValueToDeviceIndependentPixels(rightString, position.right);
+ const bottom = cssValueToDeviceIndependentPixels('100%', position.bottom) - cssValueToDeviceIndependentPixels(bottomString, position.bottom);
+ const left = cssValueToDeviceIndependentPixels(leftString, position.right);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
-function circlePath(value: string, bounds: Rect): UIBezierPath {
+function circlePath(value: string, position: Position): UIBezierPath {
const arr = value.split(/[\s]+/);
- const radius = cssValueToDeviceIndependentPixels(arr[0], (bounds.right > bounds.bottom ? bounds.bottom : bounds.right) / 2);
- const y = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
- const x = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
+ const radius = cssValueToDeviceIndependentPixels(arr[0], (position.right > position.bottom ? position.bottom : position.right) / 2);
+ const y = cssValueToDeviceIndependentPixels(arr[2], position.bottom);
+ const x = cssValueToDeviceIndependentPixels(arr[3], position.right);
return UIBezierPath.bezierPathWithArcCenterRadiusStartAngleEndAngleClockwise(CGPointMake(x, y), radius, 0, 360, true).CGPath;
}
-function ellipsePath(value: string, bounds: Rect): UIBezierPath {
+function ellipsePath(value: string, position: Position): UIBezierPath {
const arr = value.split(/[\s]+/);
- const rX = cssValueToDeviceIndependentPixels(arr[0], bounds.right);
- const rY = cssValueToDeviceIndependentPixels(arr[1], bounds.bottom);
- const cX = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
- const cY = cssValueToDeviceIndependentPixels(arr[4], bounds.bottom);
+ const rX = cssValueToDeviceIndependentPixels(arr[0], position.right);
+ const rY = cssValueToDeviceIndependentPixels(arr[1], position.bottom);
+ const cX = cssValueToDeviceIndependentPixels(arr[3], position.right);
+ const cY = cssValueToDeviceIndependentPixels(arr[4], position.bottom);
const left = cX - rX;
const top = cY - rY;
@@ -909,7 +1263,7 @@ function ellipsePath(value: string, bounds: Rect): UIBezierPath {
return UIBezierPath.bezierPathWithOvalInRect(CGRectMake(left, top, width, height)).CGPath;
}
-function polygonPath(value: string, bounds: Rect): UIBezierPath {
+function polygonPath(value: string, position: Position): UIBezierPath {
const path = CGPathCreateMutable();
let firstPoint: Point;
@@ -917,8 +1271,8 @@ function polygonPath(value: string, bounds: Rect): UIBezierPath {
for (let i = 0; i < arr.length; i++) {
const xy = arr[i].trim().split(/[\s]+/);
const point: Point = {
- x: cssValueToDeviceIndependentPixels(xy[0], bounds.right),
- y: cssValueToDeviceIndependentPixels(xy[1], bounds.bottom),
+ x: cssValueToDeviceIndependentPixels(xy[0], position.right),
+ y: cssValueToDeviceIndependentPixels(xy[1], position.bottom),
};
if (!firstPoint) {
diff --git a/packages/core/ui/styling/box-shadow.ts b/packages/core/ui/styling/box-shadow.ts
index a2427a64f..677e88b71 100644
--- a/packages/core/ui/styling/box-shadow.ts
+++ b/packages/core/ui/styling/box-shadow.ts
@@ -1,6 +1,7 @@
import { Color } from '../../color';
export class BoxShadow {
+ public inset: boolean;
public offsetX: number;
public offsetY: number;
public blurRadius: number;
diff --git a/packages/core/ui/styling/css-shadow.ts b/packages/core/ui/styling/css-shadow.ts
index e24a9d29c..c35d1362e 100644
--- a/packages/core/ui/styling/css-shadow.ts
+++ b/packages/core/ui/styling/css-shadow.ts
@@ -2,7 +2,7 @@ import { CoreTypes } from '../../core-types';
import { Color } from '../../color';
import { Length } from './style-properties';
-export interface CSSShadow {
+export interface ShadowCSSValues {
inset: boolean;
offsetX: CoreTypes.LengthType;
offsetY: CoreTypes.LengthType;
@@ -27,14 +27,14 @@ const LENGTH_RE = /^-?[0-9]+[a-zA-Z%]*?$/;
const isLength = (v) => v === '0' || LENGTH_RE.test(v);
/**
- * Parse a string into a CSSShadow
+ * Parse a string into ShadowCSSValues
* Supports any valid css box/text shadow combination.
*
* inspired by https://github.com/jxnblk/css-box-shadow/blob/master/index.js (MIT License)
*
* @param value
*/
-export function parseCSSShadow(value: string): CSSShadow {
+export function parseCSSShadow(value: string): ShadowCSSValues {
const parts = value.trim().split(PARTS_RE);
const inset = parts.includes('inset');
const first = parts[0];
diff --git a/packages/core/ui/styling/style-properties.ts b/packages/core/ui/styling/style-properties.ts
index a1062d058..0ee71247a 100644
--- a/packages/core/ui/styling/style-properties.ts
+++ b/packages/core/ui/styling/style-properties.ts
@@ -16,7 +16,7 @@ import { CoreTypes } from '../../core-types';
import { parseBackground } from '../../css/parser';
import { LinearGradient } from './linear-gradient';
-import { CSSShadow, parseCSSShadow } from './css-shadow';
+import { parseCSSShadow, ShadowCSSValues } from './css-shadow';
function equalsCommon(a: CoreTypes.LengthType, b: CoreTypes.LengthType): boolean;
function equalsCommon(a: CoreTypes.PercentLengthType, b: CoreTypes.PercentLengthType): boolean;
@@ -1226,11 +1226,22 @@ export const borderBottomLeftRadiusProperty = new CssProperty