Last active
December 28, 2020 07:31
-
-
Save ericnograles/d9af086e81512c948ba2154c5c20902e to your computer and use it in GitHub Desktop.
Signature Pad Implementation for React Native
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
import React from 'react'; | |
import { | |
Dimensions, | |
Image, | |
StyleSheet, | |
Text, | |
TouchableOpacity, | |
View, | |
} from 'react-native'; | |
import { | |
takeSnapshotAsync, | |
} from 'expo'; | |
import SignatureView from './SignatureView'; | |
import IconButton from './IconButton'; | |
export default class SignatureScreen extends React.Component { | |
static route = { | |
navigationBar: { | |
visible: false, | |
}, | |
} | |
state = { | |
result: null, | |
width: Dimensions.get('window').width | |
} | |
_cancel = () => { | |
this._signatureView.cancel(); | |
} | |
_undo = () => { | |
this._signatureView.undo(); | |
} | |
_save = async () => { | |
let result = await takeSnapshotAsync(this._signatureView, {format: 'png', result: 'base64', quality: 1.0}); | |
this.setState({result}); | |
} | |
onLayout = () => { | |
// Forces re-render | |
this.setState({width: Dimensions.get('window').width}); | |
} | |
render() { | |
return ( | |
<View style={{flex: 1}} onLayout={this.onLayout}> | |
<SignatureView | |
ref={view => { this._signatureView = view; }} | |
containerStyle={{backgroundColor: 'rgba(0,0,0,0.01)', marginTop: 60}} | |
width={this.state.width} | |
height={200} | |
/> | |
{this.state.result && ( | |
<Image | |
source={{uri: `data:image/png;base64,${this.state.result}`}} | |
style={{width: Dimensions.get('window').width / 2, height: 100}} | |
/> | |
)} | |
{this._renderHeader()} | |
</View> | |
); | |
} | |
_renderHeader() { | |
return ( | |
<View style={{position: 'absolute', top: 0, left: 0, right:0, height: 50}}> | |
<View style={styles.header}> | |
<View style={styles.headerLeft}> | |
<IconButton | |
onPress={this._cancel} | |
name="cancel" | |
/> | |
</View> | |
<View style={styles.headerRight}> | |
<IconButton | |
onPress={this._undo} | |
name="undo" | |
/> | |
<IconButton | |
onPress={this._save} | |
name="done" | |
/> | |
</View> | |
</View> | |
</View> | |
) | |
} | |
} | |
let styles = StyleSheet.create({ | |
header: { | |
paddingTop: 16, | |
flex: 1, | |
flexDirection: 'row', | |
justifyContent: 'space-between', | |
alignItems: 'center', | |
}, | |
headerRight: { | |
flexDirection: 'row', | |
marginRight: 8 + 12, | |
}, | |
headerLeft: { | |
flexDirection: 'row', | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
import React from 'react'; | |
import { View, PanResponder, StyleSheet } from 'react-native'; | |
import Svg, { G, Surface, Path } from 'react-native-svg'; | |
export default class SignatureView extends React.Component { | |
constructor(props, context) { | |
super(props, context); | |
this.state = { | |
currentMax: 1, | |
currentPoints: [], | |
donePaths: [], | |
newPaths: [], | |
reaction: new Reaction() | |
}; | |
this._panResponder = PanResponder.create({ | |
onStartShouldSetPanResponder: (evt, gs) => true, | |
onMoveShouldSetPanResponder: (evt, gs) => true, | |
onPanResponderGrant: (evt, gs) => this.onResponderGrant(evt, gs), | |
onPanResponderMove: (evt, gs) => this.onResponderMove(evt, gs), | |
onPanResponderRelease: (evt, gs) => this.onResponderRelease(evt, gs) | |
}); | |
} | |
onTouch(evt) { | |
let [x, y] = [evt.nativeEvent.pageX, evt.nativeEvent.pageY]; | |
let newCurrentPoints = this.state.currentPoints; | |
newCurrentPoints.push({ x, y }); | |
this.setState({ | |
donePaths: this.state.donePaths, | |
currentPoints: newCurrentPoints, | |
currentMax: this.state.currentMax | |
}); | |
} | |
cancel = () => { | |
this.setState({donePaths: []}); | |
}; | |
undo = () => { | |
this.setState({donePaths: []}); | |
}; | |
onResponderGrant(evt) { | |
this.onTouch(evt); | |
} | |
onResponderMove(evt) { | |
this.onTouch(evt); | |
} | |
onResponderRelease() { | |
let newPaths = this.state.donePaths; | |
if (this.state.currentPoints.length > 0) { | |
// Cache the shape object so that we aren't testing | |
// whether or not it changed; too many components? | |
newPaths.push( | |
<Path | |
key={this.state.currentMax} | |
d={this.state.reaction.pointsToSvg(this.state.currentPoints)} | |
stroke="#000000" | |
strokeWidth={4} | |
fill="none" | |
/> | |
); | |
} | |
this.state.reaction.addGesture(this.state.currentPoints); | |
this.setState({ | |
donePaths: newPaths, | |
currentPoints: [], | |
currentMax: this.state.currentMax + 1 | |
}); | |
} | |
_onLayoutContainer = e => { | |
this.state.reaction.setOffset(e.nativeEvent.layout); | |
}; | |
render() { | |
return ( | |
<View | |
onLayout={this._onLayoutContainer} | |
style={[ | |
styles.drawContainer, | |
this.props.containerStyle, | |
{ width: this.props.width, height: this.props.height } | |
]} | |
> | |
<View {...this._panResponder.panHandlers}> | |
<Svg | |
style={styles.drawSurface} | |
width={this.props.width} | |
height={this.props.height} | |
> | |
<G> | |
{this.state.donePaths.map(donePath => donePath)}; | |
<Path | |
key={this.state.currentMax} | |
d={this.state.reaction.pointsToSvg(this.state.currentPoints)} | |
stroke="#000000" | |
strokeWidth={4} | |
fill="none" | |
/> | |
</G> | |
</Svg> | |
{this.props.children} | |
</View> | |
</View> | |
); | |
} | |
} | |
class Reaction { | |
constructor(gestures) { | |
this.gestures = gestures || []; | |
this.reset(); | |
this._offsetX = 0; | |
this._offsetY = 0; | |
} | |
addGesture(points) { | |
if (points.length > 0) { | |
this.gestures.push(points); | |
} | |
} | |
setOffset(options) { | |
this._offsetX = options.x; | |
this._offsetY = options.y; | |
} | |
pointsToSvg(points) { | |
let offsetX = this._offsetX; | |
let offsetY = this._offsetY; | |
if (points.length > 0) { | |
var path = `M${points[0].x - offsetX},${points[0].y - offsetY}`; | |
points.forEach(point => { | |
path = path + ` L${point.x - offsetX},${point.y - offsetY}`; | |
}); | |
return path; | |
} else { | |
return ''; | |
} | |
} | |
replayLength() { | |
return this.replayedGestures.length; | |
} | |
reset() { | |
this.replayedGestures = [[]]; | |
} | |
empty() { | |
return this.gestures.length === 0; | |
} | |
copy() { | |
return new Reaction(this.gestures.slice()); | |
} | |
done() { | |
return ( | |
this.empty() || | |
(this.replayedGestures.length === this.gestures.length && | |
this.lastReplayedGesture().length === | |
this.gestures[this.gestures.length - 1].length) | |
); | |
} | |
lastReplayedGesture() { | |
return this.replayedGestures[this.replayedGestures.length - 1]; | |
} | |
stepGestureLength() { | |
let gestureIndex = this.replayedGestures.length - 1; | |
if (!this.gestures[gestureIndex]) { | |
return; | |
} | |
if ( | |
this.replayedGestures[gestureIndex].length >= | |
this.gestures[gestureIndex].length | |
) { | |
this.replayedGestures.push([]); | |
} | |
} | |
step() { | |
if (this.done()) { | |
return true; | |
} | |
this.stepGestureLength(); | |
let gestureIndex = this.replayedGestures.length - 1; | |
let pointIndex = this.replayedGestures[gestureIndex].length; | |
let point = this.gestures[gestureIndex][pointIndex]; | |
this.replayedGestures[gestureIndex].push(point); | |
return false; | |
} | |
} | |
let styles = StyleSheet.create({ | |
drawContainer: {}, | |
drawSurface: { | |
backgroundColor: 'transparent' | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment