前言
最近在用preact做一个IM聊天软件的时候,特别想念之前用vue的transition做动画的体验了, 所以就照着vue的API简单的撸了一个动画组件;
待实现API
我这里选择了实现vue的transition中最常用的两种使用方式, 使用方式如下:
- 简单的css动画, 只需要传入一个name,以及准备一组css过度的样式,即可实现动画,css:
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
模版使用:
class Demo extend Component{
render({}, { visiable }) {
return (
<transition
visiable={ visiable }
name="fade"
>
<div class="demo">我是动画的正文</div>
</transition>
)
}
}
- 通过回调函数beforeEnter、enter、afterEnter、beforeLeave、leave、afterLeave来控制DOM实现一些比较复杂的动画
模版使用:
class Demo extend Component{
beforeEnter(el) {}
enter(el, done) {}
afterEnter(el) {}
beforeLeave(el) {}
leave(el, done) {}
afterLeave(el) {}
render({}, { visiable }) {
return (
<transition
visiable={ visiable }
beforeEnter={ this.beforeEnter }
enter={ this.enter }
afterEnter={ this.afterEnter }
beforeLeave={ this.beforeLeave }
leave={ this.leave }
afterLeavee={ this.afterLeave }
>
<div class="demo">我是动画的正文</div>
</transition>
)
}
}
代码实现
- 首先创建一个基本的preact组件, 限制好组件的入参类型为TransitionProps; 定义一个变量控制组件中的DOM是否显示; 命名一个动画入口函数, 以及css、js动画具体实现的函数,代码如下:
import { h, Component } from "preact";
export interface TransitionProps {
visiable: boolean,
name?: string;
children?: any,
beforeEnter?: Function;
enter?: Function;
afterEnter?: Function;
beforeLeave?: Function;
leave?: Function;
afterLeave?: Function;
}
export interface TransitionState {
isShow: boolean;
}
export class Transition extends Component<TransitionProps> {
private noop = function() {};
public state: TransitionState = {
isShow: false,
};
constructor(props: TransitionProps) {
super(props);
}
private cssAnimation(node) {
}
private jsAnimation(node) {
}
private animationStart() {
const node = this.base;
const { name } = this.props;
!!name ? this.cssAnimation(node) : this.jsAnimation(node);
}
componentWillReceiveProps(nextProps: TransitionProps) {
nextProps.visiable && this.setState({ isShow: true });
}
componentDidUpdate(previousProps) {
if (previousProps.visiable !== this.props.visiable) {
this.animationStart();
}
}
render(props: TransitionProps, { isShow }) {
return (isShow && props.children[0]) ? props.children[0] : null;
}
}
入参变化的时候判断visiable是否为true, 如果是true则需要把节点显示出来再进行接下来的动画; 这里还有一点需要注意的是,在节点更新的时候必须要判断当前的visiable和上一次的是否不一致, 不一致才需要启动动画. 2. 首先我们实现css动画, 也就是来把函数cssAnimation实现出来;
private cssAnimation(node) {
const { visiable, name } = this.props;
if (visiable) {
const activeClass = ` ${name}-enter ${name}-enter-active `;
node.className += activeClass;
setTimeout(() => { this.removeClassName(node, ` ${name}-enter`); });
this.bindCSS3AnimationEvent(node, activeClass);
} else {
const activeClass = ` ${name}-leave-active`;
node.className += activeClass;
setTimeout(() => { node.className += ` ${name}-leave`; });
this.bindCSS3AnimationEvent(node, activeClass);
}
}
函数根据组件的入参visiable判断组件是enter还是leave状态;并根据不通的状态添加对应的css; 3. 监听css动画是否结束, 结束后需要删除动画的class:
private bindCSS3AnimationEvent(elem, activeClass) {
const { visiable } = this.props;
const eventName = this.transitionend();
elem.addEventListener(eventName, () => {
this.removeClassName(elem, activeClass);
!visiable && this.setState({ isShow: false });
});
}
private transitionend() {
let t;
const el = document.createElement('surface');
const transitions = {
'transition': 'transitionend',
'OTransition': 'oTransitionEnd',
'MozTransition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd'
}
for(t in transitions){
if( el.style[t] !== undefined ){
return transitions[t];
}
}
}
private removeClassName(elem, className) {
if (!elem) return;
const elClassName = elem.className;
if (elClassName.length === 0) return;
if(elClassName === className) {
elem.className = '';
return;
}
const classes = elClassName.split(' ');
const removeClesses = className.split(' ');
const newClasses = [];
classes.forEach(curr => {
if (curr !== '' && removeClesses.indexOf(curr) < 0) {
newClasses.push(curr);
}
});
elem.className = newClasses.join(' ');
}
这里transitionend是对transitionend事件的浏览器兼容, removeClass函数就是给节点删除class的方法 4. 实现js动画, 填充实现函数jsAnimation
private jsAnimation(node) {
let { visiable, beforeEnter, enter, beforeLeave, leave } = this.props;
beforeEnter = beforeEnter || this.noop;
enter = enter || this.noop;
beforeLeave = beforeLeave || this.noop;
leave = leave || this.noop;
if (visiable) {
beforeEnter(node);
setTimeout(() => enter(node, this.done.bind(this)));
} else {
beforeLeave(node);
setTimeout(() => leave(node, this.done.bind(this)));
}
}
首先判断beforeEnter、enter、beforeLeave、leave回调是否传入, 如果没有传入则默认是一个空函数;在beforeEnter、beforeLeave执行后再调用enter、leave函数; 和vue的一样, enter、leave函数的第二个参数是一个done函数, 只有在enter、leave调用了done函数组件才会执行afterEnter、afterLeave函数
- 实现我们的done函数, 代码如下:
private done() {
const node = this.base
let { visiable, afterEnter, afterLeave } = this.props;
afterEnter = afterEnter || this.noop;
afterLeave = afterLeave || this.noop;
if (visiable) {
afterEnter(node);
} else {
afterLeave(node);
setTimeout(() => this.setState({ isShow: false }));
}
}
在这里, afterLeave执行后需要把节点删除,调:this.setState({ isShow: false })
最终代码
import { h, Component } from "preact";
export interface TransitionProps {
visiable: boolean,
name?: string;
children?: any,
beforeEnter?: Function;
enter?: Function;
afterEnter?: Function;
beforeLeave?: Function;
leave?: Function;
afterLeave?: Function;
}
export interface TransitionState {
isShow: boolean;
}
export class Transition extends Component<TransitionProps> {
private noop = function() {};
public state: TransitionState = {
isShow: false,
};
constructor(props: TransitionProps) {
super(props);
}
private cssAnimation(node) {
const { visiable, name } = this.props;
if (visiable) {
const activeClass = ` ${name}-enter ${name}-enter-active `;
node.className += activeClass;
setTimeout(() => { this.removeClassName(node, ` ${name}-enter`); });
this.bindCSS3AnimationEvent(node, activeClass);
} else {
const activeClass = ` ${name}-leave-active`;
node.className += activeClass;
setTimeout(() => { node.className += ` ${name}-leave`; });
this.bindCSS3AnimationEvent(node, activeClass);
}
}
private bindCSS3AnimationEvent(elem, activeClass) {
const { visiable } = this.props;
const eventName = this.transitionend();
elem.addEventListener(eventName, () => {
this.removeClassName(elem, activeClass);
!visiable && this.setState({ isShow: false });
});
}
private transitionend() {
let t;
const el = document.createElement('surface');
const transitions = {
'transition': 'transitionend',
'OTransition': 'oTransitionEnd',
'MozTransition': 'transitionend',
'WebkitTransition': 'webkitTransitionEnd'
}
for(t in transitions){
if( el.style[t] !== undefined ){
return transitions[t];
}
}
}
private removeClassName(elem, className) {
if (!elem) return;
const elClassName = elem.className;
if (elClassName.length === 0) return;
if(elClassName === className) {
elem.className = '';
return;
}
const classes = elClassName.split(' ');
const removeClesses = className.split(' ');
const newClasses = [];
classes.forEach(curr => {
if (curr !== '' && removeClesses.indexOf(curr) < 0) {
newClasses.push(curr);
}
});
elem.className = newClasses.join(' ');
}
private jsAnimation(node) {
let { visiable, beforeEnter, enter, beforeLeave, leave } = this.props;
beforeEnter = beforeEnter || this.noop;
enter = enter || this.noop;
beforeLeave = beforeLeave || this.noop;
leave = leave || this.noop;
if (visiable) {
beforeEnter(node);
setTimeout(() => enter(node, this.done.bind(this)));
} else {
beforeLeave(node);
setTimeout(() => leave(node, this.done.bind(this)));
}
}
private animationStart() {
const node = this.base;
const { name } = this.props;
!!name ? this.cssAnimation(node) : this.jsAnimation(node);
}
private done() {
const node = this.base
let { visiable, afterEnter, afterLeave } = this.props;
afterEnter = afterEnter || this.noop;
afterLeave = afterLeave || this.noop;
if (visiable) {
afterEnter(node);
} else {
afterLeave(node);
setTimeout(() => this.setState({ isShow: false }));
}
}
componentWillReceiveProps(nextProps: TransitionProps) {
nextProps.visiable && this.setState({ isShow: true });
}
componentDidUpdate(previousProps) {
if (previousProps.visiable !== this.props.visiable) {
this.animationStart();
}
}
render(props: TransitionProps, { isShow }) {
return (isShow && props.children[0]) ? props.children[0] : null;
}
}