在去年,咱們對IScroll的源碼進行了學習,而且分離出了一段代碼本身使用,在使用學習過程當中發現幾個致命問題:javascript
① 光標移位css
② 文本框找不到(先讓文本框獲取焦點,再滑動一下,輸入文字即可重現)html
③ 偶爾致使頭部消失,頭部可不是fixed哦前端
因爲以上問題,加之去年咱們團隊的工做量極大,和中間一些組織架構調整,這個事情一直被放到了今天,內心一直對此耿耿於懷,由於IScroll讓人忘不了的好處java
小釵堅信,IScroll能夠帶來前端體驗上的革命,由於他能夠解決如下問題node
咱們不能由於一兩個小問題而放棄如此牛逼的點子,因此咱們要處理其中的問題,那麼這些問題是否真的不可解決,而引發這些問題的緣由又究竟是什麼,咱們今天來一一探索ios
第一步依舊是抽離IScroll核心邏輯,咱們這裏先在簡單層面上探究問題,以避免被旁枝末節的BUG困擾,這裏造成的一個庫只支持縱向滾動,代碼量比較少css3
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <title></title> 5 <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0"> 7 <meta name="keywords" content="" /> 8 <meta name="description" content="" /> 9 <meta name="robots" content="all" /> 10 <style type="text/css"> 11 * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } 12 html { -ms-touch-action: none; } 13 body, ul, li { padding: 0; margin: 0; border: 0; } 14 body { font-size: 12px; font-family: ubuntu, helvetica, arial; overflow: hidden; /* this is important to prevent the whole page to bounce */ } 15 #header { position: absolute; z-index: 2; top: 0; left: 0; width: 100%; height: 45px; line-height: 45px; background: #CD235C; padding: 0; color: #eee; font-size: 20px; text-align: center; font-weight: bold; } 16 #footer { position: absolute; z-index: 2; bottom: 0; left: 0; width: 100%; height: 48px; background: #444; padding: 0; border-top: 1px solid #444; } 17 #wrapper { position: absolute; z-index: 1; top: 45px; bottom: 48px; left: 0; width: 100%; background: #ccc; overflow: hidden; } 18 #scroller { position: absolute; z-index: 1; -webkit-tap-highlight-color: rgba(0,0,0,0); width: 100%; -webkit-transform: translateZ(0); -moz-transform: translateZ(0); -ms-transform: translateZ(0); -o-transform: translateZ(0); transform: translateZ(0); -webkit-touch-callout: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-text-size-adjust: none; -moz-text-size-adjust: none; -ms-text-size-adjust: none; -o-text-size-adjust: none; text-size-adjust: none; } 19 #scroller ul { list-style: none; padding: 0; margin: 0; width: 100%; text-align: left; } 20 #scroller li { padding: 0 10px; height: 40px; line-height: 40px; border-bottom: 1px solid #ccc; border-top: 1px solid #fff; background-color: #fafafa; font-size: 14px; } 21 </style> 22 </head> 23 <body> 24 <div id="header"> 25 iScroll</div> 26 <div id="wrapper"> 27 <div id="scroller"> 28 <ul> 29 <li>Pretty row 1</li> 30 <li>Pretty row 2</li> 31 <li>Pretty row 3</li> 32 <li>Pretty row 4</li> 33 <li> 34 <input type="text"></li> 35 <li>Pretty row 6</li> 36 <li>Pretty row 7</li> 37 <li>Pretty row 8</li> 38 <li> 39 <input type="checkbox"></li> 40 <li>Pretty row 10</li> 41 <li>Pretty row 11</li> 42 <li>Pretty row 12</li> 43 <li> 44 <input type="radio"></li> 45 <li>Pretty row 14</li> 46 <li>Pretty row 15</li> 47 <li>Pretty row 16</li> 48 <li> 49 <textarea></textarea></li> 50 <li>Pretty row 18</li> 51 <li>Pretty row 19</li> 52 <li>Pretty row 20</li> 53 <li> 54 <select> 55 <option>option</option> 56 </select></li> 57 <li>Pretty row 22</li> 58 <li>Pretty row 23</li> 59 <li>Pretty row 24</li> 60 <li>Pretty row 25</li> 61 <li>Pretty row 26</li> 62 <li>Pretty row 27</li> 63 <li>Pretty row 28</li> 64 <li>Pretty row 29</li> 65 <li>Pretty row 30</li> 66 <li>Pretty row 31</li> 67 <li>Pretty row 32</li> 68 <li>Pretty row 33</li> 69 <li>Pretty row 34</li> 70 <li>Pretty row 35</li> 71 <li>Pretty row 36</li> 72 <li>Pretty row 37</li> 73 <li>Pretty row 38</li> 74 <li>Pretty row 39</li> 75 <li>Pretty row 40</li> 76 <li>Pretty row 41</li> 77 <li>Pretty row 42</li> 78 <li>Pretty row 43</li> 79 <li>Pretty row 44</li> 80 <li>Pretty row 45</li> 81 <li>Pretty row 46</li> 82 <li>Pretty row 47</li> 83 <li>Pretty row 48</li> 84 <li>Pretty row 49</li> 85 <li>Pretty row 50</li> 86 </ul> 87 </div> 88 </div> 89 <div id="footer"> 90 </div> 91 <script src="../../zepto/zepto.js" type="text/javascript"></script> 92 <script src="../../fastclick-1.0.0/lib/fastclick.js" type="text/javascript"></script> 93 <script type="text/javascript" src="c.ui.scroll.js"></script> 94 <script type="text/javascript"> 95 96 var s = new IScroll({ 97 wrapper: $('#wrapper'), 98 scroller: $('#scroller') 99 100 }); 101 102 new FastClick(document.body); 103 104 </script> 105 </body> 106 </html>
1 var utils = (function () { 2 var me = {}; 3 var _elementStyle = document.createElement('div').style; 4 5 //得到須要兼容CSS3前綴 6 var _vendor = (function () { 7 var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT']; 8 var transform; 9 var i = 0; 10 var l = vendors.length; 11 12 for (; i < l; i++) { 13 transform = vendors[i] + 'ransform'; 14 if (transform in _elementStyle) return vendors[i].substr(0, vendors[i].length - 1); 15 } 16 return false; 17 })(); 18 19 //獲取樣式(CSS3兼容) 20 function _prefixStyle(style) { 21 if (_vendor === false) return false; 22 if (_vendor === '') return style; 23 return _vendor + style.charAt(0).toUpperCase() + style.substr(1); 24 } 25 26 me.getTime = Date.now || function getTime() { return new Date().getTime(); }; 27 28 me.addEvent = function (el, type, fn, capture) { 29 if (el[0]) el = el[0]; 30 el.addEventListener(type, fn, !!capture); 31 }; 32 33 me.removeEvent = function (el, type, fn, capture) { 34 if (el[0]) el = el[0]; 35 el.removeEventListener(type, fn, !!capture); 36 }; 37 38 /* 39 current:當前鼠標位置 40 start:touchStart時候記錄的Y(多是X)的開始位置,可是在touchmove時候可能被重寫 41 time: touchstart到手指離開時候經歷的時間,一樣可能被touchmove重寫 42 lowerMargin:y可移動的最大距離,這個通常爲計算得出 this.wrapperHeight - this.scrollerHeight 43 wrapperSize:若是有邊界距離的話就是可拖動,否則碰到0的時候便中止 44 */ 45 me.momentum = function (current, start, time, lowerMargin, wrapperSize) { 46 var distance = current - start, 47 speed = Math.abs(distance) / time, 48 destination, 49 duration, 50 deceleration = 0.0006; 51 52 destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1); 53 duration = speed / deceleration; 54 55 if (destination < lowerMargin) { 56 destination = wrapperSize ? lowerMargin - (wrapperSize / 2.5 * (speed / 8)) : lowerMargin; 57 distance = Math.abs(destination - current); 58 duration = distance / speed; 59 } else if (destination > 0) { 60 destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0; 61 distance = Math.abs(current) + destination; 62 duration = distance / speed; 63 } 64 65 return { 66 destination: Math.round(destination), 67 duration: duration 68 }; 69 70 }; 71 72 $.extend(me, { 73 hasTouch: 'ontouchstart' in window 74 }); 75 76 77 //咱們暫時只判斷touch 和 mouse便可 78 $.extend(me.style = {}, { 79 transform: _prefixStyle('transform'), 80 transitionTimingFunction: _prefixStyle('transitionTimingFunction'), 81 transitionDuration: _prefixStyle('transitionDuration'), 82 transitionDelay: _prefixStyle('transitionDelay'), 83 transformOrigin: _prefixStyle('transformOrigin') 84 }); 85 86 $.extend(me.eventType = {}, { 87 touchstart: 1, 88 touchmove: 1, 89 touchend: 1, 90 91 mousedown: 2, 92 mousemove: 2, 93 mouseup: 2 94 }); 95 96 $.extend(me.ease = {}, { 97 quadratic: { 98 style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 99 fn: function (k) { 100 return k * (2 - k); 101 } 102 }, 103 circular: { 104 style: 'cubic-bezier(0.1, 0.57, 0.1, 1)', // Not properly "circular" but this looks better, it should be (0.075, 0.82, 0.165, 1) 105 fn: function (k) { 106 return Math.sqrt(1 - (--k * k)); 107 } 108 }, 109 back: { 110 style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', 111 fn: function (k) { 112 var b = 4; 113 return (k = k - 1) * k * ((b + 1) * k + b) + 1; 114 } 115 }, 116 bounce: { 117 style: '', 118 fn: function (k) { 119 if ((k /= 1) < (1 / 2.75)) { 120 return 7.5625 * k * k; 121 } else if (k < (2 / 2.75)) { 122 return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; 123 } else if (k < (2.5 / 2.75)) { 124 return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; 125 } else { 126 return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; 127 } 128 } 129 }, 130 elastic: { 131 style: '', 132 fn: function (k) { 133 var f = 0.22, 134 e = 0.4; 135 136 if (k === 0) { return 0; } 137 if (k == 1) { return 1; } 138 139 return (e * Math.pow(2, -10 * k) * Math.sin((k - f / 4) * (2 * Math.PI) / f) + 1); 140 } 141 } 142 }); 143 return me; 144 })(); 145 146 function IScroll(opts) { 147 this.wrapper = typeof opts.wrapper == 'string' ? $(opts.wrapper) : opts.wrapper; 148 this.scroller = typeof opts.scroller == 'string' ? $(opts.scroller) : opts.scroller; 149 if (!opts.wrapper[0] || !opts.scroller[0]) throw 'param error'; 150 151 this.wrapper = this.wrapper[0]; 152 this.scroller = this.scroller[0]; 153 154 //這個屬性會被動態改變的,若是這裏 155 this.scrollerStyle = this.scroller.style; 156 157 this.options = { 158 //是否具備滾動條 159 scrollbars: true, 160 // 其實時期Y的位置 161 startY: 0, 162 //超出邊界還原時間點 163 bounceTime: 600, 164 //超出邊界返回的動畫 165 bounceEasing: utils.ease.circular, 166 167 //超出邊界時候是否還能拖動 168 bounce: true, 169 170 bindToWrapper: true, 171 172 //當window觸發resize事件60ms後還原 173 resizePolling: 60, 174 startX: 0, 175 startY: 0 176 }; 177 178 for (var i in opts) { 179 this.options[i] = opts[i]; 180 } 181 182 this.translateZ = ' translateZ(0)'; 183 184 this.x = 0; 185 this.y = 0; 186 this._events = {}; 187 this._init(); 188 189 //更新滾動條位置 190 this.refresh(); 191 192 //更新自己位置 193 this.scrollTo(this.options.startX, this.options.startY); 194 195 this.enable(); 196 197 }; 198 199 IScroll.prototype = { 200 _init: function () { 201 this._initEvents(); 202 203 //初始化滾動條,滾動條此處須要作重要處理 204 if (this.options.scrollbars) { 205 this._initIndicator(); 206 } 207 }, 208 refresh: function () { 209 var rf = this.wrapper.offsetHeight; // Force reflow 210 211 this.wrapperHeight = this.wrapper.clientHeight; 212 this.scrollerHeight = this.scroller.offsetHeight; 213 this.maxScrollY = this.wrapperHeight - this.scrollerHeight; 214 215 this.endTime = 0; 216 217 this._execEvent('refresh'); 218 219 this.resetPosition(); 220 221 }, 222 _initEvents: function (remove) { 223 var eventType = remove ? utils.removeEvent : utils.addEvent; 224 var target = this.options.bindToWrapper ? this.wrapper : window; 225 226 eventType(window, 'orientationchange', this); 227 eventType(window, 'resize', this); 228 229 if (utils.hasTouch) { 230 eventType(this.wrapper, 'touchstart', this); 231 eventType(target, 'touchmove', this); 232 eventType(target, 'touchcancel', this); 233 eventType(target, 'touchend', this); 234 } else { 235 eventType(this.wrapper, 'mousedown', this); 236 eventType(target, 'mousemove', this); 237 eventType(target, 'mousecancel', this); 238 eventType(target, 'mouseup', this); 239 } 240 241 eventType(this.scroller, 'transitionend', this); 242 eventType(this.scroller, 'webkitTransitionEnd', this); 243 eventType(this.scroller, 'oTransitionEnd', this); 244 eventType(this.scroller, 'MSTransitionEnd', this); 245 }, 246 _start: function (e) { 247 if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) { 248 return; 249 } 250 251 var point = e.touches ? e.touches[0] : e, pos; 252 this.initiated = utils.eventType[e.type]; 253 254 this.moved = false; 255 256 this.distY = 0; 257 258 //開啓動畫時間,若是以前有動畫的話,便要中止動畫,這裏由於沒有傳時間,因此動畫便直接中止了 259 this._transitionTime(); 260 261 this.startTime = utils.getTime(); 262 263 //若是正在進行動畫,須要中止,而且觸發滑動結束事件 264 if (this.isInTransition) { 265 this.isInTransition = false; 266 pos = this.getComputedPosition(); 267 var _x = Math.round(pos.x); 268 var _y = Math.round(pos.y); 269 270 if (_y < 0 && _y > this.maxScrollY && this.options.adjustXY) { 271 _y = this.options.adjustXY.call(this, _x, _y).y; 272 } 273 274 //移動過去 275 this._translate(_x, _y); 276 this._execEvent('scrollEnd'); 277 } 278 279 this.startX = this.x; 280 this.startY = this.y; 281 this.absStartX = this.x; 282 this.absStartY = this.y; 283 this.pointX = point.pageX; 284 this.pointY = point.pageY; 285 286 this._execEvent('beforeScrollStart'); 287 288 e.preventDefault(); 289 290 }, 291 292 _move: function (e) { 293 if (!this.enabled || utils.eventType[e.type] !== this.initiated) { 294 return; 295 } 296 e.preventDefault(); 297 298 var point = e.touches ? e.touches[0] : e, 299 deltaX = point.pageX - this.pointX, 300 deltaY = point.pageY - this.pointY, 301 timestamp = utils.getTime(), 302 newX, newY, 303 absDistX, absDistY; 304 305 this.pointX = point.pageX; 306 this.pointY = point.pageY; 307 308 this.distX += deltaX; 309 this.distY += deltaY; 310 absDistX = Math.abs(this.distX); 311 absDistY = Math.abs(this.distY); 312 313 // 若是一直按着沒反應的話這裏就直接返回了 314 if (timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10)) { 315 return; 316 } 317 318 newY = this.y + deltaY; 319 320 if (newY > 0 || newY < this.maxScrollY) { 321 newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY; 322 } 323 324 if (!this.moved) { 325 this._execEvent('scrollStart'); 326 } 327 328 this.moved = true; 329 330 this._translate(0, newY); 331 332 if (timestamp - this.startTime > 300) { 333 this.startTime = timestamp; 334 this.startX = this.x; 335 this.startY = this.y; 336 } 337 338 339 }, 340 _end: function (e) { 341 342 if (!this.enabled || utils.eventType[e.type] !== this.initiated) { 343 return; 344 } 345 346 var point = e.changedTouches ? e.changedTouches[0] : e, 347 momentumY, 348 duration = utils.getTime() - this.startTime, 349 newX = Math.round(this.x), 350 newY = Math.round(this.y), 351 distanceX = Math.abs(newX - this.startX), 352 distanceY = Math.abs(newY - this.startY), 353 time = 0, 354 easing = ''; 355 356 this.isInTransition = 0; 357 this.initiated = 0; 358 this.endTime = utils.getTime(); 359 360 if (this.resetPosition(this.options.bounceTime)) { 361 return; 362 } 363 364 this.scrollTo(newX, newY); 365 if (!this.moved) { 366 //click 的狀況 367 368 this._execEvent('scrollCancel'); 369 return; 370 } 371 372 if (duration < 300) { 373 374 momentumY = utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0); 375 // newX = momentumX.destination; 376 newY = momentumY.destination; 377 time = Math.max(momentumY.duration); 378 this.isInTransition = 1; 379 } 380 381 if (newY != this.y) { 382 if (newY > 0 || newY < this.maxScrollY) { 383 easing = utils.ease.quadratic; 384 } 385 386 this.scrollTo(newX, newY, time, easing); 387 return; 388 } 389 390 this._execEvent('scrollEnd'); 391 }, 392 393 _resize: function () { 394 var that = this; 395 396 clearTimeout(this.resizeTimeout); 397 398 this.resizeTimeout = setTimeout(function () { 399 that.refresh(); 400 }, this.options.resizePolling); 401 }, 402 403 _transitionTimingFunction: function (easing) { 404 this.scrollerStyle[utils.style.transitionTimingFunction] = easing; 405 406 this.indicator && this.indicator.transitionTimingFunction(easing); 407 }, 408 409 //開始或者中止動畫 410 _transitionTime: function (time) { 411 time = time || 0; 412 this.scrollerStyle[utils.style.transitionDuration] = time + 'ms'; 413 414 //滾動條,咱們這裏只會出現一個滾動條就不搞那麼複雜了 415 this.indicator && this.indicator.transitionTime(time); 416 417 }, 418 419 getComputedPosition: function () { 420 var matrix = window.getComputedStyle(this.scroller, null), x, y; 421 422 matrix = matrix[utils.style.transform].split(')')[0].split(', '); 423 x = +(matrix[12] || matrix[4]); 424 y = +(matrix[13] || matrix[5]); 425 426 return { x: x, y: y }; 427 }, 428 429 _initIndicator: function () { 430 //滾動條 431 var el = createDefaultScrollbar(); 432 this.wrapper.appendChild(el); 433 this.indicator = new Indicator(this, { el: el }); 434 435 this.on('scrollEnd', function () { 436 this.indicator.fade(); 437 }); 438 439 var scope = this; 440 this.on('scrollCancel', function () { 441 scope.indicator.fade(); 442 }); 443 444 this.on('scrollStart', function () { 445 scope.indicator.fade(1); 446 }); 447 448 this.on('beforeScrollStart', function () { 449 scope.indicator.fade(1, true); 450 }); 451 452 this.on('refresh', function () { 453 scope.indicator.refresh(); 454 }); 455 456 }, 457 458 //移動x,y這裏比較簡單就不分離y了 459 _translate: function (x, y) { 460 this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; 461 462 this.x = x; 463 this.y = y; 464 465 if (this.options.scrollbars) { 466 this.indicator.updatePosition(); 467 } 468 469 }, 470 471 resetPosition: function (time) { 472 var x = this.x, 473 y = this.y; 474 475 time = time || 0; 476 477 if (this.y > 0) { 478 y = 0; 479 } else if (this.y < this.maxScrollY) { 480 y = this.maxScrollY; 481 } 482 483 if (y == this.y) { 484 return false; 485 } 486 487 this.scrollTo(x, y, time, this.options.bounceEasing); 488 489 return true; 490 }, 491 492 //移動 493 scrollTo: function (x, y, time, easing) { 494 495 // //l_wang 必須項目高度的整數 496 // if (y < 0 && y > this.maxScrollY && this.options.adjustXY) { 497 // y = this.options.adjustXY.call(this, x, y).y; 498 // } 499 500 501 if (this.options.adjustXY) { 502 y = this.options.adjustXY.call(this, x, y).y; 503 } 504 505 //l_wang 驗證該項是否可選 506 if (this.options.checkSelected) { 507 y = this.options.checkSelected.call(this, x, y).y; 508 } 509 510 easing = easing || utils.ease.circular; 511 512 this.isInTransition = time > 0; 513 514 if (!time || easing.style) { 515 this._transitionTimingFunction(easing.style); 516 this._transitionTime(time); 517 this._translate(x, y); 518 } 519 }, 520 521 //統一的關閉接口 522 disable: function () { 523 this.enabled = false; 524 }, 525 //統一的open接口 526 enable: function () { 527 this.enabled = true; 528 }, 529 530 on: function (type, fn) { 531 if (!this._events[type]) { 532 this._events[type] = []; 533 } 534 535 this._events[type].push(fn); 536 }, 537 538 _execEvent: function (type) { 539 if (!this._events[type]) { 540 return; 541 } 542 543 var i = 0, 544 l = this._events[type].length; 545 546 if (!l) { 547 return; 548 } 549 550 for (; i < l; i++) { 551 this._events[type][i].call(this); 552 } 553 }, 554 destroy: function () { 555 this._initEvents(true); 556 this._execEvent('destroy'); 557 this.indicator && this.indicator.destroy(); 558 559 console.log('destroy') 560 561 }, 562 563 _transitionEnd: function (e) { 564 if (e.target != this.scroller || !this.isInTransition) { 565 return; 566 } 567 568 this._transitionTime(); 569 if (!this.resetPosition(this.options.bounceTime)) { 570 this.isInTransition = false; 571 this._execEvent('scrollEnd'); 572 } 573 }, 574 575 //事件具體觸發點 576 handleEvent: function (e) { 577 switch (e.type) { 578 case 'touchstart': 579 case 'mousedown': 580 this._start(e); 581 break; 582 case 'touchmove': 583 case 'mousemove': 584 this._move(e); 585 break; 586 case 'touchend': 587 case 'mouseup': 588 case 'touchcancel': 589 case 'mousecancel': 590 this._end(e); 591 break; 592 case 'orientationchange': 593 case 'resize': 594 this._resize(); 595 break; 596 case 'transitionend': 597 case 'webkitTransitionEnd': 598 case 'oTransitionEnd': 599 case 'MSTransitionEnd': 600 this._transitionEnd(e); 601 break; 602 } 603 } 604 605 }; 606 607 function createDefaultScrollbar() { 608 var scrollbar = document.createElement('div'), 609 indicator = document.createElement('div'); 610 611 scrollbar.style.cssText = 'position:absolute;z-index:9999'; 612 scrollbar.style.cssText += ';width:7px;bottom:2px;top:2px;right:1px'; 613 scrollbar.style.cssText += ';overflow:hidden'; 614 615 indicator.style.cssText = '-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:absolute;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);border-radius:3px'; 616 indicator.style.width = '100%'; 617 618 scrollbar.appendChild(indicator); 619 620 return scrollbar; 621 } 622 623 function Indicator(scroller, opts) { 624 this.wrapper = typeof opts.el == 'string' ? document.querySelector(opts.el) : opts.el; 625 this.indicator = this.wrapper.children[0]; 626 627 this.wrapperStyle = this.wrapper.style; 628 this.indicatorStyle = this.indicator.style; 629 this.scroller = scroller; 630 631 this.sizeRatioY = 1; 632 this.maxPosY = 0; 633 634 this.wrapperStyle[utils.style.transform] = this.scroller.translateZ; 635 this.wrapperStyle[utils.style.transitionDuration] = '0ms'; 636 //this.wrapperStyle.opacity = '0'; 637 } 638 639 Indicator.prototype = { 640 transitionTime: function (time) { 641 time = time || 0; 642 this.indicatorStyle[utils.style.transitionDuration] = time + 'ms'; 643 }, 644 transitionTimingFunction: function (easing) { 645 this.indicatorStyle[utils.style.transitionTimingFunction] = easing; 646 }, 647 refresh: function () { 648 649 this.transitionTime(); 650 651 var r = this.wrapper.offsetHeight; // force refresh 652 653 this.wrapperHeight = this.wrapper.clientHeight; 654 655 656 this.indicatorHeight = Math.max(Math.round(this.wrapperHeight * this.wrapperHeight / (this.scroller.scrollerHeight || this.wrapperHeight || 1)), 8); 657 this.indicatorStyle.height = this.indicatorHeight + 'px'; 658 659 660 this.maxPosY = this.wrapperHeight - this.indicatorHeight; 661 this.sizeRatioY = (this.scroller.maxScrollY && (this.maxPosY / this.scroller.maxScrollY)); 662 663 this.updatePosition(); 664 }, 665 destroy: function () { 666 this.wrapper.remove(); 667 }, 668 updatePosition: function () { 669 var y = Math.round(this.sizeRatioY * this.scroller.y) || 0; 670 this.y = y; 671 672 //不須要兼容方式了 673 this.indicatorStyle[utils.style.transform] = 'translate(0px,' + y + 'px)' + this.scroller.translateZ; 674 675 }, 676 fade: function (val, hold) { 677 if (hold && !this.visible) { 678 return; 679 } 680 681 clearTimeout(this.fadeTimeout); 682 this.fadeTimeout = null; 683 684 var time = val ? 250 : 500, 685 delay = val ? 0 : 300; 686 687 val = val ? '1' : '0'; 688 689 this.wrapperStyle[utils.style.transitionDuration] = time + 'ms'; 690 691 this.fadeTimeout = setTimeout($.proxy(function (val) { 692 this.wrapperStyle.opacity = val; 693 this.visible = +val; 694 }, this), delay); 695 696 } 697 }; 698 699 IScroll.utils = utils;
代碼中引入了fastclick解決其移動端點擊問題,demo效果在此:web
http://sandbox.runjs.cn/show/xq2fbetvubuntu
基本代碼出來了,咱們如今來一個個埋坑,首先解決難的問題!
光標跳動是什麼現象你們都知道了,至於致使的緣由又咱們測試下來,便可肯定罪魁禍首爲:transform,因而咱們看看滑動過程當中發生了什麼
① 每次滑動會涉及到位置的變化
this._translate(0, newY);
② 每次變化會改變transform屬性
1 //移動x,y這裏比較簡單就不分離y了 2 _translate: function (x, y) { 3 this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; 4 5 this.x = x; 6 this.y = y; 7 8 if (this.options.scrollbars) { 9 this.indicator.updatePosition(); 10 } 11 12 },
咱們這裏作一次剝離,將transform改爲直接改變top值看看效果
this.scrollerStyle['top'] = y + 'px';
而事實證實,一旦去除transform屬性,咱們這裏便再也不有光標閃動的問題了。
更進一步的分析,實驗,你會發現其實引發的緣由是這句:
// this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' ;
沒錯,就是css3d加速引發的,他的優點是讓動畫變得順暢,卻未曾想到會引發文本框光標閃爍的問題
針對ios閃爍有一個神奇的屬性是
-webkit-backface-visibility: hidden;
因而加入到,scroller元素上後觀察之,無效,捨棄該方案再來就是一些怪辦法了:
文本獲取焦點的狀況下,會隱藏虛擬鍵盤,連焦點都沒有了,這個問題天然不藥而癒,因而咱們只要滑動便讓其失去焦點,這樣彷佛狡猾的繞過了這個問題
在touchmove邏輯處加入如下邏輯
1 //暫時只考慮input問題,有效再擴展 2 var el = document.activeElement; 3 if (el.nodeName.toLowerCase() == 'input') { 4 el.blur(); 5 this.disable(); 6 setTimeout($.proxy(function () { 7 this.enable(); 8 }, this), 250); 9 return; 10 }
該方案最爲簡單粗暴,他在咱們意圖滑動時便直接致使虛擬鍵盤失效,從而根本不會滑動,便錯過了光標跳動的問題
甚至,解決了因爲滾動致使的文本框消失問題!!!
其中有一個250ms的延時,這個期間是虛擬鍵盤隱藏所用時間,這個時間段將不可對IScroll進行操做,該方案實驗下來效果還行
其中這個延時在200-300之間比較符合人的操做習慣,不設置滾動區域會亂閃,取什麼值各位本身去嘗試,測試地址:
http://sandbox.runjs.cn/show/8nkmlmz5
這個方案是我以爲最優的方案,其是否接受還要看產品態度
_translate是IScroll滑動的總控,這裏默認是使用transform進行移動,但如果獲取焦點的狀況下咱們能夠具備不同的方案
在文本框具備焦點是,咱們使用top代替transform!
PS:這是個爛方法不建議採用
1 //移動x,y這裏比較簡單就不分離y了 2 _translate: function (x, y) { 3 4 var el = document.activeElement; 5 if (el.nodeName.toLowerCase() == 'input') { 6 this.scrollerStyle['top'] = y + 'px'; 7 } else { 8 this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; 9 } 10 11 this.x = x; 12 this.y = y; 13 14 if (this.options.scrollbars) { 15 this.indicator.updatePosition(); 16 } 17 18 },
該方案被測試確實可行,不會出現光標閃的現象,可是有一個問題卻須要咱們處理,即是一旦文本框失去焦點,咱們要作top與transform的換算
因此這是一個爛方法!!!這裏換算事實上也不難,就是將top值從新歸還transform,可是整個這個邏輯倒是讓人以爲彆扭
並且咱們這裏還須要一個定時器去作計算,判斷什麼時候文本框失去焦點,整個這個邏輯就是一個字 坑!
1 //移動x,y這裏比較簡單就不分離y了 2 _translate: function (x, y) { 3 4 var el = document.activeElement; 5 if (el.nodeName.toLowerCase() == 'input') { 6 this.scrollerStyle['top'] = y + 'px'; 7 8 //便須要作距離換算相關清理,一旦文本框事情焦點,咱們要作top值還原 9 if (!this.TimerSrc) { 10 this.TimerSrc = setInterval($.proxy(function () { 11 var el = document.activeElement; 12 if (el.nodeName.toLowerCase() != 'input') { 13 14 pos = this.getComputedPosition(); 15 16 var top = $(scroller).css('top'); 17 this.scrollerStyle['top'] = '0px'; 18 console.log(pos); 19 20 var _x = Math.round(pos.x); 21 var _y = Math.round(pos.y); 22 _y = _y + parseInt(top); 23 24 //移動過去 25 this._translate(_x, _y); 26 27 clearInterval(this.TimerSrc); 28 this.TimerSrc = null; 29 } 30 }, this), 20); 31 } 32 } else { 33 this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; 34 } 35 36 this.x = x; 37 this.y = y; 38 39 if (this.options.scrollbars) { 40 this.indicator.updatePosition(); 41 } 42 43 },
經測試,該代碼能夠解決光標跳動問題,可是坑不坑你們內心有數,一旦須要被迫使用定時器的地方,一定會有點坑!測試地址
http://sandbox.runjs.cn/show/v9pno9d8
文本框消息是因爲滾動中產生動畫,將文本框搞到區域外了,這個時候一旦咱們輸入文字,致使input change,系統便會自動將文本定位到中間,而出現文本不可見問題
該問題的處理最好的方案,依舊是方案一,如果這裏要死磕,又會有許多問題,方案無非是給文本設置changed事件,或者定時器什麼的,當變化時,自動將文本元素
至於IScroll可視區域,樓主這裏就不獻醜了,由於我基本決定使用方案一了。
所謂步長移動即是我一次必須移動必定距離,這個與圖片橫向輪播功能有點相似,而這類需求在移動端數不勝數,那咱們的IScroll應該如何處理才能加上這一偉大特性呢?
去看IScroll的源碼,人家都已經實現了,竟然人家都實現了,哎,可是咱們這裏無論他,照舊作咱們的事情吧,加入步長功能
PS:這裏有點小小的失落,我覺得沒有實現呢,這樣我搞出來確定沒有官方的優雅了!
思路其實很簡單,咱們如果設置了一個步長屬性,暫時咱們認爲他是一個數字(其實能夠是bool值,由庫本身計算該值),而後每次移動時候便必須強制移動該屬性的整數倍便可,好比:
1 var s = new IScroll({ 2 wrapper: $('#wrapper'), 3 scroller: $('#scroller'), 4 setp: 40 5 });
這個便要求每次都得移動10px的步長,那麼這個如何實現呢?其實實現點,依然是_translate處,咱們這裏須要一點處理
1 //移動x,y這裏比較簡單就不分離y了 2 _translate: function (x, y, isStep) { 3 4 //處理步長 5 if (this.options.setp && !isStep) { 6 var flag2 = y > 0 ? 1 : -1; //這個會影響後面的計算結果 7 var top = Math.abs(y); 8 var mod = top % this.options.setp; 9 top = (parseInt(top / this.options.setp) * this.options.setp + (mod > (this.options.setp/2) ? this.options.setp : 0)) * flag2; 10 y = top; 11 } 12 13 this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; 14 15 this.x = x; 16 this.y = y; 17 18 if (this.options.scrollbars) { 19 this.indicator.updatePosition(); 20 } 21 22 },
這樣一改後,每次便要求移動40px的步長,固然,我這裏代碼寫的不是太好,整個效果在此
這裏惟一須要處理的就是touchmove了,每次move的時候,咱們不該該對其進行步長控制,然後皆能夠,這種控制步長的效果有什麼用呢?請看這個例子:
todo......
todo......
今日耗時比較長了,學習暫時到此,咱們下次繼續。