Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ios 有时候无法签名 #5

Open
LCtenacity opened this issue May 10, 2018 · 36 comments
Open

ios 有时候无法签名 #5

LCtenacity opened this issue May 10, 2018 · 36 comments

Comments

@LCtenacity
Copy link

不知道是不是组件加载的顺序有问题。ios下有时候无法签名,或者是签名的坐标有问题。重新刷新一下界面就可以了

@Louiszhai
Copy link
Owner

demo运行有问题吗,如果没问题,坐标问题看看页面中canvas所在父容器css样式对不对。
ios无法签名是个什么情况?有稳定的复现步骤吗,操作系统版本呢?

@LCtenacity
Copy link
Author

LCtenacity commented May 21, 2018 via email

@Louiszhai
Copy link
Owner

Louiszhai commented May 21, 2018

图1234对应的是css样式吗?你贴的是父容器的css样式吗?如果是,那我觉得这些css与问题无关。

是这样的,如果确认demo在你的手机上运行正常,而你写的代码部分ios手机无法签名(需要刷新),建议耐心debug下canvas画布的touchstart、touchmove事件,看看有无异常。或者留下你qq,我加你好友

@yuemeimei
Copy link

en能动态的修改canvas的背景图吗?

@Louiszhai
Copy link
Owner

Louiszhai commented Jun 21, 2018 via email

@alterhu2020
Copy link

@Louiszhai @LCtenacity
你好,我也遇到这个问题,只有刷新画布才能签名,你们有解决的方法吗?
谢谢!

@Louiszhai
Copy link
Owner

Louiszhai commented Nov 29, 2018 via email

@alterhu2020
Copy link

alterhu2020 commented Nov 29, 2018

@Louiszhai
直接复制的demo代码如下:

// 页面Page1.vue
 <div>
    <div id="canvasBox" :style="getHorizontalStyle">
      <div class="greet">
        <span>{{msg}}</span>
        <input type="button" value="清屏" @touchstart="clear" @mousedown="clear"/>
        <!-- <input type="button" value="生成png图片" @touchstart="savePNG" @mousedown="savePNG"/> -->
      </div>
      <canvas></canvas>
    </div>
    <!-- <div class="image-box" v-show="showBox">
      <header>
        请长按图片并保存至本地后发送好友
        <input type="button" value="返回" @click="showBox = false"/>
      </header>
      <img :src="signImage">
    </div> -->
  </div>

需要跳转到Page1.vue的代码:

this.$router.push({name: 'page1'})

类似上面的代码,当采用上面的代码跳转到Page1.vue的时候,在画布上移动指针,发现画布没有任何变化,没有画上,然后刷新Page1对应的页面,重新移动指针,发现画布上可以画上对应的签名。

debug对应的draw.js发现touchmove触发了啊!
test3

另外发现,这两个进入页面的方式唯一的不同是,如果是直接刷新页面进去的,发现console里面有这一段日志:

vue.esm.js:591 [Vue warn]: Do not use built-in or reserved HTML elements as component id: canvas

,如果是直接通过this.$router.push({name: 'page1'})进去的就不会出现上面的log, 然后画布就不能画签名,奇怪啊!

@alterhu2020
Copy link

alterhu2020 commented Nov 29, 2018

什么情况下需要刷新画布,麻烦提供尽可能详细的背景信息 Walter Hu notifications@github.com于2018年11月29日 周四21:18写道:

@Louiszhai https://github.com/Louiszhai @LCtenacity https://github.com/LCtenacity 你好,我也遇到这个问题,只有刷新画布才能签名,你们有解决的方法吗? 谢谢! — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#5 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/AIBcjeXYgGuUd4_4jPYT6V3yNUZAci0kks5uz96jgaJpZM4T6Uqs .

刷新画布是因为发现直接通过this.$router进去没有发现,只能刷新下页面,发现此时画布可以正常签名了。
@Louiszhai 你好,麻烦能帮忙看下吗?我感觉大部分都会遇到这个问题,我的QQ是: 1725641479@qq.com, 谢谢了!

