react-native-gesture-handler pan/drag and rotate wrong values when rotated - javascript

I made a single component based on the react-native-gesture-handler examples and they are working pretty well. I can transform, scale, move and rotate the image. But as soon as I rotate the image for 90° as an example, I receive wrong values for translateX and translateY. Moving down will move it right, swiping up will move it left etc.
How do I take the rotation into consideration based on my component.
Please ignore the "tilt" feature, it's not in use for now.
import React from 'react';
import {Animated, StyleSheet} from 'react-native';
import {PanGestureHandler, PinchGestureHandler, RotationGestureHandler, State,} from 'react-native-gesture-handler';
export class PinchableBox extends React.Component {
panRef = React.createRef();
rotationRef = React.createRef();
pinchRef = React.createRef();
dragRef = React.createRef();
constructor(props) {
super(props);
this.state = {
_isMounted: false
};
/* Pinching */
this._baseScale = new Animated.Value(1);
this._pinchScale = new Animated.Value(1);
this._scale = Animated.multiply(this._baseScale, this._pinchScale);
this._lastScale = 1;
this._onPinchGestureEvent = Animated.event(
[{nativeEvent: {scale: this._pinchScale}}],
{useNativeDriver: true}
);
/* Rotation */
this._rotate = new Animated.Value(0);
this._rotateStr = this._rotate.interpolate({
inputRange: [-100, 100],
outputRange: ['-100rad', '100rad'],
});
this._lastRotate = 0;
this._onRotateGestureEvent = Animated.event(
[{nativeEvent: {rotation: this._rotate}}],
{useNativeDriver: true}
);
/* Tilt */
this._tilt = new Animated.Value(0);
this._tiltStr = this._tilt.interpolate({
inputRange: [-501, -500, 0, 1],
outputRange: ['1rad', '1rad', '0rad', '0rad'],
});
this._lastTilt = 0;
this._onTiltGestureEvent = Animated.event(
[{nativeEvent: {translationY: this._tilt}}],
{useNativeDriver: false}
);
this._translateX = new Animated.Value(0);
this._translateY = new Animated.Value(0);
this._lastOffset = {x: 0, y: 0};
this._onGestureEvent = Animated.event(
[
{
nativeEvent: {
translationX: this._translateX,
translationY: this._translateY,
},
},
],
{useNativeDriver: true}
);
}
_onRotateHandlerStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastRotate += event.nativeEvent.rotation;
this._rotate.setOffset(this._lastRotate);
this._rotate.setValue(0);
}
};
_onPinchHandlerStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastScale *= event.nativeEvent.scale;
this._baseScale.setValue(this._lastScale);
this._pinchScale.setValue(1);
}
};
_onTiltGestureStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastTilt += event.nativeEvent.translationY;
this._tilt.setOffset(this._lastTilt);
this._tilt.setValue(0);
}
};
_onHandlerStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastOffset.x += event.nativeEvent.translationX;
this._lastOffset.y += event.nativeEvent.translationY;
this._translateX.setOffset(this._lastOffset.x);
this._translateX.setValue(0);
this._translateY.setOffset(this._lastOffset.y);
this._translateY.setValue(0);
}
};
render() {
const {image} = this.props;
return (
<PanGestureHandler
ref={this.dragRef}
simultaneousHandlers={[this.rotationRef, this.pinchRef]}
onGestureEvent={this._onGestureEvent}
minPointers={1}
maxPointers={2}
avgTouches
onHandlerStateChange={this._onHandlerStateChange}>
<Animated.View style={styles.wrapper}>
<RotationGestureHandler
ref={this.rotationRef}
simultaneousHandlers={this.pinchRef}
onGestureEvent={this._onRotateGestureEvent}
onHandlerStateChange={this._onRotateHandlerStateChange}>
<Animated.View style={styles.wrapper}>
<PinchGestureHandler
ref={this.pinchRef}
simultaneousHandlers={this.rotationRef}
onGestureEvent={this._onPinchGestureEvent}
onHandlerStateChange={this._onPinchHandlerStateChange}>
<Animated.View style={styles.container} collapsable={false}>
<Animated.Image
resizeMode={"contain"}
style={[
styles.pinchableImage,
{
transform: [
{scale: this._scale},
{rotate: this._rotateStr},
{rotateX: this._tiltStr},
{translateX: this._translateX},
{translateY: this._translateY},
],
},
]}
source={{uri: image}}
/>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
</RotationGestureHandler>
</Animated.View>
</PanGestureHandler>
);
}
}
export default PinchableBox;
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'transparent',
overflow: 'hidden',
alignItems: 'center',
flex: 1,
justifyContent: 'center'
},
pinchableImage: {
...StyleSheet.absoluteFillObject,
},
wrapper: {
flex: 1,
},
});

