【注:本文所有的代码和实例仅在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内核浏览器下,只要有这个属性,再去改变这个元素任何的样式,它都会以一个默认的缓动效果完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
|
/** * 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;}) ? /_super/ : /.*/; // 临时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) |
比如下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<!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的比例自适应)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<!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 { 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 { 80%; min- 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 { 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 后,横列布局的时候不用计算外围容器和容器里面的元素的宽度。然后再进行横向的滑动的效果就会省去不少麻烦。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
|
/** * css3 translate flip * -webkit-box * @author: horizon */ ( function (win, undefined) { var initializing = false , superTest = /horizon/.test( function () {horizon;}) ? /_super/ : /.*/; 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执行效率不是很高的移动终端中尤为明显。