@huantv/vue-native-components
Version:
Hippy Vue Native components for TV
277 lines (268 loc) • 8.29 kB
JavaScript
function registerAnimation(Vue) {
// Constants for animations
const MODULE_NAME = 'AnimationModule';
const DEFAULT_OPTION = {
valueType: undefined,
delay: 0,
startValue: 0,
toValue: 0,
duration: 0,
direction: 'center',
timingFunction: 'linear',
repeatCount: 0,
inputRange: [],
outputRange: [],
};
/**
* parse value of special value type
* @param {string} valueType
* @param {*} originalValue
*/
function parseValue(valueType, originalValue) {
if (valueType === 'color' && ['number', 'string'].indexOf(typeof originalValue) >= 0) {
return Vue.Native.parseColor(originalValue);
}
return originalValue;
}
/**
* Create the standalone animation
*/
function createAnimation(option) {
const {
mode = 'timing',
valueType,
startValue,
toValue,
...others
} = option;
const fullOption = {
...DEFAULT_OPTION,
...others,
};
if (valueType !== undefined) {
fullOption.valueType = option.valueType;
}
fullOption.startValue = parseValue(fullOption.valueType, startValue);
fullOption.toValue = parseValue(fullOption.valueType, toValue);
const animationId = Vue.Native.callNativeWithCallbackId(MODULE_NAME, 'createAnimation', true, mode, fullOption);
return {
animationId,
};
}
/**
* Create the animationSet
*/
function createAnimationSet(children, repeatCount = 0) {
return Vue.Native.callNativeWithCallbackId(MODULE_NAME, 'createAnimationSet', true, {
children,
repeatCount,
});
}
/**
* Generate the styles from animation and animationSet Ids.
*/
function getStyle(actions, childAnimationIdList = []) {
const style = {};
Object.keys(actions).forEach((key) => {
if (Array.isArray(actions[key])) {
// Process AnimationSet from Array.
const actionSet = actions[key];
const { repeatCount } = actionSet[actionSet.length - 1];
const animationSetActions = actionSet.map((a) => {
const action = createAnimation(Object.assign({}, a, { repeatCount: 0 }));
childAnimationIdList.push(action.animationId);
action.follow = true;
return action;
});
const animationSetId = createAnimationSet(animationSetActions, repeatCount);
style[key] = {
animationId: animationSetId,
};
} else {
// Process standalone Animation.
const action = actions[key];
const animation = createAnimation(action);
const { animationId } = animation;
style[key] = {
animationId,
};
}
});
return style;
}
/**
* Get animationIds from style for start/pause/destroy actions.
*/
function getAnimationIds(style) {
const { transform, ...otherStyles } = style;
let animationIds = Object.keys(otherStyles).map(key => style[key].animationId);
if (Array.isArray(transform) && transform.length > 0) {
const transformIds = [];
transform.forEach(entity => Object.keys(entity)
.forEach((key) => {
if (entity[key]) {
const { animationId } = entity[key];
if (typeof animationId === 'number' && animationId % 1 === 0) {
transformIds.push(animationId);
}
}
}));
animationIds = [...animationIds, ...transformIds];
}
return animationIds;
}
/**
* Register the animation component.
*/
Vue.component('Animation', {
inheritAttrs: false,
props: {
tag: {
type: String,
default: 'div',
},
playing: {
type: Boolean,
default: false,
},
actions: {
type: Object,
required: true,
},
props: Object,
},
data() {
return {
style: {},
animationIds: [],
animationEventMap: {},
childAnimationIdList: [],
};
},
watch: {
playing(to, from) {
if (!from && to) {
this.start();
} else if (from && !to) {
this.pause();
}
},
actions() {
// FIXME: Should diff the props and use updateAnimation method to update the animation.
// Hard restart the animation is no correct.
this.destroy();
this.create();
},
},
created() {
let animationEventName = 'onAnimation';
// If running in Android, change it.
if (Vue.Native.Platform === 'android') {
animationEventName = 'onHippyAnimation';
}
this.childAnimationIdList = [];
this.animationEventMap = {
start: `${animationEventName}Start`,
end: `${animationEventName}End`,
repeat: `${animationEventName}Repeat`,
cancel: `${animationEventName}Cancel`,
};
if (Vue.getApp) {
this.app = Vue.getApp();
}
},
beforeMount() {
this.create();
},
mounted() {
const { playing } = this.$props;
if (playing) {
this.start();
}
},
beforeDestroy() {
this.destroy();
},
methods: {
create() {
const { actions: { transform, ...actions } } = this.$props;
const style = getStyle(actions, this.childAnimationIdList);
if (transform) {
const transformAnimations = getStyle(transform, this.childAnimationIdList);
style.transform = Object.keys(transformAnimations).map(key => ({
[key]: transformAnimations[key],
}));
}
// Turn to be true at first startAnimation, and be false again when destroy.
this.$alreadyStarted = false;
// Generated style
this.style = style;
},
removeAnimationEvent() {
Object.keys(this.animationEventMap).forEach((key) => {
const eventName = this.animationEventMap[key];
if (eventName && this.app && this[`${eventName}`]) {
this.app.$off(eventName, this[`${eventName}`]);
}
});
},
addAnimationEvent() {
Object.keys(this.animationEventMap).forEach((key) => {
const eventName = this.animationEventMap[key];
if (eventName && this.app) {
this[`${eventName}`] = function eventHandler(animationId) {
if (this.animationIds.indexOf(animationId) >= 0) {
if (key !== 'repeat') {
this.app.$off(eventName, this[`${eventName}`]);
}
this.$emit(key);
}
}.bind(this);
this.app.$on(eventName, this[`${eventName}`]);
}
});
},
reset() {
this.$alreadyStarted = false;
},
start() {
if (!this.$alreadyStarted) {
const animationIds = getAnimationIds(this.style);
this.animationIds = animationIds;
this.$alreadyStarted = true;
this.removeAnimationEvent();
this.addAnimationEvent();
animationIds.forEach(animationId => Vue.Native.callNative(MODULE_NAME, 'startAnimation', animationId));
} else {
this.resume();
}
},
resume() {
const animationIds = getAnimationIds(this.style);
animationIds.forEach(animationId => Vue.Native.callNative(MODULE_NAME, 'resumeAnimation', animationId));
},
pause() {
if (!this.$alreadyStarted) {
return;
}
const animationIds = getAnimationIds(this.style);
animationIds.forEach(animationId => Vue.Native.callNative(MODULE_NAME, 'pauseAnimation', animationId));
},
destroy() {
this.removeAnimationEvent();
this.$alreadyStarted = false;
const animationIds = getAnimationIds(this.style);
this.childAnimationIdList.forEach(animationId => Number.isInteger(animationId)
&& Vue.Native.callNative(MODULE_NAME, 'destroyAnimation', animationId));
animationIds.forEach(animationId => Vue.Native.callNative(MODULE_NAME, 'destroyAnimation', animationId));
this.childAnimationIdList = [];
},
},
template: `
<component :is="tag" :useAnimation="true" :style="style" v-bind="props">
<slot />
</component>
`,
});
}
export default registerAnimation;