原生 JavaScript 实现手势解锁组件

澳门新葡亰手机版 5

分成 3 个图层

澳门新葡亰手机版 1

在这里我把 UI 分别绘制在 3 个图层里,对应 3 个
Canvas。最上层只有随着手指头移动的那个线段,中间是九个点,最下层是已经绘制好的线。之所以这样分,是因为随手指头移动的那条线需要不断刷新,底下两层都不用频繁更新,但是把连好的线放在最底层是因为我要做出圆点把线的一部分遮挡住的效果。

澳门新葡亰手机版 2

细节问题

实际手机触屏时,如果上下拖动,浏览器有默认行为,会导致页面上下移动,需要阻止
touchmove 的默认事件。

this.container.addEventListener('touchmove', 
      evt => evt.preventDefault(), {passive: false});

这里仍然需要注意的一点是, touchmove 事件在 chrome 下默认是一个 Passive
Event,因此
addEventListener 的时候需要传参 {passive: false},否则的话不能
preventDefault。

几个基本概念

简单介绍几个关于动画的基本概念:

:在动画过程中,每一幅静止画面即为一“帧”;
帧率:即每秒钟播放的静止画面的数量,单位是fps(Frame per
second)或赫兹(Hz);
帧时长:即每一幅静止画面的停留时间,单位一般是ms(毫秒);
丢帧:在帧率固定的动画中,某一帧的时长远高于平均帧时长,导致其后续数帧被挤压而丢失的现象;

我们在显示器上看到的动画,每一帧变化都是系统绘制出来的(GPU或者CPU)。它的最高绘制频率受限于显示器的刷新频率(而非显卡,大多数是60Hz或者75Hz)。

帧频越高,屏幕上图片闪烁感就越小,稳定性也就越高。人的眼睛不容易察觉75Hz以上刷新频率带来的闪烁感。

在这里,我提供一个参考的版本,并不是说这一版就最好,而是说,通过这一版,分析当我们遇到这样的比较复杂的
UI 需求的时候,我们应该怎样思考和实现。

SVG

SVG是英文Scalable Vector
Graphics的缩写,意为可缩放矢量图形,用来定义用于网络的基于矢量的图形,其使用
XML 格式定义图像,并且具有如下特点:

  • 不依赖分辨率,基于矢量图;
  • 支持事件处理器;
  • 最适合带有大型渲染区域的应用程序(比如谷歌地图);
  • 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快);
  • 不适合游戏应用;

来看一个简单的示例,用SVG画了一个圆:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <rect x="50" y="20" rx="20" ry="20" width="150" height="150"
  style="fill:red;stroke:black;stroke-width:5;opacity:0.5"/>
</svg>

SVG 代码以 <svg> 元素开始,包括开启标签 <svg> 和关闭标签 </svg>
。这是根元素。widthheight 属性可设置此 SVG
文档的宽度和高度。version 属性可定义所使用的 SVG 版本,xmlns
属性可定义 SVG 命名空间。

澳门新葡亰手机版,SVG 的 <circle> 用来创建一个圆。cxcy 属性定义圆中心的 x 和
y 坐标。如果忽略这两个属性,那么圆点会被设置为 (0,
0)。r属性定义圆的半径。

下面主要是介绍SVG中的几个用于动画的元素,它们分别是:

<animate>:通常放置到一个SVG图像元素里面,用来定义这个图像元素的某个属性的动画变化过程;
<animateMotion>:元素也是放置一个图像元素里面,它可以引用一个事先定义好的动画路径,让图像元素按路径定义的方式运动;
<animateTransform>:元素对图形的运动和变换有更多的控制,它可以指定图形的变换、缩放、旋转和扭曲等;
<mpath>:元素的用法在上面的例子里出现过,它是一个辅助元素,通过它,<animateMotion>等元素可以引用一个外部的定义的<path>。让图像元素按这个<path>轨迹运动;

DEMO传送门

澳门新葡亰手机版 3

requestAnimationFrame

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘

设置这个API的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。

requestAnimationFrame使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用,由于功效只是一次性的,所以想实现连续的动效,需要递归调用,示例如下:

<div id="demo" style="position:absolute; width:100px; height:100px; background:#ccc; left:0; top:0;"></div>

<script>
var demo = document.getElementById('demo');
function render(){
    demo.style.left = parseInt(demo.style.left) + 1 + 'px'; //每一帧向右移动1px
}
requestAnimationFrame(function(){
    render();
    //当超过300px后才停止
    if(parseInt(demo.style.left) <= 300) requestAnimationFrame(arguments.callee);
});
</script>

cancelAnimationFrame方法用于取消重绘:

var requestID = requestAnimationFrame(repeatOften);
cancelAnimationFrame(requestID);

使用requestAnimationFrameAPI的优势如下:

  • 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随显示器的刷新频率(60
    Hz或者75 Hz);
  • 在隐藏或不可见的元素中,将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量;

目前,主要浏览器Firefox 23 / IE 10 / Chrome /
Safari)都支持这个方法。可以用下面的方法,检查浏览器是否支持这个API。如果不支持,则自行模拟部署该方法。

