feat: Add 3D rotation to view - takeover of PR# 5950 (#8136)

* feat: add 3d rotation

* chore: fix build errors

* chore: fix tslint errors

* chore: add @types/chai dev dep

* chore: unused import cleanup

* chore: update tests for x,y rotation

* chore: rebase upstream/master

* fix: iOS Affine Transform test verification

* feat(css): Added optional css-tree parser (#8076)

* feat(css): Added optional css-tree parser

* test: css-tree parser compat tests

* test: more css-tree compat tests

* feat(dialogs): Setting the size of popup dialog thru dialog options (#8041)

* Added iOS specific height and width attributes to ShowModalOptions

* Set the height and width of the popup dialog to the presenting controller

* dialog options ios attributes presentationStyle, height & width are made optional

* Updated NativeScript.api.md for public API changes

* Update with git properties

* Public API

* CLA update

* fix: use iOS native-helper for 3d-rotate

* test: Fix tests using _getTransformMismatchError

* fix: view.__hasTransfrom not set updating properly

* test: fix css-animations test page

Co-authored-by: Alexander Vakrilov <alexander.vakrilov@gmail.com>
Co-authored-by: Darin Dimitrov <darin.dimitrov@gmail.com>
Co-authored-by: Shailesh Lolam <slolam@live.com>
Co-authored-by: Dimitar Topuzov <dtopuzov@gmail.com>
This commit is contained in:
Ryan Pendergast
2020-01-10 04:59:46 -06:00
committed by Alexander Vakrilov
parent 8550c3293d
commit e8f5ac8522
31 changed files with 709 additions and 192 deletions

View File

@ -243,7 +243,8 @@ export interface AnimationDefinition {
opacity?: number; opacity?: number;
rotate?: number; // Warning: (ae-forgotten-export) The symbol "Point3D" needs to be exported by the entry point index.d.ts
rotate?: number | Point3D;
scale?: Pair; scale?: Pair;
@ -2086,6 +2087,8 @@ export class Style extends Observable {
// (undocumented) // (undocumented)
public paddingTop: Length; public paddingTop: Length;
// (undocumented) // (undocumented)
public perspective: number;
// (undocumented)
public placeholderColor: Color; public placeholderColor: Color;
// Warning: (ae-forgotten-export) The symbol "PropertyBagClass" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PropertyBagClass" needs to be exported by the entry point index.d.ts
public readonly PropertyBag: PropertyBagClass; public readonly PropertyBag: PropertyBagClass;
@ -2094,6 +2097,10 @@ export class Style extends Observable {
// (undocumented) // (undocumented)
public rotate: number; public rotate: number;
// (undocumented) // (undocumented)
public rotateX: number;
// (undocumented)
public rotateY: number;
// (undocumented)
public scaleX: number; public scaleX: number;
// (undocumented) // (undocumented)
public scaleY: number; public scaleY: number;
@ -2702,12 +2709,15 @@ export abstract class View extends ViewBase {
opacity: number; opacity: number;
originX: number; originX: number;
originY: number; originY: number;
perspective: number;
// (undocumented) // (undocumented)
_redrawNativeBackground(value: any): void; _redrawNativeBackground(value: any): void;
// (undocumented) // (undocumented)
_removeAnimation(animation: Animation): boolean; _removeAnimation(animation: Animation): boolean;
public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number; public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number;
rotate: number; rotate: number;
rotateX: number;
rotateY: number;
scaleX: number; scaleX: number;
scaleY: number; scaleY: number;
_setCurrentLayoutBounds(left: number, top: number, right: number, bottom: number): { boundsChanged: boolean, sizeChanged: boolean }; _setCurrentLayoutBounds(left: number, top: number, right: number, bottom: number): { boundsChanged: boolean, sizeChanged: boolean };

View File

@ -0,0 +1,40 @@
import { EventData, Page } from "tns-core-modules/ui/page";
import { View } from "tns-core-modules/ui/core/view";
import { Point3D } from "tns-core-modules/ui/animation/animation";
let view: View;
export function pageLoaded(args: EventData) {
const page = <Page>args.object;
view = page.getViewById<View>("view");
}
export function onAnimateX(args: EventData) {
rotate({ x: 360, y: 0, z: 0 });
}
export function onAnimateY(args: EventData) {
rotate({ x: 0, y: 360, z: 0 });
}
export function onAnimateZ(args: EventData) {
rotate({ x: 0, y: 0, z: 360 });
}
export function onAnimateXYZ(args: EventData) {
rotate({ x: 360, y: 360, z: 360 });
}
async function rotate(rotate: Point3D) {
await view.animate({
rotate,
duration: 1000
});
reset();
}
function reset() {
view.rotate = 0;
view.rotateX = 0;
view.rotateY = 0;
}

View File

@ -0,0 +1,24 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="pageLoaded">
<ActionBar title="Rotate" />
<GridLayout rows="auto auto auto auto *" columns="* * *">
<Image src="~/res/icon_100x100.png" width="30" height="30" col="0" row="0" rotateX="60"/>
<Image src="~/res/icon_100x100.png" width="30" height="30" col="1" row="0" rotateY="60"/>
<Image src="~/res/icon_100x100.png" width="30" height="30" col="2" row="0" rotate="60"/>
<Button text="X" tap="onAnimateX" col="0" row="1"/>
<Button text="Y" tap="onAnimateY" col="1" row="1"/>
<Button text="Z" tap="onAnimateZ" col="2" row="1"/>
<Image src="~/res/icon_100x100.png" width="60" height="60" horizontalAlignment="center"
colSpan="3" row="2" rotate="60" rotateX="60" rotateY="60"/>
<Button text="XYZ" tap="onAnimateXYZ" row="3" colSpan="3"/>
<AbsoluteLayout width="300" height="300" clipToBounds="true" backgroundColor="LightGray" row="4" colSpan="3">
<Image id="view" src="~/res/icon_100x100.png"
width="100" height="100"
left="100" top="100"/>
</AbsoluteLayout>
</GridLayout>
</Page>

View File

@ -0,0 +1,75 @@
.rotate-x {
rotateX: 60;
}
.rotate-y {
rotateY: 60;
}
.rotate-z {
rotate: 60;
}
.original {
transform: none;
}
.animate-x {
animation-name: rotateX;
animation-duration: 2s;
animation-fill-mode: forwards;
}
.animate-y {
animation-name: rotateY;
animation-duration: 2s;
animation-fill-mode: forwards;
}
.animate-z {
animation-name: rotateZ;
animation-duration: 2s;
animation-fill-mode: forwards;
}
.animate-xyz-3d {
animation-name: rotateXYZ3D;
animation-duration: 2s;
animation-fill-mode: forwards;
}
.animate-xyz {
animation-name: rotateXYZ;
animation-duration: 2s;
animation-fill-mode: forwards;
}
@keyframes rotateX {
from { transform: none; }
50% { transform: rotateX(60) }
to { transform: none; }
}
@keyframes rotateY {
from { transform: none; }
50% { transform: rotateY(60) }
to { transform: none; }
}
@keyframes rotateZ {
from { transform: none; }
50% { transform: rotate(60) }
to { transform: none; }
}
@keyframes rotateXYZ3D {
from { transform: none; }
50% { transform: rotate3d(60, 60, 60) }
to { transform: none; }
}
@keyframes rotateXYZ {
from { transform: none; }
50% { transform: rotateX(60) rotateY(60) rotate(60) }
to { transform: none; }
}

View File

@ -0,0 +1,35 @@
import { EventData, Page } from "tns-core-modules/ui/page";
import { View } from "tns-core-modules/ui/core/view";
import { Point3D } from "tns-core-modules/ui/animation/animation";
let view: View;
export function pageLoaded(args: EventData) {
const page = <Page>args.object;
view = page.getViewById<View>("view");
}
export function onAnimateX(args: EventData) {
view.className = "original";
view.className = "animate-x";
}
export function onAnimateY(args: EventData) {
view.className = "original";
view.className = "animate-y";
}
export function onAnimateZ(args: EventData) {
view.className = "original";
view.className = "animate-z";
}
export function onAnimateXYZ3D(args: EventData) {
view.className = "original";
view.className = "animate-xyz-3d";
}
export function onAnimateXYZ(args: EventData) {
view.className = "original";
view.className = "animate-xyz";
}

View File

@ -0,0 +1,22 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="pageLoaded">
<ActionBar title="Rotate" />
<GridLayout rows="auto auto auto *" columns="* * *">
<Image src="~/res/icon_100x100.png" width="30" height="30" col="0" row="0" class="rotate-x"/>
<Image src="~/res/icon_100x100.png" width="30" height="30" col="1" row="0" class="rotate-y"/>
<Image src="~/res/icon_100x100.png" width="30" height="30" col="2" row="0" class="rotate-z"/>
<Button text="X" tap="onAnimateX" col="0" row="1"/>
<Button text="Y" tap="onAnimateY" col="1" row="1"/>
<Button text="Z" tap="onAnimateZ" col="2" row="1"/>
<Button text="XYZ" tap="onAnimateXYZ" row="2" col="0"/>
<Button text="XYZ-3D" tap="onAnimateXYZ3D" row="2" col="1"/>
<AbsoluteLayout width="300" height="300" clipToBounds="true" backgroundColor="LightGray" row="3" colSpan="3">
<Image id="view" src="~/res/icon_100x100.png"
width="100" height="100"
left="100" top="100" />
</AbsoluteLayout>
</GridLayout>
</Page>

View File

@ -11,6 +11,6 @@ export function pageLoaded(args: EventData) {
export function onButtonTap(args: EventData) { export function onButtonTap(args: EventData) {
const clickedButton = <Button>args.object; const clickedButton = <Button>args.object;
const destination = clickedButton.text + "/page"; const destination = "css-animations/" + clickedButton.text + "/page";
currentFrame.navigate(destination); currentFrame.navigate(destination);
} }

View File

@ -13,6 +13,7 @@
<Button text="settings" tap="onButtonTap"/> <Button text="settings" tap="onButtonTap"/>
<Button text="visual-states" tap="onButtonTap"/> <Button text="visual-states" tap="onButtonTap"/>
<Button text="initial-animation" tap="onButtonTap"/> <Button text="initial-animation" tap="onButtonTap"/>
<Button text="3d-rotate" tap="onButtonTap"/>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>
</Page> </Page>

View File

@ -18,7 +18,8 @@
<Button text="slide-in-effect" tap="onButtonTap" /> <Button text="slide-in-effect" tap="onButtonTap" />
<Button text="infinite" tap="onButtonTap" /> <Button text="infinite" tap="onButtonTap" />
<Button text="animation-curves" tap="onButtonTap" /> <Button text="animation-curves" tap="onButtonTap" />
<Button text="css-animations" tap="onButtonTap" /> <Button text="css-animations" tap="onButtonTap" />
<Button text="3d-rotate" tap="onButtonTap" />
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>

View File

@ -16,12 +16,15 @@ const TRANSFORM_MATRIXES = {
0, 1, y, 0, 1, y,
0, 0, 1, 0, 0, 1,
], ],
"rotate": angleInDeg => { "rotate": ({ x, y, z }) => {
const angleInRad = degreesToRadians(angleInDeg); // TODO: Handle rotations over X and Y axis
const radZ = degreesToRadians(z);
const cosZ = Math.cos(radZ);
const sinZ = Math.sin(radZ);
return [ return [
Math.cos(angleInRad), -Math.sin(angleInRad), 0, cosZ, -sinZ, 0,
Math.sin(angleInRad), Math.cos(angleInRad), 0, sinZ, cosZ, 0,
0, 0, 1, 0, 0, 1,
]; ];
}, },
@ -43,6 +46,7 @@ export function multiplyAffine2d(m1: number[], m2: number[]): number[] {
]; ];
} }
// TODO: Decompose rotations over X and Y axis
export function decompose2DTransformMatrix(matrix: number[]) export function decompose2DTransformMatrix(matrix: number[])
: TransformFunctionsInfo { : TransformFunctionsInfo {
@ -52,7 +56,7 @@ export function decompose2DTransformMatrix(matrix: number[])
const determinant = A * D - B * C; const determinant = A * D - B * C;
const translate = { x: E || 0, y: F || 0 }; const translate = { x: E || 0, y: F || 0 };
// rewrite with obj desctructuring using the identity matrix // rewrite with obj destructuring using the identity matrix
let rotate = 0; let rotate = 0;
let scale = { x: 1, y: 1 }; let scale = { x: 1, y: 1 };
if (A || B) { if (A || B) {
@ -67,7 +71,7 @@ export function decompose2DTransformMatrix(matrix: number[])
rotate = radiansToDegrees(rotate); rotate = radiansToDegrees(rotate);
return { translate, rotate, scale }; return { translate, rotate: { x: 0, y: 0, z: rotate }, scale };
} }
function verifyTransformMatrix(matrix: number[]) { function verifyTransformMatrix(matrix: number[]) {

View File

@ -26,6 +26,7 @@
"tslib": "1.10.0" "tslib": "1.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "~4.2.5",
"@types/node": "~10.12.18", "@types/node": "~10.12.18",
"tns-platform-declarations": "next" "tns-platform-declarations": "next"
}, },
@ -58,4 +59,4 @@
} }
} }
} }
} }