The solution was actually pretty easy. Instead of adding all computed values to one View, I had to chunk them into a single view per gesture.
import React from 'react';
import {Animated, StyleSheet} from 'react-native';
import {PanGestureHandler, PinchGestureHandler, RotationGestureHandler, State,} from 'react-native-gesture-handler';
export class PinchableBox extends React.Component {
panRef = React.createRef();
rotationRef = React.createRef();
pinchRef = React.createRef();
dragRef = React.createRef();
constructor(props) {
super(props);
this.state = {
_isMounted: false
};
/* Pinching */
this._baseScale = new Animated.Value(1);
this._pinchScale = new Animated.Value(1);
this._scale = Animated.multiply(this._baseScale, this._pinchScale);
this._lastScale = 1;
this._onPinchGestureEvent = Animated.event(
[{nativeEvent: {scale: this._pinchScale}}],
{useNativeDriver: true}
);
/* Rotation */
this._rotate = new Animated.Value(0);
this._rotateStr = this._rotate.interpolate({
inputRange: [-100, 100],
outputRange: ['-100rad', '100rad'],
});
this._lastRotate = 0;
this._onRotateGestureEvent = Animated.event(
[{nativeEvent: {rotation: this._rotate}}],
{useNativeDriver: true}
);
/* Tilt */
this._tilt = new Animated.Value(0);
this._tiltStr = this._tilt.interpolate({
inputRange: [-501, -500, 0, 1],
outputRange: ['1rad', '1rad', '0rad', '0rad'],
});
this._lastTilt = 0;
this._onTiltGestureEvent = Animated.event(
[{nativeEvent: {translationY: this._tilt}}],
{useNativeDriver: true}
);
this._translateX = new Animated.Value(0);
this._translateY = new Animated.Value(0);
this._lastOffset = {x: 0, y: 0};
this._onGestureEvent = Animated.event(
[
{
nativeEvent: {
translationX: this._translateX,
translationY: this._translateY,
},
},
],
{useNativeDriver: true}
);
}
_onRotateHandlerStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastRotate += event.nativeEvent.rotation;
this._rotate.setOffset(this._lastRotate);
this._rotate.setValue(0);
}
};
_onPinchHandlerStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastScale *= event.nativeEvent.scale;
this._baseScale.setValue(this._lastScale);
this._pinchScale.setValue(1);
}
};
_onTiltGestureStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastTilt += event.nativeEvent.translationY;
this._tilt.setOffset(this._lastTilt);
this._tilt.setValue(0);
}
};
_onHandlerStateChange = event => {
if (event.nativeEvent.oldState === State.ACTIVE) {
this._lastOffset.x += event.nativeEvent.translationX;
this._lastOffset.y += event.nativeEvent.translationY;
this._translateX.setOffset(this._lastOffset.x);
this._translateX.setValue(0);
this._translateY.setOffset(this._lastOffset.y);
this._translateY.setValue(0);
}
};
render() {
const {image, children} = this.props;
return (
<React.Fragment>
<PanGestureHandler
ref={this.dragRef}
simultaneousHandlers={[this.rotationRef, this.pinchRef]}
onGestureEvent={this._onGestureEvent}
minPointers={2}
maxPointers={2}
avgTouches
onHandlerStateChange={this._onHandlerStateChange}>
<Animated.View style={[
styles.wrapper,
{
transform: [
{translateX: this._translateX},
{translateY: this._translateY},
],
},
]}>
<RotationGestureHandler
ref={this.rotationRef}
simultaneousHandlers={this.pinchRef}
onGestureEvent={this._onRotateGestureEvent}
onHandlerStateChange={this._onRotateHandlerStateChange}>
<Animated.View style={[
styles.wrapper,
{
transform: [
{rotate: this._rotateStr},
],
},
]}
>
<PinchGestureHandler
ref={this.pinchRef}
simultaneousHandlers={this.rotationRef}
onGestureEvent={this._onPinchGestureEvent}
onHandlerStateChange={this._onPinchHandlerStateChange}>
<Animated.View style={[
styles.container,
{
transform: [
{scale: this._scale},
],
},
]} collapsable={false}>
<Animated.Image
resizeMode={"contain"}
style={[
styles.pinchableImage,
]}
source={{uri: image}}
/>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
</RotationGestureHandler>
</Animated.View>
</PanGestureHandler>
{ children }
</React.Fragment>
);
}
}
export default PinchableBox;
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'transparent',
overflow: 'hidden',
alignItems: 'center',
flex: 1,
justifyContent: 'center'
},
pinchableImage: {
backgroundColor: "transparent",
...StyleSheet.absoluteFillObject,
},
wrapper: {
flex: 1,
},
});

Related

ComponentDidMount vs useEffect

