Fixed #801: Chained animations lose state on iOS.

This commit is contained in:
Rossen Hristov
2015-10-14 16:00:36 +03:00
parent f6ef20070e
commit 21b3a0a602
2 changed files with 147 additions and 24 deletions

View File

@ -1,6 +1,7 @@
import TKUnit = require("../../TKUnit");
import helper = require("../helper");
import pageModule = require("ui/page");
import viewModule = require("ui/core/view");
import labelModule = require("ui/label");
import stackLayoutModule = require("ui/layouts/stack-layout");
import colorModule = require("color");
@ -46,6 +47,7 @@ export var test_AnimatingProperties = function (done) {
.then(() => {
////console.log("Animation finished.");
// <hide>
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
// </hide>
@ -85,6 +87,7 @@ export var test_CancellingAnimation = function (done) {
.then(() => {
////console.log("Animation finished");
// <hide>
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
// </hide>
@ -130,6 +133,7 @@ export var test_ChainingAnimations = function (done) {
.then(() => {
////console.log("Animation finished");
// <hide>
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
// </hide>
@ -176,6 +180,7 @@ export var test_ReusingAnimations = function (done) {
.then(() => {
////console.log("Animation finished");
// <hide>
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
// </hide>
@ -227,6 +232,9 @@ export var test_AnimatingMultipleViews = function (done) {
.then(() => {
////console.log("Animations finished");
// <hide>
assertIOSNativeTransformIsCorrect(label1);
assertIOSNativeTransformIsCorrect(label2);
assertIOSNativeTransformIsCorrect(label3);
helper.goBack();
done();
// </hide>
@ -319,6 +327,7 @@ export var test_AnimateTranslate = function (done) {
.then(() => {
TKUnit.assert(label.translateX === 100);
TKUnit.assert(label.translateY === 200);
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
})
@ -348,6 +357,7 @@ export var test_AnimateScale = function (done) {
.then(() => {
TKUnit.assert(label.scaleX === 2);
TKUnit.assert(label.scaleY === 3);
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
})
@ -376,6 +386,7 @@ export var test_AnimateRotate = function (done) {
label.animate({ rotate: 123 })
.then(() => {
TKUnit.assert(label.rotate === 123);
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
})
@ -412,6 +423,7 @@ export var test_AnimateTranslateScaleAndRotateSimultaneously = function (done) {
TKUnit.assert(label.scaleX === 2);
TKUnit.assert(label.scaleY === 3);
TKUnit.assert(label.rotate === 123);
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
})
@ -421,6 +433,53 @@ export var test_AnimateTranslateScaleAndRotateSimultaneously = function (done) {
});
}
export var test_AnimateTranslateScaleAndRotateSequentially = function (done) {
var mainPage: pageModule.Page;
var label: labelModule.Label;
var pageFactory = function (): pageModule.Page {
label = new labelModule.Label();
label.text = "label";
var stackLayout = new stackLayoutModule.StackLayout();
stackLayout.addChild(label);
mainPage = new pageModule.Page();
mainPage.content = stackLayout;
return mainPage;
};
helper.navigate(pageFactory);
TKUnit.waitUntilReady(() => { return label.isLoaded });
label.animate({translate: { x: 100, y: 200 }})
.then(() => {
TKUnit.assert(label.translateX === 100);
TKUnit.assert(label.translateY === 200);
assertIOSNativeTransformIsCorrect(label);
return label.animate({ scale: { x: 2, y: 3 } });
})
.then(() => {
TKUnit.assert(label.translateX === 100);
TKUnit.assert(label.translateY === 200);
TKUnit.assert(label.scaleX === 2);
TKUnit.assert(label.scaleY === 3);
assertIOSNativeTransformIsCorrect(label);
return label.animate({ rotate: 123 });
})
.then(() => {
TKUnit.assert(label.translateX === 100);
TKUnit.assert(label.translateY === 200);
TKUnit.assert(label.scaleX === 2);
TKUnit.assert(label.scaleY === 3);
TKUnit.assert(label.rotate === 123);
assertIOSNativeTransformIsCorrect(label);
helper.goBack();
done();
})
.catch((e) => {
helper.goBack();
done(e);
});
}
export var test_AnimationsAreAlwaysPlayed = function (done) {
var mainPage: pageModule.Page;
var label: labelModule.Label;
@ -517,4 +576,13 @@ export var test_PlayPromiseIsRejectedWhenAnimationIsCancelled = function (done)
});
animation.cancel();
}
function assertIOSNativeTransformIsCorrect(view: viewModule.View) {
if (view.ios) {
var errorMessage = (<any>animation)._getTransformMismatchErrorMessage(view);
if (errorMessage) {
TKUnit.assert(false, errorMessage);
}
}
}

View File

@ -1,5 +1,6 @@
import definition = require("ui/animation");
import common = require("./animation-common");
import viewModule = require("ui/core/view");
import trace = require("trace");
global.moduleMerge(common, exports);
@ -104,10 +105,11 @@ export class Animation extends common.Animation implements definition.Animation
trace.write(that._finishedAnimations + " animations finished.", trace.categories.Animation);
// Update our properties on the view.
var i = 0;
// This should not change the native transform which is already updated by the animation itself.
var i;
var len = that._propertyAnimations.length;
var a: common.PropertyAnimation;
for (; i < len; i++) {
for (i = 0; i < len; i++) {
a = that._propertyAnimations[i];
switch (a.property) {
case common.Properties.translate:
@ -124,6 +126,15 @@ export class Animation extends common.Animation implements definition.Animation
}
}
// Validate that the properties of our view are aligned with the native transform matrix.
for (i = 0; i < len; i++) {
a = that._propertyAnimations[i];
var errorMessage = _getTransformMismatchErrorMessage(a.target);
if (errorMessage) {
throw new Error(errorMessage);
}
}
that._resolveAnimationFinishedPromise();
}
}
@ -201,7 +212,7 @@ export class Animation extends common.Animation implements definition.Animation
case _transform:
originalValue = nativeView.transform;
(<any>animation)._propertyResetCallback = () => { nativeView.transform = originalValue };
nativeView.transform = animation.value;
nativeView.transform = Animation._createNativeAffineTransform(animation);
break;
default:
throw new Error("Cannot animate " + animation.property);
@ -217,6 +228,43 @@ export class Animation extends common.Animation implements definition.Animation
}
}
private static _createNativeAffineTransform(animation: common.PropertyAnimation): CGAffineTransform {
var view = animation.target;
var value = animation.value;
trace.write("Creating native affine transform. Curent transform is: " + NSStringFromCGAffineTransform(view._nativeView.transform), trace.categories.Animation);
// Order is important: translate, rotate, scale
var result: CGAffineTransform = CGAffineTransformIdentity;
trace.write("Identity: " + NSStringFromCGAffineTransform(result), trace.categories.Animation);
if (value[common.Properties.translate] !== undefined) {
result = CGAffineTransformTranslate(result, value[common.Properties.translate].x, value[common.Properties.translate].y);
}
else {
result = CGAffineTransformTranslate(result, view.translateX, view.translateY);
}
trace.write("After translate: " + NSStringFromCGAffineTransform(result), trace.categories.Animation);
if (value[common.Properties.rotate] !== undefined) {
result = CGAffineTransformRotate(result, value[common.Properties.rotate] * Math.PI / 180);
}
else {
result = CGAffineTransformRotate(result, view.rotate * Math.PI / 180);
}
trace.write("After rotate: " + NSStringFromCGAffineTransform(result), trace.categories.Animation);
if (value[common.Properties.scale] !== undefined) {
result = CGAffineTransformScale(result, value[common.Properties.scale].x, value[common.Properties.scale].y);
}
else {
result = CGAffineTransformScale(result, view.scaleX, view.scaleY);
}
trace.write("After scale: " + NSStringFromCGAffineTransform(result), trace.categories.Animation);
return result;
}
private static _isAffineTransform(property: string): boolean {
return property === _transform
|| property === common.Properties.translate
@ -236,20 +284,6 @@ export class Animation extends common.Animation implements definition.Animation
return result;
}
private static _affineTransform(matrix: CGAffineTransform, property: string, value: any): CGAffineTransform {
switch (property) {
case common.Properties.translate:
return CGAffineTransformTranslate(matrix, value.x, value.y);
case common.Properties.rotate:
return CGAffineTransformRotate(matrix, value * Math.PI / 180);
case common.Properties.scale:
return CGAffineTransformScale(matrix, value.x, value.y);
default:
throw new Error("Cannot create transform for" + property);
break;
}
}
private static _mergeAffineTransformAnimations(propertyAnimations: Array<common.PropertyAnimation>): Array<common.PropertyAnimation> {
var result = new Array<common.PropertyAnimation>();
@ -266,27 +300,31 @@ export class Animation extends common.Animation implements definition.Animation
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 }
// }
var newTransformAnimation: common.PropertyAnimation = {
target: propertyAnimations[i].target,
property: _transform,
value: Animation._affineTransform(CGAffineTransformIdentity, propertyAnimations[i].property, propertyAnimations[i].value),
value: {},
duration: propertyAnimations[i].duration,
delay: propertyAnimations[i].delay,
iterations: propertyAnimations[i].iterations
};
newTransformAnimation.value[propertyAnimations[i].property] = propertyAnimations[i].value;
trace.write("Created new transform animation: " + common.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) {
// Merge all compatible affine transform animations to the right into this new animation.
for (; j < length; j++) {
if (Animation._canBeMerged(propertyAnimations[i], propertyAnimations[j])) {
trace.write("Merging animations: " + common.Animation._getAnimationInfo(newTransformAnimation) + " + " + common.Animation._getAnimationInfo(propertyAnimations[j]) + " = ", trace.categories.Animation);
trace.write("New native transform is: " + NSStringFromCGAffineTransform(newTransformAnimation.value), trace.categories.Animation);
newTransformAnimation.value = Animation._affineTransform(newTransformAnimation.value, propertyAnimations[j].property, propertyAnimations[j].value);
trace.write("Merging animations: " + common.Animation._getAnimationInfo(newTransformAnimation) + " + " + common.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;
}
@ -299,4 +337,21 @@ export class Animation extends common.Animation implements definition.Animation
return result;
}
}
export function _getTransformMismatchErrorMessage(view: viewModule.View): string {
// Order is important: translate, rotate, scale
var result: CGAffineTransform = CGAffineTransformIdentity;
result = CGAffineTransformTranslate(result, view.translateX, view.translateY);
result = CGAffineTransformRotate(result, view.rotate * Math.PI / 180);
result = CGAffineTransformScale(result, view.scaleX, view.scaleY);
var viewTransform = NSStringFromCGAffineTransform(result);
var nativeTransform = NSStringFromCGAffineTransform(view._nativeView.transform);
if (viewTransform !== nativeTransform) {
return "View and Native transforms do not match. View: " + viewTransform + "; Native: " + nativeTransform;
}
return undefined;
}