Merge pull request #512 from NativeScript/animations

Animations
This commit is contained in:
Rossen Hristov
2015-07-29 15:41:53 +03:00
21 changed files with 1293 additions and 19 deletions

View File

@@ -134,6 +134,20 @@
<TypeScriptCompile Include="apps\action-bar-demo\pages\page-title-icon.ts">
<DependentUpon>page-title-icon.xml</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="apps\animations\model.ts" />
<TypeScriptCompile Include="apps\tests\ui\animation\animation-tests.ts" />
<TypeScriptCompile Include="ui\animation\animation.d.ts" />
<TypeScriptCompile Include="ui\animation\animation-common.ts">
<DependentUpon>animation.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\animation\animation.android.ts">
<DependentUpon>animation.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\animation\animation.ios.ts">
<DependentUpon>animation.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="apps\animations\app.ts" />
<TypeScriptCompile Include="apps\animations\main-page.ts" />
<TypeScriptCompile Include="apps\gallery-app\views\list-picker.ts">
<DependentUpon>list-picker.xml</DependentUpon>
</TypeScriptCompile>
@@ -791,6 +805,13 @@
</Content>
<Content Include="apps\action-bar-demo\pages\navigation-button.xml" />
<Content Include="apps\action-bar-demo\test-icon.png" />
<TypeScriptCompile Include="libjs.d.ts" />
</ItemGroup>
<ItemGroup>
<Content Include="apps\animations\app.css" />
<Content Include="apps\animations\main-page.xml">
<SubType>Designer</SubType>
</Content>
<Content Include="apps\gallery-app\content\border.xml" />
<Content Include="apps\gallery-app\app.css" />
<Content Include="apps\gallery-app\views\list-picker.xml" />
@@ -798,7 +819,9 @@
<Content Include="apps\gallery-app\views\segmented-bar.xml" />
<Content Include="apps\gallery-app\views\time-picker.xml" />
<Content Include="apps\gallery-app\views\date-picker.xml" />
<Content Include="apps\modal-views-demo\login-page.xml" />
<Content Include="apps\modal-views-demo\login-page.xml">
<SubType>Designer</SubType>
</Content>
<Content Include="apps\pickers-demo\main-page.xml">
<SubType>Designer</SubType>
</Content>
@@ -1741,7 +1764,9 @@
<Content Include="apps\modal-views-demo\package.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="apps\cuteness.unoptimized\package.json">
<Content Include="apps\cuteness.unoptimized\package.json" />
<Content Include="apps\animations\package.json" />
<Content Include="ui\animation\package.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="apps\action-bar-demo\package.json" />

View File

@@ -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");

3
apps/animations/app.css Normal file
View File

@@ -0,0 +1,3 @@
page {
/* CSS styles */
}

8
apps/animations/app.ts Normal file
View File

@@ -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();

View File

@@ -0,0 +1,93 @@
import observable = require("data/observable");
import pages = require("ui/page");
import buttonModule = require("ui/button");
import abs = require("ui/layouts/absolute-layout");
import animationModule = require("ui/animation");
import colorModule = require("color");
import model = require("./model");
var vm = new model.ViewModel();
var page: pages.Page;
var panel: abs.AbsoluteLayout;
var button1: buttonModule.Button;
var button2: buttonModule.Button;
var button3: buttonModule.Button;
var buttonAnimation: animationModule.Animation;
var panelAnimation: animationModule.Animation;
export function pageLoaded(args: observable.EventData) {
page = <pages.Page>args.object;
page.bindingContext = vm;
panel = page.getViewById<abs.AbsoluteLayout>("panel1");
button1 = page.getViewById<buttonModule.Button>("button1");
button2 = page.getViewById<buttonModule.Button>("button2");
button3 = page.getViewById<buttonModule.Button>("button3");
}
export function onSlideOut(args: observable.EventData) {
console.log("onSlideOut");
var curve = page.android ? new android.view.animation.AccelerateInterpolator(1) : UIViewAnimationCurve.UIViewAnimationCurveEaseIn;
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);
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 });
buttonAnimation.play().finished
.then(() => panelAnimation.play().finished)
.catch((e) => console.log(e.message));
}
export function onSlideIn(args: observable.EventData) {
console.log("onSlideIn");
var curve = page.android ? new android.view.animation.DecelerateInterpolator(1) : UIViewAnimationCurve.UIViewAnimationCurveEaseOut;
panelAnimation = panel.createAnimation({ opacity: 1, scale: { x: 1, y: 1 }, rotate: 0, backgroundColor: new colorModule.Color("yellow"), duration: vm.duration, iterations: vm.iterations });
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);
panelAnimation.play().finished
.then(() => buttonAnimation.play().finished)
.catch((e) => console.log(e.message));
}
export function onCancel(args: observable.EventData) {
console.log("onCancel");
if (panelAnimation.isPlaying) {
panelAnimation.cancel();
}
if (buttonAnimation.isPlaying) {
buttonAnimation.cancel();
}
}
export function onTap(args: observable.EventData) {
console.log((<any>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));
}

