Animations Spike

This commit is contained in:
Rossen Hristov
2015-06-15 12:01:29 +03:00
parent 47bfc1c6e7
commit 09c00cb13b
17 changed files with 878 additions and 18 deletions

View File

@@ -134,6 +134,19 @@
<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="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 +804,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 +818,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>
@@ -1742,6 +1764,8 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<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,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 = <pages.Page>args.object;
page.bindingContext = vm;
panel1 = 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) {
var animations: Array<animation.Animation>;
animations = new Array<animation.Animation>();
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<animation.Animation>();
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<animation.Animation>;
animations = new Array<animation.Animation>();
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<animation.Animation>();
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<animation.Animation>, 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((<any>args.object).text);
}

View File

@@ -0,0 +1,29 @@
<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="{{ repeatCount, 'Repeat Count: ' + repeatCount + ' times' }}" width="180" marginTop="5" marginBottom="5"/>
<Slider minValue="0" maxValue="10" value="{{ repeatCount }}" 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="Slide Out" tap="onSlideOut" width="100" marginLeft="5" marginRight="5" />
<Button text="Slide In" tap="onSlideIn" width="100" marginLeft="5" marginRight="5" />
<Button text="Cancel" tap="onStop" width="100" 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 = 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 });
}
}

View File

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

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,7 @@
export module Properties {
export var opacity = "opacity";
export var backgroundColor = "backgroundColor";
export var translate = "translate";
export var rotate = "rotate";
export var scale = "scale";
}

View File

@@ -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 = (<android.view.View>animation.target._nativeView);
var animators = new Array<android.animation.ValueAnimator>();
var propertyUpdateCallbacks = new Array<Function>();
var propertyResetCallbacks = new Array<Function>();
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] = (<color.Color>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<definition.Animation>, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): definition.Cancelable {
var i: number;
var length: number;
var animators = new Array<android.animation.Animator>();
var propertyUpdateCallbacks = new Array<Function>();
var propertyResetCallbacks = new Array<Function>();
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();
}
};
}

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

@@ -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<Animation>, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): Cancelable;
}

View File

@@ -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 <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] }
};
}
function createiOSAnimation(animations: Array<definition.Animation>, 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 = (<UIView>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;
(<any>animation)._propertyResetCallback = () => { animation.target.opacity = originalValue };
animation.target.opacity = animation.value;
break;
case definition.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();
}
}
}
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<definition.Animation>): Array<definition.Animation> {
var result = new Array<definition.Animation>();
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<definition.Animation>, 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++) {
(<UIView>mergedAnimations[i].target._nativeView).layer.removeAllAnimations();
if ((<any>mergedAnimations[i])._propertyResetCallback) {
(<any>mergedAnimations[i])._propertyResetCallback();
}
}
}
};
}

View File

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

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