叶落为重生每片落下的叶子都是为了下一次的涅槃...^_^

webkit-box & translate 的组合--流畅的滑动体验

【注:本文所有的代码和实例仅在chrome和safari等webkit内核的浏览器测试通过】

如果说从web Pages 能够转到web app时代,那么css3和html5其他相关技术一定是巨大的功臣。

唯一的遗憾就是pc端浏览器的泛滥导致了我们不得不走所谓的优雅降级,而且这种降级是降到新技术几乎木有多大的用武之地。
于是,客户端还算统一的移动端开始成了一个大的试验田。能够让众人大肆的在上面舒展拳脚。诸如众多新起的ui库或者框架(jquery-mobile, sencha, phoneGap ...),可见在移动终端上确实还有不小的田地。纵使如此,效率仍旧成为一个最大的瓶颈。

之前有一种尝试是用CSS3的transfrom或者animation给一个duration和ease的属性来做动画,这样不管改变任何style样式,都会根据这个ease有缓动的效果。
例如:

/* webkit */
-webkit-transition-duration: 500ms;

在webkit内核浏览器下,只要有这个属性,再去改变这个元素任何的样式,它都会以一个默认的缓动效果完成。

/**
 * CSS3 animation by transform
 * @example
 * Let(el)
 * 		.to(500, 200)
 * 		.rotate(180)
 * 		.scale(.5)
 * 		.set({
 *			background-color: 'red',
 *			border-color: 'green'
 * 		})
 * 		.duration(2000)
 * 		.skew(50, -10)
 * 		.then()
 * 			.set('opacity', .5)
 * 			.duration('1s')
 * 			.scale(1)
 * 			.pop()
 * 		.end();
 */