View File

@@ -0,0 +1,30 @@
<Page xmlns="http://www.nativescript.org/tns.xsd" loaded="pageLoaded" id="mainPage">
<StackLayout orientation="vertical">
<StackLayout orientation="vertical" backgroundColor="LightGray" paddingTop="5" paddingBottom="5">
<Label text="{{ duration, 'Duration: ' + duration + ' ms' }}" width="180" marginTop="5" marginBottom="5"/>
<Slider minValue="0" maxValue="10000" value="{{ duration }}" marginTop="5" marginBottom="5" marginLeft="10" marginRight="10"/>
<Label text="{{ iterations, 'Iterations: ' + iterations + ' times' }}" width="180" marginTop="5" marginBottom="5"/>
<Slider minValue="0" maxValue="10" value="{{ iterations }}" marginTop="5" marginBottom="5" marginLeft="10" marginRight="10"/>
<StackLayout orientation="horizontal" marginTop="5" marginBottom="5" horizontalAlignment="center">
<Label text="Play Sequentially?"/>
<Switch marginLeft="10" checked="{{ playSequentially }}"/>
</StackLayout>
<StackLayout orientation="horizontal" marginTop="5" marginBottom="5" horizontalAlignment="center" paddingLeft="5" paddingRight="5">
<Button text="Out" tap="onSlideOut" width="75" marginLeft="5" marginRight="5" />
<Button text="In" tap="onSlideIn" width="75" marginLeft="5" marginRight="5" />
<Button text="Single" tap="onSingle" width="75" marginLeft="5" marginRight="5" />
<Button text="Cancel" tap="onCancel" width="75" marginLeft="5" marginRight="5" />
</StackLayout>
</StackLayout>
<AbsoluteLayout id="panel1" backgroundColor="Yellow" width="300" height="190" clipToBounds="true" marginTop="10">
<Button id="button1" text="Button 1" backgroundColor="White" width="180" height="50" left="60" top="10" tap="onTap"/>
<Button id="button2" text="Button 2" backgroundColor="White" width="180" height="50" left="60" top="70" tap="onTap"/>
<Button id="button3" text="Button 3" backgroundColor="White" width="180" height="50" left="60" top="130" tap="onTap"/>
</AbsoluteLayout>
</StackLayout>
</Page>

37
apps/animations/model.ts Normal file
View File

@@ -0,0 +1,37 @@
import observable = require("data/observable");
export class ViewModel extends observable.Observable {
constructor() {
super();
this._duration = 3000;
this._iterations = 1;
}
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 _iterations: number;
get iterations(): number {
return this._iterations;
}
set iterations(value: number) {
this._iterations = value;
this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "iterations", value: value });
}
}

View File

@@ -0,0 +1,2 @@
{ "name" : "animations",
"main" : "app.js" }

View File

@@ -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");

View File

@@ -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");
// <snippet module="ui/animation" title="animation">
// # Animation
// Animating view properties requires the "ui/animation" module.
// ``` JavaScript
import animation = require("ui/animation");
// ```
// </snippet>
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 });
// <snippet module="ui/animation" title="animation">
// # 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.");
// <hide>
helper.goBack();
done();
// </hide>
})
.catch((e) => {
console.log(e.message);
// <hide>
helper.goBack();
done(e);
// </hide>
});
// ```
// </snippet>
}
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 });
// <snippet module="ui/animation" title="animation">
// # Cancelling animation
// ``` JavaScript
var animation1 = label.createAnimation({ translate: { x: 100, y: 100 } });
animation1.play().finished
.then(() => {
console.log("Animation finished");
// <hide>
helper.goBack();
done();
// </hide>
})
.catch((e) => {
console.log("Animation cancelled");
// <hide>
helper.goBack();
done(e);
// </hide>
});
animation1.cancel();
// ```
// </snippet>
}
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 });
// <snippet module="ui/animation" title="animation">
// # 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");
// <hide>
helper.goBack();
done();
// </hide>
})
.catch((e) => {
console.log(e.message);
// <hide>
helper.goBack();
done(e);
// </hide>
});
// ```
// </snippet>
}
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 });
// <snippet module="ui/animation" title="animation">
// # 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");
// <hide>
helper.goBack();
done();
// </hide>
})
.catch((e) => {
console.log(e.message);
// <hide>
helper.goBack();
done(e);
// </hide>
});
// ```
// </snippet>
}
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 });
// <snippet module="ui/animation" title="animation">
// # Animating multiple views simultaneously
// ``` JavaScript
var animations: Array<animation.AnimationDefinition> = [
{ 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");
// <hide>
helper.goBack();
done();
// </hide>
})
.catch((e) => {
console.log(e.message);
// <hide>
helper.goBack();
done(e);
// </hide>
});
// ```
// </snippet>
}

