I've created a custom text input component for my react native app. It has no issues in the android build. But In IOS, the custom component draws a border around its view.
This is how it looks in ios. You can see a border around it.
I want it took to look like this in IOS without a square border
This is my custom component (MyInput.js)
import React, {useState, useRef, useEffect} from 'react';
import {
Animated,
Text,
ActivityIndicator,
TouchableOpacity,
} from 'react-native';
const {View, TextInput, StyleSheet} = require('react-native');
const MyInput = (props) => {
const [hide, makeHide] = useState(false);
const [phAanimatedValue] = useState(new Animated.Value(0));
const [labelAanimatedValue] = useState(new Animated.Value(0));
const [overflow, setOverflow] = useState('hidden');
const movePlaceHolderLeft = phAanimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, -10],
});
const moveLabelRight = labelAanimatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-10, 0],
});
const animatePlaceHolder = (v, hide) => {
Animated.timing(phAanimatedValue, {
toValue: v,
duration: 50,
useNativeDriver: true,
}).start(() => {
makeHide(hide);
animateLabel(v);
});
};
const animateLabel = (v) => {
setOverflow('visible');
Animated.timing(labelAanimatedValue, {
toValue: v,
duration: 50,
useNativeDriver: true,
}).start();
};
return (
<View
style={[
styles.input,
{
overflow: overflow,
borderColor:props.error?"#ff0033":"#e5deda",
},
]}>
<TextInput
onFocus={() => {
animatePlaceHolder(1, true);
}}
ref={props.cref}
value={props.value}
onChangeText={(v) => props.onChangeText(v)}
style={{paddingStart: 25}}
onBlur={() => {
if (props.value.length > 0) {
makeHide(true);
} else {
animatePlaceHolder(0, false);
}
}}
/>
{!hide ? (
<Animated.Text
onPress={() => props.cref.current.focus()}
style={{
position: 'absolute',
transform: [{translateX: movePlaceHolderLeft}],
height: '100%',
fontFamily: 'Muli-Regular',
width: '100%',
marginStart: 20,
paddingVertical:15,
color: props.error ? '#ff0033' : '#a6a6a6',
textAlignVertical: 'center',
}}>
{props.label}
</Animated.Text>
) : (
<Animated.Text
onPress={() => props.cref.current.focus()}
style={{
position: 'absolute',
transform: [{translateX: moveLabelRight}],
marginTop: -10,
zIndex:1000,
backgroundColor: 'white',
color:props.error?'red':'#e5deda',
marginStart: 20,
fontFamily: 'Muli-Regular',
paddingHorizontal: 5,
textAlignVertical: 'top',
}}>
{props.label}
</Animated.Text>
)}
</View>
);
};
const styles = StyleSheet.create({
input: {
borderWidth: 2,
borderColor: '#e5deda',
backgroundColor: 'white',
fontSize: 14,
color: '#353839',
paddingVertical:15,
marginHorizontal: 20,
lineHeight: 18,
fontFamily: 'Muli-Regular',
fontWeight: '600',
borderRadius: 100,
},
});
export default MyInput;
This is How I Render:
<MyInput
value={value}
onChangeText={(v)=>
{
setValue(v)
setError(true)
}}
cref={postalRef}
error={error}
label="Postal Code"
/>
Is there any solution for this or is this a bug?
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)
So I have this code here. I don't know if it's cause of how I structured it, but the state changes using the 'set' function doesn't change the state till after I change the entry again. So for example, I'll put in text then I push "post" which should update the state, when console.warn prints it doesn't print anything. Then I change the text again and push post and console.warn will output what was there before my most recent change.
import React, {useState, createRef} from 'react';
import LinearGradient from 'react-native-linear-gradient';
import {
StyleSheet,
TextInput,
View,
Text,
ScrollView,
Keyboard,
Button,
TouchableOpacity,
KeyboardAvoidingView,
} from 'react-native';
import DateTimePickerModal from 'react-native-modal-datetime-picker';
import Loader from '../components/Loader';
const GoalRegistrationScreen = ({navigation}) => {
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [goalText, setGoal] = useState('');
const [durationText, setDuration] = useState('');
const [descriptionText, setDescription] = useState('');
const [loading, setLoading] = useState(false);
const [errortext, setErrortext] = useState('');
const durationInputRef = createRef();
const descriptionInputRef = createRef();
function showDatePicker() {
setDatePickerVisibility(true);
}
function hideDatePicker() {
setDatePickerVisibility(false);
}
function handleConfirm(date) {
hideDatePicker();
console.warn(date);
}
function onButtonClick() {
// console.warn(goalText)
console.warn(descriptionText);
}
React.useLayoutEffect(() => {
navigation.setOptions({
title: 'Create Goal',
headerTitleAlign: 'center',
headerRight: () => (
<TouchableOpacity
style={styles.addButtonStyle}
activeOpacity={0.5}
onPress={onButtonClick}>
<LinearGradient
colors={['#FBE049', '#4964FB']}
style={styles.linearGradient}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}>
<Text style={styles.buttonTextStyle}>Post</Text>
</LinearGradient>
</TouchableOpacity>
),
});
}, [navigation]);
return (
<View style={styles.mainBody}>
<Loader loading={loading} />
<ScrollView keyboardShouldPersistTaps="handled">
<View>
<KeyboardAvoidingView enabled>
<View style={styles.SectionStyle}>
<TextInput
style={styles.inputStyle}
onChangeText={goal => setGoal(goal)}
placeholder="What do I want to accomplish?"
placeholderTextColor="#8b9cb5"
autoCapitalize="none"
returnKeyType="next"
onSubmitEditing={() =>
durationInputRef.current && durationInputRef.current.focus()
}
underlineColorAndroid="#f000"
blurOnSubmit={false}
/>
</View>
<View style={styles.SectionStyle}>
<Button title="Show Date Picker" onPress={showDatePicker} />
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="date"
onConfirm={handleConfirm}
onCancel={hideDatePicker}
minimumDate={new Date()}
/>
{/* <TextInput
style={styles.inputStyle}
onChangeText={duration => setDuration(duration)}
placeholder="Duration of Goal"
placeholderTextColor="#8b9cb5"
keyboardType="default"
ref={durationInputRef}
onSubmitEditing={() =>
descriptionInputRef.current &&
descriptionInputRef.current.focus()
}
blurOnSubmit={false}
underlineColorAndroid="#f000"
returnKeyType="next"
/> */}
</View>
<View style={styles.SectionStyle}>
<TextInput
style={styles.accomplishmentTextStyle}
onChangeText={description => setDescription(description)}
placeholder="How can I accomplish this goal?"
placeholderTextColor="#8b9cb5"
keyboardType="default"
ref={descriptionInputRef}
onSubmitEditing={Keyboard.dismiss}
blurOnSubmit={false}
underlineColorAndroid="#f000"
returnKeyType="next"
/>
</View>
{errortext != '' ? (
<Text style={styles.errorTextStyle}>{errortext}</Text>
) : null}
</KeyboardAvoidingView>
</View>
</ScrollView>
</View>
);
};
export default GoalRegistrationScreen;
const styles = StyleSheet.create({
mainBody: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#FFFFFF',
alignContent: 'center',
},
SectionStyle: {
flexDirection: 'row',
marginLeft: 35,
marginRight: 35,
margin: 10,
},
buttonTextStyle: {
color: '#000000',
paddingVertical: 10,
fontSize: 16,
paddingLeft: 15,
paddingRight: 15,
borderRadius: 32,
borderColor: '#000000',
},
inputStyle: {
flex: 1,
color: 'black',
paddingLeft: 15,
paddingRight: 15,
borderWidth: 1,
borderRadius: 30,
borderColor: '#000000',
},
accomplishmentTextStyle: {
flex: 1,
color: 'black',
paddingLeft: 15,
paddingRight: 15,
borderWidth: 1,
borderRadius: 30,
borderColor: '#000000',
height: 150,
textAlignVertical: 'top',
},
addButtonStyle: {
paddingRight: 20,
},
linearGradient: {
borderRadius: 32,
},
errorTextStyle: {
color: 'red',
textAlign: 'center',
fontSize: 14,
},
});
Here's an example I had 'testdsdd' in the description and updated to 'test', the onChangeText function should automatically have changed it to 'test', but it didn't. This happens for all states from goalText, descriptionText to setting date.
Try to change the TextInput onChangeText prop to just onChange like this:
onChangeText={ handleInputChange }
and then handle the new input in the handleInputChange:
const handleInputChange = useCallback((ev) => {
const input = ev.nativeEvent.text;
// you can also do error checking here.
setDescription(input);
}, [formatMessage]);
I always suggest using a pattern like this because it allows you to check for errors or call other methods (like auto-search).
In fact, I suggest using the handleInputChange to check for errors and only set the new state for the text-input on an onEndEditing.
Android app crashes when I open build app. It's happens only when I build app react native async storage. On iOS & Android emulators it's working stability. Help me please!
At first I thought that the problem was that I did not use JSON, but it turned out that I did not. JOHN tried, but nothing came of it. Maybe I am not saving correctly in Async Storage?
import React, { useState, useEffect } from 'react'
import { View, TextInput, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'
import Icon from 'react-native-vector-icons/Octicons'
import BackNavbar from '../components/BackNavbar'
import { colors } from '../theme/theme'
import AsyncStorage from '#react-native-community/async-storage'
import GoalCard from '../components/GoalCard'
export default GoalScreen = ({ navigation }) => {
const STORAGE_KEY = '#data'
function goBack() {
navigation.goBack()
}
const [ goal, setGoal ] = useState([])
const [ goalScore, setGoalScore ] = useState()
const [ goalName, setGoalName ] = useState('')
const addGoal = () => {
setGoal([
... goal,
{
id: Date.now(),
text: goalName,
score: goalScore,
}
])
setGoalName('')
setGoalScore('')
}
const saveData = async (dataSave) => {
try {
await AsyncStorage.setItem(STORAGE_KEY, dataSave)
} catch (e) {
alert('Failed to save the data to the storage')
}
}
const loadData = async () => {
try {
const json = await AsyncStorage.getItem(STORAGE_KEY) || JSON.stringify([])
const loadData = JSON.parse(json)
if (loadData != null) {
setGoal(loadData)
alert(loadData.map())
}
} catch (e) {
alert('Failed to load the data to the storage')
}
}
useEffect(() => {
loadData()
}, [])
useEffect(() => {
saveData(JSON.stringify(goal))
}, [goal])
return (
<View style={styles.container}>
<BackNavbar title='Goals' back={goBack} />
<ScrollView>
{ goal.map( item => <GoalCard key={item.id} {... item} /> ) }
</ScrollView>
<View style={styles.inputView}>
<TextInput
keyboardType = "number-pad"
style={styles.inputTextScore}
value={goalScore}
onChangeText={score => setGoalScore(score)}
/>
<TextInput
style={styles.inputText}
value={goalName}
onChangeText={name => setGoalName(name)}
/>
<TouchableOpacity
style={styles.inputButton}
onPress={addGoal}
>
<Icon name="plus" size={26} color={colors.LIGHT_COLOR} />
</TouchableOpacity>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'flex-start',
},
inputView: {
marginTop: 15,
flexDirection: 'row',
width: '80%',
marginBottom: 15,
},
inputText: {
width: '62%',
height: 50,
paddingHorizontal: '5%',
right: 5,
borderRadius: 15,
backgroundColor: '#f2f2f2',
borderColor: colors.BORDER_COLOR,
color: colors.SECOND_COLOR,
borderWidth: 1,
fontSize: 20,
elevation: 3,
fontSize: 16,
},
inputTextScore: {
width: '20%',
height: 50,
marginRight: 10,
paddingHorizontal: '5%',
right: 5,
borderRadius: 15,
backgroundColor: '#f2f2f2',
borderColor: colors.BORDER_COLOR,
borderWidth: 1,
fontSize: 20,
elevation: 3,
color: colors.SECOND_COLOR,
fontWeight: '500',
},
inputButton: {
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 15,
paddingVertical: 10,
borderRadius: 50,
backgroundColor: colors.MAIN_COLOR,
left: 5,
}
})
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.
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]}
>