搜尋
首頁web前端js教程原生 JS 實作手勢解鎖元件實例方法(圖)

這是 第三屆 360 前端星計畫 的選拔作業題。 600多名學生參與了解答,最後通過了60人。這60名同學完成的不錯,思路、程式碼風格、功能完成度頗有可取之處,不過也有一些欠考慮的地方,比如發現很多同學能按照需求實現完整的功能,但是不知道應當如何設計開放的API ,或者說,如何分析和預判產品需求和未來的變化,從而決定什麼應當開放,什麼應當封裝。這無關於答案正確與否,還是和經驗有關。

在這裡,我提供一個參考的版本,並不是說這一版就最好,而是說,透過這一版,分析當我們遇到這樣的比較複雜的UI 需求的時候,我們應該怎樣思考和實現。

原生 JS 實作手勢解鎖元件實例方法(圖)

元件設計的一般步驟

元件設計一般來說包含如下一些流程:

  1. 理解需求

  2. 技術選項

  3. 結構(UI)設計

  4. 資料與API設計

  5. 流程設計

  6. #相容性與細部最佳化

  7. 工具& 工程化

#這些過程並不是每個元件設計的時候都會遇到,但是通常來說一個專案總是會在其中一些過程裡遇到問題需要解決。下面我們來簡單分析一下。

理解需求作業本身只是說設計一個常見的手勢密碼的UI 交互,可以透過選擇驗證密碼和設定密碼來切換兩種

狀態

,每種狀態都有自己的流程。因此大部分同學就照著需求把整個元件的狀態切換和流程封裝了起來,有的同學提供了一定的UI 樣式配置能力,但是基本上沒有同學能將流程和狀態切換過程中的節點給開放出來。實際上這個元件如果要給用戶使用,顯然需要將過程節點開放出來,也就是說, 需要由用戶決定設定密碼的過程裡執行什麼操作、驗證密碼的過程和密碼驗證成功後執行什麼操作,這些是元件開發者無法取代使用者來決定的。

var password = '11121323';

var locker = new HandLock.Locker({
  container: document.querySelector('#handlock'),
  check: {
    checked: function(res){
      if(res.err){
        console.error(res.err); //密码错误或长度太短
        [执行操作...]
      }else{
        console.log(`正确,密码是:${res.records}`);
        [执行操作...]
      }
    },
  },
  update:{
    beforeRepeat: function(res){
      if(res.err){
        console.error(res.err); //密码长度太短
        [执行操作...]
      }else{
        console.log(`密码初次输入完成,等待重复输入`);
        [执行操作...]
      }
    },
    afterRepeat: function(res){
      if(res.err){
        console.error(res.err); //密码长度太短或者两次密码输入不一致
        [执行操作...]
      }else{
        console.log(`密码更新完成,新密码是:${res.records}`);
        [执行操作...]
      }
    },
  }
});

locker.check(password);

 

技術選型這個問題的UI 展現的核心是九宮格和選取的小圓點,從技術上來講,我們有三種可選方案: DOM/

Canvas

/SVG,三者都是可以實現主體UI 的。

如果使用 DOM,最簡單的方式是使用 flex 佈局,這樣能夠做成響應式的。

DOM 實作繪製使用DOM 的優點是容易實現響應式,

事件處理

簡單,佈局也不複雜(但是和Canvas 比起來略微複雜),但是斜線(demo 裡沒有畫)的長度和斜率需要計算。

除了使用DOM 外,使用Canvas 繪製也很方便:

Canvas 實作繪製

用Canvas 實作有兩個小細節,第一個是實作響應式,可以用DOM 建構一個正方形的容器:

#container {
  position: relative;
  overflow: hidden;
  width: 100%;
  padding-top: 100%;
  height: 0px;
  background-color: white;
}

 

在這裡我們使用padding-top:100% 撐開容器高度使它等於容器寬度。

第二個細節是為了在 retina 螢幕上獲得清晰的顯示效果,我們將 Canvas 的寬度增加一倍,然後透過 transform: scale(0.5) 來縮小到匹配容器寬高。