@Louiszhai
Copy link
Owner

Louiszhai commented Nov 30, 2018 via email

@alterhu2020
Copy link

alterhu2020 commented Nov 30, 2018

跟你的demo的代码一样的,我贴下所有的代码:
唯一的不同的地方就是将你的draw.js 采用eslint格式化了,感觉是组件那里加载不对导致的,可是也没有发现哪里加载出问题了?只有刷新这个页面,画布上才能签名。

vue版本是: 2.5.2
<template>
  <div class="container">
    <div id="canvasBox" :style="getHorizontalStyle" v-show="!showBox">
      <div class="greet">
        <span>{{msg}}</span>
        <input type="button" value="清屏" @touchstart="clear" @mousedown="clear"/>
        <input type="button" value="生成png图片" @touchstart="savePNG" @mousedown="savePNG"/>
      </div>
      <canvas></canvas>
    </div>
    <div class="image-box" v-show="showBox">
      <header>
        请长按图片并保存至本地后发送好友
        <input type="button" value="返回" @click="showBox = false"/>
      </header>
      <img :src="signImage">
    </div>
  </div>
</template>

<script>
import Draw from '../components/draw'

export default {
  name: 'canvas',
  data () {
    return {
      msg: '请在下方空白处签名',
      degree: 90,
      signImage: null,
      showBox: false
    }
  },
  components: {
    Draw
  },
  beforeCreate () {
    document.title = '手写签名'
  },
  mounted () {
    this.$store.dispatch('UpdateShowHeader', true)
    this.$store.dispatch('UpdateShowTabbar', false)
    this.canvasBox = document.getElementById('canvasBox')
    this.initCanvas()
  },
  computed: {
    getHorizontalStyle () {
      const d = document
      const w = window.innerWidth || d.documentElement.clientWidth || d.body.clientWidth
      const h = window.innerHeight || d.documentElement.clientHeight || d.body.clientHeight
      let length = (h - w) / 2
      let width = w
      let height = h

      switch (this.degree) {
        case -90:
          length = -length
        /* eslint no-fallthrough: "error" */
        case 90:
          width = h
          height = w
          break
        default:
          length = 0
      }
      if (this.canvasBox) {
        this.canvasBox.removeChild(document.querySelector('canvas'))
        this.canvasBox.appendChild(document.createElement('canvas'))
        /* eslint  vue/no-async-in-computed-properties: "off" */
        setTimeout(() => {
          this.initCanvas()
        }, 200)
      }
      return {
        transform: `rotate(${this.degree}deg) translate(${length}px,${length}px)`,
        width: `${width}px`,
        height: `${height}px`,
        transformOrigin: 'center center'
      }
    }
  },
  methods: {
    initCanvas () {
      const canvas = document.querySelector('canvas')
      this.draw = new Draw(canvas, -this.degree)
    },
    clear () {
      this.draw.clear()
    },
    download () {
      this.draw.downloadPNGImage(this.draw.getPNGImage())
    },
    savePNG () {
      this.signImage = this.draw.getPNGImage()
      this.showBox = true
    },
    upload () {
      const image = this.draw.getPNGImage()
      const blob = this.draw.dataURLtoBlob(image)

      const url = ''
      const successCallback = (response) => {
        console.log(response)
      }
      const failureCallback = (error) => {
        console.log(error)
      }
      this.draw.upload(blob, url, successCallback, failureCallback)
    }
  }
}

</script>

<style>
  .container {
    width: 100%;
    height: 100%;
  }
  #canvasBox {
    display: flex;
    flex-direction: column;
    height: 100%;
  }
  .greet {
    padding: 20px;
    font-size: 20px;
    user-select: none;
  }
  input {
    font-size: 20px;
  }
  .greet select {
    font-size: 18px;
  }
  canvas {
    flex: 1;
    cursor: crosshair;
    border:2px dashed lightgray;
  }
  .image-box {
    width: 100%;
    height: 100%;
  }
  .image-box header{
    font-size: 18px;
  }
  .image-box img {
    max-width: 80%;
    max-height: 80%;
    margin-top: 50px;
    border: 1px solid gray;
  }
</style>


下面的是draw.js的代码:

/**
 * Created by louizhai on 17/6/30.
 * description: Use canvas to draw.
 */
function Draw (canvas, degree, config = {}) {
  if (!(this instanceof Draw)) {
    return new Draw(canvas, config)
  }
  if (!canvas) {
    return
  }
  let { width, height } = window.getComputedStyle(canvas, null)
  width = width.replace('px', '')
  height = height.replace('px', '')

  this.canvas = canvas
  this.context = canvas.getContext('2d')
  this.width = width
  this.height = height
  const context = this.context

  // 根据设备像素比优化canvas绘图
  const devicePixelRatio = window.devicePixelRatio
  if (devicePixelRatio) {
    canvas.style.width = `${width}px`
    canvas.style.height = `${height}px`
    canvas.height = height * devicePixelRatio
    canvas.width = width * devicePixelRatio
    context.scale(devicePixelRatio, devicePixelRatio)
  } else {
    canvas.width = width
    canvas.height = height
  }

  context.lineWidth = 6
  context.strokeStyle = 'black'
  context.lineCap = 'round'
  context.lineJoin = 'round'
  Object.assign(context, config)

  const { left, top } = canvas.getBoundingClientRect()
  const point = {}
  const isMobile = /phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone/i.test(navigator.userAgent)
  // 移动端性能太弱, 去掉模糊以提高手写渲染速度
  if (!isMobile) {
    context.shadowBlur = 1
    context.shadowColor = 'black'
  }
  let pressed = false

  const paint = (signal) => {
    switch (signal) {
      case 1:
        context.beginPath()
        context.moveTo(point.x, point.y)
      /* eslint no-fallthrough: "error" */
      case 2:
        context.lineTo(point.x, point.y)
        context.stroke()
        break
      default:
    }
  }
  const create = signal => (e) => {
    e.preventDefault()
    if (signal === 1) {
      pressed = true
    }
    if (signal === 1 || pressed) {
      e = isMobile ? e.touches[0] : e
      point.x = e.clientX - left
      point.y = e.clientY - top
      paint(signal)
    }
  }
  const start = create(1)
  const move = create(2)
  const requestAnimationFrame = window.requestAnimationFrame
  const optimizedMove = requestAnimationFrame ? (e) => {
    requestAnimationFrame(() => {
      move(e)
    })
  } : move

  if (isMobile) {
    canvas.addEventListener('touchstart', start)
    canvas.addEventListener('touchmove', optimizedMove)
  } else {
    canvas.addEventListener('mousedown', start)
    canvas.addEventListener('mousemove', optimizedMove);
    ['mouseup', 'mouseleave'].forEach((event) => {
      canvas.addEventListener(event, () => {
        pressed = false
      })
    })
  }

  // 重置画布坐标系
  if (typeof degree === 'number') {
    this.degree = degree
    context.rotate((degree * Math.PI) / 180)
    switch (degree) {
      case -90:
        context.translate(-height, 0)
        break
      case 90:
        context.translate(0, -width)
        break
      case -180:
      case 180:
        context.translate(-width, -height)
        break
      default:
    }
  }
}
Draw.prototype = {
  scale (width, height, canvas = this.canvas) {
    const w = canvas.width
    const h = canvas.height
    width = width || w
    height = height || h
    if (width !== w || height !== h) {
      const tmpCanvas = document.createElement('canvas')
      const tmpContext = tmpCanvas.getContext('2d')
      tmpCanvas.width = width
      tmpCanvas.height = height
      tmpContext.drawImage(canvas, 0, 0, w, h, 0, 0, width, height)
      canvas = tmpCanvas
    }
    return canvas
  },
  rotate (degree, image = this.canvas) {
    degree = ~~degree
    if (degree !== 0) {
      const maxDegree = 180
      const minDegree = -90
      if (degree > maxDegree) {
        degree = maxDegree
      } else if (degree < minDegree) {
        degree = minDegree
      }

      const canvas = document.createElement('canvas')
      const context = canvas.getContext('2d')
      const height = image.height
      const width = image.width
      const degreePI = (degree * Math.PI) / 180

      switch (degree) {
        // 逆时针旋转90°
        case -90:
          canvas.width = height
          canvas.height = width
          context.rotate(degreePI)
          context.drawImage(image, -width, 0)
          break
        // 顺时针旋转90°
        case 90:
          canvas.width = height
          canvas.height = width
          context.rotate(degreePI)
          context.drawImage(image, 0, -height)
          break
        // 顺时针旋转180°
        case 180:
          canvas.width = width
          canvas.height = height
          context.rotate(degreePI)
          context.drawImage(image, -width, -height)
          break
        default:
      }
      image = canvas
    }
    return image
  },
  getPNGImage (canvas = this.canvas) {
    return canvas.toDataURL('image/png')
  },
  getJPGImage (canvas = this.canvas) {
    return canvas.toDataURL('image/jpeg', 0.5)
  },
  downloadPNGImage (image) {
    const url = image.replace('image/png', 'image/octet-stream;Content-Disposition:attachment;filename=test.png')
    window.location.href = url
  },
  dataURLtoBlob (dataURL) {
    const arr = dataURL.split(',')
    const mime = arr[0].match(/:(.*?);/)[1]
    const bStr = atob(arr[1])
    let n = bStr.length
    const u8arr = new Uint8Array(n)
    while (n--) {
      u8arr[n] = bStr.charCodeAt(n)
    }
    return new Blob([u8arr], { type: mime })
  },
  clear () {
    let width
    let height
    switch (this.degree) {
      case -90:
      case 90:
        width = this.height
        height = this.width
        break
      default:
        width = this.width
        height = this.height
    }
    this.context.clearRect(0, 0, width, height)
  },
  upload (blob, url, success, failure) {
    const formData = new FormData()
    const xhr = new XMLHttpRequest()
    xhr.withCredentials = true
    formData.append('image', blob, 'sign')

    xhr.open('POST', url, true)
    xhr.onload = () => {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
        success(xhr.responseText)
      } else {
        failure()
      }
    }
    xhr.onerror = (e) => {
      if (typeof failure === 'function') {
        failure(e)
      } else {
        console.log(`upload img error: ${e}`)
      }
    }
    xhr.send(formData)
  }
}
export default Draw

