Mescoda's Blog

CSS3 Animation 操作总结

2012-10-30

在腾讯实习的时候除了做产品妹子们提来的需求,还搞了个电商前端实验室的概念版出来,实体在内网运行,这里是 demo

这是一个用于展示电商用户体验设计部前端自研工具的实验室,不考虑低级浏览器(为了更好的体验做成了 webkit only ,虽然我是 Firefox 控),是个 CSS3 Animation 密集型项目。

在 Animation 的处理上我采用的是添加 className 的方法,方便复用且不污染 html,每个动画效果都可以分为三个基本的步骤,用三种 className 来控制:

  1. 动画开始前的状态 className
  2. 动画过程中的过程 className
  3. 动画结束后的状态 className

区别市面上大多的只用 Animation 做渐进增强(类似呼吸 button 效果)的项目,这里需要状态 class,一来帮助处理动画前后元素的样式变化,二来也为可能存在的连续多个 Animation 提供方便。

一个栗子:list-item 的方形旋转为圆形的步骤为

  1. 动画开始前的状态 className:square-list-item
  2. 动画过程中的过程 className:squareRotate
  3. 动画结束后的状态 className:circle-list-item

顺便说下我的 className 命名规则:
连字符表示且仅表示层级关系;同级 className 赋予前缀表示模块;Animation 的过程 className 因为只通过 JS 赋予且只是中间量不长期存在 html 中,所以驼峰化。

具体对 Animation 的控制为触发事件后移除最初的状态 class,添加过程 class,动画执行完毕后移除过程 class,添加结束后的状态 class。

最初遇到的问题是,Animation 是异步的,所以如果有连续多个动画过程(例如 list-item 旋转结束后 list-item-detail-circle 才能呈现)就需要时间上的控制。

最初想到的是用 CSS3 原生的 animation-delay 属性,但一旦改动最初的动画时间就需要到 css 里修改之后所有的 delay,而且不利于独立每个效果帮助复用。

之后用 setTimeout ,相当于把 delay 时间提出到 JS 中控制,简单的搞下还行,写多了就越来越奇怪,除了不优雅外,依旧存在修改时间的问题,但是当时急着完成就先写了个函数简单处理了下然后上线了。

离职之后才发现 animationend 和 transitionend 事件,可以在 animation 和 transition 结束后绑定其他事件,解决了修改时间的问题。

浏览器事件名称差异的解决:


var VENDORS = ["Moz",'webkit','ms','O'];
var TRANSITION_END_NAMES = {
	"Moz" : "transitionend"
	,"webkit" : "webkitTransitionEnd"
	,"ms" : "MSTransitionEnd"
	,"O" : "oTransitionEnd"
}
var ANIMATION_END_NAMES = {
	"Moz" : "animationend"
	,"webkit" : "webkitAnimationEnd"
	,"ms" : "MSAnimationEnd"
	,"O" : "oAnimationEnd"
}
var css3Prefix,TRANSITION_END_NAME,ANIMATION_END_NAME;
var mTestElement = document.createElement("div");

for (var i = 0,l = VENDORS.length; i < l; i++) {
	css3Prefix = VENDORS[i];
	if ((css3Prefix + "Transition") in mTestElement.style) {
		break;
	}
	css3Prefix = false;
}

if(css3Prefix) {
	TRANSITION_END_NAME = TRANSITION_END_NAMES[css3Prefix];
	ANIMATION_END_NAME = ANIMATION_END_NAMES[css3Prefix];
}

animationend 之后,还有回调嵌套的问题。

多个 Animation 的连续很容易把代码写成这种糟糕的样子,


elemA.addEventListener(ANIMATION_END_NAME,function() {
	elemA.doSth();
	elemB.addEventListener(ANIMATION_END_NAME,function() {
		elemB.doSth();
		elemC.addEventListener(ANIMATION_END_NAME,function() {
			elemC.doSth();
		})
	})
})

我写了个 MesAnimateEffect 类 ,用来设定动画效果。对于连续的动画,可以用 runAnimateList 来保证一个接一个的触发,细节上有 node 的影子。


function EventEmitter() {
	this.events = {};
}
EventEmitter.prototype.on = function(eventName,callback) {
	if(this.events[eventName] instanceof Array) {
		// pass
	} else {
		this.events[eventName] = [];
	}
	this.events[eventName].push(callback);
}
EventEmitter.prototype.emit = function(eventName) {
	if(this.events[eventName] instanceof Array) {
		var callbacks = this.events[eventName];
		for(var i = 0,l = callbacks.length;i < l;i++) {
			callbacks[i]();
		}
	}
}

function MesAnimateEffect(elem,classList,stopP) {
	EventEmitter.call(this);
	this.elem = elem;
	if(classList instanceof Array) {
		this.prev = classList[0];
		this.process = classList[1];
		this.next = classList[2];
	} else {
		this.prev = classList.prev.join(" ");
		this.process = classList.process.join(" ");
		this.next = classList.next.join(" ");
	}
	
	this.stopP = stopP || false;	
}
MesAnimateEffect.prototype = new EventEmitter();
MesAnimateEffect.prototype.start = function() {
	var self = this;
	(this.elem).removeClass(this.prev).addClass(this.process);
	(this.elem).unbind(ANIMATION_END_NAME).bind(ANIMATION_END_NAME,function() {
		(self.elem).removeClass(self.process).addClass(self.next);
		self.emit("end");
	})
	return this;
}

function runAnimateList() {
	var animateList = arguments;
	var length = animateList.length;
	for(var i = 0;i < length;i++) {
		if(animateList[i+1]) {
			animateList[i].on("end",(function(num) {
				return function() {
					if(typeof animateList[num+1] == "function") {
						animateList[num+1]();
					} else {
						animateList[num+1].start();
					}
				}
			})(i))
		}
	}
	animateList[0].start();
}

现在处理这种连续 Animation 只需要先设定具体的效果,然后用按顺序添加为 runAnimateList 的参数,有了把回调嵌套拉平的感觉。

一个栗子,list-item 的完整旋转显示内容过程


var forwardEffect = new MesAnimateEffect($this,["square-list-item",forwardClassName,"circle-list-item"]);

var innerShowEffect = new MesAnimateEffect($this.find(".list-item-detail-circle"),["","detailCircleShow",""],true);

runAnimateList(forwardEffect,innerShowEffect,function() {
	if($this.hasClass("circle-list-item")) {
		$this.find(".list-item-detail").removeClass("hide-list-item-detail").addClass("show-list-item-detail");
	}
});

写完之后发给基友,被指责了临时写 list 还是麻烦。。 orz
等过段时间再监视一遍

< >