(function (win, undefined) {
 	
 	var initializing = false,
		superTest = /horizon/.test(function () {horizon;}) ? /\b_super\b/ : /.*/;
	// 临时Class
	this.Class = function () {};
	// 继承方法extend
	Class.extend = function (prop) {
		var _super = this.prototype;
		//创建一个实例,但不执行init
		initializing = true;
		var prototype = new this();
		initializing = false;

		for (var name in prop) {
			// 用闭包保证多级继承不会污染
			prototype[name] = (typeof prop[name] === 'function' && typeof _super[name] === 'function' && superTest.test(prop[name])) ? (function (name, fn) {
					return function () {
						var temp = this._super;	
						// 当前子类通过_super继承父类
						this._super = _super[name];
						//继承方法执行完毕后还原
						var ret = fn.apply(this, arguments);
						this._super = temp;

						return ret;
					}
				})(name, prop[name]) : prop[name];
		}
		
		//真实的constructor
		function Class () {
			if (!initializing && this.init) {
				this.init.apply(this, arguments);
			}
		}
		Class.prototype = prototype;
		Class.constructor = Class;
		Class.extend = arguments.callee;

		return Class;
	}
	

	// 样式为数字+px 的属性
	var map = {
		'top': 'px',
		'left': 'px',
		'right': 'px',
		'bottom': 'px',
		'width': 'px',
		'height': 'px',
		'font-size': 'px',
		'margin': 'px',
		'margin-top': 'px',
		'margin-left': 'px',
		'margin-right': 'px',
		'margin-bottom': 'px',
		'padding': 'px',
		'padding-left': 'px',
		'padding-right': 'px',
		'padding-top': 'px',
		'padding-bottom': 'px',
		'border-width': 'px'
	};

	/**
	 * Let package
	 */
	var Let = function (selector) {
		var el = Let.G(selector);
		return new Anim(el);
	};
	Let.defaults = {
		duration: 500
	};
	Let.ease = {
		'in' : 'ease-in',
		'out': 'ease-out',
		'in-out': 'ease-in-out',
		'snap' : 'cubic-bezier(0,1,.5,1)'	
	};
	Let.G = function (selector) {
		if (typeof selector != 'string' && selector.nodeType == 1) {
			return selector;
		}
		return document.getElementById(selector) || document.querySelectorAll(selector)[0];		
	};

	/**
	 * EventEmitter
	 * {Class}
	 */
	var EventEmitter = Class.extend({
		init: function () {
			this.callbacks = {};
		},		
		on: function (event, fn) {
			(this.callbacks[event] = this.callbacks[event] || []).push(fn);
			return this;
		},
		/**
		 * param {event} 指定event
		 * params 指定event的callback的参数
		 */
		fire: function (event) {
			var args = Array.prototype.slice.call(arguments, 1),
				callbacks = this.callbacks[event],
				len;
			if (callbacks) {
				for (var i = 0, len = callbacks.length; i < len; i ++) {
					callbacks[i].apply(this, args);
				}
			}
			return this;
		}
				
	});

	/**
	 * Anim
	 * {Class}
	 * @inherit from EventEmitter
	 */
	var Anim = EventEmitter.extend({
		init: function (el) {
			this._super();

			if (!(this instanceof Anim)) {
				return new Anim(el);
			}

			this.el = el;
			this._props = {};
			this._rotate = 0;
			this._transitionProps = [];
			this._transforms = [];
			this.duration(Let.defaults.duration);
			
		},
		transform : function (transform) {
			this._transforms.push(transform);
			return this;
		},
		// skew methods
		skew: function (x, y) {
			y = y || 0;
			return this.transform('skew('+ x +'deg, '+ y +'deg)');
		},
		skewX: function (x) {
			return this.transform('skewX('+ x +'deg)');	   
		},
		skewY: function (y) {
			return this.transform('skewY('+ y +'deg)');	   
		},
		// translate methods
		translate: function (x, y) {
			y = y || 0;
			return this.transform('translate('+ x +'px, '+ y +'px)');
		},
		to: function (x, y) {
			return this.translate(x, y);	
		},
		translateX: function (x) {
			return this.transform('translateX('+ x +'px)');			
		},
		x: function (x) {
			return this.translateX(x);   
		},
		translateY: function (y) {
			return this.transform('translateY('+ y +'px)');			
		},
		y: function (y) {
			return this.translateY(y);   
		},
		// scale methods
		scale: function (x, y) {
			y = (y == null) ? x : y;
			return this.transform('scale('+ x +', '+ y +')');
		},
		scaleX: function (x) {
			return this.transform('scaleX('+ x +')');
		},
		scaleY: function (y) {
			return this.transform('scaleY('+ y +')');
		},
		// rotate methods
		rotate: function (n) {
			return this.transform('rotate('+ n +'deg)');
		},

		// set transition ease
		ease: function (fn) {
			fn = Let.ease[fn] || fn || 'ease';
			return this.setVendorProperty('transition-timing-function', fn);
		},

		//set duration time
		duration: function (n) {
			n = this._duration = (typeof n == 'string') ? parseFloat(n)*1000 : n;
			return this.setVendorProperty('transition-duration', n + 'ms');
		},

		// set delay time
		delay: function (n) {
			n = (typeof n == 'string') ? parseFloat(n) * 1000 : n;
			return this.setVendorProperty('transition-delay', n + 'ms');
		},

		// set property to val
		setProperty: function (prop, val) {
			this._props[prop] = val;
			return this;
		},
		setVendorProperty: function (prop, val) {
			this.setProperty('-webkit-' + prop, val);
			this.setProperty('-moz-' + prop, val);
			this.setProperty('-ms-' + prop, val);
			this.setProperty('-o-' + prop, val);
			return this;
		},
		set: function (prop, val) {
			var _store = {};
			if (typeof prop == 'string' && val != undefined) {
				_store[prop] = val;
			} else if (typeof prop == 'object' && prop.constructor.prototype.hasOwnProperty('hasOwnProperty')) {
				_store = prop;
			}
			
			for (var key in _store) {
				this.transition(key);
				if (typeof _store[key] == 'number' && map[key]) {
					_store[key] += map[key];
				}
				this._props[key] = _store[key];
			}
			return this;

		},
		
		// add value to a property
		add: function (prop, val) {
			var self = this;
			return this.on('start', function () {
				var curr = parseInt(self.current(prop), 10);
				self.set(prop, curr + val + 'px');
			})
		},
		// sub value to a property
		sub: function (prop, val) {
			var self = this;
			return this.on('start', function () {
				var curr = parseInt(self.current(prop), 10);
				self.set(prop, curr - val + 'px');
			})
		},
		current: function (prop) {
			return !!window.getComputedStyle ? document.defaultView.getComputedStyle(this.el, null).getPropertyValue(prop) : this.el.currentStyle(prop);
		},

		transition: function (prop) {
			for (var i = 0; i < this._transitionProps.length; i ++) {
				if (this._transitionProps[i] == prop) {
					return this;
				}
			}

			this._transitionProps.push(prop);
			return this;
		},
		applyPropertys: function () {
			var props = this._props,
				el = this.el;
			for (var prop in props) {
				if (props.hasOwnProperty(prop)) {
					el.style.setProperty ? el.style.setProperty(prop, props[prop], '') : el.style[prop] = props[prop];
				}
			}
			return this;
		},
		
		// then
		then: function (fn) {
			if (fn instanceof Anim) {
				this.on('end', function () {
					fn.end();		
				})
			} else if (typeof fn == 'function') {
				this.on('end', fn);
			} else {
				var clone = new Anim(this.el);
				clone._transforms = this._transforms.slice(0);
				this.then(clone);
				clone.parent = this;
				return clone;
			}

			return this;
		},
		pop: function () {
			return this.parent;	 
		},
		end: function (fn) {
			var self = this;
			this.fire('start');

			if (this._transforms.length > 0) {
				this.setVendorProperty('transform', this._transforms.join(' '));
			}

			this.setVendorProperty('transition-properties', this._transitionProps.join(', '));
			this.applyPropertys();

			if (fn) { this.then(fn) }

			setTimeout(function () {
				self.fire('end');		
			}, this._duration);

			return this;
		}
		
	});

	this.Let = win.Let = Let;
	

 })(window)

