How to dynamically change React Native transform with state? - react-native

I'm building a custom view that will rotate its contents based on device orientation. This app has orientation locked to portrait and I just want to rotate a single view. It fetches the current device orientation, updates the state, then renders the new component with the updated style={{transform: [{rotate: 'xxxdeg'}]}}.
I'm using react-native-orientation-locker to detect orientation changes.
The view renders correctly rotated on the first render. For example, if the screen loads while the device is rotated, it will render the view rotated. But upon changing the orientation of the device or simulator, the view does not rotate. It stays locked at the rotate value it was initialized at.
It seems like updates to the transform rotate value do not change the rotation. I've verified that new rotate values are present during the render. I've verified that orientation changes are correctly updating the state. But the view is never rotated in the UI when orientation changes. It is as if React Native isn't picking up on changes to the rotate value during a render.
I would expect that updates to the rotate value would rotate the View accordingly but that does not seem to be the case. Is there another way to accomplish this or do I have a bug in this code?
Edit: Is it required for rotate to be an Animated value?
import React, {useState, useEffect} from 'react';
import {View} from 'react-native';
import Orientation from 'react-native-orientation-locker';
const RotateView = props => {
const getRotation = newOrientation => {
switch (newOrientation) {
case 'LANDSCAPE-LEFT':
return '90deg';
case 'LANDSCAPE-RIGHT':
return '-90deg';
default:
return '0deg';
}
};
const [orientation, setOrientation] = useState(
// set orientation to the initial device orientation
Orientation.getInitialOrientation(),
);
const [rotate, setRotate] = useState(
// set rotation to the initial rotation value (xxdeg)
getRotation(Orientation.getInitialOrientation()),
);
useEffect(() => {
// Set up listeners for device orientation changes
Orientation.addDeviceOrientationListener(setOrientation);
return () => Orientation.removeDeviceOrientationListener(setOrientation);
}, []);
useEffect(() => {
// when orientation changes, update the rotation
setRotate(getRotation(orientation));
}, [orientation]);
// render the view with the current rotation value
return (
<View style={{transform: [{rotate}]}}>
{props.children}
</View>
);
};
export default RotateView;