@Louiszhai
Copy link
Owner

感谢反馈,晚点我在iphone手机上测试下

@Louiszhai
Copy link
Owner

请问你的手机是什么版本?

@alterhu2020
Copy link

alterhu2020 commented Nov 30, 2018

谢谢作者 @Louiszhai , 是直接在chrome的开发工具上测试的,没有手机测试,我等会给你手机测试的结果。

Edit: 刚才在我的手机IPhone 6 Plus上测试也是一样,画布不起作用,不能签名。 这个应该跟设备没有太大关系,直接使用chrome的模拟器就可以测试。 真不太明白是哪里没有加载出现这个问题的?
刷新就可以在画布上面签名了。。。。

@alterhu2020
Copy link

@Louiszhai ,我看到你的这个项目已经有一年没有更新了,还能看到你的回复真的挺意外的。不晓得你对于这个问题还有印象没? 这个问题我感觉大部分人使用可能都会遇到这个问题,需要刷新下页面画布才能签名。否则画布不起作用。。。。

@Louiszhai
Copy link
Owner

Louiszhai commented Nov 30, 2018

稍等,我刚空下来,在看

@alterhu2020
Copy link

@Louiszhai , 多谢!

@Louiszhai
Copy link
Owner

Louiszhai commented Nov 30, 2018

