This animation using PanResponder that involves swiping up on a bar at the bottom to reveal a full-page component does not work as intended/is not smooth whatsoever when run on a native device (or simulator) - but works just as you would expect on Expo.
As you can see from the gif, despite swiping up on the bar from the bottom a considerable distance - the height only increases by a small amount.
How to reproduce: run code as 'npm react-native run-ios' in create-react-native-app on iOS simulator/run on native device through excode.
A smooth transition that bounces to top of page to expand in full and when swiped down to minimise smoothly transitions to bar at bottom.
The code works perfectly using expo on native device as demonstrated in this Gif and link on Expo.
import React, { Component } from 'react';
import {
View,
Text,
Dimensions,
Animated,
Image,
PanResponder,
Slider,
ScrollView,
} from 'react-native';
import Ionicons from "react-native-vector-icons/Ionicons";
const SCREEN_HEIGHT = Dimensions.get('window').height;
const SCREEN_WIDTH = Dimensions.get('window').width;
export default class AppleMusicUI extends Component {
state = {
isScrollEnabled: false,
};
componentWillMount() {
this.scrollOffset = 0;
this.animation = new Animated.ValueXY({ x: 0, y: SCREEN_HEIGHT - 90 });
this.PanResponder = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
if (
(this.state.isScrollEnabled &&
this.scrollOffset <= 0 &&
gestureState.dy > 0) ||
(!this.state.isScrollEnabled && gestureState.dy < 0)
) {
return true;
} else {
return false;
}
},
onPanResponderGrant: () => {
this.animation.extractOffset();
},
onPanResponderMove: (evt, gestureState) => {
this.animation.setValue({ x: 0, y: gestureState.dy });
},
onPanResponderRelease: (evt, gestureState) => {
if (gestureState.moveY > SCREEN_HEIGHT - 110) {
Animated.spring(this.animation.y, {
toValue: 0,
tension: 1,
}).start();
} else if (gestureState.moveY < 110) {
Animated.spring(this.animation.y, {
toValue: 0,
tension: 1,
}).start();
} else if (gestureState.dy < 0) {
this.setState({ isScrollEnabled: true });
Animated.spring(this.animation.y, {
toValue: -SCREEN_HEIGHT + 110,
tension: 1,
}).start();
} else if (gestureState.dy > 0) {
this.setState({ isScrollEnabled: false });
Animated.spring(this.animation.y, {
toValue: SCREEN_HEIGHT - 110,
tension: 1,
}).start();
}
},
});
}
render() {
const animatedHeight = {
transform: this.animation.getTranslateTransform(),
};
const animatedImageHeight = this.animation.y.interpolate({
inputRange: [0, SCREEN_HEIGHT - 90],
outputRange: [200, 32],
extrapolate: 'clamp',
});
const animatedSongTitleOpacity = this.animation.y.interpolate({
inputRange: [0, SCREEN_HEIGHT - 500, SCREEN_HEIGHT - 90],
outputRange: [0, 0, 1],
extrapolate: 'clamp',
});
const animatedImageMarginLeft = this.animation.y.interpolate({
inputRange: [0, SCREEN_HEIGHT - 90],
outputRange: [SCREEN_WIDTH / 2 - 100, 10],
extrapolate: 'clamp',
});
const animatedHeaderHeight = this.animation.y.interpolate({
inputRange: [0, SCREEN_HEIGHT - 90],
outputRange: [SCREEN_HEIGHT / 2, 90],
extrapolate: 'clamp',
});
const animatedSongDetailsOpacity = this.animation.y.interpolate({
inputRange: [0, SCREEN_HEIGHT - 500, SCREEN_HEIGHT - 90],
outputRange: [1, 0, 0],
extrapolate: 'clamp',
});
const animatedBackgroundColor = this.animation.y.interpolate({
inputRange: [0, SCREEN_HEIGHT - 90],
outputRange: ['rgba(0,0,0,0.5)', 'white'],
extrapolate: 'clamp',
});
return (
<Animated.View
style={{ flex: 1, backgroundColor: animatedBackgroundColor }}>
<Animated.View
{...this.PanResponder.panHandlers}
style={[
animatedHeight,
{
position: 'absolute',
left: 0,
right: 0,
zIndex: 10,
backgroundColor: 'white',
height: SCREEN_HEIGHT,
},
]}>
<ScrollView
scrollEnabled={this.state.isScrollEnabled}
scrollEventThrottle={16}
onScroll={event => {
this.scrollOffset = event.nativeEvent.contentOffset.y;
}}>
<Animated.View
style={{
height: animatedHeaderHeight,
borderTopWidth: 1,
borderTopColor: '#ebe5e5',
flexDirection: 'row',
alignItems: 'center',
}}>
<View
style={{ flex: 4, flexDirection: 'row', alignItems: 'center' }}>
<Animated.View
style={{
height: animatedImageHeight,
width: animatedImageHeight,
marginLeft: animatedImageMarginLeft,
}}>
<Image
style={{
flex: 1,
width: null,
height: null,
}}
source={require('../../assets/Logo.png')}
/>
</Animated.View>
<Animated.Text
style={{
opacity: animatedSongTitleOpacity,
fontSize: 18,
paddingLeft: 10,
}}>
Hotel California(Live)
</Animated.Text>
</View>
<Animated.View
style={{
opacity: animatedSongTitleOpacity,
flex: 1,
flexDirection: 'row',
justifyContent: 'space-around',
}}>
<Ionicons name="md-pause" size={32} />
<Ionicons name="md-play" size={32} />
</Animated.View>
</Animated.View>
<Animated.View
style={{
height: animatedHeaderHeight,
opacity: animatedSongDetailsOpacity,
}}>
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'flex-end',
}}>
<Text style={{ fontWeight: 'bold', fontSize: 22 }}>
Hotel California (Live)
</Text>
<Text style={{ fontSize: 18, color: '#fa95ed' }}>
Eagles - Hell Freezes Over
</Text>
</View>
<View
style={{
height: 40,
width: SCREEN_WIDTH,
alignItems: 'center',
}}>
<Slider
style={{ width: 300 }}
step={1}
minimumValue={18}
maximumValue={71}
value={18}
/>
</View>
<View
style={{
flex: 2,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
}}>
<Ionicons name="md-rewind" size={40} />
<Ionicons name="md-pause" size={50} />
<Ionicons name="md-fastforward" size={40} />
</View>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingBottom: 20,
}}>
<Ionicons
name="md-add"
size={32}
style={{ color: '#fa95ed' }}
/>
<Ionicons
name="md-more"
size={32}
style={{ color: '#fa95ed' }}
/>
</View>
</Animated.View>
<View style={{ height: 1000 }} />
</ScrollView>
</Animated.View>
</Animated.View>
);
}
}
Screenshot/Gif:
React Native Environment Info:
System:
OS: macOS 10.14.3
CPU: (4) x64 Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
Memory: 199.20 MB / 8.00 GB
Shell: 3.2.57 - /bin/bash
Binaries:
Node: 10.5.0 - /usr/local/bin/node
npm: 6.6.0 - /usr/local/bin/npm
Watchman: 4.9.0 - /usr/local/bin/watchman
SDKs:
iOS SDK:
Platforms: iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0
IDEs:
Xcode: 10.0/10A255 - /usr/bin/xcodebuild
npmPackages:
react: 16.6.3 => 16.6.3
react-native: 0.58.4 => 0.58.4
npmGlobalPackages:
react-native-cli: 2.0.1
react-native-git-upgrade: 0.2.7
After extensive research, I found a temporary fix as follows:
const setDefaultSettings = () => {
Navigation.setDefaultOptions({
topBar: {
visible: false
},
statusBar: {
style: "light"
},
sideMenu: {
openGestureMode: 'bezel',
left: {
visible: true,
enabled: true
},
right: {
visible: false,
enabled: false
}
}
});
};
I believe to help future users of React Native Navigation, that openGestureModel should be set to 'bezel' by default or there should be a more informative degree of information provided on the docs regarding the impact of the use of 'sideMenu' in its native form upon PanResponder based animations
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
If you believe the issue is still relevant, please test on the latest Detox and report back. Thank you for your contributions.
The issue has been closed for inactivity.
Most helpful comment
After extensive research, I found a temporary fix as follows:
I believe to help future users of React Native Navigation, that openGestureModel should be set to 'bezel' by default or there should be a more informative degree of information provided on the docs regarding the impact of the use of 'sideMenu' in its native form upon PanResponder based animations