I'm to build a calendar web application. For that I need a week view, showing all events on that week. My go to reference is Google Calendar because the design is modern and is composed really well. So for the application I'm using React with redux as state management. But I seem to have run in to a problem that has been bugging me for a while now. Getting the scroll to work as the scroll in Google calendar week view, (Vertical scroll that is).
The important components:
Lowest level component (name: body)
import React from 'react';
import { withStyles, withWidth, Typography } from '#material-ui/core';
import moment from 'moment';
import {
weekHeadMaxWidthXl,
weekHeadMinWidthXl,
weekHeadMaxWidthLg,
weekHeadMinWidthLg,
weekHeadMaxWidthMd,
weekHeadMinWidthMd,
weekHeadMaxWidthSm,
weekHeadMinWidthSm,
weekHeadMaxWidthXs,
weekHeadMinWidthXs,
weekHeadHightXl
} from '../../../util/dimension';
const style = theme => ({
bodyContainer: {
display: "flex",
flexDirection: "row",
overflow: "auto",
width: "100%",
},
hourContainer: {
display: "flex",
flexDirection: "column",
marginTop: 1,
[theme.breakpoints.only('xl')]: {
maxWidth: weekHeadMaxWidthXl,
minWidth: weekHeadMinWidthXl,
width: "100%",
},
[theme.breakpoints.only('lg')]: {
maxWidth: weekHeadMaxWidthLg,
minWidth: weekHeadMinWidthLg,
width: "100%",
},
[theme.breakpoints.only('md')]: {
maxWidth: weekHeadMaxWidthMd,
minWidth: weekHeadMinWidthMd,
width: "100%",
},
[theme.breakpoints.only('sm')]: {
maxWidth: weekHeadMaxWidthSm,
minWidth: weekHeadMinWidthSm,
width: "100%",
backgroundColor: "#"
},
[theme.breakpoints.only('xs')]: {
maxWidth: weekHeadMaxWidthXs,
minWidth: weekHeadMinWidthXs,
width: "100%",
},
},
hour: {
[theme.breakpoints.only('xl')]: {
maxHeight: "50px",
height: "46px",
minHeight: "44px",
},
[theme.breakpoints.only('lg')]: {
maxHeight: "50px",
height: "46px",
minHeight: "44px",
},
[theme.breakpoints.only('md')]: {
maxHeight: "50px",
height: "46px",
minHeight: "44px",
},
[theme.breakpoints.only('sm')]: {
maxHeight: "50px",
height: "46px",
minHeight: "44px",
},
borderBottom: "solid 1px #e7e7e7",
borderLeft: "solid 1px #e7e7e7",
backgroundColor: "#FFF"
},
offset: {
display: "flex",
justifyContent: "center",
paddingTop: 10,
backgroundColor: "#FFF",
borderBottom: "solid 1px #e7e7e7",
borderTop: "solid 1px #e7e7e7",
[theme.breakpoints.only('xl')]: {
maxWidth: 55,
minWidth: 50,
width: 53,
},
[theme.breakpoints.only('lg')]: {
maxWidth: 53,
minWidth: 47,
width: 50,
},
[theme.breakpoints.only('md')]: {
maxWidth: 50,
minWidth: 43,
width: 47,
},
[theme.breakpoints.only('sm')]: {
maxWidth: 47,
minWidth: 38,
width: 43,
},
[theme.breakpoints.only('xs')]: {
maxWidth: 47,
minWidth: 43,
width: 38,
},
},
});
const body = props => {
const { classes } = props;
let days = [];
const getHours = () => {
let hours = [];
for (let i = 0; i < 24; i++) {
hours.push(<div key={"hours " + i} className={classes.hour}>
<Typography> {i} </Typography>
</div>)
}
return hours
}
days.push(<div key="offset" className={classes.offset}></div>)
for (let j = 0; j < 7; j++) {
days.push(<div key={"days " + j} className={classes.hourContainer}>
{getHours()}
</div>)
}
return (<div className={classes.bodyContainer}>
{days}
</div>);
}
export default withWidth()(withStyles(style)(body));
Component implementing body. (name: Calendar)
import React from 'react';
import { withStyles, Typography, TableCell } from '#material-ui/core';
import withWidth, { isWidthUp } from '#material-ui/core/withWidth';
import { connect } from 'react-redux'
import { changeWindowSizeAction } from '../../redux/actions/settingsActions';
import Month from './views/month/month';
import MonthOffset from './views/month/offset';
import WeekHead from './views/week/head';
import WeekBody from './views/week/body';
const styles = theme => ({
week: {
height: "100vh",
},
weekBody: {
height: "100vh",
overflowY: "auto"
}
});
class Calendar extends React.Component {
getFullDayBookings = bookings => {
return bookings.filter(B => B.allDay);
}
generateComponent = (variant, props) => {
switch (variant) {
case "month":
return <Month onDayClick={props.onDayClick} onBookingClick={props.onBookingClick} dateNr={props.nr} day={props.day} bookings={this.formatBookings()} />
case "week":
return this.generateWeek()
case "day":
return null;
case "agenda":
return null;
case "offset":
return <MonthOffset dateNr={props.nr} variant="month" />
default:
return (
<TableCell className={props.classes.monthCell}>
</TableCell>
);
}
}
generateWeek = () => {
return (<div className={this.props.classes.week}>
<WeekHead bookings={this.formatBookings().filter(E => E.allDay)} />
<div className={this.props.classes.weekBody}>
<WeekBody />
</div>
</div>)
}
formatBookings = () => {
if (this.props.bookings.length > 0) {
return this.filterBookings(this.props.bookings, this.props.bookingFilter)
} else {
return [];
}
}
filterBookings = (bookings, filter) => {
if (bookings.length > 0) {
let notToRender = Object.keys(filter).filter(F => {
if (!filter[F]) {
return F;
}
});
return bookings.filter(E => {
if (notToRender.length > 0) {
if (notToRender.indexOf(E.room) === -1) {
return E
}
} else {
return E;
}
});
} else {
return []
}
}
render() {
return (this.generateComponent(this.props.variant, this.props))
}
}
const mapDispatchToProps = dispatch => {
return {
changeWindowSize: wSize => (dispatch(changeWindowSizeAction(wSize)))
}
}
const mapStateToProps = state => {
return {
windowSize: state.settings.windowSize,
}
}
export default withWidth()(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Calendar)));
Component implementing Calendar (name: week)
import React from 'react';
import { connect } from 'react-redux';
import Calendar from '../components/calendar/Calendar';
import { withStyles, Typography } from '#material-ui/core';
import moment from 'moment';
const styles = theme => ({
});
const testEvents = [
{ "title": "Test - NotAllDay", "id": 1, "room": "office" },
{ "title": "Test - NotAllDay", "id": 2, "room": "office" },
{ "title": "Test - Cafe", "id": 3, "room": "cafe", "allDay": true, "startDate": moment(), "endDate": moment().add(2, "days") },
{ "title": "Test - NotAllDay", "id": 11, "room": "office" }]
class Week extends React.Component {
render(){
return <Calendar variant="week" bookings={testEvents} bookingFilter={this.props.filter}/>
}
}
const mapStateToPops = state => ({
currentDate: state.settings.calendarState,
filter: state.settings.roomsRender,
});
const mapDispatchToProps = dispatch => ({
});
export default withStyles(styles)(connect(mapStateToPops, mapDispatchToProps)(Week));
Last, Component implementing Week
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withStyles } from '#material-ui/core/styles/';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Banner from '../containers/Banner';
import Sidebar from '../containers/Sidebar';
import Month from '../containers/Month';
import Week from '../containers/Week';
import './styles.css';
const styles = theme => ({
DisplayContainer: {
flex: 1,
height: "100vh"
},
content: {
height: "100%",
flex: 3,
flexGrow: 1,
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
'content-left': {
marginLeft: 0,
},
contentShift: {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
},
'contentShift-left': {
marginLeft: 240,
},
});
class Overview extends Component {
calendarView = () => {
switch (this.props.calendarType) {
case "month":
return <Month />
case "week":
return <Week />
case "day":
return <div></div>
case "agenda":
return <div></div>
default:
return <div></div>
}
}
render() {
const { classes, theme } = this.props;
return (
<div className={classes.DisplayContainer}>
<div className="Banner">
<Banner />
</div>
<div>
<Sidebar />
</div>
<main className={classNames(classes.content, classes[`content-left`], {
[classes.contentShift]: this.props.showMenu,
[classes[`contentShift-left`]]: this.props.showMenu,
})}>
{this.calendarView()}
</main>
</div>
)
}
}
Overview.propTypes ={
classes: PropTypes.object.isRequired,
}
const mapStateToProps = state => {
return {
showMenu: state.settings.showMenu,
calendarType: state.settings.calendarType,
}
}
export default withStyles(styles)(connect(mapStateToProps)(Overview));
The Month view works as it should. Tho have some bugs, but the application is long from done. So, yeah if one could point me in the direction or provide some inside knowledge, that would be great!
Thanks in advice!
Here is the CSS fix:
Add this CSS to your application:
html, #root {
height: 100%
}
.jss2.jss3{
height: calc(100% - 65px);
}
.jss189 {
height: calc(100% - 132px);
}
Related
I'm trying to use a useRef hook so a scrollview and my pan gesture handler can share a common ref. but once I initialize the useRef() hook and pass it to both components, it breaks with this error
TypeError: Attempted to assign to readonly property.
I've tried typecasting and adding types to the useRef call but it returns the same error. Can someone help please?
My component:
import { StyleSheet, Text, View, Image, Dimensions } from "react-native";
import React from "react";
import {
PanGestureHandler,
PanGestureHandlerGestureEvent,
PanGestureHandlerProps,
} from "react-native-gesture-handler";
import Animated, {
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { FontAwesome } from "#expo/vector-icons";
export interface InfluencerItemProps
extends Pick<PanGestureHandlerProps, "simultaneousHandlers"> {
id?: string;
name: string;
userName: string;
profileImg: string;
rating?: Number;
onDismiss?: (Item: InfluencerItemProps) => void;
}
const ITEM_HEIGHT = 65;
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const TRANSLATE_X_THRESHOLD = -SCREEN_WIDTH * 0.3;
const InfluencerItem = (props: InfluencerItemProps) => {
const { name, userName, profileImg, onDismiss, simultaneousHandlers } = props;
const translateX = useSharedValue(0);
const marginVertical = useSharedValue("2%");
const R_Height = useSharedValue(ITEM_HEIGHT);
const opacity = useSharedValue(1);
const panGesture = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
onActive: (event) => {
translateX.value = event.translationX;
},
onEnd: () => {
const shouldbeDismissed = translateX.value < TRANSLATE_X_THRESHOLD;
if (shouldbeDismissed) {
translateX.value = withTiming(-SCREEN_WIDTH);
R_Height.value = withTiming(0);
marginVertical.value = withTiming("0%");
opacity.value = withTiming(0, undefined, (isFinished) => {
if (isFinished && onDismiss) {
runOnJS(onDismiss)(props);
}
});
} else {
translateX.value = withTiming(0);
}
},
});
const rStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: translateX.value,
},
],
}));
const rIconContainerStyle = useAnimatedStyle(() => {
const opacity = withTiming(
translateX.value < TRANSLATE_X_THRESHOLD ? 1 : 0
);
return { opacity };
});
const RContainerStyle = useAnimatedStyle(() => {
return {
height: R_Height.value,
opacity: opacity.value,
marginVertical: marginVertical.value,
};
});
return (
<Animated.View style={[styles.wrapper, RContainerStyle]}>
<Animated.View style={[styles.iconContainer, rIconContainerStyle]}>
<FontAwesome name="trash" size={ITEM_HEIGHT * 0.5} color="white" />
</Animated.View>
<PanGestureHandler
simultaneousHandlers={simultaneousHandlers}
onGestureEvent={panGesture}
>
<Animated.View style={[styles.container, rStyle]}>
<Image
source={{
uri: profileImg,
}}
style={styles.image}
/>
<View style={styles.text}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.userName}>{userName}</Text>
</View>
</Animated.View>
</PanGestureHandler>
</Animated.View>
);
};
export default InfluencerItem;
const styles = StyleSheet.create({
wrapper: {
width: "100%",
alignItems: "center",
},
container: {
flexDirection: "row",
borderWidth: 1,
borderRadius: 12,
height: ITEM_HEIGHT,
width: "100%",
backgroundColor: "#FFFFFF",
},
image: {
marginVertical: 10,
marginHorizontal: "4%",
height: 48,
width: 48,
borderRadius: 50,
},
text: {
justifyContent: "center",
alignItems: "flex-start",
marginHorizontal: 6,
},
name: {
fontSize: 14,
fontWeight: "500",
color: "#121212",
},
userName: {
fontSize: 12,
fontWeight: "400",
color: "#121212",
},
iconContainer: {
height: ITEM_HEIGHT,
width: ITEM_HEIGHT,
backgroundColor: "red",
position: "absolute",
right: "2.5%",
justifyContent: "center",
alignItems: "center",
},
});
InfluencerItem.defaultProps = {
name: "UserName",
userName: "userName",
profileImg:
"https://d2qp0siotla746.cloudfront.net/img/use-cases/profile-picture/template_0.jpg",
rating: "4",
};
This is my Screen:
import {
StyleSheet,
Text,
View,
SafeAreaView,
TextInput,
ScrollView,
} from "react-native";
import { StatusBar } from "expo-status-bar";
import React, { useCallback, useRef, useState } from "react";
import InfluencerItem from "../../components/InfluencerItem";
import { InfluencerItemProps } from "../../components/InfluencerItem";
// import { ScrollView } from "react-native-gesture-handler";
type Props = {};
const Saved = (props: Props) => {
const [search, setSearch] = useState<string>("");
const [influencerData, setInfluencerData] = useState(influencerz);
const handleSearch = () => {
console.log(search);
};
const onDismiss = useCallback((Item: InfluencerItemProps) => {
setInfluencerData((influencers) =>
influencers.filter((item) => item.id !== Item.id)
);
}, []);
const ref = useRef(null); //useRef initialization
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<View style={styles.saved}>
{/* Saved Kikos component goes in here */}
<ScrollView ref={ref}> //passed ref here
{influencerData.map((influencer) => (
<InfluencerItem
key={influencer.id}
name={influencer.name}
userName={influencer.handle}
profileImg={influencer.image}
onDismiss={onDismiss}
simultaneousHandlers={ref} //also passed ref here
/>
))}
</ScrollView>
</View>
<Text style={styles.bottomText}>No Saved Kikos again</Text>
</View>
</SafeAreaView>
);
};
export default Saved;
const styles = StyleSheet.create({
container: {
paddingHorizontal: "4%",
},
headerText: {
color: "#121212",
fontWeight: "700",
lineHeight: 30,
fontSize: 20,
marginTop: 40,
},
search: {
borderRadius: 12,
backgroundColor: "#D9D9D9",
fontSize: 14,
lineHeight: 21,
color: "#7A7B7C",
paddingLeft: 10,
paddingRight: 5,
height: 45,
marginTop: 15,
position: "relative",
},
innerSearch: {
position: "absolute",
top: 30,
right: 10,
},
saved: {
backgroundColor: "rgba(217, 217, 217, 0.15)",
marginTop: 22,
paddingVertical: "7%",
marginBottom: 34,
},
bottomText: {
fontSize: 14,
fontWeight: "500",
textAlign: "center",
},
});
Use createRef() instead of useRef() as mentioned in the documentation.
const imagePinch = React.createRef();
return (
<RotationGestureHandler
simultaneousHandlers={imagePinch}
....
There is a complete example here in TypeScript.
Also make sure to use Animated version of components wherever applicable (<Animated.View> instead of <View>, <Animated.Image> instead of <Image> etc)
I have used npm i react-native-location-view command to install react-native-location-view for using google map but I faced typeError in ........\node_modules\react-native-location-view\src\LocationView.js Location.
The Version are node -> v16.6.2, npm -> 7.20.3 and expo -> 4.10.0.
locationView.js
import React from 'react';
import PropTypes from 'prop-types';
import { View, StyleSheet, Animated, Platform, UIManager,
TouchableOpacity, Text, ViewPropTypes } from 'react-native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import Entypo from 'react-native-vector-icons/Entypo';
import axios from 'axios';
import Events from 'react-native-simple-events';
import MapView from 'react-native-maps';
import Geolocation from '#react-native-community/geolocation';
import AutoCompleteInput from './AutoCompleteInput';
const PLACE_DETAIL_URL = 'https://maps.googleapis.com/maps/api/place/details/json';
const DEFAULT_DELTA = { latitudeDelta: 0.015, longitudeDelta: 0.0121 };
export default class LocationView extends React.Component {
static propTypes = {
apiKey: PropTypes.string.isRequired,
initialLocation: PropTypes.shape({
latitude: PropTypes.number,
longitude: PropTypes.number,
}).isRequired,
markerColor: PropTypes.string,
actionButtonStyle: ViewPropTypes.style,
actionTextStyle: Text.propTypes.style,
actionText: PropTypes.string,
onLocationSelect: PropTypes.func,
debounceDuration: PropTypes.number,
components: PropTypes.arrayOf(PropTypes.string),
timeout: PropTypes.number,
maximumAge: PropTypes.number,
enableHighAccuracy: PropTypes.bool
};
static defaultProps = {
markerColor: 'black',
actionText: 'DONE',
onLocationSelect: () => ({}),
debounceDuration: 300,
components: [],
timeout: 15000,
maximumAge: Infinity,
enableHighAccuracy: true
};
constructor(props) {
super(props);
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
componentDidMount() {
Events.listen('InputBlur', this.constructor.displayName, this._onTextBlur);
Events.listen('InputFocus', this.constructor.displayName, this._onTextFocus);
Events.listen('PlaceSelected', this.constructor.displayName, this._onPlaceSelected);
}
componentWillUnmount() {
Events.rm('InputBlur', this.constructor.displayName);
Events.rm('InputFocus', this.constructor.displayName);
Events.rm('PlaceSelected', this.constructor.displayName);
}
state = {
inputScale: new Animated.Value(1),
inFocus: false,
region: {
...DEFAULT_DELTA,
...this.props.initialLocation,
},
};
_animateInput = () => {
Animated.timing(this.state.inputScale, {
toValue: this.state.inFocus ? 1.2 : 1,
duration: 300,
}).start();
};
_onMapRegionChange = region => {
this._setRegion(region, false);
if (this.state.inFocus) {
this._input.blur();
}
};
_onMapRegionChangeComplete = region => {
this._input.fetchAddressForLocation(region);
};
_onTextFocus = () => {
this.state.inFocus = true;
this._animateInput();
};
_onTextBlur = () => {
this.state.inFocus = false;
this._animateInput();
};
_setRegion = (region, animate = true) => {
this.state.region = { ...this.state.region, ...region };
if (animate) this._map.animateToRegion(this.state.region);
};
_onPlaceSelected = placeId => {
this._input.blur();
axios.get(`${PLACE_DETAIL_URL}?key=${this.props.apiKey}&placeid=${placeId}`).then(({ data }) => {
let region = (({ lat, lng }) => ({ latitude: lat, longitude: lng }))(data.result.geometry.location);
this._setRegion(region);
this.setState({placeDetails: data.result});
});
};
_getCurrentLocation = () => {
const { timeout, maximumAge, enableHighAccuracy } = this.props;
Geolocation.getCurrentPosition(
position => {
const { latitude, longitude } = position.coords;
this._setRegion({latitude, longitude});
},
error => console.log(error.message),
{
enableHighAccuracy,
timeout,
maximumAge,
}
);
};
render() {
let { inputScale } = this.state;
return (
<View style={styles.container}>
<MapView
ref={mapView => (this._map = mapView)}
style={styles.mapView}
region={this.state.region}
showsMyLocationButton={true}
showsUserLocation={false}
onPress={({ nativeEvent }) => this._setRegion(nativeEvent.coordinate)}
onRegionChange={this._onMapRegionChange}
onRegionChangeComplete={this._onMapRegionChangeComplete}
/>
<Entypo
name={'location-pin'}
size={30}
color={this.props.markerColor}
style={{ backgroundColor: 'transparent' }}
/>
<View style={styles.fullWidthContainer}>
<AutoCompleteInput
ref={input => (this._input = input)}
apiKey={this.props.apiKey}
style={[styles.input, { transform: [{ scale: inputScale }] }]}
debounceDuration={this.props.debounceDuration}
components={this.props.components}
/>
</View>
<TouchableOpacity
style={[styles.currentLocBtn, { backgroundColor: this.props.markerColor }]}
onPress={this._getCurrentLocation}
>
<MaterialIcons name={'my-location'} color={'white'} size={25} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, this.props.actionButtonStyle]}
onPress={() => this.props.onLocationSelect({ ...this.state.region, address: this._input.getAddress(), placeDetails: this.state.placeDetails })}
>
<View>
<Text style={[styles.actionText, this.props.actionTextStyle]}>{this.props.actionText}</Text>
</View>
</TouchableOpacity>
{this.props.children}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
mapView: {
...StyleSheet.absoluteFillObject,
},
fullWidthContainer: {
position: 'absolute',
width: '100%',
top: 80,
alignItems: 'center',
},
input: {
width: '80%',
padding: 5,
},
currentLocBtn: {
backgroundColor: '#000',
padding: 5,
borderRadius: 5,
position: 'absolute',
bottom: 70,
right: 10,
},
actionButton: {
backgroundColor: '#000',
height: 50,
position: 'absolute',
bottom: 10,
left: 10,
right: 10,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 5,
},
actionText: {
color: 'white',
fontSize: 23,
},
});
And for the error, please check the attached file,
I'm trying to create an expandable banner based of this:
https://moduscreate.com/blog/expanding-and-collapsing-elements-using-animations-in-react-native/
It seems to be when I add this code for Animations:
<Animated.View
style={[styles.container, { height: this.state.animation }]}>
From what I can tell the problem has something to do with the animation and is linked with the max or min height but I cannot work out how to do it.
The full code is:
import React, { Component } from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableHighlight,
Animated,
} from "react-native";
export default class Banner extends Component {
constructor(props) {
super(props);
this.icons = {
"up": require("../assets/glyph/arrow-up.png"),
"down": require("../assets/glyph/arrow-down.png"),
};
this.state = {
title: this.props.title,
expanded: true,
animation: new Animated.Value(),
};
}
toggle() {
let initialValue = this.state.expanded
? this.state.maxHeight + this.state.minHeight
: this.state.minHeight,
finalValue = this.state.expanded
? this.state.minHeight
: this.state.maxHeight + this.state.minHeight;
this.setState({
expanded: !this.state.expanded,
});
this.state.animation.setValue(initialValue);
Animated.spring(this.state.animation, {
toValue: finalValue,
}).start();
}
_setMaxHeight(event) {
this.setState({
maxHeight: event.nativeEvent.layout.height,
});
}
_setMinHeight(event) {
this.setState({
minHeight: event.nativeEvent.layout.height,
});
}
render() {
let icon = this.icons["down"];
if (this.state.expanded) {
icon = this.icons["up"];
}
return (
<Animated.View
style={[styles.container, { height: this.state.animation }]}
>
<View
style={styles.titleContainer}
onLayout={this._setMinHeight.bind(this)}
>
<Text style={styles.title}>{this.state.title}</Text>
<TouchableHighlight
style={styles.button}
onPress={this.toggle.bind(this)}
underlayColor="#f1f1f1"
>
<Image style={styles.buttonImage} source={icon}></Image>
</TouchableHighlight>
</View>
<View style={styles.body} onLayout={this._setMaxHeight.bind(this)}>
{this.props.children}
</View>
</Animated.View>
);
}
}
var styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
margin: 10,
overflow: "hidden",
},
titleContainer: {
flexDirection: "row",
},
title: {
flex: 1,
padding: 10,
color: "#2a2f43",
fontWeight: "bold",
},
button: {},
buttonImage: {
width: 18,
margin: 5,
marginRight: 18,
height: 18,
},
body: {
padding: 10,
paddingTop: 0,
},
});
I'm currently using the latest version of expo for react-native.
The home screen of my react native app has components which animate on scroll. However, any little change to the screen resets the components to their default state and they don't animate anymore.
I am attempting to fetch data and display it on the home screen. Whenever new content is loaded, the animated components go back to their original state.
home/index.tsx
import React, { useEffect } from 'react'
import { View, StyleSheet, StatusBar, Image } from 'react-native'
import Animated, { Extrapolate } from 'react-native-reanimated'
import { useDispatch, useSelector } from 'react-redux'
import { bannerImage, logoImage } from '../../assets/images'
import CategoryContainer from './CategoryContainer'
import colors from '../../styles/colors'
import CstmBigDisplayButton from '../../components/CstmBigDisplayButton'
import globalStyles from '../../styles/globals'
import MenuButton from '../../components/MenuButton'
import TranslucentStatusBar from '../../components/TranslucentStatusBar'
import { getCategories } from '../../redux/actions/categoryAction'
import { RootState } from '../../redux/types'
const HEADER_MAX_HEIGHT = 430
const HEADER_MIN_HEIGHT = 100
const Home = ({ navigation }: any) => {
const { categories } = useSelector((state: RootState) => state.categories)
const dispatch = useDispatch()
useEffect(() => {
dispatch(getCategories())
}, [])
let scrollY = new Animated.Value(0)
const headerHeight = Animated.interpolate(
scrollY,
{
inputRange: [0, HEADER_MAX_HEIGHT],
outputRange: [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT],
extrapolate: Extrapolate.CLAMP,
}
)
const animateOverlay = Animated.interpolate(
scrollY,
{
inputRange: [0, HEADER_MAX_HEIGHT / 2, HEADER_MAX_HEIGHT - 30],
outputRange: [1, 0.5, 0.1],
extrapolate: Extrapolate.CLAMP,
}
)
const menuOpacity = Animated.interpolate(
scrollY,
{
inputRange: [0, HEADER_MAX_HEIGHT - 30],
outputRange: [0.5, 1],
extrapolate: Extrapolate.CLAMP
}
)
return (
<>
<TranslucentStatusBar />
<Animated.View
style={[
styles.header,
{
height: headerHeight,
elevation: 10
}
]}
>
<View style={styles.headerBackground} >
<Animated.View
style={{
position: 'absolute',
top: 0,
right: 0,
zIndex: 11,
marginTop: StatusBar.currentHeight,
opacity: menuOpacity,
}}
>
<View style={{ margin: 16 }}>
<MenuButton onPress={() => {navigation.openDrawer()}}/>
</View>
</Animated.View>
<Animated.Image
source={bannerImage}
resizeMode='cover'
style={{
position: 'absolute',
left: 0,
right: 0,
top: 0,
height: headerHeight,
opacity: animateOverlay,
}}/>
<Animated.View
style={[
styles.overlay,
{
backgroundColor: animateOverlay
}
]}
>
<Image
source={logoImage}
style={styles.logo}
resizeMode='contain'
/>
</Animated.View>
</View>
</Animated.View>
<Animated.ScrollView
scrollEventThrottle={16}
style={globalStyles.screenDefaults}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{
useNativeDriver: true,
listener: (event: any) => console.log(event)
}
)}
>
<View style={styles.contentContainer}>
<CategoryContainer
titleStyle={[styles.general]}
titleText='General Categories'
titleTextStyle={{ color: colors.primary["500TextColor"] }}
categories={categories.filter((category: any) => !category.special)}
navigation={navigation}
/>
<View style={styles.divider}></View>
<CategoryContainer
titleStyle={[styles.special]}
titleText='Special Categories'
titleTextStyle={{ color: colors.secondary["700TextColor"] }}
categories={categories.filter((category: any) => category.special)}
navigation={navigation}
/>
</View>
<CstmBigDisplayButton />
</Animated.ScrollView>
</>
)
}
const styles = StyleSheet.create({
container: {
flex: 1
},
header: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
height: HEADER_MAX_HEIGHT,
backgroundColor: 'grey',
zIndex: 10,
alignContent: 'center',
justifyContent: 'center'
},
headerBackground: {
width: '100%',
flex: 1,
backgroundColor: '#FFF'
},
logo: {
flex: 1,
height: undefined,
width: undefined,
},
overlay: {
flex: 1,
paddingTop: StatusBar.currentHeight
},
contentContainer: {
flexDirection: 'row',
flex: 1,
paddingTop: HEADER_MAX_HEIGHT,
paddingBottom: 24
},
general: {
backgroundColor: colors.primary[500],
color: colors.primary["500TextColor"]
},
special: {
backgroundColor: colors.secondary[700],
color: colors.secondary["700TextColor"]
},
divider: {
backgroundColor: '#909090',
height: '99%',
width: '0.1%'
},
})
export default Home
globals.ts
import { StyleSheet, StatusBar } from 'react-native'
import theme, { standardInterval } from './theme'
const globalStyles = StyleSheet.create({
gutterBottom: {
marginBottom: 8
},
captionText: {
fontSize: 12
},
screenDefaults: {
flex: 1,
backgroundColor: theme.pageBackgroundColor,
},
headlessScreenDefaults: {
paddingTop: StatusBar.currentHeight,
padding: 16
},
iconText: {
flexDirection: 'row',
alignItems: 'center'
},
iconTextMargin: {
marginLeft: 4
},
label: {
fontSize: 16,
marginBottom: 4,
paddingLeft: 12,
color: '#555'
},
textInput: {
padding: 0,
fontSize: 16,
color: '#444',
marginLeft: 6,
marginRight: 8,
flex: 1
},
textInputContainer: {
borderWidth: 1,
borderColor: '#ddd',
backgroundColor: 'white',
borderRadius: standardInterval(0.5),
padding: 8,
flexDirection: 'row',
alignItems: 'center'
},
helperText: {
marginTop: 4,
paddingLeft: 12,
fontSize: 12,
color: '#555'
},
button: {
padding: standardInterval(1.5),
borderRadius: standardInterval(.5),
// marginLeft: 12,
},
})
export default globalStyles
Points worth noting.
When the content is fetched from the drawer navigation component, the animated components work just fine. This is not reliable since if the data is received when the home screen is already mounted, the components will not animate.
DrawerNavigation.tsx
import React, { useEffect } from 'react'
import { createDrawerNavigator, DrawerContentScrollView, DrawerItemList, DrawerItem, DrawerContentComponentProps, } from '#react-navigation/drawer'
import { NavigationContainer } from '#react-navigation/native'
import { useSelector, useDispatch } from 'react-redux'
import AuthNavigator from './AuthNavigator'
import MainNavigator from './MainNavigator'
import theme from '../styles/theme'
import colors from '../styles/colors'
import Profile from '../screens/profile'
import { RootState } from '../redux/types'
import { verifyToken } from '../redux/actions/authAction'
import Splash from '../screens/splash'
import { logOut } from '../redux/actions/authAction'
import { getMetadata } from '../redux/actions/metadataActions'
import { getCategories } from '../redux/actions/categoryAction'
const Drawer = createDrawerNavigator()
const DrawerNavigator = () => {
const dispatch = useDispatch()
const { hasInitiallyLoaded, authenticationToken, authenticatedUser } = useSelector((state: RootState) => state.auth)
useEffect(() => {
if (!hasInitiallyLoaded) dispatch(verifyToken(authenticationToken))
})
useEffect(() => {
dispatch(getMetadata())
dispatch(getCategories())
}, [getMetadata])
return (
<>
{
hasInitiallyLoaded
? <NavigationContainer>
<Drawer.Navigator
drawerStyle={{ backgroundColor: theme.pageBackgroundColor }}
drawerContentOptions={{ activeTintColor: colors.secondary[500] }}
drawerContent={(props: DrawerContentComponentProps) => (
<DrawerContentScrollView {...props}>
<DrawerItemList {...props} />
{
authenticatedUser
&& <DrawerItem
label='log out'
onPress={() => {dispatch(logOut([() => props.navigation.navigate('home')]))}}
/>
}
</DrawerContentScrollView>
)}
>
<Drawer.Screen name='home' component={MainNavigator} />
{
authenticatedUser
? <Drawer.Screen name='profile' component={Profile} />
: <Drawer.Screen name='login' component={AuthNavigator} />
}
</Drawer.Navigator>
</NavigationContainer>
: <Splash />
}
</>
)
}
export default DrawerNavigator
Also, the listener in Animated.event() in the Animated.ScrollView does not work
Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{
useNativeDriver: true,
listener: (event: any) => console.log(event)
}
)}
After placing the scrollY view in state, everything seemed to work as expected.
I'm trying to build a custom autogrow text input in ReactNative.
Starting height should be 48, but after it renders it sets the height to 53.71428680419922 and I don't understand why.
The code is here
import React, { Component } from 'react'
import styled from 'styled-components/native'
const Input = styled.TextInput`
font-family: Roboto;
font-size: 16;
padding-bottom: 16;
padding-top: 16;
height: ${props => props.height};
${props => props.underline
? {
paddingLeft: 0,
paddingRight: 0,
borderBottomWidth: 1,
} : {
marginTop: 18,
paddingLeft: 22,
paddingRight: 22,
borderWidth: 1,
borderStyle: 'solid',
}}
`
export default class TextField extends Component {
state = {
height: 48,
}
handleContentSize = ({ nativeEvent: { contentSize: { height } } }) => {
this.setState({ height })
}
render() {
return (
<Input
height={this.state.height}
underline={underline}
multiline={!underline}
onContentSizeChange={this.handleContentSize}
/>
)
}
}