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;
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;
@ -2086,6 +2087,8 @@ export class Style extends Observable {
// (undocumented)
public paddingTop: Length;
// (undocumented)
public perspective: number;
// (undocumented)
public placeholderColor: Color;
// Warning: (ae-forgotten-export) The symbol "PropertyBagClass" needs to be exported by the entry point index.d.ts
public readonly PropertyBag: PropertyBagClass;
@ -2094,6 +2097,10 @@ export class Style extends Observable {
// (undocumented)
public rotate: number;
// (undocumented)
public rotateX: number;
// (undocumented)
public rotateY: number;
// (undocumented)
public scaleX: number;
// (undocumented)
public scaleY: number;
@ -2702,12 +2709,15 @@ export abstract class View extends ViewBase {
opacity: number;
originX: number;
originY: number;
perspective: number;
// (undocumented)
_redrawNativeBackground(value: any): void;
// (undocumented)
_removeAnimation(animation: Animation): boolean;
public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number;
rotate: number;
rotateX: number;
rotateY: number;
scaleX: number;
scaleY: number;
_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) {
const clickedButton = <Button>args.object;
const destination = clickedButton.text + "/page";
const destination = "css-animations/" + clickedButton.text + "/page";
currentFrame.navigate(destination);
}

View File

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

View File

@ -19,6 +19,7 @@
<Button text="infinite" tap="onButtonTap" />
<Button text="animation-curves" tap="onButtonTap" />
<Button text="css-animations" tap="onButtonTap" />
<Button text="3d-rotate" tap="onButtonTap" />
</StackLayout>
</ScrollView>

View File

@ -16,12 +16,15 @@ const TRANSFORM_MATRIXES = {
0, 1, y,
0, 0, 1,
],
"rotate": angleInDeg => {
const angleInRad = degreesToRadians(angleInDeg);
"rotate": ({ x, y, z }) => {
// TODO: Handle rotations over X and Y axis
const radZ = degreesToRadians(z);
const cosZ = Math.cos(radZ);
const sinZ = Math.sin(radZ);
return [
Math.cos(angleInRad), -Math.sin(angleInRad), 0,
Math.sin(angleInRad), Math.cos(angleInRad), 0,
cosZ, -sinZ, 0,
sinZ, cosZ, 0,
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[])
: TransformFunctionsInfo {
@ -52,7 +56,7 @@ export function decompose2DTransformMatrix(matrix: number[])
const determinant = A * D - B * C;
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 scale = { x: 1, y: 1 };
if (A || B) {
@ -67,7 +71,7 @@ export function decompose2DTransformMatrix(matrix: number[])
rotate = radiansToDegrees(rotate);
return { translate, rotate, scale };
return { translate, rotate: { x: 0, y: 0, z: rotate }, scale };
}
function verifyTransformMatrix(matrix: number[]) {

View File

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

View File

@ -1,7 +1,8 @@
// Types.
import {
CubicBezierAnimationCurve as CubicBezierAnimationCurveDefinition,
Animation as AnimationBaseDefinition
Animation as AnimationBaseDefinition,
Point3D
} from ".";
import {
AnimationDefinition, AnimationPromise as AnimationPromiseDefinition,
@ -150,24 +151,30 @@ export abstract class AnimationBase implements AnimationBaseDefinition {
}
for (let item in animationDefinition) {
if (animationDefinition[item] === undefined) {
const value = animationDefinition[item];
if (value === undefined) {
continue;
}
if ((item === Properties.opacity ||
item === Properties.rotate ||
item === "duration" ||
item === "delay" ||
item === "iterations") && typeof animationDefinition[item] !== "number") {
throw new Error(`Property ${item} must be valid number. Value: ${animationDefinition[item]}`);
item === "iterations") && typeof value !== "number") {
throw new Error(`Property ${item} must be valid number. Value: ${value}`);
} else if ((item === Properties.scale || item === Properties.translate) &&
(typeof (<Pair>animationDefinition[item]).x !== "number" || typeof (<Pair>animationDefinition[item]).y !== "number")) {
throw new Error(`Property ${item} must be valid Pair. Value: ${animationDefinition[item]}`);
(typeof (<Pair>value).x !== "number" || typeof (<Pair>value).y !== "number")) {
throw new Error(`Property ${item} must be valid Pair. Value: ${value}`);
} 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) {
// 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
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({
target: animationDefinition.target,
property: Properties.rotate,
value: animationDefinition.rotate,
value: rotationValue,
duration: animationDefinition.duration,
delay: animationDefinition.delay,
iterations: animationDefinition.iterations,

View File

@ -8,7 +8,7 @@ import {
traceEnabled, traceCategories, traceType
} from "./animation-common";
import {
opacityProperty, backgroundColorProperty, rotateProperty,
opacityProperty, backgroundColorProperty, rotateProperty, rotateXProperty, rotateYProperty,
translateXProperty, translateYProperty, scaleXProperty, scaleYProperty,
heightProperty, widthProperty, PercentLength
} 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 {
private _animatorListener: android.animation.Animator.AnimatorListener;
private _nativeAnimatorsArray: any;
@ -264,16 +294,14 @@ export class Animation extends AnimationBase {
this._target = propertyAnimation.target;
let nativeArray;
const nativeView = <android.view.View>propertyAnimation.target.nativeViewProtected;
const animators = new Array<android.animation.Animator>();
const propertyUpdateCallbacks = new Array<Function>();
const propertyResetCallbacks = new Array<Function>();
let originalValue1;
let originalValue2;
let originalValue3;
const density = layout.getDisplayDensity();
let xyObjectAnimators: any;
let animatorSet: android.animation.AnimatorSet;
let key = propertyKeys[propertyAnimation.property];
if (key) {
@ -295,8 +323,6 @@ export class Animation extends AnimationBase {
opacityProperty._initDefaultNativeValue(style);
originalValue1 = nativeView.getAlpha();
nativeArray = Array.create("float", 1);
nativeArray[0] = propertyAnimation.value;
propertyUpdateCallbacks.push(checkAnimation(() => {
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);
}
}));
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "alpha", nativeArray));
animators.push(createObjectAnimator(nativeView, "alpha", propertyAnimation.value));
break;
case Properties.backgroundColor:
@ -318,7 +344,7 @@ export class Animation extends AnimationBase {
ensureArgbEvaluator();
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[1] = java.lang.Integer.valueOf((<Color>propertyAnimation.value).argb);
let animator = android.animation.ValueAnimator.ofObject(argbEvaluator, nativeArray);
@ -350,18 +376,6 @@ export class Animation extends AnimationBase {
translateXProperty._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;
originalValue2 = nativeView.getTranslationY() / density;
@ -385,28 +399,17 @@ export class Animation extends AnimationBase {
}
}));
animatorSet = new android.animation.AnimatorSet();
animatorSet.playTogether(xyObjectAnimators);
animatorSet.setupStartValues();
animators.push(animatorSet);
animators.push(
createAnimationSet([
createObjectAnimator(nativeView, "translationX", propertyAnimation.value.x * density),
createObjectAnimator(nativeView, "translationY", propertyAnimation.value.y * density)
], propertyAnimation.iterations));
break;
case Properties.scale:
scaleXProperty._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();
originalValue2 = nativeView.getScaleY();
@ -430,34 +433,53 @@ export class Animation extends AnimationBase {
}
}));
animatorSet = new android.animation.AnimatorSet();
animatorSet.playTogether(xyObjectAnimators);
animatorSet.setupStartValues();
animators.push(animatorSet);
animators.push(
createAnimationSet([
createObjectAnimator(nativeView, "scaleX", propertyAnimation.value.x),
createObjectAnimator(nativeView, "scaleY", propertyAnimation.value.y)
], propertyAnimation.iterations));
break;
case Properties.rotate:
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(() => {
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(() => {
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 {
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) {
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;
case Properties.width:
case Properties.height: {
@ -465,7 +487,7 @@ export class Animation extends AnimationBase {
const extentProperty = isVertical ? heightProperty : widthProperty;
extentProperty._initDefaultNativeValue(style);
nativeArray = Array.create("float", 2);
const nativeArray = Array.create("float", 2);
let toValue = propertyAnimation.value;
let parent = propertyAnimation.target.parent as View;
if (!parent) {
@ -473,7 +495,7 @@ export class Animation extends AnimationBase {
}
const parentExtent: number = isVertical ? parent.getMeasuredHeight() : parent.getMeasuredWidth();
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;
originalValue1 = nativeHeight / screen.mainScreen.scale;
nativeArray[0] = originalValue1;
@ -516,7 +538,7 @@ export class Animation extends AnimationBase {
// Repeat Count
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
@ -533,8 +555,4 @@ export class Animation extends AnimationBase {
this._propertyUpdateCallbacks = this._propertyUpdateCallbacks.concat(propertyUpdateCallbacks);
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.
*/
rotate?: number;
rotate?: number | Point3D;
/**
* The length of the animation in milliseconds. The default duration is 300 milliseconds.
@ -97,14 +97,24 @@ export type Transformation = {
/**
* Defines possible css transformations
*/
export type TransformationType = "rotate" |
export type TransformationType =
"rotate" | "rotateX" | "rotateY" |
"translate" | "translateX" | "translateY" |
"scale" | "scaleX" | "scaleY";
/**
* 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.
@ -119,7 +129,7 @@ export interface Pair {
*/
export type TransformFunctionsInfo = {
translate: Pair,
rotate: number,
rotate: Point3D,
scale: Pair,
}

View File

@ -11,11 +11,13 @@ import {
traceWrite, traceEnabled, traceCategories, traceType
} from "./animation-common";
import {
opacityProperty, backgroundColorProperty, rotateProperty,
opacityProperty, backgroundColorProperty, rotateProperty, rotateXProperty, rotateYProperty,
translateXProperty, translateYProperty, scaleXProperty, scaleYProperty,
heightProperty, widthProperty, PercentLength
} from "../styling/style-properties";
import { ios as iosNativeHelper } from "../../utils/native-helper";
import { screen } from "../../platform";
export * from "./animation-common";
@ -27,6 +29,7 @@ let FLT_MAX = 340282346638528859811704183484516925440.000000;
class AnimationInfo {
public propertyNameToAnimate: string;
public subPropertiesToAnimate?: string[];
public fromValue: any;
public toValue: any;
public duration: number;
@ -69,7 +72,9 @@ class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate {
targetStyle[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value;
break;
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;
case Properties.translate:
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 ? 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) {
let x = value[Properties.scale].x;
let y = value[Properties.scale].y;
@ -267,24 +277,23 @@ export class Animation extends AnimationBase {
}
private static _getNativeAnimationArguments(animation: PropertyAnimationInfo, valueSource: "animation" | "keyframe"): AnimationInfo {
let nativeView = <UIView>animation.target.nativeViewProtected;
let propertyNameToAnimate = animation.property;
let toValue = animation.value;
let fromValue;
const parent = animation.target.parent as View;
const view = animation.target;
const style = view.style;
const nativeView = <UIView>view.nativeViewProtected;
const parent = view.parent as View;
const screenScale: number = screen.mainScreen.scale;
let tempRotate = (animation.target.rotate || 0) * Math.PI / 180;
let abs;
let propertyNameToAnimate = animation.property;
let subPropertyNameToAnimate;
let toValue = animation.value;
let fromValue;
let setLocal = valueSource === "animation";
switch (animation.property) {
case Properties.backgroundColor:
animation._originalValue = animation.target.backgroundColor;
animation._originalValue = view.backgroundColor;
animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? backgroundColorProperty.name : backgroundColorProperty.keyframe] = value;
style[setLocal ? backgroundColorProperty.name : backgroundColorProperty.keyframe] = value;
};
fromValue = nativeView.layer.backgroundColor;
if (nativeView instanceof UILabel) {
@ -293,33 +302,50 @@ export class Animation extends AnimationBase {
toValue = toValue.CGColor;
break;
case Properties.opacity:
animation._originalValue = animation.target.opacity;
animation._originalValue = view.opacity;
animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value;
style[setLocal ? opacityProperty.name : opacityProperty.keyframe] = value;
};
fromValue = nativeView.layer.opacity;
break;
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.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";
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) {
fromValue = animation.target.rotate * Math.PI / 180;
}
toValue = toValue * Math.PI / 180;
abs = fabs(fromValue - toValue);
if (abs < 0.001 && fromValue !== tempRotate) {
fromValue = tempRotate;
fromValue.z = animation.target.rotate * Math.PI / 180;
}
// 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;
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.target.style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x;
animation.target.style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.y;
style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.x;
style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.y;
};
propertyNameToAnimate = "transform";
fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform);
@ -332,10 +358,10 @@ export class Animation extends AnimationBase {
if (toValue.y === 0) {
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.target.style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x;
animation.target.style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y;
style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.x;
style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.y;
};
propertyNameToAnimate = "transform";
fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform);
@ -344,14 +370,18 @@ export class Animation extends AnimationBase {
case _transform:
fromValue = NSValue.valueWithCATransform3D(nativeView.layer.transform);
animation._originalValue = {
xs: animation.target.scaleX, ys: animation.target.scaleY,
xt: animation.target.translateX, yt: animation.target.translateY
xs: view.scaleX, ys: view.scaleY,
xt: view.translateX, yt: view.translateY,
rx: view.rotateX, ry: view.rotateY, rz: view.rotate
};
animation._propertyResetCallback = (value, valueSource) => {
animation.target.style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.xt;
animation.target.style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.yt;
animation.target.style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.xs;
animation.target.style[setLocal ? scaleYProperty.name : scaleYProperty.keyframe] = value.ys;
style[setLocal ? translateXProperty.name : translateXProperty.keyframe] = value.xt;
style[setLocal ? translateYProperty.name : translateYProperty.keyframe] = value.yt;
style[setLocal ? scaleXProperty.name : scaleXProperty.keyframe] = value.xs;
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";
toValue = NSValue.valueWithCATransform3D(Animation._createNativeAffineTransform(animation));
@ -373,10 +403,10 @@ export class Animation extends AnimationBase {
toValue = NSValue.valueWithCGRect(
CGRectMake(currentBounds.origin.x, currentBounds.origin.y, extentX, extentY)
);
animation._originalValue = animation.target.height;
animation._originalValue = view.height;
animation._propertyResetCallback = (value, valueSource) => {
const prop = isHeight ? heightProperty : widthProperty;
animation.target.style[setLocal ? prop.name : prop.keyframe] = value;
style[setLocal ? prop.name : prop.keyframe] = value;
};
break;
default:
@ -406,6 +436,7 @@ export class Animation extends AnimationBase {
return {
propertyNameToAnimate: propertyNameToAnimate,
fromValue: fromValue,
subPropertiesToAnimate: subPropertyNameToAnimate,
toValue: toValue,
duration: duration,
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) {
let nativeView = <UIView>animation.target.nativeViewProtected;
let nativeAnimation = CABasicAnimation.animationWithKeyPath(args.propertyNameToAnimate);
nativeAnimation.fromValue = args.fromValue;
nativeAnimation.toValue = args.toValue;
nativeAnimation.duration = args.duration;
if (args.repeatCount !== undefined) {
nativeAnimation.repeatCount = args.repeatCount;
}
if (args.delay !== undefined) {
nativeAnimation.beginTime = CACurrentMediaTime() + args.delay;
}
if (animation.curve !== undefined) {
nativeAnimation.timingFunction = animation.curve;
let nativeAnimation;
if (args.subPropertiesToAnimate) {
nativeAnimation = this._createGroupAnimation(args, animation);
} else {
nativeAnimation = this._createBasicAnimation(args, animation);
}
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) {
let nativeView = <UIView>animation.target.nativeViewProtected;
@ -490,9 +553,6 @@ export class Animation extends AnimationBase {
animation.target[animation.property] = value;
};
break;
case Properties.rotate:
nativeView.layer.setValueForKey(args.toValue, args.propertyNameToAnimate);
break;
case _transform:
animation._originalValue = nativeView.layer.transform;
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.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) {
animation.target.scaleX = animation.value[Properties.scale].x;
animation.target.scaleY = animation.value[Properties.scale].y;
@ -631,19 +696,41 @@ export class Animation extends AnimationBase {
}
export function _getTransformMismatchErrorMessage(view: View): string {
// Order is important: translate, rotate, scale
let result: CGAffineTransform = CGAffineTransformIdentity;
const tx = view.translateX;
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);
const expectedTransform = calculateTransform(view);
const expectedTransformString = getCATransform3DString(expectedTransform);
const actualTransformString = getCATransform3DString(view.nativeViewProtected.layer.transform);
if (viewTransform !== nativeTransform) {
return "View and Native transforms do not match. View: " + viewTransform + "; Native: " + nativeTransform;
if (actualTransformString !== expectedTransformString) {
return "View and Native transforms do not match.\nActual: " + actualTransformString + ";\nExpected: " + expectedTransformString;
}
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,
scaleXProperty, scaleYProperty,
translateXProperty, translateYProperty,
rotateProperty, opacityProperty,
rotateProperty, opacityProperty, rotateXProperty, rotateYProperty,
widthProperty, heightProperty, PercentLength
} from "../styling/style-properties";
@ -61,7 +61,7 @@ interface Keyframe {
backgroundColor?: Color;
scale?: { x: number, y: number };
translate?: { x: number, y: number };
rotate?: number;
rotate?: { x: number, y: number, z: number };
opacity?: number;
width?: PercentLength;
height?: PercentLength;
@ -213,7 +213,9 @@ export class KeyframeAnimation implements KeyframeAnimationDefinition {
view.style[translateYProperty.keyframe] = animation.translate.y;
}
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) {
view.style[opacityProperty.keyframe] = animation.opacity;

View File

@ -697,6 +697,27 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
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 {
return this.style.translateX;
}

View File

@ -10,17 +10,18 @@ import {
} from "./view-common";
import {
Length, PercentLength, Visibility, HorizontalAlignment, VerticalAlignment,
perspectiveProperty, Length, PercentLength, Visibility, HorizontalAlignment, VerticalAlignment,
visibilityProperty, opacityProperty, horizontalAlignmentProperty, verticalAlignmentProperty,
minWidthProperty, minHeightProperty, widthProperty, heightProperty,
marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty,
rotateProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty,
rotateProperty, rotateXProperty, rotateYProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty,
zIndexProperty, backgroundInternalProperty, androidElevationProperty, androidDynamicElevationOffsetProperty
} from "../../styling/style-properties";
import { Background, ad as androidBackground } from "../../styling/background";
import { profile } from "../../../profiling";
import { topmost } from "../../frame/frame-stack";
import { screen } from "../../../platform";
import { AndroidActivityBackPressedEventData, android as androidApp } from "../../../application";
import { device } from "../../../platform";
import lazy from "../../../utils/lazy";
@ -911,6 +912,20 @@ export class View extends ViewCommon {
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) {
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.
*/
y: number;
/**
* Represents the z coordinate of the location.
*/
z?: number;
}
/**
@ -315,10 +320,26 @@ export abstract class View extends ViewBase {
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;
/**
* 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.
*/

View File

@ -9,10 +9,12 @@ import {
import { ios } from "./view-helper";
import { ios as iosBackground, Background } from "../../styling/background";
import { ios as iosUtils } from "../../../utils/utils";
import { ios as iosNativeHelper } from "../../../utils/native-helper";
import {
Visibility,
perspectiveProperty, Visibility,
visibilityProperty, opacityProperty,
rotateProperty, scaleXProperty, scaleYProperty,
rotateProperty, rotateXProperty, rotateYProperty,
scaleXProperty, scaleYProperty,
translateXProperty, translateYProperty, zIndexProperty,
backgroundInternalProperty, clipPathProperty
} from "../../styling/style-properties";
@ -156,6 +158,7 @@ export class View extends ViewCommon implements ViewDefinition {
}
public _setNativeViewFrame(nativeView: UIView, frame: CGRect): void {
let oldFrame = this._cachedFrame || nativeView.frame;
if (!CGRectEqualToRect(oldFrame, frame)) {
if (traceEnabled()) {
@ -166,8 +169,8 @@ export class View extends ViewCommon implements ViewDefinition {
let transform = null;
if (this._hasTransfrom) {
// Always set identity transform before setting frame;
transform = nativeView.transform;
nativeView.transform = CGAffineTransformIdentity;
transform = nativeView.layer.transform;
nativeView.layer.transform = CATransform3DIdentity;
nativeView.frame = frame;
} else {
nativeView.frame = frame;
@ -180,7 +183,7 @@ export class View extends ViewCommon implements ViewDefinition {
if (this._hasTransfrom) {
// re-apply the transform after the frame is adjusted
nativeView.transform = transform;
nativeView.layer.transform = transform;
}
const boundsOrigin = nativeView.bounds.origin;
@ -348,18 +351,25 @@ export class View extends ViewCommon implements ViewDefinition {
public updateNativeTransform() {
const scaleX = this.scaleX || 1e-6;
const scaleY = this.scaleY || 1e-6;
const rotate = this.rotate || 0;
let newTransform = CGAffineTransformIdentity;
newTransform = CGAffineTransformTranslate(newTransform, this.translateX, this.translateY);
newTransform = CGAffineTransformRotate(newTransform, rotate * Math.PI / 180);
newTransform = CGAffineTransformScale(newTransform, scaleX, scaleY);
if (!CGAffineTransformEqualToTransform(this.nativeViewProtected.transform, newTransform)) {
const perspective = this.perspective || 300;
let transform = new CATransform3D(CATransform3DIdentity);
// Only set perspective if there is 3D rotation
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();
if (!updateSuspended) {
CATransaction.begin();
}
this.nativeViewProtected.transform = newTransform;
this._hasTransfrom = this.nativeViewProtected && !CGAffineTransformEqualToTransform(this.nativeViewProtected.transform, CGAffineTransformIdentity);
this.nativeViewProtected.layer.transform = transform;
this._hasTransfrom = this.nativeViewProtected && !CATransform3DEqualToTransform(this.nativeViewProtected.transform3D, CATransform3DIdentity);
if (!updateSuspended) {
CATransaction.commit();
}
@ -579,6 +589,27 @@ export class View extends ViewCommon implements ViewDefinition {
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 {
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 });
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 });
scaleXProperty.register(Style);
@ -500,6 +509,8 @@ const transformProperty = new ShorthandProperty<Style, string>({
let translateX = this.translateX;
let translateY = this.translateY;
let rotate = this.rotate;
let rotateX = this.rotateX;
let rotateY = this.rotateY;
let result = "";
if (translateX !== 0 || translateY !== 0) {
result += `translate(${translateX}, ${translateY}) `;
@ -507,7 +518,8 @@ const transformProperty = new ShorthandProperty<Style, string>({
if (scaleX !== 1 || scaleY !== 1) {
result += `scale(${scaleX}, ${scaleY}) `;
}
if (rotate !== 0) {
if (rotateX !== 0 || rotateY !== 0 || rotate !== 0) {
result += `rotate(${rotateX}, ${rotateY}, ${rotate}) `;
result += `rotate (${rotate})`;
}
@ -519,13 +531,16 @@ transformProperty.register(Style);
const IDENTITY_TRANSFORMATION = {
translate: { x: 0, y: 0 },
rotate: 0,
rotate: { x: 0, y: 0, z: 0 },
scale: { x: 1, y: 1 },
};
const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g);
const TRANSFORMATIONS = Object.freeze([
"rotate",
"rotateX",
"rotateY",
"rotate3d",
"translate",
"translate3d",
"translateX",
@ -547,7 +562,28 @@ const STYLE_TRANSFORMATION_MAP = Object.freeze({
"translateX": ({ x }) => ({ property: "translate", value: { x, y: IDENTITY_TRANSFORMATION.translate.y } }),
"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][] {
@ -564,7 +600,9 @@ function convertToTransform(value: string): [CssProperty<any, any>, any][] {
[scaleXProperty, scale.x],
[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)
: 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 { x, y };
return { x, y, z };
}
// Background properties.

View File

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

View File

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

View File

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

View File

@ -155,5 +155,14 @@ export module ios {
*/
export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/;
/**
*
* @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
} from "../trace";
const radToDeg = Math.PI / 180;
function isOrientationLandscape(orientation: number) {
return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ ||
orientation === UIDeviceOrientation.LandscapeRight /* 4 */;
@ -114,6 +116,22 @@ 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 {
public static ObjCProtocols = [UIDocumentInteractionControllerDelegate];

View File

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

View File

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

View File

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

View File

@ -576,6 +576,14 @@
public static getRotate(view: android.view.View): number;
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 setScaleX(view: android.view.View, value: number): void;