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

【转】http://www.cnblogs.com/hongru/archive/2011/10/10/2203744.html

 

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内核浏览器下,只要有这个属性,再去改变这个元素任何的样式,它都会以一个默认的缓动效果完成。

001 /**
002  * CSS3 animation by transform
003  * @example
004  * Let(el)
005  *      .to(500, 200)
006  *      .rotate(180)
007  *      .scale(.5)
008  *      .set({
009  *          background-color: 'red',
010  *          border-color: 'green'
011  *      })
012  *      .duration(2000)
013  *      .skew(50, -10)
014  *      .then()
015  *          .set('opacity', .5)
016  *          .duration('1s')
017  *          .scale(1)
018  *          .pop()
019  *      .end();
020  */
021  
022 (function (win, undefined) {
023      
024     var initializing = false,
025         superTest = /horizon/.test(function () {horizon;}) ? /\b_super\b/ : /.*/;
026     // 临时Class
027     this.Class = function () {};
028     // 继承方法extend
029     Class.extend = function (prop) {
030         var _super = this.prototype;
031         //创建一个实例,但不执行init
032         initializing = true;
033         var prototype = new this();
034         initializing = false;
035  
036         for (var name in prop) {
037             // 用闭包保证多级继承不会污染
038             prototype[name] = (typeof prop[name] === 'function' && typeof _super[name] === 'function' && superTest.test(prop[name])) ? (function (name, fn) {
039                     return function () {
040                         var temp = this._super;
041                         // 当前子类通过_super继承父类
042                         this._super = _super[name];
043                         //继承方法执行完毕后还原
044                         var ret = fn.apply(this, arguments);
045                         this._super = temp;
046  
047                         return ret;
048                     }
049                 })(name, prop[name]) : prop[name];
050         }
051          
052         //真实的constructor
053         function Class () {
054             if (!initializing && this.init) {
055                 this.init.apply(this, arguments);
056             }
057         }
058         Class.prototype = prototype;
059         Class.constructor = Class;
060         Class.extend = arguments.callee;
061  
062         return Class;
063     }
064      
065  
066     // 样式为数字+px 的属性
067     var map = {
068         'top': 'px',
069         'left': 'px',
070         'right': 'px',
071         'bottom': 'px',
072         'width': 'px',
073         'height': 'px',
074         'font-size': 'px',
075         'margin': 'px',
076         'margin-top': 'px',
077         'margin-left': 'px',
078         'margin-right': 'px',
079         'margin-bottom': 'px',
080         'padding': 'px',
081         'padding-left': 'px',
082         'padding-right': 'px',
083         'padding-top': 'px',
084         'padding-bottom': 'px',
085         'border-width': 'px'
086     };
087  
088     /**
089      * Let package
090      */
091     var Let = function (selector) {
092         var el = Let.G(selector);
093         return new Anim(el);
094     };
095     Let.defaults = {
096         duration: 500
097     };
098     Let.ease = {
099         'in' : 'ease-in',
100         'out': 'ease-out',
101         'in-out': 'ease-in-out',
102         'snap' : 'cubic-bezier(0,1,.5,1)'  
103     };
104     Let.G = function (selector) {
105         if (typeof selector != 'string' && selector.nodeType == 1) {
106             return selector;
107         }
108         return document.getElementById(selector) || document.querySelectorAll(selector)[0];    
109     };
110  
111     /**
112      * EventEmitter
113      * {Class}
114      */
115     var EventEmitter = Class.extend({
116         init: function () {
117             this.callbacks = {};
118         },     
119         on: function (event, fn) {
120             (this.callbacks[event] = this.callbacks[event] || []).push(fn);
121             return this;
122         },
123         /**
124          * param {event} 指定event
125          * params 指定event的callback的参数
126          */
127         fire: function (event) {
128             var args = Array.prototype.slice.call(arguments, 1),
129                 callbacks = this.callbacks[event],
130                 len;
131             if (callbacks) {
132                 for (var i = 0, len = callbacks.length; i < len; i ++) {
133                     callbacks[i].apply(this, args);
134                 }
135             }
136             return this;
137         }
138                  
139     });
140  
141     /**
142      * Anim
143      * {Class}
144      * @inherit from EventEmitter
145      */
146     var Anim = EventEmitter.extend({
147         init: function (el) {
148             this._super();
149  
150             if (!(this instanceof Anim)) {
151                 return new Anim(el);
152             }
153  
154             this.el = el;
155             this._props = {};
156             this._rotate = 0;
157             this._transitionProps = [];
158             this._transforms = [];
159             this.duration(Let.defaults.duration);
160              
161         },
162         transform : function (transform) {
163             this._transforms.push(transform);
164             return this;
165         },
166         // skew methods
167         skew: function (x, y) {
168             y = y || 0;
169             return this.transform('skew('+ x +'deg, '+ y +'deg)');
170         },
171         skewX: function (x) {
172             return this.transform('skewX('+ x +'deg)');   
173         },
174         skewY: function (y) {
175             return this.transform('skewY('+ y +'deg)');   
176         },
177         // translate methods
178         translate: function (x, y) {
179             y = y || 0;
180             return this.transform('translate('+ x +'px, '+ y +'px)');
181         },
182         to: function (x, y) {
183             return this.translate(x, y);   
184         },
185         translateX: function (x) {
186             return this.transform('translateX('+ x +'px)');        
187         },
188         x: function (x) {
189             return this.translateX(x);  
190         },
191         translateY: function (y) {
192             return this.transform('translateY('+ y +'px)');        
193         },
194         y: function (y) {
195             return this.translateY(y);  
196         },
197         // scale methods
198         scale: function (x, y) {
199             y = (y == null) ? x : y;
200             return this.transform('scale('+ x +', '+ y +')');
201         },
202         scaleX: function (x) {
203             return this.transform('scaleX('+ x +')');
204         },
205         scaleY: function (y) {
206             return this.transform('scaleY('+ y +')');
207         },
208         // rotate methods
209         rotate: function (n) {
210             return this.transform('rotate('+ n +'deg)');
211         },
212  
213         // set transition ease
214         ease: function (fn) {
215             fn = Let.ease[fn] || fn || 'ease';
216             return this.setVendorProperty('transition-timing-function', fn);
217         },
218  
219         //set duration time
220         duration: function (n) {
221             n = this._duration = (typeof n == 'string') ? parseFloat(n)*1000 : n;
222             return this.setVendorProperty('transition-duration', n + 'ms');
223         },
224  
225         // set delay time
226         delay: function (n) {
227             n = (typeof n == 'string') ? parseFloat(n) * 1000 : n;
228             return this.setVendorProperty('transition-delay', n + 'ms');
229         },
230  
231         // set property to val
232         setProperty: function (prop, val) {
233             this._props[prop] = val;
234             return this;
235         },
236         setVendorProperty: function (prop, val) {
237             this.setProperty('-webkit-' + prop, val);
238             this.setProperty('-moz-' + prop, val);
239             this.setProperty('-ms-' + prop, val);
240             this.setProperty('-o-' + prop, val);
241             return this;
242         },
243         set: function (prop, val) {
244             var _store = {};
245             if (typeof prop == 'string' && val != undefined) {
246                 _store[prop] = val;
247             } else if (typeof prop == 'object' && prop.constructor.prototype.hasOwnProperty('hasOwnProperty')) {
248                 _store = prop;
249             }
250              
251             for (var key in _store) {
252                 this.transition(key);
253                 if (typeof _store[key] == 'number' && map[key]) {
254                     _store[key] += map[key];
255                 }
256                 this._props[key] = _store[key];
257             }
258             return this;
259  
260         },
261          
262         // add value to a property
263         add: function (prop, val) {
264             var self = this;
265             return this.on('start', function () {
266                 var curr = parseInt(self.current(prop), 10);
267                 self.set(prop, curr + val + 'px');
268             })
269         },
270         // sub value to a property
271         sub: function (prop, val) {
272             var self = this;
273             return this.on('start', function () {
274                 var curr = parseInt(self.current(prop), 10);
275                 self.set(prop, curr - val + 'px');
276             })
277         },
278         current: function (prop) {
279             return !!window.getComputedStyle ? document.defaultView.getComputedStyle(this.el, null).getPropertyValue(prop) : this.el.currentStyle(prop);
280         },
281  
282         transition: function (prop) {
283             for (var i = 0; i < this._transitionProps.length; i ++) {
284                 if (this._transitionProps[i] == prop) {
285                     return this;
286                 }
287             }
288  
289             this._transitionProps.push(prop);
290             return this;
291         },
292         applyPropertys: function () {
293             var props = this._props,
294                 el = this.el;
295             for (var prop in props) {
296                 if (props.hasOwnProperty(prop)) {
297                     el.style.setProperty ? el.style.setProperty(prop, props[prop], '') : el.style[prop] = props[prop];
298                 }
299             }
300             return this;
301         },
302          
303         // then
304         then: function (fn) {
305             if (fn instanceof Anim) {
306                 this.on('end', function () {
307                     fn.end();      
308                 })
309             } else if (typeof fn == 'function') {
310                 this.on('end', fn);
311             } else {
312                 var clone = new Anim(this.el);
313                 clone._transforms = this._transforms.slice(0);
314                 this.then(clone);
315                 clone.parent = this;
316                 return clone;
317             }
318  
319             return this;
320         },
321         pop: function () {
322             return this.parent; 
323         },
324         end: function (fn) {
325             var self = this;
326             this.fire('start');
327  
328             if (this._transforms.length > 0) {
329                 this.setVendorProperty('transform', this._transforms.join(' '));
330             }
331  
332             this.setVendorProperty('transition-properties', this._transitionProps.join(', '));
333             this.applyPropertys();
334  
335             if (fn) { this.then(fn) }
336  
337             setTimeout(function () {
338                 self.fire('end');      
339             }, this._duration);
340  
341             return this;
342         }
343          
344     });
345  
346     this.Let = win.Let = Let;
347      
348  
349  })(window)

