cover_image

手机签名组件的用户体验优化之旅

joybb 政通技术团队
2021年12月31日 10:38

1. 前言

前端是软件系统的门面,是用户接触系统的直接入口,用户对系统的感知,全部来自于前端,因此前端的重要性对软件系统来说不言而喻。手机签名组件作为前端的重要组成之一,本文讲述了精益求精开发签名组件的全过程,希望对读者有所启发。

2 需求介绍

手机签名组件需要实现的功能如下:
  • 保证能实现竖屏签名,也可以重签和取消;

  • 对接给其他开发者使用时要足够简单、方便;

  • 由于用户的书写习惯是从左到右的,所以要能支持横屏签名;

  • 要支持Android浏览器、iOS浏览器、钉钉、微信等多个环境

3 需求分析

如果要满足上面所有的需求,那基本方案设计是怎样的?实现难点在哪里?因为要实现跨环境,那么只能用纯H5实现。在这个画图场景下,有Canvas API、WebGL API可以满足,调研之后,发现Canvas API足矣。整理基本思路如下:

3.1 基本思路

  • 使用H5的Touch Event监听手机上手指移动的轨迹,通过Mouse Event监听鼠标移动的轨迹;

  • 焦点(手指或者鼠标)移动时,把轨迹的终点不断地画在画布(Canvas)上;

  • 重签就清空画布;

  • Canvas需要导出File格式方便上传,导出Base64格式方便预览。

整理完思路之后,整理实现难点。

3.2 实现难点

  • Touch Event和Mouse Event 最好抽象成一套,否则同样的轨迹逻辑要写两遍;

  • 横屏签名有点复杂,由于手机默认是竖直屏幕的,并且很多用户是锁死了竖屏的,所以对屏幕被锁死成竖屏或者App只支持竖屏的情况,只能实现“假横屏”,也即是把竖直的Canvas顺时针旋转90度展示出来。这个时候就会Canvas 旋转了,绘图坐标就被旋转了,但是由于是“假横屏”,屏幕的响应事件坐标(即Touch Event、Mouse Event坐标并没有被旋转),所以这里存在一个坐标转换的问题;

  • 不管竖屏、横屏、还是“假横屏”,用户看到的只有竖屏或者横屏,所以导出的图像必须和用户看到的一致;

  • 用户有没有可能在签名时旋转屏幕的情况,旋转了,怎么处理,要清空画布吗?

4 需求实现

4.1 实现最基本功能

4.1.1 事件统一

  • 手机触摸事件TouchEvent;

  • 鼠标事件MouseEvent。

判断有手机触摸事件时则使用手机触摸事件,其他情况则使用鼠标事件,每一个步骤事件的反应都放在对应的handler中。

isSupportTouch: 'ontouchstart' in window,
events'ontouchstart' in window ?
        ['touchstart''touchmove''touchend''touchleave'] :
        ['mousedown''mousemove''mouseup''mouseleave']
        
this.$refs.canvas.addEventListener(this.events[0], this.startEventHandler, false);
this.$refs.canvas.addEventListener(this.events[1], this.moveEventHandler, false);
this.$refs.canvas.addEventListener(this.events[2], this.endEventHandler, false);
this.$refs.canvas.addEventListener(this.events[3], this.leaveEventHandler, false);

const evt = this.isSupportTouch ? event.touches[0] : event;

4.1.2 画笔迹

如果是手机触摸事件则取event.touches[0], 如果是鼠标事件则直接用event。从event中获取当前(触摸或者鼠标)焦点,然后在把线画到当前焦点即可。

moveEventHandler(event) {
      event.preventDefault();      
      const evt = this.isSupportTouch ? event.touches[0] : event;
      const coverPos = canvas.getBoundingClientRect();
      const mouseX = evt.clientX - coverPos.left;
      const mouseY = evt.clientY - coverPos.top;
      const position =  {x: mouseX, y: mouseY }// 获取当前event position
      this.ctx.lineTo(position.x, position.y)
      this.ctx.stroke();
},

4.1.3 文件导出

导出图像的base64数据。

dataURL = canvas.toDataURL('image/png');

导出图像的File数据。

export function canvas2File(canvas: HTMLCanvasElement, fileName?: string, lastModified?: number, type = "image/jpeg", quality=0.8): Promise<File> {
  return new Promise<File>((resolve, reject) => {
    canvas.toBlob(blob => {
      if (!blob) {
        reject(new Error('canvas文件导出失败'));
      } else {
        const file = new File([blob], fileName, { lastModified, type });   
        resolve(file);
      }
    }, type, quality)
  })
}

阶段效果

实现竖屏的签名效果。

图片

4.2 攻克难点

4.2.1 假横屏

实现原理:在页面不能横屏的情况下,绘图的canvas顺时针旋转90度。屏幕响应事件坐标系还是保持不变,通过代码转换事件坐标系和绘图坐标系的位置。

如下图,canvas从旧原点旋转到新原点:

图片

画布坐标为:
  1. canvas 新 Y = canvas.width - 旧 screen X;
  2. canvas 新 X = 旧 screen Y。

canvas顺时针旋转90度

通过CSS transform实现旋转。

  const width = clientWidth - leftOffset;
  const height = clientHeight - topOffset;
  const longSide = Math.max(width, height);
  const shortSide = Math.min(width, height);
  
  resultStyle.transform = `rotate(90deg)`;
  resultStyle.width = `${longSide}px`
  resultStyle.height = `${shortSide}px`
  resultStyle.transformOrigin = `${shortSide / 2}px center`

坐标系转换

当竖屏时,必须模拟横屏,对事件的x和y做转换,否则保持x和y不变。

export const getForceLandscapeEventPosition = (evt: MouseEvent , canvas: HTMLCanvasElement, forceLandscape: boolean): EventPosition => {
  const coverPos = canvas.getBoundingClientRect();
  const mouseX = evt.clientX - coverPos.left;
  const mouseY = evt.clientY - coverPos.top;

  const isReverseXY = forceLandscape && (window.orientation == null || window.orientation === 180 || window.orientation === 0)
  if (isReverseXY) {
    const { clientWidth } = document.documentElement;
    const leftOffset = 0;
    const width = clientWidth - leftOffset;
    return {'x': mouseY, 'y': (width - mouseX) };
  } else {
    return {'x': mouseX, 'y': mouseY};
  }
}

const evt = this.isSupportTouch ? event.touches[0] : event;
const position = getForceLandscapeEventPosition(evt, this.$refs.canvas, !this.allowPortrait)
this.ctx.lineTo(position.x, position.y)
this.ctx.stroke();

4.2.2 屏幕旋转处理

由于屏幕旋转后,画图的画布尺寸改变了,旋转后若还保留上次绘制结果的,无论是拉伸还是截断都不合适,所以旋转后直接清空画布。由于组件可以在PC上展示,PC上的窗口大小可变,所以实际代码中使用resize事件而不是orientationchange事件来获取更好的兼容性。

window.addEventListener("resize", (e) => {
      this.ctx.clearRect(00this.$refs.canvas.width, this.$refs.canvas.height);
      this.ctx.closePath();
});

阶段效果

实现横屏的签名效果,更符合书写习惯。
图片

4.3 代码整理

至此,已经完成了这个组件需求。但是组件达到完美状态了吗?肯定没有,那还有哪些优化点呢?
还有,代码的用户是谁?开发者都知道是最终的用户,但其实不止,还有其他开发者同事。其他开发者要看代码、用代码,他们也是用户。特别是针对这个组件的使用场景,组件就是提供给其他开发者开发具体功能用的。UI(User Interface)是展示给最终用户的接口,API(Application Programming Interface)就是展示给同事的接口。所以优化API如下:
  • 由于获取签名图像之后,大概率是要上传到后台的,file更适合上传。所以导出类型参数改为file和dataUrl(Base64)构成的数组,方便在需要file的时候既导出Base64图像又导出file;

  • 默认在竖屏时开启了假横屏,并提供参数关闭此功能;

  • 针对JPG图像格式不支持透明的情况做了明确说明,并设置了JPG的默认背景色。

图片

4.4 更加精致

在开发这个组件的过程,笔者了解到开源项目signature_pad,发现该项目实现的效果笔迹更加丝滑。

图片

经过调研发现该项目参考了Square公司的开发成果Smoother Signatures,使用了Cubic Bezier Curves插值算法对笔迹进行了平滑处理,并基于笔迹的移动速度动态调整了画线的宽度。了解清楚原理后,笔者也在组件里加上了该方案。效果如下:

图片

5 总结

5. 1 总结回顾

  • 在整理需求时提前想好实现方案,了解清楚实现难点,结果就既能按图索骥地提高效率,又能锻炼设计能力;

  • 在克服“假横屏”的难点中,学习怎样对Canvas坐标系和Event坐标系进行转换;

  • 在实现笔迹优化的过程中,接触到了贝塞尔曲线差值算法,开始让工作引入了些“技术含量”。

5.2 经验分享

做技术若想要由表及里、拥有的“核心”竞争力。笔者总结有如下经验,仅供参考:
  • 开发之前需要去打腹稿,做到胸有成竹,打腹稿能锻炼分析设计能力。只有不断锻炼设计能力,才能看懂技术实现背后的设计,才能看到相通。技术的表层实现是不通的,比如前端代码和后端不能直接放在一起编译使用。技术的深层设计上是相通的,比如事件循环,浏览器处理界面事件和后端服务器处理API事件都是用到了事件循环这个技术;

  • 不畏难,要辩证的看待难点。有创新的点,有技术含量的点,都来自难点,不难的点,都已经有成熟或常见方案了。所以处理别人不愿处理或者忽略的难点,才能学到新的东西,有竞争力的东西。基本的搞懂了,难点也搞懂了,这样就看得更清一些,下次处理这个领域的问题,就能更快找到思路;

  • 不断地提高要求。提出更高的要求,把重要的细节做的更好。西方有句话“God is in the detail”,中庸有云“君子致广大,而尽精微”。如果一个领域有细节问题至今没有解决,说明这是该领域的难点,要得到解决,往往要到另外一个领域或层面去找方案,穿过精微之处,才能走向下一个领域或层面,才能学的多、学的深;

  • 学以致用。学习和工作是相辅相成的。学习要能落实到工作中去,因为只有结合工作、以服务工作为目的,才能持续获得资源,从而反过来去支撑学习,而且知识只有用出来,才能知道一个技术里的门道。否则学习只会浮于书本,无法验证其是否正确。所以优化用户体验要成为前端开发者技术提升的目的,这样才能让技术提升和工作相辅相成;

  • 多总结,多写文章。思路的清晰不是一蹴而就的,是在不断梳理、揣摩、提炼后,逐渐清晰的。

5.3 结语

通过这个案例实现的过程,笔者阐述了怎样精益求精打造极致用户体验的过程。以及在这个过程中是怎样学习的。结合上面几条经验,笔者对前端开发者技术提升之道提出了一个口号:精益求精,打造极致用户体验。希望能明确大家提升的方向。
最后,谢谢您的阅读,2022年即将到来,祝所有热爱技术的小伙伴们新年快乐!

继续滑动看下一个
政通技术团队
向上滑动看下一个