React Native - React Navigation transitions - react-native

I'd like to use React Navigation in my new react native app but I can't find any example showing how to create custom view transitions in there. Default transitions are working fine but I'd like to be able to customize them in few places and the docs don't come very helpfull in this subject.
Anyone tried that already? Anywhere I could see a working example?
Thanks in advance.

You can find detailed version of this post on this link
I hope this is clear enough with step-by-step for how to create custom transition.
Create a Scene or Two to navigate
class SceneOne extends Component {
render() {
return (
<View>
<Text>{'Scene One'}</Text>
</View>
)
}
}
class SceneTwo extends Component {
render() {
return (
<View>
<Text>{'Scene Two'}</Text>
</View>
)
}
}
Declare your app scenes
let AppScenes = {
SceneOne: {
screen: SceneOne
},
SceneTwo: {
screen: SceneTwo
},
}
Declare custom transition
let MyTransition = (index, position) => {
const inputRange = [index - 1, index, index + 1];
const opacity = position.interpolate({
inputRange,
outputRange: [.8, 1, 1],
});
const scaleY = position.interpolate({
inputRange,
outputRange: ([0.8, 1, 1]),
});
return {
opacity,
transform: [
{scaleY}
]
};
};
Declare custom transitions configurator
let TransitionConfiguration = () => {
return {
// Define scene interpolation, eq. custom transition
screenInterpolator: (sceneProps) => {
const {position, scene} = sceneProps;
const {index} = scene;
return MyTransition(index, position);
}
}
};
Create app navigator using Stack Navigator
const AppNavigator = StackNavigator(AppScenes, {
transitionConfig: TransitionConfiguration
});
Use App Navigator in your project
class App extends Component {
return (
<View>
<AppNavigator />
</View>
)
}
Register your app in eq. index.ios.js
import { AppRegistry } from 'react-native';
AppRegistry.registerComponent('MyApp', () => App);
Update #1
As for the question on how to set transition per scene, this is how I'm doing it.
When you navigate using NavigationActions from react-navigation, you can pass through some props. In my case it looks like this
this.props.navigate({
routeName: 'SceneTwo',
params: {
transition: 'myCustomTransition'
}
})
and then inside the Configurator you can switch between these transition like this
let TransitionConfiguration = () => {
return {
// Define scene interpolation, eq. custom transition
screenInterpolator: (sceneProps) => {
const {position, scene} = sceneProps;
const {index, route} = scene
const params = route.params || {}; // <- That's new
const transition = params.transition || 'default'; // <- That's new
return {
myCustomTransition: MyCustomTransition(index, position),
default: MyTransition(index, position),
}[transition];
}
}
};

Related

Pass useAnimatedGestureHandler via forwardRef

I'm about to swap the old React Native Animated library with the new React Native Reanimated one to gain performance issues but I have encountered one problem I could not solve.
In all examples I found online, I saw that the GestureHandler, created with useAnimatedGestureHandler, is in the same component as the Animated.View. In reality that is sometimes not possible.
In my previous app, I just pass the GestureHandler object to the component via forwardRef but it seems React Native Reanimated is not able to do that. I don't know whether I have a syntax error or it is just a bug.
const App = () => {
const handlerRef = useAnimatedRef();
const y = useSharedValue(0);
handlerRef.current = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startY = y.value;
},
onActive: ({translationX, translationY}, ctx) => {
y.value = translationY;
},
onEnd: () => {},
});
const animatedStyles = useAnimatedStyle(() => ({transform: [{translateY: withSpring(y.value)}]}));
const UsingHandlerDirect = () => (
<PanGestureHandler onGestureEvent={handlerRef.current} >
<Animated.View style={[styles.blueBox, animatedStyles]} />
</PanGestureHandler>
)
const UsingHandlerForwardRef = forwardRef(({animatedStyles}, ref) => (
<PanGestureHandler onGestureEvent={ref?.handlerRef?.current}>
<Animated.View style={[styles.redBox, animatedStyles]} />
</PanGestureHandler>
));
return (
<SafeAreaView>
<View style={styles.container}>
<UsingHandlerForwardRef ref={handlerRef} animatedStyles={animatedStyles}/>
<UsingHandlerDirect />
</View>
</SafeAreaView>
);
}
I have saved the GestureHandler in a useAnimatedRef handlerRef.current = useAnimatedGestureHandler({}) to make things more representable. Then I pass the the ref directly into the PanGestureHandler of the UsingHandlerDirect component. The result is that when I drag the blue box the box will follow the handler. So this version works.
But as soon as I pass the handlerRef to the UsingHandlerForwardRef component non of the gesture events get fired. I would expect that when I drag the red box will also follow the handler but it doesn't
Has someone an idea whether it's me or it's a bug in the library?
Cheers
I have given up on the idea to pass a ref around instead, I created a hook that connects both components with each other via context.
I created a simple hook
import { useSharedValue } from 'react-native-reanimated';
const useAppState = () => {
const sharedXValue = useSharedValue(0);
return {
sharedXValue,
};
};
export default useAppState;
that holds the shared value using useSharedValue from reanimated 2
The child component uses this value in the gestureHandler like that
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = sharedXValue.value;
},
onActive: (event, ctx) => {
sharedXValue.value = ctx.startX + event.translationX;
},
onEnd: (_) => {
sharedXValue.value = withSpring(0);
},
});
and the Parent just consumes the hook value
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: -sharedXValue.value,
},
],
};
});
I have created a workable Snack which contains the 2 components - a Child with a blue box and a Parent with a red box