window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame || 
          window.webkitRequestAnimationFrame || 
          window.mozRequestAnimationFrame || 
          window.oRequestAnimationFrame || 
          window.msRequestAnimationFrame || 
          function(/* function FrameRequestCallback */ callback, /* DOMElement Element */ element){
            window.setTimeout(callback, 1000 / 60);
          };
})();

所以,可以这么说,requestAnimationFrame就是一个性能优化版、专为动画量身打造的setTimeout,不同的是requestAnimationFrame不是自己指定回调函数运行的时间,而是跟着浏览器内建的刷新频率来执行回调,这当然就能达到浏览器所能实现动画的最佳效果了。

DEMO传送门

工具 & 工程化

因为我们的代码使用了 ES6+,所以需要引入 babel 编译,我们的组件也使用
webpack 进行打包,以便于使用者在浏览器中直接引入。

这方面的内容,在之前的博客里有介绍,这里就不再一一说明。

最后,具体的代码可以直接查看 GitHub
工程。

童年.png

流程设计

接下来我们基于 Recorder 来设计设置和验证密码的流程:

语法

animation: name duration timing-function delay iteration-count direction;

具体属性值介绍如下:

描述
animation-name 规定需要绑定到选择器的 keyframe 名称。(keyframename、none)
animation-duration 规定完成动画所花费的时间,以秒或毫秒计。
animation-timing-function 规定动画的速度曲线。(linear、ease、ease-in、ease-out、ease-in-out、cubic-bezier(n,n,n,n))
animation-delay 规定在动画开始之前的延迟。
animation-iteration-count 规定动画应该播放的次数。
animation-direction 规定是否应该轮流反向播放动画。 (normal、alternate)

DEMO传送门

结构设计

使用 Canvas 实现的话 DOM
结构就比较简单。为了响应式,我们需要实现一个自适应宽度的正方形容器,方法前面已经介绍过。接着在容器中创建
Canvas。这里需要注意的一点是,我们应当把 Canvas 分层。这是因为 Canvas
的渲染机制里,要更新画布的内容,需要刷新要更新的区域重新绘制。因为我们有必要把频繁变化的内容和基本不变的内容分层管理,这样能显著提升性能。

Transition

CSS 中的 transition
属性允许块级元素中的属性在指定的时间内平滑的改变,简单看下其语法规则:

transition: property duration timing-function delay;

具体属性值介绍如下:

描述
transition-property 规定设置过渡效果的 CSS 属性的名称。(none / all / property)
transition-duration 规定完成过渡效果需要多少秒或毫秒。
transition-timing-function 规定速度效果的速度曲线。(linear、ease、ease-in、ease-out、ease-in-out、cubic-bezier(n,n,n,n))
transition-delay 定义过渡效果何时开始。

DEMO传送门

设置密码

澳门新葡亰手机版 4

有了前面异步 Promise API 的 Recorder,我们不难实现上面的两个流程。

验证密码的内部流程

async check(password){
  if(this.mode !== Locker.MODE_CHECK){
    await this.cancel();
    this.mode = Locker.MODE_CHECK;
  }  

  let checked = this.options.check.checked;

  let res = await this.record();

  if(res.err && res.err.message === Locker.ERR_USER_CANCELED){
    return Promise.resolve(res);
  }

  if(!res.err && password !== res.records){
    res.err = new Error(Locker.ERR_PASSWORD_MISMATCH)
  }

  checked.call(this, res);
  this.check(password);
  return Promise.resolve(res);
}

设置密码的内部流程

async update(){
  if(this.mode !== Locker.MODE_UPDATE){
    await this.cancel();
    this.mode = Locker.MODE_UPDATE;
  }

  let beforeRepeat = this.options.update.beforeRepeat, 
      afterRepeat = this.options.update.afterRepeat;

  let first = await this.record();

  if(first.err && first.err.message === Locker.ERR_USER_CANCELED){
    return Promise.resolve(first);
  }

  if(first.err){
    this.update();
    beforeRepeat.call(this, first);
    return Promise.resolve(first);   
  }

  beforeRepeat.call(this, first);

  let second = await this.record();      

  if(second.err && second.err.message === Locker.ERR_USER_CANCELED){
    return Promise.resolve(second);
  }

  if(!second.err && first.records !== second.records){
    second.err = new Error(Locker.ERR_PASSWORD_MISMATCH);
  }

  this.update();
  afterRepeat.call(this, second);
  return Promise.resolve(second);
}

可以看到,有了 Recorder 之后,Locker
的验证和设置密码基本上就是顺着流程用 async/await 写下来就行了。

动画即童年

动画是指由许多帧静止的画面,以一定的速度(如每秒16张)连续播放时,肉眼因视觉残象产生错觉,而误以为画面活动的作品。–
维基百科

以上是维基百科上给出的动画的定义。相信每一个像我这样有童年的孩子,应该都玩过手翻书,或者就算你的童年稍微暗淡一点,应该也看过动画片吧…嗯嗯,并没有跑题,其实这和我们今天提及的动画本质上是一样的,只不过就是呈现方式或者说载体发生了改变。

澳门新葡亰手机版 5

超人大战赛亚人.avi