There is a class components work piece. I want to convert it into a function component and implement it in the same way, but there's a problem.
Unlike the class type, the function type returns to the place where it was created without staying after the picture has moved.
This is the function components.
const [dogs, setDogs] = useState([])
function addDog(){
// setDogs([...dogs, {id: dogCount++}])
dogCount++;
console.log(dogCount)
// console.log(dogs)
const newDog = {id: dogCount, bottom: 125}
setDogs([...dogs, newDog])
}
return (
<View style={styles.entirebottomsheet}>
<View style={styles.container}>
<TouchableOpacity onPress={()=>{addDog();}} style={styles.dogButton}>
<Text>Dog</Text>
</TouchableOpacity>
{dogs.map(dog => {
return (
<DogContainer
key={dog.id}
style={{bottom: dog.bottom, position: 'absolute'}}
/>
);
})}
</View>
</View>
);
function DogContainer () {
const positionX = new Animated.Value(0);
const positionY = new Animated.Value(0);
// DogContainer.defaultProps = {
// onComplete() {}
// }
const getDogStyle = () => ({
transform: [{translateX: positionX}],
});
useEffect(() => {
Animated.spring(positionX, {
duration: 1000,
toValue: width / 3,
easing: Easing.ease,
useNativeDriver: true,
}).start();
Animated.spring(positionY, {
duration: 1000,
toValue: width / 3,
easing: Easing.ease,
useNativeDriver: true,
}).start();
return (
console.log('finish')
)
}, []);
return (
<Animated.View style={[{ bottom: 125},{
transform: [{translateX: positionX}, {translateY: positionY}],
}]}>
<Image source={dogImg} style={{width: 60, height: 60}}/>
</Animated.View>
);
}
This is the class components.
export default class App extends React.Component {
// let {up} = this.props.up;
state = {
dogs: [],
cats: [],
chicks: [],
};
addDog = () => {
console.log(...this.state.dogs);
this.setState(
{
dogs: [
...this.state.dogs,
{
id: dogCount,
bottom: 125,
},
],
},
() => {
dogCount++;
},
);
};
removeDog = id => {
this.setState({
dogs: this.state.dogs.filter(dog => {
return dog.id !== id;
}),
});
};
addCat = () => {
console.log(...this.state.cats);
this.setState(
{
cats: [
...this.state.cats,
{
id: catCount,
bottom: 125,
},
],
},
() => {
catCount++;
},
);
};
removeCat = id => {
this.setState({
cats: this.state.cats.filter(cat => {
return cat.id !== id;
}),
});
};
addChick = () => {
console.log(...this.state.chicks);
this.setState(
{
chicks: [
...this.state.chicks,
{
id: chickCount,
bottom: 125,
},
],
},
() => {
chickCount++;
},
);
};
removeChick = id => {
this.setState({
chicks: this.state.chicks.filter(chick => {
return chick.id !== id;
}),
});
};
render() {
return (
<GestureHandlerRootView>
<View style={styles.entirebottomsheet}>
<View style={styles.container}>
<TouchableOpacity onPress={this.addDog} style={styles.dogButton}>
<Text>Dog</Text>
</TouchableOpacity>
{this.state.dogs.map(dog => {
return (
<DogContainer
key={dog.id}
style={{bottom: dog.bottom, position: 'absolute'}}
onComplete={() => this.removeDog(dog.id)}
/>
);
})}
</View>
<View style={styles.container}>
<TouchableOpacity onPress={this.addCat} style={styles.catButton}>
<Text>Cat</Text>
</TouchableOpacity>
{this.state.cats.map(cat => {
return (
<CatContainer
key={cat.id}
style={{bottom: cat.bottom, position: 'absolute'}}
onComplete={() => this.removeCat(cat.id)}
/>
);
})}
</View>
<View style={styles.container}>
<TouchableOpacity onPress={this.addChick} style={styles.chickButton}>
<Text style={{position: 'absolute'}}>Chick</Text>
</TouchableOpacity>
{this.state.chicks.map(chick => {
return (
<ChickContainer
key={chick.id}
style={{bottom: chick.bottom, position: 'absolute'}}
onComplete={() => this.removeChick(chick.id)}
/>
);
})}
</View>
</View>
</GestureHandlerRootView>
);
}
}
class DogContainer extends React.Component {
state = {
position: new Animated.ValueXY({
x: 0,
y: 0,
}),
};
static defaultProps = {
onComplete() {},
};
componentDidMount() {
Animated.spring(this.state.position.x, {
duration: 1000,
toValue: width / 3,
easing: Easing.ease,
useNativeDriver: true,
}).start(this.props.onComplete);
Animated.spring(this.state.position.y, {
duration: 1000,
toValue: -340,
easing: Easing.ease,
useNativeDriver: true,
}).start(this.props.onComplete);
}
getDogStyle() {
return {
transform: [
{translateY: this.state.position.y},
{translateX: this.state.position.x},
],
};
}
render() {
return (
<Animated.View style={[this.getDogStyle(), this.props.style]}>
<Image source={dogImg} style={{width: 60, height: 60, borderRadius: 30}} />
</Animated.View>
);
}
}
Try to use useRef() hook
function DogContainer () {
const positionX = useRef(new Animated.Value(0)).current;
const positionY = useRef(new Animated.Value(0)).current;
...
In the Class-based version of DogContainer the position coordinate was a part of state.
class DogContainer extends React.Component {
state = {
position: new Animated.ValueXY({
x: 0,
y: 0,
}),
};
static defaultProps = {
onComplete() {},
};
componentDidMount() {
Animated.spring(this.state.position.x, {
duration: 1000,
toValue: width / 3,
easing: Easing.ease,
useNativeDriver: true,
}).start(this.props.onComplete);
Animated.spring(this.state.position.y, {
duration: 1000,
toValue: -340,
easing: Easing.ease,
useNativeDriver: true,
}).start(this.props.onComplete);
}
getDogStyle() {
return {
transform: [
{translateY: this.state.position.y},
{translateX: this.state.position.x},
],
};
}
render() {
return (
<Animated.View style={[this.getDogStyle(), this.props.style]}>
<Image source={dogImg} style={{width: 60, height: 60, borderRadius: 30}} />
</Animated.View>
);
}
}
Function component using the useState hook
function DogContainer ({ onComplete = () => {} }) {
const [position, setPosition] = React.useState(
new Animated.ValueXY({
x: 0,
y: 0,
})
);
const getDogStyle = () => ({
transform: [{
translateX: position.x,
translateY: position.y,
}],
});
useEffect(() => {
Animated.spring(position.x, {
duration: 1000,
toValue: width / 3,
easing: Easing.ease,
useNativeDriver: true,
}).start(onComplete);
Animated.spring(position.y, {
duration: 1000,
toValue: width / 3,
easing: Easing.ease,
useNativeDriver: true,
}).start(onComplete);
console.log('finish');
}, []);
return (
<Animated.View
style={[
{ bottom: 125},
{ transform: [
{ translateX: position.x },
{ translateY: position.y }
],
}
]}
>
<Image source={dogImg} style={{ width: 60, height: 60 }} />
</Animated.View>
);
}

React Native Wheel of Fortune

I am developing the react native application which contains the wheel of fortune. There are two wheels in my application. Wheel#1 is running smoothly but in Wheel#2 is being required to add Inner & Outer wheels. I am using D3 shape and React Native SVG packages to achieve my goal.
import React, {Component} from 'react';
import {
View,
StyleSheet,
Dimensions,
Animated,
TouchableOpacity,
Image,
} from 'react-native';
import * as d3Shape from 'd3-shape';
import Svg, {G, Text, TSpan, Path, Pattern} from 'react-native-svg';
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
const {width, height} = Dimensions.get('screen');
class WheelOfFortune extends Component {
constructor(props) {
super(props);
this.state = {
enabled: false,
started: false,
finished: false,
winner: null,
gameScreen: new Animated.Value(width - 40),
wheelOpacity: new Animated.Value(1),
imageLeft: new Animated.Value(width / 2 - 30),
imageTop: new Animated.Value(height / 2 - 70),
};
this.angle = 0;
this.prepareWheel();
}
prepareWheel = () => {
this.Rewards = this.props.options.rewards;
this.RewardCount = this.Rewards.length;
this.numberOfSegments = this.RewardCount;
this.fontSize = 20;
this.oneTurn = 360;
this.angleBySegment = this.oneTurn / this.numberOfSegments;
this.angleOffset = this.angleBySegment / 2;
this.winner = this.props.options.winner
? this.props.options.winner
: Math.floor(Math.random() * this.numberOfSegments);
this._wheelPaths = this.makeWheel();
this._angle = new Animated.Value(0);
this.props.options.onRef(this);
};
resetWheelState = () => {
this.setState({
enabled: false,
started: false,
finished: false,
winner: null,
gameScreen: new Animated.Value(width - 40),
wheelOpacity: new Animated.Value(1),
imageLeft: new Animated.Value(width / 2 - 30),
imageTop: new Animated.Value(height / 2 - 70),
});
};
_tryAgain = () => {
this.prepareWheel();
this.resetWheelState();
this.angleListener();
this._onPress();
};
angleListener = () => {
this._angle.addListener(event => {
if (this.state.enabled) {
this.setState({
enabled: false,
finished: false,
});
}
this.angle = event.value;
});
};
componentWillUnmount() {
this.props.options.onRef(undefined);
}
componentDidMount() {
this.angleListener();
}
makeWheel = () => {
const data = Array.from({length: this.numberOfSegments}).fill(1);
const arcs = d3Shape.pie()(data);
var colors = this.props.options.colors
? this.props.options.colors
: [
'#E07026',
'#E8C22E',
'#ABC937',
'#4F991D',
'#22AFD3',
'#5858D0',
'#7B48C8',
'#D843B9',
'#E23B80',
'#D82B2B',
];
return arcs.map((arc, index) => {
const instance = d3Shape
.arc()
.padAngle(0.01)
.outerRadius(width / 2)
.innerRadius(this.props.options.innerRadius || 100);
return {
path: instance(arc),
color: colors[index % colors.length],
value: this.Rewards[index],
centroid: instance.centroid(arc),
};
});
};
_getWinnerIndex = () => {
const deg = Math.abs(Math.round(this.angle % this.oneTurn));
// wheel turning counterclockwise
if (this.angle < 0) {
return Math.floor(deg / this.angleBySegment);
}
// wheel turning clockwise
return (
(this.numberOfSegments - Math.floor(deg / this.angleBySegment)) %
this.numberOfSegments
);
};
_onPress = () => {
const duration = this.props.options.duration || 10000;
this.setState({
started: true,
});
Animated.timing(this._angle, {
toValue:
365 -
this.winner * (this.oneTurn / this.numberOfSegments) +
360 * (duration / 1000),
duration: duration,
useNativeDriver: true,
}).start(() => {
const winnerIndex = this._getWinnerIndex();
this.setState({
finished: true,
winner: this._wheelPaths[winnerIndex].value,
});
this.props.getWinner(this._wheelPaths[winnerIndex].value, winnerIndex);
});
};
_textRender = (x, y, number, i) => (
<Text
x={x - number.length * 5}
y={y - 80}
fill={
this.props.options.textColor ? this.props.options.textColor : '#fff'
}
textAnchor="middle"
fontSize={this.fontSize}>
{Array.from({length: number.length}).map((_, j) => {
// Render reward text vertically
if (this.props.options.textAngle === 'vertical') {
return (
<TSpan x={x} dy={this.fontSize} key={`arc-${i}-slice-${j}`}>
{number.charAt(j)}
</TSpan>
);
}
// Render reward text horizontally
else {
return (
<TSpan
y={y - 40}
dx={this.fontSize * 0.07}
key={`arc-${i}-slice-${j}`}>
{number.charAt(j)}
</TSpan>
);
}
})}
</Text>
);
_renderSvgWheel = () => {
return (
<View style={styles.container}>
{this._renderKnob()}
<Animated.View
style={{
alignItems: 'center',
justifyContent: 'center',
transform: [
{
rotate: this._angle.interpolate({
inputRange: [-this.oneTurn, 0, this.oneTurn],
outputRange: [
`-${this.oneTurn}deg`,
`0deg`,
`${this.oneTurn}deg`,
],
}),
},
],
backgroundColor: this.props.options.backgroundColor
? this.props.options.backgroundColor
: '#fff',
width: width - 20,
height: width - 20,
borderRadius: (width - 20) / 2,
borderWidth: this.props.options.borderWidth
? this.props.options.borderWidth
: 2,
borderColor: this.props.options.borderColor
? this.props.options.borderColor
: '#fff',
opacity: this.state.wheelOpacity,
}}>
<AnimatedSvg
width={this.state.gameScreen}
height={this.state.gameScreen}
viewBox={`0 0 ${width} ${width}`}
style={{
transform: [{rotate: `-${this.angleOffset}deg`}],
margin: 10,
}}>
<G y={width / 2} x={width / 2}>
{this._wheelPaths.map((arc, i) => {
const [x, y] = arc.centroid;
const number = arc.value.toString();
return (
<G key={`arc-${i}`}>
<Path d={arc.path} strokeWidth={2} fill={arc.color} />
<G
rotation={
(i * this.oneTurn) / this.numberOfSegments +
this.angleOffset
}
origin={`${x}, ${y}`}>
{this._textRender(x, y, number, i)}
</G>
</G>
);
})}
</G>
</AnimatedSvg>
</Animated.View>
</View>
);
};
_renderKnob = () => {
const knobSize = this.props.options.knobSize
? this.props.options.knobSize
: 20;
// [0, this.numberOfSegments]
const YOLO = Animated.modulo(
Animated.divide(
Animated.modulo(
Animated.subtract(this._angle, this.angleOffset),
this.oneTurn,
),
new Animated.Value(this.angleBySegment),
),
1,
);
return (
<Animated.View
style={{
width: knobSize,
height: knobSize * 2,
justifyContent: 'flex-end',
zIndex: 1,
opacity: this.state.wheelOpacity,
transform: [
{
rotate: YOLO.interpolate({
inputRange: [-1, -0.5, -0.0001, 0.0001, 0.5, 1],
outputRange: [
'0deg',
'0deg',
'35deg',
'-35deg',
'0deg',
'0deg',
],
}),
},
],
}}>
<Svg
width={knobSize}
height={(knobSize * 100) / 57}
viewBox={`0 0 57 100`}
style={{
transform: [{translateY: 8}],
}}>
<Image
source={
this.props.options.knobSource
? this.props.options.knobSource
: require('../assets/images/knob.png')
}
style={{ width: knobSize, height: (knobSize * 100) / 57 }}
/>
</Svg>
</Animated.View>
);
};
_renderTopToPlay() {
if (this.state.started == false) {
return (
<TouchableOpacity onPress={() => this._onPress()}>
{this.props.options.playButton()}
</TouchableOpacity>
);
}
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={{
position: 'absolute',
width: width,
height: height / 2,
justifyContent: 'center',
alignItems: 'center',
}}>
<Animated.View style={[styles.content, {padding: 10}]}>
{this._renderSvgWheel()}
</Animated.View>
</TouchableOpacity>
{this.props.options.playButton ? this._renderTopToPlay() : null}
</View>
);
}
}
export default WheelOfFortune;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
content: {},
startText: {
fontSize: 50,
color: '#fff',
fontWeight: 'bold',
textShadowColor: 'rgba(0, 0, 0, 0.4)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10,
},
});
End Result Look like this
Now I want to add an Inner wheel to it Like this

