I'm facing a task where I want to place a draggable marker on a background image and afterwards get the coordinates of the marker within the background image.
I've followed this neat tutorial to make a draggable marker using Animated.Image
, PanResponder
and Animated.ValueXY
. The problem is that I cannot figure out how to limit the draggable view to only move around within the boundaries of its parent (the background image).
Any help is very much appreciated :)
Best regards
Jens
Here is one way to do it using the react-native-gesture-responder.
import React, { Component } from 'react'
import {
StyleSheet,
Animated,
View,
} from 'react-native'
import { createResponder } from 'react-native-gesture-responder'
const styles = StyleSheet.create({
container: {
height: '100%',
width: '100%',
},
draggable: {
height: 50,
width: 50,
},
})
export default class WorldMap extends Component {
constructor(props) {
super(props)
this.state = {
x: new Animated.Value(0),
y: new Animated.Value(0),
}
}
componentWillMount() {
this.Responder = createResponder({
onStartShouldSetResponder: () => true,
onStartShouldSetResponderCapture: () => true,
onMoveShouldSetResponder: () => true,
onMoveShouldSetResponderCapture: () => true,
onResponderMove: (evt, gestureState) => {
this.pan(gestureState)
},
onPanResponderTerminationRequest: () => true,
})
}
pan = (gestureState) => {
const { x, y } = this.state
const maxX = 250
const minX = 0
const maxY = 250
const minY = 0
const xDiff = gestureState.moveX - gestureState.previousMoveX
const yDiff = gestureState.moveY - gestureState.previousMoveY
let newX = x._value + xDiff
let newY = y._value + yDiff
if (newX < minX) {
newX = minX
} else if (newX > maxX) {
newX = maxX
}
if (newY < minY) {
newY = minY
} else if (newY > maxY) {
newY = maxY
}
x.setValue(newX)
y.setValue(newY)
}
render() {
const {
x, y,
} = this.state
const imageStyle = { left: x, top: y }
return (
<View
style={styles.container}
>
<Animated.Image
source={require('./img.png')}
{...this.Responder}
resizeMode={'contain'}
style={[styles.draggable, imageStyle]}
/>
</View>
)
}
}
I accomplished this another way using only the react-native PanResponder and Animated libraries. It took a number of steps to accomplish and was difficult to figure out based on the docs, however, it is working well on both platforms and seems decently performant.
The first step was to find the height, width, x, and y of the parent element (which in my case was a View). View takes an onLayout prop. onLayout={this.onLayoutContainer}
Here is the function where I get the size of the parent and then setState to the values, so I have it available in the next function.
` onLayoutContainer = async (e) => {
await this.setState({
width: e.nativeEvent.layout.width,
height: e.nativeEvent.layout.height,
x: e.nativeEvent.layout.x,
y: e.nativeEvent.layout.y
})
this.initiateAnimator()
}`
At this point, I had the parent size and position on the screen, so I did some math and initiated a new Animated.ValueXY. I set the x and y of the initial position I wanted my image offset by, and used the known values to center my image in the element.
I continued setting up my panResponder with the appropriate values, however ultimately found that I had to interpolate the x and y values to provide boundaries it could operate in, plus 'clamp' the animation to not go outside of those boundaries. The entire function looks like this:
` initiateAnimator = () => {
this.animatedValue = new Animated.ValueXY({x: this.state.width/2 - 50, y: ((this.state.height + this.state.y ) / 2) - 75 })
this.value = {x: this.state.width/2 - 50, y: ((this.state.height + this.state.y ) / 2) - 75 }
this.animatedValue.addListener((value) => this.value = value)
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: ( event, gestureState ) => true,
onMoveShouldSetPanResponder: (event, gestureState) => true,
onPanResponderGrant: ( event, gestureState) => {
this.animatedValue.setOffset({
x: this.value.x,
y: this.value.y
})
},
onPanResponderMove: Animated.event([ null, { dx: this.animatedValue.x, dy: this.animatedValue.y}]),
})
boundX = this.animatedValue.x.interpolate({
inputRange: [-10, deviceWidth - 100],
outputRange: [-10, deviceWidth - 100],
extrapolate: 'clamp'
})
boundY = this.animatedValue.y.interpolate({
inputRange: [-10, this.state.height - 90],
outputRange: [-10, this.state.height - 90],
extrapolate: 'clamp'
})
}`
The important variables here are boundX and boundY, as they are the interpolated values that will not go outside of the desired area. I then set up my Animated.Image with these values, which looks like this:
` <Animated.Image
{...this.panResponder.panHandlers}
style={{
transform: [{translateX: boundX}, {translateY: boundY}],
height: 100,
width: 100,
}}
resizeMode='contain'
source={eventEditing.resource.img_path}
/>`
The last thing I had to make sure, was that all of the values were available to the animation before it tried to render, so I put a conditional in my render method to check first for this.state.width
, otherwise, render a dummy view in the meantime. All of this together allowed me to accomplish the desired result, but like I said it seems overly verbose/involved to accomplish something that seems so simple - 'stay within my parent.'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With