【IScroll深入学习】突破移动端黑暗的利器(上)
前言
在去年,我们对IScroll的源码进行了学习,并且分离出了一段代码自己使用,在使用学习过程中发现几个致命问题:
① 光标移位
② 文本框找不到(先让文本框获取焦点,再滑动一下,输入文字便可重现)
③ 偶尔导致头部消失,头部可不是fixed哦
由于以上问题,加之去年我们团队的工作量极大,和中间一些组织架构调整,这个事情一直被放到了今天,心里一直对此耿耿于怀,因为IScroll让人忘不了的好处
小钗坚信,IScroll可以带来前端体验上的革命,因为他可以解决以下问题
- 区域滑动顺滑感的体验
- 解决fixed移位问题
- 解决动画过程中长短页的问题,并且可以优化view切换动画的顺畅度
我们不能因为一两个小问题而放弃如此牛逼的点子,所以我们要处理其中的问题,那么这些问题是否真的不可解决,而引起这些问题的原因又到底是什么,我们今天来一一探索
抽离IScroll
第一步依旧是抽离IScroll核心逻辑,我们这里先在简单层面上探究问题,以免被旁枝末节的BUG困扰,这里形成的一个库只支持纵向滚动,代码量比较少
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">
<meta name="keywords" content="" />
<meta name="description" content="" />
<meta name="robots" content="all" />
<style type="text/css">
* { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }
html { -ms-touch-action: none; }
body, ul, li { padding: 0; margin: 0; border: 0; }
body { font-size: 12px; font-family: ubuntu, helvetica, arial; overflow: hidden; /* this is important to prevent the whole page to bounce */ }
#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; }
#footer { position: absolute; z-index: 2; bottom: 0; left: 0; width: 100%; height: 48px; background: #444; padding: 0; border-top: 1px solid #444; }
#wrapper { position: absolute; z-index: 1; top: 45px; bottom: 48px; left: 0; width: 100%; background: #ccc; overflow: hidden; }
#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; }
#scroller ul { list-style: none; padding: 0; margin: 0; width: 100%; text-align: left; }
#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; }
</style>
</head>
<body>
<div id="header">
iScroll</div>
<div id="wrapper">
<div id="scroller">
<ul>
<li>Pretty row 1</li>
<li>Pretty row 2</li>
<li>Pretty row 3</li>
<li>Pretty row 4</li>
<li>
<input type="text"></li>
<li>Pretty row 6</li>
<li>Pretty row 7</li>
<li>Pretty row 8</li>
<li>
<input type="checkbox"></li>
<li>Pretty row 10</li>
<li>Pretty row 11</li>
<li>Pretty row 12</li>
<li>
<input type="radio"></li>
<li>Pretty row 14</li>
<li>Pretty row 15</li>
<li>Pretty row 16</li>
<li>
<textarea></textarea></li>
<li>Pretty row 18</li>
<li>Pretty row 19</li>
<li>Pretty row 20</li>
<li>
<select>
<option>option</option>
</select></li>
<li>Pretty row 22</li>
<li>Pretty row 23</li>
<li>Pretty row 24</li>
<li>Pretty row 25</li>
<li>Pretty row 26</li>
<li>Pretty row 27</li>
<li>Pretty row 28</li>
<li>Pretty row 29</li>
<li>Pretty row 30</li>
<li>Pretty row 31</li>
<li>Pretty row 32</li>
<li>Pretty row 33</li>
<li>Pretty row 34</li>
<li>Pretty row 35</li>
<li>Pretty row 36</li>
<li>Pretty row 37</li>
<li>Pretty row 38</li>
<li>Pretty row 39</li>
<li>Pretty row 40</li>
<li>Pretty row 41</li>
<li>Pretty row 42</li>
<li>Pretty row 43</li>
<li>Pretty row 44</li>
<li>Pretty row 45</li>
<li>Pretty row 46</li>
<li>Pretty row 47</li>
<li>Pretty row 48</li>
<li>Pretty row 49</li>
<li>Pretty row 50</li>
</ul>
</div>
</div>
<div id="footer">
</div>
<script src="../../zepto/zepto.js" type="text/javascript"></script>
<script src="../../fastclick-1.0.0/lib/fastclick.js" type="text/javascript"></script>
<script type="text/javascript" src="c.ui.scroll.js"></script>
<script type="text/javascript"> var s = new IScroll({
wrapper: $('#wrapper'),
scroller: $('#scroller') }); new FastClick(document.body); </script>
</body>
</html>
Demo
var utils = (function () {
var me = {};
var _elementStyle = document.createElement('div').style; //获得需要兼容CSS3前缀
var _vendor = (function () {
var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'];
var transform;
var i = 0;
var l = vendors.length; for (; i < l; i++) {
transform = vendors[i] + 'ransform';
if (transform in _elementStyle) return vendors[i].substr(0, vendors[i].length - 1);
}
return false;
})(); //获取样式(CSS3兼容)
function _prefixStyle(style) {
if (_vendor === false) return false;
if (_vendor === '') return style;
return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
} me.getTime = Date.now || function getTime() { return new Date().getTime(); }; me.addEvent = function (el, type, fn, capture) {
if (el[0]) el = el[0];
el.addEventListener(type, fn, !!capture);
}; me.removeEvent = function (el, type, fn, capture) {
if (el[0]) el = el[0];
el.removeEventListener(type, fn, !!capture);
}; /*
current:当前鼠标位置
start:touchStart时候记录的Y(可能是X)的开始位置,但是在touchmove时候可能被重写
time: touchstart到手指离开时候经历的时间,同样可能被touchmove重写
lowerMargin:y可移动的最大距离,这个一般为计算得出 this.wrapperHeight - this.scrollerHeight
wrapperSize:如果有边界距离的话就是可拖动,不然碰到0的时候便停止
*/
me.momentum = function (current, start, time, lowerMargin, wrapperSize) {
var distance = current - start,
speed = Math.abs(distance) / time,
destination,
duration,
deceleration = 0.0006; destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1);
duration = speed / deceleration; if (destination < lowerMargin) {
destination = wrapperSize ? lowerMargin - (wrapperSize / 2.5 * (speed / 8)) : lowerMargin;
distance = Math.abs(destination - current);
duration = distance / speed;
} else if (destination > 0) {
destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0;
distance = Math.abs(current) + destination;
duration = distance / speed;
} return {
destination: Math.round(destination),
duration: duration
}; }; $.extend(me, {
hasTouch: 'ontouchstart' in window
}); //我们暂时只判断touch 和 mouse即可
$.extend(me.style = {}, {
transform: _prefixStyle('transform'),
transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
transitionDuration: _prefixStyle('transitionDuration'),
transitionDelay: _prefixStyle('transitionDelay'),
transformOrigin: _prefixStyle('transformOrigin')
}); $.extend(me.eventType = {}, {
touchstart: 1,
touchmove: 1,
touchend: 1, mousedown: 2,
mousemove: 2,
mouseup: 2
}); $.extend(me.ease = {}, {
quadratic: {
style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
fn: function (k) {
return k * (2 - k);
}
},
circular: {
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)
fn: function (k) {
return Math.sqrt(1 - (--k * k));
}
},
back: {
style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fn: function (k) {
var b = 4;
return (k = k - 1) * k * ((b + 1) * k + b) + 1;
}
},
bounce: {
style: '',
fn: function (k) {
if ((k /= 1) < (1 / 2.75)) {
return 7.5625 * k * k;
} else if (k < (2 / 2.75)) {
return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
} else if (k < (2.5 / 2.75)) {
return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
} else {
return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
}
}
},
elastic: {
style: '',
fn: function (k) {
var f = 0.22,
e = 0.4; if (k === 0) { return 0; }
if (k == 1) { return 1; } return (e * Math.pow(2, -10 * k) * Math.sin((k - f / 4) * (2 * Math.PI) / f) + 1);
}
}
});
return me;
})(); function IScroll(opts) {
this.wrapper = typeof opts.wrapper == 'string' ? $(opts.wrapper) : opts.wrapper;
this.scroller = typeof opts.scroller == 'string' ? $(opts.scroller) : opts.scroller;
if (!opts.wrapper[0] || !opts.scroller[0]) throw 'param error'; this.wrapper = this.wrapper[0];
this.scroller = this.scroller[0]; //这个属性会被动态改变的,如果这里
this.scrollerStyle = this.scroller.style; this.options = {
//是否具有滚动条
scrollbars: true,
// 其实时期Y的位置
startY: 0,
//超出边界还原时间点
bounceTime: 600,
//超出边界返回的动画
bounceEasing: utils.ease.circular, //超出边界时候是否还能拖动
bounce: true, bindToWrapper: true, //当window触发resize事件60ms后还原
resizePolling: 60,
startX: 0,
startY: 0
}; for (var i in opts) {
this.options[i] = opts[i];
} this.translateZ = ' translateZ(0)'; this.x = 0;
this.y = 0;
this._events = {};
this._init(); //更新滚动条位置
this.refresh(); //更新本身位置
this.scrollTo(this.options.startX, this.options.startY); this.enable(); }; IScroll.prototype = {
_init: function () {
this._initEvents(); //初始化滚动条,滚动条此处需要做重要处理
if (this.options.scrollbars) {
this._initIndicator();
}
},
refresh: function () {
var rf = this.wrapper.offsetHeight; // Force reflow this.wrapperHeight = this.wrapper.clientHeight;
this.scrollerHeight = this.scroller.offsetHeight;
this.maxScrollY = this.wrapperHeight - this.scrollerHeight; this.endTime = 0; this._execEvent('refresh'); this.resetPosition(); },
_initEvents: function (remove) {
var eventType = remove ? utils.removeEvent : utils.addEvent;
var target = this.options.bindToWrapper ? this.wrapper : window; eventType(window, 'orientationchange', this);
eventType(window, 'resize', this); if (utils.hasTouch) {
eventType(this.wrapper, 'touchstart', this);
eventType(target, 'touchmove', this);
eventType(target, 'touchcancel', this);
eventType(target, 'touchend', this);
} else {
eventType(this.wrapper, 'mousedown', this);
eventType(target, 'mousemove', this);
eventType(target, 'mousecancel', this);
eventType(target, 'mouseup', this);
} eventType(this.scroller, 'transitionend', this);
eventType(this.scroller, 'webkitTransitionEnd', this);
eventType(this.scroller, 'oTransitionEnd', this);
eventType(this.scroller, 'MSTransitionEnd', this);
},
_start: function (e) {
if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) {
return;
} var point = e.touches ? e.touches[0] : e, pos;
this.initiated = utils.eventType[e.type]; this.moved = false; this.distY = 0; //开启动画时间,如果之前有动画的话,便要停止动画,这里因为没有传时间,所以动画便直接停止了
this._transitionTime(); this.startTime = utils.getTime(); //如果正在进行动画,需要停止,并且触发滑动结束事件
if (this.isInTransition) {
this.isInTransition = false;
pos = this.getComputedPosition();
var _x = Math.round(pos.x);
var _y = Math.round(pos.y); if (_y < 0 && _y > this.maxScrollY && this.options.adjustXY) {
_y = this.options.adjustXY.call(this, _x, _y).y;
} //移动过去
this._translate(_x, _y);
this._execEvent('scrollEnd');
} this.startX = this.x;
this.startY = this.y;
this.absStartX = this.x;
this.absStartY = this.y;
this.pointX = point.pageX;
this.pointY = point.pageY; this._execEvent('beforeScrollStart'); e.preventDefault(); }, _move: function (e) {
if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
return;
}
e.preventDefault(); var point = e.touches ? e.touches[0] : e,
deltaX = point.pageX - this.pointX,
deltaY = point.pageY - this.pointY,
timestamp = utils.getTime(),
newX, newY,
absDistX, absDistY; this.pointX = point.pageX;
this.pointY = point.pageY; this.distX += deltaX;
this.distY += deltaY;
absDistX = Math.abs(this.distX);
absDistY = Math.abs(this.distY); // 如果一直按着没反应的话这里就直接返回了
if (timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10)) {
return;
} newY = this.y + deltaY; if (newY > 0 || newY < this.maxScrollY) {
newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY;
} if (!this.moved) {
this._execEvent('scrollStart');
} this.moved = true; this._translate(0, newY); if (timestamp - this.startTime > 300) {
this.startTime = timestamp;
this.startX = this.x;
this.startY = this.y;
} },
_end: function (e) { if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
return;
} var point = e.changedTouches ? e.changedTouches[0] : e,
momentumY,
duration = utils.getTime() - this.startTime,
newX = Math.round(this.x),
newY = Math.round(this.y),
distanceX = Math.abs(newX - this.startX),
distanceY = Math.abs(newY - this.startY),
time = 0,
easing = ''; this.isInTransition = 0;
this.initiated = 0;
this.endTime = utils.getTime(); if (this.resetPosition(this.options.bounceTime)) {
return;
} this.scrollTo(newX, newY);
if (!this.moved) {
//click 的情况 this._execEvent('scrollCancel');
return;
} if (duration < 300) { momentumY = utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0);
// newX = momentumX.destination;
newY = momentumY.destination;
time = Math.max(momentumY.duration);
this.isInTransition = 1;
} if (newY != this.y) {
if (newY > 0 || newY < this.maxScrollY) {
easing = utils.ease.quadratic;
} this.scrollTo(newX, newY, time, easing);
return;
} this._execEvent('scrollEnd');
}, _resize: function () {
var that = this; clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(function () {
that.refresh();
}, this.options.resizePolling);
}, _transitionTimingFunction: function (easing) {
this.scrollerStyle[utils.style.transitionTimingFunction] = easing; this.indicator && this.indicator.transitionTimingFunction(easing);
}, //开始或者停止动画
_transitionTime: function (time) {
time = time || 0;
this.scrollerStyle[utils.style.transitionDuration] = time + 'ms'; //滚动条,我们这里只会出现一个滚动条就不搞那么复杂了
this.indicator && this.indicator.transitionTime(time); }, getComputedPosition: function () {
var matrix = window.getComputedStyle(this.scroller, null), x, y; matrix = matrix[utils.style.transform].split(')')[0].split(', ');
x = +(matrix[12] || matrix[4]);
y = +(matrix[13] || matrix[5]); return { x: x, y: y };
}, _initIndicator: function () {
//滚动条
var el = createDefaultScrollbar();
this.wrapper.appendChild(el);
this.indicator = new Indicator(this, { el: el }); this.on('scrollEnd', function () {
this.indicator.fade();
}); var scope = this;
this.on('scrollCancel', function () {
scope.indicator.fade();
}); this.on('scrollStart', function () {
scope.indicator.fade(1);
}); this.on('beforeScrollStart', function () {
scope.indicator.fade(1, true);
}); this.on('refresh', function () {
scope.indicator.refresh();
}); }, //移动x,y这里比较简单就不分离y了
_translate: function (x, y) {
this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; this.x = x;
this.y = y; if (this.options.scrollbars) {
this.indicator.updatePosition();
} }, resetPosition: function (time) {
var x = this.x,
y = this.y; time = time || 0; if (this.y > 0) {
y = 0;
} else if (this.y < this.maxScrollY) {
y = this.maxScrollY;
} if (y == this.y) {
return false;
} this.scrollTo(x, y, time, this.options.bounceEasing); return true;
}, //移动
scrollTo: function (x, y, time, easing) { // //l_wang 必须项目高度的整数
// if (y < 0 && y > this.maxScrollY && this.options.adjustXY) {
// y = this.options.adjustXY.call(this, x, y).y;
// } if (this.options.adjustXY) {
y = this.options.adjustXY.call(this, x, y).y;
} //l_wang 验证该项是否可选
if (this.options.checkSelected) {
y = this.options.checkSelected.call(this, x, y).y;
} easing = easing || utils.ease.circular; this.isInTransition = time > 0; if (!time || easing.style) {
this._transitionTimingFunction(easing.style);
this._transitionTime(time);
this._translate(x, y);
}
}, //统一的关闭接口
disable: function () {
this.enabled = false;
},
//统一的open接口
enable: function () {
this.enabled = true;
}, on: function (type, fn) {
if (!this._events[type]) {
this._events[type] = [];
} this._events[type].push(fn);
}, _execEvent: function (type) {
if (!this._events[type]) {
return;
} var i = 0,
l = this._events[type].length; if (!l) {
return;
} for (; i < l; i++) {
this._events[type][i].call(this);
}
},
destroy: function () {
this._initEvents(true);
this._execEvent('destroy');
this.indicator && this.indicator.destroy(); console.log('destroy') }, _transitionEnd: function (e) {
if (e.target != this.scroller || !this.isInTransition) {
return;
} this._transitionTime();
if (!this.resetPosition(this.options.bounceTime)) {
this.isInTransition = false;
this._execEvent('scrollEnd');
}
}, //事件具体触发点
handleEvent: function (e) {
switch (e.type) {
case 'touchstart':
case 'mousedown':
this._start(e);
break;
case 'touchmove':
case 'mousemove':
this._move(e);
break;
case 'touchend':
case 'mouseup':
case 'touchcancel':
case 'mousecancel':
this._end(e);
break;
case 'orientationchange':
case 'resize':
this._resize();
break;
case 'transitionend':
case 'webkitTransitionEnd':
case 'oTransitionEnd':
case 'MSTransitionEnd':
this._transitionEnd(e);
break;
}
} }; function createDefaultScrollbar() {
var scrollbar = document.createElement('div'),
indicator = document.createElement('div'); scrollbar.style.cssText = 'position:absolute;z-index:9999';
scrollbar.style.cssText += ';width:7px;bottom:2px;top:2px;right:1px';
scrollbar.style.cssText += ';overflow:hidden'; 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';
indicator.style.width = '100%'; scrollbar.appendChild(indicator); return scrollbar;
} function Indicator(scroller, opts) {
this.wrapper = typeof opts.el == 'string' ? document.querySelector(opts.el) : opts.el;
this.indicator = this.wrapper.children[0]; this.wrapperStyle = this.wrapper.style;
this.indicatorStyle = this.indicator.style;
this.scroller = scroller; this.sizeRatioY = 1;
this.maxPosY = 0; this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;
this.wrapperStyle[utils.style.transitionDuration] = '0ms';
//this.wrapperStyle.opacity = '0';
} Indicator.prototype = {
transitionTime: function (time) {
time = time || 0;
this.indicatorStyle[utils.style.transitionDuration] = time + 'ms';
},
transitionTimingFunction: function (easing) {
this.indicatorStyle[utils.style.transitionTimingFunction] = easing;
},
refresh: function () { this.transitionTime(); var r = this.wrapper.offsetHeight; // force refresh this.wrapperHeight = this.wrapper.clientHeight; this.indicatorHeight = Math.max(Math.round(this.wrapperHeight * this.wrapperHeight / (this.scroller.scrollerHeight || this.wrapperHeight || 1)), 8);
this.indicatorStyle.height = this.indicatorHeight + 'px'; this.maxPosY = this.wrapperHeight - this.indicatorHeight;
this.sizeRatioY = (this.scroller.maxScrollY && (this.maxPosY / this.scroller.maxScrollY)); this.updatePosition();
},
destroy: function () {
this.wrapper.remove();
},
updatePosition: function () {
var y = Math.round(this.sizeRatioY * this.scroller.y) || 0;
this.y = y; //不需要兼容方式了
this.indicatorStyle[utils.style.transform] = 'translate(0px,' + y + 'px)' + this.scroller.translateZ; },
fade: function (val, hold) {
if (hold && !this.visible) {
return;
} clearTimeout(this.fadeTimeout);
this.fadeTimeout = null; var time = val ? 250 : 500,
delay = val ? 0 : 300; val = val ? '1' : '0'; this.wrapperStyle[utils.style.transitionDuration] = time + 'ms'; this.fadeTimeout = setTimeout($.proxy(function (val) {
this.wrapperStyle.opacity = val;
this.visible = +val;
}, this), delay); }
}; IScroll.utils = utils;
核心代码
代码中引入了fastclick解决其移动端点击问题,demo效果在此:
http://sandbox.runjs.cn/show/xq2fbetv
基本代码出来了,我们现在来一个个埋坑,首先解决难的问题!
光标跳动/文本框消失
光标跳动是什么现象大家都知道了,至于导致的原因又我们测试下来,即可确定罪魁祸首为:transform,于是我们看看滑动过程中发生了什么
① 每次滑动会涉及到位置的变化
this._translate(0, newY);
② 每次变化会改变transform属性
//移动x,y这里比较简单就不分离y了
_translate: function (x, y) {
this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; this.x = x;
this.y = y; if (this.options.scrollbars) {
this.indicator.updatePosition();
} },
我们这里做一次剥离,将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逻辑处加入以下逻辑
//暂时只考虑input问题,有效再扩展
var el = document.activeElement;
if (el.nodeName.toLowerCase() == 'input') {
el.blur();
this.disable();
setTimeout($.proxy(function () {
this.enable();
}, this), 250);
return;
}
该方案最为简单粗暴,他在我们意图滑动时便直接导致虚拟键盘失效,从而根本不会滑动,便错过了光标跳动的问题
甚至,解决了由于滚动导致的文本框消失问题!!!
其中有一个250ms的延时,这个期间是虚拟键盘隐藏所用时间,这个时间段将不可对IScroll进行操作,该方案实验下来效果还行
其中这个延时在200-300之间比较符合人的操作习惯,不设置滚动区域会乱闪,取什么值各位自己去尝试,测试地址:
http://sandbox.runjs.cn/show/8nkmlmz5
这个方案是我觉得最优的方案,其是否接受还要看产品态度
死磕-重写_translate
_translate是IScroll滑动的总控,这里默认是使用transform进行移动,但若是获取焦点的情况下我们可以具有不一样的方案
在文本框具有焦点是,我们使用top代替transform!
PS:这是个烂方法不建议采用
//移动x,y这里比较简单就不分离y了
_translate: function (x, y) { var el = document.activeElement;
if (el.nodeName.toLowerCase() == 'input') {
this.scrollerStyle['top'] = y + 'px';
} else {
this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
} this.x = x;
this.y = y; if (this.options.scrollbars) {
this.indicator.updatePosition();
} },
该方案被测试确实可行,不会出现光标闪的现象,但是有一个问题却需要我们处理,便是一旦文本框失去焦点,我们要做top与transform的换算
所以这是一个烂方法!!!这里换算事实上也不难,就是将top值重新归还transform,但是整个这个逻辑却是让人觉得别扭
而且我们这里还需要一个定时器去做计算,判断何时文本框失去焦点,整个这个逻辑就是一个字 坑!
//移动x,y这里比较简单就不分离y了
_translate: function (x, y) { var el = document.activeElement;
if (el.nodeName.toLowerCase() == 'input') {
this.scrollerStyle['top'] = y + 'px'; //便需要做距离换算相关清理,一旦文本框事情焦点,我们要做top值还原
if (!this.TimerSrc) {
this.TimerSrc = setInterval($.proxy(function () {
var el = document.activeElement;
if (el.nodeName.toLowerCase() != 'input') { pos = this.getComputedPosition(); var top = $(scroller).css('top');
this.scrollerStyle['top'] = '0px';
console.log(pos); var _x = Math.round(pos.x);
var _y = Math.round(pos.y);
_y = _y + parseInt(top); //移动过去
this._translate(_x, _y); clearInterval(this.TimerSrc);
this.TimerSrc = null;
}
}, this), 20);
}
} else {
this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
} this.x = x;
this.y = y; if (this.options.scrollbars) {
this.indicator.updatePosition();
} },
经测试,该代码可以解决光标跳动问题,但是坑不坑大家心里有数,一旦需要被迫使用定时器的地方,必定会有点坑!测试地址
http://sandbox.runjs.cn/show/v9pno9d8
死磕-文本框消失
文本框消息是由于滚动中产生动画,将文本框搞到区域外了,这个时候一旦我们输入文字,导致input change,系统便会自动将文本定位到中间,而出现文本不可见问题
该问题的处理最好的方案,依旧是方案一,若是这里要死磕,又会有许多问题,方案无非是给文本设置changed事件,或者定时器什么的,当变化时,自动将文本元素
至于IScroll可视区域,楼主这里就不献丑了,因为我基本决定使用方案一了。
步长移动
所谓步长移动便是我一次必须移动一定距离,这个与图片横向轮播功能有点类似,而这类需求在移动端数不胜数,那我们的IScroll应该如何处理才能加上这一伟大特性呢?
去看IScroll的源码,人家都已经实现了,居然人家都实现了,哎,但是我们这里不管他,照旧做我们的事情吧,加入步长功能
PS:这里有点小小的失落,我以为没有实现呢,这样我搞出来肯定没有官方的优雅了!
思路
思路其实很简单,我们若是设置了一个步长属性,暂时我们认为他是一个数字(其实可以是bool值,由库自己计算该值),然后每次移动时候便必须强制移动该属性的整数倍即可,比如:
var s = new IScroll({
wrapper: $('#wrapper'),
scroller: $('#scroller'),
setp: 40
});
这个便要求每次都得移动10px的步长,那么这个如何实现呢?其实实现点,依然是_translate处,我们这里需要一点处理
//移动x,y这里比较简单就不分离y了
_translate: function (x, y, isStep) { //处理步长
if (this.options.setp && !isStep) {
var flag2 = y > 0 ? 1 : -1; //这个会影响后面的计算结果
var top = Math.abs(y);
var mod = top % this.options.setp;
top = (parseInt(top / this.options.setp) * this.options.setp + (mod > (this.options.setp/2) ? this.options.setp : 0)) * flag2;
y = top;
} this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; this.x = x;
this.y = y; if (this.options.scrollbars) {
this.indicator.updatePosition();
} },
这样一改后,每次便要求移动40px的步长,当然,我这里代码写的不是太好,整个效果在此
这里唯一需要处理的就是touchmove了,每次move的时候,我们不应该对其进行步长控制,而后皆可以,这种控制步长的效果有什么用呢?请看这个例子:
双IScroll的问题
todo......
异步DOM加载,不可滑动
todo......
结语
今日耗时比较长了,学习暂时到此,我们下次继续。
【IScroll深入学习】突破移动端黑暗的利器(上)的更多相关文章
- UWP学习记录1-开端
UWP学习记录1-开端 1.背景 针对不同基础的人,学习的路线自然是不同的.这篇文章记录的是我个人的学习路线,或者说笔记.我对自己的技术状态的定义是: A.有很好的windows平台编程基础: B.有 ...
- 用TVM在硬件平台上部署深度学习工作负载的端到端 IR 堆栈
用TVM在硬件平台上部署深度学习工作负载的端到端 IR 堆栈 深度学习已变得无处不在,不可或缺.这场革命的一部分是由可扩展的深度学习系统推动的,如滕索弗洛.MXNet.咖啡和皮托奇.大多数现有系统针对 ...
- 基于SwiperJs的H5/移动端下拉刷新上拉加载更多的效果
最早时,公司的H5项目中曾用过点击一个"加载更多"的DOM元素来实现分页的功能,后来又用过网上有人写的一个上拉加载更多的插件,那个插件是页面将要滚动到底部时就自动请求数据并插入到页 ...
- 基于SwiperJs的H5/移动端下拉刷新上拉加载更多
最早时,公司的H5项目中曾用过点击一个"加载更多"的DOM元素来实现分页的功能,后来又用过网上有人写的一个上拉加载更多的插件,那个插件是页面将要滚动到底部时就自动请求数据并插入到页 ...
- (转)Android学习-使用Async-Http实现图片压缩并上传功能
(转)Android学习-使用Async-Http实现图片压缩并上传功能 文章转载自:作者:RyaneLee链接:http://www.jianshu.com/p/940fc7ba39e1 让我头疼一 ...
- 端到端TVM编译器(上)
端到端TVM编译器(上) 摘要 将机器学习引入到各种各样的硬件设备中.AI框架依赖于特定于供应商的算子库,针对窄范围的服务器级gpu进行优化.将工作负载部署到新平台,例如手机.嵌入式设备和加速器(例如 ...
- Laravel学习笔记(三)--在CentOS上配置Laravel
在Laravel框架上开发了几天,不得不说,确实比较优雅,处理问题逻辑比较清楚. 今天打算在CentOS 7上配置一个Laravel,之前都是在本机上开发,打算实际配置一下. 1)系统 ...
- Node.js:上传文件,服务端如何获取文件上传进度
内容概述 multer是常用的Express文件上传中间件.服务端如何获取文件上传的进度,是使用的过程中,很常见的一个问题.在SF上也有同学问了类似问题<nodejs multer有没有查看文件 ...
- node.js服务端程序在Linux上持久运行
如果要想在服务端部署node.js程序,让其持久化运行,就不能单单使用npm start命令运行,当然了,这样运行是毫无问题的,但是当关闭xshell窗口或者是关闭进程的时候(其实关闭xshell窗口 ...
随机推荐
- FreeBSD_11-系统管理——{Part_8 - IPFW}
内核支持 方式一:静态編译进内核 options IPFIREWALL # enables IPFW options IPFIREWALL_VERBOSE # enables logging for ...
- 谈谈StringBuffer和StringBuilder
(1) 速度 在执行速度方面的比较:StringBuilder > StringBuffer > String ①String 是不可变的对象(String类源码中存放字符的数组被声明为f ...
- 弥补学生时代的遗憾~C#注册表情缘
记得当时刚接触C#的时候,喜欢编写各种小软件,而注册表系列和网络系列被当时的我认为大牛的必备技能.直到我研究注册表前一天我都感觉他是那么的高深. 今天正好有空,于是就研究了下注册表系列的操作,也随手封 ...
- C#设计模式系列:模板方法模式(Template Method)
你去银行取款的时候,银行会给你一张取款单,这张取款单就是一个模板,它把公共的内容提取到模板中,只留下部分让用户来填写.在软件系统中,将多个类的共有内容提取到一个模板中的思想便是模板方法模式的思想. 模 ...
- C#设计模式系列:访问者模式(Visitor)
1.访问者模式简介 1.1>.定义 作用于某个对象群中各个对象的操作,可以使在不改变对象本身的情况下,定义作用于对象的新操作. 1.2>.使用频率 低 2.访问者模式结构 2.1> ...
- WPF 子窗体关闭,刷新父窗体
父窗体代码 private void DGUserEdit() { if(DGUser.SelectedItem!=null) { DataRow dr = (DGUser.SelectedItem ...
- .Net 转战 Android 4.4 日常笔记(9)--常用组件的使用方法[附源码]
经过两天的学习,把常用的组件都学习了一遍,并做成了App 学习可能真没有捷径,跟学习html有点类似,都是一个控件一个控件学习并使用,最后拼凑成一个系统 链接:http://pan.baidu.com ...
- 应用程序框架实战三十:表现层及ASP.NET MVC介绍(一)
本文将介绍表现层及ASP.NET MVC的一些要点,特别是ASP.NET MVC的一些抽象和封装技巧,如果你对MVC还不了解,可以参考<ASP.NET MVC4 高级编程>,作者Jon G ...
- Oracle_多表查询
SQL多表查询 等值和不等值连接查询 从多个表中获取数据:如果在查询的时候,直接从多个表中获取数据.没有添加条件判断,会出现"笛卡尔积"错误 笛卡尔积错误 笛卡尔集会在下面条件下产 ...
- Notes:indexedDB使用
indexedDB是浏览器端保存结构化数据的一种数据库,类似于mysql,oracle等数据库,但indexedDB使用对象存储数据,而不是用表. indexedDB是全局的宿主对象,使用window ...