比如下面代码:

01 <div id="test"></div>
02 <script>
03 Let('#test')
04     .to(200, 200)
05     .rotate(1000)
06     .scale(.5)
07     .set({
08         'background-color': 'red',
09         'width': 300
10     })
11     .duration(2000)
12     .then()
13         .set('opacity', .5)
14         .set('height', 200)
15         .duration('1s')
16         .scale(1.5)
17         .to(300, 300)
18         .pop()
19     .end()
20      
21 </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

01 <!DOCTYPE html>
02 <html>
03 <style>
04 .wrap {
05     display: -webkit-box;
06     -webkit-box-orient: horizontal;
07 }
08 .child {
09     min-height: 200px;
10     border: 2px solid #666;
11     -webkit-box-flex: 1;
12     margin: 10px;
13     font-size: 100px;
14     font-weight: bold;
15     font-family: Georgia;
16     -webkit-box-align: center;
17 }
18 </style>
19  
20 <div class="wrap">
21 <div class="child">1</div>
22 <div class="child">2</div>
23 <div class="child">3</div>
24 </div>
25 </html>

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

01 <!DOCTYPE html>
02 <html>
03 <meta charset="utf-8" />
04 <style>
05 .wrap {
06     display: -webkit-box;
07     -webkit-box-orient: horizontal;
08 }
09 .child {
10     min-height: 200px;
11     border: 2px solid #666;
12     margin: 10px;
13     font-size: 40px;
14     font-weight: bold;
15     font-family: Georgia;
16     -webkit-box-align: center;
17 }
18 .w200 {width: 200px}
19 .flex1 {-webkit-box-flex: 1}
20 .flex2 {-webkit-box-flex: 2}
21 </style>
22  
23 <div class="wrap">
24 <div class="child w200">200px</div>
25 <div class="child flex1">比例1</div>
26 <div class="child flex2">比例2</div>
27 </div>
28 </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 后,横列布局的时候不用计算外围容器和容器里面的元素的宽度。然后再进行横向的滑动的效果就会省去不少麻烦。

