diff --git a/CrossPlatformModules.csproj b/CrossPlatformModules.csproj
index 950037051..34ba87a53 100644
--- a/CrossPlatformModules.csproj
+++ b/CrossPlatformModules.csproj
@@ -134,6 +134,19 @@
page-title-icon.xml
+
+
+
+ animation.d.ts
+
+
+ animation.d.ts
+
+
+ animation.d.ts
+
+
+
list-picker.xml
@@ -791,6 +804,13 @@
+
+
+
+
+
+ Designer
+
@@ -798,7 +818,9 @@
-
+
+ Designer
+
Designer
@@ -1742,6 +1764,8 @@
PreserveNewest
+
+
PreserveNewest
diff --git a/application/Readme.md b/application/Readme.md
index e5badda4c..cc215353f 100644
--- a/application/Readme.md
+++ b/application/Readme.md
@@ -1,6 +1,4 @@
-Use the frame in the following way:
-
-### To navigate to the starting page of the application
+# Ani
```javascript
// put this in the bootstrap.js
var app = require("application");
diff --git a/apps/animations/app.css b/apps/animations/app.css
new file mode 100644
index 000000000..c59011f60
--- /dev/null
+++ b/apps/animations/app.css
@@ -0,0 +1,3 @@
+page {
+ /* CSS styles */
+}
\ No newline at end of file
diff --git a/apps/animations/app.ts b/apps/animations/app.ts
new file mode 100644
index 000000000..2baebd169
--- /dev/null
+++ b/apps/animations/app.ts
@@ -0,0 +1,8 @@
+import application = require("application");
+import trace = require("trace");
+
+trace.enable();
+trace.setCategories(trace.categories.concat(trace.categories.Animation));
+
+application.mainModule = "main-page";
+application.start();
diff --git a/apps/animations/main-page.ts b/apps/animations/main-page.ts
new file mode 100644
index 000000000..7b31af095
--- /dev/null
+++ b/apps/animations/main-page.ts
@@ -0,0 +1,131 @@
+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 colorModule = require("color");
+import model = require("./model");
+
+var vm = new model.ViewModel();
+
+var page: pages.Page;
+var panel1: abs.AbsoluteLayout;
+var button1: buttonModule.Button;
+var button2: buttonModule.Button;
+var button3: buttonModule.Button;
+var cancelToken: any;
+
+export function pageLoaded(args: observable.EventData) {
+ page = args.object;
+ page.bindingContext = vm;
+ panel1 = page.getViewById("panel1");
+ button1 = page.getViewById("button1");
+ button2 = page.getViewById("button2");
+ button3 = page.getViewById("button3");
+}
+
+export function onSlideOut(args: observable.EventData) {
+ var animations: Array;
+
+ 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 });
+
+ 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 });
+
+ 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!");
+ });
+ });
+}
+
+export function onSlideIn(args: observable.EventData) {
+ var animations: Array;
+
+ 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);
+
+ cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
+ if (cancelled) {
+ console.log("Panel animation cancelled");
+ return;
+ }
+ console.log("Panel animation completed!");
+
+ 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!");
+ });
+ });
+}
+
+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;
+ }
+ }
+ else {
+ for (; i < length; i++) {
+ animations[i].iosUIViewAnimationCurve = slideOut ? UIViewAnimationCurve.UIViewAnimationCurveEaseIn : UIViewAnimationCurve.UIViewAnimationCurveEaseOut;
+ }
+ }
+}
+
+export function onStop(args: observable.EventData) {
+ cancelToken.cancel();
+}
+
+export function onTap(args: observable.EventData) {
+ console.log((args.object).text);
+}
\ No newline at end of file
diff --git a/apps/animations/main-page.xml b/apps/animations/main-page.xml
new file mode 100644
index 000000000..1e2ee8387
--- /dev/null
+++ b/apps/animations/main-page.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/animations/model.ts b/apps/animations/model.ts
new file mode 100644
index 000000000..083343379
--- /dev/null
+++ b/apps/animations/model.ts
@@ -0,0 +1,37 @@
+import observable = require("data/observable");
+
+export class ViewModel extends observable.Observable {
+ constructor() {
+ super();
+
+ this._duration = 300;
+ this._repeatCount = 0;
+ }
+
+ private _playSequentially: boolean;
+ get playSequentially(): boolean {
+ return this._playSequentially;
+ }
+ set playSequentially(value: boolean) {
+ this._playSequentially = value;
+ this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "playSequentially", value: value });
+ }
+
+ private _duration: number;
+ get duration(): number {
+ return this._duration;
+ }
+ set duration(value: number) {
+ this._duration = value;
+ this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "duration", value: value });
+ }
+
+ private _repeatCount: number;
+ get repeatCount(): number {
+ return this._repeatCount;
+ }
+ set repeatCount(value: number) {
+ this._repeatCount = value;
+ this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "repeatCount", value: value });
+ }
+}
\ No newline at end of file
diff --git a/apps/animations/package.json b/apps/animations/package.json
new file mode 100644
index 000000000..be8391bcb
--- /dev/null
+++ b/apps/animations/package.json
@@ -0,0 +1,2 @@
+{ "name" : "animations",
+ "main" : "app.js" }
\ No newline at end of file
diff --git a/declarations.d.ts b/declarations.d.ts
index 61295bc03..80659a90b 100644
--- a/declarations.d.ts
+++ b/declarations.d.ts
@@ -93,6 +93,7 @@ interface Console {
log(message: any, ...formatParams: any[]): void;
trace(): void;
dump(obj: any): void;
+ createDump(obj: any): string;
dir(obj: any): void;
}
diff --git a/trace/trace.d.ts b/trace/trace.d.ts
index 52197f126..2dff00a98 100644
--- a/trace/trace.d.ts
+++ b/trace/trace.d.ts
@@ -75,6 +75,7 @@ declare module "trace" {
export var Test: string;
export var Binding: string;
export var Error: string;
+ export var Animation: string;
export var All: string;
diff --git a/trace/trace.ts b/trace/trace.ts
index 5a7e4cc1a..4398c4e36 100644
--- a/trace/trace.ts
+++ b/trace/trace.ts
@@ -113,7 +113,8 @@ export module categories {
export var Test = "Test";
export var Binding = "Binding";
export var Error = "Error";
- export var All = VisualTreeEvents + "," + Layout + "," + Style + "," + ViewHierarchy + "," + NativeLifecycle + "," + Debug + "," + Navigation + "," + Test + "," + Binding + "," + Error;
+ export var Animation = "Animation";
+ export var All = VisualTreeEvents + "," + Layout + "," + Style + "," + ViewHierarchy + "," + NativeLifecycle + "," + Debug + "," + Navigation + "," + Test + "," + Binding + "," + Error + "," + Animation;
export var separator = ",";
diff --git a/ui/animation/animation-common.ts b/ui/animation/animation-common.ts
new file mode 100644
index 000000000..af26353d8
--- /dev/null
+++ b/ui/animation/animation-common.ts
@@ -0,0 +1,7 @@
+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
diff --git a/ui/animation/animation.android.ts b/ui/animation/animation.android.ts
new file mode 100644
index 000000000..60b375b1f
--- /dev/null
+++ b/ui/animation/animation.android.ts
@@ -0,0 +1,227 @@
+import definition = require("ui/animation");
+import common = require("ui/animation/animation-common");
+import utils = require("utils/utils");
+import color = require("color");
+import trace = require("trace");
+import types = require("utils/types");
+
+// merge the exports of the common file with the exports of this file
+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();
+
+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
+ });
+}
+
+function createAndroidAnimators(animation: definition.Animation): any {
+ trace.write("Creating ObjectAnimator(s) for animation: " + getAnimationInfo(animation) + "...", trace.categories.Animation);
+
+ if (types.isNullOrUndefined(animation.value)) {
+ throw new Error("Animation value cannot be null or undefined!");
+ }
+
+ 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) {
+
+ 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);
+ }
+ if (animation.delay) {
+ animators[i].setStartDelay(animation.delay);
+ }
+ if (animation.repeatCount) {
+ animators[i].setRepeatCount(animation.repeatCount);
+ }
+ if (animation.androidInterpolator) {
+ animators[i].setInterpolator(animation.androidInterpolator);
+ }
+ 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();
+ }
+ return;
+ }
+
+ 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];
+ }
+
+ var animatorSet = new android.animation.AnimatorSet();
+ if (playSequentially) {
+ animatorSet.playSequentially(nativeArray);
+ }
+ else {
+ animatorSet.playTogether(nativeArray);
+ }
+
+ 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]();
+ }
+ }
+ else {
+ length = propertyUpdateCallbacks.length;
+ for (; i < length; i++) {
+ propertyUpdateCallbacks[i]();
+ }
+ }
+
+ if (finishedCallback) {
+ finishedCallback(cancelled);
+ }
+ },
+ onAnimationCancel: function (animator: android.animation.Animator): void {
+ trace.write("AnimatorListener.onAnimationCancel.", trace.categories.Animation);
+ cancelled = true;
+ }
+ }));
+
+ 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();
+ }
+ };
+}
\ No newline at end of file
diff --git a/ui/animation/animation.d.ts b/ui/animation/animation.d.ts
new file mode 100644
index 000000000..eec9bde7e
--- /dev/null
+++ b/ui/animation/animation.d.ts
@@ -0,0 +1,101 @@
+declare module "ui/animation" {
+ import viewModule = require("ui/core/view");
+
+ /**
+ * Animatable properties enumeration.
+ */
+ 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 {
+
+ /**
+ * The view whose property is to be animated.
+ */
+ target: viewModule.View;
+
+ /**
+ * The property to be animated. Animatable properties are contained in the animation.Properties enumeration.
+ */
+ property: string;
+
+ /**
+ * The value of the property to animate to.
+ */
+ value: any;
+
+ /**
+ * The length of the animation in milliseconds. The default duration is 300 milliseconds.
+ */
+ duration?: number;
+
+ /**
+ * The amount of time, in milliseconds, to delay starting the animation after start() is called.
+ */
+ 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
+ */
+ repeatCount?: number;
+
+ /**
+ * An optional animation curve of type UIViewAnimationCurve.
+ */
+ 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;
+ }
+
+ /**
+ * Defines an operation that can be cancelled.
+ */
+ export interface Cancelable {
+
+ /**
+ * Cancels the opertaion.
+ */
+ cancel: () => void;
+ }
+
+ /**
+ * 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.
+ */
+ export function start(animations: Array, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): Cancelable;
+}
\ No newline at end of file
diff --git a/ui/animation/animation.ios.ts b/ui/animation/animation.ios.ts
new file mode 100644
index 000000000..3d7522130
--- /dev/null
+++ b/ui/animation/animation.ios.ts
@@ -0,0 +1,273 @@
+import definition = require("ui/animation");
+import common = require("ui/animation/animation-common");
+import trace = require("trace");
+
+// merge the exports of the common file with the exports of this file
+declare var exports;
+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
+ });
+}
+
+class AnimationDelegateImpl extends NSObject {
+ static new(): AnimationDelegateImpl {
+ return super.new();
+ }
+
+ private _finishedCallback: Function;
+
+ public initWithFinishedCallback(finishedCallback: Function): AnimationDelegateImpl {
+ this._finishedCallback = finishedCallback;
+ return this;
+ }
+
+ public animationWillStart(animationID: string, context: any): void {
+ trace.write("AnimationDelegateImpl.animationWillStart, animationID: " + animationID, trace.categories.Animation);
+ }
+
+ public animationDidStop(animationID: string, finished: boolean, context: any): void {
+ trace.write("AnimationDelegateImpl.animationDidStop, animationID: " + animationID + ", finished: " + finished, trace.categories.Animation);
+ if (this._finishedCallback) {
+ var cancelled = !finished;
+ // This could either be the master finishedCallback or an nextAnimationCallback depending on the playSequentially argument values.
+ this._finishedCallback(cancelled);
+ }
+ }
+
+ public static ObjCExposedMethods = {
+ "animationWillStart": { returns: interop.types.void, params: [NSString, NSObject] },
+ "animationDidStop": { returns: interop.types.void, params: [NSString, NSNumber, 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;
+ }
+
+ var animation = animations[index];
+ var nativeView = (animation.target._nativeView);
+
+ 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);
+ }
+
+ 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();
+ }
+ }
+}
+
+function isAffineTransform(property: string): boolean {
+ return property === _transform
+ || property === definition.Properties.translate
+ || property === definition.Properties.rotate
+ || property === definition.Properties.scale;
+}
+
+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);
+ }
+ }
+ }
+
+ return result;
+}
+
+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);
+
+ 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) => {
+ if (cancelled) {
+ cancelledAnimations++;
+ }
+ else {
+ finishedAnimations++;
+ }
+
+ if (cancelledAnimations === mergedAnimations.length) {
+ trace.write(cancelledAnimations + " animations cancelled.", trace.categories.Animation);
+ finishedCallback(cancelled);
+ return;
+ }
+
+ if (finishedAnimations === mergedAnimations.length) {
+ trace.write(finishedAnimations + " animations finished.", trace.categories.Animation);
+ finishedCallback(cancelled);
+ return;
+ }
+ };
+ }
+ }
+
+ var iOSAnimation = createiOSAnimation(mergedAnimations, 0, playSequentially, animationFinishedCallback);
+ trace.write("Starting " + mergedAnimations.length + " animations " + (playSequentially ? "sequentially." : "together."), trace.categories.Animation);
+ iOSAnimation();
+
+ 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();
+ }
+ }
+ }
+ };
+}
\ No newline at end of file
diff --git a/ui/animation/package.json b/ui/animation/package.json
new file mode 100644
index 000000000..5a168b892
--- /dev/null
+++ b/ui/animation/package.json
@@ -0,0 +1,2 @@
+{ "name" : "animation",
+ "main" : "animation.js" }
\ No newline at end of file
diff --git a/ui/layouts/layout.ts b/ui/layouts/layout.ts
index 6cabfba88..c22ee5e01 100644
--- a/ui/layouts/layout.ts
+++ b/ui/layouts/layout.ts
@@ -1,11 +1,31 @@
import definition = require("ui/layouts/layout");
import view = require("ui/core/view");
import dependencyObservable = require("ui/core/dependency-observable");
+import proxy = require("ui/core/proxy");
+
+function onClipToBoundsPropertyChanged(data: dependencyObservable.PropertyChangeData) {
+ var nativeView = (data.object)._nativeView;
+ if (!nativeView) {
+ return;
+ }
+ var value = data.newValue;
+
+ if (nativeView instanceof UIView) {
+ (nativeView).clipsToBounds = value;
+ }
+ else if (nativeView instanceof android.view.ViewGroup) {
+ (nativeView).setClipChildren(value);
+ }
+}
+
+var clipToBoundsProperty = new dependencyObservable.Property(
+ "clipToBounds",
+ "Layout",
+ new proxy.PropertyMetadata(undefined, dependencyObservable.PropertyMetadataSettings.None, onClipToBoundsPropertyChanged)
+ );
export class Layout extends view.CustomLayoutView implements definition.Layout, view.AddChildFromBuilder {
-
- public static clipToBoundsProperty = new dependencyObservable.Property("clipToBounds", "Layout",
- new dependencyObservable.PropertyMetadata(true, dependencyObservable.PropertyMetadataSettings.None, Layout.onClipToBoundsPropertyChanged));
+ public static clipToBoundsProperty = clipToBoundsProperty;
private _subViews: Array = new Array();
@@ -96,15 +116,10 @@ export class Layout extends view.CustomLayoutView implements definition.Layout,
this.style.paddingLeft = value;
}
- private static onClipToBoundsPropertyChanged(data: dependencyObservable.PropertyChangeData) {
- var layout = data.object;
- var nativeView: Object = layout._nativeView;
- var value = data.newValue;
- if (nativeView instanceof android.view.ViewGroup) {
- (nativeView).setClipChildren(value);
- }
- else if (nativeView instanceof UIView) {
- (nativeView).clipsToBounds = value;
- }
+ get clipToBounds(): boolean {
+ return this._getValue(Layout.clipToBoundsProperty);
+ }
+ set clipToBounds(value: boolean) {
+ this._setValue(Layout.clipToBoundsProperty, value);
}
}