How to make element fixed on scroll in React Native? - javascript

This is a video of what I have so far https://drive.google.com/file/d/1tJMZfpe8hcqLcNMYiE-6pzwHX0eLofv1/view?usp=sharing
I'm trying to make the "Available for 3 months" fixed once the contentOffset.y has reached the header. Similar to what a position: sticky in css would do.
So far, I thought of doing this via onScroll prop in the Scroll View Component but the issue is, I already have animations (Animated.Event) from the parent and child component in place, meaning the only way for me to do this would be via the listener function in the Animated.Event but doing that, leads to a super choppy animation if the useNativeDriver option is set to false.
If I set that to true, the whole thing won't work (it crashes). The error is along the lines "onScroll is not a function, it's an instance of Animated.event"
So, Say we have two components, the parent is Parent.js and the child (a scroll view) is ChildScrollView.js
The ChildScrollView.js already has animations on the scroll view, but we need to add some more in the Parent.js component, to do with Navigation which the ChildScrollView.js can't handle
So it's coded like this:
Parent.js
componentWillMount() {
const { navigation } = this.props
const { scrollY } = this.state
const bgColor = scrollY.interpolate({
inputRange: [HEADER_HEIGHT / 4, HEADER_HEIGHT / 2],
outputRange: ['transparent', 'rgb(255,255,255)'],
extrapolate: 'clamp',
})
const titleColor = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT / 2],
outputRange: ['transparent', colors.paragraphs],
extrapolate: 'clamp',
})
const titleMarginTop = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT / 2],
outputRange: [HEADER_HEIGHT, 0],
extrapolate: 'clamp',
})
navigation.setParams({
bgColor,
titleColor,
titleMarginTop,
})
}
onScroll() {
}
render() {
return (
<ChildScrollView
{...childProps}
onScroll={Animated.event([
{ nativeEvent: { contentOffset: { y: scrollY } } },
], {
useNativeDriver: false, // when I do this, it become super laggy, but setting it to true makes the app crash
listener: this.onScroll,
})}
>
<MakeMeFixedOnScroll>I could do this in css with "position: sticky"</MakeMeFixedOnScroll>
</ChildScrollView>
)
}
And the child is similar,
<Animated.ScrollView
{...props}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{
useNativeDriver: false,
listener: event => {
if (onScroll) onScroll(event)
},
}
)}
scrollEventThrottle={16}
>

I would use SectionList
<SectionList
renderItem={({item, index, section}) => (
<Text style={styles[item.type]}>{item.text}</Text>
)}
renderSectionHeader={({ section: { title } }) => (
title && <Text style={styles.sticky}>{title}</Text>
)}
sections={[
{ title: null, data: [{
type: 'heading', text: '133 Random Road'
}, {
type: 'heading', text: 'Donnybrook'
}, {
type: 'subtitle', text: 'Dublin 4'
}, {
type: 'title', text: 'From E1000/month'
}]
},
{ title: 'Available For 3 Month', data: [{
type: 'text', text: 'Beautiful for bedroom...'
}]
}
]}
stickySectionHeadersEnabled
/>

Related

React Native Flatlist Animated View Scrolling Problem

I tried to "change" the default refresh indicator of a Flatlist in React Native. I thought about to have something like Snapchat or Instagram or the default IOS refresh indicator instead of the ugly Android indicator. Now I tried following:
const screenHeight = Dimensions.get("screen").height;
const lockWidth = screenHeight;
const finalPosition = lockWidth;
const pan = useRef(new Animated.ValueXY({x: 0, y: 0})).current;
const translateBtn = pan.y.interpolate({
inputRange: [0, finalPosition / 0.75],
outputRange: [0, finalPosition / 2],
extrapolate: 'clamp',
});
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {},
onPanResponderMove: Animated.event([null, {dy: pan.y}], {
useNativeDriver: false,
}),
onPanResponderRelease: (e, g) => {
if (g.vy > 2 || g.dy > lockWidth / 2) {
unlock();
} else {
reset();
}
},
onPanResponderTerminate: () => reset(),
}),
).current;
const reset = () => {
setGeneralState(prevState => ({...prevState, scroll: true}))
Animated.spring(pan, {
toValue: {x: 0, y: 0},
useNativeDriver: true,
bounciness: 0,
}).start();
};
const unlock = () => {
setLoading(true)
Animated.spring(pan, {
toValue: {x: 0, y: 280},
useNativeDriver: true,
bounciness: 0,
}).start();
// alert("")
setTimeout(() => {
reset();
setLoading(false)
}, 1000)
};
It works just as I expect:
Now the problem:
When the Flatlist is so big that it can be scrolled (if there are many items) I cannot pull down to refresh anymore because React Native always tries to move the flatlist when I'm scrolling.
The FlatList:
<Animated.View
style={[
styles.receiverBox,
{transform: [{translateY: translateBtn}]},
]}
{...panResponder.panHandlers}>
<View>
<FlatList
data={["user1", "user2", "user3", "user4", "user5",]}
renderItem={({item}) => {
return(
<View style={{backgroundColor: colors.background, padding: 20, marginTop: -5, width: "100%",}}>
<Text style={{fontSize: Title1, color: colors.text, textAlign: "center",}}>{item}</Text>
</View>
)
}}
/>
</View>
</Animated.View>
Figured out that this may be a problem from react native or the combination of the flatlist with the pan responder.