001 /**
002  * css3 translate flip
003  * -webkit-box
004  * @author: horizon
005  */
006  
007 (function (win, undefined) {
008   
009     var initializing = false,
010         superTest = /horizon/.test(function () {horizon;}) ? /\b_super\b/ : /.*/;
011     this.Class = function () {};
012  
013     Class.extend = function (prop) {
014         var _super = this.prototype;
015         initializing = true;
016         var prototype = new this();
017         initializing = false;
018  
019         for (var name in prop) {
020             prototype[name] = (typeof prop[name] === 'function' && typeof _super[name] === 'function' && superTest.test(prop[name])) ? (function (name, fn) {
021                     return function () {
022                         var temp = this._super;
023                         this._super = _super[name];
024                         var ret = fn.apply(this, arguments);
025                         this._super = temp;
026  
027                         return ret;
028                     }
029                 })(name, prop[name]) : prop[name];
030         }
031          
032         function Class () {
033             if (!initializing && this.init) {
034                 this.init.apply(this, arguments);
035             }
036         }
037         Class.prototype = prototype;
038         Class.constructor = Class;
039         Class.extend = arguments.callee;
040  
041         return Class;
042     };
043  
044     var $support = {
045         transform3d: ('WebKitCSSMatrix' in win),
046         touch: ('ontouchstart' in win)
047     };
048  
049     var $E = {
050         start: $support.touch ? 'touchstart' : 'mousedown',
051         move: $support.touch ? 'touchmove' : 'mousemove',
052         end: $support.touch ? 'touchend' : 'mouseup'
053     };
054  
055     function getTranslate (x) {
056         return $support.transform3d ? 'translate3d('+x+'px, 0, 0)' : 'translate('+x+'px, 0)';
057     }
058     function getPage (event, page) {
059         return $support.touch ? event.changedTouches[0][page] : event[page];
060     }
061  
062  
063     var Css3Flip = Class.extend({
064         init: function (selector, conf) {
065             var self = this;
066              
067             if (selector.nodeType && selector.nodeType == 1) {
068                 self.element = selector;
069             } else if (typeof selector == 'string') {
070                 self.element = document.getElementById(selector) || document.querySelector(selector);
071             }
072              
073             self.element.style.display = '-webkit-box';
074             self.element.style.webkitTransitionProperty = '-webkit-transform';
075             self.element.style.webkitTransitionTimingFunction = 'cubic-bezier(0,0,0.25,1)';
076             self.element.style.webkitTransitionDuration = '0';
077             self.element.style.webkitTransform = getTranslate(0);
078  
079             self.conf = conf || {};
080             self.touchEnabled = true;
081             self.currentPoint = 0;
082             self.currentX = 0;
083  
084             self.refresh();
085              
086             // 支持handleEvent
087             self.element.addEventListener($E.start, self, false);
088             self.element.addEventListener($E.move, self, false);
089             document.addEventListener($E.end, self, false);
090  
091             return self;
092              
093         },
094         handleEvent: function(event) {
095             var self = this;
096  
097             switch (event.type) {
098                 case $E.start:
099                     self._touchStart(event);
100                     break;
101                 case $E.move:
102                     self._touchMove(event);
103                     break;
104                 case $E.end:
105                     self._touchEnd(event);
106                     break;
107                 case 'click':
108                     self._click(event);
109                     break;
110             }
111         },
112         refresh: function() {
113             var self = this;
114  
115             var conf = self.conf;
116  
117             // setting max point
118             self.maxPoint = conf.point || (function() {
119                 var childNodes = self.element.childNodes,
120                     itemLength = 0,
121                     i = 0,
122                     len = childNodes.length,
123                     node;
124                 for(; i < len; i++) {
125                     node = childNodes[i];
126                     if (node.nodeType === 1) {
127                         itemLength++;
128                     }
129                 }
130                 if (itemLength > 0) {
131                     itemLength--;
132                 }
133      
134                 return itemLength;
135             })();
136  
137             // setting distance
138             self.distance = conf.distance || self.element.scrollWidth / (self.maxPoint + 1);
139  
140             // setting maxX
141             self.maxX = conf.maxX ? - conf.maxX : - self.distance * self.maxPoint;
142      
143             self.moveToPoint(self.currentPoint);
144         },
145         hasNext: function() {
146             var self = this;
147      
148             return self.currentPoint < self.maxPoint;
149         },
150         hasPrev: function() {
151             var self = this;
152      
153             return self.currentPoint > 0;
154         },
155         toNext: function() {
156             var self = this;
157  
158             if (!self.hasNext()) {
159                 return;
160             }
161  
162             self.moveToPoint(self.currentPoint + 1);
163         },
164         toPrev: function() {
165             var self = this;
166  
167             if (!self.hasPrev()) {
168                 return;
169             }
170  
171             self.moveToPoint(self.currentPoint - 1);
172         },
173         moveToPoint: function(point) {
174             var self = this;
175  
176             self.currentPoint =
177                 (point < 0) ? 0 :
178                 (point > self.maxPoint) ? self.maxPoint :
179                 parseInt(point);
180  
181             self.element.style.webkitTransitionDuration = '500ms';
182             self._setX(- self.currentPoint * self.distance)
183  
184             var ev = document.createEvent('Event');
185             ev.initEvent('css3flip.moveend', true, false);
186             self.element.dispatchEvent(ev);
187         },
188         _setX: function(x) {
189             var self = this;
190  
191             self.currentX = x;
192             self.element.style.webkitTransform = getTranslate(x);
193         },
194         _touchStart: function(event) {
195             var self = this;
196  
197             if (!self.touchEnabled) {
198                 return;
199             }
200  
201             if (!$support.touch) {
202                 event.preventDefault();
203             }
204  
205             self.element.style.webkitTransitionDuration = '0';
206             self.scrolling = true;
207             self.moveReady = false;
208             self.startPageX = getPage(event, 'pageX');
209             self.startPageY = getPage(event, 'pageY');
210             self.basePageX = self.startPageX;
211             self.directionX = 0;
212             self.startTime = event.timeStamp;
213         },
214         _touchMove: function(event) {
215             var self = this;
216  
217             if (!self.scrolling) {
218                 return;
219             }
220  
221             var pageX = getPage(event, 'pageX'),
222                 pageY = getPage(event, 'pageY'),
223                 distX,
224                 newX,
225                 deltaX,
226                 deltaY;
227  
228             if (self.moveReady) {
229                 event.preventDefault();
230                 event.stopPropagation();
231  
232                 distX = pageX - self.basePageX;
233                 newX = self.currentX + distX;
234                 if (newX >= 0 || newX < self.maxX) {
235                     newX = Math.round(self.currentX + distX / 3);
236                 }
237                 self._setX(newX);
238  
239                 self.directionX = distX > 0 ? -1 : 1;
240             }
241             else {
242                 deltaX = Math.abs(pageX - self.startPageX);
243                 deltaY = Math.abs(pageY - self.startPageY);
244                 if (deltaX > 5) {
245                     event.preventDefault();
246                     event.stopPropagation();
247                     self.moveReady = true;
248                     self.element.addEventListener('click', self, true);
249                 }
250                 else if (deltaY > 5) {
251                     self.scrolling = false;
252                 }
253             }
254  
255             self.basePageX = pageX;
256         },
257         _touchEnd: function(event) {
258             var self = this;
259  
260             if (!self.scrolling) {
261                 return;
262             }
263  
264             self.scrolling = false;
265  
266             var newPoint = -self.currentX / self.distance;
267             newPoint =
268                 (self.directionX > 0) ? Math.ceil(newPoint) :
269                 (self.directionX < 0) ? Math.floor(newPoint) :
270                 Math.round(newPoint);
271  
272             self.moveToPoint(newPoint);
273  
274             setTimeout(function() {
275                 self.element.removeEventListener('click', self, true);
276             }, 200);
277         },
278         _click: function(event) {
279             var self = this;
280  
281             event.stopPropagation();
282             event.preventDefault();
283         },
284         destroy: function() {
285             var self = this;
286  
287             self.element.removeEventListener(touchStartEvent, self);
288             self.element.removeEventListener(touchMoveEvent, self);
289             document.removeEventListener(touchEndEvent, self);
290         }
291          
292          
293     });
294  
295     this.Css3Flip = function (selector, conf) {
296         return (this instanceof Css3Flip) ? this.init(selector, conf) : new Css3Flip(selector, conf);
297     }
298      
299   
300  })(window);

  

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

 
posted @ 2011-10-16 16:37  andygoo  阅读(616)  评论(0编辑  收藏  举报