Picker组件的惯性滑动实现

news/2025/3/12 0:26:32/文章来源:https://www.cnblogs.com/issai/p/18766558

前两天修复了 tdesign-miniprogram 的一个 bug,Picker 组件快速滑动的时候会有重影。具体表现在:缓慢滑动的时候,Pickeritem 在抖动。一开始拿到问题猜测是不是 onTouchStart 的时候频繁 setData 导致的性能问题,后来研究了一下 onTouchStart 的实现,发现不是这个原因,原本的逻辑如下:

<viewclass="{{classPrefix}}__wrapper"style="transition: transform {{ duration }}ms cubic-bezier(0.215, 0.61, 0.355, 1); transform: translate3d(0, {{ offset }}px, 0)">...</view>
onTouchMove(event) {const { pickItemHeight } = this.data;const { StartY, StartOffset } = this;// touch偏移增量const touchDeltaY = event.touches[0].clientY - StartY;const deltaY = this.calculateViewDeltaY(touchDeltaY, pickItemHeight);this.setData({offset: range(StartOffset + deltaY, -(this.getCount() * pickItemHeight), 0),duration: DefaultDuration, // DefaultDuration: 240});},/*** 将屏幕滑动距离换算为视图偏移量 模拟渐进式滚动* @param touchDeltaY 屏幕滑动距离*/calculateViewDeltaY(touchDeltaY: number, itemHeight: number): number {return Math.abs(touchDeltaY) > itemHeight ? 1.2 * touchDeltaY : touchDeltaY;}

问题很明显了,onTouchMove 频繁触发,每次计算出新的 offset 然后给 itemstyle,但是 duration240msonTouchMove 会在 240ms 内触发多次,导致上次的过度效果还没完成,又会重新进入新的过渡从而导致抖动。

原有的逻辑是通过滚动超过原本 1.2 * itemHeight,然后在 240ms 内完成过度来实现渐进式滚动的。那么有没有其它方法来实现渐进式滚动,网上找了一下资料,渐进式滚动是类似于 iOS 上滑动后手离开屏幕还会有一段距滚动的效果,这个就好比自然界中的惯性效果,更加符合人的直觉。

惯性滚动的核心部分在于两个点:

  1. 惯性滚动的速度线性减慢直到停止
  2. 滚动的距离和速度取决于手滑动的速度

第一个好理解,给一个负的加速度。第二点在于如何来判断滚动的速度快慢:即手在屏幕上滑动的距离,和持续的时间,决定了惯性速度的快慢。换句话说手在短时间内滑动很大一段距离,就代表滑动的快。还有一点需要注意,持续的时间越短速度相对越大,如果停留的时间非常短或者是缓慢滑动是不应该执行惯性滑动的,一般认为停留的时间大于 300ms 且滑动距离大于 15 才执行。

理解了这个核心思路,就该用代码实现效果了。滚动的实现离不开三个方法,onTouchStartonTouchMoveonTouchEnd

onTouchStart:记录第一次触摸屏幕的 timestamp
onTouchMove:实时更新 offset
onTouchEnd:判断是否需要惯性滚动,如果需要计算滚动速度并执行滚动。

// 动画持续时间
const ANIMATION_DURATION = 1000;
// 和上一次move事件间隔小于INERTIA_TIME
const INERTIA_TIME = 300;
// 且距离大于`MOMENTUM_DISTANCE`时,执行惯性滚动
const INERTIA_DISTANCE = 15;const range = function (num: number, min: number, max: number) {return Math.min(Math.max(num, min), max);
};const momentum = (distance: number, duration: number) => {let nDistance = distance;// 惯性滚动的速度const speed = Math.abs(nDistance / duration);// 加速度经验值: 0.005// 惯性滚动的距离,注意在上下滑动时偏移量是有正负的nDistance = (speed / 0.005) * (nDistance < 0 ? -1 : 1);return nDistance;
};
lifetimes = {created() {this.StartY = 0;this.StartOffset = 0;this.startTime = 0;},
};
methods = {onTouchStart(event) {this.StartY = event.touches[0].clientY;this.StartOffset = this.data.offset;this.startTime = Date.now();this.setData({duration: 0,});},onTouchMove(event) {const { StartY, StartOffset } = this;const { pickItemHeight } = this.data;// 偏移增量const deltaY = event.touches[0].clientY - StartY;const newOffset = range(StartOffset + deltaY, -(this.getCount() * pickItemHeight), 0);this.setData({offset: newOffset,});},onTouchEnd(event) {const { offset, labelAlias, valueAlias, columnIndex, pickItemHeight } = this.data;const { options } = this.properties;const { startTime } = this;if (offset === this.StartOffset) {return;}// 判断是否需要惯性滚动let distance = 0;const move = event.changedTouches[0].clientY - this.StartY;const moveTime = Date.now() - startTime;if (moveTime < INERTIA_TIME && Math.abs(move) > INERTIA_DISTANCE) {distance = momentum(move, moveTime);}// 调整偏移量const newOffset = range(offset + distance, -this.getCount() * pickItemHeight, 0);const index = range(Math.round(-newOffset / pickItemHeight), 0, this.getCount() - 1);this.setData({offset: -index * pickItemHeight,duration: ANIMATION_DURATION,curIndex: index,});if (index === this._selectedIndex) {return;}this._selectedIndex = index;// ... 省略事件触发逻辑},
}

另外改了组件逻辑,还得同步该一下测试用例,原本测试用例模拟渐进滚动用的 1.2 * pickItemHeight,新的逻辑会导致部分用例不通过。顺便了解了组件的测试用例方法:大致就是验证 stylepropsevent。在 event 这里用 dispatchEvent('tap') 来模拟点击,.dispatchEvent(touchMove, { touches: [{ x, y }]}) 模拟滚动,同时还要注意要给动画执行时间,比如 sleep(1000) 后再去 expect

最后还有一点不足的是,在 onTouchEnd 后会立马算出 selectedIndex,导致最终的滚动结位置的 item 有了 active 效果,但是滚动还在进行中,视觉上稍微有点不统一,暂时没有好的解决办法。

看了一圈小程序组件库的 Picker 组件,很多都没有渐进式滚动效果,体验效果属实一般,动画直来直往,往往细节决定了用户的体验好坏。效果最好的一个是 NutUI,不仅有合理的动画效果,而且因为跨端的原因,在编译成小程序后用的微信小程序原生 Picker,滚动过程中会有震动的效果,这是原生 Picker 才有的能力。还有一点,在 active 效果的实现上,Picker 上覆盖率一层 mask,中间的一项始终保持 active 效果,这样就不用计算 selectedIndex 了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/897496.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《Python极客编程 : 用代码探索世界》 | PDF免费下载

16个有趣的编程项目,项目式的学习方法,有趣案例提升Python编程技能,培养解决现实问题的思维,附赠源代码,适合零基础计算机小白入门学习。点击下载书籍信息 作者: [美]李沃恩(Lee Vaughan) 出版社: 人民邮电出版社 副标题: 用代码探索世界 译者: 王海鹏 出版年: 2022-8-1…

密度大于0.9408

题目: from Crypto.Util.number import * import random from Crypto.Cipher import AES import hashlib from Crypto.Util.Padding import pad from secrets import flaglist = [] bag = [] p=random.getrandbits(64) assert len(bin(p)[2:])==64 for i in range(4):t = pa=[g…

背包密码密度的作用(LLL算法的实现)

背包问题通解: 当密度d满足以下条件时,LLL算法能有效地解决这类背包问题![](https://cdn.nlark.com/yuque/0/2025/png/49294098/1739349426771-401f2fa7-aefe-422b-b642-5d8f664c415d.png)其中:len(M)是向量M的长度,即向量中元素的数量 max(Mi)是向量M中的最大元素计算: import…

Elliptic(椭圆)曲线

题目: from Crypto.Util.number import getPrime from libnum import s2n from secret import flagp = getPrime(256) a = getPrime(256) b = getPrime(256) E = EllipticCurve(GF(p),[a,b]) m = E.random_point() G = E.random_point() k = getPrime(256) K = k * G r = getPr…

Twisted Hessian曲线(求a)

题目: from Crypto.Util.number import * from Crypto.Cipher import AES from Crypto.Util.Padding import pad from random import randint import hashlib from secrets import flagdef add_THCurve(P, Q):if P == (0, 0):return Qif Q == (0, 0):return Px1, y1 = Px2, y2 …

基于双PI控制器结构的六步逆变器供电无刷直流电机调速simulink仿真

1.课题概述基于双PI控制器结构的六步逆变器供电无刷直流电机调速simulink仿真。双PI控制器是一种结合了两个独立的PI控制器的控制策略,用于提高系统的稳定性和动态性能。2.系统仿真结果 (完整程序运行后无水印) 3.核心程序与模型 版本:MATLAB2022a 4.系统原理简介 4.1 无…

第十课 从人类反馈中引导强化学习

GPT-1GPT-2(至少要三个支持是因为网络上的垃圾信息很多,有了支持能够显著减少垃圾信息的量)GPT-2首次实现了零样本学习。零样本学习是指模型在从未接触过特定任务的训练数据 、无需额外示例 、无需调整参数(梯度更新)的情况下,直接完成任务的能力。例如,用户只需给模型一…

Olive直播管理系统

在校园教学与在线教育场景中,师生对实时音视频传输的需求日益增长。传统直播工具往往缺乏定制化功能,且难以满足多平台流分发、低延迟交互等教育场景的特殊需求。因此,本软件旨在打造一款轻量级、高稳定性的直播管理工具,专注于解决以下问题:简化直播流程:教师可快速搭建…

Redis--Lesson05--Redis进阶

一.Redis中的事务 在Redis中,单条命令依旧保持原子性,但是对于事务来说(命令集)不保证原子性 Redis事务的本质:一组命令的集合,一个事务中所有的命令都会被序列化,在事务的执行过程中,会按照顺序执行,一次性,顺序性,排他性!执行一些命令 如:--- 队列 set1,set2,…

基于入侵野草算法的KNN分类优化matlab仿真

1.程序功能描述 基于入侵野草算法的KNN分类优化。其中,入侵野草算法是一种启发式优化算法,它模拟了自然界中野草的扩散与竞争过程。该算法通过一系列的步骤来寻找样板的最优特征,参与KNN的分类训练和测试。 2.测试软件版本以及运行结果展示MATLAB2022A版本运行 (完…

PowerShell实现全屏七彩渐变 呼吸 屏保

引言 想做一下屏幕保护程序的效果-----全屏颜色渐变,类似呼吸灯的效果。就用Windows自带的PowerShell脚本。脚本预设好了七彩颜色,然后循环变化。首先 我们先实现七彩循环切换的全屏效果,也就是不带渐变。 要想实现全屏颜色填充,必须借助"窗口"。对于PowerShell而…

三剑客与正则系列-awk勇闯天下

1.awk概述四剑客 特点 擅长find 查找文件 查找文件,与其他命令配合.grep/egrep 过滤 过滤速度最快.sed 过滤,取行,替换,删除 替换,修改文件内容,取行.awk 过滤,取行,取列,统计计算,判断,循环 ... 取列,取行,统计计算awk是一个语言,叫做单行脚本.2.概述 2.1.格式 取出/etc/passw…