stop dragging after a limit has reached

I am using react native gesture handlers to create a bar that can be scrolled up and down. Currently I can scroll it as much as I want. I want to modify it such that it should stop scrolling when I certain limit has reached.
export const SwipeablePanel: React.FunctionalComponent = () => {
//set up animation variables
const dragY = useRef(new Animated.Value(0));
const onGestureEvent = Animated.event(
[{ nativeEvent: { translationY: dragY.current } }],
{
useNativeDriver: true,
},
);
const onHandleStateChange = (event: any) => {
if (event.nativeEvent.oldState === State.ACTIVE) {
dragY.current.extractOffset();
}
console.log('EVENT', event.nativeEvent)
};
const animateInterpolation = dragY.current.interpolate({
inputRange: [-(SCREEN_HEIGHT * 2) / 3, 0],
outputRange: [moderateScale(80) - (SCREEN_HEIGHT * 2) / 3, 0],
extrapolate: 'clamp',
});
const animatedStyles = {
transform: [
{
translateY: animateInterpolation,
},
],
};
const whitelistBarMarginInterpolation = dragY.current.interpolate({
inputRange: [-SCREEN_HEIGHT + SCREEN_HEIGHT / 3, 0],
outputRange: [0, moderateScale(150)],
extrapolate: 'clamp',
});
const whitelistBarStyles = {
transform: [
{
translateY: whitelistBarMarginInterpolation,
},
],
};
return (
<PanGestureHandler
onGestureEvent={onGestureEvent}
onHandlerStateChange={onHandleStateChange}
activeOffsetX={[-1000, 1000]}
activeOffsetY={[-10, 10]}
>
<Animated.View
style={[
styles.container,
animatedStyles
]}>
<ScrollBar />
);
};
In onHandleStateChange, I can get values of event.nativeEvent such as
absoluteX: 237
absoluteY: 348.5
handlerTag: 109
numberOfPointers: 0
oldState: 4
state: 5
target: 5235
translationX: 7
translationY: 200.5
velocityX: 0
velocityY: 0
x: 237
y: 84.84616088867188
I want to use an if else condition in the code such that I can set limits after which point the scrolling stops. But I am not sure how to do that since the scrolling happens from the onGestureEvent.
I thought of adding checks in here but if I change it like this, it no longer works:
const onGestureEvent = () => {
Animated.event([{ nativeEvent: { translationY: dragY.current } }], {
useNativeDriver: true,
});
};
Snack Expo : https://snack.expo.io/#nhammad/insane-pizza
I tried to reproduce it but here I can't scroll at it. I am using the same code in my app and it scrolls over there.
Try ScrollView, It has prop called scrollEnabled. By help of this prop you can turn to false when you don't want user to be able to scroll any more. When false, the view cannot be scrolled via touch interaction.
Here is documentation: https://facebook.github.io/react-native/docs/scrollview.html#scrollenabled
ScrollView also has onScroll so it should be easy to implement the logic here.

Victory events not re-rendering chart

I am using Victory for data visualisation in my project. However, while implementing event handlers in my charts, I noticed that while they change the target properties, the charts are never re-rendered so nothing changes.
Below is an example from Victory's documentation, which does not work on my machine:
<div>
<h3>Click Me</h3>
<VictoryScatter
style={{ data: { fill: "#c43a31" } }}
size={9}
labels={() => null}
events={[{
target: "data",
eventHandlers: {
onClick: () => {
return [
{
target: "data",
mutation: (props) => {
const fill = props.style && props.style.fill;
return fill === "black" ? null : { style: { fill: "black" } };
}
}, {
target: "labels",
mutation: (props) => {
return props.text === "clicked" ?
null : { text: "clicked" };
}
}
];
}
}
}]}
data={[{ x: 1, y: 2 },
{ x: 2, y: 3 },
{ x: 3, y: 5 },
{ x: 4, y: 4 },
{ x: 5, y: 7 }]}
/>
</div>
After some debugging, I can confirm that the data and labels properties of the component are changed as a result of the onClick event, but these changes are not actually reflected in the chart. Any solutions?
It seems the issue is caused by having <React.StrictMode> on in the code inside index.js. Removing it solves the problem. I am not exactly sure why though!!

Panresponder + interpolate

I have a Component Parent which has an Animated.value "this.progress" which takes values in [1, 3].
The Parent renders a Component Child and pass it the progress as a props:
// File Parent.js
import React from "react";
import { Animated } from "react-native";
import Child from "./Child.js"
class Parent extends React.Component {
constructor(props) {
super(props);
this.progress = new Animated.Value(1)
}
render() {
return (
<Child
progress = {this.progress}
/>
);
}
}
The Child position depends on the progress:
if progress = 1, the child position is [1, 3]
if progress = 2, the child position is [2, 2]
if progress = 3, the child position is [1, 1]
If an animation changes the value of the progress, the Child must move accoringly (e.g., if progress is animated from 1 to 2, the user must see the Child move from [1, 3] to [2, 2] in a smooth way).
To that end I use, two interpolations:
// In the constructor of Child.js
this.interpolateX = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [1, 2, 1],
});
this.interpolateY = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [3, 2, 1],
});
and I use them to fix the position of the Child using "transform":
// Render method of Child.js
render() {
return (
<Animated.View
style={[
{
transform: [
{
translateX: this.interpolateX,
},
{
translateY: this.interpolateY,
},
],
},
{ position: 'absolute' },
{ left: 0 },
{ top: 0 },
]}
/>
);
}
It works well, but now I also want to make the Child movable using the user finger. To that end I defined a panResponder. The problem is that the panResponder also uses a transform. I now have 2 transforms:
one used to interpolate the Child's position from this.props.progress ;
one used by the panResponder to move the Child with the user finger.
I don't know how to combine these two transform. Here is what I tried:
// File Child.js
import React from "react";
import { Animated, PanResponder } from "react-native";
class Child extends React.Component {
constructor(props) {
super(props);
this.interpolateX = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [1, 2, 1],
});
this.interpolateY = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [3, 2, 1],
});
this.offset = new Animated.ValueXY({ x: 0, y: 0 });
this._val = { x: 0, y: 0 };
this.offset.addListener((value) => (this._val = value));
// Initiate the panResponder
this.panResponder = PanResponder.create({
// Ask to be the responder
onStartShouldSetPanResponder: () => true,
// Called when the gesture starts
onPanResponderGrant: () => {
this.offset.setOffset({
x: this._val.x,
y: this._val.y,
});
this.offset.setValue({ x: 0, y: 0 });
},
// Called when a move is made
onPanResponderMove: Animated.event([
null,
{ dx: this.offset.x, dy: this.offset.y },
]),
onPanResponderRelease: (evt, gesturestate) => {
console.log(
"released with offsetX: " +
this.offset.x._value +
"/" +
this.offset.y._value
);
},
});
}
render() {
const panStyle = {
transform: this.offset.getTranslateTransform(),
};
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
panStyle,
{
transform: [
{
translateX: this.interpolateX,
},
{
translateY: this.interpolateY,
},
],
},
{ position: "absolute" },
{ left: 0 },
{ top: 0 },
]}
/>
);
}
}
export default Child;
It does not work. panStyle seems to be ignored. I guess you cannot have two transforms together.
I also tried to "add" the two expressions in the transforms without success (maybe its feasible).
My last idea was to have a boolean "isMoving" equal to true when the user moves the Child. If isMoving is false, I use the first transform, otherwise, I use the second one. It seems promising but I did not succeed in making it work.
Could you tell me how I could do that?
A boolean variable isMoving which indicates which transform to use was the good idea. The trick was:
to put isMoving in the state;
to set the offset of my offset variable to the current value of the interpolate.
Here is the result:
// File Child.js
import React from "react";
import { Animated, PanResponder } from "react-native";
class Child extends React.Component {
constructor(props) {
super(props);
this.interpolateX = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [1, 2, 1],
});
this.interpolateY = this.props.progress.interpolate({
inputRange: [1, 2, 3],
outputRange: [3, 2, 1],
});
this.offset = new Animated.ValueXY({ x: 0, y: 0 });
this.state = {
isMoving: false,
};
// Initiate the panResponder
this.panResponder = PanResponder.create({
// Ask to be the responder
onStartShouldSetPanResponder: () => true,
// Called when the gesture starts
onPanResponderGrant: () => {
this.setState({
isMoving: true,
});
this.offset.setOffset({
x: this.interpolateX.__getValue(),
y: this.interpolateY.__getValue(),
});
this.offset.setValue({ x: 0, y: 0 });
},
// Called when a move is made
onPanResponderMove: Animated.event([
null,
{ dx: this.offset.x, dy: this.offset.y },
]),
onPanResponderRelease: (evt, gesturestate) => {
this.setState({
isMoving: false,
});
},
});
}
render() {
const panStyle = {
transform: this.offset.getTranslateTransform(),
};
if (this.state.isMoving)
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[panStyle, { position: "absolute" }, { left: 0 }, { top: 0 }]}
/>
);
else
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[
{
transform: [
{
translateX: this.interpolateX,
},
{
translateY: this.interpolateY,
},
],
},
{ position: "absolute" },
{ left: 0 },
{ top: 0 },
]}
/>
);
}
}
export default Child;

react-native can't open expo project

I am trying to run an expo project Collapsible Header with TabView it works fine in this link.
But when I copy and paste the code to my own expo project, it gives me an error.
Invariant Violation: Element type is invalid: expected a string or a class/function. Check the render method of App
The only thing I changed is class TabView to class App but it is giving me this error.
Also, if I want to build a native code project not expo. How would I run this code? because there is import {Constants } from expo this wouldn't work in react-native run-ios
import * as React from 'react';
import {
View,
StyleSheet,
Dimensions,
ImageBackground,
Animated,
Text
} from 'react-native';
import { Constants } from 'expo';
import { TabViewAnimated, TabBar } from 'react-native-tab-view'; // Version can be specified in package.json
import ContactsList from './components/ContactsList';
const initialLayout = {
height: 0,
width: Dimensions.get('window').width,
};
const HEADER_HEIGHT = 240;
const COLLAPSED_HEIGHT = 52 + Constants.statusBarHeight;
const SCROLLABLE_HEIGHT = HEADER_HEIGHT - COLLAPSED_HEIGHT;
export default class TabView extends React.Component {
constructor(props: Props) {
super(props);
this.state = {
index: 0,
routes: [
{ key: '1', title: 'First' },
{ key: '2', title: 'Second' },
{ key: '3', title: 'Third' },
],
scroll: new Animated.Value(0),
};
}
_handleIndexChange = index => {
this.setState({ index });
};
_renderHeader = props => {
const translateY = this.state.scroll.interpolate({
inputRange: [0, SCROLLABLE_HEIGHT],
outputRange: [0, -SCROLLABLE_HEIGHT],
extrapolate: 'clamp',
});
return (
<Animated.View style={[styles.header, { transform: [{ translateY }] }]}>
<ImageBackground
source={{ uri: 'https://picsum.photos/900' }}
style={styles.cover}>
<View style={styles.overlay} />
<TabBar {...props} style={styles.tabbar} />
</ImageBackground>
</Animated.View>
);
};
_renderScene = () => {
return (
<ContactsList
scrollEventThrottle={1}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scroll } } }],
{ useNativeDriver: true }
)}
contentContainerStyle={{ paddingTop: HEADER_HEIGHT }}
/>
);
};
render() {
return (
<TabViewAnimated
style={styles.container}
navigationState={this.state}
renderScene={this._renderScene}
renderHeader={this._renderHeader}
onIndexChange={this._handleIndexChange}
initialLayout={initialLayout}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, .32)',
},
cover: {
height: HEADER_HEIGHT,
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1,
},
tabbar: {
backgroundColor: 'rgba(0, 0, 0, .32)',
elevation: 0,
shadowOpacity: 0,
},
});
I got it worked! I had to replace TabViewAnimated to TabView.

Categories

Resources