首先,你的代码是完全能运行的,还没有发现无法签名的情况,从其他路由跳过来也能正常签名。
唯一的不同在于,以下代码我本地没有store,因此注释了
image
请确认下,是否还有其他问题?

@Louiszhai
Copy link
Owner

Louiszhai commented Nov 30, 2018

另外,补充下,项目代码拉下来默认存在两个路由//sign,两个路由之前的跳转也不会导致出现无法签名的情况,猜你可能是别的原因。

@Louiszhai
Copy link
Owner

刚又测试了,使用this.$router.push({name: 'page1'})跳转至你粘贴的代码生成的路由,可以直接签名。
我使用chrome直接测试。

@alterhu2020
Copy link

@Louiszhai 嗯? 这个奇了怪了,我重新检查下现在。。。。 谢谢

@Louiszhai
Copy link
Owner

Louiszhai commented Nov 30, 2018

我在项目中也是使用的这段代码,实际上,项目中的场景要更加复杂。
当时项目中工作流如下:

  1. 上个页面按下按钮,唤出签名页面
  2. 签名页旋转90°加载(因为app强制不横屏),并正常签名
  3. 签名后,直接进入预览页,然后才是提交到服务器

跟你的操作基本一致,目前线上没有收到无法签名反馈。
应该是兼容到iphone低版本的,以及Android 4.0左右
希望能给你带来信心~

@alterhu2020
Copy link

alterhu2020 commented Nov 30, 2018

@Louiszhai 有什么办法debug这个问题吗?
我现在实在没有发现什么特别的代码或者组件没有加载,但是看这个问题应该是哪里没有加载?所以刷新页面就可以签名成功了,代码都是一样的啊,移植到自己的项目跳转到页面就是不能签名,需要刷新才能签名。

@Louiszhai
Copy link
Owner

方便将你的项目剥离出一个简单的无法签名版本吗?
我尝试运行看看

@alterhu2020
Copy link

alterhu2020 commented Nov 30, 2018

还有一个问题就是发现如果签名的页面存在顶部返回按钮,这个时候的签名的笔画下来的是有一点的偏移的,这个是在这段代码里面的,这个应该怎么计算位置让画笔画下去的时候不会偏移:

 getHorizontalStyle () {
      const d = document
      const w = window.innerWidth || d.documentElement.clientWidth || d.body.clientWidth
      const h = window.innerHeight || d.documentElement.clientHeight || d.body.clientHeight
      let length = (h - w) / 2
      let width = w
      let height = h

      switch (this.degree) {
        case -90:
          length = -length
        /* eslint no-fallthrough: "error" */
        case 90:
          width = h
          height = w
          break
        default:
          length = 0
      }
      if (this.canvasBox) {
        this.canvasBox.removeChild(document.querySelector('canvas'))
        this.canvasBox.appendChild(document.createElement('canvas'))
        /* eslint  vue/no-async-in-computed-properties: "off" */
        setTimeout(() => {
          console.log(`测试执行`)
          this.initCanvas()
        }, 200)
      }
      return {
        transform: `rotate(${this.degree}deg) translate(${length}px,${length}px)`,
        width: `${width}px`,
        height: `${height}px`,
        transformOrigin: 'center center'
      }
    }

我现在就建一个项目,你帮我看下。多谢!

@alterhu2020
Copy link

@Louiszhai ,我新建了一个项目: https://github.com/ForkProjects/error-signature , 操作步骤如下:

$ cnpm i 
$ npm run start
$ 打开页面:http://127.0.0.1:3000
$ 点击“进入签名页面”按钮
$ 在对应页面画布签名不起作用
$ 刷新当前页面重新签名成功

重现几率100%
耽误你时间了,非常感谢!

@Louiszhai
Copy link
Owner

终于等到了...

@Louiszhai
Copy link
Owner

你引用的组件内有包含canvas名称的组件,因此每次运行vue会抛出如下错误:
image
猜测可能是这个原因,导致原生canvas解析变慢了,延迟100毫秒左右去执行initCanvas,便能成功初始化画布。
另外,建议把无关组件全部注释掉,然后试着debug,祝顺利~

