@quick-tv/react
Version:
QuickTV react framework
360 lines (321 loc) • 11.6 kB
text/typescript
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2017-2019 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { HippyEventEmitter, HippyEventRevoker } from '../event';
import { Bridge, Device } from '../native';
import { warn } from '../utils';
import { repeatCountDict } from '../utils/animation';
import { colorParse } from '../color';
import { Platform } from '../types';
type AnimationValue = number | { animationId: number} | string;
type AnimationCallback = () => void;
type AnimationDirection = 'left' | 'right' | 'top' | 'bottom' | 'center';
interface AnimationOptions {
/**
* Initial value at `Animation` start
*/
startValue: AnimationValue;
/**
* End value when `Animation` end.
*/
toValue: AnimationValue;
/**
* Animation execution time
*/
duration: number;
/**
* Timeline mode of animation
*/
mode?: 'timing'; // TODO: fill more options
/**
* Delay starting time
*/
delay?: number;
/**
* Value type, leave it blank in most case, except use rotate/color related
* animation, set it to be 'deg' or 'color'.
*/
valueType?: 'deg' | 'rad' | 'color'; // TODO: fill more options
/**
* Animation start position
*/
direction?: AnimationDirection;
/**
* Animation interpolation type
*/
timingFunction?: 'linear' | 'ease' | 'bezier' | 'in' | 'ease-in' | 'out' | 'ease-out' | 'inOut' | 'ease-in-out' | (string & {});
/**
* Animation repeat times, use 'loop' to be always repeating.
*/
repeatCount?: number | 'loop';
inputRange?: any[];
outputRange?: any[];
}
interface Animation extends AnimationOptions {
animationId: number;
onAnimationStartCallback?: AnimationCallback;
onAnimationEndCallback?: AnimationCallback;
onAnimationCancelCallback?: AnimationCallback;
onAnimationRepeatCallback?: AnimationCallback;
animationStartListener?: HippyEventRevoker;
animationEndListener?: HippyEventRevoker;
animationCancelListener?: HippyEventRevoker;
animationRepeatListener?: HippyEventRevoker;
// Fallback event handlers
onRNfqbAnimationStart?: Function;
onRNfqbAnimationEnd?: Function;
onRNfqbAnimationCancel?: Function;
onRNfqbAnimationRepeat?: Function;
onHippyAnimationStart?: Function;
onHippyAnimationEnd?: Function;
onHippyAnimationCancel?: Function;
onHippyAnimationRepeat?: Function;
}
const AnimationEventEmitter = new HippyEventEmitter();
/**
* parse value of special value type
* @param valueType
* @param originalValue
*/
function parseValue(valueType: string | undefined, originalValue: number | string) {
if (valueType === 'color' && ['number', 'string'].indexOf(typeof originalValue) >= 0) {
return colorParse(originalValue);
}
return originalValue;
}
/**
* Better performance of Animation solution.
*
* It pushes the animation scheme to native at once.
*/
class Animation implements Animation {
public constructor(config: AnimationOptions) {
let startValue: AnimationValue = 0;
if (config.startValue?.constructor && config.startValue.constructor.name === 'Animation') {
startValue = { animationId: (config.startValue as Animation).animationId };
} else {
const { startValue: tempStartValue } = config;
startValue = parseValue(config.valueType, tempStartValue as number|string);
}
const toValue = parseValue(config.valueType, config.toValue as number|string);
this.mode = config.mode || 'timing';
this.delay = config.delay || 0;
this.startValue = startValue || 0;
this.toValue = toValue || 0;
this.valueType = config.valueType || undefined;
this.duration = config.duration || 0;
this.direction = config.direction || 'center';
this.timingFunction = config.timingFunction || 'linear';
this.repeatCount = repeatCountDict(config.repeatCount || 0);
this.inputRange = config.inputRange || [];
this.outputRange = config.outputRange || [];
this.animationId = Bridge.callNativeWithCallbackId('AnimationModule', 'createAnimation', true, this.mode, Object.assign({
delay: this.delay,
startValue: this.startValue,
toValue: this.toValue,
duration: this.duration,
direction: this.direction,
timingFunction: this.timingFunction,
repeatCount: this.repeatCount,
inputRange: this.inputRange,
outputRange: this.outputRange,
}, (this.valueType ? { valueType: this.valueType } : {})));
this.destroy = this.destroy.bind(this);
// TODO: Deprecated compatible, will remove soon.
this.onRNfqbAnimationStart = this.onAnimationStart.bind(this);
this.onRNfqbAnimationEnd = this.onAnimationEnd.bind(this);
this.onRNfqbAnimationCancel = this.onAnimationCancel.bind(this);
this.onRNfqbAnimationRepeat = this.onAnimationRepeat.bind(this);
this.onHippyAnimationStart = this.onAnimationStart.bind(this);
this.onHippyAnimationEnd = this.onAnimationEnd.bind(this);
this.onHippyAnimationCancel = this.onAnimationCancel.bind(this);
this.onHippyAnimationRepeat = this.onAnimationRepeat.bind(this);
}
/**
* Remove all of animation event listener
*/
public removeEventListener() {
if (this.animationStartListener) {
this.animationStartListener.remove();
}
if (this.animationEndListener) {
this.animationEndListener.remove();
}
if (this.animationCancelListener) {
this.animationCancelListener.remove();
}
if (this.animationRepeatListener) {
this.animationRepeatListener.remove();
}
}
/**
* Start animation execution
*/
public start() {
this.removeEventListener();
// Set as iOS default
let animationEventName = 'onAnimation';
// If running in Android, change it.
if (__PLATFORM__ === Platform.android || Device.platform.OS === Platform.android) {
animationEventName = 'onHippyAnimation';
}
if (typeof this.onAnimationStartCallback === 'function') {
this.animationStartListener = AnimationEventEmitter.addListener(`${animationEventName}Start`, (animationId) => {
if (animationId === this.animationId) {
(this.animationStartListener as HippyEventRevoker).remove();
if (typeof this.onAnimationStartCallback === 'function') {
this.onAnimationStartCallback();
}
}
});
}
if (typeof this.onAnimationEndCallback === 'function') {
this.animationEndListener = AnimationEventEmitter.addListener(`${animationEventName}End`, (animationId) => {
if (animationId === this.animationId) {
(this.animationEndListener as HippyEventRevoker).remove();
if (typeof this.onAnimationEndCallback === 'function') {
this.onAnimationEndCallback();
}
}
});
}
if (typeof this.onAnimationCancelCallback === 'function') {
this.animationCancelListener = AnimationEventEmitter.addListener(`${animationEventName}Cancel`, (animationId) => {
if (animationId === this.animationId) {
(this.animationCancelListener as HippyEventRevoker).remove();
if (typeof this.onAnimationCancelCallback === 'function') {
this.onAnimationCancelCallback();
}
}
});
}
if (typeof this.onAnimationRepeatCallback === 'function') {
this.animationRepeatListener = AnimationEventEmitter.addListener(`${animationEventName}Repeat`, (animationId) => {
if (animationId === this.animationId) {
if (typeof this.onAnimationRepeatCallback === 'function') {
this.onAnimationRepeatCallback();
}
}
});
}
Bridge.callNative('AnimationModule', 'startAnimation', this.animationId);
}
/**
* Use destroy() to destroy animation.
*/
public destory() {
warn('Animation.destory() method will be deprecated soon, please use Animation.destroy() as soon as possible');
this.destroy();
}
/**
* Destroy the animation
*/
public destroy() {
this.removeEventListener();
Bridge.callNative('AnimationModule', 'destroyAnimation', this.animationId);
}
/**
* Pause the running animation
*/
public pause() {
Bridge.callNative('AnimationModule', 'pauseAnimation', this.animationId);
}
/**
* Resume execution of paused animation
*/
public resume() {
Bridge.callNative('AnimationModule', 'resumeAnimation', this.animationId);
}
/**
* Update to new animation scheme
*
* @param {Object} newConfig - new animation schema
*/
public updateAnimation(newConfig: Partial<AnimationOptions>) {
if (typeof newConfig !== 'object') {
throw new TypeError('Invalid arguments');
}
if (typeof newConfig.mode === 'string' && newConfig.mode !== this.mode) {
throw new TypeError('Update animation mode not supported');
}
(Object.keys(newConfig) as (keyof AnimationOptions)[]).forEach((prop) => {
const value = newConfig[prop];
if (prop === 'startValue') {
let startValue: AnimationValue = 0;
if (newConfig.startValue instanceof Animation) {
startValue = { animationId: newConfig.startValue.animationId };
} else {
const { startValue: tempStartValue } = newConfig;
startValue = parseValue(this.valueType, tempStartValue as number|string);
}
this.startValue = startValue || 0;
} else if (prop === 'repeatCount') {
this.repeatCount = repeatCountDict(newConfig.repeatCount || 0);
} else {
Object.defineProperty(this, prop, {
value,
});
}
});
Bridge.callNative('AnimationModule', 'updateAnimation', this.animationId, Object.assign({
delay: this.delay,
startValue: this.startValue,
toValue: parseValue(this.valueType, this.toValue as number|string),
duration: this.duration,
direction: this.direction,
timingFunction: this.timingFunction,
repeatCount: this.repeatCount,
inputRange: this.inputRange,
outputRange: this.outputRange,
}, (this.valueType ? { valueType: this.valueType } : {})));
}
/**
* Call when animation started.
* @param {Function} cb - callback when animation started.
*/
public onAnimationStart(cb: AnimationCallback) {
this.onAnimationStartCallback = cb;
}
/**
* Call when animation is ended.
* @param {Function} cb - callback when animation started.
*/
public onAnimationEnd(cb: AnimationCallback) {
this.onAnimationEndCallback = cb;
}
/**
* Call when animation is canceled.
* @param {Function} cb - callback when animation started.
*/
public onAnimationCancel(cb: AnimationCallback) {
this.onAnimationCancelCallback = cb;
}
/**
* Call when animation is repeated.
* @param {Function} cb - callback when animation started.
*/
public onAnimationRepeat(cb: AnimationCallback) {
this.onAnimationRepeatCallback = cb;
}
}
export default Animation;
export {
Animation,
AnimationCallback,
};