android 阻尼函数,数学的 H5 应用:拖动阻尼
我们在 ios 应用(特别是浏览器)中经常看到这样的 “橡皮筋” 效果:当页面滚动到边缘时若继续拖动,其位移变化量和拖动距离成反比;就像橡皮筋一样,拉动的距离越大则继续发生形变的难度越大,也就是所受到的阻尼力越大:
接下来我会基于 vue 和 移动端 Touch 事件实现这样的 “橡皮筋” 效果。
阻尼曲线
以横坐标为 拖动距离,纵坐标为 实际位移 建立坐标轴。如此,符合 “橡皮筋” 效果的数学函数模型并不难找,我在这里提供两个基础函数模型,对数函数 和 幂函数:
各自对应的函数图像趋势大致如下:
为了满足 H5 向下拖动的实际场景,我们需要对函数体进行微调。此外,还需要设置一个 容器高度值
作为被拖动元素的位移最大值的参考。那么函数调整为:
不妨设
,绘制函数图像:
可见曲线差距不大,我们选择基于幂函数
来制作 demo:
如 gif 图所示,在刚开始往下拖动的阶段,元素发生了较大幅度的跳动,这是由于该阶段的函数值
,也就是元素的位移甚至比手指拖动的距离还要大,从而产生不合理的 “跳动”。
使
,借助 WolframAlpha计算引擎 求解得
,因此在
的区间内,
都是比
大的。
换句话说,我们需要 降低函数图像曲线首段的陡度,使元素随手指拖动的变化幅度更加平缓。由于数学水平有限,我在这里仅提供一种比较麻烦的方式 —— 分段线性函数。
以 ios 原生的 “橡皮筋” 效果为参考,经过大量的测试,我刻画出了一套较为合理的分段线性函数:
同样地使
,绘制函数图像:
demo 实际效果:
函数效率
对于 JS 引擎来说,简单的线性四则运算要比复杂的幂函数、对数函数等运算耗时更短,性能损耗更低。但是在拖动阻尼的场景下,由于实现分段线性函数需要利用循环和声明更多的临时变量,代码性能往往比单单调用 Math.pow() 或 Math.log() 方法要低很多。
我对上述中的三种函数模型都分别提供了代码实现及 测试用例:
linear: 分段线性函数,log: 对数函数,pow: 幂函数
性能差距惨不忍睹…
那么,我们能否找出一个合适的数学表达式,既能符合或近似于上面提出的分段线性函数的图像曲线,又能降低性能损耗呢?
曲线拟合
在分段线性函数的图像上取样关键点:
x
0
500
1000
1500
2500
6000
8000
10000
12000
y
0
90
160
210
260
347.5
357.5
367.5
377.5
通过 在线曲线拟合神器,使用 四参数方程模型 拟合曲线,得
如果有条件的话,这里建议使用 matlab 做曲线拟合。
舍去
,其他常数四舍五入,并化简表达式,得
通过 Wolfram Cloud平台 绘制该表达式在
范围的图像曲线:
Prefect!
然而这个表达式是在
的条件下的,我们需要还原
值,最终表达式为
curve: 拟合函数,linear: 分段线性函数,log: 对数函数,pow: 幂函数
多点触控
在元素拖动的交互场景里,实现多点触控其实非常简单,主要围绕 TouchEvent 事件中的
TouchEvent.touches 对象
包含所有当前接触触摸平面的触点的 Touch 对象;
TouchEvent.changedTouches 对象
包含从上一次触摸事件到此次事件过程中状态发生改变的触点的 Touch 对象。譬如某个触点从触摸平面中释放时,touchend 事件中的 changedTouches 对象就会包含该触点;
处理流程如下:
当有新触点接触平面时,touchstart 事件被触发,以 Touch.identifier 为 id 缓存触点起始坐标;
触点移动时,touchmove 事件被触发,根据 id 计算各个触点当前位置与起始坐标的偏移值并求和;
当有触点从平面中释放时,touchend 事件被触发,记录该触点所“贡献”的偏移值,若所有触点都已释放则重置;
代码实现
body, ul {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.wrapper {
position: absolute;
top: 50%;
left: 0;
right: 0;
margin: 0 auto;
height: 80%;
width: 80%;
max-width: 300px;
max-height: 500px;
border: 1px solid #000;
transform: translateY(-50%);
overflow: hidden;
}
.list {
background-color: #70f3b7;
transition-timing-function: cubic-bezier(.165, .84, .44, 1);
}
.list-item {
height: 40px;
line-height: 40px;
width: 100%;
text-align: center;
border-bottom: 1px solid #ccc;
}
class="wrapper"
ref="wrapper"
@touchstart.prevent="onStart"
@touchmove.prevent="onMove"
@touchend.prevent="onEnd"
@touchcancel.prevent="onEnd">
class="list"
ref="scroller"
:style="scrollerStyle">
class="list-item"
v-for="item in list">
{{item}}
new Vue({
el: '#app',
template: '#tpl',
computed: {
list() {
const list = [];
for (let i = 0; i < 100; i++) {
list.push(i);
}
return list;
},
scrollerStyle() {
return {
'transform': `translate3d(0, ${this.offsetY}px, 0)`,
'transition-duration': `${this.duration}ms`,
};
},
},
data() {
return {
wrapper: null,
scroller: null,
minY: 0,
maxY: 0,
wrapperHeight: 0,
offsetY: 0,
duration: 0,
pos: {},
cacheOffsetY: 0,
};
},
mounted() {
this.$nextTick(() => {
this.wrapper = this.$refs.wrapper;
this.scroller = this.$refs.scroller;
const { height: wrapperHeight } = this.wrapper.getBoundingClientRect();
const { height: scrollHeight } = this.scroller.getBoundingClientRect();
this.wrapperHeight = wrapperHeight;
this.minY = wrapperHeight - scrollHeight;
});
},
methods: {
onStart(e) {
this.duration = 0;
this.stop();
// 是否为第一个触点,若是则需要重置 cacheOffsetY 值
let isFirstTouch = true;
Array.from(e.touches).forEach(touch => {
const id = touch.identifier;
if (!this.pos[id]) {
this.pos[id] = touch.pageY;
return;
}
isFirstTouch = false;
});
if (isFirstTouch) {
this.cacheOffsetY = this.offsetY;
}
},
onMove(e) {
let offset = 0;
Array.from(e.touches).forEach(touch => {
const id = touch.identifier;
if (this.pos[id]) {
offset += Math.round(touch.pageY - this.pos[id]);
}
});
offset = this.cacheOffsetY + offset;
// 超出边界时增加阻尼效果
if (offset < this.minY || offset > this.maxY) {
this.offsetY = this.damping(offset, this.wrapperHeight);
} else {
this.offsetY = offset;
}
},
onEnd(e) {
Array.from(e.changedTouches).forEach(touch => {
const id = touch.identifier;
if (this.pos[id]) {
this.cacheOffsetY += Math.round(touch.pageY - this.pos[id]);
}
});
// 当所有触点都离开平面
if (!e.touches.length) {
this.cacheOffsetY = 0;
this.pos = {};
this.resetPosition();
}
},
stop() {
// 获取当前 translate 的位置
const matrix = window.getComputedStyle(this.scroller).getPropertyValue('transform');
this.offsetY = Math.round(+matrix.split(')')[0].split(', ')[5]);
},
// 超出边界时重置位置
resetPosition() {
let offsetY;
if (this.offsetY < this.minY) {
offsetY = this.minY;
} else if (this.offsetY > this.maxY) {
offsetY = this.maxY;
}
if (typeof offsetY !== 'undefined') {
this.offsetY = offsetY;
this.duration = 500;
}
},
// 阻尼函数
damping(x, max) {
let y = Math.abs(x);
y = 0.82231 * max / (1 + 4338.47 / Math.pow(y, 1.14791));
return Math.round(x < 0 ? -y : y);
},
},
});
Reference
android 阻尼函数,数学的 H5 应用:拖动阻尼相关推荐
- Android回调函数理解
Android回调函数理解,比如我用一个activity去做显示下载进度的一个进度条,但是下载是另外一个B类来做的,这个时候我Activity获取下载的进度就可以提供一个回调接口,然后让下载类来回调就 ...
- Android绘制函数图象及正弦函数的介绍
零.前言 这篇是为了下一篇做点铺垫,也是来复习一些数学基础 本篇属于休闲娱乐,不要太较真,小科普一下,不喜勿喷 本文知识点前4点你可以随便看看,但第5点非常重要,本文源码见捷文规范 本文知识点: 1) ...
- android 回调函数二:应用实例
前言:如果对android回调的概念不明白的请看:android 回调函数一:基本概念 1.定义接口 package com.app.util;public interface ZYJCallBack ...
- android 回调函数一:基本概念
1.概念 客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数. 一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C ...
- Android服务函数远程调用源码分析
在Android服务查询完整过程源码分析中介绍了客户进程向ServiceManager进程查询服务的完整过程,ServiceManager进程根据服务名称在自身维护的服务链表中查找ServiceMan ...
- matlab中计算运行时间的函数,【谁能都我一个计算时间的函数?是计算:例如上午08:00...-计算时间的函数-数学-伊凡渴同学...
概述:本道作业题是伊凡渴同学的课后练习,分享的知识点是计算时间的函数,指导老师为干老师,涉及到的知识点涵盖:[谁能都我一个计算时间的函数?是计算:例如上午08:00...-计算时间的函数-数学,下面是 ...
- Mysql函数-数学函数
文章目录 1.Mysql函数介绍 1.1Mysql函数-数学函数 2.实操练习 1.Mysql函数介绍 函数表示对输入参数值返回一个具有特定关系的值,MySQL提供了大量丰富的函数,在进行数据库管理以 ...
- H5鼠标拖动事件(drag)
H5鼠标拖动事件 一.元素拖动 二.相关事件 1.拖拽元素 (1)dargstart (2)drag (3)dragend 2.目标元素 (1)drop (2)dragover (3)dragente ...
- 盘点mysql中的那些函数-----数学与字符串函数(8.31更新)
盘点mysql中的那些函数 数学函数 平方根与取余 最小整数与最大整数 随机函数---rand 四舍五入函数----round 符号函数 幂运算函数 对数函数 角度和弧度函数 三角函数 字符串函数 字 ...
- Oracle函数——数学函数
数学函数 数学函数用来处理数值数据,主要的数学函数有:绝对值函数.三角函数.对数函数.随机函数等,在有错误产生时,数学函数将会返回空值NULL 1.1绝对值函数ABS(X):返回的是参数X的绝对值 绝 ...
最新文章
- 灯泡亮度控制单片机_南航电赛-灯光控制系统
- windows中以管理员身份运行cmd
- 神经网络 | Mask Scoring R-CNN:实例分割综述
- 视觉(5)A Fast Area-Based Stereo Matching Algorithm
- smote算法_SMOTE过采样框架+逻辑回归模型案例
- 32. My Experiences in the Factories 我在工厂的经历
- DockerFile的编写构建镜像步骤,常用命令和案例
- trans系列是sci几区_怎么确定SCI论文期刊是几区的
- SpringBoot整合EasyExcel实现Excel表格的导出功能
- 【数据分析师---数据可视化】第三章:逐步详解操作Tableau进行20+种类图表绘制
- 微信小程序如何关联视频号直播
- scp:Mac使用方法(文件上传与下载服务器)
- TM4C123G学习笔记(1)——配置Keil开发环境(TM4C123G+Keil)
- 【Scratch二次开发】06-修改界面字体大小
- 最新各大CMS采集资源站网址合集
- 计算机网络的发展历程,你真的清楚吗
- Gym - 101611G God of Winds 思路思路思路 2017-2018 ACM-ICPC, NEERC, Moscow Subregional Contest
- Anaconda 虚拟环境安装部署Tensorflow 2.x版本
- 用数据线连接电脑是电脑会对手机强行充电会对电池产生影响
- android.graphics.Rect类的详解
热门文章
- mipi的dsi全称_高通mipi dsi代码理解
- python正弦函数_Python之正弦曲线实现方法分析
- i3 10105f和i5 10400f差多少 I3 10105F和I5 10400F区别
- 【HTML】使用Vscode快速书写HTML
- 获取句子迷的经典语录[Python版本]
- android 集成 firebase 推送
- 单片机:红外遥控实验(内含红外遥控介绍+硬件原理+软件编程+配置环境)
- Input用户输入完后再执行方法
- Android ApiDemos示例解析(110):Views-Data Widgets-1. Dialog
- java面向对象编程培训_Java面向对象编程简介