How to pass localization info from this.context in react component to its child consts?

I have implemented localization for React-native app according to this file as LocalizationContext.js:
import React from 'react';
import Translations, {DEFAULT_LANGUAGE} from '../constants/Translations';
import AsyncStorage from '#react-native-community/async-storage';
import * as RNLocalize from 'react-native-localize';
const APP_LANGUAGE = 'appLanguage';
export const LocalizationContext = React.createContext({
Translations,
setAppLanguage: () => {},
appLanguage: DEFAULT_LANGUAGE,
initializeAppLanguage: () => {},
});
export const LocalizationProvider = ({children}) => {
const [appLanguage, setAppLanguage] = React.useState(DEFAULT_LANGUAGE);
const setLanguage = language => {
Translations.setLanguage(language);
setAppLanguage(language);
AsyncStorage.setItem(APP_LANGUAGE, language);
};
const initializeAppLanguage = async () => {
const currentLanguage = await AsyncStorage.getItem(APP_LANGUAGE);
if (!currentLanguage) {
let localeCode = DEFAULT_LANGUAGE;
const supportedLocaleCodes = Translations.getAvailableLanguages();
const phoneLocaleCodes = RNLocalize.getLocales().map(
locale => locale.languageCode,
);
phoneLocaleCodes.some(code => {
if (supportedLocaleCodes.includes(code)) {
localeCode = code;
return true;
}
});
setLanguage(localeCode);
} else {
setLanguage(currentLanguage);
}
};
return (
<LocalizationContext.Provider
value={{
Translations,
setAppLanguage: setLanguage,
appLanguage,
initializeAppLanguage,
}}>
{children}
</LocalizationContext.Provider>
);
};
and it works fine in different screens but App.js file which is something like:
const MainTabs = createBottomTabNavigator(
{
Profile: {
screen: ProfileStack,
navigationOptions: {
// tabBarLabel: Translations.PROFILE_TAB,
},
},
HomePage: {
screen: HomeStack,
navigationOptions: {
tabBarLabel: Translations.HOME_TAB,
},
},
},
{
initialRouteName: 'HomePage'
},
},
);
export default class App extends Component {
// static contextType = LocalizationContext;
render() {
// const Translations = this.context.Translations;
// console.log(Translations.PROFILE_TAB);
return (
<LocalizationProvider>
<SafeAreaView style={{flex: 1}}>
<AppNavigator />
</SafeAreaView>
</LocalizationProvider>
);
}
}
I do access Translation in App component as you can find them in commented lines, but how can I pass related information to some const like tab titles? Translations.PROFILE_TAB is undefined.
I ended up changing this into a service.
Also I use redux and pass the store in to get user preferences and set them:
import * as RNLocalize from 'react-native-localize';
import { saveUserOptions } from '../actions/login';
import LocalizedStrings from 'react-native-localization';
export const DEFAULT_LANGUAGE = 'ar';
let _reduxStore;
function setStore(store){
_reduxStore = store
}
const _translations = {
en: {
WELCOME_TITLE: 'Welcome!',
STEP1: 'Step One',
SEE_CHANGES: 'See Your Changes',
CHANGE_LANGUAGE: 'Change Language',
LANGUAGE_SETTINGS: 'Change Language',
BACK: 'Back'
},
ar: {
WELCOME_TITLE: 'صباحك فُل!',
...
}
};
let translation = new LocalizedStrings(_translations);
const setAppLanguage = language => {
translation.setLanguage(language);
_reduxStore.dispatch(saveUserOptions('user_language',language))
};
const initializeAppLanguage = async () => {
const currentLanguage = _reduxStore.getState().login.user_language
if (!currentLanguage) {
let localeCode = DEFAULT_LANGUAGE;
const supportedLocaleCodes = translation.getAvailableLanguages();
const phoneLocaleCodes = RNLocalize.getLocales().map(
locale => locale.languageCode,
);
phoneLocaleCodes.some(code => {
if (supportedLocaleCodes.includes(code)) {
localeCode = code;
return true;
}
});
setAppLanguage(localeCode);
} else {
setAppLanguage(currentLanguage);
}
};
export default {
setStore,
translation,
setAppLanguage,
initializeAppLanguage
}
I need to first setup things in my top main component:
LocalizationService.setStore(store)
...
// add the below to componentDidMount by which time persisted stores are populated usually
LocalizationService.initializeAppLanguage()
where I need to get strings I do:
import LocalizationService from '../../utils/LocalizationService';
....
static navigationOptions = () => {
// return here was needed otherwise the translation didn't work
return {
title: LocalizationService.translation.WELCOME_TITLE,
}
}
EDIT
to force update of title you will need to set a navigation param:
this.props.navigation.setParams({ otherParam: 'Updated!' })
** Further Edit **
The props navigation change hack only works for the current screen, if we want to refresh all screens we need to setParams for all screens. This could possibly be done using a listener on each screen or tying the screen navigationOptions to the redux state.
I'm using NavigationService (see https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html) so I've created the following function to run through my screens and setparam on all of them and force an update on all:
function setParamsForAllScreens(param, value) {
const updateAllScreens = (navState) => {
// Does this route have more routes in it?
if (navState.routes) {
navState.routes.forEach((route) => {
updateAllScreens(route)
})
}
// Does this route have a name?
else if (navState.routeName) {
// Does it end in Screen? This is a convention we are using
if (navState.routeName.endsWith('Screen')) {
// this is likely a leaf
const action = NavigationActions.setParams({
params: {
[param]: value
},
key: navState.key,
})
_navigator.dispatch(action)
}
}
}
if (_navigator && _navigator.state)
updateAllScreens(_navigator.state.nav)
}

How to update the header while the component is still rendered using React Navigation?

I'm writing a React Native app and I'm using React Navigation (V2) with it. I want to update the navigationOptions and add a new button, after my component has updated. Here is the code with which I tried it:
static navigationOptions = ({ navigation }) => {
const options = {
headerTitle: SCREEN_TEXT_MENU_HEADER,
headerStyle: {
borderBottomWidth: 0,
marginBottom: -5
}
};
if (navigation.getParam("drawer", true)) {
options["headerLeft"] = (
<HeaderIconButton
onClick={() => {
navigation.openDrawer();
}}
icon={require("../../assets/icons/burgerMenu.png")}
/>
);
}
if (navigation.getParam("renderBillButton", false)) {
options["headerRight"] = (
<HeaderIconButton
onClick={() => {
navigation.navigate("BillScreen");
}}
type="primary"
icon={require("../../assets/icons/euro.png")}
/>
);
}
return options;
};
componentDidUpdate = prevProps => {
const { navigation, orders } = this.props;
if (prevProps.orders.length !== orders.length) {
navigation.setParams({
renderBillButton: orders.length > 0
});
}
};
The problem with this approach is, that the navigationOptions do not get reset after componentDidUpdate(). How can I dynamically adjust the header with React Navigation?
You can use this.props.navigation.setParams() function to update the navigation state params.
Reference: https://reactnavigation.org/docs/en/headers.html#updating-navigationoptions-with-setparams
Okay here is what went wrong: I also had to call the same code within componentDidMount(), otherwise it would not affect the page upon loading. So in addition to the code of my question I added:
componentDidMount = () => {
const { navigation, order } = this.props;
navigation.setParams({
renderBillButton: orders.length > 0
});
}

React-native-navigation Change state from another tabnavigator

