From 21b3a0a60206bc0395b625d7678a5802a99eb332 Mon Sep 17 00:00:00 2001 From: Rossen Hristov Date: Wed, 14 Oct 2015 16:00:36 +0300 Subject: [PATCH] Fixed #801: Chained animations lose state on iOS. --- apps/tests/ui/animation/animation-tests.ts | 68 ++++++++++++++ ui/animation/animation.ios.ts | 103 ++++++++++++++++----- 2 files changed, 147 insertions(+), 24 deletions(-) diff --git a/apps/tests/ui/animation/animation-tests.ts b/apps/tests/ui/animation/animation-tests.ts index b75df1340..3b25f9447 100644 --- a/apps/tests/ui/animation/animation-tests.ts +++ b/apps/tests/ui/animation/animation-tests.ts @@ -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."); // + assertIOSNativeTransformIsCorrect(label); helper.goBack(); done(); // @@ -85,6 +87,7 @@ export var test_CancellingAnimation = function (done) { .then(() => { ////console.log("Animation finished"); // + assertIOSNativeTransformIsCorrect(label); helper.goBack(); done(); // @@ -130,6 +133,7 @@ export var test_ChainingAnimations = function (done) { .then(() => { ////console.log("Animation finished"); // + assertIOSNativeTransformIsCorrect(label); helper.goBack(); done(); // @@ -176,6 +180,7 @@ export var test_ReusingAnimations = function (done) { .then(() => { ////console.log("Animation finished"); // + assertIOSNativeTransformIsCorrect(label); helper.goBack(); done(); // @@ -227,6 +232,9 @@ export var test_AnimatingMultipleViews = function (done) { .then(() => { ////console.log("Animations finished"); // + assertIOSNativeTransformIsCorrect(label1); + assertIOSNativeTransformIsCorrect(label2); + assertIOSNativeTransformIsCorrect(label3); helper.goBack(); done(); // @@ -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 = (animation)._getTransformMismatchErrorMessage(view); + if (errorMessage) { + TKUnit.assert(false, errorMessage); + } + } } \ No newline at end of file diff --git a/ui/animation/animation.ios.ts b/ui/animation/animation.ios.ts index 4a64ddb64..4d79f8d5c 100644 --- a/ui/animation/animation.ios.ts +++ b/ui/animation/animation.ios.ts @@ -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; (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): Array { var result = new Array(); @@ -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; }