diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj
index 34ba87a53..9688da4ba 100644
--- a/CrossPlatformModules.csproj
+++ b/CrossPlatformModules.csproj
@@ -135,6 +135,7 @@
page-title-icon.xml
+
animation.d.ts
@@ -1763,7 +1764,7 @@
PreserveNewest
-
+
PreserveNewest
diff --git a/apps/animations/main-page.ts b/apps/animations/main-page.ts
index 7b31af095..39c7d293f 100644
--- a/apps/animations/main-page.ts
+++ b/apps/animations/main-page.ts
@@ -2,130 +2,92 @@ import observable = require("data/observable");
import pages = require("ui/page");
import buttonModule = require("ui/button");
import abs = require("ui/layouts/absolute-layout");
-import animation = require("ui/animation");
+import animationModule = require("ui/animation");
import colorModule = require("color");
import model = require("./model");
var vm = new model.ViewModel();
var page: pages.Page;
-var panel1: abs.AbsoluteLayout;
+var panel: abs.AbsoluteLayout;
var button1: buttonModule.Button;
var button2: buttonModule.Button;
var button3: buttonModule.Button;
-var cancelToken: any;
+var buttonAnimation: animationModule.Animation;
+var panelAnimation: animationModule.Animation;
export function pageLoaded(args: observable.EventData) {
page = args.object;
page.bindingContext = vm;
- panel1 = page.getViewById("panel1");
+ panel = page.getViewById("panel1");
button1 = page.getViewById("button1");
button2 = page.getViewById("button2");
button3 = page.getViewById("button3");
}
export function onSlideOut(args: observable.EventData) {
- var animations: Array;
+ console.log("onSlideOut");
+ var curve = page.android ? new android.view.animation.AccelerateInterpolator(1) : UIViewAnimationCurve.UIViewAnimationCurveEaseIn;
- animations = new Array();
- animations.push({ target: button1, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: button1, property: animation.Properties.scale, value: { x: 0.5, y: 0.5 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: button1, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
+ var buttonAnimations = [
+ { target: button1, translate: { x: -240, y: 0 }, scale: { x: 0.5, y: 0.5 }, opacity: 0, duration: vm.duration, delay: 0, iterations: vm.iterations, curve: curve },
+ { target: button2, translate: { x: -240, y: 0 }, scale: { x: 0.5, y: 0.5 }, opacity: 0, duration: vm.duration, delay: vm.duration, iterations: vm.iterations, curve: curve },
+ { target: button3, translate: { x: -240, y: 0 }, scale: { x: 0.5, y: 0.5 }, opacity: 0, duration: vm.duration, delay: vm.duration * 2, iterations: vm.iterations, curve: curve },
+ ]
+ buttonAnimation = new animationModule.Animation(buttonAnimations, vm.playSequentially);
- animations.push({ target: button2, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
- animations.push({ target: button2, property: animation.Properties.scale, value: { x: 0.5, y: 0.5 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
- animations.push({ target: button2, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
+ panelAnimation = panel.createAnimation({ opacity: 0, scale: { x: 0.5, y: 0.5 }, rotate: -360, backgroundColor: new colorModule.Color("red"), duration: vm.duration, iterations: vm.iterations });
- animations.push({ target: button3, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
- animations.push({ target: button3, property: animation.Properties.scale, value: { x: 0, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
- animations.push({ target: button3, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
-
- configureAnimationCurve(animations, true);
-
- cancelToken = animation.start(animations, vm.playSequentially, (cancelled?: boolean) => {
- if (cancelled) {
- console.log("Buttons slide out animations cancelled");
- return;
- }
- console.log("Buttons slide out animations completed!");
-
- animations = new Array();
- animations.push({ target: panel1, property: animation.Properties.scale, value: { x: 0, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: panel1, property: animation.Properties.rotate, value: 1080, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: panel1, property: animation.Properties.backgroundColor, value: new colorModule.Color("red"), duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- configureAnimationCurve(animations, true);
-
- cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
- if (cancelled) {
- console.log("Panel animation cancelled");
- return;
- }
- console.log("Panel animation completed!");
- });
- });
+ buttonAnimation.play().finished
+ .then(() => panelAnimation.play().finished)
+ .catch((e) => console.log(e.message));
}
export function onSlideIn(args: observable.EventData) {
- var animations: Array;
+ console.log("onSlideIn");
+ var curve = page.android ? new android.view.animation.DecelerateInterpolator(1) : UIViewAnimationCurve.UIViewAnimationCurveEaseOut;
- animations = new Array();
- animations.push({ target: panel1, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: panel1, property: animation.Properties.rotate, value: 0, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: panel1, property: animation.Properties.backgroundColor, value: new colorModule.Color("yellow"), duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- configureAnimationCurve(animations, false);
+ panelAnimation = panel.createAnimation({ opacity: 1, scale: { x: 1, y: 1 }, rotate: 0, backgroundColor: new colorModule.Color("yellow"), duration: vm.duration, iterations: vm.iterations });
- cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
- if (cancelled) {
- console.log("Panel animation cancelled");
- return;
- }
- console.log("Panel animation completed!");
+ var buttonAnimations = [
+ { target: button3, translate: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, opacity: 1, duration: vm.duration, delay: 0, iterations: vm.iterations, curve: curve },
+ { target: button2, translate: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, opacity: 1, duration: vm.duration, delay: vm.duration, iterations: vm.iterations, curve: curve },
+ { target: button1, translate: { x: 0, y: 0 }, scale: { x: 1, y: 1 }, opacity: 1, duration: vm.duration, delay: vm.duration * 2, iterations: vm.iterations, curve: curve },
+ ]
+ buttonAnimation = new animationModule.Animation(buttonAnimations, vm.playSequentially);
- animations = new Array();
- animations.push({ target: button1, property: animation.Properties.translate, value: { x: 0, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: button1, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
- animations.push({ target: button1, property: animation.Properties.opacity, value: 1, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
-
- animations.push({ target: button2, property: animation.Properties.translate, value: { x: 0, y: 0 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
- animations.push({ target: button2, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
- animations.push({ target: button2, property: animation.Properties.opacity, value: 1, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
-
- animations.push({ target: button3, property: animation.Properties.translate, value: { x: 0, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
- animations.push({ target: button3, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
- animations.push({ target: button3, property: animation.Properties.opacity, value: 1, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
-
- configureAnimationCurve(animations, false);
-
- cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
- if (cancelled) {
- console.log("Buttons slide in animations cancelled");
- return;
- }
- console.log("Buttons slide in animations completed!");
- });
- });
+ panelAnimation.play().finished
+ .then(() => buttonAnimation.play().finished)
+ .catch((e) => console.log(e.message));
}
-function configureAnimationCurve(animations: Array, slideOut: boolean) {
- var i = 0;
- var length = animations.length;
- if (page.android) {
- var interpolator = slideOut ? new android.view.animation.AccelerateInterpolator(1) : new android.view.animation.DecelerateInterpolator(1);
- for (; i < length; i++) {
- animations[i].androidInterpolator = interpolator;
- }
+export function onCancel(args: observable.EventData) {
+ console.log("onCancel");
+ if (panelAnimation.isPlaying) {
+ panelAnimation.cancel();
}
- else {
- for (; i < length; i++) {
- animations[i].iosUIViewAnimationCurve = slideOut ? UIViewAnimationCurve.UIViewAnimationCurveEaseIn : UIViewAnimationCurve.UIViewAnimationCurveEaseOut;
- }
+ if (buttonAnimation.isPlaying) {
+ buttonAnimation.cancel();
}
}
-export function onStop(args: observable.EventData) {
- cancelToken.cancel();
-}
-
export function onTap(args: observable.EventData) {
console.log((args.object).text);
+}
+
+export function onSingle(args: observable.EventData) {
+ console.log("onSingle");
+ button1.animate({
+ opacity: 0.75,
+ backgroundColor: new colorModule.Color("Red"),
+ translate: { x: 100, y: 100 },
+ scale: { x: 2, y: 2 },
+ rotate: 180,
+ duration: vm.duration,
+ delay: 0,
+ iterations: vm.iterations,
+ curve: button1.ios ? UIViewAnimationCurve.UIViewAnimationCurveEaseIn : new android.view.animation.AccelerateInterpolator(1),
+ })
+ .then(() => console.log("Animation finished"))
+ .catch((e) => console.log(e.message));
}
\ No newline at end of file
diff --git a/apps/animations/main-page.xml b/apps/animations/main-page.xml
index 1e2ee8387..8a132ce87 100644
--- a/apps/animations/main-page.xml
+++ b/apps/animations/main-page.xml
@@ -5,8 +5,8 @@
-
-
+
+
@@ -14,9 +14,10 @@
-
-
-
+
+
+
+
diff --git a/apps/animations/model.ts b/apps/animations/model.ts
index 083343379..8123a1c26 100644
--- a/apps/animations/model.ts
+++ b/apps/animations/model.ts
@@ -4,8 +4,8 @@ export class ViewModel extends observable.Observable {
constructor() {
super();
- this._duration = 300;
- this._repeatCount = 0;
+ this._duration = 3000;
+ this._iterations = 1;
}
private _playSequentially: boolean;
@@ -26,12 +26,12 @@ export class ViewModel extends observable.Observable {
this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "duration", value: value });
}
- private _repeatCount: number;
- get repeatCount(): number {
- return this._repeatCount;
+ private _iterations: number;
+ get iterations(): number {
+ return this._iterations;
}
- set repeatCount(value: number) {
- this._repeatCount = value;
- this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "repeatCount", value: value });
+ set iterations(value: number) {
+ this._iterations = value;
+ this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "iterations", value: value });
}
}
\ No newline at end of file
diff --git a/apps/tests/testRunner.ts b/apps/tests/testRunner.ts
index 59680937e..25399b0b2 100644
--- a/apps/tests/testRunner.ts
+++ b/apps/tests/testRunner.ts
@@ -78,6 +78,7 @@ allTests["WEAK-EVENTS"] = require("./weak-event-listener-tests");
allTests["REPEATER"] = require("./ui/repeater/repeater-tests");
allTests["SEARCH-BAR"] = require('./ui/search-bar/search-bar-tests');
allTests["CONNECTIVITY"] = require("./connectivity-tests");
+allTests["ANIMATION"] = require("./ui/animation/animation-tests");
if (!isRunningOnEmulator()) {
allTests["LOCATION"] = require("./location-tests");
diff --git a/apps/tests/ui/animation/animation-tests.ts b/apps/tests/ui/animation/animation-tests.ts
new file mode 100644
index 000000000..48544bcc7
--- /dev/null
+++ b/apps/tests/ui/animation/animation-tests.ts
@@ -0,0 +1,243 @@
+import TKUnit = require("../../TKUnit");
+import helper = require("../helper");
+import pageModule = require("ui/page");
+import labelModule = require("ui/label");
+import stackLayoutModule = require("ui/layouts/stack-layout");
+import colorModule = require("color");
+
+//
+// # Animation
+// Animating view properties requires the "ui/animation" module.
+// ``` JavaScript
+import animation = require("ui/animation");
+// ```
+//
+
+export var test_AnimatingProperties = 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 });
+
+ //
+ // # Animating properties
+ // ``` JavaScript
+ label.animate({
+ opacity: 0.75,
+ backgroundColor: new colorModule.Color("Red"),
+ translate: { x: 100, y: 100 },
+ scale: { x: 2, y: 2 },
+ rotate: 180,
+ duration: 1000,
+ delay: 100,
+ iterations: 3,
+ curve: label.ios ? UIViewAnimationCurve.UIViewAnimationCurveEaseIn : new android.view.animation.AccelerateInterpolator(1),
+ })
+ .then(() => {
+ console.log("Animation finished.");
+ //
+ helper.goBack();
+ done();
+ //
+ })
+ .catch((e) => {
+ console.log(e.message);
+ //
+ helper.goBack();
+ done(e);
+ //
+ });
+ // ```
+ //
+}
+
+export var test_CancellingAnimation = 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 });
+
+ //
+ // # Cancelling animation
+ // ``` JavaScript
+ var animation1 = label.createAnimation({ translate: { x: 100, y: 100 } });
+ animation1.play().finished
+ .then(() => {
+ console.log("Animation finished");
+ //
+ helper.goBack();
+ done();
+ //
+ })
+ .catch((e) => {
+ console.log("Animation cancelled");
+ //
+ helper.goBack();
+ done(e);
+ //
+ });
+ animation1.cancel();
+ // ```
+ //
+}
+
+export var test_ChainingAnimations = 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 });
+
+ //
+ // # Chaining animations
+ // ``` JavaScript
+ label.animate({ opacity: 0 })
+ .then(() => label.animate({ opacity: 1 }))
+ .then(() => label.animate({ translate: { x: 200, y: 200 } }))
+ .then(() => label.animate({ translate: { x: 0, y: 0 } }))
+ .then(() => label.animate({ scale: { x: 5, y: 5 } }))
+ .then(() => label.animate({ scale: { x: 1, y: 1 } }))
+ .then(() => label.animate({ rotate: 180 }))
+ .then(() => label.animate({ rotate: 0 }))
+ .then(() => {
+ console.log("Animation finished");
+ //
+ helper.goBack();
+ done();
+ //
+ })
+ .catch((e) => {
+ console.log(e.message);
+ //
+ helper.goBack();
+ done(e);
+ //
+ });
+ // ```
+ //
+}
+
+export var test_ReusingAnimations = 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 });
+
+ //
+ // # Reusing animations
+ // ``` JavaScript
+ var animation1 = label.createAnimation({ translate: { x: 100, y: 100 } });
+ var animation2 = label.createAnimation({ translate: { x: 0, y: 0 } });
+
+ animation1.play().finished
+ .then(() => animation2.play().finished)
+ .then(() => animation1.play().finished)
+ .then(() => animation2.play().finished)
+ .then(() => animation1.play().finished)
+ .then(() => animation2.play().finished)
+ .then(() => {
+ console.log("Animation finished");
+ //
+ helper.goBack();
+ done();
+ //
+ })
+ .catch((e) => {
+ console.log(e.message);
+ //
+ helper.goBack();
+ done(e);
+ //
+ });
+ // ```
+ //
+}
+
+export var test_AnimatingMultipleViews = function (done) {
+ var mainPage: pageModule.Page;
+ var label1: labelModule.Label;
+ var label2: labelModule.Label;
+ var label3: labelModule.Label;
+ var pageFactory = function (): pageModule.Page {
+ label1 = new labelModule.Label();
+ label1.text = "label1";
+ label2 = new labelModule.Label();
+ label2.text = "label2";
+ label3 = new labelModule.Label();
+ label3.text = "label3";
+ var stackLayout = new stackLayoutModule.StackLayout();
+ stackLayout.addChild(label1);
+ stackLayout.addChild(label2);
+ stackLayout.addChild(label3);
+ mainPage = new pageModule.Page();
+ mainPage.content = stackLayout;
+ return mainPage;
+ };
+ helper.navigate(pageFactory);
+ TKUnit.waitUntilReady(() => { return label1.isLoaded && label2.isLoaded });
+
+ //
+ // # Animating multiple views simultaneously
+ // ``` JavaScript
+ var animations: Array = [
+ { target: label1, translate: { x: 200, y: 200 }, duration: 1000, delay: 0 },
+ { target: label2, translate: { x: 200, y: 200 }, duration: 1000, delay: 333 },
+ { target: label3, translate: { x: 200, y: 200 }, duration: 1000, delay: 666 },
+ ];
+ var animation = new animation.Animation(animations);
+ animation.play().finished
+ .then(() => {
+ console.log("Animations finished");
+ //
+ helper.goBack();
+ done();
+ //
+ })
+ .catch((e) => {
+ console.log(e.message);
+ //
+ helper.goBack();
+ done(e);
+ //
+ });
+ // ```
+ //
+}
\ No newline at end of file
diff --git a/ui/animation/animation-common.ts b/ui/animation/animation-common.ts
index af26353d8..906aac1c3 100644
--- a/ui/animation/animation-common.ts
+++ b/ui/animation/animation-common.ts
@@ -1,7 +1,180 @@
-export module Properties {
+import definition = require("ui/animation");
+import viewModule = require("ui/core/view");
+import trace = require("trace");
+
+export module Properties {
export var opacity = "opacity";
export var backgroundColor = "backgroundColor";
export var translate = "translate";
export var rotate = "rotate";
export var scale = "scale";
-}
\ No newline at end of file
+}
+
+export interface PropertyAnimation {
+ target: viewModule.View;
+ property: string;
+ value: any;
+ duration?: number;
+ delay?: number;
+ iterations?: number;
+ curve?: any;
+}
+
+export class Animation implements definition.Animation {
+ public _propertyAnimations: Array;
+ public _playSequentially: boolean;
+ private _isPlaying: boolean;
+ private _resolve;
+ private _reject;
+ private _animationFinishedPromise: Promise;
+
+ public play(): Animation {
+ if (this.isPlaying) {
+ throw new Error("Animation is already playing.");
+ }
+
+ this._isPlaying = true;
+ return this;
+ }
+
+ public cancel(): void {
+ if (!this.isPlaying) {
+ throw new Error("Animation is not currently playing.");
+ }
+ }
+
+ public get finished(): Promise {
+ return this._animationFinishedPromise;
+ }
+
+ public get isPlaying(): boolean {
+ return this._isPlaying;
+ }
+
+ constructor(animationDefinitions: Array, playSequentially?: boolean) {
+ if (!animationDefinitions || animationDefinitions.length === 0) {
+ throw new Error("No animation definitions specified");
+ }
+
+ trace.write("Analyzing " + animationDefinitions.length + " animation definitions...", trace.categories.Animation);
+ this._propertyAnimations = new Array();
+ var i = 0;
+ var length = animationDefinitions.length;
+ for (; i < length; i++) {
+ this._propertyAnimations = this._propertyAnimations.concat(Animation._createPropertyAnimations(animationDefinitions[i]));
+ }
+
+ if (this._propertyAnimations.length === 0) {
+ throw new Error("Nothing to animate.");
+ }
+ trace.write("Created " + this._propertyAnimations.length + " individual property animations.", trace.categories.Animation);
+
+ this._playSequentially = playSequentially;
+ var that = this;
+ this._animationFinishedPromise = new Promise((resolve, reject) => {
+ that._resolve = resolve;
+ that._reject = reject;
+ });
+ }
+
+ public _resolveAnimationFinishedPromise() {
+ this._isPlaying = false;
+ this._resolve();
+ }
+
+ public _rejectAnimationFinishedPromise() {
+ this._isPlaying = false;
+ this._reject(new Error("Animation cancelled."));
+ }
+
+ private static _createPropertyAnimations(animationDefinition: definition.AnimationDefinition): Array {
+ if (!animationDefinition.target) {
+ throw new Error("No animation target specified.");
+ }
+
+ var propertyAnimations = new Array();
+
+ // opacity
+ if (animationDefinition.opacity !== undefined) {
+ propertyAnimations.push({
+ target: animationDefinition.target,
+ property: Properties.opacity,
+ value: animationDefinition.opacity,
+ duration: animationDefinition.duration,
+ delay: animationDefinition.delay,
+ iterations: animationDefinition.iterations,
+ curve: animationDefinition.curve
+ });
+ }
+
+ // backgroundColor
+ if (animationDefinition.backgroundColor !== undefined) {
+ propertyAnimations.push({
+ target: animationDefinition.target,
+ property: Properties.backgroundColor,
+ value: animationDefinition.backgroundColor,
+ duration: animationDefinition.duration,
+ delay: animationDefinition.delay,
+ iterations: animationDefinition.iterations,
+ curve: animationDefinition.curve
+ });
+ }
+
+ // translate
+ if (animationDefinition.translate !== undefined) {
+ propertyAnimations.push({
+ target: animationDefinition.target,
+ property: Properties.translate,
+ value: animationDefinition.translate,
+ duration: animationDefinition.duration,
+ delay: animationDefinition.delay,
+ iterations: animationDefinition.iterations,
+ curve: animationDefinition.curve
+ });
+ }
+
+ // scale
+ if (animationDefinition.scale !== undefined) {
+ propertyAnimations.push({
+ target: animationDefinition.target,
+ property: Properties.scale,
+ value: animationDefinition.scale,
+ duration: animationDefinition.duration,
+ delay: animationDefinition.delay,
+ iterations: animationDefinition.iterations,
+ curve: animationDefinition.curve
+ });
+ }
+
+ // rotate
+ if (animationDefinition.rotate !== undefined) {
+ propertyAnimations.push({
+ target: animationDefinition.target,
+ property: Properties.rotate,
+ value: animationDefinition.rotate,
+ duration: animationDefinition.duration,
+ delay: animationDefinition.delay,
+ iterations: animationDefinition.iterations,
+ curve: animationDefinition.curve
+ });
+ }
+
+ if (propertyAnimations.length === 0) {
+ throw new Error("No animation property specified.");
+ }
+
+ return propertyAnimations;
+ }
+
+ public static _getAnimationInfo(animation: PropertyAnimation): string {
+ return JSON.stringify({
+ target: animation.target.id,
+ property: animation.property,
+ value: animation.value,
+ duration: animation.duration,
+ delay: animation.delay,
+ iterations: animation.iterations,
+ curve: animation.curve
+ });
+ }
+}
diff --git a/ui/animation/animation.android.ts b/ui/animation/animation.android.ts
index 60b375b1f..023810cb1 100644
--- a/ui/animation/animation.android.ts
+++ b/ui/animation/animation.android.ts
@@ -9,219 +9,248 @@ import types = require("utils/types");
declare var exports;
require("utils/module-merge").merge(common, exports);
-var density = utils.layout.getDisplayDensity();
var intType = java.lang.Integer.class.getField("TYPE").get(null);
var floatType = java.lang.Float.class.getField("TYPE").get(null);
-var argbEvaluator = new android.animation.ArgbEvaluator();
+var argbEvaluator: android.animation.ArgbEvaluator = new android.animation.ArgbEvaluator();
-function getAnimationInfo(animation: definition.Animation): string {
- return JSON.stringify({
- target: animation.target.id,
- property: animation.property,
- value: animation.value,
- duration: animation.duration,
- delay: animation.delay,
- repeatCount: animation.repeatCount,
- androidInterpolator: animation.androidInterpolator
- });
-}
+export class Animation extends common.Animation implements definition.Animation {
+ private _animatorListener: android.animation.Animator.AnimatorListener;
+ private _nativeAnimatorsArray: any;
+ private _animatorSet: android.animation.AnimatorSet;
+ private _animators: Array;
+ private _propertyUpdateCallbacks: Array;
+ private _propertyResetCallbacks: Array;
-function createAndroidAnimators(animation: definition.Animation): any {
- trace.write("Creating ObjectAnimator(s) for animation: " + getAnimationInfo(animation) + "...", trace.categories.Animation);
+ public play(): Animation {
+ super.play();
- if (types.isNullOrUndefined(animation.value)) {
- throw new Error("Animation value cannot be null or undefined!");
- }
+ var i: number;
+ var length: number;
- var nativeArray;
- var nativeView = (animation.target._nativeView);
- var animators = new Array();
- var propertyUpdateCallbacks = new Array();
- var propertyResetCallbacks = new Array();
- var animator: android.animation.ValueAnimator;
- var originalValue;
- switch (animation.property) {
+ this._animators = new Array();
+ this._propertyUpdateCallbacks = new Array();
+ this._propertyResetCallbacks = new Array();
- case definition.Properties.opacity:
- originalValue = nativeView.getAlpha();
- if (animation.value !== animation.target.opacity) {
- nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
- nativeArray[0] = animation.value;
- animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "alpha", nativeArray));
- propertyUpdateCallbacks.push(() => { animation.target.opacity = animation.value });
- propertyResetCallbacks.push(() => { nativeView.setAlpha(originalValue); });
- }
- break;
-
- case definition.Properties.backgroundColor:
- originalValue = nativeView.getBackground();
- if (!color.Color.equals(animation.value, animation.target.backgroundColor)) {
- nativeArray = java.lang.reflect.Array.newInstance(intType, 1);
- nativeArray[0] = (animation.value).argb;
- animator = android.animation.ObjectAnimator.ofInt(nativeView, "backgroundColor", nativeArray);
- animator.setEvaluator(argbEvaluator);
- animators.push(animator);
- propertyUpdateCallbacks.push(() => { animation.target.backgroundColor = animation.value; });
- propertyResetCallbacks.push(() => { nativeView.setBackground(originalValue); });
- }
- break;
-
- case definition.Properties.translate:
- originalValue = nativeView.getTranslationX();
- if (animation.value.x * density !== originalValue) {
- nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
- nativeArray[0] = animation.value.x * density;
- animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "translationX", nativeArray));
- propertyResetCallbacks.push(() => { nativeView.setTranslationX(originalValue); });
- }
-
- originalValue = nativeView.getTranslationY();
- if (animation.value.y * density !== originalValue) {
- nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
- nativeArray[0] = animation.value.y * density;
- animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "translationY", nativeArray));
- propertyResetCallbacks.push(() => { nativeView.setTranslationY(originalValue); });
- }
- break;
-
- case definition.Properties.rotate:
- originalValue = nativeView.getRotation();
- if (animation.value !== originalValue) {
- nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
- nativeArray[0] = animation.value;
- animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "rotation", nativeArray));
- propertyResetCallbacks.push(() => { nativeView.setRotation(originalValue); });
- }
- break;
-
- case definition.Properties.scale:
- originalValue = nativeView.getScaleX();
- if (animation.value.x !== originalValue) {
- nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
- nativeArray[0] = animation.value.x;
- animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "scaleX", nativeArray));
- propertyResetCallbacks.push(() => { nativeView.setScaleX(originalValue); });
- }
-
- originalValue = nativeView.getScaleY();
- if (animation.value.y !== originalValue) {
- nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
- nativeArray[0] = animation.value.y;
- animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "scaleY", nativeArray));
- propertyResetCallbacks.push(() => { nativeView.setScaleY(originalValue); });
- }
- break;
-
- default:
- throw new Error("Cannot animate " + animation.property);
- break;
- }
-
- var i = 0;
- var length = animators.length;
- for (; i < length; i++) {
- if (animation.duration) {
- animators[i].setDuration(animation.duration);
+ i = 0;
+ length = this._propertyAnimations.length;
+ for (; i < length; i++) {
+ this._createAnimators(this._propertyAnimations[i]);
}
- if (animation.delay) {
- animators[i].setStartDelay(animation.delay);
+
+ if (this._animators.length === 0) {
+ trace.write("Nothing to animate.", trace.categories.Animation);
+ this._resolveAnimationFinishedPromise();
+ return this;
}
- if (animation.repeatCount) {
- animators[i].setRepeatCount(animation.repeatCount);
+
+ this._nativeAnimatorsArray = java.lang.reflect.Array.newInstance(android.animation.ObjectAnimator.class, this._animators.length);
+ i = 0;
+ length = this._animators.length;
+ for (; i < length; i++) {
+ this._nativeAnimatorsArray[i] = this._animators[i];
}
- if (animation.androidInterpolator) {
- animators[i].setInterpolator(animation.androidInterpolator);
+
+ this._animatorSet = new android.animation.AnimatorSet();
+ this._animatorSet.addListener(this._animatorListener);
+ if (this._playSequentially) {
+ this._animatorSet.playSequentially(this._nativeAnimatorsArray);
}
- trace.write("ObjectAnimator created: " + animators[i], trace.categories.Animation);
- }
-
- return {
- animators: animators,
- propertyUpdateCallbacks: propertyUpdateCallbacks,
- propertyResetCallbacks: propertyResetCallbacks
- };
-}
-
-export var start = function start(animations: Array, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): definition.Cancelable {
- var i: number;
- var length: number;
-
- var animators = new Array();
- var propertyUpdateCallbacks = new Array();
- var propertyResetCallbacks = new Array();
-
- i = 0;
- length = animations.length;
- for (; i < length; i++) {
- var result = createAndroidAnimators(animations[i]);
- animators = animators.concat(result.animators);
- propertyUpdateCallbacks = propertyUpdateCallbacks.concat(result.propertyUpdateCallbacks);
- propertyResetCallbacks = propertyResetCallbacks.concat(result.propertyResetCallbacks);
- }
-
- if (animators.length === 0) {
- if (finishedCallback) {
- finishedCallback();
+ else {
+ this._animatorSet.playTogether(this._nativeAnimatorsArray);
}
- return;
+
+ trace.write("Starting " + this._nativeAnimatorsArray.length + " animations " + (this._playSequentially ? "sequentially." : "together."), trace.categories.Animation);
+ this._animatorSet.start();
+ return this;
}
- var nativeArray = java.lang.reflect.Array.newInstance(android.animation.Animator.class, animators.length);
- i = 0;
- length = animators.length;
- for (; i < length; i++) {
- nativeArray[i.toString()] = animators[i];
+ public cancel(): void {
+ super.cancel();
+ trace.write("Cancelling AnimatorSet.", trace.categories.Animation);
+ this._animatorSet.cancel();
}
- var animatorSet = new android.animation.AnimatorSet();
- if (playSequentially) {
- animatorSet.playSequentially(nativeArray);
- }
- else {
- animatorSet.playTogether(nativeArray);
+ constructor(animationDefinitions: Array, playSequentially?: boolean) {
+ super(animationDefinitions, playSequentially);
+
+ var that = this;
+ this._animatorListener = new android.animation.Animator.AnimatorListener({
+ onAnimationStart: function (animator: android.animation.Animator): void {
+ that._onAndroidAnimationStart();
+ },
+ onAnimationRepeat: function (animator: android.animation.Animator): void {
+ that._onAndroidAnimationRepeat();
+ },
+ onAnimationEnd: function (animator: android.animation.Animator): void {
+ that._onAndroidAnimationEnd();
+ },
+ onAnimationCancel: function (animator: android.animation.Animator): void {
+ that._onAndroidAnimationCancel();
+ }
+ });
}
- var cancelled: boolean;
- animatorSet.addListener(new android.animation.Animator.AnimatorListener({
- onAnimationStart: function (animator: android.animation.Animator): void {
- trace.write("AnimatorListener.onAnimationStart.", trace.categories.Animation);
- },
- onAnimationRepeat: function (animator: android.animation.Animator): void {
- trace.write("AnimatorListener.onAnimationRepeat.", trace.categories.Animation);
- },
- onAnimationEnd: function (animator: android.animation.Animator): void {
- trace.write("AnimatorListener.onAnimationEnd.", trace.categories.Animation);
- i = 0;
- if (cancelled) {
- length = propertyResetCallbacks.length;
- for (; i < length; i++) {
- propertyResetCallbacks[i]();
+ private _onAndroidAnimationStart() {
+ trace.write("AndroidAnimation._onAndroidAnimationStart.", trace.categories.Animation);
+ }
+
+ private _onAndroidAnimationRepeat() {
+ trace.write("AndroidAnimation._onAndroidAnimationRepeat.", trace.categories.Animation);
+ }
+
+ private _onAndroidAnimationEnd() {
+ trace.write("AndroidAnimation._onAndroidAnimationEnd.", trace.categories.Animation);
+
+ if (!this.isPlaying) {
+ // It has been cancelled
+ return;
+ }
+
+ var i = 0;
+ var length = this._propertyUpdateCallbacks.length;
+ for (; i < length; i++) {
+ this._propertyUpdateCallbacks[i]();
+ }
+ this._resolveAnimationFinishedPromise();
+ }
+
+ private _onAndroidAnimationCancel() {
+ trace.write("AndroidAnimation._onAndroidAnimationCancel.", trace.categories.Animation);
+ var i = 0;
+ var length = this._propertyResetCallbacks.length;
+ for (; i < length; i++) {
+ this._propertyResetCallbacks[i]();
+ }
+ this._rejectAnimationFinishedPromise();
+ }
+
+ private _createAnimators(propertyAnimation: common.PropertyAnimation): void {
+ trace.write("Creating ObjectAnimator(s) for animation: " + common.Animation._getAnimationInfo(propertyAnimation) + "...", trace.categories.Animation);
+
+ if (types.isNullOrUndefined(propertyAnimation.target)) {
+ throw new Error("Animation target cannot be null or undefined!");
+ }
+
+ if (types.isNullOrUndefined(propertyAnimation.property)) {
+ throw new Error("Animation property cannot be null or undefined!");
+ }
+
+ if (types.isNullOrUndefined(propertyAnimation.value)) {
+ throw new Error("Animation value cannot be null or undefined!");
+ }
+
+ var nativeArray;
+ var nativeView: android.view.View = (propertyAnimation.target._nativeView);
+ var animators = new Array();
+ var propertyUpdateCallbacks = new Array();
+ var propertyResetCallbacks = new Array();
+ var animator: android.animation.ObjectAnimator;
+ var originalValue;
+ var density = utils.layout.getDisplayDensity();
+ switch (propertyAnimation.property) {
+
+ case common.Properties.opacity:
+ originalValue = nativeView.getAlpha();
+ if (propertyAnimation.value !== propertyAnimation.target.opacity) {
+ nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
+ nativeArray[0] = propertyAnimation.value;
+ animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "alpha", nativeArray));
+ propertyUpdateCallbacks.push(() => { propertyAnimation.target.opacity = propertyAnimation.value });
+ propertyResetCallbacks.push(() => { nativeView.setAlpha(originalValue); });
+ }
+ break;
+
+ case common.Properties.backgroundColor:
+ originalValue = nativeView.getBackground();
+ if (!color.Color.equals(propertyAnimation.value, propertyAnimation.target.backgroundColor)) {
+ nativeArray = java.lang.reflect.Array.newInstance(intType, 1);
+ nativeArray[0] = (propertyAnimation.value).argb;
+ //https://github.com/NativeScript/android-runtime/issues/168
+ //animator = android.animation.ObjectAnimator.ofObject(nativeView, "backgroundColor", argbEvaluator, nativeArray);
+ animator = android.animation.ObjectAnimator.ofInt(nativeView, "backgroundColor", nativeArray);
+ animator.setEvaluator(argbEvaluator);
+ animators.push(animator);
+ propertyUpdateCallbacks.push(() => { propertyAnimation.target.backgroundColor = propertyAnimation.value; });
+ propertyResetCallbacks.push(() => { nativeView.setBackground(originalValue); });
+ }
+ break;
+
+ case common.Properties.translate:
+ originalValue = nativeView.getTranslationX();
+ if (propertyAnimation.value.x * density !== originalValue) {
+ nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
+ nativeArray[0] = propertyAnimation.value.x * density;
+ animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "translationX", nativeArray));
+ propertyResetCallbacks.push(() => { nativeView.setTranslationX(originalValue); });
+ }
+
+ originalValue = nativeView.getTranslationY();
+ if (propertyAnimation.value.y * density !== originalValue) {
+ nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
+ nativeArray[0] = propertyAnimation.value.y * density;
+ animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "translationY", nativeArray));
+ propertyResetCallbacks.push(() => { nativeView.setTranslationY(originalValue); });
+ }
+ break;
+
+ case common.Properties.rotate:
+ originalValue = nativeView.getRotation();
+ if (propertyAnimation.value !== originalValue) {
+ nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
+ nativeArray[0] = propertyAnimation.value;
+ animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "rotation", nativeArray));
+ propertyResetCallbacks.push(() => { nativeView.setRotation(originalValue); });
+ }
+ break;
+
+ case common.Properties.scale:
+ originalValue = nativeView.getScaleX();
+ if (propertyAnimation.value.x !== originalValue) {
+ nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
+ nativeArray[0] = propertyAnimation.value.x;
+ animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "scaleX", nativeArray));
+ propertyResetCallbacks.push(() => { nativeView.setScaleX(originalValue); });
+ }
+
+ originalValue = nativeView.getScaleY();
+ if (propertyAnimation.value.y !== originalValue) {
+ nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
+ nativeArray[0] = propertyAnimation.value.y;
+ animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "scaleY", nativeArray));
+ propertyResetCallbacks.push(() => { nativeView.setScaleY(originalValue); });
+ }
+ break;
+
+ default:
+ throw new Error("Cannot animate " + propertyAnimation.property);
+ break;
+ }
+
+ var i = 0;
+ var length = animators.length;
+ for (; i < length; i++) {
+ if (propertyAnimation.duration !== undefined) {
+ animators[i].setDuration(propertyAnimation.duration);
+ }
+ if (propertyAnimation.delay !== undefined) {
+ animators[i].setStartDelay(propertyAnimation.delay);
+ }
+ if (propertyAnimation.iterations !== undefined) {
+ if (propertyAnimation.iterations === Number.POSITIVE_INFINITY) {
+ animators[i].setRepeatCount(android.view.animation.Animation.INFINITE);
+ }
+ else {
+ animators[i].setRepeatCount(propertyAnimation.iterations - 1);
}
}
- else {
- length = propertyUpdateCallbacks.length;
- for (; i < length; i++) {
- propertyUpdateCallbacks[i]();
- }
+ if (propertyAnimation.curve !== undefined) {
+ animators[i].setInterpolator(propertyAnimation.curve);
}
-
- if (finishedCallback) {
- finishedCallback(cancelled);
- }
- },
- onAnimationCancel: function (animator: android.animation.Animator): void {
- trace.write("AnimatorListener.onAnimationCancel.", trace.categories.Animation);
- cancelled = true;
+ trace.write("ObjectAnimator created: " + animators[i], trace.categories.Animation);
}
- }));
- trace.write("Starting " + animators.length + " animations " + (playSequentially ? "sequentially." : "together."), trace.categories.Animation);
- animatorSet.start();
-
- return {
- cancel: () => {
- trace.write("Cancelling AnimatorSet.", trace.categories.Animation);
- animatorSet.cancel();
- }
- };
+ this._animators = this._animators.concat(animators);
+ this._propertyUpdateCallbacks = this._propertyUpdateCallbacks.concat(propertyUpdateCallbacks);
+ this._propertyResetCallbacks = this._propertyResetCallbacks.concat(propertyResetCallbacks);
+ }
}
\ No newline at end of file
diff --git a/ui/animation/animation.d.ts b/ui/animation/animation.d.ts
index eec9bde7e..f1a0ac6f0 100644
--- a/ui/animation/animation.d.ts
+++ b/ui/animation/animation.d.ts
@@ -1,55 +1,40 @@
declare module "ui/animation" {
import viewModule = require("ui/core/view");
+ import colorModule = require("color");
/**
- * Animatable properties enumeration.
+ * Defines animation options for the View.animate method.
*/
- module Properties {
- /**
- * Animates the opacity of the target view. Value should be a number between 0.0 and 1.0
- */
- export var opacity: string;
-
- /**
- * Animates the backgroundColor of the target view. Value should be an instance of Color.
- */
- export var backgroundColor: string;
-
- /**
- * Animates the translation affine transform of the target view. Value should be a JSON object of the form {x: 100, y: 100}.
- */
- export var translate: string;
-
- /**
- * Animates the rotate affine transform of the target view. Value should be a number specifying the roation amount in degrees.
- */
- export var rotate: string;
-
- /**
- * Animates the scale affine transform of the target view. Value should be a JSON object of the form {x: 0.5, y: 0.5}.
- */
- export var scale: string;
- }
-
- /**
- * Defines the data for an animation.
- */
- export interface Animation {
-
+ export interface AnimationDefinition {
/**
* The view whose property is to be animated.
*/
- target: viewModule.View;
+ target?: viewModule.View;
/**
- * The property to be animated. Animatable properties are contained in the animation.Properties enumeration.
+ * Animates the opacity of the view. Value should be a number between 0.0 and 1.0
*/
- property: string;
+ opacity?: number;
/**
- * The value of the property to animate to.
+ * Animates the backgroundColor of the view.
*/
- value: any;
+ backgroundColor?: colorModule.Color;
+
+ /**
+ * Animates the translate affine transform of the view.
+ */
+ translate?: Pair;
+
+ /**
+ * Animates the scale affine transform of the view.
+ */
+ scale?: Pair;
+
+ /**
+ * Animates the rotate affine transform of the view. Value should be a number specifying the rotation amount in degrees.
+ */
+ rotate?: number;
/**
* The length of the animation in milliseconds. The default duration is 300 milliseconds.
@@ -57,45 +42,39 @@
duration?: number;
/**
- * The amount of time, in milliseconds, to delay starting the animation after start() is called.
+ * The amount of time, in milliseconds, to delay starting the animation.
*/
delay?: number;
/**
- * Specifies how many times the animation should be repeated.
- * The default repeat count is 0 meaning the animation is never repeated, i.e. it is played only once.
- * iOS animations support fractional repeat counts, i.e. 1.5
+ * Specifies how many times the animation should be played. Default is 1.
+ * iOS animations support fractional iterations, i.e. 1.5.
+ * To repeat an animation infinitely, use Number.POSITIVE_INFINITY
*/
- repeatCount?: number;
+ iterations?: number;
/**
- * An optional animation curve of type UIViewAnimationCurve.
+ * An optional animation curve of type UIViewAnimationCurve for iOS or android.animation.TimeInterpolator for Android.
*/
- iosUIViewAnimationCurve?: any;
-
- /**
- * An optional android.animation.TimeInterpolator instance used in calculating the elapsed fraction of this animation.
- * The interpolator determines whether the animation runs with linear or non-linear motion, such as acceleration and deceleration.
- * The default value is AccelerateDecelerateInterpolator.
- */
- androidInterpolator?: any;
+ curve?: any;
}
/**
- * Defines an operation that can be cancelled.
+ * Defines a pair of values (horizontal and vertical) for translate and scale animations.
*/
- export interface Cancelable {
-
- /**
- * Cancels the opertaion.
- */
- cancel: () => void;
+ export interface Pair {
+ x: number;
+ y: number;
}
/**
- * Starts the specified animations and returns a Cancelable object that can be used to stop the animations.
- * @param animations - The animations to start.
- * @param finishedCallback - Callback function which will be executed when all animations finish. Useful for chaining multiple animation sets on after another.
+ * Defines a animation set.
*/
- export function start(animations: Array, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): Cancelable;
+ export class Animation {
+ constructor(animationDefinitions: Array, playSequentially?: boolean);
+ public play: () => Animation;
+ public cancel: () => void;
+ public finished: Promise;
+ public isPlaying: boolean;
+ }
}
\ No newline at end of file
diff --git a/ui/animation/animation.ios.ts b/ui/animation/animation.ios.ts
index 3d7522130..0e278513a 100644
--- a/ui/animation/animation.ios.ts
+++ b/ui/animation/animation.ios.ts
@@ -9,17 +9,7 @@ require("utils/module-merge").merge(common, exports);
var _transform = "_transform";
var _skip = "_skip";
-function getAnimationInfo(animation: definition.Animation): string {
- return JSON.stringify({
- target: animation.target.id ? animation.target.id : animation.target._domId,
- property: animation.property,
- value: animation.value,
- duration: animation.duration,
- delay: animation.delay,
- repeatCount: animation.repeatCount,
- iosUIViewAnimationCurve: animation.iosUIViewAnimationCurve
- });
-}
+var FLT_MAX = 340282346638528859811704183484516925440.000000;
class AnimationDelegateImpl extends NSObject {
static new(): AnimationDelegateImpl {
@@ -52,222 +42,241 @@ class AnimationDelegateImpl extends NSObject {
};
}
-function createiOSAnimation(animations: Array, index: number, playSequentially: boolean, finishedCallback: (cancelled?: boolean) => void): Function {
- return (cancelled?: boolean) => {
- if (cancelled && finishedCallback) {
- trace.write("Animation " + (index - 1).toString() + " was cancelled. Will skip the rest of animations and call finishedCallback(true).", trace.categories.Animation);
- finishedCallback(cancelled);
- return;
- }
+export class Animation extends common.Animation implements definition.Animation {
+ private _iOSAnimationFunction: Function;
+ private _finishedAnimations: number;
+ private _cancelledAnimations: number;
+ private _mergedPropertyAnimations: Array;
- var animation = animations[index];
- var nativeView = (animation.target._nativeView);
+ public play(): Animation {
+ super.play();
- var nextAnimationCallback: Function;
- var animationDelegate: AnimationDelegateImpl;
- if (index === animations.length - 1) {
- // This is the last animation, so tell it to call the master finishedCallback when done.
- animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(finishedCallback);
- }
- else {
- nextAnimationCallback = createiOSAnimation(animations, index + 1, playSequentially, finishedCallback);
- // If animations are to be played sequentially, tell it to start the next animation when done.
- // If played together, all individual animations will call the master finishedCallback, which increments a counter every time it is called.
- animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(playSequentially ? nextAnimationCallback : finishedCallback);
- }
+ this._finishedAnimations = 0;
+ this._cancelledAnimations = 0;
+ this._iOSAnimationFunction();
- trace.write("UIView.beginAnimationsContext("+index+"): " + getAnimationInfo(animation), trace.categories.Animation);
- UIView.beginAnimationsContext(index.toString(), null);
-
- if (animationDelegate) {
- UIView.setAnimationDelegate(animationDelegate);
- UIView.setAnimationWillStartSelector("animationWillStart");
- UIView.setAnimationDidStopSelector("animationDidStop");
- }
-
- if (animation.duration !== undefined) {
- UIView.setAnimationDuration(animation.duration / 1000.0);
- }
- else {
- UIView.setAnimationDuration(0.3); //Default duration.
- }
- if (animation.delay !== undefined) {
- UIView.setAnimationDelay(animation.delay / 1000.0);
- }
- if (animation.repeatCount !== undefined) {
- UIView.setAnimationRepeatCount(animation.repeatCount);
- }
- if (animation.iosUIViewAnimationCurve !== undefined) {
- UIView.setAnimationCurve(animation.iosUIViewAnimationCurve);
- }
-
- var originalValue;
- switch (animation.property) {
- case definition.Properties.opacity:
- originalValue = animation.target.opacity;
- (animation)._propertyResetCallback = () => { animation.target.opacity = originalValue };
- animation.target.opacity = animation.value;
- break;
- case definition.Properties.backgroundColor:
- originalValue = animation.target.backgroundColor;
- (animation)._propertyResetCallback = () => { animation.target.backgroundColor = originalValue };
- animation.target.backgroundColor = animation.value;
- break;
- case _transform:
- originalValue = nativeView.transform;
- (animation)._propertyResetCallback = () => { nativeView.transform = originalValue };
- nativeView.transform = animation.value;
- break;
- default:
- throw new Error("Cannot animate " + animation.property);
- break;
- }
-
- trace.write("UIView.commitAnimations " + index, trace.categories.Animation);
- UIView.commitAnimations();
-
- if (!playSequentially && nextAnimationCallback) {
- nextAnimationCallback();
- }
+ return this;
}
-}
-function isAffineTransform(property: string): boolean {
- return property === _transform
- || property === definition.Properties.translate
- || property === definition.Properties.rotate
- || property === definition.Properties.scale;
-}
+ public cancel(): void {
+ super.cancel();
-function canBeMerged(animation1: definition.Animation, animation2: definition.Animation) {
- var result =
- isAffineTransform(animation1.property) &&
- isAffineTransform(animation2.property) &&
- animation1.target === animation2.target &&
- animation1.duration === animation2.duration &&
- animation1.delay === animation2.delay &&
- animation1.repeatCount === animation2.repeatCount &&
- animation1.iosUIViewAnimationCurve === animation2.iosUIViewAnimationCurve;
- return result;
-}
-
-function affineTransform(matrix: CGAffineTransform, property: string, value: any): CGAffineTransform {
- switch (property) {
- case definition.Properties.translate:
- return CGAffineTransformTranslate(matrix, value.x, value.y);
- case definition.Properties.rotate:
- return CGAffineTransformRotate(matrix, value * Math.PI / 180);
- case definition.Properties.scale:
- return CGAffineTransformScale(matrix, value.x, value.y);
- default:
- throw new Error("Cannot create transform for" + property);
- break;
- }
-}
-
-function mergeAffineTransformAnimations(animations: Array): Array {
- var result = new Array();
-
- var i = 0;
- var j;
- var length = animations.length;
- for (; i < length; i++) {
- if (animations[i].property !== _skip) {
-
- if (!isAffineTransform(animations[i].property)) {
- // This is not an affine transform animation, so there is nothing to merge.
- result.push(animations[i]);
- }
- else {
-
- // This animation has not been merged anywhere. Create a new transform animation.
- var newTransformAnimation: definition.Animation = {
- target: animations[i].target,
- property: _transform,
- value: affineTransform(CGAffineTransformIdentity, animations[i].property, animations[i].value),
- duration: animations[i].duration,
- delay: animations[i].delay,
- repeatCount: animations[i].repeatCount,
- iosUIViewAnimationCurve: animations[i].iosUIViewAnimationCurve
- };
- //trace.write("Created new transform animation: " + getAnimationInfo(newTransformAnimation), trace.categories.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 (canBeMerged(animations[i], animations[j])) {
- //trace.write("Merging animations: " + getAnimationInfo(newTransformAnimation) + " + " + getAnimationInfo(animations[j]) + " = ", trace.categories.Animation);
- //trace.write("New native transform is: " + NSStringFromCGAffineTransform(newTransformAnimation.value), trace.categories.Animation);
- newTransformAnimation.value = affineTransform(newTransformAnimation.value, animations[j].property, animations[j].value);
-
- // Mark that it has been merged so we can skip it on our outer loop.
- animations[j].property = _skip;
- }
- }
- }
-
- result.push(newTransformAnimation);
+ var i = 0;
+ var length = this._mergedPropertyAnimations.length;
+ for (; i < length; i++) {
+ (this._mergedPropertyAnimations[i].target._nativeView).layer.removeAllAnimations();
+ if ((this._mergedPropertyAnimations[i])._propertyResetCallback) {
+ (this._mergedPropertyAnimations[i])._propertyResetCallback();
}
}
}
- return result;
-}
+ constructor(animationDefinitions: Array, playSequentially?: boolean) {
+ super(animationDefinitions, playSequentially);
-export var start = function start(animations: Array, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): definition.Cancelable {
- //trace.write("Non-merged: " + animations.length, trace.categories.Animation);
- var mergedAnimations = mergeAffineTransformAnimations(animations);
- //trace.write("Merged: " + mergedAnimations.length, trace.categories.Animation);
+ trace.write("Non-merged Property Animations: " + this._propertyAnimations.length, trace.categories.Animation);
+ this._mergedPropertyAnimations = Animation._mergeAffineTransformAnimations(this._propertyAnimations);
+ trace.write("Merged Property Animations: " + this._mergedPropertyAnimations.length, trace.categories.Animation);
- var animationFinishedCallback: () => void;
- if (finishedCallback) {
- if (playSequentially) {
- // This callback will be called by the last animation when done or by another animation if the user cancels them halfway through.
- animationFinishedCallback = finishedCallback;
- }
- else {
- var finishedAnimations = 0;
- var cancelledAnimations = 0;
-
- // This callback will be called by each individual animation when it finishes or is cancelled.
- animationFinishedCallback = (cancelled?: boolean) => {
+ var that = this;
+ var 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) {
- cancelledAnimations++;
+ that._rejectAnimationFinishedPromise();
}
else {
- finishedAnimations++;
+ 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 (cancelledAnimations === mergedAnimations.length) {
- trace.write(cancelledAnimations + " animations cancelled.", trace.categories.Animation);
- finishedCallback(cancelled);
- return;
+ if (that._cancelledAnimations === that._mergedPropertyAnimations.length) {
+ trace.write(that._cancelledAnimations + " animations cancelled.", trace.categories.Animation);
+ that._rejectAnimationFinishedPromise();
}
+ else if (that._finishedAnimations === that._mergedPropertyAnimations.length) {
+ trace.write(that._finishedAnimations + " animations finished.", trace.categories.Animation);
+ that._resolveAnimationFinishedPromise();
+ }
+ }
+ };
- if (finishedAnimations === mergedAnimations.length) {
- trace.write(finishedAnimations + " animations finished.", trace.categories.Animation);
- finishedCallback(cancelled);
- return;
+ this._iOSAnimationFunction = Animation._createiOSAnimationFunction(this._mergedPropertyAnimations, 0, this._playSequentially, animationFinishedCallback);
+ }
+
+ private static _createiOSAnimationFunction(propertyAnimations: Array, index: number, playSequentially: boolean, finishedCallback: (cancelled?: boolean) => void): Function {
+ return (cancelled?: boolean) => {
+ if (cancelled && finishedCallback) {
+ trace.write("Animation " + (index - 1).toString() + " was cancelled. Will skip the rest of animations and call finishedCallback(true).", trace.categories.Animation);
+ finishedCallback(cancelled);
+ return;
+ }
+
+ var animation = propertyAnimations[index];
+ var nativeView = (animation.target._nativeView);
+
+ var nextAnimationCallback: Function;
+ var animationDelegate: AnimationDelegateImpl;
+ if (index === propertyAnimations.length - 1) {
+ // This is the last animation, so tell it to call the master finishedCallback when done.
+ animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(finishedCallback);
+ }
+ else {
+ nextAnimationCallback = Animation._createiOSAnimationFunction(propertyAnimations, index + 1, playSequentially, finishedCallback);
+ // If animations are to be played sequentially, tell it to start the next animation when done.
+ // If played together, all individual animations will call the master finishedCallback, which increments a counter every time it is called.
+ animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(playSequentially ? nextAnimationCallback : finishedCallback);
+ }
+
+ trace.write("UIView.beginAnimationsContext(" + index + "): " + common.Animation._getAnimationInfo(animation), trace.categories.Animation);
+ UIView.beginAnimationsContext(index.toString(), null);
+
+ if (animationDelegate) {
+ UIView.setAnimationDelegate(animationDelegate);
+ UIView.setAnimationWillStartSelector("animationWillStart");
+ UIView.setAnimationDidStopSelector("animationDidStop");
+ }
+
+ if (animation.duration !== undefined) {
+ UIView.setAnimationDuration(animation.duration / 1000.0);
+ }
+ else {
+ UIView.setAnimationDuration(0.3); //Default duration.
+ }
+ if (animation.delay !== undefined) {
+ UIView.setAnimationDelay(animation.delay / 1000.0);
+ }
+ if (animation.iterations !== undefined) {
+ if (animation.iterations === Number.POSITIVE_INFINITY) {
+ UIView.setAnimationRepeatCount(FLT_MAX);
}
- };
+ else {
+ UIView.setAnimationRepeatCount(animation.iterations - 1);
+ }
+ }
+ if (animation.curve !== undefined) {
+ UIView.setAnimationCurve(animation.curve);
+ }
+
+ var originalValue;
+ switch (animation.property) {
+ case common.Properties.opacity:
+ originalValue = animation.target.opacity;
+ (animation)._propertyResetCallback = () => { animation.target.opacity = originalValue };
+ animation.target.opacity = animation.value;
+ break;
+ case common.Properties.backgroundColor:
+ originalValue = animation.target.backgroundColor;
+ (animation)._propertyResetCallback = () => { animation.target.backgroundColor = originalValue };
+ animation.target.backgroundColor = animation.value;
+ break;
+ case _transform:
+ originalValue = nativeView.transform;
+ (animation)._propertyResetCallback = () => { nativeView.transform = originalValue };
+ nativeView.transform = animation.value;
+ break;
+ default:
+ throw new Error("Cannot animate " + animation.property);
+ break;
+ }
+
+ trace.write("UIView.commitAnimations " + index, trace.categories.Animation);
+ UIView.commitAnimations();
+
+ if (!playSequentially && nextAnimationCallback) {
+ nextAnimationCallback();
+ }
}
}
- var iOSAnimation = createiOSAnimation(mergedAnimations, 0, playSequentially, animationFinishedCallback);
- trace.write("Starting " + mergedAnimations.length + " animations " + (playSequentially ? "sequentially." : "together."), trace.categories.Animation);
- iOSAnimation();
+ private static _isAffineTransform(property: string): boolean {
+ return property === _transform
+ || property === common.Properties.translate
+ || property === common.Properties.rotate
+ || property === common.Properties.scale;
+ }
- return {
- cancel: () => {
- var i = 0;
- var length = mergedAnimations.length;
- for (; i < length; i++) {
- (mergedAnimations[i].target._nativeView).layer.removeAllAnimations();
- if ((mergedAnimations[i])._propertyResetCallback) {
- (mergedAnimations[i])._propertyResetCallback();
+ private static _canBeMerged(animation1: common.PropertyAnimation, animation2: common.PropertyAnimation) {
+ var 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 _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();
+
+ var i = 0;
+ var j;
+ var length = propertyAnimations.length;
+ for (; i < length; i++) {
+ if (propertyAnimations[i].property !== _skip) {
+
+ 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.
+ var newTransformAnimation: common.PropertyAnimation = {
+ target: propertyAnimations[i].target,
+ property: _transform,
+ value: Animation._affineTransform(CGAffineTransformIdentity, propertyAnimations[i].property, propertyAnimations[i].value),
+ duration: propertyAnimations[i].duration,
+ delay: propertyAnimations[i].delay,
+ iterations: propertyAnimations[i].iterations,
+ iosUIViewAnimationCurve: propertyAnimations[i].curve
+ };
+ trace.write("Created new transform animation: " + common.Animation._getAnimationInfo(newTransformAnimation), trace.categories.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);
+
+ // Mark that it has been merged so we can skip it on our outer loop.
+ propertyAnimations[j].property = _skip;
+ }
+ }
+ }
+
+ result.push(newTransformAnimation);
}
}
}
- };
+
+ return result;
+ }
}
\ No newline at end of file
diff --git a/ui/core/view-common.ts b/ui/core/view-common.ts
index 32d02702b..e79f02477 100644
--- a/ui/core/view-common.ts
+++ b/ui/core/view-common.ts
@@ -12,6 +12,7 @@ import styleScope = require("ui/styling/style-scope");
import enums = require("ui/enums");
import utils = require("utils/utils");
import color = require("color");
+import animationModule = require("ui/animation");
export function getViewById(view: View, id: string): View {
if (!view) {
@@ -967,4 +968,14 @@ export class View extends proxy.ProxyObject implements definition.View {
public focus(): boolean {
return undefined;
}
+
+ public animate(animation: animationModule.AnimationDefinition): Promise {
+ return this.createAnimation(animation).play().finished;
+ }
+
+ public createAnimation(animation: animationModule.AnimationDefinition): animationModule.Animation {
+ var that = this;
+ animation.target = that;
+ return new animationModule.Animation([animation]);
+ }
}
\ No newline at end of file
diff --git a/ui/core/view.d.ts b/ui/core/view.d.ts
index d5c62d64c..94ca5efce 100644
--- a/ui/core/view.d.ts
+++ b/ui/core/view.d.ts
@@ -5,6 +5,7 @@ declare module "ui/core/view" {
import gestures = require("ui/gestures");
import color = require("color");
import observable = require("data/observable");
+ import animation = require("ui/animation");
/**
* Gets a child view by id.
@@ -403,6 +404,9 @@ declare module "ui/core/view" {
*/
on(event: "unloaded", callback: (args: observable.EventData) => void, thisArg?: any);
+ public animate(options: animation.AnimationDefinition): Promise;
+ public createAnimation(options: animation.AnimationDefinition): animation.Animation;
+
// Lifecycle events
onLoaded(): void;
onUnloaded(): void;
@@ -521,4 +525,5 @@ declare module "ui/core/view" {
*/
_applyXmlAttribute(attributeName: string, attrValue: any): boolean;
}
+
}