I had this same problem, and solved it by using an Animated.View from react-native-reanimated. (Animated.View from the standard react-native package might also work, but I haven't checked). I didn't need to use an Animated value, I still just used the actual value from the state, and it worked.

If you use Animated.Value + Animated.View directly from react native you'll be fine.
Had the same issue and solved it using an Animated.Value class field (in your case I guess you'd use a useState for this one since functional + a useEffect to set the value of the Animated.Value upon changes in props.rotation), and then pass that into the Animated.View as the transform = [{ rotate: animatedRotationValue }]
Here's the class component form of this as a snippet:
interface Props {
rotation: number;
}
class SomethingThatNeedsRotation extends React.PureComponent<Props> {
rotation = new Animated.Value(0);
rotationValue = this.rotation.interpolate({
inputRange: [0, 2 * Math.PI],
outputRange: ['0deg', '360deg'],
});
render() {
this.rotation.setValue(this.props.rotation);
const transform = [{ rotate: this.rotationValue }];
return (
<Animated.View style={{ transform }} />
);
}
}
Note that in my example I also have the interpolation there since my input is in radians and I wanted it to be in degrees.

Here is my completed component that handles rotation. It will rotate its children based on device orientation while the app is locked to portrait. I'm sure this could be cleaned up some but it works for my purposes.
import React, {useState, useEffect, useRef} from 'react';
import {Animated, Easing, View, StyleSheet} from 'react-native';
import {Orientation} from '../utility/constants';
import OrientationManager from '../utility/orientation';
const OrientedView = (props) => {
const getRotation = useRef((newOrientation) => {
switch (newOrientation) {
case Orientation.LANDSCAPE_LEFT:
return 90;
case Orientation.LANDSCAPE_RIGHT:
return -90;
default:
return 0;
}
});
const {duration = 100, style} = props;
const initialized = useRef(false);
const [orientation, setOrientation] = useState();
const [rotate, setRotate] = useState();
const [containerStyle, setContainerStyle] = useState(styles.containerStyle);
// Animation kept as a ref
const rotationAnim = useRef();
// listen for orientation changes and update state
useEffect(() => {
OrientationManager.getDeviceOrientation((initialOrientation) => {
const initialRotation = getRotation.current(initialOrientation);
// default the rotation based on initial orientation
setRotate(initialRotation);
rotationAnim.current = new Animated.Value(initialRotation);
setContainerStyle([
styles.containerStyle,
{
transform: [{rotate: `${initialRotation}deg`}],
},
]);
initialized.current = true;
// set orientation and trigger the first render
setOrientation(initialOrientation);
});
OrientationManager.addDeviceOrientationListener(setOrientation);
return () =>
OrientationManager.removeDeviceOrientationListener(setOrientation);
}, []);
useEffect(() => {
if (initialized.current === true) {
const rotation = getRotation.current(orientation);
setRotate(
rotationAnim.current.interpolate({
inputRange: [-90, 0, 90],
outputRange: ['-90deg', '0deg', '90deg'],
}),
);
Animated.timing(rotationAnim.current, {
toValue: rotation,
duration: duration,
easing: Easing.ease,
useNativeDriver: true,
}).start();
}
}, [duration, orientation]);
// FIXME: This is causing unnessary animation outside of the oriented view. Disabling removes the scale animation.
// useEffect(() => {
// applyLayoutAnimation.current();
// }, [orientation]);
useEffect(() => {
if (initialized.current === true) {
setContainerStyle([
styles.containerStyle,
{
transform: [{rotate}],
},
]);
}
}, [rotate]);
if (initialized.current === false) {
return <View style={[containerStyle, style]} />;
}
return (
<Animated.View style={[containerStyle, style]}>
{props.children}
</Animated.View>
);
};
const styles = StyleSheet.create({
containerStyle: {flex: 0, justifyContent: 'center', alignItems: 'center'},
});
export default OrientedView;

This is a bug, as the rotation is supposed to change when the value of rotate updates. A workaround is to set the View's key attribute to the rotate value as well.
For example:
return (
<View
key={rotate} // <~~~ fix!
style={{transform: [{rotate}]}}
>
{props.children}
</View>
)
I found this solution here.

Related

Warning: React has detected a change in the order of Hooks

I have run into this error in my code, and don't really know how to solve it, can anyone help me?
I get the following error message:
ERROR Warning: React has detected a change in the order of Hooks called by ScreenA. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
import React, { useCallback, useEffect, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useNavigation } from '#react-navigation/native';
import { DancingScript_400Regular } from "#expo-google-fonts/dancing-script";
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Keep the splash screen visible while we fetch resources
await SplashScreen.preventAutoHideAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({ DancingScript_400Regular });
// Artificially delay for two seconds to simulate a slow loading
// experience. Please remove this if you copy and paste the code!
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
const navigation = useNavigation();
const onPressHandler = () => {
// navigation.navigate('Screen_B', { itemName: 'Item from Screen A', itemID: 12 });
}
return (
<View style={styles.body} onLayout={onLayoutRootView}>
<Text style={styles.text}>
Screen A
</Text>
<Pressable
onPress={onPressHandler}
style={({ pressed }) => ({ backgroundColor: pressed ? '#ddd' : '#0f0' })}
>
<Text style={styles.text}>
Go To Screen B
</Text>
</Pressable>
<Text style={styles.text}>{route.params?.Message}</Text>
</View>
)
}
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 40,
margin: 10,
fontFamily: 'DancingScript_400Regular'
}
})
I have read the rules of hooks: https://reactjs.org/docs/hooks-rules.html
The output is correct, but i want to fix this error before i add more additions to the app
You need to move useNavigation use before early returns.
Instead, always use Hooks at the top level of your React function, before any early returns.
The key is you need to call all the hooks in the exact same order on every component lifecycle update, which means you can't use hooks with conditional operators or loop statements such as:
if (customValue) useHook();
// or
for (let i = 0; i< customValue; i++) useHook();
// or
if (customValue) return;
useHook();
So moving const navigation = useNavigation(); before if (!appIsReady) {return null;}, should solve your problem:
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
const navigation = useNavigation();
// ...
}

Get the last scroll from a FlatList and redirect

