背景
事情是这样的,本人目前一直从事着移动端H5的研发工作,很多情况下都会涉及到列表页的上拉加载交互。公司内部有自研开源框架better-scroll,也有基于better-scroll的Cube-UI的Scroll组件的实现,但是这些SDK对于我当时的项目来说,均有一定的改造成本,我需要的只是一个可以简单实现页面上拉效果的动画,同时在用户松手的时候执行一个回调函数。
实现效果
ChatGPT问答
ChatGPT是一个非常好用的AI助手,每一个编程人员都应该熟练掌握它。你所需要做的就是捋清思路,以产品经理的角度告诉他你的需求,循序渐进,从功能概括、到细节优化。
帮我用js实现用户滑动页面到底部后,拉扯页面上拉加载的函数
GPT回答:
要实现用户滑动页面到底部后的上拉加载函数,你可以监听
scroll
事件,检查滚动位置和文档高度,然后触发加载操作。以下是一个简单的实例:
javascript
// 设置触发加载的阈值(距离底部多少像素开始加载)
const threshold = 100;
// 标记是否正在加载中,防止重复加载
let isLoading = false;
// 监听滚动事件
window.addEventListener('scroll', function() {
// 文档总高度
const documentHeight = document.documentElement.scrollHeight;
// 当前滚动位置
const scrollPosition = window.innerHeight + window.scrollY;
// 判断是否滑动到底部
if (scrollPosition >= documentHeight - threshold && !isLoading) {
// 触发加载
isLoading = true;
console.log('触发加载数据的操作,比如向服务器请求新数据');
// 模拟加载过程(在实际应用中,这里应该是异步请求数据的地方)
setTimeout(function() {
isLoading = false;
console.log('加载完成');
}, 2000); // 这里模拟加载过程,实际情况应根据异步请求的时间进行调整
}
});
在这个示例中,当用户滑动到页面底部时,触发加载操作。注意,这只是一个简单的示例,实际应用中,你可能需要根据你的后端逻辑进行相应的异步加载操作。
代码很简单,只是帮我检测了用户是否滑动到底部,因此我需要让他知道我想要的是动画实现。
我想要的是这个拉扯动画
理解了,你想要在用户滑动到页面底部时,执行一些拉扯(pull-to-refresh)的动画,类似于一些应用中的上拉加载功能。以下是一个使用简单的HTML和JavaScript实现的例子:
此次对话的关键代码:
javascript
function handleRelease() {
const scrollPosition = window.scrollY;
const threshold = 50;
if (!isRefreshing && scrollPosition >= threshold) {
// 触发刷新动作
isRefreshing = true;
refreshContainer.style.top = '0';
refreshText.innerText = 'Refreshing...';
refreshIcon.innerText = '🔄';
// 模拟异步加载过程
setTimeout(() => {
isRefreshing = false;
refreshContainer.style.top = '-60px';
refreshText.innerText = 'Pull to refresh';
refreshIcon.innerText = '🔄';
}, 2000); // 这里模拟加载过程,实际情况应根据异步请求的时间进行调整
}
}
这个例子包含了一个简单的上拉加载容器
refreshContainer
,当用户上拉时,会出现下拉刷新的提示。你可以根据自己的需求进行更改和定制。在实际应用中,你可能需要根据异步加载的过程进行更复杂的处理。
我想要的是用户触底后,拉扯页面,页面会随用户的手势往上滑动,但是有阻尼感
理解了,你希望在用户触底后,拉扯页面时,页面会跟随用户的手势往上滑动,并且有阻尼感。以下是一个简单的实现,使用了 HTML、CSS 和 JavaScript。
此次对话的关键代码:
javascript
function handleTouchStart(event) {
if (content.scrollTop === content.scrollHeight - content.clientHeight) {
isDragging = true;
startY = event.touches[0].clientY;
}
}
function handleTouchMove(event) {
if (isDragging) {
const deltaY = event.touches[0].clientY - startY;
const translateY = Math.min(0, deltaY);
content.style.transform = `translateY(${translateY}px)`;
}
}
function handleTouchEnd() {
if (isDragging) {
isDragging = false;
content.style.transform = 'translateY(0)';
// 模拟异步加载过程
loaderText.innerText = 'Loading...';
loaderIcon.innerText = '🔄';
setTimeout(() => {
loaderText.innerText = 'Pull to load more';
loaderIcon.innerText = '🔄';
}, 2000); // 这里模拟加载过程,实际情况应根据异步请求的时间进行调整
}
}
这个例子使用了
translateY
属性来实现页面的垂直平移,从而达到拉扯页面的效果。当用户触底并开始拉扯时,页面会根据手势进行垂直平移,并且有阻尼感。在实际应用中,你可以根据需求调整样式和动画效果。
在优化一下,用户拉扯的过程中,页面的滚动距离随用户滑动距离越来越小,阻尼越来越大
理解了,你想要在用户拉扯的过程中,页面的滚动距离随着用户滑动距离增加而减小,即增加阻尼效果。以下是经过调整的代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pull to Load</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
height: 100vh;
overflow: hidden;
transition: transform 0.3s ease-out;
}
#content {
background-color: #f0f0f0;
height: 200vh;
}
#loader {
display: flex;
justify-content: center;
align-items: center;
height: 60px;
background-color: #f0f0f0;
}
</style>
</head>
<body>
<div id="content">
<!-- Your page content goes here -->
</div>
<div id="loader">
<div id="loaderIcon">🔄</div>
<div id="loaderText">Pull to load more</div>
</div>
<script>
const content = document.getElementById('content');
const loader = document.getElementById('loader');
const loaderText = document.getElementById('loaderText');
const loaderIcon = document.getElementById('loaderIcon');
let isDragging = false;
let startY = 0;
let initialTranslateY = 0;
function handleTouchStart(event) {
if (content.scrollTop === content.scrollHeight - content.clientHeight) {
isDragging = true;
startY = event.touches[0].clientY;
initialTranslateY = getTransformTranslateY(content);
}
}
function handleTouchMove(event) {
if (isDragging) {
const deltaY = event.touches[0].clientY - startY;
const translateY = Math.min(0, deltaY / 2 + initialTranslateY); // 调整除数来增加阻尼效果
content.style.transform = `translateY(${translateY}px)`;
}
}
function handleTouchEnd() {
if (isDragging) {
isDragging = false;
// 模拟异步加载过程
loaderText.innerText = 'Loading...';
loaderIcon.innerText = '🔄';
setTimeout(() => {
loaderText.innerText = 'Pull to load more';
loaderIcon.innerText = '🔄';
}, 2000); // 这里模拟加载过程,实际情况应根据异步请求的时间进行调整
// 添加过渡效果,平滑过渡到原始位置
content.style.transition = 'transform 0.3s ease-out';
content.style.transform = 'translateY(0)';
// 在过渡结束后移除过渡属性,以避免影响后续的滚动
content.addEventListener('transitionend', function transitionEnd() {
content.style.transition = '';
content.removeEventListener('transitionend', transitionEnd);
});
}
}
function getTransformTranslateY(element) {
const transform = window.getComputedStyle(element).transform;
const translateYMatch = transform.match(/matrix\(1, 0, 0, 1, 0, (-?\d+)\)/);
return translateYMatch ? parseFloat(translateYMatch[1]) : 0;
}
content.addEventListener('touchstart', handleTouchStart);
content.addEventListener('touchmove', handleTouchMove);
content.addEventListener('touchend', handleTouchEnd);
</script>
</body>
</html>
在这个版本中,我在
handleTouchMove
函数中调整了deltaY
的计算,通过除以一个较大的数值,使得页面的滚动距离在用户滑动距离增加时逐渐减小,达到阻尼效果。你可以根据需要调整这个数值来改变阻尼效果的强弱。
代码整合
经过最后一步问答后,GPT给出的代码运行效果即为文章开头的实现,我们需要对代码进行以下优化:
- 将他给出的代码抽离为一个
class
模块儿 - 将动画实现所使用的
transform
改为position
的变化(对子元素的fixed
定位有影响,transform
会改变屏幕视口)
fixed
属性会创建新的层叠上下文。当元素祖先的transform
、perspective
、filter
或backdrop-filter
属性非none
时,容器由视口改为该祖先。
最终代码:
typescript
class PullLoad {
hitY = 0
startY = 0
container: HTMLElement
isFlick = false
isDragging = false
options = {
bounce: true,
dampingFactor: 4 // 阻尼系数
}
destory = () => { /* */ }
onPullingUp = () => { /* */ }
constructor(container: HTMLElement, onPullingUp?: () => void, options?: { dampingFactor?: number; bounce?: boolean }) {
if (!container) {
return
}
this.container = container
container.style.position = 'relative'
const handleTouchStart = this.handleTouchStart.bind(this)
const handleTouchMove = this.handleTouchMove.bind(this)
const handleTouchEnd = this.handleTouchEnd.bind(this)
const initListener = () => {
container.addEventListener('touchstart', handleTouchStart)
container.addEventListener('touchmove', handleTouchMove)
container.addEventListener('touchend', handleTouchEnd)
}
this.destory = () => {
this.container.removeEventListener('touchstart', handleTouchStart)
this.container.removeEventListener('touchmove', handleTouchMove)
this.container.removeEventListener('touchend', handleTouchEnd)
}
initListener()
Object.assign(this.options, options)
if (onPullingUp) {
this.onPullingUp = onPullingUp
}
}
handleTouchStart (event: TouchEvent) {
this.startY = event.touches[0].clientY
}
handleTouchMove (event: TouchEvent) {
if (event.changedTouches[0].clientY > this.startY) {
return
}
const contentEl = this.container
const documentEl = document.documentElement
const scrollTop = documentEl.scrollTop || window.pageYOffset || document.body.scrollTop // 兼容处理微信安卓版获取不到documentEl.scrollTop的问题
const innerHeight = window.innerHeight
const scrollHeight = documentEl.scrollHeight
const distance = scrollHeight - (scrollTop + innerHeight)
if (distance < 1) { // 部分安卓浏览器(qq浏览器)橡皮筋效果计算后的值大于0小于1,其余均小于或等于0
if (!this.isDragging) {
contentEl.style.transition = ''
// 保证从碰撞开始的位置进行拉扯
this.hitY = event.touches[0].clientY
}
const deltaY = event.touches[0].clientY - this.hitY
const translateY = Math.min(0, deltaY / this.options.dampingFactor) // 调整除数来增加阻尼效果
if (this.options.bounce) {
contentEl.style.top = `${translateY}px`
}
this.isFlick = Math.abs(translateY) <= 10
this.isDragging = true
}
}
handleTouchEnd (event: TouchEvent, onTransitionend?: () => void) {
if (this.isDragging) {
this.isDragging = false
if (!this.isFlick) {
this.onPullingUp()
}
if (this.options.bounce) {
const contentEl = this.container
// 添加过渡效果,平滑过渡到原始位置
contentEl.style.transition = 'top 0.3s ease-out'
contentEl.style.top = '0'
// 在过渡结束后移除过渡属性,以避免影响后续的滚动
contentEl.addEventListener('transitionend', function transitionEnd () {
contentEl.style.transition = ''
contentEl.removeEventListener('transitionend', transitionEnd)
// eslint-disable-next-line no-unused-expressions
onTransitionend?.()
})
}
}
}
}
export default PullLoad
使用方法如下:
typescript
import PullLoad from './pullLoad'
// 初始化
const el: HTMLDivElement = document.querySelector('#container')
const instance = new PullLoad(el, () => {
console.log('callback,用户松手啦!')
})
// 组件卸载的时候
instance.destory()