How to set dynamic height for each row with react-native-swipe-list-view?

Description
I am working on a react-native project using expo SDK36.
I want to do a swipe left/right list view. I use react-native-swipe-list-view to achieve it.
So far everything worked perfectly, the default example uses a fixed height: 50 per row, while I want to set the height of each row dynamically.
Every attempt where a failure, note that I already use <SwipeListView recalculateHiddenLayout={true} />
This is bad for the UX, since the default line is having a small height: 50, it is nearly impossible to drag the line on iOS and android properly.
Reproduction
Snack: https://snack.expo.io/#kopax/react-native-swipe-list-view-408
import React from 'react';
import { Dimensions, Text, View, StyleSheet } from 'react-native';
import Constants from 'expo-constants';
// You can import from local files
import SwipeListView from './components/SwipeListView';
// or any pure javascript modules available in npm
import { Card } from 'react-native-paper';
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.paragraph}>
Change code in the editor and watch it change on your phone! Save to get a shareable url.
</Text>
<Card>
<SwipeListView
dimensions={Dimensions.get('window')}
listViewData={Array(20).fill('').map((d, i) => ({
...d,
title: `Item ${i}`,
description: `This is a very long description for item number #${i},
it should be so long that you cannot see all the content,
the issue is about fixing the dynamic height for each row`
}))
}
minHeight={200}
/>
</Card>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});
This is my components/SwipeListView.js
import React, { Component } from 'react';
import {
Animated,
Image,
StyleSheet,
TouchableOpacity,
TouchableHighlight,
View,
} from 'react-native';
import {
Avatar,
Button,
Text,
Title,
Subheading,
TouchableRipple,
withTheme,
} from 'react-native-paper';
import { SwipeListView as SwipeListViewDefault } from 'react-native-swipe-list-view';
/* eslint-disable react/prop-types, react/destructuring-assignment, react/no-access-state-in-setstate */
class SwipeListView extends Component {
leftBtnRatio = 0.25;
rightBtnRatio = 0.75;
constructor(props) {
super(props);
this.state = {
listType: 'FlatList',
listViewData: props.listViewData
.map((data, i) => ({ key: `${i}`, ...data })),
};
this.rowTranslateAnimatedValues = {};
props.listViewData
.forEach((data, i) => {
this.rowTranslateAnimatedValues[`${i}`] = new Animated.Value(1);
});
}
getStyles() {
const { minHeight, theme } = this.props;
const { colors } = theme;
return StyleSheet.create({
rowFrontContainer: {
overflow: 'hidden',
},
rowFront: {
alignItems: 'center',
backgroundColor: colors.surface,
borderBottomColor: colors.accent,
borderBottomWidth: 1,
justifyContent: 'center',
minHeight: '100%',
flex: 1,
},
rowBack: {
alignItems: 'center',
backgroundColor: colors.surface,
flexDirection: 'row',
justifyContent: 'space-between',
paddingLeft: 15,
minHeight: '100%',
},
backBtn: {
alignItems: 'center',
bottom: 0,
justifyContent: 'center',
position: 'absolute',
top: 0,
},
backLeftBtn: {
backgroundColor: colors.primary,
left: 0,
width: `${this.leftBtnRatio * 100}%`,
},
backRightBtn: {
backgroundColor: colors.accent,
right: 0,
width: `${this.rightBtnRatio * 100}%`,
},
});
}
onRowDidOpen = (rowKey) => {
console.log('This row opened', rowKey);
};
onSwipeValueChange = swipeData => {
const { dimensions } = this.props;
const { key, value } = swipeData;
if (value < -dimensions.width * this.rightBtnRatio && !this.animationIsRunning) {
this.animationIsRunning = true;
Animated.timing(this.rowTranslateAnimatedValues[key], {
toValue: 0,
duration: 200,
}).start(() => {
const newData = [...this.state.listViewData];
const prevIndex = this.state.listViewData.findIndex(item => item.key === key);
newData.splice(prevIndex, 1);
this.setState({listViewData: newData});
this.animationIsRunning = false;
});
}
};
closeRow(rowMap, rowKey) {
if (rowMap[rowKey]) {
rowMap[rowKey].closeRow();
}
}
deleteRow(rowMap, rowKey) {
this.closeRow(rowMap, rowKey);
const newData = [...this.state.listViewData];
const prevIndex = this.state.listViewData.findIndex(
(item) => item.key === rowKey,
);
newData.splice(prevIndex, 1);
this.setState({ listViewData: newData });
}
render() {
const { minHeight, dimensions, theme } = this.props;
const { colors } = theme;
const styles = this.getStyles();
return (
<SwipeListViewDefault
data={this.state.listViewData}
renderItem={data => (
<Animated.View
style={[styles.rowFrontContainer, {
height: this.rowTranslateAnimatedValues[data.item.key].interpolate({
inputRange: [0, 1],
outputRange: [0, minHeight],
})}]}
>
<TouchableRipple
onPress={() => console.log('You touched me')}
style={styles.rowFront}
underlayColor={colors.background}
>
<View>
<Title>{data.item.title}</Title>
<Text>
{data.item.description}
</Text>
</View>
</TouchableRipple>
</Animated.View>
)}
renderHiddenItem={(data, rowMap) => (
<View style={styles.rowBack}>
<TouchableOpacity
style={[
styles.backLeftBtn,
styles.backBtn,
]}
onPress={() => this.closeRow(rowMap, data.item.key)}
>
<Text>Tap pour annuler</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.backRightBtn,
styles.backBtn,
]}
onPress={() => this.deleteRow(rowMap, data.item.key)}
>
<Animated.View
style={[
styles.trash,
{
transform: [
{
scale: this.rowTranslateAnimatedValues[
data.item.key
].interpolate({
inputRange: [
45,
90,
],
outputRange: [0, 1],
extrapolate:
'clamp',
}),
},
],
},
]}
>
<Text>Swipe left to delete</Text>
</Animated.View>
</TouchableOpacity>
</View>
)}
leftOpenValue={dimensions.width * this.leftBtnRatio}
rightOpenValue={-dimensions.width * this.rightBtnRatio}
previewRowKey={'0'}
previewOpenValue={-40}
previewOpenDelay={3000}
onRowDidOpen={this.onRowDidOpen}
onSwipeValueChange={this.onSwipeValueChange}
recalculateHiddenLayout={true}
/>
);
}
}
export default withTheme(SwipeListView);
Expect
I expect when using recalculateHiddenLayout={true}, to get the hidden row height calculated dynamically
Result Screenshots
On the web, I am able to set the height:
but I when using iOS and Android, the height is forced.
Environment
OS: ios/android/web
RN Version: expo SDK36
How can I set the height of each row dynamically?
Important edit
The problem is the fixed value here in the animation:
height: this.rowTranslateAnimatedValues[data.item.key].interpolate({
inputRange: [0, 1],
outputRange: [0, 200], // <--- here
})}]}
I have replaced it in the example with props.minHeight:
height: this.rowTranslateAnimatedValues[data.item.key].interpolate({
inputRange: [0, 1],
outputRange: [0, this.props.minHeight],
})}]}
It doesn't permit dynamic height, how can I get the row height dynamically?

