The best way to create a scrollable tab in the middle of the screen? - react-native

The mobile app of Twitter has a scrollable tab in the middle of the screen when you are on your profile. The top half of the screen displaying your profile info etc doesn't change when you click on the scrollable tabs mid screen : "Tweets & replies", "Media" etc. I am wondering how one would create this? Having half the screen stay the same and then having tabs which change mid screen... At the moment I have react navigation tabs as my main navigation - so on one of these tabs (the profile tab) I want to create the same concept as the picture..

Late answer but (for anyone else and future reference), react-navigation uses this package, react-native-tab-view: https://github.com/react-native-community/react-native-tab-view
for their tabs.
You can nest this within a screen, just like you desire (the previous answer only addresses the navigator inside navigator and that isn't what you want).
Here is an example (not exactly like you want, but you get the idea that you can. so instead of a background image, swap it out and use a view or scrollview accordingly to create that layout):
https://snack.expo.io/#satya164/collapsible-header-with-tabview
cheers :)
EDIT: i just found a way with just using react-navigation after all:
https://snack.expo.io/#mattx/collapsible-header-tabs
check it out
and another library: https://github.com/benevbright/react-navigation-collapsible

I don't know if you've figured it out yet, but you can nest the TabNavigator inside a StackNavigator. That way, you can have a scrollable Tab.
class ProfileMenu extends React.Component{
render() {
return(
//whatever you wanted at the top
)
}
}
const TabNaviga = createMaterialTopTabNavigator({
Tweets: {screen: TweetScreen,},
Replies: {screen: RepliesScreen,},
})
const YNavigator = createStackNavigator ({
Home:{screen: TabNaviga,
navigationOptions: ({navigation}) => ({
header: <ProfileMenu navigation= {navigation} />,
})
}
})
export default YNavigator

I found this tutorial and followed it,
EDIT: it seems there's a new library out that supports it https://github.com/PedroBern/react-native-collapsible-tab-view
https://medium.com/#linjunghsuan/implementing-a-collapsible-header-with-react-native-tab-view-24f15a685e07
I also wrote a bit of an explaination if you are interested.
create a ScrollY with useRef and .current at the end
create a handleScroll function which returns an event like so -
const handleScroll = Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: true }
);
Pass it down in props to the wanted component
<TabNavigator handleScroll={handleScroll} scrollY={scrollY} />
And also the scrollY so you can use the value in the Child component aswell
pass it farther down the line to actual events like and call handleScroll in the Child Child component onScroll prop. like so
<Animated.FlatList
...
onScroll={handleScroll}
/>
And now you can use the ScrollY value wherever you want.
what it does is checking if the current route is the one we check, it then caluclates the offset and scrollToOffset function of flatlist using the flatlist refs we got from here
return (
<Pictures
handleScroll={handleScroll}
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
onGetRef={ref => {
if (ref) {
const found = listRefArr.current.find(e => e.key === route.key);
if (!found) {
listRefArr.current.push({ key: route.key, value: ref });
}
}
}}
/>
);
the onGetRef is connected to the FlatList ref
return (
<AnimatedFlatList
ref={onGetRef}
scrollToOverflowEnabled
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
onScroll={handleScroll}
scrollEventThrottle={16}
contentContainerStyle={{
paddingTop: HeaderHeight + TabBarHeight,
paddingHorizontal: 10,
minHeight: windowHeight - TabBarHeight
}}
data={data}
renderItem={({ item }) => {
return <Comment data={item} />;
}}
keyExtractor={({ commentId }): any => {
return commentId.toString();
}}
/>
);
then we have these three functions which we send the flatlist as well
const onMomentumScrollBegin = () => {
isListGliding.current = true;
};
const onMomentumScrollEnd = () => {
isListGliding.current = false;
syncScrollOffset();
};
const onScrollEndDrag = () => {
syncScrollOffset();
};
and last but not least we still need to animate the TabBar so when the header is 500 height his is 0 when the header is 450 in the y the tabbar should be 50, we do that by getting the scrollY in the props and use it to interpolate.
const renderTabBar = (props: any) => {
return (
<Animated.View
style={{
top: 0,
zIndex: 1,
position: "absolute",
transform: [{ translateY: tabViewHeight }],
width: "100%"
}}
>
<TabBar ... />
</Animated.View>
);
};