1
declarations.d.ts vendored
View File

@@ -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;
}

1
trace/trace.d.ts vendored
View File

@@ -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;

View File

@@ -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 = ",";

View File

@@ -0,0 +1,180 @@
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";
}
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<PropertyAnimation>;
public _playSequentially: boolean;
private _isPlaying: boolean;
private _resolve;
private _reject;
private _animationFinishedPromise: Promise<void>;
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<void> {
return this._animationFinishedPromise;
}
public get isPlaying(): boolean {
return this._isPlaying;
}
constructor(animationDefinitions: Array<definition.AnimationDefinition>, 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<PropertyAnimation>();
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<void>((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<PropertyAnimation> {
if (!animationDefinition.target) {
throw new Error("No animation target specified.");
}
var propertyAnimations = new Array<PropertyAnimation>();
// 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
});
}
}

View File

@@ -0,0 +1,256 @@
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 intType = java.lang.Integer.class.getField("TYPE").get(null);
var floatType = java.lang.Float.class.getField("TYPE").get(null);
var argbEvaluator: android.animation.ArgbEvaluator = new android.animation.ArgbEvaluator();
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<android.animation.ObjectAnimator>;
private _propertyUpdateCallbacks: Array<Function>;
private _propertyResetCallbacks: Array<Function>;
public play(): Animation {
super.play();
var i: number;
var length: number;
this._animators = new Array<android.animation.ObjectAnimator>();
this._propertyUpdateCallbacks = new Array<Function>();
this._propertyResetCallbacks = new Array<Function>();
i = 0;
length = this._propertyAnimations.length;
for (; i < length; i++) {
this._createAnimators(this._propertyAnimations[i]);
}
if (this._animators.length === 0) {
trace.write("Nothing to animate.", trace.categories.Animation);
this._resolveAnimationFinishedPromise();
return this;
}
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];
}
this._animatorSet = new android.animation.AnimatorSet();
this._animatorSet.addListener(this._animatorListener);
if (this._playSequentially) {
this._animatorSet.playSequentially(this._nativeAnimatorsArray);
}
else {
this._animatorSet.playTogether(this._nativeAnimatorsArray);
}
trace.write("Starting " + this._nativeAnimatorsArray.length + " animations " + (this._playSequentially ? "sequentially." : "together."), trace.categories.Animation);
this._animatorSet.start();
return this;
}
public cancel(): void {
super.cancel();
trace.write("Cancelling AnimatorSet.", trace.categories.Animation);
this._animatorSet.cancel();
}
constructor(animationDefinitions: Array<definition.AnimationDefinition>, 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();
}
});
}
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 = (<android.view.View>propertyAnimation.target._nativeView);
var animators = new Array<android.animation.ObjectAnimator>();
var propertyUpdateCallbacks = new Array<Function>();
var propertyResetCallbacks = new Array<Function>();
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] = (<color.Color>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);
}
}
if (propertyAnimation.curve !== undefined) {
animators[i].setInterpolator(propertyAnimation.curve);
}
trace.write("ObjectAnimator created: " + animators[i], trace.categories.Animation);
}
this._animators = this._animators.concat(animators);
this._propertyUpdateCallbacks = this._propertyUpdateCallbacks.concat(propertyUpdateCallbacks);
this._propertyResetCallbacks = this._propertyResetCallbacks.concat(propertyResetCallbacks);
}
}

80
ui/animation/animation.d.ts vendored Normal file
View File

@@ -0,0 +1,80 @@
declare module "ui/animation" {
import viewModule = require("ui/core/view");
import colorModule = require("color");
/**
* Defines animation options for the View.animate method.
*/
export interface AnimationDefinition {
/**
* The view whose property is to be animated.
*/
target?: viewModule.View;
/**
* Animates the opacity of the view. Value should be a number between 0.0 and 1.0
*/
opacity?: number;
/**
* Animates the backgroundColor of the view.
*/
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.
*/
duration?: number;
/**
* The amount of time, in milliseconds, to delay starting the animation.
*/
delay?: number;
/**
* 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
*/
iterations?: number;
/**
* An optional animation curve of type UIViewAnimationCurve for iOS or android.animation.TimeInterpolator for Android.
*/
curve?: any;
}
/**
* Defines a pair of values (horizontal and vertical) for translate and scale animations.
*/
export interface Pair {
x: number;
y: number;
}
/**
* Defines a animation set.
*/
export class Animation {
constructor(animationDefinitions: Array<AnimationDefinition>, playSequentially?: boolean);
public play: () => Animation;
public cancel: () => void;
public finished: Promise<void>;
public isPlaying: boolean;
}
}