react-native can't open expo project

I am trying to run an expo project Collapsible Header with TabView it works fine in this link.
But when I copy and paste the code to my own expo project, it gives me an error.
Invariant Violation: Element type is invalid: expected a string or a class/function. Check the render method of App
The only thing I changed is class TabView to class App but it is giving me this error.
Also, if I want to build a native code project not expo. How would I run this code? because there is import {Constants } from expo this wouldn't work in react-native run-ios
import * as React from 'react';
import {
View,
StyleSheet,
Dimensions,
ImageBackground,
Animated,
Text
} from 'react-native';
import { Constants } from 'expo';
import { TabViewAnimated, TabBar } from 'react-native-tab-view'; // Version can be specified in package.json
import ContactsList from './components/ContactsList';
const initialLayout = {
height: 0,
width: Dimensions.get('window').width,
};
const HEADER_HEIGHT = 240;
const COLLAPSED_HEIGHT = 52 + Constants.statusBarHeight;
const SCROLLABLE_HEIGHT = HEADER_HEIGHT - COLLAPSED_HEIGHT;
export default class TabView extends React.Component {
constructor(props: Props) {
super(props);
this.state = {
index: 0,
routes: [
{ key: '1', title: 'First' },
{ key: '2', title: 'Second' },
{ key: '3', title: 'Third' },
],
scroll: new Animated.Value(0),
};
}
_handleIndexChange = index => {
this.setState({ index });
};
_renderHeader = props => {
const translateY = this.state.scroll.interpolate({
inputRange: [0, SCROLLABLE_HEIGHT],
outputRange: [0, -SCROLLABLE_HEIGHT],
extrapolate: 'clamp',
});
return (
<Animated.View style={[styles.header, { transform: [{ translateY }] }]}>
<ImageBackground
source={{ uri: 'https://picsum.photos/900' }}
style={styles.cover}>
<View style={styles.overlay} />
<TabBar {...props} style={styles.tabbar} />
</ImageBackground>
</Animated.View>
);
};
_renderScene = () => {
return (
<ContactsList
scrollEventThrottle={1}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scroll } } }],
{ useNativeDriver: true }
)}
contentContainerStyle={{ paddingTop: HEADER_HEIGHT }}
/>
);
};
render() {
return (
<TabViewAnimated
style={styles.container}
navigationState={this.state}
renderScene={this._renderScene}
renderHeader={this._renderHeader}
onIndexChange={this._handleIndexChange}
initialLayout={initialLayout}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, .32)',
},
cover: {
height: HEADER_HEIGHT,
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1,
},
tabbar: {
backgroundColor: 'rgba(0, 0, 0, .32)',
elevation: 0,
shadowOpacity: 0,
},
});
I got it worked! I had to replace TabViewAnimated to TabView.