比如下面代码:

<div id="test"></div>
<script>
Let('#test')
    .to(200, 200)
    .rotate(1000)
    .scale(.5)
    .set({
        'background-color': 'red',
        'width': 300
    })
    .duration(2000)
    .then()
        .set('opacity', .5)
        .set('height', 200)
        .duration('1s')
        .scale(1.5)
        .to(300, 300)
        .pop()
    .end()
    
</script>

这样子有好处是可以针对所有的style样式。所以可以用同样的方式来对 left, top,margin-left,margin-top 之类的css2 的style属性来完成dom的相应变化。

但是,其实,用transform或者animation来操作css2的style属性。效率依然不高。在当前的移动终端,ipad还ok(毕竟是乔帮主的产品),iphone和android pad上执行效率在大部分情况下很难达到优秀app所要求的体验。

所以要做滑动之类的改变dom位置的体验。更好的实现应该是用纯粹的translate来改变位置,为了更好的与之配合,布局就尤为重要。

下面看看webkit提供的 display:-webkit-box; 亦即

Flexible Box Module

我称其为【流体盒模型】
W3C草案(http://www.w3.org/TR/css3-flexbox/)的描述 如下:

 a CSS box model optimized for interface design. It provides an additional layout system alongside the ones already in CSS. [CSS21] In this new box model, the children of a box are laid out either horizontally or vertically, and unused space can be assigned to a particular child or distributed among the children by assignment of “flex” to the children that should expand. Nesting of these boxes (horizontal inside vertical, or vertical inside horizontal) can be used to build layouts in two dimensions. This model is based on the box model in the XUL user-interface language used for the user interface of many Mozilla-based applications (such as Firefox).

偶英文蹩脚,就不翻译了,用另外一番话来看它的意思:

1.之前要实现横列的web布局,通常就是float或者display:inline-block; 但是都不能做到真正的流体布局。至少width要自己去算百分比。
2.flexible box 就可以实现真正意义上的流体布局。只要给出相应属性,浏览器会帮我们做额外的计算。

提供的关于盒模型的几个属性:

box-orient           子元素排列 vertical or horizontal
box-flex             兄弟元素之间比例,仅作一个系数
box-align            box 排列
box-direction        box 方向
box-flex-group       以组为单位的流体系数
box-lines            
box-ordinal-group    以组为单位的子元素排列方向
box-pack

以下是关于flexible box的几个实例
三列自适应布局,且有固定margin

<!DOCTYPE html>
<html>
<style>
.wrap {
    display: -webkit-box;
    -webkit-box-orient: horizontal;
}
.child {
    min-height: 200px;
    border: 2px solid #666;
    -webkit-box-flex: 1;
    margin: 10px;
    font-size: 100px;
    font-weight: bold;
    font-family: Georgia;
    -webkit-box-align: center;
}
</style>

<div class="wrap">
<div class="child">1</div>
<div class="child">2</div>
<div class="child">3</div>
</div>
</html>

 当一列定宽,其余两列分配不同比例亦可(三列布局,一列定宽,其余两列按1:2的比例自适应)

<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<style>
.wrap {
    display: -webkit-box;
    -webkit-box-orient: horizontal;
}
.child {
    min-height: 200px;
    border: 2px solid #666;
    margin: 10px;
    font-size: 40px;
    font-weight: bold;
    font-family: Georgia;
    -webkit-box-align: center;
}
.w200 {width: 200px}
.flex1 {-webkit-box-flex: 1}
.flex2 {-webkit-box-flex: 2}
</style>

<div class="wrap">
<div class="child w200">200px</div>
<div class="child flex1">比例1</div>
<div class="child flex2">比例2</div>
</div>
</html>

  

 下面是一个常见的web page 的基本布局

<style>
header, footer, section {
    border: 10px solid #333;
    font-family: Georgia;
    font-size: 40px;
    text-align: center;
    margin: 10px;
}
#doc {
    width: 80%;
    min-width: 600px;
    height: 100%;
    display: -webkit-box; 
	-webkit-box-orient: vertical;
    margin: 0 auto;
}
header,
footer {
    min-height: 100px;
    -webkit-box-flex: 1;
}
#content {
    min-height: 400px;
    display: -webkit-box;
    -webkit-box-orient: horizontal;
}