#container canvas{
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(0.5);
}

 

由於Canvas 的定位是absolute,它本身的預設寬高並不等於容器的寬高,需要透過JS 設定:

let width = 2 * container.getBoundingClientRect().width;
canvas.width = canvas.height = width;

 

這樣我們就可以透過在Canvas 上繪製實心圓和連線來實現UI 了。具體的方法在後續的內容有更詳細的講解。

最後我們來看看用SVG 繪製:

SVG 實作繪製

#由於SVG 原生操作的API 不是很方便,這裡使用了Snap.svg 函式庫,實作起來和使用Canvas 大同小異,這裡就不贅述了。

SVG 的問題是行動裝置相容性不如 DOM 和 Canvas 好。

綜合上面三者的情況,最後我選擇使用 Canvas 來實現。

結構設計

使用 Canvas 實作的話 DOM 結構就比較簡單。為了響應式,我們需要實作一個自適應寬度的正方形容器,方法前面已經介紹過。接著在容器中建立 Canvas。這裡要注意的一點是,我們應該把 Canvas 分層。這是因為 Canvas 的渲染機制裡,要更新畫布的內容,需要刷新要更新的區域重新繪製。因為我們有必要把頻繁變化的內容和基本上不變的內容分層管理,這樣能顯著提升效能。

分成 3 個圖層原生 JS 實作手勢解鎖元件實例方法(圖)

#######

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

确定圆点的位置

原生 JS 實作手勢解鎖元件實例方法(圖)

圆点的位置有两种定位法,第一种是九个九宫格,圆点在小九宫格的中心位置。如果认真的同学,已经发现在前面 DOM 方案里,我们就是采用这样的方式,圆点的直径为 11.1%。第二种方式是用横竖三条线把宽高四等分,圆点在这些线的交点处。

在 Canvas 里我们采用第二种方法来确定圆点(代码里的 n = 3)。

let range = Math.round(width / (n + 1));

let circles = [];