Related

screenOptions:{{tabBarHideonKeyboard: true}} not Working

When I am using custom tab bar through tabBar function tabBarHideOnKeyboard does not work but without tabBar function it works fine, any ideas on how I can make it work using tabBar function as well.
Add "softwareKeyboardLayoutMode": "pan" in app.json file under "android" key and then restart your expo server with expo start -c
<Tab.Navigator
tabBarOptions={{
showLabel: false,
keyboardHidesTabBar: true, // use this props to hide bottom tabs when keyboard shown
}}
the docs says to use tabBarHideOnKeyboard, but not working at all.
then i found keyboardHidesTabBar and works like a charm
I was using my customTab as well. And after huge amount of search, solved the problem with the help of Keyboard event listeners.
This is the best solution, I've found so far.
Here's my solution:
import { useEffect, useState } from "react";
import { Keyboard, Text, TouchableOpacity, View } from "react-native"
export default function TabBar({ state, descriptors, navigation }) {
// As default it should be visible
const [visible, setVisible] = useState(true);
useEffect(() => {
const showSubscription = Keyboard.addListener("keyboardDidShow", () => {
//Whenever keyboard did show make it don't visible
setVisible(false);
});
const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
setVisible(true);
});
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);
//Return your whole container like so
return visible && (
<View>
...
</View>
)
tabBarHideOnKeyboard or keyboardHidesTabBar options didn't work for me.
You'll get the tabBarHideOnKeyboard from the props for the custom tabBar.
tabBar={(props) => {
return (
<View>
{props.state.routes.map((route, index) => {
// You can replace Pressable and View with anything you like
return (
props.descriptors[route.key].options.tabBarHideOnKeyboard && (
<Pressable>
<View
style={{
width: 200,
height: 200,
backgroundColor: "green",
}}
/>
</Pressable>
)
);
})}
</View>
);
You can read more here

React Native Navigation Header title too long, ends up overflowing

I've been following up a tutorial for React Native, and I'm at a point where my output differs from the video's.
You basically get your title param from the previous screen and then display it on the header title, his gets truncated if it's too long, but mine just overlaps everything, I considered just manually truncating the string but the length is going to be different on different screens, so I was wondering if anyone could point me in the right direction, I'd appreciate it a lot.
This is the code for the component:
const MealDetailScreen = props => {
const mealId = props.navigation.getParam("mealId");
const meal = MEALS.find(meal => meal.id === mealId);
return (
<View style={styles.screen}>
<Text>This is the {meal.title}!</Text>
<Button title="Back" onPress={() => props.navigation.goBack()} />
</View>
);
};
MealDetailScreen.navigationOptions = navigationData => {
const mealId = navigationData.navigation.getParam("mealId");
const meal = MEALS.find(meal => meal.id === mealId);
return {
headerTitle: meal.title,
};
};
, and this is the output: output
I had the same issue. I however am using react navigation v5.
My main fix was setting a maxWidth.
headerTitleStyle: {
maxWidth: 250,
},
for good measure I also added the following:
headerTitleAlign: 'center'
https://reactnavigation.org/docs/stack-navigator/#headertitlealign
Cutting your title dynamically according to its length should help you.
return {
headerTitle: meal.title.length >= 25 ? meal.title.slice(0, 24) + "..." : meal.title
};
Add this to your return value
return{
headerTitleContainerStyle:{
width:'60%',
alignItems:'center'
},}
You can then make adjustments to the header title width using this navigation option

How can I display 30 pages of text in a (scrolling) screen

I want to display 30 pages of text on a screen. I've tried ScrollView and FlatList but I get a white screen. Only when I try with ScrollView to display only 2 pages, works fine.
I do not want to use a WebView, because I would like to have all data in the app (no internet connection needed).
Here is what I've already tried:
With FlatList:
I have a text.js as a model, which I use to create a Text Object in an array, which I then use as data for the FlatList. For the renderItem function (of FlatList) I use a TextItem to display the text.
text.js
function Text(info) {
this.id = info.id;
this.text = info.text;
}
export default Text;
LongTextModule.js
import Text from '../../models/text';
export const LONGTEXT = [
new Text({
id:'text_1',
text:`.....longtext....`
})
]
TextItem.js
const TextItem = (props) => {
return (
<View style={styles.screen} >
<Text style={styles.textStyle}>{props.longText}</Text>
</View >
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
textStyle: {
justifyContent: 'flex-start',
alignItems: 'flex-start',
fontFamily: 'GFSNeohellenic-Regular',
fontSize: 20,
padding: 10,
}
});
TextDetailScreen.js
const TextDetailScreen = (props) => {
const renderText = data => {
return <TextItem longText={data.item.text} />
}
return <FlatList
data={LONGTEXT}
keyExtractor={(item, index) => item.id}
renderItem={renderText}
/>
};
I think it's needless to show the code with ScrollView, since ScrollView is only for a small list.
I even tried to render the longText like this in the screen.
Without the ScrollView I get the first portion, but with ScrollView a white screen.
const TextDetailScreen = (props) => {
return (
<ScrollView>
<Text> ...longText...</Text>
</ScrollView>
);
};
I'm sure there is a way to display a lot of pages of text on a screen?
But how?
Thank you :)
It seems not to be an unknown Issue, I've also read from time to time about this issue.
But not to use Webview, because you wan't to have all Data in your app - don't have to be an Argument against Webview. With WebView, you also can display Data from your App-Storage.
Example:
<WebView style={styles.myStyle} source={{html: `<p style="font-size:48px">${longtext}</p>`}} />

React native updates state "on its own"

I have two screens, one list (Flatlist) and one filter screen where I want to be able to set some filters for the list. the list screen has the states "data" and "usedFilters". When I am switching to the filters screen, the states are set as navigation parameters for react navigation and then passed via navigation.navigate, together with the onChange function, as props to the filter screen. There they are read, and the filters screen class' state is set (usually with passed filters from the list screen, if no valid filters has been passed, some are initialized).
After that the filters can be changed. If that happens, the state of the filter screen gets updated.
If then the apply button is clicked the filter screens' state is passed to the onChange function and via that back to the list screen, the onChange function updates the state "usedFilters" state of the list screen. If the cancel button is pressed null is passed to the onChange function and there is no setState call.
Setting new states for the list screen works perfectly fine. the problem is, that when i press the cancel button (or the back button automatically rendered by react navigation) the changes are kept nevertheless. That only happens if the state has been changed before. So if there has never been applied a change and hence the "usedFitlers" state of the list screen is null, this behavior does not occur. Only if I already made some changes and hence the "usedFitlers" state of the list screen has a valid value which is passed to the filters screen the cancel or go back buttons won't work as expected.
I am using expo-cli 3 and tried on my android smartphone as well as the iOS simulator. Same behavior. I looked into it with chrome dev tools as well but i simply couldn't figure out where the "usedFitlers" state was updated.
I am using react native 0.60 and react navigation 3.11.0
My best guess is that for some reason the two states share the same memory or one is pointer to the other or sth like that. (Had problems like that with python some time ago, not knowing the it uses pointers when assigning variables).
Anyone got an idea?
List Screen:
export default class ListScreen extends React.Component {
state = { data: [], usedFilters: null };
static navigationOptions = ({ navigation }) => {
let data = navigation.getParam('data')
let changefilter = navigation.getParam('changeFilter')
let currfilter = navigation.getParam('currFilter')
return {
headerTitle:
<Text style={Styles.headerTitle}>{strings('List')}</Text>,
headerRight: (
<TouchableOpacity
onPress={() => navigation.navigate('FilterScreen', {
dataset: data, onChange: changefilter, activeFilters:
currfilter })} >
<View paddingRight={16}>
<Icon name="settings" size={24} color=
{Colors.headerTintColor} />
</View>
</TouchableOpacity>
),
};
};
_onChangeFilter = (newFilter) => {
if (newFilter) {
this.setState({ usedFilters: newFilter })
this.props.navigation.setParams({ currFilter: newFilter });
} // added for debugging reasons
else {
this.forceUpdate();
let a = this.state.usedFilters;
}
}
_fetchData() {
this.setState({ data: fakedata.results },
() => this.props.navigation.setParams({ data: fakedata.results,
changeFilter: this._onChangeFilter }));
}
componentDidMount() {
this._fetchData();
}
render() {
return (
<ScrollView>
<FlatList/>
// Just data rendering, no problems here
</ScrollView>
);
}
}
Filter Screen:
export default class FilterScreen extends React.Component {
static navigationOptions = () => {
return {
headerTitle: <Text style={Styles.headerTitle}> {strings('filter')}
</Text>
};
};
state = { currentFilters: null }
_onChange = (filter, idx) => {
let tmp = this.state.currentFilters;
tmp[idx] = filter;
this.setState({ currentFilters: tmp })
}
_initFilterElems() {
const filters = this.props.navigation.getParam('activeFilters');
const dataset = this.props.navigation.getParam('dataset');
let filterA = [];
let filterB = [];
let filterC = [];
if (filters) {
// so some checks
} else {
// init filters
}
const filterElements = [filterA, filterB, filterC];
this.setState({ currentFilters: filterElements })
}
componentDidMount() {
this._initFilterElems()
}
render() {
const onChange = this.props.navigation.getParam('onChange');
return (
<ScrollView style={Styles.screenView}>
<FlatList
data={this.state.currentFilters} // Listeneinträge
keyExtractor={(item, index) => 'key' + index}
renderItem={({ item, index }) => (
<FilterCategory filter={item} name={filterNames[index]}
idx={index} onChange={this._onChange} />
)}
ItemSeparatorComponent={() => <View style=
{Styles.listSeperator} />}
/>
<View style={Layout.twoHorizontalButtons}>
<TouchableOpacity onPress={() => {
onChange(this.state.currentFilters);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('apply')} </Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={() => {
onChange(null);
this.setState({ currentFilters: null });
this.props.navigation.goBack();
}}>
<View style={Styles.smallButton}>
<Text style={Styles.buttonText}>{strings('cancel')}
</Text>
</View>
</TouchableOpacity>
</View>
</ScrollView >
);
}
}
So when I press the cancel button, null is returned to the _onChangeFilter function of the list screen. This part works, and according to console.log and the debugger, the setState is not called. But if i set a breakpoint within the else part, i can see that this.state.usedFilters has changed.
Ok after a while i figured it out. The problem was that the whole filters list was always just referenced since react native (js) seems to always use references, even when changing sub-parts of the lists.
fixed that by using lodash cloneDeep.

React Native - how to scroll a ScrollView to a given location after navigation from another screen

Is it possible to tell a ScrollView to scroll to a specific position when we just navigated to the current screen via StackNavigator?
I have two screens; Menu and Items. The Menu is a list of Buttons, one for each item. The Items screen contain a Carousel built using ScrollView with the picture and detailed description of each Item.
When I click on a button in the Menu screen, I want to navigate to the Items screen, and automatically scroll to the Item that the button represent.
I read that you can pass in parameters when using the StackNavigator like so: but I don't know how to read out that parameter in my Items screen.
navigate('Items', { id: '1' })
So is this something that is possible in React Native and how do I do it? Or perhaps I'm using the wrong navigator?
Here's a dumbed down version of my two screens:
App.js:
const SimpleApp = StackNavigator({
Menu: { screen: MenuScreen},
Items: { screen: ItemScreen }
}
);
export default class App extends React.Component {
render() {
return <SimpleApp />;
}
}
Menu.js
export default class Menu extends React.Component {
constructor(props){
super(props)
this.seeDetail = this.seeDetail.bind(this)
}
seeDetail(){
const { navigate } = this.props.navigation;
navigate('Items')
}
render(){
<Button onPress={this.seeDetail} title='1'/>
<Button onPress={this.seeDetail} title='2'/>
}
}
Items.js
export default class Items extends React.Component {
render(){
let scrollItems = [] //Somecode that generates and array of items
return (
<View>
<View style={styles.scrollViewContainer}>
<ScrollView
horizontal
pagingEnabled
ref={(ref) => this.myScroll = ref}>
{scrollItems}
</ScrollView>
</View>
</View>
)
}
}
P.S I am specifically targeting Android at the moment, but ideally there could be a cross-platform solution.
I read that you can pass in parameters when using the StackNavigator like so: but I don't know how to read out that parameter in my Items screen.
That is achieved by accessing this.props.navigation.state.params inside your child component.
I think the best time to call scrollTo on your scrollview reference is when it first gets assigned. You're already giving it a reference and running a callback function - I would just tweak it so that it also calls scrollTo at the same time:
export default class Items extends React.Component {
render(){
let scrollItems = [] //Somecode that generates and array of items
const {id} = this.props.navigation.state.params;
return (
<View>
<View style={styles.scrollViewContainer}>
<ScrollView
horizontal
pagingEnabled
ref={(ref) => {
this.myScroll = ref
this.myScroll.scrollTo() // !!
}>
{scrollItems}
</ScrollView>
</View>
</View>
)
}
}
And this is why I use FlatLists or SectionLists (which inherit from VirtualizedList) instead of ScrollViews. VirtualizedList has a scrollToIndex function which is much more intuitive. ScrollView's scrollTo expects x and y parameters meaning that you would have to calculate the exact spot to scroll to - multiplying width of each scroll item by the index of the item you're scrolling to. And if there is padding involved for each item it becomes more of a pain.
Here is an example of scroll to the props with id.
import React, { Component } from 'react';
import { StyleSheet, Text, View, ScrollView, TouchableOpacity, Dimensions, Alert, findNodeHandle, Image } from 'react-native';
class MyCustomScrollToElement extends Component {
constructor(props) {
super(props)
this.state = {
}
this._nodes = new Map();
}
componentDidMount() {
const data = ['First Element', 'Second Element', 'Third Element', 'Fourth Element', 'Fifth Element' ];
data.filter((el, idx) => {
if(el===this.props.id){
this.scrollToElement(idx);
}
})
}
scrollToElement =(indexOf)=>{
const node = this._nodes.get(indexOf);
const position = findNodeHandle(node);
this.myScroll.scrollTo({ x: 0, y: position, animated: true });
}
render(){
const data = ['First Element', 'Second Element', 'Third Element', 'Fourth Element', 'Fifth Element' ];
return (
<View style={styles.container}>
<ScrollView ref={(ref) => this.myScroll = ref} style={[styles.container, {flex:0.9}]} keyboardShouldPersistTaps={'handled'}>
<View style={styles.container}>
{data.map((elm, idx) => <View ref={ref => this._nodes.set(idx, ref)} style={{styles.element}}><Text>{elm}</Text></View>)}
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
backgroundColor:"#d7eff9"
},
element:{
width: 200,
height: 200,
backgroundColor: 'red'
}
});
export default MyCustomScrollToElement;
Yes, this is possible by utilising the scrollTo method - see the docs. You can call this method in componentDidMount. You just need a ref to call it like: this.myScroll.scrollTo(...). Note that if you have an array of items which are all of the same type, you should use FlatList instead.
For iOS - the best way to use ScrollView's contentOffset property. In this way it will be initially rendered in a right position. Using scrollTo will add additional excess render after the first one.
For Android - there is no other option rather then scrollTo