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.
Related
I am developing a Meals App refering to a React Native - The Practical Guide [2023]
course in Udemy. While using a native route prop i am facin with the mentioned error i.e can't find itemData
Can some help me to figure out where this code is going wrong.
CategoriesScreen.js
import { FlatList, StyleSheet } from "react-native";
import { CATEGORIES } from "../data/dummy-data";
import CategoryGridTile from "../components/CategoryGridTile";
function CategoriesScreen({ navigation }) {
function pressHandler() {
navigation.navigate("Meals Overview", { categoryId: itemData.item.id, });
}
function renderCategoryItem(itemData) {
return (
<CategoryGridTile
title={itemData.item.title}
color={itemData.item.color}
onPress={pressHandler}
/>
);
}
return (
<FlatList
data={CATEGORIES}
keyExtractor={(item) => item.id}
renderItem={renderCategoryItem}
numColumns={2}
/>
);
}
export default CategoriesScreen;
MealsOverviewScreen
import { View,Text,StyleSheet } from "react-native";
import {MEALS} from "../data/dummy-data"
function MealsOverviewScreen({route}){
const catId=route.params.categoryId;
return(
<View style={styles.container}>
<Text>Meals Overview Screen - {catId}</Text>
</View>
)
}
export default MealsOverviewScreen;
const styles=StyleSheet.create(
{
container:{
flex:1,
padding:16,
}
}
)
CategoryGridTile.js
import { Pressable, View, Text, StyleSheet, Platform } from "react-native";
function CategoryGridTile({ title, color,onPress}) {
return (
<View style={styles.gridItem}>
<Pressable
style={({ pressed }) => [styles.buttonStyle,pressed?styles.buttonPressed:null,]}
android_ripple={{ color: "#ccc" }}
onPress={onPress}
>
<View style={[styles.innerContainer,{backgroundColor:color }]}>
<Text style={styles.title}>{title}</Text>
</View>
</Pressable>
</View>
);
}
export default CategoryGridTile;
const styles = StyleSheet.create({
gridItem: {
flex: 1,
margin: 16,
height: 150,
borderRadius: 8,
elevation: 4,
backgroundColor: "white",
shadowColor: "black",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 8,
overflow: Platform.OS == "android" ? "hidden" : "visible",
},
buttonPressed: {
opacity: 0.25,
},
buttonStyle: {
flex: 1,
},
innerContainer: {
flex: 1,
padding: 16,
borderRadius:8,
alignItems: "center",
justifyContent: "center",
},
title: {
fontSize: 18,
fontWeight: "bold",
},
});
Issue is in pressHandler function . There is no itemData variable declared and initialised in function but still you are trying to use it ..
You can change code in CategoriesScreen function created in CategoriesScreen.js as mentioned below
From
function renderCategoryItem(itemData) {
return (
<CategoryGridTile
title={itemData.item.title}
color={itemData.item.color}
onPress={pressHandler}
/>
);
}
To
function renderCategoryItem(itemData) {
return (
<CategoryGridTile
title={itemData.item.title}
color={itemData.item.color}
onPress={()=>{
pressHandler(itemData)
}}
/>
);
}
From
function pressHandler() {
navigation.navigate("Meals Overview", { categoryId: itemData.item.id, });
}
To
function pressHandler(itemData) {
navigation.navigate("Meals Overview", { categoryId: itemData.item.id, });
}
I deployed this repository to netlify and this url displayed my chart as expected on chrome with my macbook pro.
however if I open the url, it display nothing.
Actually, when I see victory chart official doc site, I can see the graphs both of devices.
so I'm wondering why my site dosen't work on iphone.
you can check my code on [my repo](this repository)
the essential part of the code is below.
import React from 'react';
import {
StringOrNumberOrCallback,
VictoryAxis,
VictoryChart,
VictoryLine,
} from 'victory';
const Chart: React.FC = () => {
const profitHistoryList = [
{
datetime: '2021-12-31 00:00:00',
cash: 10001.53,
id: 57,
},
{
datetime: '2022-01-03 00:00:00',
cash: 10001.53,
id: 58,
},
{
datetime: '2022-01-18 00:00:00',
cash: 9999.72,
id: 59,
},
{
datetime: '2022-01-19 00:00:00',
cash: 9999.72,
id: 60,
},
];
const data = profitHistoryList.map((p) => ({
x: new Date(p.datetime),
y: p.cash,
}));
const areaColor: StringOrNumberOrCallback =
data[data.length - 1].y > 10000 ? 'green' : 'red';
const { innerHeight: height } = window;
return (
<td className="w-40 md:w-full">
<VictoryChart
style={{
background: { fill: 'black' },
}}
height={(height * 1) / 12}
scale={{ x: 'time' }}
padding={{ top: 10, left: 10, right: 10, bottom: 10 }}
>
<VictoryLine
data={data}
style={{
data: {
stroke: areaColor,
},
}}
/>
<VictoryAxis dependentAxis style={{ axis: { stroke: 'none' } }} />
<VictoryAxis style={{ axis: { stroke: 'none' } }} />
</VictoryChart>
</td>
);
};
export default Chart;
import Position from 'Position';
import { VFC } from 'react';
import './App.css';
import './index.css';
const App: VFC = () => (
<div className="App">
<Position />
</div>
);
export default App;
as i am new in react native. i have no much knowledge of class component. i was stuck in code as class components are used in this code but i want to convert them into functional components. anyone please help me to convert this given code into functional component. this is a code of a swipeable card in react native all the given code in class component and use of constructor and this. i want to just convert it into functional component.
//This is an example of Tinder like Swipeable Card//
import React, { Component } from 'react';
//import react in our code.
import {
Platform, StyleSheet, View, Text,
Dimensions, Animated, PanResponder,
} from 'react-native';
//import all the components we are going to use.
const SCREEN_WIDTH = Dimensions.get('window').width;
class SwipeableCard extends React.Component {
constructor() {
super();
this.panResponder;
this.state = {
Xposition: new Animated.Value(0),
RightText: false,
LeftText: false,
};
this.Card_Opacity = new Animated.Value(1);
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderMove: (evt, gestureState) => {
this.state.Xposition.setValue(gestureState.dx);
if (gestureState.dx > SCREEN_WIDTH - 250) {
this.setState({
RightText: true,
LeftText: false,
});
} else if (gestureState.dx < -SCREEN_WIDTH + 250) {
this.setState({
LeftText: true,
RightText: false,
});
}
},
onPanResponderRelease: (evt, gestureState) => {
if (
gestureState.dx < SCREEN_WIDTH - 150 &&
gestureState.dx > -SCREEN_WIDTH + 150
) {
this.setState({
LeftText: false,
RightText: false,
});
Animated.spring(
this.state.Xposition,
{
toValue: 0,
speed: 5,
bounciness: 10,
},
{ useNativeDriver: true }
).start();
} else if (gestureState.dx > SCREEN_WIDTH - 150) {
Animated.parallel(
[
Animated.timing(this.state.Xposition, {
toValue: SCREEN_WIDTH,
duration: 200,
}),
Animated.timing(this.Card_Opacity, {
toValue: 0,
duration: 200,
}),
],
{ useNativeDriver: true }
).start(() => {
this.setState({ LeftText: false, RightText: false }, () => {
this.props.removeCard();
});
});
} else if (gestureState.dx < -SCREEN_WIDTH + 150) {
Animated.parallel(
[
Animated.timing(this.state.Xposition, {
toValue: -SCREEN_WIDTH,
duration: 200,
}),
Animated.timing(this.Card_Opacity, {
toValue: 0,
duration: 200,
}),
],
{ useNativeDriver: true }
).start(() => {
this.setState({ LeftText: false, RightText: false }, () => {
this.props.removeCard();
});
});
}
},
});
}
render() {
const rotateCard = this.state.Xposition.interpolate({
inputRange: [-200, 0, 200],
outputRange: ['-20deg', '0deg', '20deg'],
});
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
styles.card_Style,
{
backgroundColor: this.props.item.backgroundColor,
opacity: this.Card_Opacity,
transform: [
{ translateX: this.state.Xposition },
{ rotate: rotateCard },
],
},
]}>
<Text style={styles.Card_Title}> {this.props.item.card_Title} </Text>
{this.state.LeftText ? (
<Text style={styles.Left_Text_Style}> Left Swipe </Text>
) : null}
{this.state.RightText ? (
<Text style={styles.Right_Text_Style}> Right Swipe </Text>
) : null}
</Animated.View>
);
}
}
export default class App extends React.Component {
constructor() {
super();
this.state = {
Sample_Card_Array: [{
id: '1', card_Title: 'Card 1', backgroundColor: '#FFC107',
}, {
id: '2', card_Title: 'Card 2', backgroundColor: '#ED2525',
}, {
id: '3', card_Title: 'Card 3', backgroundColor: '#E7088E',
}, {
id: '4', card_Title: 'Card 4', backgroundColor: '#00BCD4',
}, {
id: '5', card_Title: 'Card 5', backgroundColor: '#FFFB14',
}],
No_More_Card: false,
};
}
componentDidMount() {
this.setState({
Sample_Card_Array: this.state.Sample_Card_Array.reverse(),
});
if (this.state.Sample_Card_Array.length == 0) {
this.setState({ No_More_Card: true });
}
}
removeCard = id => {
this.state.Sample_Card_Array.splice(
this.state.Sample_Card_Array.findIndex(x => x.id == id),
1
);
this.setState({ Sample_Card_Array: this.state.Sample_Card_Array }, () => {
if (this.state.Sample_Card_Array.length == 0) {
this.setState({ No_More_Card: true });
}
});
};
render() {
return (
<View style={styles.MainContainer}>
{this.state.Sample_Card_Array.map((item, key) => (
<SwipeableCard
key={key}
item={item}
removeCard={this.removeCard.bind(this, item.id)}
/>
))}
{this.state.No_More_Card ? (
<Text style={{ fontSize: 22, color: '#000' }}>No Cards Found.</Text>
) : null}
</View>
);
}
}
const styles = StyleSheet.create({
MainContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: Platform.OS === 'ios' ? 20 : 0,
},
card_Style: {
width: '75%',
height: '45%',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
borderRadius: 7,
},
Card_Title: {
color: '#fff',
fontSize: 24,
},
Left_Text_Style: {
top: 22,
right: 32,
position: 'absolute',
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
backgroundColor: 'transparent',
},
Right_Text_Style: {
top: 22,
left: 32,
position: 'absolute',
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
backgroundColor: 'transparent',
},
});
the part in the render method is what you return.
to create stateObjects in functional components you will need to use the useState method
const functionalComponent = (props)=>{//props are passed in via props arg...
const defaultState = Xposition: new Animated.Value(0),
RightText: false,
LeftText: false
}
const [state,setState] = useState(defaultState);
... // more stuff
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
styles.card_Style,
{
backgroundColor: props.item.backgroundColor,
opacity: Card_Opacity,
transform: [
{ translateX: state.Xposition },
{ rotate: rotateCard },
],
},
]}>
<Text style={styles.Card_Title}> {props.item.card_Title} </Text>
{this.state.LeftText ? (
<Text style={styles.Left_Text_Style}> Left Swipe </Text>
) : null}
{this.state.RightText ? (
<Text style={styles.Right_Text_Style}> Right Swipe </Text>
) : null}
</Animated.View>
);
}
you should really go watch some videos on useState, you can be much more granular
to set the state you will need to use the setState method returned from the useState call : setState({..state,{XPosition:55}) or something ... you do the ...state to include the old state values, as the state variable will be overwritten with exactly what you pass in... it wont "update" the existing state it will overwrite it
the next bit is hooking into the functionality in componentDidMount you can do this with useEffect
useEffect(()=>{ // this is setup
// do the stuff from componentDidMount
return ()=>{
// any required teardown can be done here
},[] //[] signifies only do this when component mounts... not every update
);// end useEffect componentDidMount
again there is alot more to useEffect, if you want to do stuff when specific state or props are updated
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?
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,
},
});