Skip to content

Commit

Permalink
修改
Browse files Browse the repository at this point in the history
yejiaming committed Oct 16, 2017
1 parent 5f78932 commit 8e7c164
Showing 3 changed files with 457 additions and 4 deletions.
90 changes: 86 additions & 4 deletions README.md
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)


215 changes: 215 additions & 0 deletions scroll-top.js
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);
}
};
156 changes: 156 additions & 0 deletions scroll.js
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);
}
};

0 comments on commit 8e7c164

Please sign in to comment.