View File

@ -1,18 +1,19 @@
// Types. // Types.
import { import {
CubicBezierAnimationCurve as CubicBezierAnimationCurveDefinition, CubicBezierAnimationCurve as CubicBezierAnimationCurveDefinition,
Animation as AnimationBaseDefinition Animation as AnimationBaseDefinition,
Point3D
} from "."; } from ".";
import { import {
AnimationDefinition, AnimationPromise as AnimationPromiseDefinition, AnimationDefinition, AnimationPromise as AnimationPromiseDefinition,
Pair, PropertyAnimation Pair, PropertyAnimation
} from "./animation-interfaces"; } from "./animation-interfaces";
// Requires. // Requires.
import { Color } from "../../color"; import { Color } from "../../color";
import { import {
isEnabled as traceEnabled, write as traceWrite, isEnabled as traceEnabled, write as traceWrite,
categories as traceCategories, messageType as traceType categories as traceCategories, messageType as traceType
} from "../../trace"; } from "../../trace";
import { PercentLength } from "../styling/style-properties"; import { PercentLength } from "../styling/style-properties";
@ -150,24 +151,30 @@ export abstract class AnimationBase implements AnimationBaseDefinition {
} }
for (let item in animationDefinition) { for (let item in animationDefinition) {
if (animationDefinition[item] === undefined) { const value = animationDefinition[item];
if (value === undefined) {
continue; continue;
} }
if ((item === Properties.opacity || if ((item === Properties.opacity ||
item === Properties.rotate ||
item === "duration" || item === "duration" ||
item === "delay" || item === "delay" ||
item === "iterations") && typeof animationDefinition[item] !== "number") { item === "iterations") && typeof value !== "number") {
throw new Error(`Property ${item} must be valid number. Value: ${animationDefinition[item]}`); throw new Error(`Property ${item} must be valid number. Value: ${value}`);
} else if ((item === Properties.scale || item === Properties.translate) && } else if ((item === Properties.scale || item === Properties.translate) &&
(typeof (<Pair>animationDefinition[item]).x !== "number" || typeof (<Pair>animationDefinition[item]).y !== "number")) { (typeof (<Pair>value).x !== "number" || typeof (<Pair>value).y !== "number")) {
throw new Error(`Property ${item} must be valid Pair. Value: ${animationDefinition[item]}`); throw new Error(`Property ${item} must be valid Pair. Value: ${value}`);
} else if (item === Properties.backgroundColor && !Color.isValid(animationDefinition.backgroundColor)) { } else if (item === Properties.backgroundColor && !Color.isValid(animationDefinition.backgroundColor)) {
throw new Error(`Property ${item} must be valid color. Value: ${animationDefinition[item]}`); throw new Error(`Property ${item} must be valid color. Value: ${value}`);
} else if (item === Properties.width || item === Properties.height) { } else if (item === Properties.width || item === Properties.height) {
// Coerce input into a PercentLength object in case it's a string. // Coerce input into a PercentLength object in case it's a string.
animationDefinition[item] = PercentLength.parse(<any>animationDefinition[item]); animationDefinition[item] = PercentLength.parse(<any>value);
} else if (item === Properties.rotate) {
const rotate: number | Point3D = value;
if ((typeof rotate !== "number") &&
!(typeof rotate.x === "number" && typeof rotate.y === "number" && typeof rotate.z === "number")) {
throw new Error(`Property ${rotate} must be valid number or Point3D. Value: ${value}`);
}
} }
} }
@ -228,10 +235,18 @@ export abstract class AnimationBase implements AnimationBaseDefinition {
// rotate // rotate
if (animationDefinition.rotate !== undefined) { if (animationDefinition.rotate !== undefined) {
// Make sure the value of the rotation property is always Point3D
let rotationValue: Point3D;
if (typeof animationDefinition.rotate === "number") {
rotationValue = { x: 0, y: 0, z: animationDefinition.rotate };
} else {
rotationValue = animationDefinition.rotate;
}
propertyAnimations.push({ propertyAnimations.push({
target: animationDefinition.target, target: animationDefinition.target,
property: Properties.rotate, property: Properties.rotate,
value: animationDefinition.rotate, value: rotationValue,
duration: animationDefinition.duration, duration: animationDefinition.duration,
delay: animationDefinition.delay, delay: animationDefinition.delay,
iterations: animationDefinition.iterations, iterations: animationDefinition.iterations,

View File

@ -8,7 +8,7 @@ import {
traceEnabled, traceCategories, traceType traceEnabled, traceCategories, traceType
} from "./animation-common"; } from "./animation-common";
import { import {
opacityProperty, backgroundColorProperty, rotateProperty, opacityProperty, backgroundColorProperty, rotateProperty, rotateXProperty, rotateYProperty,
translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty,
heightProperty, widthProperty, PercentLength heightProperty, widthProperty, PercentLength
} from "../styling/style-properties"; } from "../styling/style-properties";
@ -91,6 +91,36 @@ export function _resolveAnimationCurve(curve: string | CubicBezierAnimationCurve
} }
} }
function getAndroidRepeatCount(iterations: number): number {
return (iterations === Number.POSITIVE_INFINITY) ? android.view.animation.Animation.INFINITE : iterations - 1;
}
function createObjectAnimator(nativeView: android.view.View, propertyName: string, value: number): android.animation.ObjectAnimator {
let arr = Array.create("float", 1);
arr[0] = value;
return android.animation.ObjectAnimator.ofFloat(nativeView, propertyName, arr);
}
function createAnimationSet(animators: android.animation.ObjectAnimator[], iterations: number): android.animation.AnimatorSet {
iterations = getAndroidRepeatCount(iterations);
const animatorSet = new android.animation.AnimatorSet();
const animatorsArray = Array.create(android.animation.Animator, animators.length);
animators.forEach((animator, index) => {
animatorsArray[index] = animator;
//TODO: not sure if we have to do that for each animator
animatorsArray[index].setRepeatCount(iterations);
});
animatorSet.playTogether(animatorsArray);
animatorSet.setupStartValues();
return animatorSet;
}
export class Animation extends AnimationBase { export class Animation extends AnimationBase {
private _animatorListener: android.animation.Animator.AnimatorListener; private _animatorListener: android.animation.Animator.AnimatorListener;
private _nativeAnimatorsArray: any; private _nativeAnimatorsArray: any;
@ -264,16 +294,14 @@ export class Animation extends AnimationBase {
this._target = propertyAnimation.target; this._target = propertyAnimation.target;
let nativeArray;
const nativeView = <android.view.View>propertyAnimation.target.nativeViewProtected; const nativeView = <android.view.View>propertyAnimation.target.nativeViewProtected;
const animators = new Array<android.animation.Animator>(); const animators = new Array<android.animation.Animator>();
const propertyUpdateCallbacks = new Array<Function>(); const propertyUpdateCallbacks = new Array<Function>();
const propertyResetCallbacks = new Array<Function>(); const propertyResetCallbacks = new Array<Function>();
let originalValue1; let originalValue1;
let originalValue2; let originalValue2;
let originalValue3;
const density = layout.getDisplayDensity(); const density = layout.getDisplayDensity();
let xyObjectAnimators: any;
let animatorSet: android.animation.AnimatorSet;
let key = propertyKeys[propertyAnimation.property]; let key = propertyKeys[propertyAnimation.property];
if (key) { if (key) {
@ -295,8 +323,6 @@ export class Animation extends AnimationBase {
opacityProperty._initDefaultNativeValue(style); opacityProperty._initDefaultNativeValue(style);
originalValue1 = nativeView.getAlpha(); originalValue1 = nativeView.getAlpha();
nativeArray = Array.create("float", 1);
nativeArray[0] = propertyAnimation.value;
propertyUpdateCallbacks.push(checkAnimation(() => { propertyUpdateCallbacks.push(checkAnimation(() => {
propertyAnimation.target.style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = propertyAnimation.value; propertyAnimation.target.style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = propertyAnimation.value;
})); }));
@ -310,7 +336,7 @@ export class Animation extends AnimationBase {
propertyAnimation.target[opacityProperty.setNative](propertyAnimation.target.style.opacity); propertyAnimation.target[opacityProperty.setNative](propertyAnimation.target.style.opacity);
} }
})); }));
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "alpha", nativeArray)); animators.push(createObjectAnimator(nativeView, "alpha", propertyAnimation.value));
break; break;
case Properties.backgroundColor: case Properties.backgroundColor:
@ -318,7 +344,7 @@ export class Animation extends AnimationBase {
ensureArgbEvaluator(); ensureArgbEvaluator();
originalValue1 = propertyAnimation.target.backgroundColor; originalValue1 = propertyAnimation.target.backgroundColor;
nativeArray = Array.create(java.lang.Object, 2); const nativeArray = Array.create(java.lang.Object, 2);
nativeArray[0] = propertyAnimation.target.backgroundColor ? java.lang.Integer.valueOf((<Color>propertyAnimation.target.backgroundColor).argb) : java.lang.Integer.valueOf(-1); nativeArray[0] = propertyAnimation.target.backgroundColor ? java.lang.Integer.valueOf((<Color>propertyAnimation.target.backgroundColor).argb) : java.lang.Integer.valueOf(-1);
nativeArray[1] = java.lang.Integer.valueOf((<Color>propertyAnimation.value).argb); nativeArray[1] = java.lang.Integer.valueOf((<Color>propertyAnimation.value).argb);
let animator = android.animation.ValueAnimator.ofObject(argbEvaluator, nativeArray); let animator = android.animation.ValueAnimator.ofObject(argbEvaluator, nativeArray);
@ -350,18 +376,6 @@ export class Animation extends AnimationBase {
translateXProperty._initDefaultNativeValue(style); translateXProperty._initDefaultNativeValue(style);
translateYProperty._initDefaultNativeValue(style); translateYProperty._initDefaultNativeValue(style);
xyObjectAnimators = Array.create(android.animation.Animator, 2);
nativeArray = Array.create("float", 1);
nativeArray[0] = propertyAnimation.value.x * density;
xyObjectAnimators[0] = android.animation.ObjectAnimator.ofFloat(nativeView, "translationX", nativeArray);
xyObjectAnimators[0].setRepeatCount(Animation._getAndroidRepeatCount(propertyAnimation.iterations));
nativeArray = Array.create("float", 1);
nativeArray[0] = propertyAnimation.value.y * density;
xyObjectAnimators[1] = android.animation.ObjectAnimator.ofFloat(nativeView, "translationY", nativeArray);
xyObjectAnimators[1].setRepeatCount(Animation._getAndroidRepeatCount(propertyAnimation.iterations));
originalValue1 = nativeView.getTranslationX() / density; originalValue1 = nativeView.getTranslationX() / density;
originalValue2 = nativeView.getTranslationY() / density; originalValue2 = nativeView.getTranslationY() / density;
@ -385,28 +399,17 @@ export class Animation extends AnimationBase {
} }
})); }));
animatorSet = new android.animation.AnimatorSet(); animators.push(
animatorSet.playTogether(xyObjectAnimators); createAnimationSet([
animatorSet.setupStartValues(); createObjectAnimator(nativeView, "translationX", propertyAnimation.value.x * density),
animators.push(animatorSet); createObjectAnimator(nativeView, "translationY", propertyAnimation.value.y * density)
], propertyAnimation.iterations));
break; break;
case Properties.scale: case Properties.scale:
scaleXProperty._initDefaultNativeValue(style); scaleXProperty._initDefaultNativeValue(style);
scaleYProperty._initDefaultNativeValue(style); scaleYProperty._initDefaultNativeValue(style);
xyObjectAnimators = Array.create(android.animation.Animator, 2);
nativeArray = Array.create("float", 1);
nativeArray[0] = propertyAnimation.value.x;
xyObjectAnimators[0] = android.animation.ObjectAnimator.ofFloat(nativeView, "scaleX", nativeArray);
xyObjectAnimators[0].setRepeatCount(Animation._getAndroidRepeatCount(propertyAnimation.iterations));
nativeArray = Array.create("float", 1);
nativeArray[0] = propertyAnimation.value.y;
xyObjectAnimators[1] = android.animation.ObjectAnimator.ofFloat(nativeView, "scaleY", nativeArray);
xyObjectAnimators[1].setRepeatCount(Animation._getAndroidRepeatCount(propertyAnimation.iterations));
originalValue1 = nativeView.getScaleX(); originalValue1 = nativeView.getScaleX();
originalValue2 = nativeView.getScaleY(); originalValue2 = nativeView.getScaleY();
@ -430,34 +433,53 @@ export class Animation extends AnimationBase {
} }
})); }));
animatorSet = new android.animation.AnimatorSet(); animators.push(
animatorSet.playTogether(xyObjectAnimators); createAnimationSet([
animatorSet.setupStartValues(); createObjectAnimator(nativeView, "scaleX", propertyAnimation.value.x),
animators.push(animatorSet); createObjectAnimator(nativeView, "scaleY", propertyAnimation.value.y)
], propertyAnimation.iterations));
break; break;
case Properties.rotate: case Properties.rotate:
rotateProperty._initDefaultNativeValue(style); rotateProperty._initDefaultNativeValue(style);
rotateXProperty._initDefaultNativeValue(style);
rotateYProperty._initDefaultNativeValue(style);
originalValue1 = nativeView.getRotationX();
originalValue2 = nativeView.getRotationY();
originalValue3 = nativeView.getRotation();
originalValue1 = nativeView.getRotation();
nativeArray = Array.create("float", 1);
nativeArray[0] = propertyAnimation.value;
propertyUpdateCallbacks.push(checkAnimation(() => { propertyUpdateCallbacks.push(checkAnimation(() => {
propertyAnimation.target.style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = propertyAnimation.value; propertyAnimation.target.style[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = propertyAnimation.value.x;
propertyAnimation.target.style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = propertyAnimation.value.y;
propertyAnimation.target.style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = propertyAnimation.value.z;
})); }));
propertyResetCallbacks.push(checkAnimation(() => { propertyResetCallbacks.push(checkAnimation(() => {
if (setLocal) { if (setLocal) {
propertyAnimation.target.style[rotateProperty.name] = originalValue1; propertyAnimation.target.style[rotateXProperty.name] = originalValue1;
propertyAnimation.target.style[rotateYProperty.name] = originalValue2;
propertyAnimation.target.style[rotateProperty.name] = originalValue3;
} else { } else {
propertyAnimation.target.style[rotateProperty.keyframe] = originalValue1; propertyAnimation.target.style[rotateXProperty.keyframe] = originalValue1;
propertyAnimation.target.style[rotateYProperty.keyframe] = originalValue2;
propertyAnimation.target.style[rotateProperty.keyframe] = originalValue3;
} }
if (propertyAnimation.target.nativeViewProtected) { if (propertyAnimation.target.nativeViewProtected) {
propertyAnimation.target[rotateProperty.setNative](propertyAnimation.target.style.rotate); propertyAnimation.target[rotateProperty.setNative](propertyAnimation.target.style.rotate);
propertyAnimation.target[rotateXProperty.setNative](propertyAnimation.target.style.rotateX);
propertyAnimation.target[rotateYProperty.setNative](propertyAnimation.target.style.rotateY);
} }
})); }));
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "rotation", nativeArray));
animators.push(
createAnimationSet([
createObjectAnimator(nativeView, "rotationX", propertyAnimation.value.x),
createObjectAnimator(nativeView, "rotationY", propertyAnimation.value.y),
createObjectAnimator(nativeView, "rotation", propertyAnimation.value.z)
], propertyAnimation.iterations));
break; break;
case Properties.width: case Properties.width:
case Properties.height: { case Properties.height: {
@ -465,7 +487,7 @@ export class Animation extends AnimationBase {
const extentProperty = isVertical ? heightProperty : widthProperty; const extentProperty = isVertical ? heightProperty : widthProperty;
extentProperty._initDefaultNativeValue(style); extentProperty._initDefaultNativeValue(style);
nativeArray = Array.create("float", 2); const nativeArray = Array.create("float", 2);
let toValue = propertyAnimation.value; let toValue = propertyAnimation.value;
let parent = propertyAnimation.target.parent as View; let parent = propertyAnimation.target.parent as View;
if (!parent) { if (!parent) {
@ -473,7 +495,7 @@ export class Animation extends AnimationBase {
} }
const parentExtent: number = isVertical ? parent.getMeasuredHeight() : parent.getMeasuredWidth(); const parentExtent: number = isVertical ? parent.getMeasuredHeight() : parent.getMeasuredWidth();
toValue = PercentLength.toDevicePixels(toValue, parentExtent, parentExtent) / screen.mainScreen.scale; toValue = PercentLength.toDevicePixels(toValue, parentExtent, parentExtent) / screen.mainScreen.scale;
const nativeHeight: number = isVertical ? nativeView.getHeight() : nativeView.getWidth(); let nativeHeight: number = isVertical ? nativeView.getHeight() : nativeView.getWidth();
const targetStyle: string = setLocal ? extentProperty.name : extentProperty.keyframe; const targetStyle: string = setLocal ? extentProperty.name : extentProperty.keyframe;
originalValue1 = nativeHeight / screen.mainScreen.scale; originalValue1 = nativeHeight / screen.mainScreen.scale;
nativeArray[0] = originalValue1; nativeArray[0] = originalValue1;
@ -516,7 +538,7 @@ export class Animation extends AnimationBase {
// Repeat Count // Repeat Count
if (propertyAnimation.iterations !== undefined && animators[i] instanceof android.animation.ValueAnimator) { if (propertyAnimation.iterations !== undefined && animators[i] instanceof android.animation.ValueAnimator) {
(<android.animation.ValueAnimator>animators[i]).setRepeatCount(Animation._getAndroidRepeatCount(propertyAnimation.iterations)); (<android.animation.ValueAnimator>animators[i]).setRepeatCount(getAndroidRepeatCount(propertyAnimation.iterations));
} }
// Interpolator // Interpolator
@ -533,8 +555,4 @@ export class Animation extends AnimationBase {
this._propertyUpdateCallbacks = this._propertyUpdateCallbacks.concat(propertyUpdateCallbacks); this._propertyUpdateCallbacks = this._propertyUpdateCallbacks.concat(propertyUpdateCallbacks);
this._propertyResetCallbacks = this._propertyResetCallbacks.concat(propertyResetCallbacks); this._propertyResetCallbacks = this._propertyResetCallbacks.concat(propertyResetCallbacks);
} }
private static _getAndroidRepeatCount(iterations: number): number {
return (iterations === Number.POSITIVE_INFINITY) ? android.view.animation.Animation.INFINITE : iterations - 1;
}
} }

View File

@ -47,7 +47,7 @@ export interface AnimationDefinition {
/** /**
* Animates the rotate affine transform of the view. Value should be a number specifying the rotation amount in degrees. * Animates the rotate affine transform of the view. Value should be a number specifying the rotation amount in degrees.
*/ */
rotate?: number; rotate?: number | Point3D;
/** /**
* The length of the animation in milliseconds. The default duration is 300 milliseconds. * The length of the animation in milliseconds. The default duration is 300 milliseconds.
@ -97,14 +97,24 @@ export type Transformation = {
/** /**
* Defines possible css transformations * Defines possible css transformations
*/ */
export type TransformationType = "rotate" | export type TransformationType =
"rotate" | "rotateX" | "rotateY" |
"translate" | "translateX" | "translateY" | "translate" | "translateX" | "translateY" |
"scale" | "scaleX" | "scaleY"; "scale" | "scaleX" | "scaleY";
/** /**
* Defines possible css transformation values * Defines possible css transformation values
*/ */
export type TransformationValue = Pair | number; export type TransformationValue = Point3D | Pair | number;
/**
* Defines a point in 3d space (x, y and z) for rotation in 3d animations.
*/
export interface Point3D {
x: number;
y: number;
z: number;
}
/** /**
* Defines a pair of values (horizontal and vertical) for translate and scale animations. * Defines a pair of values (horizontal and vertical) for translate and scale animations.
@ -119,7 +129,7 @@ export interface Pair {
*/ */
export type TransformFunctionsInfo = { export type TransformFunctionsInfo = {
translate: Pair, translate: Pair,
rotate: number, rotate: Point3D,
scale: Pair, scale: Pair,
} }

View File

@ -1,21 +1,23 @@
// Types // Types
import { import {
AnimationDefinitionInternal, AnimationPromise, IOSView, AnimationDefinitionInternal, AnimationPromise, IOSView,
PropertyAnimation, PropertyAnimationInfo PropertyAnimation, PropertyAnimationInfo
} from "./animation-common"; } from "./animation-common";
import { View } from "../core/view"; import { View } from "../core/view";
// Requires // Requires
import { import {
AnimationBase, Properties, CubicBezierAnimationCurve, AnimationBase, Properties, CubicBezierAnimationCurve,
traceWrite, traceEnabled, traceCategories, traceType traceWrite, traceEnabled, traceCategories, traceType
} from "./animation-common"; } from "./animation-common";
import { import {
opacityProperty, backgroundColorProperty, rotateProperty, opacityProperty, backgroundColorProperty, rotateProperty, rotateXProperty, rotateYProperty,
translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty,
heightProperty, widthProperty, PercentLength heightProperty, widthProperty, PercentLength
} from "../styling/style-properties"; } from "../styling/style-properties";
import { ios as iosNativeHelper } from "../../utils/native-helper";
import { screen } from "../../platform"; import { screen } from "../../platform";
export * from "./animation-common"; export * from "./animation-common";
@ -27,6 +29,7 @@ let FLT_MAX = 340282346638528859811704183484516925440.000000;
class AnimationInfo { class AnimationInfo {
public propertyNameToAnimate: string; public propertyNameToAnimate: string;
public subPropertiesToAnimate?: string[];
public fromValue: any; public fromValue: any;
public toValue: any; public toValue: any;
public duration: number; public duration: number;
@ -69,7 +72,9 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
targetStyle[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value; targetStyle[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value;
break; break;
case Properties.rotate: case Properties.rotate:
targetStyle[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value; targetStyle[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value.x;
targetStyle[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.y;
targetStyle[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value.z;
break; break;
case Properties.translate: case Properties.translate:
targetStyle[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x; targetStyle[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x;
@ -90,6 +95,11 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
targetStyle[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value[Properties.translate].x; targetStyle[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value[Properties.translate].x;
targetStyle[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value[Properties.translate].y; targetStyle[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value[Properties.translate].y;
} }
if (value[Properties.rotate] !== undefined) {
targetStyle[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value[Properties.rotate].x;
targetStyle[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value[Properties.rotate].y;
targetStyle[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value[Properties.rotate].z;
}
if (value[Properties.scale] !== undefined) { if (value[Properties.scale] !== undefined) {
let x = value[Properties.scale].x; let x = value[Properties.scale].x;
let y = value[Properties.scale].y; let y = value[Properties.scale].y;
@ -267,24 +277,23 @@ export class Animation extends AnimationBase {
} }
private static _getNativeAnimationArguments(animation: PropertyAnimationInfo, valueSource: "animation" | "keyframe"): AnimationInfo { private static _getNativeAnimationArguments(animation: PropertyAnimationInfo, valueSource: "animation" | "keyframe"): AnimationInfo {
const view = animation.target;
let nativeView = <UIView>animation.target.nativeViewProtected; const style = view.style;
let propertyNameToAnimate = animation.property; const nativeView = <UIView>view.nativeViewProtected;
let toValue = animation.value; const parent = view.parent as View;
let fromValue;
const parent = animation.target.parent as View;
const screenScale: number = screen.mainScreen.scale; const screenScale: number = screen.mainScreen.scale;
let tempRotate = (animation.target.rotate || 0) * Math.PI / 180; let propertyNameToAnimate = animation.property;
let abs; let subPropertyNameToAnimate;
let toValue = animation.value;
let fromValue;
let setLocal = valueSource === "animation"; let setLocal = valueSource === "animation";
switch (animation.property) { switch (animation.property) {
case Properties.backgroundColor: case Properties.backgroundColor:
animation._originalValue = animation.target.backgroundColor; animation._originalValue = view.backgroundColor;
animation._propertyResetCallback = (value, valueSource) => { animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? backgroundColorProperty.name : backgroundColorProperty.keyframe] = value; style[setLocal ? backgroundColorProperty.name : backgroundColorProperty.keyframe] = value;
}; };
fromValue = nativeView.layer.backgroundColor; fromValue = nativeView.layer.backgroundColor;
if (nativeView instanceof UILabel) { if (nativeView instanceof UILabel) {
@ -293,33 +302,50 @@ export class Animation extends AnimationBase {
toValue = toValue.CGColor; toValue = toValue.CGColor;
break; break;
case Properties.opacity: case Properties.opacity:
animation._originalValue = animation.target.opacity; animation._originalValue = view.opacity;
animation._propertyResetCallback = (value, valueSource) => { animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value; style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value;
}; };
fromValue = nativeView.layer.opacity; fromValue = nativeView.layer.opacity;
break; break;
case Properties.rotate: case Properties.rotate:
animation._originalValue = animation.target.rotate !== undefined ? animation.target.rotate : 0; animation._originalValue = { x: view.rotateX, y: view.rotateY, z: view.rotate };
animation._propertyResetCallback = (value, valueSource) => { animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value; style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value.z;
style[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value.x;
style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.y;
}; };
propertyNameToAnimate = "transform.rotation"; propertyNameToAnimate = "transform.rotation";
fromValue = nativeView.layer.valueForKeyPath("transform.rotation"); subPropertyNameToAnimate = ["x", "y", "z"];
fromValue = {
x: nativeView.layer.valueForKeyPath("transform.rotation.x"),
y: nativeView.layer.valueForKeyPath("transform.rotation.y"),
z: nativeView.layer.valueForKeyPath("transform.rotation.z")
};
if (animation.target.rotateX !== undefined && animation.target.rotateX !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) {
fromValue.x = animation.target.rotateX * Math.PI / 180;
}
if (animation.target.rotateY !== undefined && animation.target.rotateY !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) {
fromValue.y = animation.target.rotateY * Math.PI / 180;
}
if (animation.target.rotate !== undefined && animation.target.rotate !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) { if (animation.target.rotate !== undefined && animation.target.rotate !== 0 && Math.floor(toValue / 360) - toValue / 360 === 0) {
fromValue = animation.target.rotate * Math.PI / 180; fromValue.z = animation.target.rotate * Math.PI / 180;
}
toValue = toValue * Math.PI / 180;
abs = fabs(fromValue - toValue);
if (abs < 0.001 && fromValue !== tempRotate) {
fromValue = tempRotate;
} }
// Respect only value.z for back-compat until 3D rotations are implemented
toValue = {
x: toValue.x * Math.PI / 180,
y: toValue.y * Math.PI / 180,
z: toValue.z * Math.PI / 180
};
break; break;
case Properties.translate: case Properties.translate:
animation._originalValue = { x: animation.target.translateX, y: animation.target.translateY }; animation._originalValue = { x: view.translateX, y: view.translateY };
animation._propertyResetCallback = (value, valueSource) => { animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x; style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x;
animation.target.style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.y; style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.y;
}; };
propertyNameToAnimate = "transform"; propertyNameToAnimate = "transform";
fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform);
@ -332,10 +358,10 @@ export class Animation extends AnimationBase {
if (toValue.y === 0) { if (toValue.y === 0) {
toValue.y = 0.001; toValue.y = 0.001;
} }
animation._originalValue = { x: animation.target.scaleX, y: animation.target.scaleY }; animation._originalValue = { x: view.scaleX, y: view.scaleY };
animation._propertyResetCallback = (value, valueSource) => { animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x; style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x;
animation.target.style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y; style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y;
}; };
propertyNameToAnimate = "transform"; propertyNameToAnimate = "transform";
fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform);
@ -344,14 +370,18 @@ export class Animation extends AnimationBase {
case _transform: case _transform:
fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform); fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform);
animation._originalValue = { animation._originalValue = {
xs: animation.target.scaleX, ys: animation.target.scaleY, xs: view.scaleX, ys: view.scaleY,
xt: animation.target.translateX, yt: animation.target.translateY xt: view.translateX, yt: view.translateY,
rx: view.rotateX, ry: view.rotateY, rz: view.rotate
}; };
animation._propertyResetCallback = (value, valueSource) => { animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.xt; style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.xt;
animation.target.style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.yt; style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.yt;
animation.target.style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.xs; style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.xs;
animation.target.style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.ys; style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.ys;
style[setLocal ? rotateXProperty.name : rotateXProperty.keyframe] = value.rx;
style[setLocal ? rotateYProperty.name : rotateYProperty.keyframe] = value.ry;
style[setLocal ? rotateProperty.name : rotateProperty.keyframe] = value.rz;
}; };
propertyNameToAnimate = "transform"; propertyNameToAnimate = "transform";
toValue = NSValue.valueWithCATransform3D(Animation._createNativeAffineTransform(animation)); toValue = NSValue.valueWithCATransform3D(Animation._createNativeAffineTransform(animation));
@ -373,10 +403,10 @@ export class Animation extends AnimationBase {
toValue = NSValue.valueWithCGRect( toValue = NSValue.valueWithCGRect(
CGRectMake(currentBounds.origin.x, currentBounds.origin.y, extentX, extentY) CGRectMake(currentBounds.origin.x, currentBounds.origin.y, extentX, extentY)
); );
animation._originalValue = animation.target.height; animation._originalValue = view.height;
animation._propertyResetCallback = (value, valueSource) => { animation._propertyResetCallback = (value, valueSource) => {
const prop = isHeight ? heightProperty : widthProperty; const prop = isHeight ? heightProperty : widthProperty;
animation.target.style[setLocal ? prop.name : prop.keyframe] = value; style[setLocal ? prop.name : prop.keyframe] = value;
}; };
break; break;
default: default:
@ -406,6 +436,7 @@ export class Animation extends AnimationBase {
return { return {
propertyNameToAnimate: propertyNameToAnimate, propertyNameToAnimate: propertyNameToAnimate,
fromValue: fromValue, fromValue: fromValue,
subPropertiesToAnimate: subPropertyNameToAnimate,
toValue: toValue, toValue: toValue,
duration: duration, duration: duration,
repeatCount: repeatCount, repeatCount: repeatCount,
@ -416,18 +447,12 @@ export class Animation extends AnimationBase {
private static _createNativeAnimation(propertyAnimations: Array<PropertyAnimation>, index: number, playSequentially: boolean, args: AnimationInfo, animation: PropertyAnimation, valueSource: "animation" | "keyframe", finishedCallback: (cancelled?: boolean) => void) { private static _createNativeAnimation(propertyAnimations: Array<PropertyAnimation>, index: number, playSequentially: boolean, args: AnimationInfo, animation: PropertyAnimation, valueSource: "animation" | "keyframe", finishedCallback: (cancelled?: boolean) => void) {
let nativeView = <UIView>animation.target.nativeViewProtected; let nativeView = <UIView>animation.target.nativeViewProtected;
let nativeAnimation = CABasicAnimation.animationWithKeyPath(args.propertyNameToAnimate); let nativeAnimation;
nativeAnimation.fromValue = args.fromValue;
nativeAnimation.toValue = args.toValue; if (args.subPropertiesToAnimate) {
nativeAnimation.duration = args.duration; nativeAnimation = this._createGroupAnimation(args, animation);
if (args.repeatCount !== undefined) { } else {
nativeAnimation.repeatCount = args.repeatCount; nativeAnimation = this._createBasicAnimation(args, animation);
}
if (args.delay !== undefined) {
nativeAnimation.beginTime = CACurrentMediaTime() + args.delay;
}
if (animation.curve !== undefined) {
nativeAnimation.timingFunction = animation.curve;
} }
let animationDelegate = AnimationDelegateImpl.initWithFinishedCallback(finishedCallback, animation, valueSource); let animationDelegate = AnimationDelegateImpl.initWithFinishedCallback(finishedCallback, animation, valueSource);
@ -447,6 +472,44 @@ export class Animation extends AnimationBase {
} }
} }
private static _createGroupAnimation(args: AnimationInfo, animation: PropertyAnimation) {
let groupAnimation = CAAnimationGroup.new();
groupAnimation.duration = args.duration;
const animations = NSMutableArray.alloc<CAAnimation>().initWithCapacity(3);
args.subPropertiesToAnimate.forEach(property => {
const basicAnimationArgs = { ...args };
basicAnimationArgs.propertyNameToAnimate = `${args.propertyNameToAnimate}.${property}`;
basicAnimationArgs.fromValue = args.fromValue[property];
basicAnimationArgs.toValue = args.toValue[property];
const basicAnimation = this._createBasicAnimation(basicAnimationArgs, animation);
animations.addObject(basicAnimation);
});
groupAnimation.animations = animations;
return groupAnimation;
}
private static _createBasicAnimation(args: AnimationInfo, animation: PropertyAnimation) {
let basicAnimation = CABasicAnimation.animationWithKeyPath(args.propertyNameToAnimate);
basicAnimation.fromValue = args.fromValue;
basicAnimation.toValue = args.toValue;
basicAnimation.duration = args.duration;
if (args.repeatCount !== undefined) {
basicAnimation.repeatCount = args.repeatCount;
}
if (args.delay !== undefined) {
basicAnimation.beginTime = CACurrentMediaTime() + args.delay;
}
if (animation.curve !== undefined) {
basicAnimation.timingFunction = animation.curve;
}
return basicAnimation;
}
private static _createNativeSpringAnimation(propertyAnimations: Array<PropertyAnimationInfo>, index: number, playSequentially: boolean, args: AnimationInfo, animation: PropertyAnimationInfo, valueSource: "animation" | "keyframe", finishedCallback: (cancelled?: boolean) => void) { private static _createNativeSpringAnimation(propertyAnimations: Array<PropertyAnimationInfo>, index: number, playSequentially: boolean, args: AnimationInfo, animation: PropertyAnimationInfo, valueSource: "animation" | "keyframe", finishedCallback: (cancelled?: boolean) => void) {
let nativeView = <UIView>animation.target.nativeViewProtected; let nativeView = <UIView>animation.target.nativeViewProtected;
@ -490,9 +553,6 @@ export class Animation extends AnimationBase {
animation.target[animation.property] = value; animation.target[animation.property] = value;
}; };
break; break;
case Properties.rotate:
nativeView.layer.setValueForKey(args.toValue, args.propertyNameToAnimate);
break;
case _transform: case _transform:
animation._originalValue = nativeView.layer.transform; animation._originalValue = nativeView.layer.transform;
nativeView.layer.setValueForKey(args.toValue, args.propertyNameToAnimate); nativeView.layer.setValueForKey(args.toValue, args.propertyNameToAnimate);
@ -508,6 +568,11 @@ export class Animation extends AnimationBase {
animation.target.translateX = animation.value[Properties.translate].x; animation.target.translateX = animation.value[Properties.translate].x;
animation.target.translateY = animation.value[Properties.translate].y; animation.target.translateY = animation.value[Properties.translate].y;
} }
if (animation.value[Properties.rotate] !== undefined) {
animation.target.rotateX = animation.value[Properties.rotate].x;
animation.target.rotateY = animation.value[Properties.rotate].y;
animation.target.rotate = animation.value[Properties.rotate].z;
}
if (animation.value[Properties.scale] !== undefined) { if (animation.value[Properties.scale] !== undefined) {
animation.target.scaleX = animation.value[Properties.scale].x; animation.target.scaleX = animation.value[Properties.scale].x;
animation.target.scaleY = animation.value[Properties.scale].y; animation.target.scaleY = animation.value[Properties.scale].y;
@ -631,19 +696,41 @@ export class Animation extends AnimationBase {
} }
export function _getTransformMismatchErrorMessage(view: View): string { export function _getTransformMismatchErrorMessage(view: View): string {
// Order is important: translate, rotate, scale const expectedTransform = calculateTransform(view);
let result: CGAffineTransform = CGAffineTransformIdentity; const expectedTransformString = getCATransform3DString(expectedTransform);
const tx = view.translateX; const actualTransformString = getCATransform3DString(view.nativeViewProtected.layer.transform);
const ty = view.translateY;
result = CGAffineTransformTranslate(result, tx, ty);
result = CGAffineTransformRotate(result, (view.rotate || 0) * Math.PI / 180);
result = CGAffineTransformScale(result, view.scaleX || 1, view.scaleY || 1);
let viewTransform = NSStringFromCGAffineTransform(result);
let nativeTransform = NSStringFromCGAffineTransform(view.nativeViewProtected.transform);
if (viewTransform !== nativeTransform) { if (actualTransformString !== expectedTransformString) {
return "View and Native transforms do not match. View: " + viewTransform + "; Native: " + nativeTransform; return "View and Native transforms do not match.\nActual: " + actualTransformString + ";\nExpected: " + expectedTransformString;
} }
return undefined; return undefined;
} }
function calculateTransform(view: View): CATransform3D {
const scaleX = view.scaleX || 1e-6;
const scaleY = view.scaleY || 1e-6;
const perspective = view.perspective || 300;
// Order is important: translate, rotate, scale
let expectedTransform = new CATransform3D(CATransform3DIdentity);
// Only set perspective if there is 3D rotation
if (view.rotateX || view.rotateY) {
expectedTransform.m34 = -1 / perspective;
}
expectedTransform = CATransform3DTranslate(expectedTransform, view.translateX, view.translateY, 0);
expectedTransform = iosNativeHelper.applyRotateTransform(expectedTransform, view.rotateX, view.rotateY, view.rotate);
expectedTransform = CATransform3DScale(expectedTransform, scaleX, scaleY, 1);
return expectedTransform;
}
function getCATransform3DString(t: CATransform3D) {
return `[
${t.m11}, ${t.m12}, ${t.m13}, ${t.m14},
${t.m21}, ${t.m22}, ${t.m23}, ${t.m24},
${t.m31}, ${t.m32}, ${t.m33}, ${t.m34},
${t.m41}, ${t.m42}, ${t.m43}, ${t.m44}]`;
}

View File

@ -21,7 +21,7 @@ import {
backgroundColorProperty, backgroundColorProperty,
scaleXProperty, scaleYProperty, scaleXProperty, scaleYProperty,
translateXProperty, translateYProperty, translateXProperty, translateYProperty,
rotateProperty, opacityProperty, rotateProperty, opacityProperty, rotateXProperty, rotateYProperty,
widthProperty, heightProperty, PercentLength widthProperty, heightProperty, PercentLength
} from "../styling/style-properties"; } from "../styling/style-properties";
@ -61,7 +61,7 @@ interface Keyframe {
backgroundColor?: Color; backgroundColor?: Color;
scale?: { x: number, y: number }; scale?: { x: number, y: number };
translate?: { x: number, y: number }; translate?: { x: number, y: number };
rotate?: number; rotate?: { x: number, y: number, z: number };
opacity?: number; opacity?: number;
width?: PercentLength; width?: PercentLength;
height?: PercentLength; height?: PercentLength;
@ -213,7 +213,9 @@ export class KeyframeAnimation implements KeyframeAnimationDefinition {
view.style[translateYProperty.keyframe] = animation.translate.y; view.style[translateYProperty.keyframe] = animation.translate.y;
} }
if ("rotate" in animation) { if ("rotate" in animation) {
view.style[rotateProperty.keyframe] = animation.rotate; view.style[rotateXProperty.keyframe] = animation.rotate.x;
view.style[rotateYProperty.keyframe] = animation.rotate.y;
view.style[rotateProperty.keyframe] = animation.rotate.z;
} }
if ("opacity" in animation) { if ("opacity" in animation) {
view.style[opacityProperty.keyframe] = animation.opacity; view.style[opacityProperty.keyframe] = animation.opacity;

View File

@ -697,6 +697,27 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.style.rotate = value; this.style.rotate = value;
} }
get rotateX(): number {
return this.style.rotateX;
}
set rotateX(value: number) {
this.style.rotateX = value;
}
get rotateY(): number {
return this.style.rotateY;
}
set rotateY(value: number) {
this.style.rotateY = value;
}
get perspective(): number {
return this.style.perspective;
}
set perspective(value: number) {
this.style.perspective = value;
}
get translateX(): dip { get translateX(): dip {
return this.style.translateX; return this.style.translateX;
} }

View File

@ -10,17 +10,18 @@ import {
} from "./view-common"; } from "./view-common";
import { import {
Length, PercentLength, Visibility, HorizontalAlignment, VerticalAlignment, perspectiveProperty, Length, PercentLength, Visibility, HorizontalAlignment, VerticalAlignment,
visibilityProperty, opacityProperty, horizontalAlignmentProperty, verticalAlignmentProperty, visibilityProperty, opacityProperty, horizontalAlignmentProperty, verticalAlignmentProperty,
minWidthProperty, minHeightProperty, widthProperty, heightProperty, minWidthProperty, minHeightProperty, widthProperty, heightProperty,
marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty, marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty,
rotateProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty, rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty,
zIndexProperty, backgroundInternalProperty, androidElevationProperty, androidDynamicElevationOffsetProperty zIndexProperty, backgroundInternalProperty, androidElevationProperty, androidDynamicElevationOffsetProperty
} from "../../styling/style-properties"; } from "../../styling/style-properties";
import { Background, ad as androidBackground } from "../../styling/background"; import { Background, ad as androidBackground } from "../../styling/background";
import { profile } from "../../../profiling"; import { profile } from "../../../profiling";
import { topmost } from "../../frame/frame-stack"; import { topmost } from "../../frame/frame-stack";
import { screen } from "../../../platform";
import { AndroidActivityBackPressedEventData, android as androidApp } from "../../../application"; import { AndroidActivityBackPressedEventData, android as androidApp } from "../../../application";
import { device } from "../../../platform"; import { device } from "../../../platform";
import lazy from "../../../utils/lazy"; import lazy from "../../../utils/lazy";
@ -911,6 +912,20 @@ export class View extends ViewCommon {
org.nativescript.widgets.ViewHelper.setRotate(this.nativeViewProtected, float(value)); org.nativescript.widgets.ViewHelper.setRotate(this.nativeViewProtected, float(value));
} }
[rotateXProperty.setNative](value: number) {
org.nativescript.widgets.ViewHelper.setRotateX(this.nativeViewProtected, float(value));
}
[rotateYProperty.setNative](value: number) {
org.nativescript.widgets.ViewHelper.setRotateY(this.nativeViewProtected, float(value));
}
[perspectiveProperty.setNative](value: number) {
const scale = screen.mainScreen.scale;
const distance = value * scale;
org.nativescript.widgets.ViewHelper.setPerspective(this.nativeViewProtected, float(distance));
}
[scaleXProperty.setNative](value: number) { [scaleXProperty.setNative](value: number) {
org.nativescript.widgets.ViewHelper.setScaleX(this.nativeViewProtected, float(value)); org.nativescript.widgets.ViewHelper.setScaleX(this.nativeViewProtected, float(value));
} }

View File

@ -75,6 +75,11 @@ export interface Point {
* Represents the y coordinate of the location. * Represents the y coordinate of the location.
*/ */
y: number; y: number;
/**
* Represents the z coordinate of the location.
*/
z?: number;
} }
/** /**
@ -315,10 +320,26 @@ export abstract class View extends ViewBase {
opacity: number; opacity: number;
/** /**
* Gets or sets the rotate affine transform of the view. * Gets or sets the rotate affine transform of the view along the Z axis.
*/ */
rotate: number; rotate: number;
/**
* Gets or sets the rotate affine transform of the view along the X axis.
*/
rotateX: number;
/**
* Gets or sets the rotate affine transform of the view along the Y axis.
*/
rotateY: number;
/**
* Gets or sets the distance of the camera form the view perspective.
* Usually needed when rotating the view over the X or Y axis.
*/
perspective: number;
/** /**
* Gets or sets the translateX affine transform of the view in device independent pixels. * Gets or sets the translateX affine transform of the view in device independent pixels.
*/ */

View File

@ -9,10 +9,12 @@ import {
import { ios } from "./view-helper"; import { ios } from "./view-helper";
import { ios as iosBackground, Background } from "../../styling/background"; import { ios as iosBackground, Background } from "../../styling/background";
import { ios as iosUtils } from "../../../utils/utils"; import { ios as iosUtils } from "../../../utils/utils";
import { ios as iosNativeHelper } from "../../../utils/native-helper";
import { import {
Visibility, perspectiveProperty, Visibility,
visibilityProperty, opacityProperty, visibilityProperty, opacityProperty,
rotateProperty, scaleXProperty, scaleYProperty, rotateProperty, rotateXProperty, rotateYProperty,
scaleXProperty, scaleYProperty,
translateXProperty, translateYProperty, zIndexProperty, translateXProperty, translateYProperty, zIndexProperty,
backgroundInternalProperty, clipPathProperty backgroundInternalProperty, clipPathProperty
} from "../../styling/style-properties"; } from "../../styling/style-properties";
@ -156,6 +158,7 @@ export class View extends ViewCommon implements ViewDefinition {
} }
public _setNativeViewFrame(nativeView: UIView, frame: CGRect): void { public _setNativeViewFrame(nativeView: UIView, frame: CGRect): void {
let oldFrame = this._cachedFrame || nativeView.frame; let oldFrame = this._cachedFrame || nativeView.frame;
if (!CGRectEqualToRect(oldFrame, frame)) { if (!CGRectEqualToRect(oldFrame, frame)) {
if (traceEnabled()) { if (traceEnabled()) {
@ -166,8 +169,8 @@ export class View extends ViewCommon implements ViewDefinition {
let transform = null; let transform = null;
if (this._hasTransfrom) { if (this._hasTransfrom) {
// Always set identity transform before setting frame; // Always set identity transform before setting frame;
transform = nativeView.transform; transform = nativeView.layer.transform;
nativeView.transform = CGAffineTransformIdentity; nativeView.layer.transform = CATransform3DIdentity;
nativeView.frame = frame; nativeView.frame = frame;
} else { } else {
nativeView.frame = frame; nativeView.frame = frame;
@ -177,10 +180,10 @@ export class View extends ViewCommon implements ViewDefinition {
if (adjustedFrame) { if (adjustedFrame) {
nativeView.frame = adjustedFrame; nativeView.frame = adjustedFrame;
} }
if (this._hasTransfrom) { if (this._hasTransfrom) {
// re-apply the transform after the frame is adjusted // re-apply the transform after the frame is adjusted
nativeView.transform = transform; nativeView.layer.transform = transform;
} }
const boundsOrigin = nativeView.bounds.origin; const boundsOrigin = nativeView.bounds.origin;
@ -348,18 +351,25 @@ export class View extends ViewCommon implements ViewDefinition {
public updateNativeTransform() { public updateNativeTransform() {
const scaleX = this.scaleX || 1e-6; const scaleX = this.scaleX || 1e-6;
const scaleY = this.scaleY || 1e-6; const scaleY = this.scaleY || 1e-6;
const rotate = this.rotate || 0; const perspective = this.perspective || 300;
let newTransform = CGAffineTransformIdentity;
newTransform = CGAffineTransformTranslate(newTransform, this.translateX, this.translateY); let transform = new CATransform3D(CATransform3DIdentity);
newTransform = CGAffineTransformRotate(newTransform, rotate * Math.PI / 180);
newTransform = CGAffineTransformScale(newTransform, scaleX, scaleY); // Only set perspective if there is 3D rotation
if (!CGAffineTransformEqualToTransform(this.nativeViewProtected.transform, newTransform)) { if (this.rotateX || this.rotateY) {
transform.m34 = -1 / perspective;
}
transform = CATransform3DTranslate(transform, this.translateX, this.translateY, 0);
transform = iosNativeHelper.applyRotateTransform(transform, this.rotateX, this.rotateY, this.rotate);
transform = CATransform3DScale(transform, scaleX, scaleY, 1);
if (!CATransform3DEqualToTransform(this.nativeViewProtected.layer.transform, transform)) {
const updateSuspended = this._isPresentationLayerUpdateSuspeneded(); const updateSuspended = this._isPresentationLayerUpdateSuspeneded();
if (!updateSuspended) { if (!updateSuspended) {
CATransaction.begin(); CATransaction.begin();
} }
this.nativeViewProtected.transform = newTransform; this.nativeViewProtected.layer.transform = transform;
this._hasTransfrom = this.nativeViewProtected && !CGAffineTransformEqualToTransform(this.nativeViewProtected.transform, CGAffineTransformIdentity); this._hasTransfrom = this.nativeViewProtected && !CATransform3DEqualToTransform(this.nativeViewProtected.transform3D, CATransform3DIdentity);
if (!updateSuspended) { if (!updateSuspended) {
CATransaction.commit(); CATransaction.commit();
} }
@ -579,6 +589,27 @@ export class View extends ViewCommon implements ViewDefinition {
this.updateNativeTransform(); this.updateNativeTransform();
} }
[rotateXProperty.getDefault](): number {
return 0;
}
[rotateXProperty.setNative](value: number) {
this.updateNativeTransform();
}
[rotateYProperty.getDefault](): number {
return 0;
}
[rotateYProperty.setNative](value: number) {
this.updateNativeTransform();
}
[perspectiveProperty.getDefault](): number {
return 300;
}
[perspectiveProperty.setNative](value: number) {
this.updateNativeTransform();
}
[scaleXProperty.getDefault](): number { [scaleXProperty.getDefault](): number {
return 1; return 1;
} }

View File

@ -472,6 +472,15 @@ function convertToPaddings(this: void, value: string | Length): [CssProperty<any
export const rotateProperty = new CssAnimationProperty<Style, number>({ name: "rotate", cssName: "rotate", defaultValue: 0, valueConverter: parseFloat }); export const rotateProperty = new CssAnimationProperty<Style, number>({ name: "rotate", cssName: "rotate", defaultValue: 0, valueConverter: parseFloat });
rotateProperty.register(Style); rotateProperty.register(Style);
export const rotateXProperty = new CssAnimationProperty<Style, number>({ name: "rotateX", cssName: "rotatex", defaultValue: 0, valueConverter: parseFloat });
rotateXProperty.register(Style);
export const rotateYProperty = new CssAnimationProperty<Style, number>({ name: "rotateY", cssName: "rotatey", defaultValue: 0, valueConverter: parseFloat });
rotateYProperty.register(Style);
export const perspectiveProperty = new CssAnimationProperty<Style, number>({ name: "perspective", cssName: "perspective", defaultValue: 1000, valueConverter: parseFloat });
perspectiveProperty.register(Style);
export const scaleXProperty = new CssAnimationProperty<Style, number>({ name: "scaleX", cssName: "scaleX", defaultValue: 1, valueConverter: parseFloat }); export const scaleXProperty = new CssAnimationProperty<Style, number>({ name: "scaleX", cssName: "scaleX", defaultValue: 1, valueConverter: parseFloat });
scaleXProperty.register(Style); scaleXProperty.register(Style);
@ -500,6 +509,8 @@ const transformProperty = new ShorthandProperty<Style, string>({
let translateX = this.translateX; let translateX = this.translateX;
let translateY = this.translateY; let translateY = this.translateY;
let rotate = this.rotate; let rotate = this.rotate;
let rotateX = this.rotateX;
let rotateY = this.rotateY;
let result = ""; let result = "";
if (translateX !== 0 || translateY !== 0) { if (translateX !== 0 || translateY !== 0) {
result += `translate(${translateX}, ${translateY}) `; result += `translate(${translateX}, ${translateY}) `;
@ -507,7 +518,8 @@ const transformProperty = new ShorthandProperty<Style, string>({
if (scaleX !== 1 || scaleY !== 1) { if (scaleX !== 1 || scaleY !== 1) {
result += `scale(${scaleX}, ${scaleY}) `; result += `scale(${scaleX}, ${scaleY}) `;
} }
if (rotate !== 0) { if (rotateX !== 0 || rotateY !== 0 || rotate !== 0) {
result += `rotate(${rotateX}, ${rotateY}, ${rotate}) `;
result += `rotate (${rotate})`; result += `rotate (${rotate})`;
} }
@ -519,13 +531,16 @@ transformProperty.register(Style);
const IDENTITY_TRANSFORMATION = { const IDENTITY_TRANSFORMATION = {
translate: { x: 0, y: 0 }, translate: { x: 0, y: 0 },
rotate: 0, rotate: { x: 0, y: 0, z: 0 },
scale: { x: 1, y: 1 }, scale: { x: 1, y: 1 },
}; };
const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g); const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g);
const TRANSFORMATIONS = Object.freeze([ const TRANSFORMATIONS = Object.freeze([
"rotate", "rotate",
"rotateX",
"rotateY",
"rotate3d",
"translate", "translate",
"translate3d", "translate3d",
"translateX", "translateX",
@ -547,7 +562,28 @@ const STYLE_TRANSFORMATION_MAP = Object.freeze({
"translateX": ({ x }) => ({ property: "translate", value: { x, y: IDENTITY_TRANSFORMATION.translate.y } }), "translateX": ({ x }) => ({ property: "translate", value: { x, y: IDENTITY_TRANSFORMATION.translate.y } }),
"translateY": ({ y }) => ({ property: "translate", value: { y, x: IDENTITY_TRANSFORMATION.translate.x } }), "translateY": ({ y }) => ({ property: "translate", value: { y, x: IDENTITY_TRANSFORMATION.translate.x } }),
"rotate": value => ({ property: "rotate", value }), "rotate3d": value => ({ property: "rotate", value }),
"rotateX": (x) => ({
property: "rotate", value: {
x,
y: IDENTITY_TRANSFORMATION.rotate.y,
z: IDENTITY_TRANSFORMATION.rotate.z
}
}),
"rotateY": (y) => ({
property: "rotate", value: {
x: IDENTITY_TRANSFORMATION.rotate.x,
y,
z: IDENTITY_TRANSFORMATION.rotate.z
}
}),
"rotate": (z) => ({
property: "rotate", value: {
x: IDENTITY_TRANSFORMATION.rotate.x,
y: IDENTITY_TRANSFORMATION.rotate.y,
z
}
}),
}); });
function convertToTransform(value: string): [CssProperty<any, any>, any][] { function convertToTransform(value: string): [CssProperty<any, any>, any][] {
@ -564,7 +600,9 @@ function convertToTransform(value: string): [CssProperty<any, any>, any][] {
[scaleXProperty, scale.x], [scaleXProperty, scale.x],
[scaleYProperty, scale.y], [scaleYProperty, scale.y],
[rotateProperty, rotate], [rotateProperty, rotate.z],
[rotateXProperty, rotate.x],
[rotateYProperty, rotate.y],
]; ];
} }
@ -619,13 +657,13 @@ function normalizeTransformation({ property, value }: Transformation): Transform
function convertTransformValue(property: string, stringValue: string) function convertTransformValue(property: string, stringValue: string)
: TransformationValue { : TransformationValue {
const [x, y = x] = stringValue.split(",").map(parseFloat); const [x, y = x, z = y] = stringValue.split(",").map(parseFloat);
if (property === "rotate") { if (property === "rotate" || property === "rotateX" || property === "rotateY") {
return stringValue.slice(-3) === "rad" ? radiansToDegrees(x) : x; return stringValue.slice(-3) === "rad" ? radiansToDegrees(x) : x;
} }
return { x, y }; return { x, y, z };
} }
// Background properties. // Background properties.

View File

@ -525,6 +525,8 @@ export class CssState {
const view = this.viewRef.get(); const view = this.viewRef.get();
if (view) { if (view) {
view.style["keyframe:rotate"] = unsetValue; view.style["keyframe:rotate"] = unsetValue;
view.style["keyframe:rotateX"] = unsetValue;
view.style["keyframe:rotateY"] = unsetValue;
view.style["keyframe:scaleX"] = unsetValue; view.style["keyframe:scaleX"] = unsetValue;
view.style["keyframe:scaleY"] = unsetValue; view.style["keyframe:scaleY"] = unsetValue;
view.style["keyframe:translateX"] = unsetValue; view.style["keyframe:translateX"] = unsetValue;

View File

@ -53,6 +53,9 @@ export class Style extends Observable {
public backgroundInternal: Background; public backgroundInternal: Background;
public rotate: number; public rotate: number;
public rotateX: number;
public rotateY: number;
public perspective: number;
public scaleX: number; public scaleX: number;
public scaleY: number; public scaleY: number;
public translateX: dip; public translateX: dip;

View File

@ -86,6 +86,10 @@ export class Style extends Observable implements StyleDefinition {
public backgroundInternal: Background; public backgroundInternal: Background;
public rotate: number; public rotate: number;
public rotateX: number;
public rotateY: number;
public perspective: number;
public scaleX: number; public scaleX: number;
public scaleY: number; public scaleY: number;
public translateX: dip; public translateX: dip;

View File

@ -155,5 +155,14 @@ export module ios {
*/ */
export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/; export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/;
export class UIDocumentInteractionControllerDelegateImpl {} /**
*
* @param transform Applies a rotation transform over X,Y and Z axis
* @param x Rotation over X axis in degrees
* @param y Rotation over Y axis in degrees
* @param z Rotation over Z axis in degrees
*/
export function applyRotateTransform(transform: any /* CATransform3D*/, x: number, y: number, z: number): any /* CATransform3D*/;
export class UIDocumentInteractionControllerDelegateImpl { }
} }

View File

@ -4,6 +4,8 @@ import {
write as traceWrite write as traceWrite
} from "../trace"; } from "../trace";
const radToDeg = Math.PI / 180;
function isOrientationLandscape(orientation: number) { function isOrientationLandscape(orientation: number) {
return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ || return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ ||
orientation === UIDeviceOrientation.LandscapeRight /* 4 */; orientation === UIDeviceOrientation.LandscapeRight /* 4 */;
@ -114,23 +116,39 @@ export module ios {
} }
export function applyRotateTransform(transform: CATransform3D, x: number, y: number, z: number): CATransform3D {
if (x) {
transform = CATransform3DRotate(transform, x * radToDeg, 1, 0, 0);
}
if (y) {
transform = CATransform3DRotate(transform, y * radToDeg, 0, 1, 0);
}
if (z) {
transform = CATransform3DRotate(transform, z * radToDeg, 0, 0, 1);
}
return transform;
}
export class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { export class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate {
public static ObjCProtocols = [UIDocumentInteractionControllerDelegate]; public static ObjCProtocols = [UIDocumentInteractionControllerDelegate];
public getViewController(): UIViewController { public getViewController(): UIViewController {
const app = UIApplication.sharedApplication; const app = UIApplication.sharedApplication;
return app.keyWindow.rootViewController; return app.keyWindow.rootViewController;
} }
public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) { public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) {
return this.getViewController(); return this.getViewController();
} }
public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) { public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) {
return this.getViewController().view; return this.getViewController().view;
} }
public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect { public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect {
return this.getViewController().view.frame; return this.getViewController().view.frame;
} }

View File

@ -251,6 +251,7 @@ export module ios {
* Returns the visible UIViewController. * Returns the visible UIViewController.
*/ */
export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/; export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/;
} }
/** /**

View File

@ -14,7 +14,7 @@ export function openFile(filePath: string): boolean {
const path = filePath.replace("~", appPath); const path = filePath.replace("~", appPath);
const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path)); const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path));
controller.delegate = <UIDocumentInteractionControllerDelegate> new ios.UIDocumentInteractionControllerDelegateImpl(); controller.delegate = <UIDocumentInteractionControllerDelegate>new ios.UIDocumentInteractionControllerDelegateImpl();
return controller.presentPreviewAnimated(true); return controller.presentPreviewAnimated(true);
} }
@ -48,4 +48,4 @@ export function openUrl(location: string): boolean {
return false; return false;
} }
mainScreenScale = UIScreen.mainScreen.scale; mainScreenScale = UIScreen.mainScreen.scale;

View File

@ -433,6 +433,7 @@ function animateExtentAndAssertExpected(along: "height" | "width", value: Percen
expectedNumber, expectedNumber,
`PercentLength.toDevicePixels(${inputString}) should be "${expectedNumber}" but is "${observedNumber}"` `PercentLength.toDevicePixels(${inputString}) should be "${expectedNumber}" but is "${observedNumber}"`
); );
assertIOSNativeTransformIsCorrect(label); assertIOSNativeTransformIsCorrect(label);
}); });
} }

View File

@ -122,7 +122,7 @@ export function test_ReadTransformAllSet() {
const animation = createAnimationFromCSS(css, "test"); const animation = createAnimationFromCSS(css, "test");
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations); const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
TKUnit.assertAreClose(rotate, 10, DELTA); TKUnit.assertAreClose(rotate.z, 10, DELTA);
TKUnit.assertAreClose(scale.x, 5, SCALE_DELTA); TKUnit.assertAreClose(scale.x, 5, SCALE_DELTA);
TKUnit.assertAreClose(scale.y, 1, SCALE_DELTA); TKUnit.assertAreClose(scale.y, 1, SCALE_DELTA);
@ -136,7 +136,7 @@ export function test_ReadTransformNone() {
const animation = createAnimationFromCSS(css, "test"); const animation = createAnimationFromCSS(css, "test");
const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations); const { rotate, scale, translate } = getTransformsValues(animation.keyframes[0].declarations);
TKUnit.assertEqual(rotate, 0); TKUnit.assertEqual(rotate.z, 0);
TKUnit.assertEqual(scale.x, 1); TKUnit.assertEqual(scale.x, 1);
TKUnit.assertEqual(scale.y, 1); TKUnit.assertEqual(scale.y, 1);
@ -271,7 +271,7 @@ export function test_ReadRotate() {
const { rotate } = getTransforms(animation.keyframes[0].declarations); const { rotate } = getTransforms(animation.keyframes[0].declarations);
TKUnit.assertEqual(rotate.property, "rotate"); TKUnit.assertEqual(rotate.property, "rotate");
TKUnit.assertAreClose(rotate.value, 5, DELTA); TKUnit.assertAreClose(rotate.value.z, 5, DELTA);
} }
export function test_ReadRotateDeg() { export function test_ReadRotateDeg() {
@ -280,7 +280,7 @@ export function test_ReadRotateDeg() {
const { rotate } = getTransforms(animation.keyframes[0].declarations); const { rotate } = getTransforms(animation.keyframes[0].declarations);
TKUnit.assertEqual(rotate.property, "rotate"); TKUnit.assertEqual(rotate.property, "rotate");
TKUnit.assertAreClose(rotate.value, 45, DELTA); TKUnit.assertAreClose(rotate.value.z, 45, DELTA);
} }
export function test_ReadRotateRad() { export function test_ReadRotateRad() {
@ -289,7 +289,7 @@ export function test_ReadRotateRad() {
const { rotate } = getTransforms(animation.keyframes[0].declarations); const { rotate } = getTransforms(animation.keyframes[0].declarations);
TKUnit.assertEqual(rotate.property, "rotate"); TKUnit.assertEqual(rotate.property, "rotate");
TKUnit.assertAreClose(rotate.value, 45, DELTA); TKUnit.assertAreClose(rotate.value.z, 45, DELTA);
} }
export function test_ReadAnimationWithUnsortedKeyframes() { export function test_ReadAnimationWithUnsortedKeyframes() {

View File

@ -576,6 +576,14 @@
public static getRotate(view: android.view.View): number; public static getRotate(view: android.view.View): number;
public static setRotate(view: android.view.View, value: number): void; public static setRotate(view: android.view.View, value: number): void;
public static getRotateX(view: android.view.View): number;
public static setRotateX(view: android.view.View, value: number): void;
public static getRotateY(view: android.view.View): number;
public static setRotateY(view: android.view.View, value: number): void;
public static setPerspective(view: android.view.View, value: number): void;
public static getScaleX(view: android.view.View): number; public static getScaleX(view: android.view.View): number;
public static setScaleX(view: android.view.View, value: number): void; public static setScaleX(view: android.view.View, value: number): void;