我们在 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 应用:拖动阻尼相关推荐

  1. Android回调函数理解

    Android回调函数理解,比如我用一个activity去做显示下载进度的一个进度条,但是下载是另外一个B类来做的,这个时候我Activity获取下载的进度就可以提供一个回调接口,然后让下载类来回调就 ...

  2. Android绘制函数图象及正弦函数的介绍

    零.前言 这篇是为了下一篇做点铺垫,也是来复习一些数学基础 本篇属于休闲娱乐,不要太较真,小科普一下,不喜勿喷 本文知识点前4点你可以随便看看,但第5点非常重要,本文源码见捷文规范 本文知识点: 1) ...

  3. android 回调函数二:应用实例

    前言:如果对android回调的概念不明白的请看:android 回调函数一:基本概念 1.定义接口 package com.app.util;public interface ZYJCallBack ...

  4. android 回调函数一:基本概念

    1.概念 客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数. 一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C ...

  5. Android服务函数远程调用源码分析

    在Android服务查询完整过程源码分析中介绍了客户进程向ServiceManager进程查询服务的完整过程,ServiceManager进程根据服务名称在自身维护的服务链表中查找ServiceMan ...

  6. matlab中计算运行时间的函数,【谁能都我一个计算时间的函数?是计算:例如上午08:00...-计算时间的函数-数学-伊凡渴同学...

    概述:本道作业题是伊凡渴同学的课后练习,分享的知识点是计算时间的函数,指导老师为干老师,涉及到的知识点涵盖:[谁能都我一个计算时间的函数?是计算:例如上午08:00...-计算时间的函数-数学,下面是 ...

  7. Mysql函数-数学函数

    文章目录 1.Mysql函数介绍 1.1Mysql函数-数学函数 2.实操练习 1.Mysql函数介绍 函数表示对输入参数值返回一个具有特定关系的值,MySQL提供了大量丰富的函数,在进行数据库管理以 ...

  8. H5鼠标拖动事件(drag)

    H5鼠标拖动事件 一.元素拖动 二.相关事件 1.拖拽元素 (1)dargstart (2)drag (3)dragend 2.目标元素 (1)drop (2)dragover (3)dragente ...

  9. 盘点mysql中的那些函数-----数学与字符串函数(8.31更新)

    盘点mysql中的那些函数 数学函数 平方根与取余 最小整数与最大整数 随机函数---rand 四舍五入函数----round 符号函数 幂运算函数 对数函数 角度和弧度函数 三角函数 字符串函数 字 ...

  10. Oracle函数——数学函数

    数学函数 数学函数用来处理数值数据,主要的数学函数有:绝对值函数.三角函数.对数函数.随机函数等,在有错误产生时,数学函数将会返回空值NULL 1.1绝对值函数ABS(X):返回的是参数X的绝对值 绝 ...

最新文章

  1. 灯泡亮度控制单片机_南航电赛-灯光控制系统
  2. windows中以管理员身份运行cmd
  3. 神经网络 | Mask Scoring R-CNN:实例分割综述
  4. 视觉(5)A Fast Area-Based Stereo Matching Algorithm
  5. smote算法_SMOTE过采样框架+逻辑回归模型案例
  6. 32. My Experiences in the Factories 我在工厂的经历
  7. DockerFile的编写构建镜像步骤,常用命令和案例
  8. trans系列是sci几区_怎么确定SCI论文期刊是几区的
  9. SpringBoot整合EasyExcel实现Excel表格的导出功能
  10. 【数据分析师---数据可视化】第三章:逐步详解操作Tableau进行20+种类图表绘制
  11. 微信小程序如何关联视频号直播
  12. scp:Mac使用方法(文件上传与下载服务器)
  13. TM4C123G学习笔记(1)——配置Keil开发环境(TM4C123G+Keil)
  14. 【Scratch二次开发】06-修改界面字体大小
  15. 最新各大CMS采集资源站网址合集
  16. 计算机网络的发展历程,你真的清楚吗
  17. Gym - 101611G God of Winds 思路思路思路 2017-2018 ACM-ICPC, NEERC, Moscow Subregional Contest
  18. Anaconda 虚拟环境安装部署Tensorflow 2.x版本
  19. 用数据线连接电脑是电脑会对手机强行充电会对电池产生影响
  20. android.graphics.Rect类的详解

热门文章

  1. mipi的dsi全称_高通mipi dsi代码理解
  2. python正弦函数_Python之正弦曲线实现方法分析
  3. i3 10105f和i5 10400f差多少 I3 10105F和I5 10400F区别
  4. 【HTML】使用Vscode快速书写HTML
  5. 获取句子迷的经典语录[Python版本]
  6. android 集成 firebase 推送
  7. 单片机:红外遥控实验(内含红外遥控介绍+硬件原理+软件编程+配置环境)
  8. Input用户输入完后再执行方法
  9. Android ApiDemos示例解析(110):Views-Data Widgets-1. Dialog
  10. java面向对象编程培训_Java面向对象编程简介