I made some code based on your lecture.
When onPanResponderRelease is executed, if dx<-50, goLeft.start() is used to move toValue: -200.
I also refreshed the position using position.flattenOffset(); In onPanResponderGrant, I saved the position value using setOffset.
However, when goLeft.start() is executed and the tovalue has shifted by -200 and the card is touched again, onPanResponderGrant starts at -200 but ,the position is set to -400 and the card starts at an entirely different position.
How can I fix the code so that after goLeft.start() is executed, the touch will start at -200?
this is my code
import React, { useRef, useState } from "react";
import { Animated, PanResponder, Text, View } from "react-native";
import styled from "styled-components/native";
import { Ionicons } from "#expo/vector-icons";
import icons from "./icons";
const Container = styled.View`
flex: 1;
justify-content: center;
align-items: center;
background-color: #00a8ff;
`;
const Card = styled(Animated.createAnimatedComponent(View))`
background-color: white;
width: 100px;
height: 100px;
justify-content: center;
align-items: center;
border-radius: 12px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
position: absolute;
`;
const Btn = styled.TouchableOpacity`
margin: 0px 10px;
`;
const BtnContainer = styled.View`
flex-direction: row;
flex: 1;
`;
const CardContainer = styled.View`
flex: 3;
justify-content: center;
align-items: center;
`;
export default function App() {
const [left, setLeft] = useState(false);
// Values
const position = useRef(new Animated.Value(0)).current;
// Animations
const goCenter = Animated.spring(position, {
toValue: 0,
useNativeDriver: true,
});
const goLeft = Animated.spring(position, {
toValue: -200,
tension: 5,
useNativeDriver: true,
restDisplacementThreshold: 100,
restSpeedThreshold: 100,
});
const goRight = Animated.spring(position, {
toValue: 500,
tension: 5,
useNativeDriver: true,
});
// Pan Responders
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
console.log("Grant", position._value);
position.setOffset(position._value);
position.flattenOffset();
// position.addListener(() => {});
},
onPanResponderMove: (_, { dx }) => {
console.log("Move:", dx);
position.addListener(() => {});
position.setValue(dx);
// position.addListener(() => {});
},
onPanResponderRelease: (_, { dx }) => {
console.log("Release:", dx);
if (dx < -30) {
console.log("!!!");
goLeft.start();
position.flattenOffset();
} else {
position.flattenOffset();
}
},
})
).current;
position.addListener(() => console.log("p:", position));
// State
const [index, setIndex] = useState(0);
const onDismiss = () => {
// position.setValue(-100);
// Animated.timing(position, { toValue: -60, useNativeDriver: true }).start();
};
const closePress = () => {
console.log("222");
goLeft.start(onDismiss);
};
const checkPress = () => {
console.log("111");
goRight.start(onDismiss);
};
return (
<Container>
<CardContainer>
<Card
{...panResponder.panHandlers}
style={{
transform: [{ translateX: position }],
}}
>
<Ionicons color="#192a56" size={98} />
</Card>
</CardContainer>
<BtnContainer>
<Btn onPress={closePress}>
<Ionicons name="close-circle" color="white" size={58} />
</Btn>
<Btn onPress={checkPress}>
<Ionicons name="checkmark-circle" color="white" size={58} />
</Btn>
</BtnContainer>
</Container>
);
}
Related
Error message:
TypeError: _native.default.div is not a function. (In '_native.default.div(_templateObject || (_templateObject = (0, _taggedTemplateLiteralLoose2.default)([""])))', '_native.default.div' is undefined)
I am new to react native. Not sure if this is some simple problems that I didn't understand correct. I was trying to use one of the example in Victory chart.
But the error massage keep showing up. I tried everything I can find online but nothing helps.
App.js
import { StyleSheet, Text, View } from 'react-native';
import { registerRootComponent } from 'expo';
import styled from 'styled-components/native';
import React from "react";
import { VictoryBar, VictoryChart, VictoryTheme } from "victory-native";
const LIGHT_GREY = "hsl(355, 20%, 90%)";
const PRIMARY_COLOR = "hsl(355, 92%, 67%)";
const Container = styled.div``;
const Card = styled.div`
background-color: #2b2a31;
padding: 40px 36px 30px;
border-radius: 5px;
// when rendered in the gallery preview
a & {
padding: 24px 20px 10px;
}
`;
const yearToSeason = year => `${year}-${(year + 1 + "").slice(2, 4)}`;
const YEARS = Object.keys(basketballData).map(year => parseInt(year, 10));
const FIRST_YEAR = YEARS[0];
const LAST_YEAR = YEARS[YEARS.length - 1];
const TOTAL_YEARS = LAST_YEAR - FIRST_YEAR;
const getTooltipText = ({ datum }) => {
const { binnedData, x0, x1 } = datum;
const playerCount = binnedData.length;
if (!playerCount) {
return null;
}
const playerNames = binnedData
.slice(0, 2)
.map(({ player }) => {
const [firstName, lastName] = player.split(" ");
return lastName ? `${firstName.slice(0, 1)}. ${lastName}` : firstName;
})
.join(", ");
const playerNamesList = `\n (${playerNames}${
playerCount > 2 ? `, and ${playerCount - 2} more players` : ""
})`;
return `${playerCount} player${
playerCount === 1 ? "" : "s"
} averaged between ${x0}-${x1} 3PT attempts ${playerNamesList}`;
};
const sharedAxisStyles = {
axis: {
stroke: "transparent"
},
tickLabels: {
fill: LIGHT_GREY,
fontSize: 14
},
axisLabel: {
fill: LIGHT_GREY,
padding: 36,
fontSize: 15,
fontStyle: "italic"
}
};
const GradientSvg = styled.svg`
position: fixed;
opacity: 0;
`;
const App = () => {
const [year, setYear] = React.useState(FIRST_YEAR);
return (
<Container>
<GradientSvg>
<defs>
<linearGradient id="gradient1" x1="0%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stopColor="#FFE29F" />
<stop offset="40%" stopColor="#FFA99F" />
<stop offset="100%" stopColor={PRIMARY_COLOR} />
</linearGradient>
</defs>
</GradientSvg>
<Card>
<VictoryChart
containerComponent={
<VictoryVoronoiContainer
labels={getTooltipText}
voronoiDimension="x"
labelComponent={
<VictoryTooltip
constrainToVisibleArea
style={{
fill: LIGHT_GREY,
fontSize: 11
}}
flyoutStyle={{
fill: "#24232a",
stroke: PRIMARY_COLOR,
strokeWidth: 0.5
}}
/>
}
/>
}
height={280}
>
<VictoryLabel
text={`3pt Attempts Per Game Averages (${yearToSeason(year)})`}
x={225}
y={18}
textAnchor="middle"
style={{ fill: LIGHT_GREY, fontSize: 16 }}
/>
<VictoryAxis
style={{
...sharedAxisStyles,
grid: {
fill: LIGHT_GREY,
stroke: LIGHT_GREY,
pointerEvents: "painted",
strokeWidth: 0.5
}
}}
label="# of players"
dependentAxis
/>
<VictoryAxis
style={{
...sharedAxisStyles,
axisLabel: { ...sharedAxisStyles.axisLabel, padding: 35 }
}}
label="3pt attempts per game"
/>
<VictoryHistogram
cornerRadius={2}
domain={{ y: [0, 125] }}
animate={{ duration: 300 }}
data={basketballData[year]}
bins={_.range(0, 16, 2)}
style={{
data: {
stroke: "transparent",
fill: "url(#gradient1)",
strokeWidth: 1,
},
labels: {
fill: "red"
}
}}
x="3pa"
/>
</VictoryChart>
<YearSlider year={year} setYear={setYear} />
</Card>
</Container>
);
};
const SliderContainer = styled.div`
padding: 64px 25px 10px;
// when rendered in the gallery preview
a & {
padding: 24px 36px 0px;
}
`;
const getYear = percent =>
Math.round(FIRST_YEAR + TOTAL_YEARS * (percent / 100));
const SEASONS = YEARS.map(year => yearToSeason(year));
const YearSlider = ({ year, setYear }) => {
const [value, setValue] = React.useState(0);
return (
<SliderContainer>
<Slider
onChange={newValue => {
setValue(newValue);
const calculatedYear = getYear(newValue);
if (year !== calculatedYear) {
setYear(calculatedYear);
}
}}
color={PRIMARY_COLOR}
value={value}
maxValue={100}
tooltipValues={SEASONS}
/>
</SliderContainer>
);
};
ReactDOM.render(<App/>, mountNode);
I am really new to React-native-reanimated. I am trying to create one custom bottom-sheet like this app. I am using PanGestureHandler from react-native-gesture-handler for move the Animated View to go up and down. For gestureHandler I am using useAnimatedGestureHandler props from react-native-reanimated. I want to move the Animated View from start point to middle screen and bottom of screen. This is My Bottom sheet start point image, when scroll the card down it should come middle of the screen like this image, again scroll down the card it will come bottom like this image.
I am having difficulties with the conditional useAnimatedGestureHandler onEnd movement. Currently I am tracking onEnd's event.translationY and make a condition out of it.
This is how it works currently:
When the App start, the Animated View is top of the screen, if I move the card scroll to bottom it goes middle of the screen and it does not go down from middle of the screen, I can move it to up from middle of the screen or if I scroll hard to bottom it goes all the way to bottom and if I try scroll the View up it does not go middle, it just goes up to start View.
I am trying to make the condition based screen size but I don't know how to make it.
I shared my code in expo-snacks
This is my all code
import React, { useState, useEffect } from "react";
import { StyleSheet, useWindowDimensions, RefreshControl } from "react-native";
import MapView from "react-native-maps";
import styled from "styled-components";
import {
PanGestureHandler,
PanGestureHandlerGestureEvent,
FlatList,
} from "react-native-gesture-handler";
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
withSpring,
} from "react-native-reanimated";
const initialRegion = {
latitudeDelta: 15,
longitudeDelta: 15,
latitude: 60.1098678,
longitude: 24.7385084,
};
const api =
"http://open-api.myhelsinki.fi/v1/events/?distance_filter=60.1699%2C24.9384%2C10&language_filter=en&limit=50";
export default function App() {
const { height } = useWindowDimensions();
const top = useSharedValue(height);
const [event, setEvent] = useState([]);
const [loading, setLoading] = useState(false);
const prevTop = useSharedValue(height * 0.5);
// This is Fetch Data
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(api);
const data = await response.json();
setEvent(data.data);
setLoading(false);
} catch (error) {
console.log("erro", error);
}
};
useEffect(() => {
fetchData();
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
top: top.value * 0.2,
bottom: 0,
};
});
const gestureHandler = useAnimatedGestureHandler(
{
onStart(_, context) {
context.translateY = top.value;
},
onActive(event, context) {
top.value = context.translateY + event.translationY;
},
onEnd(event, _) {
// THIS IS MY CONDITION OF ANIMATED VIEW
if (event.translationY > 0 && event.translationY < 400) {
console.log("middle-top", top.value);
console.log("middle-height", height);
top.value = withSpring(height * 2.5, {
duration: 500,
easing: Easing.inOut(Easing.ease),
});
} else if (event.translationY > 450 && event.translationY < 800) {
console.log("bottom-top", top.value);
console.log("bottom-height", height);
top.value = withSpring(height * 4, {
duration: 500,
easing: Easing.inOut(Easing.ease),
});
} else if (event.translationY < 0) {
console.log("start-top", top.value);
console.log("start-height", height);
top.value = withSpring(height, {
duration: 500,
easing: Easing.inOut(Easing.ease),
});
}
},
},
[top]
);
return (
<>
<MapView style={styles.mapStyle} initialRegion={initialRegion} />
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.container, animatedStyle]}>
<Title>I am scroll sheet</Title>
<HeroFlatList
data={event}
refreshControl={
<RefreshControl
enabled={true}
refreshing={loading}
onRefresh={fetchData}
/>
}
keyExtractor={(_, index) => index.toString()}
renderItem={({ item, index }) => {
const image = item?.description.images.map((img) => img.url);
const startDate = item?.event_dates?.starting_day;
return (
<EventContainer key={index}>
<EventImage
source={{
uri:
image[0] ||
"https://res.cloudinary.com/drewzxzgc/image/upload/v1631085536/zma1beozwbdc8zqwfhdu.jpg",
}}
/>
<DescriptionContainer>
<Title ellipsizeMode="tail" numberOfLines={1}>
{item?.name?.en}
</Title>
<DescriptionText>
{item?.description?.intro || "No description available"}
</DescriptionText>
<DateText>{startDate}</DateText>
</DescriptionContainer>
</EventContainer>
);
}}
/>
</Animated.View>
</PanGestureHandler>
</>
);
}
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 0,
right: 0,
top: 0,
backgroundColor: "white",
shadowOffset: {
height: -6,
width: 0,
},
shadowOpacity: 0.1,
shadowRadius: 5,
borderTopEndRadius: 15,
borderTopLeftRadius: 15,
},
mapStyle: {
flex: 1,
},
});
const HeroFlatList = styled(FlatList).attrs({
contentContainerStyle: {
flexGrow: 1,
},
})`
padding: 12px;
`;
const Title = styled.Text`
font-size: 16px;
font-weight: 700;
margin-bottom: 10px;
align-self: center;
padding: 10px;
`;
const DescriptionText = styled.Text`
font-size: 14px;
opacity: 0.7;
`;
const DateText = styled.Text`
font-size: 14px;
opacity: 0.8;
color: #0099cc;
`;
const EventImage = styled.Image`
width: 70px;
height: 70px;
border-radius: 70px;
margin-right: 20px;
`;
const DescriptionContainer = styled.View`
width: 200px;
`;
const EventContainer = styled(Animated.View)`
flex-direction: row;
padding: 20px;
margin-bottom: 10px;
border-radius: 20px;
background-color: #fff;
shadow-color: #000;
shadow-opacity: 0.3;
shadow-radius: 20px;
shadow-offset: 0 10px;
`;
Tech information
Tech
Version
react-native-gesture-handler
^1.10.3
react-native-reanimated
^2.2.0
Not the perfect solution...
added a new sharedValue to track if its moving up or down.
const prevTop = useSharedValue(height * 0.5);
and respective code on gesture end.
onEnd() {
if (top.value > prevTop.value) {
top.value = withTiming(height * 0.98);
} else {
top.value = withTiming(Math.min(200, top.value));
}
prevTop.value = top.value;
},
Still there is a scope of improvement.
I want to replicate the long press to record and slide left to cancel of whatsapp/viber messengers.
import React, {useRef, useState} from 'react';
import {
Dimensions,
TextInput,
TouchableWithoutFeedback,
View,
PanResponder,
Animated as NativeAnimated,
} from 'react-native';
import Animated, {Easing} from 'react-native-reanimated';
import styled from 'styled-components';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
const {Value, timing} = Animated;
let isMoving = false;
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
const RecordButton = ({onPress, onPressIn, onPressOut}) => (
<RecordButton.Container
accessibilityLabel="send message"
accessibilityRole="button"
accessibilityHint="tap to send message">
<TouchableWithoutFeedback
delayPressOut={900}
pressRetentionOffset={300}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}>
<RecordButton.Icon />
</TouchableWithoutFeedback>
</RecordButton.Container>
);
RecordButton.Container = styled(View)`
height: 46px;
justify-content: center;
`;
RecordButton.Icon = styled(MaterialCommunityIcons).attrs({
size: 26,
name: 'microphone',
color: 'red',
})``;
const Input = styled(TextInput).attrs((props) => ({}))`
background-color: grey;
border-radius: 10px;
color: black;
flex: 1;
font-size: 17px;
max-height: 180px;
padding: 12px 18px;
text-align-vertical: top;
`;
const App = () => {
const [isFocused, setIsFocused] = useState(false);
const inputBoxTranslateX = useRef(new Value(0)).current;
const contentTranslateY = useRef(new Value(0)).current;
const contentOpacity = useRef(new Value(0)).current;
const textTranslateX = useRef(new Value(-10)).current;
const position = useRef(new NativeAnimated.ValueXY()).current;
const handlePressIn = () => {
setIsFocused(true);
const input_box_translate_x_config = {
duration: 200,
toValue: -width,
easing: Easing.inOut(Easing.ease),
};
const text_translate_x_config = {
duration: 200,
toValue: -50,
easing: Easing.inOut(Easing.ease),
};
const content_translate_y_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
const content_opacity_config = {
duration: 200,
toValue: 1,
easing: Easing.inOut(Easing.ease),
};
timing(inputBoxTranslateX, input_box_translate_x_config).start();
timing(contentTranslateY, content_translate_y_config).start();
timing(contentOpacity, content_opacity_config).start();
timing(textTranslateX, text_translate_x_config).start();
};
const handlePressOut = ({isFromPan, pos}) => {
// console.log(position._value);
if (!isFromPan) {
return;
}
if (isMoving && !isFromPan) {
return;
}
console.log(isMoving);
setIsFocused(false);
const input_box_translate_x_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
const text_translate_x_config = {
duration: 200,
toValue: -10,
easing: Easing.inOut(Easing.ease),
};
const content_translate_y_config = {
duration: 0,
toValue: height,
easing: Easing.inOut(Easing.ease),
};
const content_opacity_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
timing(inputBoxTranslateX, input_box_translate_x_config).start();
timing(contentTranslateY, content_translate_y_config).start();
timing(contentOpacity, content_opacity_config).start();
timing(textTranslateX, text_translate_x_config).start();
};
const panResponder = React.useRef(
PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
const {dx, dy} = gestureState;
const shouldCap = dx > 2 || dx < -2;
if (shouldCap) {
isMoving = true;
}
return shouldCap;
},
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
const {dx, dy} = gestureState;
const shouldCap = dx > 2 || dx < -2;
if (shouldCap) {
isMoving = true;
}
return shouldCap;
},
onPanResponderMove: NativeAnimated.event(
[null, {dx: position.x, dy: position.y}],
{
useNativeDriver: false,
listener: (event, gestureState) => {
let {pageX, pageY} = event.nativeEvent;
isMoving = true;
console.log({pageX});
if (pageX < width / 2) {
console.log('Message cancelled');
}
},
},
),
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
let {pageX, pageY} = evt.nativeEvent;
isMoving = false;
// if (pageX > 300) {
handlePressOut({isFromPan: true});
// }
NativeAnimated.spring(position, {
toValue: {x: 0, y: 0},
friction: 10,
useNativeDriver: true,
}).start();
},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
}),
).current;
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
flex: 1,
marginHorizontal: 12,
}}>
<Animated.View
style={{
height: 50,
transform: [{translateX: inputBoxTranslateX}],
flexGrow: 1,
}}>
<Input style={{width: '100%', height: 40}} />
</Animated.View>
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<Animated.View
style={{
opacity: contentOpacity,
transform: [{translateX: textTranslateX}],
}}>
{isFocused ? (
<Animated.Text
style={{
color: 'black',
fontSize: 20,
}}>
Slide Left to cancel
</Animated.Text>
) : null}
</Animated.View>
<NativeAnimated.View
style={[
{
alignItems: 'center',
justifyContent: 'center',
width: 46,
},
{
transform: [
{
translateX: position.x.interpolate({
inputRange: [-width + 80, 0],
outputRange: [-width + 80, 0],
extrapolate: 'clamp',
}),
},
{
scale: position.x.interpolate({
inputRange: [-width - 60, 0],
outputRange: [1.8, 1],
extrapolate: 'clamp',
}),
},
],
},
isFocused
? {
backgroundColor: 'orange',
borderRadius: 10,
}
: {},
]}
{...panResponder.panHandlers}>
<RecordButton
onPressIn={handlePressIn}
onPressOut={() => handlePressOut({pos: position})}
/>
</NativeAnimated.View>
</View>
</View>
);
};
export default App;
snippet above produces the following:
The problems with this snippet are:
the pan responder of the mic button allows it to move horizontally even if I do not press the button (not happening on video but in real device)
pan gesture allows moving both left/right while it should be moving only to left
when the mic button arrives at the middle of the screen, the button should be "released" and return to the initial position.
when dragging the button, the text "slide to cancel" should move along the button and not stay static.
whatsapp demo:
viber demo:
I use react-navigation > 5 and try to configure a iOS type modal. The one they show in the documentation works
well but I would like the modal to appear and stop in a certain height (like one third of the screen) and let the possibility to the user to drag it higher(with a certain margin from the top) or disabled the modal by swiping it down.
I checked all the config used in ModalPresentationIOS but I still have difficulties to get the right behavior. For the moment I'm only able to set up the correct height, but the user can't drag the modal higher. Here my config:
<RootStack.Navigator
headerMode="none"
mode="modal"
screenOptions={({ route, navigation }) => ({
cardStyle: { backgroundColor: 'transparent' },
cardOverlayEnabled: true,
gestureEnabled: true,
headerStatusBarHeight:
navigation.dangerouslyGetState().routes.indexOf(route) > 0
? 0
: undefined,
gestureDirection: 'vertical',
transitionSpec: {
open: TransitionIOSSpec,
close: TransitionIOSSpec,
},
cardStyleInterpolator: forModalPresentationIOS,
headerStyleInterpolator: forFade
})}
>
With this config for forModalPresentationIOS:
function forModalPresentationIOS({
index,
current,
next,
inverted,
layouts: { screen },
insets,
}: StackCardInterpolationProps): StackCardInterpolatedStyle {
const isLandscape = screen.width > screen.height;
const topOffset = isLandscape ? 0 : 10;
const statusBarHeight = insets.top;
const aspectRatio = screen.height / screen.width;
const topMargin = 100; // Variable that I use to set up the height of the modal
const progress = add(
current.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: 'clamp',
}),
next
? next.progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolate: 'clamp',
})
: 0
);
const translateY = multiply(
progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [
screen.height,
index === 0 ? 0 : topMargin,
(index === 0 ? statusBarHeight : 0) - topOffset * aspectRatio,
],
}),
inverted
);
const overlayOpacity = progress.interpolate({
inputRange: [0, 1, 1.0001, 2],
outputRange: [0, 0.3, 1, 1],
});
const scale = isLandscape
? 1
: progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [
1,
1,
screen.width ? 1 - (topOffset * 2) / screen.width : 1,
],
});
const borderRadius = isLandscape
? 0
: index === 0
? progress.interpolate({
inputRange: [0, 1, 1.0001, 2],
outputRange: [0, 0, isIphoneX() ? 38 : 0, 10],
})
: 10;
return {
cardStyle: {
overflow: 'hidden',
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
marginTop: index === 0 ? 0 : statusBarHeight,
marginBottom: index === 0 ? 0 : topOffset,
transform: [{ translateY }, { scale }],
},
overlayStyle: { opacity: overlayOpacity },
};
}
Thanks!
Haven't found any right config, so I came with this solution that works. We just check when the user press the header of the modal and move it, and adjust the marginTop of the modal to make it move. Here the code:
RootStack
import React, { useState, useEffect } from 'react';
import { Dimensions } from 'react-native';
import {
createStackNavigator,
} from '#react-navigation/stack';
// import components
import MainNav from './MainNav';
import ModalScreen from '../screens/Modal/ModalScreen';
// Types
export type RootStackParamList = {
Main: undefined;
ModalScreen: { handleModalHeight: (arg1: number) => void };
};
interface Props {};
// ***
const { height } = Dimensions.get('window');
const RootStack = createStackNavigator<RootStackParamList>();
const RootNav: React.FunctionComponent<Props> = props => {
function handleModalHeight(modalHeight: number): void {
if(modalHeight >= Math.floor(height/3) && modalHeight <= Math.floor(height -(height/3))){
setMarginTop(modalHeight);
}
}
const [ marginTop, setMarginTop ] = useState<number>(Math.floor(height -(height/3)));
return(
<RootStack.Navigator
mode="modal"
headerMode="none"
>
<RootStack.Screen
name="Main"
component={MainNav}
/>
<RootStack.Screen
name="ModalScreen"
component={ModalScreen}
initialParams={{handleModalHeight}}
options={{
cardStyle: {
overflow: 'hidden',
borderTopLeftRadius: 38,
borderTopRightRadius: 38,
marginTop
},
gestureResponseDistance: {
vertical: marginTop
}
}}
/>
</RootStack.Navigator>
);
}
export default RootNav;
ModalScreen
import React, { useState, useRef, useEffect } from 'react';
import {
Text,
PanResponder,
YellowBox,
TouchableWithoutFeedback,
PanResponderInstance
} from 'react-native';
import styled from 'styled-components/native';
import { RouteProp } from '#react-navigation/native';
YellowBox.ignoreWarnings([
'Non-serializable values were found in the navigation state',
]);
import { RootStackParamList } from '../../navigation/RootNav';
type ModalScreenRouteProps = RouteProp<RootStackParamList, 'ModalScreen'>;
type Props = {
route: ModalScreenRouteProps;
};
const ModalScreen: React.FunctionComponent<Props> = ({route}) => {
const handleModalHeight = route.params?.handleModalHeight;
const [ isModalSpreading, setIsModalSpreading ] = useState<boolean>(false);
const panResponder = useRef<PanResponderInstance | null>(null);
useEffect(()=> {
panResponder.current = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderMove: (evt, gestureState) => {
const { moveY } = gestureState;
if(isModalSpreading) {
handleModalHeight(moveY);
}
},
onPanResponderRelease: () => {
setIsModalSpreading(false)
}
})
}, [isModalSpreading]);
return (
<Container {...panResponder.current?.panHandlers} >
<TouchableWithoutFeedback
onPressIn={() => setIsModalSpreading(true)}
onPressOut={() => setIsModalSpreading(false)}
>
<Header>
<BarHeader/>
</Header>
</TouchableWithoutFeedback>
<BodyModal>
<Text>ModalPlace</Text>
</BodyModal>
</Container>
);
}
const Container = styled.View`
flex: 1;
justify-content: flex-start;
align-items: center;
background-color: white;
`;
const Header = styled.View`
width: 100%;
height: 40px;
justify-content: center;
align-items: center;
`;
const BarHeader = styled.View`
width: 60px;
height: 5px;
background-color: lightgrey;
border-radius: 2.5px;
`;
const BodyModal = styled.View`
justify-content: center;
align-items: center;
background-color: white;
`;
export default ModalScreen;
This method is rough and with poor performance. The best I think is too open a classic modal with transparent background and to use an Animated.View to animate the height of a View when the user will drag the modal up.
I want to create a navigation drawer like in uber . So Is there are any libraries or packages that i can used to develop the navigation drawer of my app based on react router 4.
Use this
https://github.com/Tinysymphony/react-native-drawer-menu
https://github.com/root-two/react-native-drawer
and you can creat a view by yourself, it't easy, like this:
import React, { PureComponent } from 'react';
import {
View,
Animated,
Easing,
Dimensions,
TouchableOpacity,
TouchableWithoutFeedback
} from 'react-native';
import mitt from 'mitt';
import styled from 'styled-components';
import { t } from '../i18n';
import { TOOLBAR_HEIGHT } from './Toolbar';
import Avatar from '../components/Avatar';
import Icon from '../components/Icon';
import Text from './Text';
const { width } = Dimensions.get('window');
//-----------------------------------------------
class MenuDrawer extends PureComponent {
static defaultProps = {
width: (width * 75) / 100
};
constructor(props) {
super(props);
this.state = { show: false };
this.emitter = mitt();
this.animateValue = {
opacity: new Animated.Value(0),
marginLeft: new Animated.Value(-props.width)
};
}
animation({ opacity, mgleft }) {
this.animateValue.opacity.setValue(opacity.start);
this.animateValue.marginLeft.setValue(mgleft.start);
return Animated.parallel([
Animated.timing(this.animateValue.opacity, {
toValue: opacity.end,
duration: 100,
easing: Easing.linear,
useNativeDriver: true
}),
Animated.timing(this.animateValue.marginLeft, {
toValue: mgleft.end,
duration: 100,
easing: Easing.linear,
useNativeDriver: true
})
]);
}
subscribe = cb => {
this.emitter.on('state-change', status => {
cb(status);
});
};
show = () => {
this.emitter.emit('state-change', true);
this.setState({ ...this.state, show: true }, () => {
const opacity = { start: 0, end: 0.6 };
const mgleft = { start: this.props.width, end: 0 };
this.animation({ opacity, mgleft }).start();
});
};
hide = () => {
this.emitter.emit('state-change', false);
const opacity = { start: 0.6, end: 0 };
const mgleft = { start: 0, end: this.props.width };
this.animation({ opacity, mgleft }).start(({ finished }) => {
if (finished) {
this.setState({ ...this.state, show: false });
}
});
};
handlePressOnBackground = () => {
this.hide();
};
renderContent() {
const { me } = this.props;
const name = me.fullname;
const avatar_url = me.avatar_url;
return (
<StyledView>
<StyledMyInfo>
<Avatar size={96} uri={avatar_url} name={name} isAgent={true} />
<StyledName text={name} />
</StyledMyInfo>
<StyledMenu>
<TouchableOpacity onPress={this.props.handlePressSettings}>
<StyledMenuItem>
<StyledIcon>
<Icon size={20} source={require('../assets/setting.png')} />
</StyledIcon>
<StyledLabel text={t('general_settings')} />
</StyledMenuItem>
</TouchableOpacity>
<TouchableOpacity onPress={this.props.handlePressChangePassword}>
<StyledMenuItem>
<StyledIcon>
<Icon
size={20}
ml={2}
source={require('../assets/password.png')}
/>
</StyledIcon>
<StyledLabel text={t('profile_language_change_password_link')} />
</StyledMenuItem>
</TouchableOpacity>
<TouchableOpacity onPress={this.props.handlePressLogout}>
<StyledMenuItem>
<StyledIcon>
<Icon
size={20}
ml={-1}
source={require('../assets/logout.png')}
/>
</StyledIcon>
<StyledLabel text={t('agent_profile_logout')} />
</StyledMenuItem>
</TouchableOpacity>
</StyledMenu>
</StyledView>
);
}
render() {
const { id } = this.props.me;
if (!this.state.show || !id) return null;
return (
<StyledViewContainer>
<TouchableWithoutFeedback
style={{ flex: 1 }}
onPress={this.handlePressOnBackground}
>
<Animated.View
style={[
{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
backgroundColor: 'black'
},
{ opacity: this.animateValue.opacity }
]}
/>
</TouchableWithoutFeedback>
<Animated.View
style={[
{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
width: '75%'
},
{ marginLeft: -this.animateValue.marginLeft }
]}
>
{this.renderContent()}
</Animated.View>
</StyledViewContainer>
);
}
}
//-----------------------------------------------
const StyledViewContainer = styled(View)`
position: absolute;
top: ${TOOLBAR_HEIGHT};
right: 0;
bottom: 0;
left: 0;
`;
const StyledView = styled(View)`
flex: 1;
background-color: #f9f9f9;
`;
const StyledMyInfo = styled(View)`
justify-content: center;
align-items: center;
height: 240;
background-color: white;
`;
const StyledName = styled(Text)`
margin-top: 10px;
font-size: ${props => props.theme.fontSizeLarge};
font-family: ${props => props.theme.fontSemiBold};
`;
const StyledMenu = styled(View)`
padding: 12px 15px;
`;
const StyledMenuItem = styled(View)`
margin-bottom: 5px;
flex-direction: row;
align-items: center;
`;
const StyledIcon = styled(View)`
width: 40;
height: 40;
justify-content: center;
align-items: center;
`;
const StyledLabel = styled(Text)``;
export default MenuDrawer;