@alterhu2020
Copy link

alterhu2020 commented Dec 1, 2018

@Louiszhai , 谢谢你的回复,你晚上工作的很晚啊!

这个签名的页面的代码是跟你的基本一样的代码。。。我又尝试了你提到的方法:

  1. 将对应的签名页面的getHorizontalStyle属性中的 this.initCanvas()设置了时间我100毫秒,重新运行,还是一样直接进去画布不能签名
  2. 将所有的无关代码注释掉,尝试重新运行,还是不行。

麻烦你能再帮我看下这个问题吗?新的修改的仓库代码在这里: https://github.com/ForkProjects/error-signature , 已经是最新的重现这个问题的代码。

Edit: debug发现getHorizontalStyle属性中的下面代码实际是没有执行:

if (this.canvasBox) {
        this.canvasBox.removeChild(document.querySelector('canvas'))
        this.canvasBox.appendChild(document.createElement('canvas'))
        /* eslint  vue/no-async-in-computed-properties: "off" */
        setTimeout(() => {
          console.log(`测试执行`)
          this.initCanvas()
        }, 100)
      }

@Louiszhai
Copy link
Owner

Louiszhai commented Dec 1, 2018 via email

@alterhu2020
Copy link

好的,非常感谢,我看看 , @Louiszhai

@alterhu2020
Copy link

alterhu2020 commented Dec 1, 2018

还是不行! 找不到原因了!

@leehave
Copy link

leehave commented May 18, 2019

我也遇到了,暂时是在initCanvas时先将画布清空。执行一次this.draw.clear(); 就没有这种情况了

@zzlit
Copy link

zzlit commented Aug 11, 2020

同样遇到了,模拟出了一种复现的情况:

<canvas ref="signature" style="width: 100%; height: 100%;" v-show="show"></canvas>

mounted() {
  this.init()
  setTimeout(() => {
    this.show = true
  }, 50)
}

methods: {
  init() {
    const canvas = this.$refs.signature;
    this.draw = new Draw(canvas, 0);
  }
}

添加了一个50毫秒的延迟,然后初始化还是正常初始化就可以模拟出这种情况,然后解决结果如下:

  // 源码中这个width height 是 100% 而不是 px 单位的,所以我在原型上加了一个判断方法
  let { width, height } = window.getComputedStyle(canvas, null); 

  Draw.prototype = {
    isDraw() {
    // 然后在调用这个的地方判断如果是false则重新初始化一遍就可以了
      if (this.width === '100%' && this.height === '100%') return false
      ...
    }
  }

@zzlit
Copy link

zzlit commented Aug 12, 2020

同样遇到了,模拟出了一种复现的情况:

<canvas ref="signature" style="width: 100%; height: 100%;" v-show="show"></canvas>

mounted() {
  this.init()
  setTimeout(() => {
    this.show = true
  }, 50)
}

methods: {
  init() {
    const canvas = this.$refs.signature;
    this.draw = new Draw(canvas, 0);
  }
}

添加了一个50毫秒的延迟,然后初始化还是正常初始化就可以模拟出这种情况,然后解决结果如下:

  // 源码中这个width height 是 100% 而不是 px 单位的,所以我在原型上加了一个判断方法
  let { width, height } = window.getComputedStyle(canvas, null); 

  Draw.prototype = {
    isDraw() {
    // 然后在调用这个的地方判断如果是false则重新初始化一遍就可以了
      if (this.width === '100%' && this.height === '100%') return false
      ...
    }
  }

更新一下,H5上又发现了一种复现情况,当上一个页面有滚动条的时候,如果滚动条是滑在下面进入签名页面的时候,这时候就不能签名,猜测其实是签了,可能是因为滚动条的原因导致坐标异常了看不到签的位置,如果在进入签名页面的时候把scrollTop置为0就可以正常签名了。

@Louiszhai
Copy link
Owner

辛苦,签名时,一定要剔除滚动条的干扰

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants