Is there a way to animate the increased size of a View when new children are added? - react-native

I’m currently using LayoutAnimation to animate a view when children are added. However, since LayoutAnimation causes everything to be animated, globally, and I can’t easily use built-in Animated library to fit my use-case, I’m wondering if react-native-reanimated is able to help.
Here's a snack of my current solution:
https://snack.expo.io/#insats/height-adapation
This is what the result of that looks like:
Is there a way to achieve the same thing without using LayoutAnimation? I've looked through all exampled in react-native-reanimated, and I've read through the docs but I'm still not sure if this is possible to do or how I should get started. I've thought about using Animated to move the item-wrapper out of the viewable area and "scroll" it upwards (using transform translateY) when items are added, but that would require fixed height, which I don't have.

I have 2 approaches that I can suggest out of my mind:
You can configure your LayoutAnimation only when your desired state changed. If you use hooks it would be too easy:
const [state,setState] = useState([]);
useEffect(()=>{
/*rest code*/
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
},[state])
Or if you use class component you can catch your desired state change in componentDidUpdate:
componentDidUpdate(prevProps,prevState){
if(prevState.items!==state.items){
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
}
}
You can use onLayout function of view:
addItem = () => {
this.setState({
items: [...this.state.items, {title:'An item',isNew:true}]
})
};
renderItems = () => {
return this.state.items.map((item, index) => {
let opacity = new Animated.Value(0);
return (
<Animated.View onLayout={({nativeEvent})=>{
if(this.state.item.isNew){
// here you got the height from nativeEvent.layout.height
// Then you have to store the height animate height and opacity to its precise value
// PS I used opacity:0 to calculate the height
}
}} key={index} style={[styles.item,{opacity}>
<Text>{item.title}</Text>
</View>
)
});
};
When it comes to react-native-reanimated I regard it as more faster version of react-native's Animated library. So either way you will have to calculate the height!

Related

How to virtualize a FlatList inside FlatList (RN Web)?

How do I convince a vertical React Native FlatList to virtualize correctly inside another vertical (non-virtualizing) FlatList, in React Native Web?
So far, it seems that by default, scrolling to a certain point or responsive resize re-renderings tend to cause the virtualization to go haywire. This Snack demonstrates the problem. Be sure you're on the "Web" tab as the device builds seem to work correctly. Here's a repro through codesandbox too.
Update: Per request, here's the code inline as well. This is a full program that can paste into, say, a new expo init project (or similar) to see the strange behavior and experiment with it.
import React, { useCallback } from 'react';
import { FlatList, Text, useWindowDimensions, View } from 'react-native';
// Make 200 rows for the big list (which will draw green and red with some info).
const bigListData = Array(200).fill(0).map((element, index) => index);
function onViewableChange({ viewableItems }) {
if (viewableItems.length < 2) {
console.log(`VIEWABLE CHANGE! Only ${viewableItems.length} visible...`);
} else {
console.log(`VIEWABLE CHANGE! ${viewableItems[0].index} to ${viewableItems[viewableItems.length - 1].index}`);
}
}
function BigList() {
const { height, width } = useWindowDimensions();
const betweenRows = 10;
const itemHeight = height / 8;
const totalRowHeight = itemHeight + betweenRows;
const renderer = useCallback(({ item }) => {
const key = `i_${item}`;
return <View key={key} style={{
backgroundColor: item % 2 ? "red" : "green",
height: itemHeight,
width: '90%',
marginLeft: '5%',
marginBottom: betweenRows }}>
<Text>{key}, rh: {totalRowHeight}, offset: {totalRowHeight * item}, i {item}</Text>
</View>;
}, [itemHeight, totalRowHeight]);
const getItemLayout = useCallback((__data, index) => ({
index,
length: itemHeight,
offset: index * totalRowHeight
}), [itemHeight, totalRowHeight]);
return <FlatList
data={bigListData}
getItemLayout={getItemLayout}
key={'flatList'}
numColumns={1}
onViewableItemsChanged={onViewableChange}
renderItem={renderer}
/>;
}
function NoNestedFlatLists() {
const windowHeight = useWindowDimensions().height;
return <View style={{ height: windowHeight, width: '80%' }}><BigList /></View>;
}
function renderComponent({ item }) {
if (item.type === "widget") {
// Using height 600 here, but assume we cannot easily predict this height (due to text wrappings).
return <View key={item.type} style={{ backgroundColor: 'blue', height: 600, width: '100%', marginBottom: 15 }} />
}
return <BigList key={item.type} />;
}
function NestedFlatLists() {
const windowHeight = useWindowDimensions().height;
const components = [{ type: "widget" }, { type: "bigList" }];
return <FlatList
data={components}
key={'dynamicAppFlatList'}
numColumns={1}
renderItem={renderComponent}
style={{ height: windowHeight, width: '80%' }}
/>;
}
export default function App() {
const windowHeight = useWindowDimensions().height;
// Rendering just the following has no virtualization issues.
// The viewable change events make sense, no items suddenly disappear, no complete app meltdown...
//return <NoNestedFlatLists />;
// However:
// Any useful dynamic "rows of components" architecture melts down when virtualization comes into play.
// This sample represents such an app whose feeds have asked the app to render a "widget" followed by a
// "bigList" who could well have a few hundred items itself and thus really needs virtualization to work
// well on low-end devices. This demo leans on console logs. In snack.expo.dev, at time of writing, these
// feel hidden: Click the footer bar, either on the checkmark or an empty space, and then the "Logs" tab.
// Once you scroll down about half way in the "App", even slowly, you'll get logs like the following:
// Chrome: VIEWABLE CHANGE! 83 to 90
// Chrome: VIEWABLE CHANGE! 85 to 92
// Chrome: VIEWABLE CHANGE! Only 0 visible...
// Chrome: VIEWABLE CHANGE! 176 to 183
// Chrome: VIEWABLE CHANGE! 177 to 184
// At which time, all the UI disappears. What it thinks is viewable is quite wrong. Try to scroll around,
// but none of the mid rows are drawing. There is no easy way to repair app behavior from this state. The
// only rows which still draw correctly during the problem are the top and bottom non-virtualizing rows.
//
// As an alternate repro, you can scroll to near the middle and then resize the bottom of the window, and
// similar virtualization problems can occur. (In our real app, we can be scrolled almost anywhere out of
// the non-virtualizing rows, and make a 1px window resize to break the app. We have a more complex app
// structure, but I'm hoping a fix for this snack will still be applicable to our own symptoms...)
return <NestedFlatLists />;
}
Hopefully I am missing something trivial, as it seems clear React Native is attempting to handle nested FlatLists of the same orientation, and for the most part does great. Until you happen to have enough data items to bring virtualization into play, and even then, only fails for Web. (We've tried upgrading React Native to all the way to 0.67.2 and React Native Web to 0.17.5 - the latest releases - with no luck, and none of the Expo dropdown versions yield correct behavior in the linked Snack either.) What can I change in either sample to have correct virtualization in the nested FlatList?
Short answer is: You can't convince FlatList to virtualize this way correctly. At least currently (0.17), it's broken.
Although I was able to get some FlatList virtualization improvements into React Native Web's 0.18 preview, ultimately the measurement problems are deeper than I could afford to spend more weeks to fully fix. (If someone wants to try picking up from there - I recommend to focus on reconciling RNW's ScrollView versus RN's ScrollView and then digging into the ScrollView's measurements going absolutely haywire in the repro scenario, if replicating RN's evolution of ScrollView to RNW isn't enough.)
It ended up being much faster though to build our own virtualizing list component from scratch. Ours is specialized to our needs ATM so probably won't become open source, but who knows. But if you need to go this route... think about throttling reactions to scroll events and such to ".measure" the container view ref periodically and decide which things you need to render versus just rendering reserved empty space for... etc. There are other approaches but that seems to work.

Janky translateY on Reanimated View inside of ScrollView as user scrolls

I am using react-native-reanimated v1. I want to make a <Reanimated.View> appear as if it is fixed within the scroll view by using translateY. GIF of how of it should behave is at very end of post. I have simplified the code in the snippet below, and I have the full working code in the snack.
https://snack.expo.io/#noitidart/reanimated-scroll-view
As you scroll, you notice the position of the view is not staying fixed at the top. I attached a video taken on iOS of the snack.
If I add scrollEventThrottle={16} it fixes the issue on iOS, but on Android if you scroll even a little faster than normal you see the transform is lagging. I think there should be a way on iOS without the scrollEventThrottle property too, it doesn't make sense to me that we need this as reanimated is supposed to update every frame.
Any ideas on how to fix this?
const ReanimatedScrollView = Reanimated.createAnimatedComponent(ScrollView);
export default function App() {
const translateY = Reanimated.useValue(0);
const handleScroll = Reanimated.event([
{
nativeEvent: nativeEvent => Reanimated.block([Reanimated.set(translateY, nativeEvent.contentOffset.y)])
}])
return (
<ReanimatedScrollView onScroll={handleScroll}>
<Reanimated.View style={{ transform: [{ translateY }] }} />
</ReanimatedScrollView>
);
}
Janky on iOS
Janky on Android (with and without scrollEventThrottle={16})
Here is video of how it should be (with scrollEventThrottle={16} on iOS, but it doesn't fix up Android)

Render FlatList of Videos in a performant way

I am using a react native with expo. I have a lot of videos that I need to render (sort of like TikTok does). When I fetch about 30 videos and put them in the flat list in the renderItem method, it gets stuck and luggish. I was thinking about getting an amount of videos but sending to the renderItem method only 3 videos each time, and when the user will scroll down and reach index 2 it will shift the first index and append the fourth video from the fetched one. The idea was to have a small array of size 3 and change the items in it every scroll, in order to prevent rendering all the videos at once. That required array manipulation and caused a rerender each time the array of videos was updated(each change made sort of a flash - what was indicating a whole rerender).
My question is how should it be implemented in order the transition between the videos to be as fast and clean as possible from the client side perspective? What is the correct way to render videos in a flat list so it won't be stuck? I dont think It should be done that way, there has to be a better way.
This is what I have tried:
// challenges is an array coming from a fetch, just sliced it for the purpose of the example
// suppose it is an array that contains 30 items
const [currentVideos, setCurrentVideos] = useState([challenges.slice(0,3)]);
<FlatList
data={currentVideos}
renderItem={renderItem}
keyExtractor={(challenge, i) => challenge._id}
showsVerticalScrollIndicator={false}
snapToInterval={Dimensions.get("window").height - UIConsts.bottomNavbarHeight}
snapToAlignment={"start"}
decelerationRate={"fast"}
ref={(ref) => {
flatListRef.current = ref;
}}
onScrollToIndexFailed={() => alert("no such index")}
onViewableItemsChanged={onViewRef.current}
onScrollEndDrag={() => (scrollEnded.current = true)}
onScrollBeginDrag={beginDarg}
></FlatList>
useEffect(() => {
// just wanted to check on 3 videos
if (currentlyPlaying === 2) {
let temp = currentVideos;
temp.shift(); // pop the top item
temp.push(challenges[4]) // append a new one
setCurrentVideos(temp);
}
}, [currentlyPlaying]);
const onViewRef = useRef(({ viewableItems }) => {
// change playing video only after user stop dragging
scrollEnded.current && setCurrentlyPlaying(viewableItems[0]?.index);
});
I would avoid manipulating the data array and doing business logic inside of the component.
Besides, you can achieve your desired behaviour without the need to manipulate your data array at all, with the maxToRenderPerBatch FlatList prop. As mentioned in the official RN docs for FlatList optimization techniques.
You should avoid using anonymous functions and objects inside of your component's properties, move them outside of the return statement and use the useMemo and useCallback hooks to avoid their unnecessary recreation on every re-render. For example instead of writing your code like this:
const App = () => {
return (
<FlatList
keyExtractor={(challenge, i) => challenge._id}
snapToInterval={Dimensions.get('window').height - UIConsts.bottomNavbarHeight}
/>
);
};
A better approach would be to re-write it to something like this:
const App = () => {
// Because of useCallback, the keyExtractor function will be memoized and won't recreate itself on every re-render
const keyExtractor = useCallback((challenge, i) => challenge._id, []);
// useMemo is almost the same as useCallback, but it is used to return non-function types
// Defining your snapToInterval variable like this will cause it to memoize its value and it
// won't recreate itself on every re-render
const snapToInterval = useMemo(() => Dimensions.get('window').height - UIConsts.bottomNavbarHeight, []);
return (
<FlatList
keyExtractor={keyExtractor}
snapToInterval={snapToInterval}
/>
);
};
If you haven't already, you should consider extracting the component returned from the renderItem function to a different file and applying React.memo to it.
Note: try not to overuse useCallback and useMemo. You can find good and detailed explanation of why not to overuse them here and here.
If you're able to, you should optimize your videos before uploading them to the server. You can optimize your client side part of the app as much as you want, but if the content isn't properly optimized, you won't be able to achieve a smooth and performant experience regardless of your efforts.
Here's also some articles describing how you can optimize your FlatList component:
How did I optimize my React Native FlatList?
8 ways to optimize React native FlatList performance
Optimizing a React Native FlatList With Many Child Components
React Native Performance Optimisation With Hooks
React Native: Optimized FlatList of videos
I hope that some of this will be helpful to you. Good luck.
I have been searching for a solution as well. I have worked out a solution based on some previous work using InViewPort. you can check it out here https://github.com/471Q/React-Native-FlatList-Video-Feed

Detect Swipe direction using React Native Gesture Handler and Reanimated

We have already developed a swiper using React Native PanResponder, (before somebody comment) - we cannot use any other swiper library here. In the swiper we developed, we are facing an issue when a user ends the swipe(meaning releases the Pan) there is a lag when starting a spring animation.
So to fix that, we are trying to move from React Native Animated API to Reanimated Libary which could solve the problem of lag the user is facing.
But while developing with React Native Gesture Handler (PanGestureHandler) and
Reanimated Library, we're unable to detect swipe direction in the
gesture handler.
I'm adding the part of code which we used to detect swipe direction in PanResponder
onPanMoving = (evt, {dx}) => {
this.draggedEnd = dx < -this.swipeThreshold; //You are moving towards right screen
this.draggedStart = dx > this.swipeThreshold; //You are moving towards left screen
}
As you see, detecting the direction while the pan is moving was so easy in PanResponder.
Here comes the problem with Gesture Handler,
I'm unable to detect swipe while the gesture state is active
I have already searched for issues in Gesture Handler and I found this.
There were two workaround suggested in the issue
First one is by Sonaye which has two handler and detects the
swipe direction in onHandlerStateChange for both the handlers
which was not helpful while using reanimated because it only sets swipe direction
when handlers state changes.
Second one is by Satya which actually detects the swipe when
the State is ACTIVE but he uses translationX property, the
translationX property can also negative for us and can move in
either direction similar to swiper.
Both the workarounds doesn't solve our problem.
So is there any other way to find direction using PanGestureHandler and Reanimated. I tried using PanResponder with Reanimated with dx value but ended up with error message i.e nativeEvent properties are only support as dx is from gestureState param from PanResponder.
Note: We cannot use FlingGestureHandler and Swipeables because we need to find direction in Fling still onHandlerStateChange and Swipeables doesn't use Reanimated.
You can detect direction when swipe horizontal via property velocityX
Example :
const handleGesture = (evt) => {
const { nativeEvent } = evt;
if (nativeEvent.velocityX > 0) {
console.log('Swipe right');
} else {
console.log('Swipe left');
}
};
return (
<PanGestureHandler onGestureEvent={handleGesture}>
// child component
</PanGestureHandler>
);
For those who want to use double FlingGestureHandler to have vertical or horizontal fling gesture here is the solution:
First you have to initialize the stateHandlers for the direction you want. In my case, i was trying to implement a vertical flingGestureHandler.
const onDownFlingHandlerStateChange = ({
nativeEvent,
}: FlingGestureHandlerStateChangeEvent) => {
if (nativeEvent.oldState === State.ACTIVE) {
progressHeight.value = withTiming(COLLAPSED_HEIGHT, {
duration: ANIMATION_DURATION,
});
}
};
const onUpFlingHandlerStateChange = ({
nativeEvent,
}: FlingGestureHandlerStateChangeEvent) => {
if (nativeEvent.oldState === State.ACTIVE) {
progressHeight.value = withTiming(
DEVICE_HEIGHT - TOP_PADDING_FOR_EXPANDED,
{
duration: ANIMATION_DURATION,
},
);
}
};
and then you have to bind those handlers to 2 FlingGestureHandler component of RNGH
<FlingGestureHandler
onHandlerStateChange={onUpFlingHandlerStateChange}
onEnded={() => {
onPressChecklistIcon();
}}
direction={Directions.UP}>
<FlingGestureHandler
direction={Directions.DOWN}
onEnded={() => {
onPressChecklistIcon();
}}
onHandlerStateChange={onDownFlingHandlerStateChange}>
<Animated.View style={[styles.container, reanimatedStyle]}>
<View style={styles.closerContainer}>
<View style={styles.closer} />
</View>
{renderChecklistContent()}
</Animated.View>
</FlingGestureHandler>
</FlingGestureHandler>
Use translationX property rather than velocityX property by nativeEvent:
const handleGesture = (evt) => {
const { nativeEvent } = evt;
if (nativeEvent.translationX > 0) {
Alert.alert('Swipe right');
} else {
Alert.alert('Swipe left');
}
};
In my case I have used double FlingGestureHandler to detect the direction. It's worked for me.

React native detect screen rotation

I'm using onLayout to detect screen orientation and it's working fine inside my root view, but when I implemented inside the drawer it didn't work, any reason why this happens ?
code :
import Drawer from 'react-native-drawer'
...
onLayout(e) {
console.log('onLayout');
}
<Drawer onLayout={this.onLayout}
It didn't log any thing when orientation changed!
This is because the Drawer component doesn't take onLayout as a prop. You can see in the source code that the rendered View does use onLayout, but it's not pulling from something like this.props.onLayout.
I'm not exactly sure what you're looking to do, but maybe this issue will help you. As it shows, you can pass a function into openDrawerOffset instead of an integer or a ratio in order to be a little more dynamic with how you set your offset:
openDrawerOffset={(viewport) => {
if (viewport.width < 400) {
return viewport.width * 0.1;
}
return viewport.width - 400;
}}
You might also benefit from the Event handlers that react-native-drawer has to offer.