mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Animations Spike
This commit is contained in:
@@ -134,6 +134,19 @@
|
||||
<TypeScriptCompile Include="apps\action-bar-demo\pages\page-title-icon.ts">
|
||||
<DependentUpon>page-title-icon.xml</DependentUpon>
|
||||
</TypeScriptCompile>
|
||||
<TypeScriptCompile Include="apps\animations\model.ts" />
|
||||
<TypeScriptCompile Include="ui\animation\animation.d.ts" />
|
||||
<TypeScriptCompile Include="ui\animation\animation-common.ts">
|
||||
<DependentUpon>animation.d.ts</DependentUpon>
|
||||
</TypeScriptCompile>
|
||||
<TypeScriptCompile Include="ui\animation\animation.android.ts">
|
||||
<DependentUpon>animation.d.ts</DependentUpon>
|
||||
</TypeScriptCompile>
|
||||
<TypeScriptCompile Include="ui\animation\animation.ios.ts">
|
||||
<DependentUpon>animation.d.ts</DependentUpon>
|
||||
</TypeScriptCompile>
|
||||
<TypeScriptCompile Include="apps\animations\app.ts" />
|
||||
<TypeScriptCompile Include="apps\animations\main-page.ts" />
|
||||
<TypeScriptCompile Include="apps\gallery-app\views\list-picker.ts">
|
||||
<DependentUpon>list-picker.xml</DependentUpon>
|
||||
</TypeScriptCompile>
|
||||
@@ -791,6 +804,13 @@
|
||||
</Content>
|
||||
<Content Include="apps\action-bar-demo\pages\navigation-button.xml" />
|
||||
<Content Include="apps\action-bar-demo\test-icon.png" />
|
||||
<TypeScriptCompile Include="libjs.d.ts" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="apps\animations\app.css" />
|
||||
<Content Include="apps\animations\main-page.xml">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
<Content Include="apps\gallery-app\content\border.xml" />
|
||||
<Content Include="apps\gallery-app\app.css" />
|
||||
<Content Include="apps\gallery-app\views\list-picker.xml" />
|
||||
@@ -798,7 +818,9 @@
|
||||
<Content Include="apps\gallery-app\views\segmented-bar.xml" />
|
||||
<Content Include="apps\gallery-app\views\time-picker.xml" />
|
||||
<Content Include="apps\gallery-app\views\date-picker.xml" />
|
||||
<Content Include="apps\modal-views-demo\login-page.xml" />
|
||||
<Content Include="apps\modal-views-demo\login-page.xml">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
<Content Include="apps\pickers-demo\main-page.xml">
|
||||
<SubType>Designer</SubType>
|
||||
</Content>
|
||||
@@ -1742,6 +1764,8 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="apps\cuteness.unoptimized\package.json">
|
||||
<Content Include="apps\animations\package.json" />
|
||||
<Content Include="ui\animation\package.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="apps\action-bar-demo\package.json" />
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
Use the frame in the following way:
|
||||
|
||||
### To navigate to the starting page of the application
|
||||
# Ani
|
||||
```javascript
|
||||
// put this in the bootstrap.js
|
||||
var app = require("application");
|
||||
|
||||
3
apps/animations/app.css
Normal file
3
apps/animations/app.css
Normal file
@@ -0,0 +1,3 @@
|
||||
page {
|
||||
/* CSS styles */
|
||||
}
|
||||
8
apps/animations/app.ts
Normal file
8
apps/animations/app.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import application = require("application");
|
||||
import trace = require("trace");
|
||||
|
||||
trace.enable();
|
||||
trace.setCategories(trace.categories.concat(trace.categories.Animation));
|
||||
|
||||
application.mainModule = "main-page";
|
||||
application.start();
|
||||
131
apps/animations/main-page.ts
Normal file
131
apps/animations/main-page.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import observable = require("data/observable");
|
||||
import pages = require("ui/page");
|
||||
import buttonModule = require("ui/button");
|
||||
import abs = require("ui/layouts/absolute-layout");
|
||||
import animation = require("ui/animation");
|
||||
import colorModule = require("color");
|
||||
import model = require("./model");
|
||||
|
||||
var vm = new model.ViewModel();
|
||||
|
||||
var page: pages.Page;
|
||||
var panel1: abs.AbsoluteLayout;
|
||||
var button1: buttonModule.Button;
|
||||
var button2: buttonModule.Button;
|
||||
var button3: buttonModule.Button;
|
||||
var cancelToken: any;
|
||||
|
||||
export function pageLoaded(args: observable.EventData) {
|
||||
page = <pages.Page>args.object;
|
||||
page.bindingContext = vm;
|
||||
panel1 = page.getViewById<abs.AbsoluteLayout>("panel1");
|
||||
button1 = page.getViewById<buttonModule.Button>("button1");
|
||||
button2 = page.getViewById<buttonModule.Button>("button2");
|
||||
button3 = page.getViewById<buttonModule.Button>("button3");
|
||||
}
|
||||
|
||||
export function onSlideOut(args: observable.EventData) {
|
||||
var animations: Array<animation.Animation>;
|
||||
|
||||
animations = new Array<animation.Animation>();
|
||||
animations.push({ target: button1, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button1, property: animation.Properties.scale, value: { x: 0.5, y: 0.5 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button1, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
|
||||
animations.push({ target: button2, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button2, property: animation.Properties.scale, value: { x: 0.5, y: 0.5 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button2, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
|
||||
|
||||
animations.push({ target: button3, property: animation.Properties.translate, value: { x: -240, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button3, property: animation.Properties.scale, value: { x: 0, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button3, property: animation.Properties.opacity, value: 0, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
|
||||
|
||||
configureAnimationCurve(animations, true);
|
||||
|
||||
cancelToken = animation.start(animations, vm.playSequentially, (cancelled?: boolean) => {
|
||||
if (cancelled) {
|
||||
console.log("Buttons slide out animations cancelled");
|
||||
return;
|
||||
}
|
||||
console.log("Buttons slide out animations completed!");
|
||||
|
||||
animations = new Array<animation.Animation>();
|
||||
animations.push({ target: panel1, property: animation.Properties.scale, value: { x: 0, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: panel1, property: animation.Properties.rotate, value: 1080, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: panel1, property: animation.Properties.backgroundColor, value: new colorModule.Color("red"), duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
configureAnimationCurve(animations, true);
|
||||
|
||||
cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
|
||||
if (cancelled) {
|
||||
console.log("Panel animation cancelled");
|
||||
return;
|
||||
}
|
||||
console.log("Panel animation completed!");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function onSlideIn(args: observable.EventData) {
|
||||
var animations: Array<animation.Animation>;
|
||||
|
||||
animations = new Array<animation.Animation>();
|
||||
animations.push({ target: panel1, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: panel1, property: animation.Properties.rotate, value: 0, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: panel1, property: animation.Properties.backgroundColor, value: new colorModule.Color("yellow"), duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
configureAnimationCurve(animations, false);
|
||||
|
||||
cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
|
||||
if (cancelled) {
|
||||
console.log("Panel animation cancelled");
|
||||
return;
|
||||
}
|
||||
console.log("Panel animation completed!");
|
||||
|
||||
animations = new Array<animation.Animation>();
|
||||
animations.push({ target: button1, property: animation.Properties.translate, value: { x: 0, y: 0 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button1, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button1, property: animation.Properties.opacity, value: 1, duration: vm.duration, delay: 0, repeatCount: vm.repeatCount });
|
||||
|
||||
animations.push({ target: button2, property: animation.Properties.translate, value: { x: 0, y: 0 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button2, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button2, property: animation.Properties.opacity, value: 1, duration: vm.duration, delay: vm.duration, repeatCount: vm.repeatCount });
|
||||
|
||||
animations.push({ target: button3, property: animation.Properties.translate, value: { x: 0, y: 0 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button3, property: animation.Properties.scale, value: { x: 1, y: 1 }, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
|
||||
animations.push({ target: button3, property: animation.Properties.opacity, value: 1, duration: vm.duration, delay: vm.duration * 2, repeatCount: vm.repeatCount });
|
||||
|
||||
configureAnimationCurve(animations, false);
|
||||
|
||||
cancelToken = animation.start(animations, vm.playSequentially,(cancelled?: boolean) => {
|
||||
if (cancelled) {
|
||||
console.log("Buttons slide in animations cancelled");
|
||||
return;
|
||||
}
|
||||
console.log("Buttons slide in animations completed!");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configureAnimationCurve(animations: Array<animation.Animation>, slideOut: boolean) {
|
||||
var i = 0;
|
||||
var length = animations.length;
|
||||
if (page.android) {
|
||||
var interpolator = slideOut ? new android.view.animation.AccelerateInterpolator(1) : new android.view.animation.DecelerateInterpolator(1);
|
||||
for (; i < length; i++) {
|
||||
animations[i].androidInterpolator = interpolator;
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (; i < length; i++) {
|
||||
animations[i].iosUIViewAnimationCurve = slideOut ? UIViewAnimationCurve.UIViewAnimationCurveEaseIn : UIViewAnimationCurve.UIViewAnimationCurveEaseOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function onStop(args: observable.EventData) {
|
||||
cancelToken.cancel();
|
||||
}
|
||||
|
||||
export function onTap(args: observable.EventData) {
|
||||
console.log((<any>args.object).text);
|
||||
}
|
||||
29
apps/animations/main-page.xml
Normal file
29
apps/animations/main-page.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<Page xmlns="http://www.nativescript.org/tns.xsd" loaded="pageLoaded" id="mainPage">
|
||||
<StackLayout orientation="vertical">
|
||||
<StackLayout orientation="vertical" backgroundColor="LightGray" paddingTop="5" paddingBottom="5">
|
||||
|
||||
<Label text="{{ duration, 'Duration: ' + duration + ' ms' }}" width="180" marginTop="5" marginBottom="5"/>
|
||||
<Slider minValue="0" maxValue="10000" value="{{ duration }}" marginTop="5" marginBottom="5" marginLeft="10" marginRight="10"/>
|
||||
|
||||
<Label text="{{ repeatCount, 'Repeat Count: ' + repeatCount + ' times' }}" width="180" marginTop="5" marginBottom="5"/>
|
||||
<Slider minValue="0" maxValue="10" value="{{ repeatCount }}" marginTop="5" marginBottom="5" marginLeft="10" marginRight="10"/>
|
||||
|
||||
<StackLayout orientation="horizontal" marginTop="5" marginBottom="5" horizontalAlignment="center">
|
||||
<Label text="Play Sequentially?"/>
|
||||
<Switch marginLeft="10" checked="{{ playSequentially }}"/>
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout orientation="horizontal" marginTop="5" marginBottom="5" horizontalAlignment="center" paddingLeft="5" paddingRight="5">
|
||||
<Button text="Slide Out" tap="onSlideOut" width="100" marginLeft="5" marginRight="5" />
|
||||
<Button text="Slide In" tap="onSlideIn" width="100" marginLeft="5" marginRight="5" />
|
||||
<Button text="Cancel" tap="onStop" width="100" marginLeft="5" marginRight="5" />
|
||||
</StackLayout>
|
||||
|
||||
</StackLayout>
|
||||
<AbsoluteLayout id="panel1" backgroundColor="Yellow" width="300" height="190" clipToBounds="true" marginTop="10">
|
||||
<Button id="button1" text="Button 1" backgroundColor="White" width="180" height="50" left="60" top="10" tap="onTap"/>
|
||||
<Button id="button2" text="Button 2" backgroundColor="White" width="180" height="50" left="60" top="70" tap="onTap"/>
|
||||
<Button id="button3" text="Button 3" backgroundColor="White" width="180" height="50" left="60" top="130" tap="onTap"/>
|
||||
</AbsoluteLayout>
|
||||
</StackLayout>
|
||||
</Page>
|
||||
37
apps/animations/model.ts
Normal file
37
apps/animations/model.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import observable = require("data/observable");
|
||||
|
||||
export class ViewModel extends observable.Observable {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._duration = 300;
|
||||
this._repeatCount = 0;
|
||||
}
|
||||
|
||||
private _playSequentially: boolean;
|
||||
get playSequentially(): boolean {
|
||||
return this._playSequentially;
|
||||
}
|
||||
set playSequentially(value: boolean) {
|
||||
this._playSequentially = value;
|
||||
this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "playSequentially", value: value });
|
||||
}
|
||||
|
||||
private _duration: number;
|
||||
get duration(): number {
|
||||
return this._duration;
|
||||
}
|
||||
set duration(value: number) {
|
||||
this._duration = value;
|
||||
this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "duration", value: value });
|
||||
}
|
||||
|
||||
private _repeatCount: number;
|
||||
get repeatCount(): number {
|
||||
return this._repeatCount;
|
||||
}
|
||||
set repeatCount(value: number) {
|
||||
this._repeatCount = value;
|
||||
this.notify({ object: this, eventName: observable.Observable.propertyChangeEvent, propertyName: "repeatCount", value: value });
|
||||
}
|
||||
}
|
||||
2
apps/animations/package.json
Normal file
2
apps/animations/package.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{ "name" : "animations",
|
||||
"main" : "app.js" }
|
||||
1
declarations.d.ts
vendored
1
declarations.d.ts
vendored
@@ -93,6 +93,7 @@ interface Console {
|
||||
log(message: any, ...formatParams: any[]): void;
|
||||
trace(): void;
|
||||
dump(obj: any): void;
|
||||
createDump(obj: any): string;
|
||||
dir(obj: any): void;
|
||||
}
|
||||
|
||||
|
||||
1
trace/trace.d.ts
vendored
1
trace/trace.d.ts
vendored
@@ -75,6 +75,7 @@ declare module "trace" {
|
||||
export var Test: string;
|
||||
export var Binding: string;
|
||||
export var Error: string;
|
||||
export var Animation: string;
|
||||
|
||||
export var All: string;
|
||||
|
||||
|
||||
@@ -113,7 +113,8 @@ export module categories {
|
||||
export var Test = "Test";
|
||||
export var Binding = "Binding";
|
||||
export var Error = "Error";
|
||||
export var All = VisualTreeEvents + "," + Layout + "," + Style + "," + ViewHierarchy + "," + NativeLifecycle + "," + Debug + "," + Navigation + "," + Test + "," + Binding + "," + Error;
|
||||
export var Animation = "Animation";
|
||||
export var All = VisualTreeEvents + "," + Layout + "," + Style + "," + ViewHierarchy + "," + NativeLifecycle + "," + Debug + "," + Navigation + "," + Test + "," + Binding + "," + Error + "," + Animation;
|
||||
|
||||
export var separator = ",";
|
||||
|
||||
|
||||
7
ui/animation/animation-common.ts
Normal file
7
ui/animation/animation-common.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export module Properties {
|
||||
export var opacity = "opacity";
|
||||
export var backgroundColor = "backgroundColor";
|
||||
export var translate = "translate";
|
||||
export var rotate = "rotate";
|
||||
export var scale = "scale";
|
||||
}
|
||||
227
ui/animation/animation.android.ts
Normal file
227
ui/animation/animation.android.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import definition = require("ui/animation");
|
||||
import common = require("ui/animation/animation-common");
|
||||
import utils = require("utils/utils");
|
||||
import color = require("color");
|
||||
import trace = require("trace");
|
||||
import types = require("utils/types");
|
||||
|
||||
// merge the exports of the common file with the exports of this file
|
||||
declare var exports;
|
||||
require("utils/module-merge").merge(common, exports);
|
||||
|
||||
var density = utils.layout.getDisplayDensity();
|
||||
var intType = java.lang.Integer.class.getField("TYPE").get(null);
|
||||
var floatType = java.lang.Float.class.getField("TYPE").get(null);
|
||||
var argbEvaluator = new android.animation.ArgbEvaluator();
|
||||
|
||||
function getAnimationInfo(animation: definition.Animation): string {
|
||||
return JSON.stringify({
|
||||
target: animation.target.id,
|
||||
property: animation.property,
|
||||
value: animation.value,
|
||||
duration: animation.duration,
|
||||
delay: animation.delay,
|
||||
repeatCount: animation.repeatCount,
|
||||
androidInterpolator: animation.androidInterpolator
|
||||
});
|
||||
}
|
||||
|
||||
function createAndroidAnimators(animation: definition.Animation): any {
|
||||
trace.write("Creating ObjectAnimator(s) for animation: " + getAnimationInfo(animation) + "...", trace.categories.Animation);
|
||||
|
||||
if (types.isNullOrUndefined(animation.value)) {
|
||||
throw new Error("Animation value cannot be null or undefined!");
|
||||
}
|
||||
|
||||
var nativeArray;
|
||||
var nativeView = (<android.view.View>animation.target._nativeView);
|
||||
var animators = new Array<android.animation.ValueAnimator>();
|
||||
var propertyUpdateCallbacks = new Array<Function>();
|
||||
var propertyResetCallbacks = new Array<Function>();
|
||||
var animator: android.animation.ValueAnimator;
|
||||
var originalValue;
|
||||
switch (animation.property) {
|
||||
|
||||
case definition.Properties.opacity:
|
||||
originalValue = nativeView.getAlpha();
|
||||
if (animation.value !== animation.target.opacity) {
|
||||
nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
|
||||
nativeArray[0] = animation.value;
|
||||
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "alpha", nativeArray));
|
||||
propertyUpdateCallbacks.push(() => { animation.target.opacity = animation.value });
|
||||
propertyResetCallbacks.push(() => { nativeView.setAlpha(originalValue); });
|
||||
}
|
||||
break;
|
||||
|
||||
case definition.Properties.backgroundColor:
|
||||
originalValue = nativeView.getBackground();
|
||||
if (!color.Color.equals(animation.value, animation.target.backgroundColor)) {
|
||||
nativeArray = java.lang.reflect.Array.newInstance(intType, 1);
|
||||
nativeArray[0] = (<color.Color>animation.value).argb;
|
||||
animator = android.animation.ObjectAnimator.ofInt(nativeView, "backgroundColor", nativeArray);
|
||||
animator.setEvaluator(argbEvaluator);
|
||||
animators.push(animator);
|
||||
propertyUpdateCallbacks.push(() => { animation.target.backgroundColor = animation.value; });
|
||||
propertyResetCallbacks.push(() => { nativeView.setBackground(originalValue); });
|
||||
}
|
||||
break;
|
||||
|
||||
case definition.Properties.translate:
|
||||
originalValue = nativeView.getTranslationX();
|
||||
if (animation.value.x * density !== originalValue) {
|
||||
nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
|
||||
nativeArray[0] = animation.value.x * density;
|
||||
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "translationX", nativeArray));
|
||||
propertyResetCallbacks.push(() => { nativeView.setTranslationX(originalValue); });
|
||||
}
|
||||
|
||||
originalValue = nativeView.getTranslationY();
|
||||
if (animation.value.y * density !== originalValue) {
|
||||
nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
|
||||
nativeArray[0] = animation.value.y * density;
|
||||
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "translationY", nativeArray));
|
||||
propertyResetCallbacks.push(() => { nativeView.setTranslationY(originalValue); });
|
||||
}
|
||||
break;
|
||||
|
||||
case definition.Properties.rotate:
|
||||
originalValue = nativeView.getRotation();
|
||||
if (animation.value !== originalValue) {
|
||||
nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
|
||||
nativeArray[0] = animation.value;
|
||||
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "rotation", nativeArray));
|
||||
propertyResetCallbacks.push(() => { nativeView.setRotation(originalValue); });
|
||||
}
|
||||
break;
|
||||
|
||||
case definition.Properties.scale:
|
||||
originalValue = nativeView.getScaleX();
|
||||
if (animation.value.x !== originalValue) {
|
||||
nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
|
||||
nativeArray[0] = animation.value.x;
|
||||
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "scaleX", nativeArray));
|
||||
propertyResetCallbacks.push(() => { nativeView.setScaleX(originalValue); });
|
||||
}
|
||||
|
||||
originalValue = nativeView.getScaleY();
|
||||
if (animation.value.y !== originalValue) {
|
||||
nativeArray = java.lang.reflect.Array.newInstance(floatType, 1);
|
||||
nativeArray[0] = animation.value.y;
|
||||
animators.push(android.animation.ObjectAnimator.ofFloat(nativeView, "scaleY", nativeArray));
|
||||
propertyResetCallbacks.push(() => { nativeView.setScaleY(originalValue); });
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Cannot animate " + animation.property);
|
||||
break;
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
var length = animators.length;
|
||||
for (; i < length; i++) {
|
||||
if (animation.duration) {
|
||||
animators[i].setDuration(animation.duration);
|
||||
}
|
||||
if (animation.delay) {
|
||||
animators[i].setStartDelay(animation.delay);
|
||||
}
|
||||
if (animation.repeatCount) {
|
||||
animators[i].setRepeatCount(animation.repeatCount);
|
||||
}
|
||||
if (animation.androidInterpolator) {
|
||||
animators[i].setInterpolator(animation.androidInterpolator);
|
||||
}
|
||||
trace.write("ObjectAnimator created: " + animators[i], trace.categories.Animation);
|
||||
}
|
||||
|
||||
return {
|
||||
animators: animators,
|
||||
propertyUpdateCallbacks: propertyUpdateCallbacks,
|
||||
propertyResetCallbacks: propertyResetCallbacks
|
||||
};
|
||||
}
|
||||
|
||||
export var start = function start(animations: Array<definition.Animation>, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): definition.Cancelable {
|
||||
var i: number;
|
||||
var length: number;
|
||||
|
||||
var animators = new Array<android.animation.Animator>();
|
||||
var propertyUpdateCallbacks = new Array<Function>();
|
||||
var propertyResetCallbacks = new Array<Function>();
|
||||
|
||||
i = 0;
|
||||
length = animations.length;
|
||||
for (; i < length; i++) {
|
||||
var result = createAndroidAnimators(animations[i]);
|
||||
animators = animators.concat(result.animators);
|
||||
propertyUpdateCallbacks = propertyUpdateCallbacks.concat(result.propertyUpdateCallbacks);
|
||||
propertyResetCallbacks = propertyResetCallbacks.concat(result.propertyResetCallbacks);
|
||||
}
|
||||
|
||||
if (animators.length === 0) {
|
||||
if (finishedCallback) {
|
||||
finishedCallback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var nativeArray = java.lang.reflect.Array.newInstance(android.animation.Animator.class, animators.length);
|
||||
i = 0;
|
||||
length = animators.length;
|
||||
for (; i < length; i++) {
|
||||
nativeArray[i.toString()] = animators[i];
|
||||
}
|
||||
|
||||
var animatorSet = new android.animation.AnimatorSet();
|
||||
if (playSequentially) {
|
||||
animatorSet.playSequentially(nativeArray);
|
||||
}
|
||||
else {
|
||||
animatorSet.playTogether(nativeArray);
|
||||
}
|
||||
|
||||
var cancelled: boolean;
|
||||
animatorSet.addListener(new android.animation.Animator.AnimatorListener({
|
||||
onAnimationStart: function (animator: android.animation.Animator): void {
|
||||
trace.write("AnimatorListener.onAnimationStart.", trace.categories.Animation);
|
||||
},
|
||||
onAnimationRepeat: function (animator: android.animation.Animator): void {
|
||||
trace.write("AnimatorListener.onAnimationRepeat.", trace.categories.Animation);
|
||||
},
|
||||
onAnimationEnd: function (animator: android.animation.Animator): void {
|
||||
trace.write("AnimatorListener.onAnimationEnd.", trace.categories.Animation);
|
||||
i = 0;
|
||||
if (cancelled) {
|
||||
length = propertyResetCallbacks.length;
|
||||
for (; i < length; i++) {
|
||||
propertyResetCallbacks[i]();
|
||||
}
|
||||
}
|
||||
else {
|
||||
length = propertyUpdateCallbacks.length;
|
||||
for (; i < length; i++) {
|
||||
propertyUpdateCallbacks[i]();
|
||||
}
|
||||
}
|
||||
|
||||
if (finishedCallback) {
|
||||
finishedCallback(cancelled);
|
||||
}
|
||||
},
|
||||
onAnimationCancel: function (animator: android.animation.Animator): void {
|
||||
trace.write("AnimatorListener.onAnimationCancel.", trace.categories.Animation);
|
||||
cancelled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
trace.write("Starting " + animators.length + " animations " + (playSequentially ? "sequentially." : "together."), trace.categories.Animation);
|
||||
animatorSet.start();
|
||||
|
||||
return {
|
||||
cancel: () => {
|
||||
trace.write("Cancelling AnimatorSet.", trace.categories.Animation);
|
||||
animatorSet.cancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
101
ui/animation/animation.d.ts
vendored
Normal file
101
ui/animation/animation.d.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
declare module "ui/animation" {
|
||||
import viewModule = require("ui/core/view");
|
||||
|
||||
/**
|
||||
* Animatable properties enumeration.
|
||||
*/
|
||||
module Properties {
|
||||
/**
|
||||
* Animates the opacity of the target view. Value should be a number between 0.0 and 1.0
|
||||
*/
|
||||
export var opacity: string;
|
||||
|
||||
/**
|
||||
* Animates the backgroundColor of the target view. Value should be an instance of Color.
|
||||
*/
|
||||
export var backgroundColor: string;
|
||||
|
||||
/**
|
||||
* Animates the translation affine transform of the target view. Value should be a JSON object of the form {x: 100, y: 100}.
|
||||
*/
|
||||
export var translate: string;
|
||||
|
||||
/**
|
||||
* Animates the rotate affine transform of the target view. Value should be a number specifying the roation amount in degrees.
|
||||
*/
|
||||
export var rotate: string;
|
||||
|
||||
/**
|
||||
* Animates the scale affine transform of the target view. Value should be a JSON object of the form {x: 0.5, y: 0.5}.
|
||||
*/
|
||||
export var scale: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the data for an animation.
|
||||
*/
|
||||
export interface Animation {
|
||||
|
||||
/**
|
||||
* The view whose property is to be animated.
|
||||
*/
|
||||
target: viewModule.View;
|
||||
|
||||
/**
|
||||
* The property to be animated. Animatable properties are contained in the animation.Properties enumeration.
|
||||
*/
|
||||
property: string;
|
||||
|
||||
/**
|
||||
* The value of the property to animate to.
|
||||
*/
|
||||
value: any;
|
||||
|
||||
/**
|
||||
* The length of the animation in milliseconds. The default duration is 300 milliseconds.
|
||||
*/
|
||||
duration?: number;
|
||||
|
||||
/**
|
||||
* The amount of time, in milliseconds, to delay starting the animation after start() is called.
|
||||
*/
|
||||
delay?: number;
|
||||
|
||||
/**
|
||||
* Specifies how many times the animation should be repeated.
|
||||
* The default repeat count is 0 meaning the animation is never repeated, i.e. it is played only once.
|
||||
* iOS animations support fractional repeat counts, i.e. 1.5
|
||||
*/
|
||||
repeatCount?: number;
|
||||
|
||||
/**
|
||||
* An optional animation curve of type UIViewAnimationCurve.
|
||||
*/
|
||||
iosUIViewAnimationCurve?: any;
|
||||
|
||||
/**
|
||||
* An optional android.animation.TimeInterpolator instance used in calculating the elapsed fraction of this animation.
|
||||
* The interpolator determines whether the animation runs with linear or non-linear motion, such as acceleration and deceleration.
|
||||
* The default value is AccelerateDecelerateInterpolator.
|
||||
*/
|
||||
androidInterpolator?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an operation that can be cancelled.
|
||||
*/
|
||||
export interface Cancelable {
|
||||
|
||||
/**
|
||||
* Cancels the opertaion.
|
||||
*/
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the specified animations and returns a Cancelable object that can be used to stop the animations.
|
||||
* @param animations - The animations to start.
|
||||
* @param finishedCallback - Callback function which will be executed when all animations finish. Useful for chaining multiple animation sets on after another.
|
||||
*/
|
||||
export function start(animations: Array<Animation>, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): Cancelable;
|
||||
}
|
||||
273
ui/animation/animation.ios.ts
Normal file
273
ui/animation/animation.ios.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import definition = require("ui/animation");
|
||||
import common = require("ui/animation/animation-common");
|
||||
import trace = require("trace");
|
||||
|
||||
// merge the exports of the common file with the exports of this file
|
||||
declare var exports;
|
||||
require("utils/module-merge").merge(common, exports);
|
||||
|
||||
var _transform = "_transform";
|
||||
var _skip = "_skip";
|
||||
|
||||
function getAnimationInfo(animation: definition.Animation): string {
|
||||
return JSON.stringify({
|
||||
target: animation.target.id ? animation.target.id : animation.target._domId,
|
||||
property: animation.property,
|
||||
value: animation.value,
|
||||
duration: animation.duration,
|
||||
delay: animation.delay,
|
||||
repeatCount: animation.repeatCount,
|
||||
iosUIViewAnimationCurve: animation.iosUIViewAnimationCurve
|
||||
});
|
||||
}
|
||||
|
||||
class AnimationDelegateImpl extends NSObject {
|
||||
static new(): AnimationDelegateImpl {
|
||||
return <AnimationDelegateImpl>super.new();
|
||||
}
|
||||
|
||||
private _finishedCallback: Function;
|
||||
|
||||
public initWithFinishedCallback(finishedCallback: Function): AnimationDelegateImpl {
|
||||
this._finishedCallback = finishedCallback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public animationWillStart(animationID: string, context: any): void {
|
||||
trace.write("AnimationDelegateImpl.animationWillStart, animationID: " + animationID, trace.categories.Animation);
|
||||
}
|
||||
|
||||
public animationDidStop(animationID: string, finished: boolean, context: any): void {
|
||||
trace.write("AnimationDelegateImpl.animationDidStop, animationID: " + animationID + ", finished: " + finished, trace.categories.Animation);
|
||||
if (this._finishedCallback) {
|
||||
var cancelled = !finished;
|
||||
// This could either be the master finishedCallback or an nextAnimationCallback depending on the playSequentially argument values.
|
||||
this._finishedCallback(cancelled);
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjCExposedMethods = {
|
||||
"animationWillStart": { returns: interop.types.void, params: [NSString, NSObject] },
|
||||
"animationDidStop": { returns: interop.types.void, params: [NSString, NSNumber, NSObject] }
|
||||
};
|
||||
}
|
||||
|
||||
function createiOSAnimation(animations: Array<definition.Animation>, index: number, playSequentially: boolean, finishedCallback: (cancelled?: boolean) => void): Function {
|
||||
return (cancelled?: boolean) => {
|
||||
if (cancelled && finishedCallback) {
|
||||
trace.write("Animation " + (index - 1).toString() + " was cancelled. Will skip the rest of animations and call finishedCallback(true).", trace.categories.Animation);
|
||||
finishedCallback(cancelled);
|
||||
return;
|
||||
}
|
||||
|
||||
var animation = animations[index];
|
||||
var nativeView = (<UIView>animation.target._nativeView);
|
||||
|
||||
var nextAnimationCallback: Function;
|
||||
var animationDelegate: AnimationDelegateImpl;
|
||||
if (index === animations.length - 1) {
|
||||
// This is the last animation, so tell it to call the master finishedCallback when done.
|
||||
animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(finishedCallback);
|
||||
}
|
||||
else {
|
||||
nextAnimationCallback = createiOSAnimation(animations, index + 1, playSequentially, finishedCallback);
|
||||
// If animations are to be played sequentially, tell it to start the next animation when done.
|
||||
// If played together, all individual animations will call the master finishedCallback, which increments a counter every time it is called.
|
||||
animationDelegate = AnimationDelegateImpl.new().initWithFinishedCallback(playSequentially ? nextAnimationCallback : finishedCallback);
|
||||
}
|
||||
|
||||
trace.write("UIView.beginAnimationsContext("+index+"): " + getAnimationInfo(animation), trace.categories.Animation);
|
||||
UIView.beginAnimationsContext(index.toString(), null);
|
||||
|
||||
if (animationDelegate) {
|
||||
UIView.setAnimationDelegate(animationDelegate);
|
||||
UIView.setAnimationWillStartSelector("animationWillStart");
|
||||
UIView.setAnimationDidStopSelector("animationDidStop");
|
||||
}
|
||||
|
||||
if (animation.duration !== undefined) {
|
||||
UIView.setAnimationDuration(animation.duration / 1000.0);
|
||||
}
|
||||
else {
|
||||
UIView.setAnimationDuration(0.3); //Default duration.
|
||||
}
|
||||
if (animation.delay !== undefined) {
|
||||
UIView.setAnimationDelay(animation.delay / 1000.0);
|
||||
}
|
||||
if (animation.repeatCount !== undefined) {
|
||||
UIView.setAnimationRepeatCount(animation.repeatCount);
|
||||
}
|
||||
if (animation.iosUIViewAnimationCurve !== undefined) {
|
||||
UIView.setAnimationCurve(animation.iosUIViewAnimationCurve);
|
||||
}
|
||||
|
||||
var originalValue;
|
||||
switch (animation.property) {
|
||||
case definition.Properties.opacity:
|
||||
originalValue = animation.target.opacity;
|
||||
(<any>animation)._propertyResetCallback = () => { animation.target.opacity = originalValue };
|
||||
animation.target.opacity = animation.value;
|
||||
break;
|
||||
case definition.Properties.backgroundColor:
|
||||
originalValue = animation.target.backgroundColor;
|
||||
(<any>animation)._propertyResetCallback = () => { animation.target.backgroundColor = originalValue };
|
||||
animation.target.backgroundColor = animation.value;
|
||||
break;
|
||||
case _transform:
|
||||
originalValue = nativeView.transform;
|
||||
(<any>animation)._propertyResetCallback = () => { nativeView.transform = originalValue };
|
||||
nativeView.transform = animation.value;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Cannot animate " + animation.property);
|
||||
break;
|
||||
}
|
||||
|
||||
trace.write("UIView.commitAnimations " + index, trace.categories.Animation);
|
||||
UIView.commitAnimations();
|
||||
|
||||
if (!playSequentially && nextAnimationCallback) {
|
||||
nextAnimationCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isAffineTransform(property: string): boolean {
|
||||
return property === _transform
|
||||
|| property === definition.Properties.translate
|
||||
|| property === definition.Properties.rotate
|
||||
|| property === definition.Properties.scale;
|
||||
}
|
||||
|
||||
function canBeMerged(animation1: definition.Animation, animation2: definition.Animation) {
|
||||
var result =
|
||||
isAffineTransform(animation1.property) &&
|
||||
isAffineTransform(animation2.property) &&
|
||||
animation1.target === animation2.target &&
|
||||
animation1.duration === animation2.duration &&
|
||||
animation1.delay === animation2.delay &&
|
||||
animation1.repeatCount === animation2.repeatCount &&
|
||||
animation1.iosUIViewAnimationCurve === animation2.iosUIViewAnimationCurve;
|
||||
return result;
|
||||
}
|
||||
|
||||
function affineTransform(matrix: CGAffineTransform, property: string, value: any): CGAffineTransform {
|
||||
switch (property) {
|
||||
case definition.Properties.translate:
|
||||
return CGAffineTransformTranslate(matrix, value.x, value.y);
|
||||
case definition.Properties.rotate:
|
||||
return CGAffineTransformRotate(matrix, value * Math.PI / 180);
|
||||
case definition.Properties.scale:
|
||||
return CGAffineTransformScale(matrix, value.x, value.y);
|
||||
default:
|
||||
throw new Error("Cannot create transform for" + property);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeAffineTransformAnimations(animations: Array<definition.Animation>): Array<definition.Animation> {
|
||||
var result = new Array<definition.Animation>();
|
||||
|
||||
var i = 0;
|
||||
var j;
|
||||
var length = animations.length;
|
||||
for (; i < length; i++) {
|
||||
if (animations[i].property !== _skip) {
|
||||
|
||||
if (!isAffineTransform(animations[i].property)) {
|
||||
// This is not an affine transform animation, so there is nothing to merge.
|
||||
result.push(animations[i]);
|
||||
}
|
||||
else {
|
||||
|
||||
// This animation has not been merged anywhere. Create a new transform animation.
|
||||
var newTransformAnimation: definition.Animation = {
|
||||
target: animations[i].target,
|
||||
property: _transform,
|
||||
value: affineTransform(CGAffineTransformIdentity, animations[i].property, animations[i].value),
|
||||
duration: animations[i].duration,
|
||||
delay: animations[i].delay,
|
||||
repeatCount: animations[i].repeatCount,
|
||||
iosUIViewAnimationCurve: animations[i].iosUIViewAnimationCurve
|
||||
};
|
||||
//trace.write("Created new transform animation: " + getAnimationInfo(newTransformAnimation), trace.categories.Animation);
|
||||
|
||||
j = i + 1;
|
||||
if (j < length) {
|
||||
// Merge all compatible affine transform animations to the right into this new animation.
|
||||
for (; j < length; j++) {
|
||||
if (canBeMerged(animations[i], animations[j])) {
|
||||
//trace.write("Merging animations: " + getAnimationInfo(newTransformAnimation) + " + " + getAnimationInfo(animations[j]) + " = ", trace.categories.Animation);
|
||||
//trace.write("New native transform is: " + NSStringFromCGAffineTransform(newTransformAnimation.value), trace.categories.Animation);
|
||||
newTransformAnimation.value = affineTransform(newTransformAnimation.value, animations[j].property, animations[j].value);
|
||||
|
||||
// Mark that it has been merged so we can skip it on our outer loop.
|
||||
animations[j].property = _skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(newTransformAnimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export var start = function start(animations: Array<definition.Animation>, playSequentially: boolean, finishedCallback?: (cancelled?: boolean) => void): definition.Cancelable {
|
||||
//trace.write("Non-merged: " + animations.length, trace.categories.Animation);
|
||||
var mergedAnimations = mergeAffineTransformAnimations(animations);
|
||||
//trace.write("Merged: " + mergedAnimations.length, trace.categories.Animation);
|
||||
|
||||
var animationFinishedCallback: () => void;
|
||||
if (finishedCallback) {
|
||||
if (playSequentially) {
|
||||
// This callback will be called by the last animation when done or by another animation if the user cancels them halfway through.
|
||||
animationFinishedCallback = finishedCallback;
|
||||
}
|
||||
else {
|
||||
var finishedAnimations = 0;
|
||||
var cancelledAnimations = 0;
|
||||
|
||||
// This callback will be called by each individual animation when it finishes or is cancelled.
|
||||
animationFinishedCallback = (cancelled?: boolean) => {
|
||||
if (cancelled) {
|
||||
cancelledAnimations++;
|
||||
}
|
||||
else {
|
||||
finishedAnimations++;
|
||||
}
|
||||
|
||||
if (cancelledAnimations === mergedAnimations.length) {
|
||||
trace.write(cancelledAnimations + " animations cancelled.", trace.categories.Animation);
|
||||
finishedCallback(cancelled);
|
||||
return;
|
||||
}
|
||||
|
||||
if (finishedAnimations === mergedAnimations.length) {
|
||||
trace.write(finishedAnimations + " animations finished.", trace.categories.Animation);
|
||||
finishedCallback(cancelled);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var iOSAnimation = createiOSAnimation(mergedAnimations, 0, playSequentially, animationFinishedCallback);
|
||||
trace.write("Starting " + mergedAnimations.length + " animations " + (playSequentially ? "sequentially." : "together."), trace.categories.Animation);
|
||||
iOSAnimation();
|
||||
|
||||
return {
|
||||
cancel: () => {
|
||||
var i = 0;
|
||||
var length = mergedAnimations.length;
|
||||
for (; i < length; i++) {
|
||||
(<UIView>mergedAnimations[i].target._nativeView).layer.removeAllAnimations();
|
||||
if ((<any>mergedAnimations[i])._propertyResetCallback) {
|
||||
(<any>mergedAnimations[i])._propertyResetCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
2
ui/animation/package.json
Normal file
2
ui/animation/package.json
Normal file
@@ -0,0 +1,2 @@
|
||||
{ "name" : "animation",
|
||||
"main" : "animation.js" }
|
||||
@@ -1,11 +1,31 @@
|
||||
import definition = require("ui/layouts/layout");
|
||||
import view = require("ui/core/view");
|
||||
import dependencyObservable = require("ui/core/dependency-observable");
|
||||
import proxy = require("ui/core/proxy");
|
||||
|
||||
function onClipToBoundsPropertyChanged(data: dependencyObservable.PropertyChangeData) {
|
||||
var nativeView = (<Layout>data.object)._nativeView;
|
||||
if (!nativeView) {
|
||||
return;
|
||||
}
|
||||
var value = <boolean>data.newValue;
|
||||
|
||||
if (nativeView instanceof UIView) {
|
||||
(<UIView>nativeView).clipsToBounds = value;
|
||||
}
|
||||
else if (nativeView instanceof android.view.ViewGroup) {
|
||||
(<android.view.ViewGroup>nativeView).setClipChildren(value);
|
||||
}
|
||||
}
|
||||
|
||||
var clipToBoundsProperty = new dependencyObservable.Property(
|
||||
"clipToBounds",
|
||||
"Layout",
|
||||
new proxy.PropertyMetadata(undefined, dependencyObservable.PropertyMetadataSettings.None, onClipToBoundsPropertyChanged)
|
||||
);
|
||||
|
||||
export class Layout extends view.CustomLayoutView implements definition.Layout, view.AddChildFromBuilder {
|
||||
|
||||
public static clipToBoundsProperty = new dependencyObservable.Property("clipToBounds", "Layout",
|
||||
new dependencyObservable.PropertyMetadata(true, dependencyObservable.PropertyMetadataSettings.None, Layout.onClipToBoundsPropertyChanged));
|
||||
public static clipToBoundsProperty = clipToBoundsProperty;
|
||||
|
||||
private _subViews: Array<view.View> = new Array<view.View>();
|
||||
|
||||
@@ -96,15 +116,10 @@ export class Layout extends view.CustomLayoutView implements definition.Layout,
|
||||
this.style.paddingLeft = value;
|
||||
}
|
||||
|
||||
private static onClipToBoundsPropertyChanged(data: dependencyObservable.PropertyChangeData) {
|
||||
var layout = <Layout>data.object;
|
||||
var nativeView: Object = layout._nativeView;
|
||||
var value = <boolean>data.newValue;
|
||||
if (nativeView instanceof android.view.ViewGroup) {
|
||||
(<android.view.ViewGroup>nativeView).setClipChildren(value);
|
||||
}
|
||||
else if (nativeView instanceof UIView) {
|
||||
(<UIView>nativeView).clipsToBounds = value;
|
||||
}
|
||||
get clipToBounds(): boolean {
|
||||
return this._getValue(Layout.clipToBoundsProperty);
|
||||
}
|
||||
set clipToBounds(value: boolean) {
|
||||
this._setValue(Layout.clipToBoundsProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user