カミナシの浦岡です。弊社が開発している「カミナシ」には、
以下のような用途を想定して、手書きメモ機能を組み込んでいます。
- カメラで撮影した写真の上に矢印マークやメモを追加したい
- キーボード入力に不慣れなユーザーでも素早く簡単にメモを録りたい
- 手書きで入力できる署名欄
ちょっとした機能ですが、弊社アプリのように現場作業で使われるケースにおいて何かと重宝されている機能です。
今回、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%',
},
})
動作イメージ
最後に
以上、シンプルな手書きアプリができました。 もっと多機能にしてみたい!と興味持ってもらえた方がいたら、ぜひ自分向けの手書きアプリにカスタマイズしてみてください!