I'm creating an Onboarding screen. I don't want to add buttons to this onboarding screen, the only way to go to the next page is by swiping. Although I need to have a way to get when the user is at the last screen, and when he swipes right, I will be redirecting him to the login screen.
This is the last screen on the app:
When the user swipes right from this screen, I want to make a redirect, like a function or a onPress.
This is the onboarding code:
import React, { useState, useRef } from 'react';
import {
Container,
FlatListContainer
} from './styles';
import {
FlatList,
Animated,
TouchableOpacity
} from 'react-native'
import OnboardingData from '../../utils/onboarding';
import { OnboardingItem } from '../../components/OnboardingItem';
import { Paginator } from '../../components/Paginator';
export function Onboarding(){
const [currentIndex, setCurrentIndex] = useState(0);
const scrollX = useRef(new Animated.Value(0)).current;
const scrollY = useRef(new Animated.Value(0)).current;
const onboardingDataRef = useRef(null);
const viewableItemsChanged = useRef(({ viewableItems }: any) => {
setCurrentIndex(viewableItems[0].index);
}).current;
const viewConfig = useRef({ viewAreaCoveragePercentThreshold: 50 }).current;
return (
<Container>
<FlatListContainer>
<FlatList
data={OnboardingData}
renderItem={({ item }) => <OnboardingItem image={item.image} title={item.title} description={item.description}/>}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled={true}
bounces={false}
keyExtractor={(item) => String(item.id)}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } }}], {
useNativeDriver: false
})}
scrollEventThrottle={32}
onViewableItemsChanged={viewableItemsChanged}
viewabilityConfig={viewConfig}
ref={onboardingDataRef}
/>
</FlatListContainer>
<Paginator data={OnboardingData} scrollX={scrollX} scrollY={scrollY} currentIndex={currentIndex}/>
</Container>
);
}
I thought about it, and the old method wouldnt work, as there isnt a viewableItem beyond the final page.
You could use the FlatList onScroll function to listen for swipes. When at the final page and a scroll event occurs, verify that it wasnt an attempt to go to the previous page and then navigate. e.g.
// store previous scroll values
const prevScroll = useRef({});
// scrollX, scrollY already contains the data
useEffect(()=>{
let isLastIndex = currentIndex == onBoardingData.length - 1;
let isScrollingToNextPage = scrollX > prevScroll.current.x
if( isLastIndex && isScrollingToNextPage ){
props.navigation.navigate("Login");
return
}
// store offsets
prevScroll.current = { x:scrollX, y:scrollY}
},[scrollX, scrollY])
If you would like for the old method to work you could just add an additional page (could be an empty view or a view with a small message)
--------OLD--------
Im assuming that OnBordingData is an array since you're passing it to the Flatlist data prop. If so, then couldnt you add a conditional to your viewableItemsChanged so that it goes to the login screen when at index > OnBoardingData.length - 1? e.g
const viewableItemsChanged = useRef(({ viewableItems }: any) => {
const index = viewableItems[0].index;
if(index >= OnBoardinData.length -1){
// be sure to add props as a parameter to OnBoarding function
props.navigation.navigate("Login");
return
}
setCurrentIndex(index);
}).current;
--------OLD--------

Determining Camera Texture