I'm using react-navigation / TabNavigator, is there a way to change the state of a tab from another tab without using Redux or mobx?
Yes you can. It is a little complicated, a little hacky and probably has some side-effects but in theory you can do it. I have created a working example snack here.
In react-navigation you can set parameters for other screens using route's key.
When dispatching SetParams, the router will produce a new state that
has changed the params of a particular route, as identified by the key
params - object - required - New params to be merged into existing route params
key - string - required - Route key that should get the new params
Example
import { NavigationActions } from 'react-navigation'
const setParamsAction = NavigationActions.setParams({
params: { title: 'Hello' },
key: 'screen-123',
})
this.props.navigation.dispatch(setParamsAction)
For this to work you need to know key prop for the screen you want to pass parameter. Now this is the place we get messy. We can combine onNavigationStateChange and screenProps props to get the current stacks keys and then pass them as a property to the screen we are currently in.
Important Note: Because onNavigationStateChange is not fired when the app first launched this.state.keys will be an empty array. Because of that you need to do a initial navigate action.
Example
class App extends Component {
constructor(props) {
super(props);
this.state = {
keys: []
};
}
onNavigationChange = (prevState, currentState) => {
this.setState({
keys: currentState.routes
});
}
render() {
return(
<Navigation
onNavigationStateChange={this.onNavigationChange}
screenProps={{keys: this.state.keys}}
/>
);
}
}
And now we can use keys prop to get the key of the screen we need and then we can pass the required parameter.
class Tab1 extends Component {
onTextPress = () => {
if(this.props.screenProps.keys.length > 0) {
const Tab2Key = this.props.screenProps.keys.find((key) => (key.routeName === 'Tab2')).key;
const setParamsAction = NavigationActions.setParams({
params: { title: 'Some Value From Tab1' },
key: Tab2Key,
});
this.props.navigation.dispatch(setParamsAction);
}
}
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph} onPress={this.onTextPress}>{`I'm Tab1 Component`}</Text>
</View>
)
}
}
class Tab2 extends Component {
render() {
const { params } = this.props.navigation.state;
return(
<View style={styles.container}>
<Text style={styles.paragraph}>{`I'm Tab2 Component`}</Text>
<Text style={styles.paragraph}>{ params ? params.title : 'no-params-yet'}</Text>
</View>
)
}
}
Now that you can get new parameter from the navigation, you can use it as is in your screen or you can update your state in componentWillReceiveProps.
componentWillReceiveProps(nextProps) {
const { params } = nextProps.navigation.state;
if(this.props.navigation.state.params && params && this.props.navigation.state.params.title !== params.title) {
this.setState({ myStateTitle: params.title});
}
}
UPDATE
Now react-navigation supports listeners which you can use to detect focus or blur state of screen.
addListener - Subscribe to updates to navigation lifecycle
React Navigation emits events to screen components that subscribe to
them:
willBlur - the screen will be unfocused
willFocus - the screen will focus
didFocus - the screen focused (if there was a transition, the transition completed)
didBlur - the screen unfocused (if there was a transition, the transition completed)
Example from the docs
const didBlurSubscription = this.props.navigation.addListener(
'didBlur',
payload => {
console.debug('didBlur', payload);
}
);
// Remove the listener when you are done
didBlurSubscription.remove();
// Payload
{
action: { type: 'Navigation/COMPLETE_TRANSITION', key: 'StackRouterRoot' },
context: 'id-1518521010538-2:Navigation/COMPLETE_TRANSITION_Root',
lastState: undefined,
state: undefined,
type: 'didBlur',
};
If i understand what you want Its how i figure out to refresh prevous navigation screen. In my example I refresh images witch i took captured from camera:
Screen A
onPressCamera() {
const { navigate } = this.props.navigation;
navigate('CameraScreen', {
refreshImages: function (data) {
this.setState({images: this.state.images.concat(data)});
}.bind(this),
});
}
Screen B
takePicture() {
const {params = {}} = this.props.navigation.state;
this.camera.capture()
.then((data) => {
params.refreshImages([data]);
})
.catch(err => console.error(err));
}

react-navigation from react-community is triggering transition with delay

Below is the c/p of the code I'm using.
I'm using react-community/react-navigation and redux store to dispatch actions. One of actions is navigate from NavigationActions which is part of the react-navigations package.
As you can see, I've defined a custom transition (simple fade) from one
screen to another.
The problem is that transition starts with a little bit of delay. Even if I specify transitionSpec in the TransitionConfiguration with Animated.timing and duration of 100ms it makes no difference.
Also, when navigating back using back action from NavigationActions it needs about half a second to a second for the scene to become responsive.
Please see attached gif.
Note #1 - same is happening on a real iPhone6,7.
Note #2 - same think if I remove transitionConfig: TransitionConfiguration
It works, but it doesn't feel anything that you can experience in native apps.
// Transition definition
const FadeTransition = (index, position) => {
const inputRange = [index - 1, index, index + 1];
const opacity = position.interpolate({
inputRange,
outputRange: [0, 1, 1],
});
return {
opacity
};
};
// Transition configurator
const TransitionConfiguration = () => {
return {
screenInterpolator: (sceneProps) => {
const {position, scene} = sceneProps;
const {index, route} = scene;
return FadeTransition(index, position);
}
}
};
// Active scenes
const appScenes = {
Demo: {
screen: Demo
},
Foo: {
screen: Foo
}
};
// Basic stack navigator
import { StackNavigator } from 'react-navigation';
const AppNavigator = StackNavigator(appScenes, {
headerMode: 'none',
transitionConfig: TransitionConfiguration
});
import { NavigationActions } from 'react-navigation';
this.props.navigate({
routeName: 'Foo',
params: {
transition: 'fade'
}
})
]1
I would appreciate any advice given.
const MyTransitionSpec = ({
duration: 200,
});
// Transition configurator
const TransitionConfiguration = () => {
return {
transitionSpec: MyTransitionSpec,
screenInterpolator: (sceneProps) => {
const {position, scene} = sceneProps;
const {index, route} = scene;
return FadeTransition(index, position);
}
}
};