From 36ad24c037ea06111beb4176bb3836a319307b81 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 23 Sep 2022 10:27:01 -0700 Subject: [PATCH] fix(animations): error handling closes https://github.com/NativeScript/NativeScript/issues/10037 --- .../core/ui/animation/animation-common.ts | 25 +- packages/core/ui/animation/index.ios.ts | 298 +++++++++--------- 2 files changed, 168 insertions(+), 155 deletions(-) diff --git a/packages/core/ui/animation/animation-common.ts b/packages/core/ui/animation/animation-common.ts index 0771f3654..be51c87f4 100644 --- a/packages/core/ui/animation/animation-common.ts +++ b/packages/core/ui/animation/animation-common.ts @@ -42,7 +42,8 @@ export abstract class AnimationBase implements AnimationBaseDefinition { constructor(animationDefinitions: Array, playSequentially?: boolean) { if (!animationDefinitions || animationDefinitions.length === 0) { - throw new Error('No animation definitions specified'); + console.error('No animation definitions specified'); + return; } if (Trace.isEnabled()) { @@ -58,7 +59,10 @@ export abstract class AnimationBase implements AnimationBaseDefinition { } if (this._propertyAnimations.length === 0) { - throw new Error('Nothing to animate.'); + if (Trace.isEnabled()) { + Trace.write('Nothing to animate.', Trace.categories.Animation); + } + return; } if (Trace.isEnabled()) { Trace.write('Created ' + this._propertyAnimations.length + ' individual property animations.', Trace.categories.Animation); @@ -137,7 +141,8 @@ export abstract class AnimationBase implements AnimationBaseDefinition { private static _createPropertyAnimations(animationDefinition: AnimationDefinition): Array { if (!animationDefinition.target) { - throw new Error('No animation target specified.'); + console.error('No animation target specified.'); + return; } for (const item in animationDefinition) { @@ -147,18 +152,22 @@ export abstract class AnimationBase implements AnimationBaseDefinition { } if ((item === Properties.opacity || item === 'duration' || item === 'delay' || item === 'iterations') && typeof value !== 'number') { - throw new Error(`Property ${item} must be valid number. Value: ${value}`); + console.error(`Property ${item} must be valid number. Value: ${value}`); + return; } else if ((item === Properties.scale || item === Properties.translate) && (typeof (value).x !== 'number' || typeof (value).y !== 'number')) { - throw new Error(`Property ${item} must be valid Pair. Value: ${value}`); + console.error(`Property ${item} must be valid Pair. Value: ${value}`); + return; } else if (item === Properties.backgroundColor && !Color.isValid(animationDefinition.backgroundColor)) { - throw new Error(`Property ${item} must be valid color. Value: ${value}`); + console.error(`Property ${item} must be valid color. Value: ${value}`); + return; } else if (item === Properties.width || item === Properties.height) { // Coerce input into a PercentLength object in case it's a string. animationDefinition[item] = PercentLength.parse(value); } else if (item === Properties.rotate) { const rotate: number | Point3D = value; if (typeof rotate !== 'number' && !(typeof rotate.x === 'number' && typeof rotate.y === 'number' && typeof rotate.z === 'number')) { - throw new Error(`Property ${rotate} must be valid number or Point3D. Value: ${value}`); + console.error(`Property ${rotate} must be valid number or Point3D. Value: ${value}`); + return; } } } @@ -265,7 +274,7 @@ export abstract class AnimationBase implements AnimationBaseDefinition { } if (propertyAnimations.length === 0) { - throw new Error('No known animation properties specified'); + console.error('No known animation properties specified'); } return propertyAnimations; diff --git a/packages/core/ui/animation/index.ios.ts b/packages/core/ui/animation/index.ios.ts index 9838b7054..350de5692 100644 --- a/packages/core/ui/animation/index.ios.ts +++ b/packages/core/ui/animation/index.ios.ts @@ -136,7 +136,7 @@ export function _resolveAnimationCurve(curve: string | CubicBezierAnimationCurve return CAMediaTimingFunction.functionWithControlPoints(animationCurve.x1, animationCurve.y1, animationCurve.x2, animationCurve.y2); } else { - throw new Error(`Invalid animation curve: ${curve}`); + console.error(`Invalid animation curve: ${curve}`); } } } @@ -269,140 +269,142 @@ export class Animation extends AnimationBase { let subPropertyNameToAnimate; let toValue = animation.value; let fromValue; - const setLocal = valueSource === 'animation'; + if (nativeView?.layer) { + const setLocal = valueSource === 'animation'; - switch (animation.property) { - case Properties.backgroundColor: - animation._originalValue = view.backgroundColor; - animation._propertyResetCallback = (value, valueSource) => { - style[setLocal ? backgroundColorProperty.name : backgroundColorProperty.keyframe] = value; - }; - fromValue = nativeView.layer.backgroundColor; - if (nativeView instanceof UILabel) { - nativeView.setValueForKey(UIColor.clearColor, 'backgroundColor'); - } - toValue = toValue.CGColor; - break; - case Properties.opacity: - animation._originalValue = view.opacity; - animation._propertyResetCallback = (value, valueSource) => { - style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value; - }; - fromValue = nativeView.layer.opacity; - break; - case Properties.rotate: - animation._originalValue = { - x: view.rotateX, - y: view.rotateY, - z: view.rotate, - }; - animation._propertyResetCallback = (value, valueSource) => { - style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value.z; - style[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value.x; - style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.y; - }; + switch (animation.property) { + case Properties.backgroundColor: + animation._originalValue = view.backgroundColor; + animation._propertyResetCallback = (value, valueSource) => { + style[setLocal ? backgroundColorProperty.name : backgroundColorProperty.keyframe] = value; + }; + fromValue = nativeView.layer.backgroundColor; + if (nativeView instanceof UILabel) { + nativeView.setValueForKey(UIColor.clearColor, 'backgroundColor'); + } + toValue = toValue.CGColor; + break; + case Properties.opacity: + animation._originalValue = view.opacity; + animation._propertyResetCallback = (value, valueSource) => { + style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value; + }; + fromValue = nativeView.layer.opacity; + break; + case Properties.rotate: + animation._originalValue = { + x: view.rotateX, + y: view.rotateY, + z: view.rotate, + }; + animation._propertyResetCallback = (value, valueSource) => { + style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value.z; + style[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value.x; + style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.y; + }; - propertyNameToAnimate = 'transform.rotation'; - subPropertyNameToAnimate = ['x', 'y', 'z']; - fromValue = { - x: nativeView.layer.valueForKeyPath('transform.rotation.x'), - y: nativeView.layer.valueForKeyPath('transform.rotation.y'), - z: nativeView.layer.valueForKeyPath('transform.rotation.z'), - }; + propertyNameToAnimate = 'transform.rotation'; + subPropertyNameToAnimate = ['x', 'y', 'z']; + fromValue = { + x: nativeView.layer.valueForKeyPath('transform.rotation.x'), + y: nativeView.layer.valueForKeyPath('transform.rotation.y'), + z: nativeView.layer.valueForKeyPath('transform.rotation.z'), + }; - if (animation.target.rotateX !== undefined && animation.target.rotateX !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { - fromValue.x = (animation.target.rotateX * Math.PI) / 180; - } - if (animation.target.rotateY !== undefined && animation.target.rotateY !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { - fromValue.y = (animation.target.rotateY * Math.PI) / 180; - } - if (animation.target.rotate !== undefined && animation.target.rotate !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { - fromValue.z = (animation.target.rotate * Math.PI) / 180; - } + if (animation.target.rotateX !== undefined && animation.target.rotateX !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { + fromValue.x = (animation.target.rotateX * Math.PI) / 180; + } + if (animation.target.rotateY !== undefined && animation.target.rotateY !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { + fromValue.y = (animation.target.rotateY * Math.PI) / 180; + } + if (animation.target.rotate !== undefined && animation.target.rotate !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { + fromValue.z = (animation.target.rotate * Math.PI) / 180; + } - // Respect only value.z for back-compat until 3D rotations are implemented - toValue = { - x: (toValue.x * Math.PI) / 180, - y: (toValue.y * Math.PI) / 180, - z: (toValue.z * Math.PI) / 180, - }; - break; - case Properties.translate: - animation._originalValue = { - x: view.translateX, - y: view.translateY, - }; - animation._propertyResetCallback = (value, valueSource) => { - style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x; - style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.y; - }; - propertyNameToAnimate = 'transform'; - fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); - toValue = NSValue.valueWithCATransform3D(CATransform3DTranslate(nativeView.layer.transform, toValue.x, toValue.y, 0)); - break; - case Properties.scale: - if (toValue.x === 0) { - toValue.x = 0.001; + // Respect only value.z for back-compat until 3D rotations are implemented + toValue = { + x: (toValue.x * Math.PI) / 180, + y: (toValue.y * Math.PI) / 180, + z: (toValue.z * Math.PI) / 180, + }; + break; + case Properties.translate: + animation._originalValue = { + x: view.translateX, + y: view.translateY, + }; + animation._propertyResetCallback = (value, valueSource) => { + style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x; + style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.y; + }; + propertyNameToAnimate = 'transform'; + fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); + toValue = NSValue.valueWithCATransform3D(CATransform3DTranslate(nativeView.layer.transform, toValue.x, toValue.y, 0)); + break; + case Properties.scale: + if (toValue.x === 0) { + toValue.x = 0.001; + } + if (toValue.y === 0) { + toValue.y = 0.001; + } + animation._originalValue = { x: view.scaleX, y: view.scaleY }; + animation._propertyResetCallback = (value, valueSource) => { + style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x; + style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y; + }; + propertyNameToAnimate = 'transform'; + fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); + toValue = NSValue.valueWithCATransform3D(CATransform3DScale(nativeView.layer.transform, toValue.x, toValue.y, 1)); + break; + case _transform: + fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); + animation._originalValue = { + xs: view.scaleX, + ys: view.scaleY, + xt: view.translateX, + yt: view.translateY, + rx: view.rotateX, + ry: view.rotateY, + rz: view.rotate, + }; + animation._propertyResetCallback = (value, valueSource) => { + style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.xt; + style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.yt; + style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.xs; + style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.ys; + style[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value.rx; + style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.ry; + style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value.rz; + }; + propertyNameToAnimate = 'transform'; + toValue = NSValue.valueWithCATransform3D(Animation._createNativeAffineTransform(animation)); + break; + case Properties.width: + case Properties.height: { + const direction: string = animation.property; + const isHeight: boolean = direction === 'height'; + propertyNameToAnimate = 'bounds'; + if (!parent) { + console.error(`cannot animate ${direction} on root view`); + } + const parentExtent: number = isHeight ? parent.getMeasuredHeight() : parent.getMeasuredWidth(); + const asNumber = PercentLength.toDevicePixels(PercentLength.parse(toValue), parentExtent, parentExtent) / Screen.mainScreen.scale; + const currentBounds = nativeView.layer.bounds; + const extentX = isHeight ? currentBounds.size.width : asNumber; + 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._propertyResetCallback = (value, valueSource) => { + const prop = isHeight ? heightProperty : widthProperty; + style[setLocal ? prop.name : prop.keyframe] = value; + }; + break; } - if (toValue.y === 0) { - toValue.y = 0.001; - } - animation._originalValue = { x: view.scaleX, y: view.scaleY }; - animation._propertyResetCallback = (value, valueSource) => { - style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x; - style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y; - }; - propertyNameToAnimate = 'transform'; - fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); - toValue = NSValue.valueWithCATransform3D(CATransform3DScale(nativeView.layer.transform, toValue.x, toValue.y, 1)); - break; - case _transform: - fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); - animation._originalValue = { - xs: view.scaleX, - ys: view.scaleY, - xt: view.translateX, - yt: view.translateY, - rx: view.rotateX, - ry: view.rotateY, - rz: view.rotate, - }; - animation._propertyResetCallback = (value, valueSource) => { - style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.xt; - style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.yt; - style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.xs; - style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.ys; - style[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value.rx; - style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.ry; - style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value.rz; - }; - propertyNameToAnimate = 'transform'; - toValue = NSValue.valueWithCATransform3D(Animation._createNativeAffineTransform(animation)); - break; - case Properties.width: - case Properties.height: { - const direction: string = animation.property; - const isHeight: boolean = direction === 'height'; - propertyNameToAnimate = 'bounds'; - if (!parent) { - throw new Error(`cannot animate ${direction} on root view`); - } - const parentExtent: number = isHeight ? parent.getMeasuredHeight() : parent.getMeasuredWidth(); - const asNumber = PercentLength.toDevicePixels(PercentLength.parse(toValue), parentExtent, parentExtent) / Screen.mainScreen.scale; - const currentBounds = nativeView.layer.bounds; - const extentX = isHeight ? currentBounds.size.width : asNumber; - 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._propertyResetCallback = (value, valueSource) => { - const prop = isHeight ? heightProperty : widthProperty; - style[setLocal ? prop.name : prop.keyframe] = value; - }; - break; + default: + console.error(`Animating property '${animation.property}' is unsupported`); } - default: - throw new Error(`Animating property '${animation.property}' is unsupported`); } let duration = 0.3; @@ -437,26 +439,28 @@ 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; - let nativeAnimation; + if (nativeView?.layer) { + let nativeAnimation; - if (args.subPropertiesToAnimate) { - nativeAnimation = this._createGroupAnimation(args, animation); - } else { - nativeAnimation = this._createBasicAnimation(args, animation); - } - - const animationDelegate = AnimationDelegateImpl.initWithFinishedCallback(finishedCallback, animation, valueSource); - nativeAnimation.setValueForKey(animationDelegate, 'delegate'); - - nativeView.layer.addAnimationForKey(nativeAnimation, args.propertyNameToAnimate); - - let callback = undefined; - if (index + 1 < propertyAnimations.length) { - callback = Animation._createiOSAnimationFunction(propertyAnimations, index + 1, playSequentially, valueSource, finishedCallback); - if (!playSequentially) { - callback(); + if (args.subPropertiesToAnimate) { + nativeAnimation = this._createGroupAnimation(args, animation); } else { - animationDelegate.nextAnimation = callback; + nativeAnimation = this._createBasicAnimation(args, animation); + } + + const animationDelegate = AnimationDelegateImpl.initWithFinishedCallback(finishedCallback, animation, valueSource); + nativeAnimation.setValueForKey(animationDelegate, 'delegate'); + + nativeView.layer.addAnimationForKey(nativeAnimation, args.propertyNameToAnimate); + + let callback = undefined; + if (index + 1 < propertyAnimations.length) { + callback = Animation._createiOSAnimationFunction(propertyAnimations, index + 1, playSequentially, valueSource, finishedCallback); + if (!playSequentially) { + callback(); + } else { + animationDelegate.nextAnimation = callback; + } } } }