App with scroll
In the image I have drawn you can see two areas, the yellow one is a ScrollView, and the red one is a flatlist.
When I scroll from the red zone, I want the tabs to go up to the header, and once they touch the header, start scrolling the red zone of FlatList.
To do this, when the tabs touch the header I set the scrollEnabled from the yellow zone to false, the problem is that the red zone doesn't scroll until I stop pressing and press again.
The behavior I want it to have is similar to the instagram profile, where there are some tabs and then a list of photos, when the tabs touch the header, you can continue scrolling from the images.
I'll add a few options
First one is a snack, which again uses parent and child scrollviews
Collapsible Header Tabs Snack
Problems: This has issues on Android. the scroll stutters on android while both parent and child scrollviews are scrolling.
Second is github repo React-Native-Collapsing-TabView
Problems: if one of the tab is scrolled and you go to another tab then there will white space on top.issue
Using Native Base.
import React, {Component} from "react";
import {Animated, Dimensions, Platform, Text, TouchableOpacity, View} from "react-native";
import {Body, Header, List, ListItem as Item, ScrollableTab, Tab, TabHeading, Tabs, Title} from "native-base";
import LinearGradient from "react-native-linear-gradient";
const {width: SCREEN_WIDTH} = Dimensions.get("window");
const IMAGE_HEIGHT = 250;
const HEADER_HEIGHT = Platform.OS === "ios" ? 64 : 50;
const SCROLL_HEIGHT = IMAGE_HEIGHT - HEADER_HEIGHT;
const THEME_COLOR = "rgba(85,186,255, 1)";
const FADED_THEME_COLOR = "rgba(85,186,255, 0.8)";
export default class ParallaxDemo extends Component {
nScroll = new Animated.Value(0);
scroll = new Animated.Value(0);
textColor = this.scroll.interpolate({
inputRange: [0, SCROLL_HEIGHT / 5, SCROLL_HEIGHT],
outputRange: [THEME_COLOR, FADED_THEME_COLOR, "white"],
extrapolate: "clamp"
});
tabBg = this.scroll.interpolate({
inputRange: [0, SCROLL_HEIGHT],
outputRange: ["white", THEME_COLOR],
extrapolate: "clamp"
});
tabY = this.nScroll.interpolate({
inputRange: [0, SCROLL_HEIGHT, SCROLL_HEIGHT + 1],
outputRange: [0, 0, 1]
});
headerBg = this.scroll.interpolate({
inputRange: [0, SCROLL_HEIGHT, SCROLL_HEIGHT + 1],
outputRange: ["transparent", "transparent", THEME_COLOR],
extrapolate: "clamp"
});
imgScale = this.nScroll.interpolate({
inputRange: [-25, 0],
outputRange: [1.1, 1],
extrapolateRight: "clamp"
});
imgOpacity = this.nScroll.interpolate({
inputRange: [0, SCROLL_HEIGHT],
outputRange: [1, 0],
});
tabContent = (x, i) => <View style={{height: this.state.height}}>
<List onLayout={({nativeEvent: {layout: {height}}}) => {
this.heights[i] = height;
if (this.state.activeTab === i) this.setState({height})
}}>
{new Array(x).fill(null).map((_, i) => <Item key={i}><Text>Item {i}</Text></Item>)}
</List></View>;
heights = [500, 500];
state = {
activeTab: 0,
height: 500
};
constructor(props) {
super(props);
this.nScroll.addListener(Animated.event([{value: this.scroll}], {useNativeDriver: false}));
}
render() {
return (
<View>
<Animated.View style={{position: "absolute", width: "100%", backgroundColor: this.headerBg, zIndex: 1}}>
<Header style={{backgroundColor: "transparent"}} hasTabs>
<Body>
<Title>
<Animated.Text style={{color: this.textColor, fontWeight: "bold"}}>
Tab Parallax
</Animated.Text>
</Title>
</Body>
</Header>
</Animated.View>
<Animated.ScrollView
scrollEventThrottle={5}
showsVerticalScrollIndicator={false}
onScroll={Animated.event([{nativeEvent: {contentOffset: {y: this.nScroll}}}], {useNativeDriver: true})}
style={{zIndex: 0}}>
<Animated.View style={{
transform: [{translateY: Animated.multiply(this.nScroll, 0.65)}, {scale: this.imgScale}],
backgroundColor: THEME_COLOR
}}>
<Animated.Image
source={{uri: "https://upload.wikimedia.org/wikipedia/commons/c/c5/Moraine_Lake_17092005.jpg"}}
style={{height: IMAGE_HEIGHT, width: "100%", opacity: this.imgOpacity}}>
{/*gradient*/}
{/* <LinearGradient
colors={["rgba(255,255,255,0.9)", "rgba(255,255,255,0.35)", "rgba(255,255,255,0)"]}
locations={[0, 0.25, 1]}
style={{position: "absolute", height: "100%", width: "100%"}}/> */}
</Animated.Image>
</Animated.View>
<Tabs
prerenderingSiblingsNumber={3}
onChangeTab={({i}) => {
this.setState({height: this.heights[i], activeTab: i})
}}
renderTabBar={(props) => <Animated.View
style={{transform: [{translateY: this.tabY}], zIndex: 1, width: "100%", backgroundColor: "white"}}>
<ScrollableTab {...props}
renderTab={(name, page, active, onPress, onLayout) => (
<TouchableOpacity key={page}
onPress={() => onPress(page)}
onLayout={onLayout}
activeOpacity={0.4}>
<Animated.View
style={{
flex: 1,
height: 100,
backgroundColor: this.tabBg
}}>
<TabHeading scrollable
style={{
backgroundColor: "transparent",
width: SCREEN_WIDTH / 2
}}
active={active}>
<Animated.Text style={{
fontWeight: active ? "bold" : "normal",
color: this.textColor,
fontSize: 14
}}>
{name}
</Animated.Text>
</TabHeading>
</Animated.View>
</TouchableOpacity>
)}
underlineStyle={{backgroundColor: this.textColor}}/>
</Animated.View>
}>
<Tab heading="Tab 1">
{this.tabContent(30, 0)}
</Tab>
<Tab heading="Tab 2">
{this.tabContent(15, 1)}
</Tab>
</Tabs>
</Animated.ScrollView>
</View>
)
}
}
Problems: All screens scrolling because of same offset
Fourth is a module Sticky parallax header you can use tabbed header from here
Problems: Since its module not a lot of room for customization, but there are enough. And also when i was using it there were some issues like all screens scrolling because of same offset which might have been solved now.
So basically speaking all of them have some problems that you will have to solve later on.
But I would recommend the 4th option using the sticky-parallax-header by netguru.
Related
I'm trying to create a ScrollView which contains one sticky selector, that allow the selection between two nested ScollViews. It's like the twitter profile screen, or the instagram screen, where you can switch between my posts and posts where I was tagged.
Now my problem actually is that this two nested ScollViews, let's say "MY POSTS" and "TAGGED" could have different sizes, but the RootScrollView consider only the biggest height of the two scrollviews, so if in the first I've 20 items, and let's say height=1000, in the second if I don't have items, or less items, I'll have an empty space y offset like the first.
I know it's not so clear, but if you open instagram or twitter profile screens you'll immediately get it, the problem of the different heights.
Now as you'll see, what I've tried to do is create a RootScrollView, put inside it two views, the header and the sticky selector, in twitter it's the "Tweet", "Tweets and replies" ... , and the a NestedScrollView which initially has scrollEnabled=false, and then, by scroll the root I'll update it to true and to false the root one. But it seems not to work correctly.
Here's the code:
const HEADER_HEIGHT = height / 3;
const STIKY_SELECTOR_HEIGHT = 100;
const App = () => {
const rootScrollRef = useRef();
const nestedScrollRef = useRef();
const [offset, setOffset] = useState(0);
const [scrollEnabled, setScrollEnabled] = useState(false);
const onRootScroll = ({
nativeEvent: {
contentOffset: { y },
},
}) => {
const direction = y > offset ? "down" : "up";
setOffset(y);
if (y > HEADER_HEIGHT - 10 && direction == "down") {
setScrollEnabled(true);
}
};
const onNestedScroll = ({
nativeEvent: {
contentOffset: { y },
},
}) => {
if (y < 20) setScrollEnabled(false);
};
const renderItem = () => {
return <View style={styles.cell} />;
};
return (
<View style={{ flex: 1 }}>
{/* ROOT SCROLLVIEW */}
<ScrollView
simultaneousHandlers={nestedScrollRef}
scrollEventThrottle={16}
ref={rootScrollRef}
onScroll={onRootScroll}
stickyHeaderIndices={[1]}
scrollEnabled={!scrollEnabled}
style={{ flex: 1, backgroundColor: "gray" }}
>
{/* HEADER */}
<View
style={{ width, height: HEADER_HEIGHT, backgroundColor: "darkblue" }}
></View>
{/* STIKY SELECTOR VIEW */}
<View
style={{ height: STIKY_SELECTOR_HEIGHT, backgroundColor: "red" }}
></View>
{/* NESTED SCROLLVIEW */}
<View style={{ height: height - STIKY_SELECTOR_HEIGHT }}>
<FlatList
data={[1, 2, 3, 4, 5, 6, 7]}
ref={nestedScrollRef}
scrollEventThrottle={16}
onScroll={onNestedScroll}
scrollEnabled={scrollEnabled}
renderItem={renderItem}
numColumns={2}
contentContainerStyle={{
justifyContent: "space-between",
}}
/>
</View>
</ScrollView>
</View>
);
};
If someone is facing the same problem there a component for that react-native-collapsible-tab-view
<Tabs.Container
renderHeader={Header}
headerHeight={HEADER_HEIGHT} // optional>
<Tabs.Tab name="A">
<Tabs.FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={identity}
/>
</Tabs.Tab>
<Tabs.Tab name="B">
<Tabs.ScrollView>
<View style={[styles.box, styles.boxA]} />
<View style={[styles.box, styles.boxB]} />
</Tabs.ScrollView>
</Tabs.Tab>
</Tabs.Container>
I have implemented this using Animated library in native react-native where I interpolate when I am scrolling. The problem is it is very laggy on ios and somewhat laggy on android. Is there a library or a better way to do this header hiding when scrolling in react-native ?
Here is my current implementation:
const scrollY = new Animated.Value(0);
const diffClamp = Animated.diffClamp(scrollY, 0, HEADER_HEIGHT);
const translateY = diffClamp.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT],
});
return (
<ExploreHeader
...
translateY={translateY}
/>
<ExploreData
...
scrollY={scrollY}
offset={HEADER_HEIGHT}
scrollRef={scrollRef}
/>
and in ExploreHeader I have this:
<Animated.View
style={{
position: 'absolute',
top: 0,
right: 0,
left: 0,
height: height,
transform: [{translateY: translateY}],
elevation: 100,
zIndex: 100,
}}>
and in ExploreData:
<FlatList
...
onScroll={(e) => {
scrollY.setValue(e.nativeEvent.contentOffset.y);
}}
style={{paddingTop: offset}}
/>
An re-animation (1.13) is used to toggle accordion in React Native app. When accordion is open or close, an arrow is up or down. interpolate is used to animate when arrow from up to down or vise verse.
import Animated, { useValue, interpolate, Easing, useCode, State, greaterThan, lessThan } from "react-native-reanimated";
const animatedController = useValue(0); //<<==animated value between [0, 1]
const arrowAngle = interpolate(animatedController, { //<<==interpolate causes error of node cannot be cast to number
inputRange: [0, 0.5, 1],
outputRange: ['0rad', `${0.5*Math.PI}rad`,`${Math.PI}rad`],
extrapolate: Extrapolate.CLAMP,
});
return (
<>
<TouchableWithoutFeedback onPress={() => setOpen(!open)}>
<View style={styles.titleContainer}>
<Text>{title}</Text>
<Animated.View style={{ transform: [{ rotateZ: arrowAngle }] }}> //<<==arrowAngle call here
<Icon name="chevron-down-outline" size={20} />
</Animated.View>
</View>
</TouchableWithoutFeedback>
</>
)
However the method above causes the error of node cannot be cast to number as below. This interpolate seems very simple. What's wrong here?
The output range shall be a number like this:
const arrowAngle = interpolate(animatedController, {
inputRange: [0, 0.5, 1],
outputRange: [0.01, 1/2*Math.PI,Math.PI], //<<==shall be digital
extrapolate: Extrapolate.CLAMP,
});
I created 2 Views that display overlaps to each others. The top and the bottom
When the bottom view background wasn't configured. It responded to the press event correctly. Let's say when I press on the overlap zone, it showed that the bottom one had been pressed
However, when I configured the bottom view backgroundColor. When I pressed on the overlap zone, on Android, it responded as I pressed on the top view which I think it's incorrect. (iOS it responded correctly that the bottom was pressed)
Steps To Reproduce
Provide a detailed list of steps that reproduce the issue.
Here is an example component
const OverlapseTouchExample = ({backgroundColor}) => {
const [pressedBox, setPressefBox] = React.useState('')
return (
<View>
<Text>{pressedBox} pressed</Text>
<TouchableOpacity style={[styles.box, {backgroundColor: 'blue'}]} onPress={() => setPressefBox('top')} />
<View style={backgroundColor ? { backgroundColor: 'orange' } : null}>
<View style={{marginTop: -75}}>
<TouchableOpacity style={[styles.boxBottom, backgroundColor ? { backgroundColor: 'green '} : null]} onPress={() => setPressefBox('bottom')} />
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
box: {
width: 150,
height: 150,
borderWidth: 1,
},
boxBottom: {
width: 120,
height: 200,
borderWidth: 1,
}
})
The problem found when set the backgroundColor to true
<OverlapseTouchExample backgroundColor={true} />
You could see it in Snack https://snack.expo.io/#gie3d/9b6c32 (Android)
So Ideally, When i scroll down, I want the header to disappear(slide down) and when I scroll up I want it to show (slide up). Idc where im at in the page. I just want the animation to fire when those 2 events occur. I see some apps have this but I can't think of how to replicate it. please help me set a basis for this
You can use Animated.FlatList or Animated.ScrollView to make the scroll view, and attach a callback to listen onScroll event when it is changed. Then, using interpolation to map value between y-axis and opacity.
searchBarOpacityAnim is a component's state. By using Animated.event, the state will be updated when a callback is called. Also, don't forget to set useNativeDriver to be true. I've attached the link to document below about why you have to set it.
<Animated.FlatList
...
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: searchBarOpacityAnim } } }],
{ useNativeDriver: true },
)}
...
/>
Then, use Animated.View wraps your component which you want to animate it. Use .interpolate to map value between the state and component's opacity like the example below...
<Animated.View
style={{
opacity: searchBarOpacityAnim.interpolate({
inputRange: [213, 215],
outputRange: [0, 1],
}),
}}
>
<SearchBar />
</Animated.View>
You can read more information about useNativeDriver, .interpolate, and Animated.event here.
https://facebook.github.io/react-native/docs/animated#using-the-native-driver
https://facebook.github.io/react-native/docs/animations#interpolation
https://facebook.github.io/react-native/docs/animated#handling-gestures-and-other-events
You can use Animated from 'react-native'
here an example changing the Topbar height:
import { Animated } from 'react-native';
define maxHeight and minHeight topbar
const HEADER_MAX_HEIGHT = 120;
const HEADER_MIN_HEIGHT = 48;
initialize a variable with the scrollY value
constructor(props) {
super(props);
this.state = {
scrollY: new Animated.Value(
Platform.OS === 'ios' ? -HEADER_MAX_HEIGHT : 0,
),
};
}
on render you can interpolate a value acording the scrollY Value
render() {
const { scrollY } = this.state;
// this will set a height for topbar
const headerHeight = scrollY.interpolate({
inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT],
outputRange: [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT],
extrapolate: 'clamp',
});
// obs: the inputRange is the scrollY value, (starts on 0)
// and can go until (HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT)
// outputRange is the height that will set on topbar
// obs: you must add a onScroll function on a scrollView like below:
return (
<View>
<Animated.View style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: '#2e4265',
height: headerHeight,
zIndex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
}}>
<Text>{title}</Text>
</Animated.View>
<ScrollView
style={{ flex: 1 }}
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scrollY } } }],
)}>
<View style={{ height: 1000 }} />
</ScrollView>
</View>
);
}