カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

react-native-svgで手書きアプリを作ろう

f:id:kaminashi-developer:20210428102834p:plain カミナシの浦岡です。弊社が開発している「カミナシ」には、 以下のような用途を想定して、手書きメモ機能を組み込んでいます。

  • カメラで撮影した写真の上に矢印マークやメモを追加したい
  • キーボード入力に不慣れなユーザーでも素早く簡単にメモを録りたい
  • 手書きで入力できる署名欄

ちょっとした機能ですが、弊社アプリのように現場作業で使われるケースにおいて何かと重宝されている機能です。

今回、react-native-svgを使った実装について紹介します。

手書き線の表現

手書きされた一筆、一筆をsvgのpathタグを使用して表現することにします。

pathタグはd属性に指定された座標群の情報を基に線を描画してくれます。

https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/d

例えば、以下のように3点の座標を指定すると、それらを結ぶ線ができあがります。

gyazo.com

手書きイベントの取得

手書きの際のジェスチャーイベントの取得にはreact-nativeのPanResponderを使用します。

手書き対象のviewにPanResponderを設定することで以下の図のような順でイベントが取得できます。

gyazo.com

  1. 画面へのタッチ開始を起点に、pathタグを生成します。
  2. 指が移動している最中も、先ほどのd属性の値を指の軌跡に沿って更新することで表現できます。
  3. この時点で1つのpathタグを表現するための座標群が確定するので、確定情報として保持します。

コード

import React from 'react'
import { View, PanResponder, StyleSheet, PanResponderInstance, GestureResponderEvent } from 'react-native'
import Svg, { G, Path } from 'react-native-svg'

const pointsToSvg = (points: { x: number; y: number }[]) => {
  // 筆跳ね防止のための閾値
  const distanceThreshold = 40

  const filteredPoints = points.filter((point, index) => {
    if (!points[index - 1]) return true
    const distance = Math.sqrt(Math.pow(points[index - 1].x - point.x, 2) + Math.pow(points[index - 1].y - point.y, 2))
    return distance < distanceThreshold
  })

  if (!filteredPoints.length) {
    return ''
  }

  // svg-pathのd属性を生成
  let path = `M ${filteredPoints[0].x},${filteredPoints[0].y}`
  for (let point of filteredPoints) {
    path = `${path} L ${point.x},${point.y}`
  }

  return path
}

export default function App() {
  // 現在、一筆書きしている最中のパスの座標郡
  const [points, setPoints] = React.useState<{ x: number; y: number }[]>([])
  // 書き終わったパス情報の配列
  const [paths, setPaths] = React.useState<{ d: string }[]>([])

  const panResponder: PanResponderInstance = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onMoveShouldSetPanResponder: () => true,
    onPanResponderGrant: (event: GestureResponderEvent) => {
      if (!event.nativeEvent.touches.length) {
        return
      }
      setPoints([...points, { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY }])
    },
    onPanResponderMove: (event: GestureResponderEvent) => {
      if (!event.nativeEvent.touches.length) {
        return
      }
      setPoints([...points, { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY }])
    },
    onPanResponderRelease: () => {
      setPoints([])
      setPaths([
        ...paths,
        {
          d: pointsToSvg(points),
        },
      ])
    },
  })

  return (
    <View style={styles.container} {...panResponder.panHandlers}>
      <Svg width="100%" height="100%" preserveAspectRatio="none">
        <G>
          {/** 書き終わったパス情報の描画 */}
          {paths.map((path, index) => (
            <Path
              key={index}
              d={path.d}
              stroke="black"
              strokeWidth="3"
              strokeLinecap="round"
              strokeLinejoin="round"
              fill="none"
            />
          ))}
          {/** 現在、一筆書きしている最中のパスの描画 */}
          <Path
            d={pointsToSvg(points)}
            stroke="red"
            strokeWidth="3"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeDasharray="4"
            fill="none"
          />
        </G>
      </Svg>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    width: '100%',
    height: '100%',
  },
})

動作イメージ

gyazo.com

最後に

以上、シンプルな手書きアプリができました。 もっと多機能にしてみたい!と興味持ってもらえた方がいたら、ぜひ自分向けの手書きアプリにカスタマイズしてみてください!