Has anyone here ever done a vertical video Flatlist with a button that makes the video fullscreen (not the native UI). If yes could you give some advice on how to achieve it? Should I make the video position absolute and make it go from top to bottom (tried but couldn't make it work in the Flatlist)? Should I have a hidden video component that shares the state with the one in the Flatlist? Thanks any guidance is appreciated
I would recommend using modal feature from react navigation package(check this)
with this package you can render your full screen video inside a separate view.
Here you go.
const App: () => Node = () => {
const [popup, setPopup] = useState(false);
return (
<View style={{flex: 1, backgroundColor: 'aliceblue'}}>
<FlatList
data={[
{title: '1', url: 'https://www.w3schools.com/html/mov_bbb.mp4'},
{title: '2', url: 'https://www.w3schools.com/html/mov_bbb.mp4'},
]}
renderItem={({item}) => (
<Button title={item.title} onPress={() => setPopup(true)} />
)}
/>
{popup ? (
<View
style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
backgroundColor: '#00000050',
}}>
<View
style={{
backgroundColor: '#FFFFFF50',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text>Use whatever library you want to use to play the video</Text>
</View>
<Button title="Close" onPress={() => setPopup(false)} />
</View>
) : null}
</View>
);
};
I've built a sample that works for your requirement. Didn't do the Video stuff tho. Hope this helps.
Updated code with Popup component
const Popup = ({videoUrl, onClose}) => {
return (
<View
style={{
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
backgroundColor: '#00000050',
}}>
<View
style={{
backgroundColor: '#FFFFFF50',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Text>{videoUrl}</Text>
</View>
<Button title="Close" onPress={onClose} />
</View>
);
};
const App: () => Node = () => {
const [popup, setPopup] = useState(null);
return (
<View style={{flex: 1, backgroundColor: 'aliceblue'}}>
<FlatList
data={[
{title: '1', url: 'https://www.w3schools.com/html/mov_bbb.mp4'},
{title: '2', url: 'https://www.w3schools.com/html/mov_bbb.mp4'},
]}
renderItem={({item}) => (
<Button
title={item.title}
onPress={() =>
setPopup({
videoUrl: item.url,
})
}
/>
)}
/>
{popup != null ? (
<Popup videoUrl={popup.videoUrl} onClose={() => this.setPopup(null)} />
) : null}
</View>
);
};
What is exact problem? You can't make it fullscreen by custom button? Or you want to make it fullscreen with your own custom layout? I guess first.
Not clean example, but should show how it works.
https://snack.expo.dev/#valera.bitkovsky/react-native-flatlist-video-custom-fullscreen
import React from "react";
import { StyleSheet, Text, View, FlatList, Button } from "react-native";
import { Video, AVPlaybackStatus } from 'expo-av';
const VideoItem = React.forwardRef(({ url }, ref) => {
const video = React.useRef(null);
React.useImperativeHandle(ref, () => ({
full: () => {
video.current.presentFullscreenPlayer();
}
}), [])
return <Video
ref={video}
source={{
uri: url,
}}
style={{
width: 400,
height: 200
}}
useNativeControls
resizeMode="contain"
isLooping
/>
});
function App() {
const videoRefs = React.useRef([]);
return (
<View style={styles.app}>
<FlatList
data={[
{ url: "https://www.w3schools.com/html/mov_bbb.mp4" },
{ url: "https://www.w3schools.com/html/mov_bbb.mp4" }
]}
renderItem={({ item, index }) => (<View>
<VideoItem ref={ref => videoRefs.current[index] = ref} url={item.url} />
<Button title="Fullscreen" onPress={() => videoRefs.current[index].full()} />
</View>)}
/>
</View>
);
}
const styles = StyleSheet.create({
app: {
marginTop: 50,
marginHorizontal: "auto",
maxWidth: 500
}
});
export default App;
And if you want second varint, then you can just add state and change layout to absolute and do whatever you want.
UPD
Regarding the aproach where we use absolute styles, seems that isn't possible, see this issue
https://github.com/facebook/react-native/issues/29867
So, we still can use our custom controls, but probably we should use native fullscreen mode.
You can try use simple ScrollView, I know that it isn't optimazied for that very well, but absolute position should work
I have three tabs in a top tab bar navigation with different width text. Is it possible to make the indicator width match the text? On a similar note, how can I make the tabs match the width of the text too without making it display weird. I've tried width auto but it doesn't stay center.
This is how it looks with auto width:
<Tab.Navigator
initialRouteName="Open"
tabBarOptions={{
style: {
backgroundColor: "white",
paddingTop: 20,
paddingHorizontal: 25
},
indicatorStyle: {
borderBottomColor: colorScheme.teal,
borderBottomWidth: 2,
width: '30%',
left:"9%"
},
tabStyle : {
justifyContent: "center",
width: tabBarWidth/3,
}
}}
>
<Tab.Screen
name="Open"
component={WriterRequestScreen}
initialParams={{ screen: 'Open' }}
options={{
tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> Open </Text>,
}}
/>
<Tab.Screen
name="In Progress"
component={WriterRequestScreen}
initialParams={{ screen: 'InProgress' }}
options={{
tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> In Progress </Text>}}
/>
<Tab.Screen
name="Completed"
component={WriterRequestScreen}
initialParams={{ screen: 'Completed' }}
options={{ tabBarLabel: ({focused}) => <Text style = {{fontSize: 18, fontWeight: 'bold', color: focused? colorScheme.teal : colorScheme.grey}}> Completed </Text>}}
/>
</Tab.Navigator>
I also needed to make the indicator fit the text size, a dynamic width for the labels, and a scrollable top bar because of long labels. The result looks like this:
tab bar with dynamic indicator width
If you don't care about the indicator width fitting the labels, you can simply use screenOptions.tabBarScrollEnabled: true in combination with width: "auto" in screenOptions.tabBarIndicatorStyle.
Otherwise, you'll need to make your own tab bar component and pass it to the tabBar property of your <Tab.Navigator>. I used a ScrollView but if you have only a few tabs with short labels, a View would be more simple. Here is the Typescript code for this custom TabBar component:
import { MaterialTopTabBarProps } from "#react-navigation/material-top-tabs";
import { useEffect, useRef, useState } from "react";
import {
Animated,
Dimensions,
View,
TouchableOpacity,
StyleSheet,
ScrollView,
I18nManager,
LayoutChangeEvent,
} from "react-native";
const screenWidth = Dimensions.get("window").width;
const DISTANCE_BETWEEN_TABS = 20;
const TabBar = ({
state,
descriptors,
navigation,
position,
}: MaterialTopTabBarProps): JSX.Element => {
const [widths, setWidths] = useState<(number | undefined)[]>([]);
const scrollViewRef = useRef<ScrollView>(null);
const transform = [];
const inputRange = state.routes.map((_, i) => i);
// keep a ref to easily scroll the tab bar to the focused label
const outputRangeRef = useRef<number[]>([]);
const getTranslateX = (
position: Animated.AnimatedInterpolation,
routes: never[],
widths: number[]
) => {
const outputRange = routes.reduce((acc, _, i: number) => {
if (i === 0) return [DISTANCE_BETWEEN_TABS / 2 + widths[0] / 2];
return [
...acc,
acc[i - 1] + widths[i - 1] / 2 + widths[i] / 2 + DISTANCE_BETWEEN_TABS,
];
}, [] as number[]);
outputRangeRef.current = outputRange;
const translateX = position.interpolate({
inputRange,
outputRange,
extrapolate: "clamp",
});
return Animated.multiply(translateX, I18nManager.isRTL ? -1 : 1);
};
// compute translateX and scaleX because we cannot animate width directly
if (
state.routes.length > 1 &&
widths.length === state.routes.length &&
!widths.includes(undefined)
) {
const translateX = getTranslateX(
position,
state.routes as never[],
widths as number[]
);
transform.push({
translateX,
});
const outputRange = inputRange.map((_, i) => widths[i]) as number[];
transform.push({
scaleX:
state.routes.length > 1
? position.interpolate({
inputRange,
outputRange,
extrapolate: "clamp",
})
: outputRange[0],
});
}
// scrolls to the active tab label when a new tab is focused
useEffect(() => {
if (
state.routes.length > 1 &&
widths.length === state.routes.length &&
!widths.includes(undefined)
) {
if (state.index === 0) {
scrollViewRef.current?.scrollTo({
x: 0,
});
} else {
// keep the focused label at the center of the screen
scrollViewRef.current?.scrollTo({
x: (outputRangeRef.current[state.index] as number) - screenWidth / 2,
});
}
}
}, [state.index, state.routes.length, widths]);
// get the label widths on mount
const onLayout = (event: LayoutChangeEvent, index: number) => {
const { width } = event.nativeEvent.layout;
const newWidths = [...widths];
newWidths[index] = width - DISTANCE_BETWEEN_TABS;
setWidths(newWidths);
};
// basic labels as suggested by react navigation
const labels = state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = route.name;
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: "tabPress",
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
// The `merge: true` option makes sure that the params inside the tab screen are preserved
// eslint-disable-next-line
// #ts-ignore
navigation.navigate({ name: route.name, merge: true });
}
};
const inputRange = state.routes.map((_, i) => i);
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map((i) => (i === index ? 1 : 0.5)),
});
return (
<TouchableOpacity
key={route.key}
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
onPress={onPress}
style={styles.button}
>
<View
onLayout={(event) => onLayout(event, index)}
style={styles.buttonContainer}
>
<Animated.Text style={[styles.text, { opacity }]}>
{label}
</Animated.Text>
</View>
</TouchableOpacity>
);
});
return (
<View style={styles.contentContainer}>
<Animated.ScrollView
horizontal
ref={scrollViewRef}
showsHorizontalScrollIndicator={false}
style={styles.container}
>
{labels}
<Animated.View style={[styles.indicator, { transform }]} />
</Animated.ScrollView>
</View>
);
};
const styles = StyleSheet.create({
button: {
alignItems: "center",
justifyContent: "center",
},
buttonContainer: {
paddingHorizontal: DISTANCE_BETWEEN_TABS / 2,
},
container: {
backgroundColor: "black",
flexDirection: "row",
height: 34,
},
contentContainer: {
height: 34,
marginTop: 30,
},
indicator: {
backgroundColor: "white",
bottom: 0,
height: 3,
left: 0,
position: "absolute",
right: 0,
// this must be 1 for the scaleX animation to work properly
width: 1,
},
text: {
color: "white",
fontSize: 14,
textAlign: "center",
},
});
export default TabBar;
I managed to make it work with a mix of:
react navigation example
react-native-tab-view original indicator
jsindos answer
Please let me know if you find a more convenient solution.
You have to add width:auto to tabStyle to make tab width flexible.
Then inside each tabBarLabel <Text> component add style textAlign: "center" and width: YOUR_WIDTH .
YOUR_WIDTH can be different for each tab and can be your text.length * 10 (if you want to make it depended on your text length) or get screen width from Dimensions and divide it by any other number to make it equal widths in screen. Example:
const win = Dimensions.get('window');
...
bigTab: {
fontFamily: "Mulish-Bold",
fontSize: 11,
color: "#fff",
textAlign: "center",
width: win.width/2 - 40
},
smallTab: {
fontFamily: "Mulish-Bold",
fontSize: 11,
color: "#fff",
textAlign: "center",
width: win.width / 5 + 10
}
Remove width from indicatorStyle and use flex:1
indicatorStyle: { borderBottomColor: colorScheme.teal,
borderBottomWidth: 2,
flex:1,
left:"9%"
},
I've achieved this using some hacks around onLayout, please note I've made this with the assumptions of two tabs, and that the second tabs width is greater than the first. It probably will need tweaking for other use cases.
import React, { useEffect, useState } from 'react'
import { createMaterialTopTabNavigator } from '#react-navigation/material-top-tabs'
import { Animated, Text, TouchableOpacity, View } from 'react-native'
const Stack = createMaterialTopTabNavigator()
const DISTANCE_BETWEEN_TABS = 25
function MyTabBar ({ state, descriptors, navigation, position }) {
const [widths, setWidths] = useState([])
const [transform, setTransform] = useState([])
const inputRange = state.routes.map((_, i) => i)
useEffect(() => {
if (widths.length === 2) {
const [startingWidth, transitionWidth] = widths
const translateX = position.interpolate({
inputRange,
outputRange: [0, startingWidth + DISTANCE_BETWEEN_TABS + (transitionWidth - startingWidth) / 2]
})
const scaleX = position.interpolate({
inputRange,
outputRange: [1, transitionWidth / startingWidth]
})
setTransform([{ translateX }, { scaleX }])
}
}, [widths])
return (
<View style={{ flexDirection: 'row' }}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key]
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name
const isFocused = state.index === index
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true
})
if (!isFocused && !event.defaultPrevented) {
// The `merge: true` option makes sure that the params inside the tab screen are preserved
navigation.navigate({ name: route.name, merge: true })
}
}
const onLayout = event => {
const { width } = event.nativeEvent.layout
setWidths([...widths, width])
}
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map(i => (i === index ? 0.87 : 0.53))
})
return (
<TouchableOpacity
key={index}
accessibilityRole='button'
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
style={{ marginRight: DISTANCE_BETWEEN_TABS }}
>
<Animated.Text
onLayout={onLayout}
style={{
opacity,
color: '#000',
fontSize: 18,
fontFamily: 'OpenSans-Bold',
marginBottom: 15
}}
>
{label}
</Animated.Text>
</TouchableOpacity>
)
})}
<View style={{ backgroundColor: '#DDD', height: 2, position: 'absolute', bottom: 0, left: 0, right: 0 }} />
<Animated.View style={{ position: 'absolute', bottom: 0, left: 0, width: widths.length ? widths[0] : 0, backgroundColor: '#222', height: 2, transform }} />
</View>
)
}
export default () => {
return (
<>
<Stack.Navigator tabBar={props => <MyTabBar {...props} />} style={{ paddingHorizontal: 25 }}>
<Stack.Screen name='Orders' component={() => <Text>A</Text>} />
<Stack.Screen name='Reviews' component={() => <Text>B</Text>} />
</Stack.Navigator>
</>
)
}
Update:
If the menu names are static, it is probably a more robust solution to hard code the widths inside of widths, although this is a little more costly to maintain.
Resources:
https://reactnavigation.org/docs/material-top-tab-navigator/#tabbar
https://github.com/facebook/react-native/issues/13107
In the screenOptions, add following props for
tabBarScrollEnabled: true
tabBarItemStyle: {{width: "auto", minWidht: "100"}}
minWidth is just to keep the design consistent.
Please note, I am using react-navigation 6.x and Camille Hg answer was really helpful.
I had the same issue and I was finally able to make the indicator takes exactly the text size.
I don't know in which version this was possible .. but apparently you can add a custom indicator component (beside the ability to add a custom tabBar component)
when creating the TopTabNavigator it is important to add the properties as described in the code under
// assuming that you want to add paddingHorizontal: 10 for each item!
const TAB_BAR_ITEM_PADDING = 10;
const Tab = createMaterialTopTabNavigator();
function TopTabNavigator() {
return (
<Tab.Navigator
.....
... .
screenOptions={{
....
...
tabBarItemStyle: {
// these properties are important for this method to work !!
width: "auto",
marginHorizontal: 0, // this is to make sure that the spacing of the item comes only from the paddingHorizontal!.
paddingHorizontal: TAB_BAR_ITEM_PADDING, // the desired padding for the item .. stored in a constant to be passed in the custom Indicator
},
tabBarIndicator: props => {
return (
<CustomTabBarIndicator
// the default props
getTabWidth={props.getTabWidth}
jumpTo={props.jumpTo}
layout={props.layout}
navigationState={props.state}
position={props.position}
width={props.width}
style={{
left: TAB_BAR_ITEM_PADDING,
backgroundColor: Colors.primary,
}}
// this is an additional property we will need to make the indicator exactly
tabBarItemPadding={TAB_BAR_ITEM_PADDING}
/>
);
},
}}
>
<Tab.Screen .... />
<Tab.Screen ..... />
<Tab.Screen .... />
</Tab.Navigator>
);
}
now for the CustomTabBarIndIndicator component we simply go to the official github repository for react-native-tab-view and then go to TabBarIndicator.tsx and copy the component over in a file called CustomTabBarIndicator "just to be consistence with the example, but you can call it what ever you want", and don't forget to add the additional property to the Props type for tabBarItemPadding "if you are using typescript"
and now make this small change to the line that is highlighted in the image
change:
const outputRange = inputRange.map(getTabWidth);
to be:
const outputRange = inputRange.map(x => {
// this part is customized to get the indicator to be the same width like the label
// subtract the tabBarItemPadding from the tabWidth
// so that you indicator will be exactly the same size like the label text
return getTabWidth(x) - this.props.tabBarItemPadding * 2;
});
and that was it :)
P.S. I added the image because I didn't know how to exactly describe where to make the change
and if you don't want the typescript .. jsut remove all the types from the code and you are good to go :)
I am trying to apply an active color for the clicked icon(List and Grid) but couldn't find a correct solution since new into RN. I have pasted the component code separately for someone's reference.
CODE:
export default class Gallery extends Component {
state = {
loading: true,
gridView: true,
iconColor: "#ccc"
};
changeViewList = () => {
this.setState({ gridView: false });
};
changeViewGrid = () => {
this.setState({ gridView: true });
};
render() {
const { imageData, loading } = this.state;
return (
<View style={{ flex: 1 }}>
<TouchableOpacity activeOpacity={0.8} onPress={this.changeViewGrid}>
<Icon name="th-large" size={25} color={this.state.iconColor} />
</TouchableOpacity>
<TouchableOpacity activeOpacity={0.8} onPress={this.changeViewList}>
<Icon name="list" size={25} style={styles.iconAlign} color= {this.state.iconColor} />
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
btnDesign: {
padding: 10,
backgroundColor: "#e45",
width: "30%",
alignSelf: "center",
marginBottom: 10
},
btnText: {
color: "#fff",
textAlign: "center",
alignSelf: "center"
}
});
Image reference:
Expected would be:
List View
Grid View
You can use the ternary operator inside the color prop to colorize the icon depending on a variable.
// if the this.state.gridView is true colorize the icon green otherwise take the standard color
<Icon name="th-large" size={25} color={this.state.gridView ? 'green' : this.state.iconColor } />
and here you can do it the other way around:
// if this.state.gridView is true, take the regular color otherwise use make it green
<Icon name="list" size={25} style={styles.iconAlign} color= {this.state.gridView ? this.state.iconColor : 'green' } />
I am using react-navigation .
I want to add icon for the tab.
CustomTabs.js from example directory
if you are to use react-native-vector-icon is much easier, just create an array like the one i created below, for all the names of the icon you want to use and if you want to use image, then you will have to use image links because the last time i checked react native won't allow you to load static assets dynamically.
Benefit of using an icon especially react-native-vector-icon:
Access to tonnes of iconsets.
Styling based on if its focused or not.
....and others things i can't remember.
`
.....
import Icon from 'react-native-vector-icons/Ionicons';
const styles = {
body: {
backgroundColor: '#3b4147',
height: 60,
},
tabWrapper: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: 50,
},
tabInnerWrapper: {
marginRight: 12,
marginLeft: 12,
justifyContent: 'center',
alignItems: 'center',
},
textStyle: {
fontSize: 12,
color: '#62666b',
},
focusTextStyle: {
fontSize: 12,
color: '#acafb1',
},
};
const {body, tabWrapper, tabInnerWrapper, textStyle, focusTextStyle} = styles;
const focusIconColor = '#acafb1';
const iconColor = '#62666b';
const IconNames = ['ios-compass-outline', 'ios-cut-outline', 'ios-chatboxes-outline'];
const IconNamesFocus = ['ios-compass', 'ios-cut', 'ios-chatboxes'];
const CustomTabBar = ({ navigation: { state, navigate }}) => {
const { routes } = state;
return (
<View style={body}>
<View style={tabWrapper}>
{routes && routes.map((route, index) => {
const focused = index === state.index;
return (
<TouchableOpacity
key={route.key}
onPress={() => navigate(route.routeName)}
style={tabInnerWrapper}
>
<Icon
name={focused ? IconNamesFocus[index] : IconNames[index]}
size={25}
color={focused ? focusIconColor : iconColor}
/>
<Text style={focused ? focusTextStyle : textStyle}>
{route.routeName}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
);
};
`