Vue3从0到1组件开发-业务组件:列表上下拉动画

467 阅读2分钟

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战

介绍一下

这个组件是公司项目中运用到的一个组件,我接受项目时仅完成了下拉即以及下拉刷新的动画,后来boss觉得滑到底部之后无交互不好看,于是加了上拉的动画,完善用户体验的同时,反馈已经到底部的信息。

光说可能不表达不清楚,看一波gif。

juejinCarousel.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部分的样式处理清除。