How do I port an iOS index file in react native to android

I have the following code for my index.io.js file. I am new to react native and don't quite know how to port it to index.android.js. Is there a certain standard that I can follow to migrate this to the android version? What needs to be done exactly?
'use strict';
var ShakeEvent = require('react-native-shake-event-ios');
var NetworkImage = require('react-native-image-progress');
var Progress = require('react-native-progress');
var Swiper = require('react-native-swiper');
var RandManager = require('./RandManager.js');
var Utils = require('./Utils.js');
var React = require('react-native');
// Components
var ProgressHUD = require('./ProgressHUD.js');
var {
AppRegistry,
StyleSheet,
Text,
View,
Component,
ActivityIndicatorIOS,
Dimensions,
PanResponder,
CameraRoll,
AlertIOS
} = React;
var {width, height} = Dimensions.get('window');
const NUM_WALLPAPERS = 5;
const DOUBLE_TAP_DELAY = 300; // milliseconds
const DOUBLE_TAP_RADIUS = 20;
class SplashWalls extends Component{
constructor(props) {
super(props);
this.state = {
wallsJSON: [],
isLoading: true,
isHudVisible: false
};
this.imagePanResponder = {};
// Previous touch information
this.prevTouchInfo = {
prevTouchX: 0,
prevTouchY: 0,
prevTouchTimeStamp: 0
};
this.currentWallIndex = 0;
// Binding this to methods
this.handlePanResponderGrant = this.handlePanResponderGrant.bind(this);
this.onMomentumScrollEnd = this.onMomentumScrollEnd.bind(this);
}
componentDidMount() {
this.fetchWallsJSON();
}
componentWillMount() {
this.imagePanResponder = PanResponder.create({
onStartShouldSetPanResponder: this.handleStartShouldSetPanResponder,
onPanResponderGrant: this.handlePanResponderGrant,
onPanResponderRelease: this.handlePanResponderEnd,
onPanResponderTerminate: this.handlePanResponderEnd
});
// Fetch new wallpapers on shake
ShakeEvent.addEventListener('shake', () => {
this.initialize();
this.fetchWallsJSON();
});
}
render() {
var {isLoading} = this.state;
if(isLoading)
return this.renderLoadingMessage();
else
return this.renderResults();
}
initialize() {
this.setState({
wallsJSON: [],
isLoading: true,
isHudVisible: false
});
this.currentWallIndex = 0;
}
fetchWallsJSON() {
var url = 'http://unsplash.it/list';
fetch(url)
.then( response => response.json() )
.then( jsonData => {
var randomIds = RandManager.uniqueRandomNumbers(NUM_WALLPAPERS, 0, jsonData.length);
var walls = [];
randomIds.forEach(randomId => {
walls.push(jsonData[randomId]);
});
this.setState({
isLoading: false,
wallsJSON: [].concat(walls)
});
})
.catch( error => console.log('JSON Fetch error : ' + error) );
}
renderLoadingMessage() {
return (
<View style={styles.loadingContainer}>
<ActivityIndicatorIOS
animating={true}
color={'#fff'}
size={'small'}
style={{margin: 15}} />
<Text style={{color: '#fff'}}>Contacting Unsplash</Text>
</View>
);
}
saveCurrentWallpaperToCameraRoll() {
// Make Progress HUD visible
this.setState({isHudVisible: true});
var {wallsJSON} = this.state;
var currentWall = wallsJSON[this.currentWallIndex];
var currentWallURL = `http://unsplash.it/${currentWall.width}/${currentWall.height}?image=${currentWall.id}`;
CameraRoll.saveImageWithTag(currentWallURL, (data) => {
// Hide Progress HUD
this.setState({isHudVisible: false});
AlertIOS.alert(
'Saved',
'Wallpaper successfully saved to Camera Roll',
[
{text: 'High 5!', onPress: () => console.log('OK Pressed!')}
]
);
},(err) =>{
console.log('Error saving to camera roll', err);
});
}
onMomentumScrollEnd(e, state, context) {
this.currentWallIndex = state.index;
}
renderResults() {
var {wallsJSON, isHudVisible} = this.state;
return (
<View>
<Swiper
dot={<View style={{backgroundColor:'rgba(255,255,255,.4)', width: 8, height: 8,borderRadius: 10, marginLeft: 3, marginRight: 3, marginTop: 3, marginBottom: 3,}} />}
activeDot={<View style={{backgroundColor: '#fff', width: 13, height: 13, borderRadius: 7, marginLeft: 7, marginRight: 7}} />}
onMomentumScrollEnd={this.onMomentumScrollEnd}
index={this.currentWallIndex}
loop={false}>
{wallsJSON.map((wallpaper, index) => {
return(
<View key={wallpaper.id}>
<NetworkImage
source={{uri: `https://unsplash.it/${wallpaper.width}/${wallpaper.height}?image=${wallpaper.id}`}}
indicator={Progress.Circle}
indicatorProps={{
color: 'rgba(255, 255, 255)',
size: 60,
thickness: 7
}}
threshold={0}
style={styles.wallpaperImage}
{...this.imagePanResponder.panHandlers}>
<Text style={styles.label}>Photo by</Text>
<Text style={styles.label_authorName}>{wallpaper.author}</Text>
</NetworkImage>
</View>
);
})}
</Swiper>
<ProgressHUD width={width} height={height} isVisible={isHudVisible}/>
</View>
);
}
// Pan Handler methods
handleStartShouldSetPanResponder(e, gestureState){
return true;
}
handlePanResponderGrant(e, gestureState) {
var currentTouchTimeStamp = Date.now();
if( this.isDoubleTap(currentTouchTimeStamp, gestureState) )
this.saveCurrentWallpaperToCameraRoll();
this.prevTouchInfo = {
prevTouchX: gestureState.x0,
prevTouchY: gestureState.y0,
prevTouchTimeStamp: currentTouchTimeStamp
};
}
handlePanResponderEnd(e, gestureState) {
// When finger is pulled up from the screen
}
isDoubleTap(currentTouchTimeStamp, {x0, y0}) {
var {prevTouchX, prevTouchY, prevTouchTimeStamp} = this.prevTouchInfo;
var dt = currentTouchTimeStamp - prevTouchTimeStamp;
return (dt < DOUBLE_TAP_DELAY && Utils.distance(prevTouchX, prevTouchY, x0, y0) < DOUBLE_TAP_RADIUS);
}
};
var styles = StyleSheet.create({
loadingContainer: {
flexDirection: 'row',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#000'
},
wallpaperImage: {
flex: 1,
width: width,
height: height,
backgroundColor: '#000'
},
label: {
position: 'absolute',
color: '#fff',
fontSize: 13,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 2,
paddingLeft: 5,
top: 20,
left: 20,
width: width/2
},
label_authorName: {
position: 'absolute',
color: '#fff',
fontSize: 15,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 2,
paddingLeft: 5,
top: 41,
left: 20,
fontWeight: 'bold',
width: width/2
}
});
AppRegistry.registerComponent('SplashWalls', () => SplashWalls);
Copy content of index.ios.js to index.android.js first. If it does not contain any iOS specific code/libs, it will just work. If not, go to step 2.
Adjust code to use Android specific code, or lib API - if such code exists. If not, go to step 3.
Either swap iOS code/libs for Android ones, or write required code yourself.

Categories

Resources