forked from yejiaming/scroll
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
3 changed files
with
457 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,89 @@ | ||
# scroll | ||
超流畅移动端自定义滚动 | ||
# JS实现一个scroll自定义滚动效果 | ||
在公司开发项目的时候,原生滚动条中有些东西没办法自定义去精细的控制,于是开发一个类似于better-scroll一样的浏览器滚动监听的JS实现,下面我们就来探究一下自定义滚动需要考虑哪些东西,经过哪些过程。 | ||
##选择滚动监听的事件 | ||
因为是自定义手机端的滚动事件,那我选择的是监听手机端的三个touch事件来实现监听,并实现了两种滚动效果,一种是通过-webkit-transform,一种是通过top属性。两种实现对于滚动的基本效果够能达到,可是top的不适合滚动中还存在滚动,可是能解决滚动中存在postion:fixed属性的问题;而transform可以实现滚动中有滚动,可是又不能解决postion:fixed的问题,所以,最后选择性考虑使用哪一种实现方式,用法一样。 | ||
##主要的实现业务逻辑 | ||
|
||
##用法 | ||
用法其实很简单,只要在需要使用自定义滚动条的地方加一个v-scroll指令即可,这是针对vue项目来说,原生项目也类似做相应的修改即可。 | ||
```js | ||
handleTouchMove(event){ | ||
event.preventDefault(); | ||
this.currentY = event.targetTouches[0].screenY; | ||
this.currentTime = new Date().getTime(); | ||
// 二次及以上次数滚动(间歇性滚动)时间和路程重置计算,0.05是间歇性滚动的停顿位移和时间比 | ||
if (Math.abs(this.currentY - this.lastY) / Math.abs(this.currentTime - this.lastTime) < 0.05) { | ||
this.startTime = new Date().getTime(); | ||
this.resetY = this.currentY; | ||
} | ||
this.distance = this.currentY - this.startY; | ||
let temDis = this.distance + this.oldY; | ||
/*设置移动最小值*/ | ||
temDis = temDis > this.minValue ? temDis * 1 / 3 : temDis; | ||
/*设置移动最大值*/ | ||
temDis = temDis < -this.maxValue ? -this.maxValue + (temDis + this.maxValue) * 1 / 3 : temDis; | ||
this.$el.style["top"] = temDis + 'px'; | ||
this.lastY = this.currentY; | ||
this.lastTime = this.currentTime; | ||
this.dispatchEvent(); | ||
this.scrollFunc(event); | ||
}, | ||
``` | ||
**代码解读:**这是监听touchmove事件的回调,其中主要计算出目标节点**this.$el**的**top**或者-webkit-transform中**translateY**的值,而计算的参考主要以事件节点的screenY的垂直移动距离为参考,当然其中还要判断一下最大值和最小值,为了保证移动可以的超出最大值小值一定的距离所以加了一个**1/3**的移动计算。这里可能主要到了有一个**间歇性滚动**的判断和计算,主要是服务于**惯性滚动**的,目的是让惯性滚动的值更加精确。 | ||
|
||
|
||
```js | ||
handleTouchEnd(event){ | ||
/*点透事件允许通过*/ | ||
if (!this.distance) return; | ||
event.preventDefault(); | ||
let temDis = this.distance + this.oldY; | ||
/*计算缓动值*/ | ||
temDis = this.computeSlowMotion(temDis); | ||
/*设置最小值*/ | ||
temDis = temDis > this.minValue ? this.minValue : temDis; | ||
/*设置最大值*/ | ||
temDis = temDis < -this.maxValue ? -this.maxValue : temDis; | ||
this.$el.style["transitionDuration"] = '500ms'; | ||
this.$el.style["transitionTimingFunction"] = 'ease-out'; | ||
/*确定最终的滚动位置*/ | ||
setTimeout(()=> { | ||
this.$el.style["top"] = temDis + 'px'; | ||
}, 0); | ||
// 判断使用哪一种监听事件 | ||
if (this.slowMotionFlag) { | ||
this.dispatchEventLoop(); | ||
} else { | ||
this.dispatchEvent(); | ||
} | ||
this.$el.addEventListener('transitionend', ()=> { | ||
window.cancelAnimationFrame(this.timer); | ||
}); | ||
this.scrollFunc(event); | ||
} | ||
``` | ||
**代码解读:**这是touchend事件监听的回调,其中这里要判断是否要拦截click和tap事件,并且这里还要计算**惯性缓动值**,设置最终的最大最小值,以及设置动画效果和缓动效果。下面来谈一下滚性滚动的计算: | ||
|
||
```js | ||
// 计算惯性滚动值 | ||
computeSlowMotion(temDis){ | ||
var duration = new Date().getTime() - this.startTime; | ||
// 300毫秒是判断间隔的最佳时间 | ||
var resetDistance = this.currentY - this.resetY; | ||
if (duration < 300 && Math.abs(resetDistance) > 10) { | ||
var speed = Math.abs(resetDistance) / duration, | ||
destination; | ||
// 末速度为0 距离等于初速度的平方除以2倍加速度 | ||
destination = (speed * speed) / (2 * this.deceleration) * (resetDistance < 0 ? -1 : 1); | ||
this.slowMotionFlag = true; | ||
return temDis += destination; | ||
} else { | ||
this.slowMotionFlag = false; | ||
return temDis; | ||
} | ||
}, | ||
``` | ||
**代码解读**:滚性滚动的算法主要是根据一个路程和时间计算出初速度,以及原生滚动的加速度的大于值0.006来计算滚动的总位移。这里主要还要判断一下一个300ms的经验值。 | ||
|
||
##总结 | ||
大概的流程和思考就是这样了,后续还会增加更多的功能进行扩展,下面附上git地址:[https://github.com/yejiaming/scroll](https://github.com/yejiaming/scroll) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
/** | ||
* Created by yejiaming on 2017/10/14. | ||
* Scroll自定义滚动事件 | ||
*/ | ||
export function Scroll(el) { | ||
var me = this; | ||
me.init(el); | ||
me.forbiddenScroll(me.getStyleTargetDom(me.$el, 'overflowY', ['scroll', 'auto'])); | ||
me.setMaxValue(); | ||
me.createRequestAnimFrame(); | ||
me.$el.addEventListener('touchstart', me.handleTouchStart.bind(me), false); | ||
me.$el.addEventListener('touchmove', me.handleTouchMove.bind(me), false); | ||
me.$el.addEventListener('touchend', me.handleTouchEnd.bind(me), false); | ||
} | ||
|
||
Scroll.prototype = { | ||
// 初始化参数 | ||
init(el){ | ||
this.$el = el; | ||
this.startTime = 0; | ||
this.startY = 0; | ||
this.oldY = null; | ||
this.$event = this.createEvent(); | ||
this.currentY = null; | ||
this.distance = null; | ||
this.deceleration = 0.0006; | ||
this.minValue = 0; | ||
this.resetY = null; | ||
this.slowMotionFlag = false; | ||
this.maxValue = null; | ||
this.currentTime = null; | ||
this.lastY = 0; | ||
this.lastTime = 0; | ||
this.timer = null; | ||
el.style['position'] = 'absolute'; | ||
el.style['width'] = '100%'; | ||
}, | ||
// requestAnimFrame重绘监听初始化 | ||
createRequestAnimFrame(){ | ||
/** | ||
* 监听页面重绘的监听兼容性写法,一般配合动画效果 | ||
*/ | ||
window.requestAnimFrame = (function () { | ||
return window.requestAnimationFrame || | ||
window.webkitRequestAnimationFrame || | ||
window.mozRequestAnimationFrame || | ||
window.oRequestAnimationFrame || | ||
window.msRequestAnimationFrame || | ||
function (callback) { | ||
window.setTimeout(callback, 1000 / 60); | ||
}; | ||
})(); | ||
/** | ||
* 取消页面重绘监听的兼容性写法 | ||
*/ | ||
window.cancelAnimationFrame = (function () { | ||
return window.cancelAnimationFrame || | ||
window.webkitCancelAnimationFrame || | ||
window.mozCancelAnimationFrame || | ||
window.oCancelAnimationFrame || | ||
function (timer) { | ||
window.clearTimeout(timer); | ||
}; | ||
})(); | ||
}, | ||
// 禁用原生滚动 | ||
forbiddenScroll(scrollTarge){ | ||
var me = this; | ||
// IE和webkit下鼠标滚动事件 | ||
scrollTarge.addEventListener('mousewheel', function (e) { | ||
me.scrollFunc(e); | ||
}); | ||
//火狐下的鼠标滚动事件 | ||
scrollTarge.addEventListener('DOMMouseScroll', function (e) { | ||
me.scrollFunc(e); | ||
}); | ||
document.documentElement.style['overflow'] = 'hidden'; // 禁用根节点(html)的滚动条 | ||
}, | ||
// 设置最大值 | ||
setMaxValue(){ | ||
var me = this; | ||
var hiddenDom = me.getStyleTargetDom(me.$el, 'overflowY', 'hidden'); | ||
var hdHight = me.getPxValue(document.defaultView.getComputedStyle(hiddenDom)['height']); | ||
var clientHeight = Math.abs(hdHight) > 0 ? Math.abs(hdHight) : document.documentElement.clientHeight; | ||
me.maxValue = me.$el.clientHeight - clientHeight; | ||
}, | ||
// 获取想要的参数的目标节点,并指定属性以及属性值 | ||
getStyleTargetDom: function (element, attr, value) { | ||
let currentNode = element; | ||
while (currentNode && currentNode.tagName !== 'HTML' && | ||
currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) { | ||
let target = document.defaultView.getComputedStyle(currentNode)[attr]; | ||
if (value && value.indexOf(target) > -1) { | ||
return currentNode; | ||
} | ||
currentNode = currentNode.parentNode; | ||
} | ||
return window; | ||
}, | ||
// 阻止事件的冒泡 | ||
scrollFunc: function (e) { | ||
e = e || window.event; | ||
if (e && e.stopPropagation) { | ||
// e.preventDefault(); | ||
e.stopPropagation(); | ||
} else { | ||
e.returnvalue = false; | ||
return false; | ||
} | ||
}, | ||
// 获取dom节点的top的值 | ||
getTop(dom){ | ||
var transformString = dom.style.top; | ||
if (transformString) { | ||
return this.getPxValue(transformString); | ||
} else { | ||
return 0; | ||
} | ||
}, | ||
// 获取100px中100的值 | ||
getPxValue(str){ | ||
return Number(String(str).match(/\+?\-?\d+/g)[0]); | ||
}, | ||
// 创建事件 | ||
createEvent(){ | ||
return new Event('y-scroll'); | ||
}, | ||
// 分发事件循环 | ||
dispatchEventLoop(){ | ||
this.timer = window.requestAnimFrame(()=> { | ||
this.dispatchEvent(); | ||
this.dispatchEventLoop(); | ||
}); | ||
}, | ||
// 分发事件 | ||
dispatchEvent(){ | ||
this.$event.scrollTop = this.$el.getBoundingClientRect().top; | ||
this.$el.dispatchEvent(this.$event); | ||
}, | ||
// 计算缓动值 | ||
computeSlowMotion(temDis){ | ||
var duration = new Date().getTime() - this.startTime; | ||
// 300毫秒是判断间隔的最佳时间 | ||
var resetDistance = this.currentY - this.resetY; | ||
if (duration < 300 && Math.abs(resetDistance) > 10) { | ||
var speed = Math.abs(resetDistance) / duration, | ||
destination; | ||
// 初速度为0 距离等于速度的平方除以2倍加速度 | ||
destination = (speed * speed) / (2 * this.deceleration) * (resetDistance < 0 ? -1 : 1); | ||
this.slowMotionFlag = true; | ||
return temDis += destination; | ||
} else { | ||
this.slowMotionFlag = false; | ||
return temDis; | ||
} | ||
}, | ||
handleTouchStart(event){ | ||
this.lastTime = this.startTime = new Date().getTime(); | ||
this.distance = 0; | ||
this.resetY = this.startY = event.targetTouches[0].screenY; | ||
/*每次移动开始时设置初始的oldY的值*/ | ||
this.oldY = this.getTop(this.$el); | ||
this.$el.style["transitionDuration"] = '0ms'; | ||
this.scrollFunc(event); | ||
}, | ||
handleTouchMove(event){ | ||
event.preventDefault(); | ||
this.currentY = event.targetTouches[0].screenY; | ||
this.currentTime = new Date().getTime(); | ||
// 二次及以上次数滚动(间歇性滚动)时间和路程重置计算,0.05是间歇性滚动的停顿位移和时间比 | ||
if (Math.abs(this.currentY - this.lastY) / Math.abs(this.currentTime - this.lastTime) < 0.05) { | ||
this.startTime = new Date().getTime(); | ||
this.resetY = this.currentY; | ||
} | ||
this.distance = this.currentY - this.startY; | ||
let temDis = this.distance + this.oldY; | ||
/*设置移动最小值*/ | ||
temDis = temDis > this.minValue ? temDis * 1 / 3 : temDis; | ||
/*设置移动最大值*/ | ||
temDis = temDis < -this.maxValue ? -this.maxValue + (temDis + this.maxValue) * 1 / 3 : temDis; | ||
this.$el.style["top"] = temDis + 'px'; | ||
this.lastY = this.currentY; | ||
this.lastTime = this.currentTime; | ||
this.dispatchEvent(); | ||
this.scrollFunc(event); | ||
}, | ||
handleTouchEnd(event){ | ||
/*点透事件允许通过*/ | ||
if (!this.distance) return; | ||
event.preventDefault(); | ||
let temDis = this.distance + this.oldY; | ||
/*计算缓动值*/ | ||
temDis = this.computeSlowMotion(temDis); | ||
/*设置最小值*/ | ||
temDis = temDis > this.minValue ? this.minValue : temDis; | ||
/*设置最大值*/ | ||
temDis = temDis < -this.maxValue ? -this.maxValue : temDis; | ||
this.$el.style["transitionDuration"] = '500ms'; | ||
this.$el.style["transitionTimingFunction"] = 'ease-out'; | ||
/*确定最终的滚动位置*/ | ||
setTimeout(()=> { | ||
this.$el.style["top"] = temDis + 'px'; | ||
}, 0); | ||
// 判断使用哪一种监听事件 | ||
if (this.slowMotionFlag) { | ||
this.dispatchEventLoop(); | ||
} else { | ||
this.dispatchEvent(); | ||
} | ||
this.$el.addEventListener('transitionend', ()=> { | ||
window.cancelAnimationFrame(this.timer); | ||
}); | ||
this.scrollFunc(event); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/** | ||
* Created by yejiaming on 2017/10/14. | ||
* Scroll自定义滚动事件 | ||
*/ | ||
export function Scroll(el) { | ||
var me = this; | ||
me.init(el); | ||
me.forbiddenScroll(me.getStyleTargetDom(me.$el, 'overflowY', ['scroll', 'auto'])); | ||
me.setMaxValue(); | ||
me.$el.addEventListener('touchstart', me.handleTouchStart.bind(me), false); | ||
me.$el.addEventListener('touchmove', me.handleTouchMove.bind(me), false); | ||
me.$el.addEventListener('touchend', me.handleTouchEnd.bind(me), false); | ||
} | ||
|
||
Scroll.prototype = { | ||
// 初始化参数 | ||
init(el){ | ||
this.$el = el; | ||
this.startTime = 0; | ||
this.startY = 0; | ||
this.oldY = null; | ||
this.$event = new Event('y-scroll'); | ||
this.currentY = null; | ||
this.distance = null; | ||
this.deceleration = 0.0006; | ||
this.minValue = 0; | ||
this.resetY = null; | ||
this.maxValue = null; | ||
this.currentTime = null; | ||
this.lastY = 0; | ||
this.lastTime = 0; | ||
}, | ||
// 禁用原生滚动 | ||
forbiddenScroll(scrollTarge){ | ||
var me = this; | ||
// IE和webkit下鼠标滚动事件 | ||
scrollTarge.addEventListener('mousewheel', function (e) { | ||
me.scrollFunc(e); | ||
}); | ||
//火狐下的鼠标滚动事件 | ||
scrollTarge.addEventListener('DOMMouseScroll', function (e) { | ||
me.scrollFunc(e); | ||
}); | ||
document.documentElement.style['overflow'] = 'hidden'; // 禁用根节点(html)的滚动条 | ||
}, | ||
// 设置最大值 | ||
setMaxValue(){ | ||
var me = this; | ||
var hiddenDom = me.getStyleTargetDom(me.$el, 'overflowY', 'hidden'); | ||
var hdHight = me.getPxValue(document.defaultView.getComputedStyle(hiddenDom)['height']); | ||
var clientHeight = Math.abs(hdHight) < document.documentElement.clientHeight ? Math.abs(hdHight) : document.documentElement.clientHeight; | ||
me.maxValue = me.$el.clientHeight - clientHeight; | ||
}, | ||
// 获取想要的参数的目标节点,并指定属性以及属性值 | ||
getStyleTargetDom: function (element, attr, value) { | ||
let currentNode = element; | ||
while (currentNode && currentNode.tagName !== 'HTML' && | ||
currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) { | ||
let target = document.defaultView.getComputedStyle(currentNode)[attr]; | ||
if (value && value.indexOf(target) > -1) { | ||
return currentNode; | ||
} | ||
currentNode = currentNode.parentNode; | ||
} | ||
return window; | ||
}, | ||
// 阻止事件的冒泡 | ||
scrollFunc: function (e) { | ||
e = e || window.event; | ||
if (e && e.stopPropagation) { | ||
// e.preventDefault(); | ||
e.stopPropagation(); | ||
} else { | ||
e.returnvalue = false; | ||
return false; | ||
} | ||
}, | ||
// 获取dom节点的TranslateY的值 | ||
getTranslateY(dom){ | ||
var transformString = dom.style.transform; | ||
if (transformString) { | ||
return this.getPxValue(transformString); | ||
} else { | ||
return 0; | ||
} | ||
}, | ||
// 获取100px中100的值 | ||
getPxValue(str){ | ||
return Number(String(str).match(/\+?\-?\d+/g)[0]); | ||
}, | ||
handleTouchStart(event){ | ||
this.lastTime = this.startTime = new Date().getTime(); | ||
this.distance = 0; | ||
this.resetY = this.startY = event.targetTouches[0].screenY; | ||
/*每次移动开始时设置初始的oldY的值*/ | ||
this.oldY = this.getTranslateY(this.$el); | ||
this.$el.style["transitionDuration"] = '0ms'; | ||
this.scrollFunc(event); | ||
}, | ||
handleTouchMove(event){ | ||
event.preventDefault(); | ||
this.currentY = event.targetTouches[0].screenY; | ||
this.currentTime = new Date().getTime(); | ||
// 二次及以上次数滚动(间歇性滚动)时间和路程重置计算,0.05是间歇性滚动的停顿位移和时间比 | ||
if (Math.abs(this.currentY - this.lastY) / Math.abs(this.currentTime - this.lastTime) < 0.05) { | ||
this.startTime = new Date().getTime(); | ||
this.resetY = this.currentY; | ||
} | ||
this.distance = this.currentY - this.startY; | ||
let temDis = this.distance + this.oldY; | ||
/*设置移动最小值*/ | ||
temDis = temDis > this.minValue ? temDis * 1 / 3 : temDis; | ||
/*设置移动最大值*/ | ||
temDis = temDis < -this.maxValue ? -this.maxValue + (temDis + this.maxValue) * 1 / 3 : temDis; | ||
this.$el.style["-webkit-transform"] = 'translateY(' + temDis + 'px)'; | ||
this.lastY = this.currentY; | ||
this.lastTime = this.currentTime; | ||
this.$el.dispatchEvent(this.$event); | ||
this.scrollFunc(event); | ||
}, | ||
handleTouchEnd(event){ | ||
/*点透事件允许通过*/ | ||
if (!this.distance) { | ||
return; | ||
} | ||
event.preventDefault(); | ||
let temDis = this.distance + this.oldY; | ||
/*计算缓动值*/ | ||
var duration = new Date().getTime() - this.startTime; | ||
// 300毫秒是判断间隔的最佳时间 | ||
var resetDistance = this.currentY - this.resetY; | ||
if (duration < 300 && Math.abs(resetDistance) > 10) { | ||
var speed = Math.abs(resetDistance) / duration, | ||
destination; | ||
// 初速度为0 距离等于速度的平方除以2倍加速度 | ||
destination = (speed * speed) / (2 * this.deceleration) * (resetDistance < 0 ? -1 : 1); | ||
temDis += destination; | ||
} | ||
/*设置最小值*/ | ||
if (temDis > this.minValue) { | ||
temDis = this.minValue; | ||
} | ||
/*设置最大值*/ | ||
if (temDis < -this.maxValue) { | ||
temDis = -this.maxValue; | ||
} | ||
this.$el.style["transitionDuration"] = '1000ms'; | ||
this.$el.style["transitionTimingFunction"] = 'ease-out'; | ||
/*确定最终的滚动位置*/ | ||
setTimeout(()=> { | ||
this.$el.style["-webkit-transform"] = 'translateY(' + temDis + 'px)'; | ||
}, 0); | ||
this.$el.dispatchEvent(this.$event); | ||
this.scrollFunc(event); | ||
} | ||
}; |