这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战
介绍一下
这个组件是公司项目中运用到的一个组件,我接受项目时仅完成了下拉即以及下拉刷新的动画,后来boss觉得滑到底部之后无交互不好看,于是加了上拉的动画,完善用户体验的同时,反馈已经到底部的信息。
光说可能不表达不清楚,看一波gif。
可以很清楚的看到,分别包含下拉,下拉刷新图,loading动画,以及上拉的效果。
因为是公司的项目,这里不会贴全部的代码,也并不是规范的组件开发思想,所以文章主要是讲解核心思想以及组件化的部分。
先从结构开始。
内容部分肯定是一个slot交由开发部分去渲染列表了。组件主要是包含下拉、下拉刷新的事件反馈、上拉三大部分。
结构部分的核心是动画,即上下拉的时候位置偏移等处理,在这里是针对父级设置css的transform: translateY()+tranition: transform .2s ease-out来实现动画的。
结构部分的代码如下
<template>
<view>
<view class="refresh-box" @touchstart.stop="refresh.controlTouchstart" @touchmove.stop="refresh.controlTouchmove" @touchend.stop="refresh.controlTouchend">
<view class="load-more-box">
<!-- 刷新图片区 -->
<view class="load-more-control"
:style="{ minHeight: 140 + refreshTop + 'rpx' }">
<!-- loading 图标 -->
<uarea-icons class="refresh-icon" type="home-refresh" color="#c8c8c8" size="30" />
<view class="control-title">放开即可刷新求租</view>
</view>
<view class="load-more-dots hollow-dots-spinner">
<!-- loading 动画 -->
<view class="dot" :style="[{ animationPlayState: playState }]"></view>
<view class="dot" :style="[{ animationPlayState: playState }]"></view>
<view class="dot" :style="[{ animationPlayState: playState }]"></view>
</view>
</view>
<!-- 内容区 -->
<view style="width: 100%;"><slot /></view>
</view>
</view>
</template>
结构比较简单,但是在结构中能看看到,主要是通过touch事件来控制用户的手指情况
因为公司项目是主移动端的,所以代码偏向移动端,但是只要略微调整即可,主要是逻辑部分。
逻辑部分
逻辑部分比较多,但是关于上下拉这部分的代码则相对比较精简,主要是针对三个触摸事件的处理。
controlTouchstart(e) {
if (!this.isRefresh || this.isCurrentRequest) {
return;
}
this.fingerLeave = false;
//清理定时器
this.fingerNum = e.touches.length;
clearTimeout(this.waitMoveDistanceTimer);
this.waitMoveDistanceTimer = 0;
clearTimeout(this.refreshEndTimer);
this.refreshEndTimer = 0;
this.touchClient.startY = e.touches.slice(-1)[0].clientY;
}
controlTouchmove(e) {
if (!this.isRefresh || this.isCurrentRequest) {
return;
}
this.touchClient.endY = e.touches.slice(-1)[0].clientY;
this.moveDistance = this.touchClient.endY - this.touchClient.startY;
this.touchClient.startY = this.touchClient.endY;
if (this.distance < (this.allDistance * 0.94)) {
this.dragNum = 0.45 * (1 - (this.lastMoveDistance / (this.allDistance * 0.94)));
this.distance = Number((this.lastMoveDistance + Math.floor(this.dragNum * this.moveDistance)).toFixed(
2));
this.lastMoveDistance = this.distance;
} else {
this.distance = Number((this.allDistance * 0.94).toFixed(2));
}
//用于计算圆轮滚动的角度
this.tempDistance = Number((this.tempLastMoveDistance + Math.floor((this.dragNum <= 0.1 ? 0.1 : this
.dragNum) *
this.moveDistance)).toFixed(2));
this.tempLastMoveDistance = this.tempDistance;
//显示load-more-control
if (this.distance <= 0) {
if (e.touches.length < 2) {
this.$emit('disableScroll', false);
}
this.dom.getElementsByClassName('refresh-box')[0].style.transform = `translateY(0px)`;
this.dom.getElementsByClassName('refresh-box')[0].classList.remove('refresh-box-transition');
//解开禁止滚动
this.distance = 0;
this.moveDistance = 0;
this.lastMoveDistance = 0;
this.dom.getElementsByClassName('load-more-box')[0].style.top = up2rpx(-140 - this.refreshTop, 1) + "px";
this.dom.getElementsByClassName('refresh-icon')[0].style.transform = "rotate(0deg)";
this.dom.getElementsByClassName('load-more-control')[0].style.height = up2rpx(140 + this.refreshTop, 1) + "px";
return;
} else {
// #ifdef H5
e.preventDefault();
// #endif
this.startRefresh = true;
this.$emit('disableScroll', true);
if (this.distance > 30) {
//锁定禁止滚动
this.dom.getElementsByClassName('load-more-control')[0].classList.add('load-more-transition');
if ((this.distance - 30) / 10 < 1) {
this.dom.getElementsByClassName('load-more-control')[0].style.opacity = (this.distance -
30) / 10;
} else {
this.dom.getElementsByClassName('load-more-control')[0].style.opacity = 1;
}
} else {
this.dom.getElementsByClassName('load-more-control')[0].classList.remove('load-more-transition');
this.dom.getElementsByClassName('load-more-control')[0].style.opacity = 0;
}
}
if (this.distance > up2rpx(140, 1)) {
this.dom.getElementsByClassName('load-more-control')[0].style.height = `${this.distance+up2rpx(this.refreshTop,1)}px`;
this.dom.getElementsByClassName('load-more-box')[0].style.top = `-${this.distance+up2rpx(this.refreshTop,1)}px`;
}
this.dom.getElementsByClassName('refresh-box')[0].style.transform = `translateY(${this.distance}px)`;
this.dom.getElementsByClassName('refresh-icon')[0].style.transform = `rotate(${360*2*this.tempDistance/this.allDistance}deg)`;
}
controlTouchend(e, instance) {
if (!this.fingerLeave) {
this.fingerLeave = true;
this.isRefresh = true;
}
if (!this.isRefresh || this.isCurrentRequest || !this.startRefresh) {
return;
}
if (e.touches.length == 1 && --this.fingerNum > 0) {
return;
}
let refreshTop = this.refreshTop;
if (this.distance <= 30) {
//回弹时触发动画
this.distance = 0;
this.moveDistance = 0;
this.lastMoveDistance = 0;
this.startRefresh = false;
this.dom.getElementsByClassName('refresh-box')[0].classList.add('refresh-box-transition');
this.dom.getElementsByClassName('refresh-icon')[0].classList.add('control-icon');
this.dom.getElementsByClassName('load-more-control')[0].classList.add('load-more-transition');
//0.1s后关闭动画
setTimeout(() => {
if (this.dom.getElementsByClassName('refresh-box')[0]) {
this.dom.getElementsByClassName('refresh-box')[0].classList.remove('refresh-box-transition');
this.dom.getElementsByClassName('load-more-control')[0].classList.remove( 'load-more-transition');
}
if (this.dom.getElementsByClassName('refresh-icon')[0]) {
this.dom.getElementsByClassName('refresh-icon')[0].classList.remove('control-icon');
}
}, 100)
return;
}
this.dom.getElementsByClassName('refresh-box')[0].classList.add('refresh-box-transition');
//设置200ms的脱手时间如果200之内可继续拉升
this.waitMoveDistanceTimer = setTimeout(() => {
this.$emit('disableScroll', false);
this.startRefresh = false;
this.isCurrentRequest = true;
if (this.isHome) {
this.dom.getElementsByClassName('refresh-box')[0].style.transform = `translateY(${up2rpx(100-refreshTop,1)}px)`;
} else {
this.dom.getElementsByClassName('refresh-box')[0].style.transform = `translateY(${up2rpx(100-refreshTop*0.5,1)}px)`;
}
//开启请求加载动画
instance.callMethod('changePlayState', 'running');
clearTimeout(this.waitMoveDistanceTimer);
this.waitMoveDistanceTimer = 0;
}, 200)
}
代码比较长,规则不过大部分是针对图标,以及比例的划算,毕竟不能用户移动多少就拉动多少,那样的话会导致移动过多的。 所以在move事件中换算较多。
而在touchend中则是以样式处理为主,将移动距离,loading部分的样式处理清除。