Files
NativeScript/nativescript-core/ui/animation/index.ios.ts
Nathan Walker da3c0b973d chore: format
2020-07-11 11:01:40 -07:00

1143 lines
30 KiB
TypeScript

// Types
import {
AnimationDefinitionInternal,
AnimationPromise,
IOSView,
PropertyAnimation,
PropertyAnimationInfo,
} from "./animation-common";
import { View } from "../core/view";
// Requires
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 iosNativeHelper } from "../../utils/native-helper";
import { screen } from "../../platform";
export * from "./animation-common";
let _transform = "_transform";
let _skip = "_skip";
let FLT_MAX = 340282346638528859811704183484516925440.0;
class AnimationInfo {
public propertyNameToAnimate: string;
public subPropertiesToAnimate?: string[];
public fromValue: any;
public toValue: any;
public duration: number;
public repeatCount: number;
public delay: number;
}
class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
public nextAnimation: Function;
// The CAAnimationDelegate protocol has been introduced in the iOS 10 SDK
static ObjCProtocols = (<any>global).CAAnimationDelegate
? [(<any>global).CAAnimationDelegate]
: [];
private _finishedCallback: Function;
private _propertyAnimation: PropertyAnimationInfo;
private _valueSource: "animation" | "keyframe";
public static initWithFinishedCallback(
finishedCallback: Function,
propertyAnimation: PropertyAnimationInfo,
valueSource: "animation" | "keyframe"
): AnimationDelegateImpl {
let delegate = <AnimationDelegateImpl>AnimationDelegateImpl.new();
delegate._finishedCallback = finishedCallback;
delegate._propertyAnimation = propertyAnimation;
delegate._valueSource = valueSource;
return delegate;
}
animationDidStart(anim: CAAnimation): void {
let value = this._propertyAnimation.value;
let setLocal = this._valueSource === "animation";
let targetStyle = this._propertyAnimation.target.style;
(<IOSView>(
this._propertyAnimation.target
))._suspendPresentationLayerUpdates();
switch (this._propertyAnimation.property) {
case Properties.backgroundColor:
targetStyle[
setLocal
? backgroundColorProperty.name
: backgroundColorProperty.keyframe
] = value;
break;
case Properties.opacity:
targetStyle[
setLocal ? opacityProperty.name : opacityProperty.keyframe
] = value;
break;
case Properties.rotate:
targetStyle[
setLocal ? rotateXProperty.name : rotateXProperty.keyframe
] = value.x;
targetStyle[
setLocal ? rotateYProperty.name : rotateYProperty.keyframe
] = value.y;
targetStyle[
setLocal ? rotateProperty.name : rotateProperty.keyframe
] = value.z;
break;
case Properties.translate:
targetStyle[
setLocal
? translateXProperty.name
: translateXProperty.keyframe
] = value.x;
targetStyle[
setLocal
? translateYProperty.name
: translateYProperty.keyframe
] = value.y;
break;
case Properties.height:
targetStyle[
setLocal ? heightProperty.name : heightProperty.keyframe
] = value;
break;
case Properties.width:
targetStyle[
setLocal ? widthProperty.name : widthProperty.keyframe
] = value;
break;
case Properties.scale:
targetStyle[
setLocal ? scaleXProperty.name : scaleXProperty.keyframe
] = value.x === 0 ? 0.001 : value.x;
targetStyle[
setLocal ? scaleYProperty.name : scaleYProperty.keyframe
] = value.y === 0 ? 0.001 : value.y;
break;
case _transform:
if (value[Properties.translate] !== undefined) {
targetStyle[
setLocal
? translateXProperty.name
: translateXProperty.keyframe
] = value[Properties.translate].x;
targetStyle[
setLocal
? translateYProperty.name
: translateYProperty.keyframe
] = value[Properties.translate].y;
}
if (value[Properties.rotate] !== undefined) {
targetStyle[
setLocal
? rotateXProperty.name
: rotateXProperty.keyframe
] = value[Properties.rotate].x;
targetStyle[
setLocal
? rotateYProperty.name
: rotateYProperty.keyframe
] = value[Properties.rotate].y;
targetStyle[
setLocal ? rotateProperty.name : rotateProperty.keyframe
] = value[Properties.rotate].z;
}
if (value[Properties.scale] !== undefined) {
let x = value[Properties.scale].x;
let y = value[Properties.scale].y;
targetStyle[
setLocal ? scaleXProperty.name : scaleXProperty.keyframe
] = x === 0 ? 0.001 : x;
targetStyle[
setLocal ? scaleYProperty.name : scaleYProperty.keyframe
] = y === 0 ? 0.001 : y;
}
break;
}
(<IOSView>(
this._propertyAnimation.target
))._resumePresentationLayerUpdates();
}
public animationDidStopFinished(
anim: CAAnimation,
finished: boolean
): void {
if (this._finishedCallback) {
this._finishedCallback(!finished);
}
if (finished && this.nextAnimation) {
this.nextAnimation();
}
}
}
export function _resolveAnimationCurve(
curve: string | CubicBezierAnimationCurve | CAMediaTimingFunction
): CAMediaTimingFunction | string {
switch (curve) {
case "easeIn":
return CAMediaTimingFunction.functionWithName(
kCAMediaTimingFunctionEaseIn
);
case "easeOut":
return CAMediaTimingFunction.functionWithName(
kCAMediaTimingFunctionEaseOut
);
case "easeInOut":
return CAMediaTimingFunction.functionWithName(
kCAMediaTimingFunctionEaseInEaseOut
);
case "linear":
return CAMediaTimingFunction.functionWithName(
kCAMediaTimingFunctionLinear
);
case "spring":
return curve;
case "ease":
return CAMediaTimingFunction.functionWithControlPoints(
0.25,
0.1,
0.25,
1.0
);
default:
if (curve instanceof CAMediaTimingFunction) {
return curve;
} else if (curve instanceof CubicBezierAnimationCurve) {
let animationCurve = <CubicBezierAnimationCurve>curve;
return CAMediaTimingFunction.functionWithControlPoints(
animationCurve.x1,
animationCurve.y1,
animationCurve.x2,
animationCurve.y2
);
} else {
throw new Error(`Invalid animation curve: ${curve}`);
}
}
}
export class Animation extends AnimationBase {
private _iOSAnimationFunction: Function;
private _finishedAnimations: number;
private _cancelledAnimations: number;
private _mergedPropertyAnimations: Array<PropertyAnimationInfo>;
private _valueSource: "animation" | "keyframe";
constructor(
animationDefinitions: Array<AnimationDefinitionInternal>,
playSequentially?: boolean
) {
super(animationDefinitions, playSequentially);
this._valueSource = "animation";
if (
animationDefinitions.length > 0 &&
animationDefinitions[0].valueSource !== undefined
) {
this._valueSource = animationDefinitions[0].valueSource;
}
if (!playSequentially) {
if (Trace.isEnabled()) {
Trace.write(
"Non-merged Property Animations: " +
this._propertyAnimations.length,
Trace.categories.Animation
);
}
this._mergedPropertyAnimations = Animation._mergeAffineTransformAnimations(
this._propertyAnimations
);
if (Trace.isEnabled()) {
Trace.write(
"Merged Property Animations: " +
this._mergedPropertyAnimations.length,
Trace.categories.Animation
);
}
} else {
this._mergedPropertyAnimations = this._propertyAnimations;
}
let that = this;
let animationFinishedCallback = (cancelled: boolean) => {
if (that._playSequentially) {
// This function will be called by the last animation when done or by another animation if the user cancels them halfway through.
if (cancelled) {
that._rejectAnimationFinishedPromise();
} else {
that._resolveAnimationFinishedPromise();
}
} else {
// This callback will be called by each INDIVIDUAL animation when it finishes or is cancelled.
if (cancelled) {
that._cancelledAnimations++;
} else {
that._finishedAnimations++;
}
if (
that._cancelledAnimations > 0 &&
that._cancelledAnimations + that._finishedAnimations ===
that._mergedPropertyAnimations.length
) {
if (Trace.isEnabled()) {
Trace.write(
that._cancelledAnimations +
" animations cancelled.",
Trace.categories.Animation
);
}
that._rejectAnimationFinishedPromise();
} else if (
that._finishedAnimations ===
that._mergedPropertyAnimations.length
) {
if (Trace.isEnabled()) {
Trace.write(
that._finishedAnimations + " animations finished.",
Trace.categories.Animation
);
}
that._resolveAnimationFinishedPromise();
}
}
};
this._iOSAnimationFunction = Animation._createiOSAnimationFunction(
this._mergedPropertyAnimations,
0,
this._playSequentially,
this._valueSource,
animationFinishedCallback
);
}
public play(): AnimationPromise {
if (this.isPlaying) {
return this._rejectAlreadyPlaying();
}
let animationFinishedPromise = super.play();
this._finishedAnimations = 0;
this._cancelledAnimations = 0;
this._iOSAnimationFunction();
return animationFinishedPromise;
}
public cancel(): void {
if (!this.isPlaying) {
Trace.write(
"Animation is not currently playing.",
Trace.categories.Animation,
Trace.messageType.warn
);
return;
}
let i = 0;
let length = this._mergedPropertyAnimations.length;
for (; i < length; i++) {
let propertyAnimation = this._mergedPropertyAnimations[i];
propertyAnimation.target.nativeViewProtected.layer.removeAllAnimations();
if (propertyAnimation._propertyResetCallback) {
propertyAnimation._propertyResetCallback(
propertyAnimation._originalValue,
this._valueSource
);
}
}
}
public _resolveAnimationCurve(
curve: string | CubicBezierAnimationCurve | CAMediaTimingFunction
): CAMediaTimingFunction | string {
return _resolveAnimationCurve(curve);
}
private static _createiOSAnimationFunction(
propertyAnimations: Array<PropertyAnimation>,
index: number,
playSequentially: boolean,
valueSource: "animation" | "keyframe",
finishedCallback: (cancelled?: boolean) => void
): Function {
return (cancelled?: boolean) => {
if (cancelled && finishedCallback) {
if (Trace.isEnabled()) {
Trace.write(
"Animation " +
(index - 1).toString() +
" was cancelled. Will skip the rest of animations and call finishedCallback(true).",
Trace.categories.Animation
);
}
finishedCallback(cancelled);
return;
}
let animation = propertyAnimations[index];
let args = Animation._getNativeAnimationArguments(
animation,
valueSource
);
if (animation.curve === "spring") {
Animation._createNativeSpringAnimation(
propertyAnimations,
index,
playSequentially,
args,
animation,
valueSource,
finishedCallback
);
} else {
Animation._createNativeAnimation(
propertyAnimations,
index,
playSequentially,
args,
animation,
valueSource,
finishedCallback
);
}
};
}
private static _getNativeAnimationArguments(
animation: PropertyAnimationInfo,
valueSource: "animation" | "keyframe"
): AnimationInfo {
const view = animation.target;
const style = view.style;
const nativeView = <UIView>view.nativeViewProtected;
const parent = view.parent as View;
const screenScale: number = screen.mainScreen.scale;
let propertyNameToAnimate = animation.property;
let subPropertyNameToAnimate;
let toValue = animation.value;
let fromValue;
let 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;
};
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;
}
// 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) {
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
) / screenScale;
let currentBounds = nativeView.layer.bounds;
let extentX = isHeight ? currentBounds.size.width : asNumber;
let 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:
throw new Error(
`Animating property '${animation.property}' is unsupported`
);
}
let duration = 0.3;
if (animation.duration !== undefined) {
duration = animation.duration / 1000.0;
}
let delay = undefined;
if (animation.delay) {
delay = animation.delay / 1000.0;
}
let repeatCount = undefined;
if (animation.iterations !== undefined) {
if (animation.iterations === Number.POSITIVE_INFINITY) {
repeatCount = FLT_MAX;
} else {
repeatCount = animation.iterations;
}
}
return {
propertyNameToAnimate: propertyNameToAnimate,
fromValue: fromValue,
subPropertiesToAnimate: subPropertyNameToAnimate,
toValue: toValue,
duration: duration,
repeatCount: repeatCount,
delay: delay,
};
}
private static _createNativeAnimation(
propertyAnimations: Array<PropertyAnimation>,
index: number,
playSequentially: boolean,
args: AnimationInfo,
animation: PropertyAnimation,
valueSource: "animation" | "keyframe",
finishedCallback: (cancelled?: boolean) => void
) {
let nativeView = <UIView>animation.target.nativeViewProtected;
let nativeAnimation;
if (args.subPropertiesToAnimate) {
nativeAnimation = this._createGroupAnimation(args, animation);
} else {
nativeAnimation = this._createBasicAnimation(args, animation);
}
let 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;
}
}
}
private static _createGroupAnimation(
args: AnimationInfo,
animation: PropertyAnimation
) {
let groupAnimation = CAAnimationGroup.new();
groupAnimation.duration = args.duration;
const animations = NSMutableArray.alloc<CAAnimation>().initWithCapacity(
3
);
args.subPropertiesToAnimate.forEach((property) => {
const basicAnimationArgs = { ...args };
basicAnimationArgs.propertyNameToAnimate = `${args.propertyNameToAnimate}.${property}`;
basicAnimationArgs.fromValue = args.fromValue[property];
basicAnimationArgs.toValue = args.toValue[property];
const basicAnimation = this._createBasicAnimation(
basicAnimationArgs,
animation
);
animations.addObject(basicAnimation);
});
groupAnimation.animations = animations;
return groupAnimation;
}
private static _createBasicAnimation(
args: AnimationInfo,
animation: PropertyAnimation
) {
let basicAnimation = CABasicAnimation.animationWithKeyPath(
args.propertyNameToAnimate
);
basicAnimation.fromValue = args.fromValue;
basicAnimation.toValue = args.toValue;
basicAnimation.duration = args.duration;
if (args.repeatCount !== undefined) {
basicAnimation.repeatCount = args.repeatCount;
}
if (args.delay !== undefined) {
basicAnimation.beginTime = CACurrentMediaTime() + args.delay;
}
if (animation.curve !== undefined) {
basicAnimation.timingFunction = animation.curve;
}
return basicAnimation;
}
private static _createNativeSpringAnimation(
propertyAnimations: Array<PropertyAnimationInfo>,
index: number,
playSequentially: boolean,
args: AnimationInfo,
animation: PropertyAnimationInfo,
valueSource: "animation" | "keyframe",
finishedCallback: (cancelled?: boolean) => void
) {
let nativeView = <UIView>animation.target.nativeViewProtected;
let callback = undefined;
let nextAnimation;
if (index + 1 < propertyAnimations.length) {
callback = Animation._createiOSAnimationFunction(
propertyAnimations,
index + 1,
playSequentially,
valueSource,
finishedCallback
);
if (!playSequentially) {
callback();
} else {
nextAnimation = callback;
}
}
let delay = 0;
if (args.delay) {
delay = args.delay;
}
UIView.animateWithDurationDelayUsingSpringWithDampingInitialSpringVelocityOptionsAnimationsCompletion(
args.duration,
delay,
0.2,
0,
UIViewAnimationOptions.CurveLinear,
() => {
if (args.repeatCount !== undefined) {
UIView.setAnimationRepeatCount(args.repeatCount);
}
switch (animation.property) {
case Properties.backgroundColor:
animation.target.backgroundColor = args.toValue;
break;
case Properties.opacity:
animation.target.opacity = args.toValue;
break;
case Properties.height:
case Properties.width:
animation._originalValue =
animation.target[animation.property];
nativeView.layer.setValueForKey(
args.toValue,
args.propertyNameToAnimate
);
animation._propertyResetCallback = function (value) {
animation.target[animation.property] = value;
};
break;
case _transform:
animation._originalValue = nativeView.layer.transform;
nativeView.layer.setValueForKey(
args.toValue,
args.propertyNameToAnimate
);
animation._propertyResetCallback = function (value) {
nativeView.layer.transform = value;
};
break;
}
},
function (animationDidFinish: boolean) {
if (animationDidFinish) {
if (animation.property === _transform) {
if (
animation.value[Properties.translate] !== undefined
) {
animation.target.translateX =
animation.value[Properties.translate].x;
animation.target.translateY =
animation.value[Properties.translate].y;
}
if (animation.value[Properties.rotate] !== undefined) {
animation.target.rotateX =
animation.value[Properties.rotate].x;
animation.target.rotateY =
animation.value[Properties.rotate].y;
animation.target.rotate =
animation.value[Properties.rotate].z;
}
if (animation.value[Properties.scale] !== undefined) {
animation.target.scaleX =
animation.value[Properties.scale].x;
animation.target.scaleY =
animation.value[Properties.scale].y;
}
}
} else {
if (animation._propertyResetCallback) {
animation._propertyResetCallback(
animation._originalValue
);
}
}
if (finishedCallback) {
let cancelled = !animationDidFinish;
finishedCallback(cancelled);
}
if (animationDidFinish && nextAnimation) {
nextAnimation();
}
}
);
}
private static _createNativeAffineTransform(
animation: PropertyAnimation
): CATransform3D {
let value = animation.value;
let result: CATransform3D = CATransform3DIdentity;
if (value[Properties.translate] !== undefined) {
let x = value[Properties.translate].x;
let y = value[Properties.translate].y;
result = CATransform3DTranslate(result, x, y, 0);
}
if (value[Properties.scale] !== undefined) {
let x = value[Properties.scale].x;
let y = value[Properties.scale].y;
result = CATransform3DScale(
result,
x === 0 ? 0.001 : x,
y === 0 ? 0.001 : y,
1
);
}
return result;
}
private static _isAffineTransform(property: string): boolean {
return (
property === _transform ||
property === Properties.translate ||
property === Properties.scale
);
}
private static _canBeMerged(
animation1: PropertyAnimation,
animation2: PropertyAnimation
) {
let result =
Animation._isAffineTransform(animation1.property) &&
Animation._isAffineTransform(animation2.property) &&
animation1.target === animation2.target &&
animation1.duration === animation2.duration &&
animation1.delay === animation2.delay &&
animation1.iterations === animation2.iterations &&
animation1.curve === animation2.curve;
return result;
}
private static _mergeAffineTransformAnimations(
propertyAnimations: Array<PropertyAnimation>
): Array<PropertyAnimation> {
let result = new Array<PropertyAnimation>();
let i = 0;
let j;
let length = propertyAnimations.length;
for (; i < length; i++) {
if (propertyAnimations[i][_skip]) {
continue;
}
if (!Animation._isAffineTransform(propertyAnimations[i].property)) {
// This is not an affine transform animation, so there is nothing to merge.
result.push(propertyAnimations[i]);
} else {
// This animation has not been merged anywhere. Create a new transform animation.
// The value becomes a JSON object combining all affine transforms together like this:
// {
// translate: {x: 100, y: 100 },
// rotate: 90,
// scale: {x: 2, y: 2 }
// }
let newTransformAnimation: PropertyAnimation = {
target: propertyAnimations[i].target,
property: _transform,
value: {},
duration: propertyAnimations[i].duration,
delay: propertyAnimations[i].delay,
iterations: propertyAnimations[i].iterations,
curve: propertyAnimations[i].curve,
};
if (Trace.isEnabled()) {
Trace.write(
"Curve: " + propertyAnimations[i].curve,
Trace.categories.Animation
);
}
newTransformAnimation.value[propertyAnimations[i].property] =
propertyAnimations[i].value;
if (Trace.isEnabled()) {
Trace.write(
"Created new transform animation: " +
Animation._getAnimationInfo(newTransformAnimation),
Trace.categories.Animation
);
}
// Merge all compatible affine transform animations to the right into this new animation.
j = i + 1;
if (j < length) {
for (; j < length; j++) {
if (
Animation._canBeMerged(
propertyAnimations[i],
propertyAnimations[j]
)
) {
if (Trace.isEnabled()) {
Trace.write(
"Merging animations: " +
Animation._getAnimationInfo(
newTransformAnimation
) +
" + " +
Animation._getAnimationInfo(
propertyAnimations[j]
) +
";",
Trace.categories.Animation
);
}
newTransformAnimation.value[
propertyAnimations[j].property
] = propertyAnimations[j].value;
// Mark that it has been merged so we can skip it on our outer loop.
propertyAnimations[j][_skip] = true;
}
}
}
result.push(newTransformAnimation);
}
}
return result;
}
}
export function _getTransformMismatchErrorMessage(view: View): string {
const expectedTransform = calculateTransform(view);
const expectedTransformString = getCATransform3DString(expectedTransform);
const actualTransformString = getCATransform3DString(
view.nativeViewProtected.layer.transform
);
if (actualTransformString !== expectedTransformString) {
return (
"View and Native transforms do not match.\nActual: " +
actualTransformString +
";\nExpected: " +
expectedTransformString
);
}
return undefined;
}
function calculateTransform(view: View): CATransform3D {
const scaleX = view.scaleX || 1e-6;
const scaleY = view.scaleY || 1e-6;
const perspective = view.perspective || 300;
// Order is important: translate, rotate, scale
let expectedTransform = new CATransform3D(CATransform3DIdentity);
// Only set perspective if there is 3D rotation
if (view.rotateX || view.rotateY) {
expectedTransform.m34 = -1 / perspective;
}
expectedTransform = CATransform3DTranslate(
expectedTransform,
view.translateX,
view.translateY,
0
);
expectedTransform = iosNativeHelper.applyRotateTransform(
expectedTransform,
view.rotateX,
view.rotateY,
view.rotate
);
expectedTransform = CATransform3DScale(
expectedTransform,
scaleX,
scaleY,
1
);
return expectedTransform;
}
function getCATransform3DString(t: CATransform3D) {
return `[
${t.m11}, ${t.m12}, ${t.m13}, ${t.m14},
${t.m21}, ${t.m22}, ${t.m23}, ${t.m24},
${t.m31}, ${t.m32}, ${t.m33}, ${t.m34},
${t.m41}, ${t.m42}, ${t.m43}, ${t.m44}]`;
}