import React, { useRef, useState, useEffect } from "react";
import {
Animated,
Dimensions,
View,
StyleSheet,
PanResponder,
Text,
Image,
} from "react-native";
import { catarray } from "./categoryimages";
const { width, height } = Dimensions.get("screen");
const App = () => {
const pan = useRef(new Animated.ValueXY()).current;
const [currentIndex, setCurrentIndex] = useState(0);
const rotate = pan.x.interpolate({
inputRange: [-width / 2, 0, width / 2],
outputRange: ["-10deg", "0deg", "10deg"],
extrapolate: "clamp",
});
useEffect(() => {
pan.setValue({ x: 0, y: 0 });
}, [currentIndex]);
const nextCardOpacity = pan.x.interpolate({
inputRange: [-width / 2, 0, width / 2],
outputRange: [1, 0, 1],
extrapolate: "clamp",
});
const nextCardScale = pan.x.interpolate({
inputRange: [-width / 2, 0, width / 2],
outputRange: [1, 0.8, 1],
extrapolate: "clamp",
});
const renderImages = () => {
let rotateandtranslate = {
transform: [{ rotate: rotate }, ...pan.getTranslateTransform()],
};
return catarray
.map((item, index) => {
if (index < currentIndex) {
return null;
} else if (index === currentIndex) {
return (
<Animated.View
key={index}
style={[
rotateandtranslate,
{
width: width * 0.9,
height: height * 0.85,
position: "absolute",
},
]}
{...panResponder.panHandlers}
>
<Image
source={{ uri: item.uri }}
style={{
flex: 1,
height: null,
width: null,
borderRadius: 30,
}}
/>
</Animated.View>
);
} else {
return (
<Animated.View
key={index}
style={[
{
opacity: nextCardOpacity,
transform: [{ scale: nextCardScale }],
},
{
width: width * 0.9,
height: height * 0.85,
position: "absolute",
},
]}
>
<Image
source={{ uri: item.uri }}
style={{
flex: 1,
height: null,
width: null,
borderRadius: 30,
}}
/>
</Animated.View>
);
}
})
.reverse();
};
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
pan.setOffset({
x: pan.x._value,
y: pan.y._value,
});
},
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }]),
onPanResponderRelease: (e, gestureState) => {
if (gestureState.dx > 140) {
Animated.spring(pan, {
toValue: { x: width + width, y: gestureState.dy },
}).start(() => {
setCurrentIndex(currentIndex + 1);
});
} else if (gestureState.dx < -140) {
Animated.spring(pan, {
toValue: { x: -width - width, y: gestureState.dy },
}).start(() => {
setCurrentIndex(currentIndex + 1);
});
} else {
Animated.spring(pan, {
toValue: { x: 0, y: 0 },
friction: 2,
}).start();
}
},
})
).current;
return <View style={styles.container}>{renderImages()}</View>;
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
titleText: {
fontSize: 14,
lineHeight: 24,
fontWeight: "bold",
},
box: {
height: 150,
width: 150,
backgroundColor: "blue",
borderRadius: 5,
},
});
export default App;
I have been trying a tinder swipe animation. in react native hooks and everything works fine for the first two cards.But after that the currentIndex remains at "1" and is not updating.But i found out that the currentIndex value is reinitializing to 0 somehow.The value of currentIndex doesn't update even though im calling setCurrentIndex(currentIndex+1).Please help me if anyone knows where the problem is.Thank you.
Related
I have used the below model to make a handler rotate but cannot find a way to manipulate it as to make the handle rotate from where it begins instead of its center. As well, I am struggling to make the rotation occur with only one finger instead of two.
I think maybe the answer lies in the withAnchorPoint library but Iām not sure of how to apply it.
Requiring assistance šš¾
import React from 'react';
import {Animated, StatusBar, Image, ImageBackground, Dimensions, StyleSheet, View, Text} from "react-native";
import { PanGestureHandler, PinchGestureHandler, State, RotationGestureHandler } from 'react-native-gesture-handler';
//import { withAnchorPoint } from 'react-native-anchor-point';
import { USE_NATIVE_DRIVER } from '../../config';
const {width} = Dimensions.get('screen');
const SIZE = width * .9;
export default class MovableClock extends React.Component {
panRef = React.createRef();
rotationRef = React.createRef();
pinchRef = React.createRef();
constructor(props) {
super(props);
/* 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: USE_NATIVE_DRIVER }
);
/* 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: USE_NATIVE_DRIVER }
);
/* 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: USE_NATIVE_DRIVER }
);
}
_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);
}
};
render() {
return (
<View style={styles.background}>
<StatusBar hidden={true}/>
<View style={[styles.bigQuadran]}>
</View>
<Image style={styles.numbers}
source={require("../assets/nobgfreeclock2.png")}/>
<View style={[styles.mediumQuadran]}/>
<PanGestureHandler
ref={this.panRef}
onGestureEvent={this._onTiltGestureEvent}
onHandlerStateChange={this._onTiltGestureStateChange}
minDist={10}
minPointers={2}
maxPointers={2}
avgTouches>
<Animated.View style={[styles.mover
//{transform: [{ translateX: this.translateX, }, { translateY: this.translateY}]}
]}>
<RotationGestureHandler
ref={this.rotationRef}
simultaneousHandlers={this.pinchRef}
onGestureEvent={this._onRotateGestureEvent}
onHandlerStateChange={this._onRotateHandlerStateChange}
>
<Animated.View style={[styles.mover]}>
<PinchGestureHandler
ref={this.pinchRef}
simultaneousHandlers={this.rotationRef}
onGestureEvent={this._onPinchGestureEvent}
onHandlerStateChange={this._onPinchHandlerStateChange}
>
<Animated.View style={[styles.hours,
{transform: [
{ perspective: 200 },
{ scale: this._scale },
{ rotate: this._rotateStr },
{ rotateX: this._tiltStr },
]},
]}/>
</PinchGestureHandler>
</Animated.View>
</RotationGestureHandler>
</Animated.View>
</PanGestureHandler>
{/*
<PanGestureHandler onGestureEvent={this.onPanGestureEvent}>
<Animated.View style={[styles.mover, {
transform: [{ translateX: this.translateX, }, { translateY: this.translateY}]}]}>
<View style={styles.minutes}/>
</Animated.View>
</PanGestureHandler>
<PanGestureHandler onGestureEvent={this.onPanGestureEvent}>
<Animated.View style={[styles.mover, {
transform: [{ translateX: this.translateX, }, { translateY: this.translateY}]}]}>
<View style={styles.seconds}/>
</Animated.View>
</PanGestureHandler>
*/}
<View style={[styles.smallQuadran]}/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'whitesmoke',
alignItems: 'center',
justifyContent: 'center',
},
background: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent'
},
mover: {
position: 'absolute',
width: SIZE,
height: SIZE,
borderRadius: SIZE/2,
alignItems: 'center',
justifyContent: 'flex-start',
top: 68
},
hours: {
backgroundColor: 'black',
height: '20%',
marginTop: '30%',
width: 4.5,
borderRadius: 4.5
},
minutes: {
backgroundColor: 'black',
height: '35%',
marginTop: '15%',
width: 3.5,
borderRadius: 3.5
},
seconds: {
backgroundColor: 'rgba(227, 71, 134, 1)',
height: '35%',
marginTop: '15%',
width: 2,
borderRadius: 2
},
bigQuadran: {
width: SIZE * 0.815,
height: SIZE * 0.815,
borderRadius: SIZE * 0.40,
backgroundColor: 'ghostwhite',
borderColor: 'black',
borderWidth: SIZE * 0.030,
position: 'absolute',
top: 100
},
smallQuadran: {
width: 10,
height: 10,
borderRadius: 5,
position: 'absolute',
backgroundColor: 'rgba(227, 71, 134, 1)',
top: 250
},
numbers: {
width: SIZE * 0.78,
height: SIZE * 0.78,
position: 'absolute',
top: 107,
borderRadius: SIZE * 4
}
})
Specifically, how do I make one side of the card disappear once flipped. I am using Android, so backfaceVisibility did not help solve my problem. I used animate to remove the object via opacity, but the problem is that once I removed one side of the card, the 'invisible' buttons still worked, and the buttons on the current side of the card did not. I tried playing around with zIndex and 'disabled' in Pressable, and I am unsure how to fix this. I have attached source code below:
let animatedValue = new Animated.Value(0)
let val = 0;
animatedValue.addListener(({ value }) => {
val = value;
})
let frontOpacity = animatedValue.interpolate({
inputRange: [89, 90],
outputRange: [1, 0]
})
let backOpacity = animatedValue.interpolate({
inputRange: [89, 90],
outputRange: [0, 1]
})
let frontInterpolate = animatedValue.interpolate({
inputRange: [0, 180],
outputRange: ['0deg', '180deg']
})
let backInterpolate = animatedValue.interpolate({
inputRange: [0, 180],
outputRange: ['180deg', '360deg']
})
const frontAnimatedStyle = {
transform: [
{ rotateY: frontInterpolate }
],
opacity: frontOpacity,
}
const backAnimatedStyle = {
transform: [
{ rotateY: backInterpolate }
],
opacity: backOpacity,
}
let isFront = true;
const flipCard = () => {
isFront = !isFront;
if (val >= 90) {
Animated.spring(animatedValue, {
toValue:0,
friction: 8,
tension: 10,
useNativeDriver: true,
}).start();
} else {
Animated.spring(animatedValue, {
toValue:180,
friction: 8,
tension: 10,
useNativeDriver: true,
}).start();
}
}
const getFrontZ = () => {
return isFront ? 1 : 0;
}
return(
<View style={styles.container}>
<Animated.View
style={[styles.cardStyle,
styles.frontCardStyle,
frontAnimatedStyle,
{height: cardHeight, zIndex: -1,}]}>
<Pressable
onPress={() => console.log('Open Stack')}
style={{height: '100%', width: '100%'}}>
<Pressable
onPress={() => flipCard()}
style={{backgroundColor: 'blue', height: 50, width: 50,}}
>
</Pressable>
</Pressable>
</Animated.View>
<Animated.View
style={[styles.cardStyle,
styles.backCardStyle,
backAnimatedStyle,
{height: cardHeight, zIndex: 0}]}>
<Pressable
onPress={() => flipCard()}
style={{height: '100%', width: '100%'}}
>
</Pressable>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
cardStyle: {
height: '100%',
width: '100%',
backfaceVisibility: 'hidden',
},
frontCardStyle: {
backgroundColor: 'red',
},
backCardStyle: {
backgroundColor: 'purple',
position: 'absolute',
top: 0,
}
})
try this library react-native-card-flip
and import this in your file and use like this
import CardFlip from 'react-native-card-flip';
<CardFlip style={styles.cardContainer} ref={(card) => this.card = card} >
<TouchableOpacity style={styles.card} onPress={() => this.card.flip()} >
<Text>AB</Text></TouchableOpacity>
<TouchableOpacity style={styles.card} onPress={() => this.card.flip()} >
<Text>CD</Text></TouchableOpacity>
</CardFlip>
I am trying to implement a multi-color color wheel, which lets users drag multiple pickers to change their colors.
The issue here is that, when the user starts dragging one of the pickers and keeps dragging to the edge of the wheel, the dragging gets canceled as soon as the picker hits the edge.
The needed implementation is to keep the dragging going when outside the wheel, but let the picker follow the edge of the wheel until the user lifts the thumb.
I already implemented the outBounds method to detect if the gesture is out of the wheel, but every attempt I did, trying to set the picker to follow the edge using Math.cos and Math.sin has failed.
Any help will be appreciated.
Thanks.
Code:
import React, { Component } from 'react';
import { Animated, Image, Dimensions, PanResponder, StyleSheet, View, Text } from 'react-native';
import colorsys from 'colorsys';
import wheelPng from './color_wheel.png';
import pickerPng from './picker.png';
import colors from '../../../common/colors';
import { isSmallerDevice } from '../../../helpers/layoutFunctions';
class ColorWheel extends Component {
static defaultProps = {
thumbSize: 40,
initialColor: '#ffffff',
onColorChange: () => { },
}
constructor(props) {
super(props)
this.state = {
offset: { x: 0, y: 0 },
currentColor: props.initialColor,
colors: props.colors,
pans: props.colors.map(color => new Animated.ValueXY()),
activeIndex: null,
radius: 0,
renew: false,
spring: new Animated.Value(1)
}
}
static getDerivedStateFromProps(nextProps, prevState) {
let update = { ...prevState };
if (nextProps.colors && nextProps.colors.length && nextProps.colors !== prevState.colors) {
if (nextProps.colors.length > prevState.colors.length) {
update.colors = nextProps.colors;
update.pans = [...prevState.pans, new Animated.ValueXY()];
update.renew = true;
}
}
return update;
}
componentDidUpdate(prevProps, prevState) {
if (this.state.renew) {
this.renewResponders();
this.props.colors.forEach((col, index) => {
this.forceUpdate(col);
});
}
}
componentDidMount = () => {
this.renewResponders();
}
renewResponders = () => {
const { colors } = this.props;
this._panResponders = colors.map((color, index) => this.createResponder(color, index));
this.setState({ renew: false });
}
createResponder = (color, index) => {
const responder = PanResponder.create({
onPanResponderTerminationRequest: () => false,
onStartShouldSetPanResponderCapture: ({ nativeEvent }) => {
this.state.spring.setValue(1.3);
const { onSwiperDisabled } = this.props;
onSwiperDisabled && onSwiperDisabled();
if (this.outBounds(nativeEvent)) return
this.updateColor({ index, nativeEvent })
this.setState({ panHandlerReady: true })
this.state.pans[index].setValue({
x: -this.state.left + nativeEvent.pageX - this.props.thumbSize / 2,
y: -this.state.top + nativeEvent.pageY - this.props.thumbSize / 2 - 40,
})
return true
},
onStartShouldSetPanResponder: (e, gestureState) => true,
onMoveShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => true,
onPanResponderMove: (event, gestureState) => {
this.setState({ activeIndex: index });
if (this.outBounds(gestureState)) return
this.resetPanHandler(index)
return Animated.event(
[
null,
{
dx: this.state.pans[index].x,
dy: this.state.pans[index].y,
},
],
{ listener: (ev) => this.updateColor({ nativeEvent: ev.nativeEvent, index }), useNativeDriver: false },
)(event, gestureState)
},
onPanResponderRelease: ({ nativeEvent }) => {
const { onSwiperEnabled } = this.props;
onSwiperEnabled && onSwiperEnabled();
this.state.pans[index].flattenOffset()
const { radius } = this.calcPolar(nativeEvent)
if (radius < 0.1) {
this.forceUpdate('#ffffff', index)
}
Animated.spring(this.state.spring, {
toValue: 1,
stiffness: 400,
damping: 10,
useNativeDriver: false,
}).start(() => {
this.setState({ panHandlerReady: true, activeIndex: null })
});
if (this.props.onColorChangeComplete) {
this.props.onColorChangeComplete({ index, color: this.state.hsv });
}
},
})
return { color, responder };
}
onLayout() {
setTimeout(() => {
this.self && this.measureOffset()
}, 200);
}
measureOffset() {
/*
* const {x, y, width, height} = nativeEvent.layout
* onlayout values are different than measureInWindow
* x and y are the distances to its previous element
* but in measureInWindow they are relative to the window
*/
this.self.measureInWindow((x, y, width, height) => {
const window = Dimensions.get('window')
const absX = x % width
const radius = Math.min(width, height) / 2
const offset = {
x: absX + width / 2,
y: y % window.height + height / 2,
}
this.setState({
offset,
radius,
height,
width,
top: y % window.height,
left: absX,
});
//
this.forceUpdate(this.state.currentColor)
});
}
calcPolar(gestureState) {
const {
pageX, pageY, moveX, moveY,
} = gestureState
const [x, y] = [pageX || moveX, pageY || moveY]
const [dx, dy] = [x - this.state.offset.x, y - this.state.offset.y]
return {
deg: Math.atan2(dy, dx) * (-180 / Math.PI),
// pitagoras r^2 = x^2 + y^2 normalized
radius: Math.sqrt(dy * dy + dx * dx) / this.state.radius,
}
}
outBounds(gestureState) {
const { radius } = this.calcPolar(gestureState);
return radius > 1
}
resetPanHandler(index) {
if (!this.state.panHandlerReady) {
return
}
this.setState({ panHandlerReady: false })
this.state.pans[index].setOffset({
x: this.state.pans[index].x._value,
y: this.state.pans[index].y._value,
})
this.state.pans[index].setValue({ x: 0, y: 0 })
}
calcCartesian(deg, radius) {
const r = radius * this.state.radius; // was normalized
const rad = Math.PI * deg / 180;
const x = r * Math.cos(rad);
const y = r * Math.sin(rad);
return {
left: this.state.width / 2 + x,
top: this.state.height / 2 - y,
}
}
updateColor = ({ nativeEvent, index }) => {
const { deg, radius } = this.calcPolar(nativeEvent);
const hsv = { h: deg, s: 100 * radius, v: 100 };
this.setState({ hsv });
this.props.onColorChange({ index, color: hsv });
}
forceUpdate = (color, index) => {
const { h, s, v } = colorsys.hex2Hsv(color);
const { left, top } = this.calcCartesian(h, s / 100);
this.props.onColorChange({ color: { h, s, v }, index });
if (index)
this.state.pans[index].setValue({
x: left - this.props.thumbSize / 2,
y: top - this.props.thumbSize / 2,
});
else
this.props.colors.forEach((col, index) => {
this.animatedUpdate(col, index);
});
}
animatedUpdate = (color, index) => {
const { h, s, v } = colorsys.hex2Hsv(color);
const { left, top } = this.calcCartesian(h, s / 100)
// this.setState({ currentColor: color })
// this.props.onColorChange({ h, s, v })
Animated.spring(this.state.pans[index], {
toValue: {
x: left - this.props.thumbSize / 2,
y: top - this.props.thumbSize / 2 - 40,
},
useNativeDriver: false
}).start()
}
render() {
const { radius, activeIndex } = this.state
const thumbStyle = [
styles.circle,
this.props.thumbStyle,
{
position: 'absolute',
width: this.props.thumbSize,
height: this.props.thumbSize,
borderRadius: this.props.thumbSize / 2,
// backgroundColor: this.state.currentColor,
opacity: this.state.offset.x === 0 ? 0 : 1,
flexDirection: 'row',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
},
]
const { colors } = this.props;
// const panHandlers = this._panResponder && this._panResponder.panHandlers || {}
return (
<View
ref={node => {
this.self = node
}}
onLayout={nativeEvent => this.onLayout(nativeEvent)}
style={[styles.coverResponder, this.props.style]}>
{!!radius && <Image
style={[styles.img,
{
height: radius * 2,
width: radius * 2
}]}
source={wheelPng}
/>}
{colors && colors.map((color, index) =>
<Animated.View key={index} style={[this.state.pans[index].getLayout(), thumbStyle, { zIndex: activeIndex === index ? 9 : 3, transform: [{ scale: activeIndex === index ? this.state.spring : 1 }] }]} {...this._panResponders && this._panResponders[index] && this._panResponders[index].responder.panHandlers}>
<Animated.Image
style={[
{
height: this.props.thumbSize * 2,
width: this.props.thumbSize * 2,
resizeMode: 'contain',
position: 'absolute',
tintColor: '#000000'
}]}
source={pickerPng}
/>
<Animated.View style={[styles.circle, {
position: 'absolute',
top: -8,
left: 2,
width: this.props.thumbSize,
height: this.props.thumbSize,
borderRadius: this.props.thumbSize / 2,
backgroundColor: color,
opacity: this.state.offset.x === 0 ? 0 : 1,
flexDirection: 'row',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center'
}]} >
<Text style={isSmallerDevice ? styles.smallerDeviceCountText : styles.countText}>{index + 1}</Text>
</Animated.View>
</Animated.View>
)}
</View>
)
}
}
const styles = StyleSheet.create({
coverResponder: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
img: {
alignSelf: 'center',
},
circle: {
position: 'absolute',
backgroundColor: '#000000',
// borderWidth: 3,
// borderColor: '#EEEEEE',
elevation: 3,
shadowColor: 'rgb(46, 48, 58)',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.8,
shadowRadius: 2,
},
countText: {
flex: 1,
textAlign: 'center',
fontFamily: 'Rubik-Bold',
fontSize: 20,
color: colors.titleMain
},
smallerDeviceCountText: {
flex: 1,
textAlign: 'center',
fontFamily: 'Rubik-Bold',
fontSize: 16,
color: colors.titleMain
}
})
export default ColorWheel;
I am developing an animation using the react-native-reanimated andreact-native-gesture-handler libraries. When I open the page it works as I expected. But when I change any data using fast hook or hook in the function. The value of posX is being reset. And even though it appears in debug 'in the event, it does not update the data in Animated.View`.
import React, { useState } from 'react';
import { StyleSheet, View, Image, Dimensions } from 'react-native';
import Animated, {
Value,
event,
set,
block,
cond,
add,
eq,
debug,
greaterThan,
lessThan,
multiply,
useCode,
} from 'react-native-reanimated';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
interface Props {}
const R = 70;
const image1 = require('./img1.jpeg');
const image2 = require('./img3.jpeg');
const { width, height } = Dimensions.get('window');
export const SplitView: React.FC<Props> = () => {
const MAX = width - R;
const MIN = 0;
const posX = new Value<number>(0);
const offsetX = new Value<number>((width - R) / 2);
const panState = new Value(State.UNDETERMINED);
const onGestureHandler = event([
{
nativeEvent: ({
translationX,
state,
}: {
translationX: number;
state: State;
}) =>
block([
set(panState, state),
set(posX, add(translationX, offsetX)),
cond(
lessThan(posX, MIN),
set(posX, MIN),
cond(greaterThan(posX, MAX), set(posX, MAX)),
),
debug('posX ', posX), // <=== always show on console
cond(eq(state, State.END), [
set(offsetX, add(offsetX, translationX)),
cond(
lessThan(offsetX, MIN),
set(offsetX, MIN),
cond(greaterThan(offsetX, MAX), set(offsetX, MAX)),
),
]),
]),
},
]);
return (
<View style={styles.container}>
<View style={[styles.left, { width, height }]}>
<Animated.Image style={styles.image} source={image1} />
</View>
// But after value change or run fast refresh not working.
// Initial value gets stuck
<Animated.View
style={[styles.right, { width, height, left: add(posX, R / 2) }]}>
<Animated.Image
style={[styles.image, { left: multiply(add(posX, R / 2), -1) }]}
source={image2}
/>
</Animated.View>
<PanGestureHandler
onGestureEvent={onGestureHandler}
onHandlerStateChange={onGestureHandler}>
<Animated.View
style={[
styles.ball,
{
left: posX,
top: (height - R) / 2,
},
]}
/>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'blue',
},
ball: {
width: R,
height: R,
backgroundColor: 'red',
borderRadius: R / 2,
zIndex: 2,
},
image: {
flex: 1,
},
left: {
position: 'absolute',
top: 0,
left: 0,
flex: 1,
},
right: {
position: 'absolute',
flex: 1,
top: 0,
left: 0,
zIndex: 1,
overflow: 'hidden',
backgroundColor: 'yellow',
},
});
I am trying to build a full screen image view with zoom and pan capabilities in react native. I am using react-native-gesture-handler for handling multi touches. It is working fine on iOS but does nothing on Android.
I am completely confused on why this code would work on iOS but not on Android. I can confirm that my react-native-gesture-handler setup is working because I have another PanGestureHandler working as expected.
/*********************** Imports ***********************/
import React, { useRef, useEffect } from 'react';
import {
View, StyleSheet, Animated,
} from 'react-native';
import ZoomImage from './ZoomableImage';
import { vw, vh, isiPhoneX } from '../Styles/StyleUtils';
/********************* End Imports *********************/
/*********************** ImageView Function ***********************/
const ImageView = ({ dismiss, uri, imageLayout }) => {
const animation = useRef(new Animated.Value(0));
useEffect(() => {
Animated.timing(animation.current, {
toValue: 1,
duration: 250,
}).start();
}, []);
function closeImage() {
Animated.timing(animation.current, {
toValue: 0,
duration: 250,
}).start(() => dismiss());
}
return (
<View style={styles.mainContainer}>
<Animated.View style={[
styles.container,
{
backgroundColor: animation.current.interpolate({
inputRange: [0, 1],
outputRange: ["rgba(0, 0, 0, 0)", "rgba(0, 0, 0, 0.5)"],
})
}
]}>
{/* <View style={styles.header}>
<TouchableOpacity style={styles.closeBtn} onPress={closeImage}>
<Icon name="close" color={blackColor} size={30} />
</TouchableOpacity>
</View> */}
<ZoomImage dismiss={closeImage} imageStyle={[
{
left: animation.current.interpolate({
inputRange: [0, 1],
outputRange: [imageLayout.pageX, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
top: animation.current.interpolate({
inputRange: [0, 1],
outputRange: [imageLayout.pageY, vh(24)] // 0 : 150, 0.5 : 75, 1 : 0
}),
width: animation.current.interpolate({
inputRange: [0, 1],
outputRange: [imageLayout.width, vw(100)],
}),
height: animation.current.interpolate({
inputRange: [0, 1],
outputRange: [imageLayout.height, vw(100)],
})
}
]}
source={{ uri: uri }} />
</Animated.View>
</View>
);
};
/********************** End ImageView Function *******************/
export default ImageView;
const styles = StyleSheet.create({
header: {
marginTop: isiPhoneX() ? 40 : 25,
alignItems: "flex-end"
},
closeBtn: {
paddingHorizontal: 20,
},
mainContainer: {
position: "absolute",
width: vw(100),
height: vh(100)
},
container: {
position: "absolute",
top: 0,
left: 0,
right: 0, bottom: 0,
},
image: {
position: "absolute",
}
});
/*********************** Imports ***********************/
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Animated } from 'react-native';
import {
State,
PanGestureHandler,
PinchGestureHandler
} from 'react-native-gesture-handler';
import { vw, vh } from '../Styles/StyleUtils';
/********************* End Imports *********************/
/*********************** ZoomableImage Function ***********************/
const ZoomableImage = (props) => {
const panRef = useRef();
const pinchRef = useRef();
const closeAnimation = useRef(new Animated.ValueXY({ x: 0, y: 0 }));
const scaleAnimation = useRef(new Animated.Value(1));
const baseScale = useRef(new Animated.Value(1));
const scale = useRef(Animated.multiply(baseScale.current, scaleAnimation.current));
const [lastScale, setLastScale] = useState(1);
useEffect(() => {
console.log('Refs', panRef);
}, [panRef.current]);
const onPanHandlerGesture = useCallback(({ nativeEvent }) => {
console.log('Native Event', nativeEvent);
closeAnimation.current.setValue({
x: nativeEvent.translationX,
y: nativeEvent.translationY
});
}, []);
const onPanHandlerStateChange = useCallback(({ nativeEvent }) => {
console.log('New Pan Event', nativeEvent);
if (nativeEvent.oldState === State.ACTIVE) {
if (
nativeEvent.translationY > 250
|| nativeEvent.velocityY > 1200
) {
Animated.parallel([
Animated.timing(scaleAnimation.current, {
toValue: 1,
duration: 200
}),
Animated.timing(baseScale.current, {
toValue: 1,
duration: 200
}),
Animated.timing(closeAnimation.current, {
toValue: { x: 0, y: 0 },
duration: 200
})
]).start(() => props.dismiss());
}
else {
Animated.timing(closeAnimation.current, {
toValue: { x: 0, y: 0 },
duration: 100
}).start();
}
}
}, [lastScale]);
const onPinchGestureEvent = Animated.event([{ nativeEvent: { scale: scaleAnimation.current } }]);
useCallback(({ nativeEvent }) => {
scaleAnimation.current.setValue(nativeEvent.scale);
}, [lastScale]);
const onPinchHandlerStateChange = ({ nativeEvent }) => {
console.log('New Pinch Event', nativeEvent);
if (nativeEvent.oldState === State.ACTIVE) {
const newLastScale = lastScale * nativeEvent.scale;
setLastScale(newLastScale);
baseScale.current.setValue(newLastScale);
scaleAnimation.current.setValue(1);
}
};
return (
<PanGestureHandler maxPointers={2} avgTouches onHandlerStateChange={onPanHandlerStateChange}
minDist={10} onGestureEvent={onPanHandlerGesture} ref={panRef}>
<PinchGestureHandler ref={pinchRef} simultaneousHandlers={panRef}
onHandlerStateChange={onPinchHandlerStateChange} onGestureEvent={onPinchGestureEvent}>
<Animated.Image style={[
props.imageStyle,
{
transform: [
{ perspective: 1000 },
{
translateY: closeAnimation.current.y.interpolate({
inputRange: [-vh(25), vh(25)],
outputRange: [-vh(25), vh(25)],
extrapolate: "clamp"
})
},
{
translateX: closeAnimation.current.x.interpolate({
inputRange: [-vw(25), vw(25)],
outputRange: [-vw(10), vw(10)],
extrapolate: "clamp"
})
},
{
scale: scale.current.interpolate({
inputRange: [1, 2.5],
outputRange: [1, 2.5],
extrapolate: "clamp"
})
}
]
}
]} source={props.source} />
</PinchGestureHandler>
</PanGestureHandler>
);
};
ZoomableImage.defaultProps = {
imageStyle: {},
source: { uri: "" }
};
/********************** End ZoomableImage Function *******************/
export default ZoomableImage;
Could someone please help me?
according to me you need to include an other Animated.View (only acting as a wrapper) between your PanGH and PinchGH componant.
return (
<PanGestureHandler ...>
<Animated.View...>
<PinchGestureHandler ...>
<Animated.Image...>