//drawCircleCenters
for(let i = 1; i <p> </p><p>最后一点,严格说不属于结构设计,但是因为我们的 UI 是通过触屏操作,我们需要考虑 Touch 事件处理和坐标的转换。</p><pre class="brush:php;toolbar:false">function getCanvasPoint(canvas, x, y){
  let rect = canvas.getBoundingClientRect();
  return {
    x: 2 * (x - rect.left), 
    y: 2 * (y - rect.top),
  };
}

 

我们将 Touch 相对于屏幕的坐标转换为 Canvas 相对于画布的坐标。代码里的 2 倍是因为我们前面说了要让 retina 屏下清晰,我们将 Canvas 放大为原来的 2 倍。

API 设计

接下来我们需要设计给使用者使用的 API 了。在这里,我们将组件功能分解一下,独立出一个单纯记录手势的 Recorder。将组件功能分解为更加底层的组件,是一种简化组件设计的常用模式。

原生 JS 實作手勢解鎖元件實例方法(圖)

我们抽取出底层的 Recorder,让 Locker 继承 Recorder,Recorder 负责记录,Locker 管理实际的设置和验证密码的过程。

我们的 Recorder 只负责记录用户行为,由于用户操作是异步操作,我们将它设计为 Promise 规范的 API,它可以以如下方式使用:

var recorder = new HandLock.Recorder({
  container: document.querySelector('#main')
});

function recorded(res){
  if(res.err){
    console.error(res.err);
    recorder.clearPath();
    if(res.err.message !== HandLock.Recorder.ERR_USER_CANCELED){
      recorder.record().then(recorded);
    }
  }else{
    console.log(res.records);
    recorder.record().then(recorded);
  }      
}

recorder.record().then(recorded);

 

对于输出结果,我们简单用选中圆点的行列坐标拼接起来得到一个唯一的序列。例如 "11121323" 就是如下选择图形:

原生 JS 實作手勢解鎖元件實例方法(圖)

为了让 UI 显示具有灵活性,我们还可以将外观配置抽取出来。

const defaultOptions = {
  container: null, //创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层
  focusColor: '#e06555',  //当前选中的圆的颜色
  fgColor: '#d6dae5',     //未选中的圆的颜色
  bgColor: '#fff',        //canvas背景颜色
  n: 3, //圆点的数量: n x n
  innerRadius: 20,  //圆点的内半径
  outerRadius: 50,  //圆点的外半径,focus 的时候显示
  touchRadius: 70,  //判定touch事件的圆半径
  render: true,     //自动渲染
  customStyle: false, //自定义样式
  minPoints: 4,     //最小允许的点数
};

 

这样我们实现完整的 Recorder 对象,核心代码如下:

[...] //定义一些私有方法

const defaultOptions = {
  container: null, //创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层
  focusColor: '#e06555',  //当前选中的圆的颜色
  fgColor: '#d6dae5',     //未选中的圆的颜色
  bgColor: '#fff',        //canvas背景颜色
  n: 3, //圆点的数量: n x n
  innerRadius: 20,  //圆点的内半径
  outerRadius: 50,  //圆点的外半径,focus 的时候显示
  touchRadius: 70,  //判定touch事件的圆半径
  render: true,     //自动渲染
  customStyle: false, //自定义样式
  minPoints: 4,     //最小允许的点数
};

export default class Recorder{
  static get ERR_NOT_ENOUGH_POINTS(){
    return 'not enough points';
  }
  static get ERR_USER_CANCELED(){
    return 'user canceled';
  }
  static get ERR_NO_TASK(){
    return 'no task';
  }
  constructor(options){
    options = Object.assign({}, defaultOptions, options);

    this.options = options;
    this.path = [];

    if(options.render){
      this.render();
    }
  }
  render(){
    if(this.circleCanvas) return false;

    let options = this.options;
    let container = options.container || document.createElement('p');

    if(!options.container && !options.customStyle){
      Object.assign(container.style, {
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        lineHeight: '100%',
        overflow: 'hidden',
        backgroundColor: options.bgColor
      });
      document.body.appendChild(container); 
    }
    this.container = container;

    let {width, height} = container.getBoundingClientRect();

    //画圆的 canvas,也是最外层监听事件的 canvas
    let circleCanvas = document.createElement('canvas'); 

    //2 倍大小,为了支持 retina 屏
    circleCanvas.width = circleCanvas.height = 2 * Math.min(width, height);
    if(!options.customStyle){
      Object.assign(circleCanvas.style, {
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%) scale(0.5)', 
      });
    }

    //画固定线条的 canvas
    let lineCanvas = circleCanvas.cloneNode(true);

    //画不固定线条的 canvas
    let moveCanvas = circleCanvas.cloneNode(true);

    container.appendChild(lineCanvas);
    container.appendChild(moveCanvas);
    container.appendChild(circleCanvas);

    this.lineCanvas = lineCanvas;
    this.moveCanvas = moveCanvas;
    this.circleCanvas = circleCanvas;

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

    this.clearPath();
    return true;
  }
  clearPath(){
    if(!this.circleCanvas) this.render();

    let {circleCanvas, lineCanvas, moveCanvas} = this,
        circleCtx = circleCanvas.getContext('2d'),
        lineCtx = lineCanvas.getContext('2d'),
        moveCtx = moveCanvas.getContext('2d'),
        width = circleCanvas.width,
        {n, fgColor, innerRadius} = this.options;

    circleCtx.clearRect(0, 0, width, width);
    lineCtx.clearRect(0, 0, width, width);
    moveCtx.clearRect(0, 0, width, width);

    let range = Math.round(width / (n + 1));

    let circles = [];

    //drawCircleCenters
    for(let i = 1; i {
      this.clearPath();
    });

    let records = [];

    let handler = evt => {
      let {clientX, clientY} = evt.changedTouches[0],
          {bgColor, focusColor, innerRadius, outerRadius, touchRadius} = options,
          touchPoint = getCanvasPoint(moveCanvas, clientX, clientY);

      for(let i = 0; i  {
      recordingTask.cancel = (res = {}) => {
        let promise = this.recordingTask.promise;

        res.err = res.err || new Error(Recorder.ERR_USER_CANCELED);
        circleCanvas.removeEventListener('touchstart', handler);
        circleCanvas.removeEventListener('touchmove', handler);
        document.removeEventListener('touchend', done);
        resolve(res);
        this.recordingTask = null;

        return promise;
      }

      let done = evt => {
        moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height);
        if(!records.length) return;

        circleCanvas.removeEventListener('touchstart', handler);
        circleCanvas.removeEventListener('touchmove', handler);
        document.removeEventListener('touchend', done);

        let err = null;

        if(records.length  o.pos.join('')).join('')};

        resolve(res);
        this.recordingTask = null;
      };
      document.addEventListener('touchend', done);
    });

    recordingTask.promise = promise;

    this.recordingTask = recordingTask;

    return promise;
  }
}

 