Been stuck a while trying to figure an issue out for android devices. The sample code from the tensorFlow.js library says that the resolution of the camera has to be determined empirically. With iphone it's been relatively consistent across versions (only changing for version 6 and above), but android phones are so varied I need to figure out a way to automatically determine it. When the resolution is incorrect, the waypoints used to refer to different parts of the body in the body scan app are in the wrong locations (Ex: Head is near the shoulder). Does anyone have tips to find the resolution? A lot of the resources just refer to them as magic numbers. I've also linked someone with a similar issue who has had no response.
import { Camera } from 'expo-camera';
import { cameraWithTensors } from '#tensorflow/tfjs-react-native';
const TensorCamera = cameraWithTensors(Camera);
class MyComponent {
handleCameraStream(images, updatePreview, gl) {
const loop = async () => {
const nextImageTensor = images.next().value
//
// do something with tensor here
//
// if autorender is false you need the following two lines.
// updatePreview();
// gl.endFrameEXP();
requestAnimation(loop);
}
loop();
}
render() {
// Currently expo does not support automatically determining the
// resolution of the camera texture used. So it must be determined
// empirically for the supported devices and preview size.
let textureDims;
if (Platform.OS === 'ios') {
textureDims = {
height: 1920,
width: 1080,
};
} else {
textureDims = {
height: 1200,
width: 1600,
};
}
return <View>
<TensorCamera
// Standard Camera props
style={styles.camera}
type={Camera.Constants.Type.front}
// Tensor related props
cameraTextureHeight={textureDims.height}
cameraTextureWidth={textureDims.width}
resizeHeight={200}
resizeWidth={152}
resizeDepth={3}
onReady={this.handleCameraStream}
autorender={true}
/>
</View>
}
}
The textureDims also varies depending on whether the user is in potrait or landscape so the current hard-coded value for iOS devices wouldn't work as well if the user were to change the orientation of the device.
What you could use instead is useWindowDimensions but this only works with functional components not with class components.
import React, { useState } from "react";
import { View, useWindowDimensions } from "react-native";
const [textureDims, setTextureDims] = useState();
const windowWidth = useWindowDimensions().width;
const windowHeight = useWindowDimensions().height;
setTextureDims({ height: windowHeight, width: windowWidth });
return (
<View>
<TensorCamera
// Standard Camera props
style={styles.camera}
type={Camera.Constants.Type.front}
// Tensor related props
cameraTextureHeight={textureDims.height}
cameraTextureWidth={textureDims.width}
resizeHeight={200}
resizeWidth={152}
resizeDepth={3}
onReady={handleCameraStream}
autorender={true}
/>
</View>
);

React native: Avoid screen going light before blur effect

I have a background image, and I want to blur it out more and more.
So far the blurring works fine:
export default class Example extends Component {
state = { blurState: 1 };
componentWillMount = () => {
this.getRandomBlurIntervals();
}
getRandomBlurIntervals = () => {
var blurState = this.state.blurState;
setInterval(() => {
blurState = blurState + Math.random();
this.setState({blurState});
}, 2000);
}
render() {
return (
<ImageBackground source={require('../images/awesomeImage.jpg')}
blurRadius = {this.state.blurState}
style={styles.backgroundImage}>
<Text >Some awesome text</Text>
</ImageBackground>
);
}
}
const styles = StyleSheet.create({
backgroundImage: {
flex: 1,
width: null,
height: null
}
});
My problem is, that when changing from blurry state to the next one, the screen goes light for a moment, like it's reloading the page. I do not need a super smooth transition from one blur state to the other, but I want to get rid of these white flashed screens.
Is there any setting that helps avoiding them?
(I don't know if react-native-blur would solve this better, but I am currently using expo, so I cannot use that one.)

How do we set speed to scrollTo() in React Native?

I'm scroll the page on click of a button using:
this.scrollTo({y: height, x: 0, animated: true})
The scroll works fine, however I'd like to slow down the scroll animation.
How do we do that?
This is a pretty neat solution that uses the scrollview's content height to scroll an entire view (on mount). However, the same trick can be used (add a listener to an animated value) to create a scroll function that can be triggered by some event at any given moment (to any given value).
import { useEffect, useRef, useState } from 'react'
import { Animated, Easing, ScrollView } from 'react-native'
const SlowAutoScroller = ({ children }) => {
const scrollRef = useRef()
const scrollAnimation = useRef(new Animated.Value(0))
const [contentHeight, setContentHeight] = useState(0)
useEffect(() => {
scrollAnimation.current.addListener((animation) => {
scrollRef.current &&
scrollRef.current.scrollTo({
y: animation.value,
animated: false,
})
})
if (contentHeight) {
Animated.timing(scrollAnimation.current, {
toValue: contentHeight,
duration: contentHeight * 100,
useNativeDriver: true,
easing: Easing.linear,
}).start()
}
return () => scrollAnimation.current.removeAllListeners()
}, [contentHeight])
return (
<Animated.ScrollView
ref={scrollRef}
onContentSizeChange={(width, height) => {
setContentHeight(height)
}}
onScrollBeginDrag={() => scrollAnimation.current.stopAnimation()}
>
{children}
</Animated.ScrollView>
)
}
On android you can use the smoothScrollTo option