View File

@@ -0,0 +1,282 @@
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";
var FLT_MAX = 340282346638528859811704183484516925440.000000;
class AnimationDelegateImpl extends NSObject {
static new(): AnimationDelegateImpl {
return <AnimationDelegateImpl>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] }
};
}
export class Animation extends common.Animation implements definition.Animation {
private _iOSAnimationFunction: Function;
private _finishedAnimations: number;
private _cancelledAnimations: number;
private _mergedPropertyAnimations: Array<common.PropertyAnimation>;
public play(): Animation {
super.play();
this._finishedAnimations = 0;
this._cancelledAnimations = 0;
this._iOSAnimationFunction();
return this;
}
public cancel(): void {
super.cancel();
var i = 0;
var length = this._mergedPropertyAnimations.length;
for (; i < length; i++) {
(<UIView>this._mergedPropertyAnimations[i].target._nativeView).layer.removeAllAnimations();
if ((<any>this._mergedPropertyAnimations[i])._propertyResetCallback) {
(<any>this._mergedPropertyAnimations[i])._propertyResetCallback();
}
}
}
constructor(animationDefinitions: Array<definition.AnimationDefinition>, playSequentially?: boolean) {
super(animationDefinitions, playSequentially);
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 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) {
that._rejectAnimationFinishedPromise();
}
else {
that._resolveAnimationFinishedPromise();
}
}
else {
// This callback will be called by each INDIVIDUAL animation when it finishes or is cancelled.
if (cancelled) {
that._cancelledAnimations++;
}
else {
that._finishedAnimations++;
}
if (that._cancelledAnimations === 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();
}
}
};
this._iOSAnimationFunction = Animation._createiOSAnimationFunction(this._mergedPropertyAnimations, 0, this._playSequentially, animationFinishedCallback);
}
private static _createiOSAnimationFunction(propertyAnimations: Array<common.PropertyAnimation>, 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 = (<UIView>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;
(<any>animation)._propertyResetCallback = () => { animation.target.opacity = originalValue };
animation.target.opacity = animation.value;
break;
case common.Properties.backgroundColor:
originalValue = animation.target.backgroundColor;
(<any>animation)._propertyResetCallback = () => { animation.target.backgroundColor = originalValue };
animation.target.backgroundColor = animation.value;
break;
case _transform:
originalValue = nativeView.transform;
(<any>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();
}
}
}
private static _isAffineTransform(property: string): boolean {
return property === _transform
|| property === common.Properties.translate
|| property === common.Properties.rotate
|| property === common.Properties.scale;
}
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<common.PropertyAnimation>): Array<common.PropertyAnimation> {
var result = new Array<common.PropertyAnimation>();
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;
}
}

View File

@@ -0,0 +1,2 @@
{ "name" : "animation",
"main" : "animation.js" }

View File

@@ -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<void> {
return this.createAnimation(animation).play().finished;
}
public createAnimation(animation: animationModule.AnimationDefinition): animationModule.Animation {
var that = this;
animation.target = that;
return new animationModule.Animation([animation]);
}
}

5
ui/core/view.d.ts vendored
View File

@@ -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<void>;
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;
}
}

View File

@@ -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 = (<Layout>data.object)._nativeView;
if (!nativeView) {
return;
}
var value = <boolean>data.newValue;
if (nativeView instanceof UIView) {
(<UIView>nativeView).clipsToBounds = value;
}
else if (nativeView instanceof android.view.ViewGroup) {
(<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<view.View> = new Array<view.View>();
@@ -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 = <Layout>data.object;
var nativeView: Object = layout._nativeView;
var value = <boolean>data.newValue;
if (nativeView instanceof android.view.ViewGroup) {
(<android.view.ViewGroup>nativeView).setClipChildren(value);
}
else if (nativeView instanceof UIView) {
(<UIView>nativeView).clipsToBounds = value;
}
get clipToBounds(): boolean {
return this._getValue(Layout.clipToBoundsProperty);
}
set clipToBounds(value: boolean) {
this._setValue(Layout.clipToBoundsProperty, value);
}
}