它的几个公开的方法,recorder 负责记录绘制结果, clearPath 负责在画布上清除上一次记录的结果,cancel 负责终止记录过程,这是为后续流程准备的。

流程设计

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

验证密码


设置密码

原生 JS 實作手勢解鎖元件實例方法(圖)

有了前面异步 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 写下来就行了。

细节问题

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

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

 

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

工具 & 工程化

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

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

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

总结

以上就是今天要讲的全部内容,这里面有几个点我想再强调一下:

  1. 在设计 API 的时候思考真正的需求,判断什么该开放、什么该封装

  2. 做好技术调研和核心方案研究,选择合适的方案

  3. 优化和解决细节问题

以上是原生 JS 實作手勢解鎖元件實例方法(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
JavaScript的角色:使網絡交互和動態JavaScript的角色:使網絡交互和動態Apr 24, 2025 am 12:12 AM

JavaScript是現代網站的核心,因為它增強了網頁的交互性和動態性。 1)它允許在不刷新頁面的情況下改變內容,2)通過DOMAPI操作網頁,3)支持複雜的交互效果如動畫和拖放,4)優化性能和最佳實踐提高用戶體驗。

C和JavaScript:連接解釋C和JavaScript:連接解釋Apr 23, 2025 am 12:07 AM

C 和JavaScript通過WebAssembly實現互操作性。 1)C 代碼編譯成WebAssembly模塊,引入到JavaScript環境中,增強計算能力。 2)在遊戲開發中,C 處理物理引擎和圖形渲染,JavaScript負責遊戲邏輯和用戶界面。

從網站到應用程序:JavaScript的不同應用從網站到應用程序:JavaScript的不同應用Apr 22, 2025 am 12:02 AM

JavaScript在網站、移動應用、桌面應用和服務器端編程中均有廣泛應用。 1)在網站開發中,JavaScript與HTML、CSS一起操作DOM,實現動態效果,並支持如jQuery、React等框架。 2)通過ReactNative和Ionic,JavaScript用於開發跨平台移動應用。 3)Electron框架使JavaScript能構建桌面應用。 4)Node.js讓JavaScript在服務器端運行,支持高並發請求。

Python vs. JavaScript:比較用例和應用程序Python vs. JavaScript:比較用例和應用程序Apr 21, 2025 am 12:01 AM

Python更適合數據科學和自動化,JavaScript更適合前端和全棧開發。 1.Python在數據科學和機器學習中表現出色,使用NumPy、Pandas等庫進行數據處理和建模。 2.Python在自動化和腳本編寫方面簡潔高效。 3.JavaScript在前端開發中不可或缺,用於構建動態網頁和單頁面應用。 4.JavaScript通過Node.js在後端開發中發揮作用,支持全棧開發。

C/C在JavaScript口譯員和編譯器中的作用C/C在JavaScript口譯員和編譯器中的作用Apr 20, 2025 am 12:01 AM

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。

JavaScript在行動中:現實世界中的示例和項目JavaScript在行動中:現實世界中的示例和項目Apr 19, 2025 am 12:13 AM

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

JavaScript和Web:核心功能和用例JavaScript和Web:核心功能和用例Apr 18, 2025 am 12:19 AM

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

了解JavaScript引擎:實施詳細信息了解JavaScript引擎:實施詳細信息Apr 17, 2025 am 12:05 AM

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境