カミナシの浦岡です。弊社が開発している「カミナシ」には、 以下のような用途を想定して、手書きメモ機能を組み込んでいます。
- カメラで撮影した写真の上に矢印マークやメモを追加したい
- キーボード入力に不慣れなユーザーでも素早く簡単にメモを録りたい
- 手書きで入力できる署名欄
ちょっとした機能ですが、弊社アプリのように現場作業で使われるケースにおいて何かと重宝されている機能です。
今回、react-native-svgを使った実装について紹介します。
手書き線の表現
手書きされた一筆、一筆をsvgのpathタグを使用して表現することにします。
pathタグはd属性に指定された座標群の情報を基に線を描画してくれます。
https://developer.mozilla.org/ja/docs/Web/SVG/Attribute/d
例えば、以下のように3点の座標を指定すると、それらを結ぶ線ができあがります。
手書きイベントの取得
手書きの際のジェスチャーイベントの取得にはreact-nativeのPanResponderを使用します。
手書き対象のviewにPanResponderを設定することで以下の図のような順でイベントが取得できます。
- 画面へのタッチ開始を起点に、pathタグを生成します。
- 指が移動している最中も、先ほどのd属性の値を指の軌跡に沿って更新することで表現できます。
- この時点で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%', }, })
動作イメージ
最後に
以上、シンプルな手書きアプリができました。 もっと多機能にしてみたい!と興味持ってもらえた方がいたら、ぜひ自分向けの手書きアプリにカスタマイズしてみてください!