You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Canvas电子签名功能在Android设备上无法正常运行的技术问询

问题分析与解决方案

核心问题原因

  1. 页面滚动干扰:Android设备触摸Canvas时会触发页面滚动,导致clientX/clientY坐标随页面滚动偏移,签名轨迹错位。
  2. Canvas坐标映射错误:Canvas的元素属性尺寸(width/height)与CSS显示尺寸不一致时,直接用显示坐标计算画布位置会导致轨迹偏移。
  3. 未处理pointercancel事件:Android部分场景下(如系统触发滚动)会触发pointercancel,此时未终止签名状态,导致异常绘制。

具体修复步骤

1. 阻止页面滚动干扰

修改pointerdown事件监听的passive属性为false,并在handlePointerDown中阻止默认行为,避免触摸Canvas时页面滚动:

const handlePointerDown = (event) => {
  event.preventDefault(); // 阻止页面滚动等默认行为
  writingMode = true;
  ctx.beginPath();
  const [positionX, positionY] = getCursorPosition(event);
  ctx.moveTo(positionX, positionY);
}

// 修改事件监听的第三个参数
canvas.addEventListener('pointerdown', handlePointerDown, { passive: false });

2. 修正Canvas坐标映射

更新getCursorPosition函数,将触摸的显示坐标转换为Canvas画布的实际坐标,适配CSS缩放后的尺寸:

const getCursorPosition = (event) => {
  const rect = event.target.getBoundingClientRect();
  // 计算显示尺寸与画布实际尺寸的比例
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  // 转换为画布实际坐标
  const positionX = (event.clientX - rect.x) * scaleX;
  const positionY = (event.clientY - rect.y) * scaleY;
  return [positionX, positionY];
}

3. 处理pointercancel事件

添加pointercancel事件监听,在触摸被系统中断时终止签名状态:

const handlePointerCancel = () => {
  writingMode = false;
}

canvas.addEventListener('pointercancel', handlePointerCancel, { passive: true });

4. 确保Canvas样式适配

在样式中添加Canvas的自适应规则,避免拉伸变形:

.signature-pad {
  border: 1px solid #ccc;
  max-width: 100%;
  height: auto;
  touch-action: none; /* 直接禁用触摸相关的默认行为,增强兼容性 */
}

完整修复后的代码

<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Singature Pad</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
  <style>
    .signature-pad {
      border: 1px solid #ccc;
      max-width: 100%;
      height: auto;
      touch-action: none;
    }
  </style>
</head>
<body>
  <form class="signature-pad-form" method="POST">
    <canvas height="250" width="500" class="signature-pad"></canvas>
    <button class="clear-button">Cancella</button>
    <button class="submit-button" type="submit">Salva</button>
  </form>
  <script>
const canvas = document.querySelector('canvas');
const form = document.querySelector('.signature-pad-form');
const clearButton = document.querySelector('.clear-button');

const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.lineJoin = ctx.lineCap = 'round';

let writingMode = false;

const handlePointerDown = (event) => {
  event.preventDefault();
  writingMode = true;
  ctx.beginPath();
  const [positionX, positionY] = getCursorPosition(event);
  ctx.moveTo(positionX, positionY);
}

const handlePointerUp = () => {
  writingMode = false;
}

const handlePointerCancel = () => {
  writingMode = false;
}

const handlePointerMove = (event) => {
  if (!writingMode) return;
  const [positionX, positionY] = getCursorPosition(event);
  ctx.lineTo(positionX, positionY);
  ctx.stroke();
}

const getCursorPosition = (event) => {
  const rect = event.target.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  const positionX = (event.clientX - rect.x) * scaleX;
  const positionY = (event.clientY - rect.y) * scaleY;
  return [positionX, positionY];
}

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const imageURL = canvas.toDataURL('image/jpeg');
  const imageBlob = dataURItoBlob(imageURL);

  const formData = new FormData();
  formData.append('firma', imageBlob);

  fetch('/firma', {
    method: 'POST',
    body: formData
  })
  .then(response => {
    if (response.ok) {
      clearPad();
      form.reset();
      window.location.href = "{{ url_for('succ') }}"
    } else {
      console.error('Errore durante il salvataggio della firma.');
    }
  })
  .catch(error => {
    console.error('Si è verificato un errore:', error);
  });
});

const clearPad = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = 'white';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = 'black';
}

clearButton.addEventListener('click', (event) => {
  event.preventDefault();
  clearPad();
});

canvas.addEventListener('pointerdown', handlePointerDown, { passive: false });
canvas.addEventListener('pointerup', handlePointerUp, { passive: true });
canvas.addEventListener('pointercancel', handlePointerCancel, { passive: true });
canvas.addEventListener('pointermove', handlePointerMove, { passive: true });

function dataURItoBlob(dataURI) {
  const byteString = atob(dataURI.split(',')[1]);
  const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
  const ab = new ArrayBuffer(byteString.length);
  const ia = new Uint8Array(ab);

  for (let i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i);
  }

  return new Blob([ab], { type: mimeString });
}
  </script>
</body>
</html>

内容的提问来源于stack exchange,提问作者Lendy345

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 06:05:56