.w200 {width: 200px}
.flex1 {-webkit-box-flex: 1}
.flex2 {-webkit-box-flex: 2}
.flex3 {-webkit-box-flex: 3}
</style>

<div id="doc">
    <header>Header</header>
    <div id="content">
        <section class="w200">定宽200</section>
        <section class="flex3">比例3</section>
        <section class="flex1">比例1</section>
    </div>
    <footer>Footer</footer>
</div>

  

 有了 flexible box 后,横列布局的时候不用计算外围容器和容器里面的元素的宽度。然后再进行横向的滑动的效果就会省去不少麻烦。

/**
 * css3 translate flip
 * -webkit-box
 * @author: horizon
 */

(function (win, undefined) {
 
 	var initializing = false,
		superTest = /horizon/.test(function () {horizon;}) ? /\b_super\b/ : /.*/;
	this.Class = function () {};

	Class.extend = function (prop) {
		var _super = this.prototype;
		initializing = true;
		var prototype = new this();
		initializing = false;

		for (var name in prop) {
			prototype[name] = (typeof prop[name] === 'function' && typeof _super[name] === 'function' && superTest.test(prop[name])) ? (function (name, fn) {
					return function () {
						var temp = this._super;	
						this._super = _super[name];
						var ret = fn.apply(this, arguments);
						this._super = temp;

						return ret;
					}
				})(name, prop[name]) : prop[name];
		}
		
		function Class () {
			if (!initializing && this.init) {
				this.init.apply(this, arguments);
			}
		}
		Class.prototype = prototype;
		Class.constructor = Class;
		Class.extend = arguments.callee;

		return Class;
	};

	var $support = {
		transform3d: ('WebKitCSSMatrix' in win),
		touch: ('ontouchstart' in win)
	};

	var $E = {
		start: $support.touch ? 'touchstart' : 'mousedown',
		move: $support.touch ? 'touchmove' : 'mousemove',
		end: $support.touch ? 'touchend' : 'mouseup'
	};

	function getTranslate (x) {
		return $support.transform3d ? 'translate3d('+x+'px, 0, 0)' : 'translate('+x+'px, 0)';
	}
	function getPage (event, page) {
		return $support.touch ? event.changedTouches[0][page] : event[page];
	}


	var Css3Flip = Class.extend({
		init: function (selector, conf) {
			var self = this;
			
			if (selector.nodeType && selector.nodeType == 1) {
				self.element = selector;
			} else if (typeof selector == 'string') {
				self.element = document.getElementById(selector) || document.querySelector(selector);
			}
            
            self.element.style.display = '-webkit-box';
			self.element.style.webkitTransitionProperty = '-webkit-transform';
			self.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
			self.element.style.webkitTransitionDuration = '0';
			self.element.style.webkitTransform = getTranslate(0);

			self.conf = conf || {};
			self.touchEnabled = true;
			self.currentPoint = 0;
			self.currentX = 0;

			self.refresh();
			
			// 支持handleEvent
			self.element.addEventListener($E.start, self, false);
			self.element.addEventListener($E.move, self, false);
			document.addEventListener($E.end, self, false);

			return self;
			
		},
		handleEvent: function(event) {
			var self = this;

			switch (event.type) {
				case $E.start:
					self._touchStart(event);
					break;
				case $E.move:
					self._touchMove(event);
					break;
				case $E.end:
					self._touchEnd(event);
					break;
				case 'click':
					self._click(event);
					break;
			}
		},
		refresh: function() {
			var self = this;

			var conf = self.conf;

			// setting max point
			self.maxPoint = conf.point || (function() {
				var childNodes = self.element.childNodes,
					itemLength = 0,
					i = 0,
					len = childNodes.length,
					node;
				for(; i < len; i++) {
					node = childNodes[i];
					if (node.nodeType === 1) {
						itemLength++;
					}
				}
				if (itemLength > 0) {
					itemLength--;
				}
	
				return itemLength;
			})();

			// setting distance
			self.distance = conf.distance || self.element.scrollWidth / (self.maxPoint + 1);

			// setting maxX
			self.maxX = conf.maxX ? - conf.maxX : - self.distance * self.maxPoint;
	
			self.moveToPoint(self.currentPoint);
		},
		hasNext: function() {
			var self = this;
	
			return self.currentPoint < self.maxPoint;
		},
		hasPrev: function() {
			var self = this;
	
			return self.currentPoint > 0;
		},
		toNext: function() {
			var self = this;

			if (!self.hasNext()) {
				return;
			}

			self.moveToPoint(self.currentPoint + 1);
		},
		toPrev: function() {
			var self = this;

			if (!self.hasPrev()) {
				return;
			}

			self.moveToPoint(self.currentPoint - 1);
		},
        moveToPoint: function(point) {
            var self = this;

            self.currentPoint = 
                (point < 0) ? 0 :
                (point > self.maxPoint) ? self.maxPoint :
                parseInt(point);

            self.element.style.webkitTransitionDuration = '500ms';
            self._setX(- self.currentPoint * self.distance)

            var ev = document.createEvent('Event');
            ev.initEvent('css3flip.moveend', true, false);
            self.element.dispatchEvent(ev);
        },
        _setX: function(x) {
            var self = this;

            self.currentX = x;
            self.element.style.webkitTransform = getTranslate(x);
        },
        _touchStart: function(event) {
            var self = this;

            if (!self.touchEnabled) {
                return;
            }

            if (!$support.touch) {
                event.preventDefault();
            }

            self.element.style.webkitTransitionDuration = '0';
            self.scrolling = true;
            self.moveReady = false;
            self.startPageX = getPage(event, 'pageX');
            self.startPageY = getPage(event, 'pageY');
            self.basePageX = self.startPageX;
            self.directionX = 0;
            self.startTime = event.timeStamp;
        },
        _touchMove: function(event) {
            var self = this;

            if (!self.scrolling) {
                return;
            }

            var pageX = getPage(event, 'pageX'),
                pageY = getPage(event, 'pageY'),
                distX,
                newX,
                deltaX,
                deltaY;

            if (self.moveReady) {
                event.preventDefault();
                event.stopPropagation();

                distX = pageX - self.basePageX;
                newX = self.currentX + distX;
                if (newX >= 0 || newX < self.maxX) {
                    newX = Math.round(self.currentX + distX / 3);
                }
                self._setX(newX);

                self.directionX = distX > 0 ? -1 : 1;
            }
            else {
                deltaX = Math.abs(pageX - self.startPageX);
                deltaY = Math.abs(pageY - self.startPageY);
                if (deltaX > 5) {
                    event.preventDefault();
                    event.stopPropagation();
                    self.moveReady = true;
                    self.element.addEventListener('click', self, true);
                }
                else if (deltaY > 5) {
                    self.scrolling = false;
                }
            }

            self.basePageX = pageX;
        },
        _touchEnd: function(event) {
            var self = this;

            if (!self.scrolling) {
                return;
            }

            self.scrolling = false;

            var newPoint = -self.currentX / self.distance;
            newPoint =
                (self.directionX > 0) ? Math.ceil(newPoint) :
                (self.directionX < 0) ? Math.floor(newPoint) :
                Math.round(newPoint);

            self.moveToPoint(newPoint);

            setTimeout(function() {
                self.element.removeEventListener('click', self, true);
            }, 200);
        },
        _click: function(event) {
            var self = this;

            event.stopPropagation();
            event.preventDefault();
        },
        destroy: function() {
            var self = this;

            self.element.removeEventListener(touchStartEvent, self);
            self.element.removeEventListener(touchMoveEvent, self);
            document.removeEventListener(touchEndEvent, self);
        }
		
		
	});

	this.Css3Flip = function (selector, conf) {
		return (this instanceof Css3Flip) ? this.init(selector, conf) : new Css3Flip(selector, conf);
	}
 	
 
 })(window);

  

 通过改变translate 而不是改变 left 或者margin-left 来实现滑动,效率提升会很明显,平滑度几乎可以媲美native app。在对js执行效率不是很高的移动终端中尤为明显。

posted on 2011-10-10 14:53  岑安  阅读(16977)  评论(14编辑  收藏  举报

导航