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',
},
});
Related
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;
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.
In my current app the animated y value only changes after I hot reload my app.
Before the reload the app's y value of my component doesn't change or atleast it doesnt interpolate the y value
I'm unsure what causes this strange ocurrance.
Here is a snippet of the code.
Here I create the animated Y value
import React from "react";
import { View, StyleSheet } from "react-native";
import Animated from "react-native-reanimated";
import ListHeader from "./ListHeader";
import ListBody from "./ListBody";
const { Value } = Animated;
const TransitionList = ({ album }) => {
const y = new Value(0);
return (
<View style={styles.container}>
<ListHeader {...{ y, album }} />
<ListBody {...{ y, album }} />
</View>
);
};
export default TransitionList
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "black",
},
});
Here I change the Y value onScroll
import React from "react";
import styled from "styled-components";
import { StyleSheet, View } from "react-native";
import { onScrollEvent } from "react-native-redash";
import { LinearGradient } from "expo-linear-gradient";
import Animated from "react-native-reanimated";
import ListTitle from "./ListTitle";
import ListItem from "./ListItem";
import {
MAX_HEADER_HEIGHT,
MIN_HEADER_HEIGHT,
BUTTON_HEIGHT,
} from "../helpers";
const { interpolate, Extrapolate } = Animated;
const ListBody = ({ album: { artist, tracks }, y }) => {
const height = interpolate(y, {
inputRange: [-MAX_HEADER_HEIGHT, -BUTTON_HEIGHT / 2],
outputRange: [0, MAX_HEADER_HEIGHT + BUTTON_HEIGHT],
extrapolate: Extrapolate.CLAMP,
});
const opacity = interpolate(y, {
inputRange: [-MAX_HEADER_HEIGHT / 2, 0, MAX_HEADER_HEIGHT / 2],
outputRange: [0, 1, 0],
extrapolate: Extrapolate.CLAMP,
});
return (
<Animated.ScrollView
onScroll={onScrollEvent({ y })}
style={styles.container}
showsVerticalScrollIndicator={false}
scrollEventThrottle={1}
stickyHeaderIndices={[1]}
>
<View style={styles.cover}>
<Animated.View style={[styles.gradient, { height }]}>
<LinearGradient
style={StyleSheet.absoluteFill}
start={[0, 0.3]}
end={[0, 1]}
colors={["transparent", "rgba(0, 0, 0, 0.2)", "black"]}
/>
</Animated.View>
<View style={styles.artistContainer}>
<Animated.Text style={[styles.artist, { opacity }]}>
{artist}
</Animated.Text>
</View>
</View>
<View style={styles.header}>
<ListTitle {...{ y, artist }} />
</View>
<View style={styles.tracks}>
{tracks.map((track, key) =>
<ListItem index={key + 1} {...{ track, key, artist }} />
)}
</View>
</Animated.ScrollView>
);
};
export default ListBody
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: MIN_HEADER_HEIGHT - BUTTON_HEIGHT / 2,
},
cover: {
height: MAX_HEADER_HEIGHT - BUTTON_HEIGHT,
},
gradient: {
position: "absolute",
left: 0,
bottom: 0,
right: 0,
alignItems: "center",
},
artistContainer: {
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",
},
artist: {
textAlign: "center",
color: "white",
fontSize: 48,
fontWeight: "bold",
},
header: {
marginTop: -BUTTON_HEIGHT,
},
tracks: {
paddingTop: 32,
backgroundColor: "black",
},
});
Here the Y value's change should also occur in this component
import React from "react";
import Animated from "react-native-reanimated";
import { Image, StyleSheet } from "react-native";
import { MAX_HEADER_HEIGHT, HEADER_DELTA, BUTTON_HEIGHT } from "../helpers";
const { interpolate, Extrapolate } = Animated;
const ListHeader = ({ album: { cover }, y }) => {
const scale = interpolate(y, {
inputRange: [-MAX_HEADER_HEIGHT, 0],
outputRange: [4, 1],
extrapolateRight: Extrapolate.CLAMP,
});
const opacity = interpolate(y, {
inputRange: [-64, 0, HEADER_DELTA],
outputRange: [0, 0.2, 1],
extrapolate: Extrapolate.CLAMP,
});
return (
<Animated.View style={[styles.container, { transform: [{ scale }] }]}>
<Image style={styles.image} source={cover} />
<Animated.View
style={{
...StyleSheet.absoluteFillObject,
backgroundColor: "black",
opacity,
}}
/>
</Animated.View>
);
};
export default ListHeader
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
height: MAX_HEADER_HEIGHT + BUTTON_HEIGHT * 2,
},
image: {
...StyleSheet.absoluteFillObject,
width: undefined,
height: undefined,
},
});
I fixed it by removing the reanimated and react-native-redash libraries and using RN's own Animated library
Example below:
const TransitionList = ({
album,
children,
image,
title,
sub,
list,
header,
}) => {
const [scrollY, setScrollY] = useState(new Animated.Value(0));
return (
<Container SCREEN_HEIGHT={SCREEN_HEIGHT} ThemeColors={ThemeColors}>
<ListHeader y={scrollY} album={album} image={image} />
<ListBody
setY={setScrollY}
y={scrollY}
album={album}
title={title}
sub={sub}
header={header}
list={list}
>
{children}
</ListBody>
</Container>
);
};
<Animated.ScrollView
onScroll={event =>
setY(new Animated.Value(event.nativeEvent.contentOffset.y))}
style={styles.container}
showsVerticalScrollIndicator={false}
scrollEventThrottle={1}
stickyHeaderIndices={[1]}
>
I have created a custom tab bar component using react native navigation following this tutorial: https://reactnavigation.org/docs/bottom-tab-navigator#tabbar. I have then placed a custom component on top of the navigation to mimick spotify minimized player so that when I can execute a drag gesture from minimized to a full sized player. However, When I run the app, the gesture works fine on the first screen, but when I switch screens the gesture doesn't work https://www.youtube.com/watch?v=w-8NSQyWPHI&feature=youtu.be.
import React, { Fragment, useState } from 'react';
import { Text, View, TouchableOpacity, Dimensions, StyleSheet } from 'react-native'
import { PanGestureHandler, State } from 'react-native-gesture-handler'
import { clamp, onGestureEvent, timing, withSpring } from 'react-native-redash'
import Animated from 'react-native-reanimated'
import { getBottomSpace } from 'react-native-iphone-x-helper'
import Player from './player/Player'
import MiniPlayer from './player/MiniPlayer'
const { height } = Dimensions.get('window')
const TABBAR_HEIGHT = getBottomSpace() + 50
const MINIMIZED_PLAYER_HEIGHT = 52
const SNAP_TOP = 0
const SNAP_BOTTOM = height - TABBAR_HEIGHT - MINIMIZED_PLAYER_HEIGHT
const config = {
damping: 30,
mass: 1,
stiffness: 150,
overshootClamping: false,
restSpeedThreshold: 0.1,
restDisplacementThreshold: 0.1
}
const {
Clock,
Value,
cond,
useCode,
set,
block,
not,
clockRunning,
interpolate,
diffClamp,
Extrapolate
} = Animated
const styles = StyleSheet.create({
container: {
flex: 1,
},
playerSheet: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'white'
}
})
export default ({ statee, descriptors, navigation }) => {
const translationY = new Value(0)
const velocityY = new Value(0)
const state = new Value(State.UNDETERMINED)
const offset = new Value(SNAP_BOTTOM)
const goUp: Animated.Value<0 | 1> = new Value(0)
const goDown: Animated.Value<0 | 1> = new Value(0)
const gestureHandler = onGestureEvent({
state,
translationY,
velocityY
})
const translateY = clamp(
withSpring({
state,
offset,
value: translationY,
velocity: velocityY,
snapPoints: [SNAP_TOP, SNAP_BOTTOM],
config
}),
SNAP_TOP,
SNAP_BOTTOM
)
const translateBottomTab = interpolate(translateY, {
inputRange: [SNAP_TOP, SNAP_BOTTOM],
outputRange: [TABBAR_HEIGHT, 0],
extrapolate: Extrapolate.CLAMP
})
const opacity = interpolate(translateY, {
inputRange: [SNAP_BOTTOM - MINIMIZED_PLAYER_HEIGHT, SNAP_BOTTOM],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP
})
const opacity2 = interpolate(translateY, {
inputRange: [
SNAP_BOTTOM - MINIMIZED_PLAYER_HEIGHT * 2,
SNAP_BOTTOM - MINIMIZED_PLAYER_HEIGHT
],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP
})
const clock = new Clock()
useCode(
block([
cond(goUp, [
set(
offset,
timing({
clock,
from: offset,
to: SNAP_TOP
})
),
cond(not(clockRunning(clock)), [set(goUp, 0)])
]),
cond(goDown, [
set(
offset,
timing({
clock,
from: offset,
to: SNAP_BOTTOM
})
),
cond(not(clockRunning(clock)), [set(goDown, 0)])
])
]),
[]
)
goUpHandler = () => {
console.log('TAPPERD UP');
goUp.setValue(1)
}
goDownHandler = () => {
console.log('TAPPERD DOWN');
goDown.setValue(1)
}
return (
<>
<PanGestureHandler {...gestureHandler}>
<Animated.View
style={[styles.playerSheet, { transform: [{ translateY }] }]}
>
<Player onPress={this.goDownHandler} />
<Animated.View
pointerEvents='none'
style={{
opacity: opacity2,
backgroundColor: 'white',
...StyleSheet.absoluteFillObject
}}
/>
<Animated.View style={{
opacity: opacity,
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: MINIMIZED_PLAYER_HEIGHT
}}>
<MiniPlayer onPress={this.goUpHandler} />
</Animated.View>
</Animated.View>
</PanGestureHandler>
<Animated.View style={{
flexDirection: 'row',
height: TABBAR_HEIGHT,
paddingTop: 8,
justifyContent: 'center',
backgroundColor: 'white',
transform: [{ translateY: translateBottomTab }]
}}>
{statee.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
const Icon = options.tabBarIcon
const isFocused = statee.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
return (
<Animated.View style={styles.container} key={index}>
<Animated.View style={{
alignItems: 'center',
justifyContent: 'center'
}}>
<TouchableOpacity
accessibilityRole="button"
accessibilityStates={isFocused ? ['selected'] : []}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
onLongPress={onLongPress}
>
<Icon />
</TouchableOpacity>
</Animated.View>
</Animated.View>
);
})}
</Animated.View>
</>
);
}
I have the exact issue. This is because the reanimated is messed up when re-rendering.
I fixed that by moving the below code out of the functional/class component to make it global.
const translationY = new Value(0)
const velocityY = new Value(0)
const state = new Value(State.UNDETERMINED)
const offset = new Value(SNAP_BOTTOM)
const goUp: Animated.Value<0 | 1> = new Value(0)
const goDown: Animated.Value<0 | 1> = new Value(0)
const gestureHandler = onGestureEvent({
state,
translationY,
velocityY
})
const translateY = clamp(
withSpring({
state,
offset,
value: translationY,
velocity: velocityY,
snapPoints: [SNAP_TOP, SNAP_BOTTOM],
config
}),
SNAP_TOP,
SNAP_BOTTOM
)
const translateBottomTab = interpolate(translateY, {
inputRange: [SNAP_TOP, SNAP_BOTTOM],
outputRange: [TABBAR_HEIGHT, 0],
extrapolate: Extrapolate.CLAMP
})
const opacity = interpolate(translateY, {
inputRange: [SNAP_BOTTOM - MINIMIZED_PLAYER_HEIGHT, SNAP_BOTTOM],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP
})
const opacity2 = interpolate(translateY, {
inputRange: [
SNAP_BOTTOM - MINIMIZED_PLAYER_HEIGHT * 2,
SNAP_BOTTOM - MINIMIZED_PLAYER_HEIGHT
],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP
})
const clock = new Clock()
useCode(
block([
cond(goUp, [
set(
offset,
timing({
clock,
from: offset,
to: SNAP_TOP
})
),
cond(not(clockRunning(clock)), [set(goUp, 0)])
]),
cond(goDown, [
set(
offset,
timing({
clock,
from: offset,
to: SNAP_BOTTOM
})
),
cond(not(clockRunning(clock)), [set(goDown, 0)])
])
]),
[]
)
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...>