I would like to center the active tab in the scrollable navbar. Currently I have a scrollable horizontal view but am stuck on updating the view. I am looking at other examples such as here on github. I would love some feedback on creating the _updateView method, there seems to be more to solving this problem than simply adding a few flexbox rules.
Here is my component code.
export default class MenuNavBar extends Component {
static propTypes: {
goToPage: React.PropTypes.func,
activeTab: React.PropTypes.number,
tabs: React.PropTypes.array,
}
constructor(props) {
super(props)
var screenWidth = Dimensions.get('window').width;
this.state = {
paddingLeft: 5,
paddingRight: 5,
titleMenuWidth: screenWidth - 100
};
this._onScroll = this._onScroll.bind(this);
this._onClickMenu = this._onClickMenu.bind(this);
componentDidMount() {
this._listener = this.props.scrollValue.addListener(this.setAnimationValue.bind(this));
}
_onClickMenu(index) {
this.props.goToPage(index);
this._updateView(index);
}
_updateView(index) {
var navContainerWidth = Dimensions.get('window').width - 100;
console.log('navContainerWidth', navContainerWidth)
}
_onTabLayout(event, i) {
this.menuTabs[i] = (this.menuTabs[i]) ? this.menuTabs[i] : event.nativeEvent.layout.width;
}
_onScroll(event) {
let {
contentSize,
contentInset,
contentOffset,
layoutMeasurement,
} = event.nativeEvent;
}
_renderMain() {
return (
<NavigationBar
title={
<ScrollView ref='menuScrollView' onScroll={this._onScroll} onLayout={this._onLayout} style={{width: this.state._titleMenuWidth}} horizontal={true} showsHorizontalScrollIndicator={false}>
{this.props.tabs.map((tab, i) => {
if (i == 0) {
ref_name = 'tab_' + i;
component_style = {
paddingTop: 5,
paddingBottom: 5,
paddingLeft: this.state.paddingLeft,
paddingRight: 5,
};
} else if (i == this.props.tabs.length - 1) {
ref_name = 'tab_' + i;
component_style = {
paddingTop: 5,
paddingBottom: 5,
paddingLeft: 5,
paddingRight: this.state.paddingRight,
};
} else {
ref_name = "tab_" + i;
component_style = styles.navbarMenuButton;
}
return <TouchableOpacity ref={ref_name} key={tab} onLayout={(event) => this._onTabLayout(event, i)} onPress={() => this._onClickMenu(i)} style={component_style}>
<Text style={this.props.activeTab === i ? styles.navbarMenuTextActive : styles.navbarMenuText}>{tab}</Text>
</TouchableOpacity>;
})}
</ScrollView>
}
leftButton={
<View>
<Image source={require('../../assets/imgs/logo.png')} style={styles.navbarLogo} />
</View>
}
style={styles.headerStyle}
statusBar={{tintColor: '#6C0104'}} />
);
}
_renderTrend() {
return (
<NavigationBar
title={
<ScrollView ref='menuScrollView' onScroll={this._onScroll} onLayout={this._onLayout} style={{width: titleMenuWidth}} horizontal={true} showsHorizontalScrollIndicator={false}>
{this.props.tabs.map((tab, i) => {
if (i == 0) {
ref_name = 'tab_' + i;
component_style = {
paddingTop: 5,
paddingBottom: 5,
paddingLeft: this.state.paddingLeft,
paddingRight: 5,
};
} else if (i == this.props.tabs.length - 1) {
ref_name = 'tab_' + i;
component_style = {
paddingTop: 5,
paddingBottom: 5,
paddingLeft: 5,
paddingRight: this.state.paddingRight,
};
} else {
ref_name = "tab_" + i;
component_style = styles.navbarMenuButton;
}
return <TouchableOpacity ref={ref_name} key={tab} onLayout={(event) => this._onTabLayout(event, i)} onPress={() => this._onClickMenu(i)} style={component_style}>
<Text style={this.props.activeTab === i ? styles.navbarMenuTextActive : styles.navbarMenuText}>{tab}</Text>
</TouchableOpacity>;
})}
</ScrollView>
}
leftButton={
<View>
<Image source={require('../../assets/imgs/logo.png')} style={styles.navbarLogo} />
</View>
}
rightButton={
<View>
<TouchableOpacity onPress={() => Actions.country()}>
<Image source={require('../../assets/imgs/ic_world.png')} style={styles.worldLogo} />
</TouchableOpacity>
</View>
}
style={styles.headerStyle}
statusBar={{tintColor: '#6C0104'}} />
);
}
render() {
var screenWidth = Dimensions.get('window').width;
var titleMenuWidth = screenWidth - 100;
return (this.props.mode == 'main') ? this._renderMain() : this._renderTrend();
}
}
P.S. I will update this question to be more clear as I fully understand it better.
Related
I'm trying convert "expo-react-native audio player and video example" to funtional component.
This's code base:
expo/playlist-example
and this's my code that I'm trying convert to funtional component:
import React, { useState } from "react";
import {
Dimensions,
Image,
StyleSheet,
Text,
TouchableHighlight,
View
} from "react-native";
import { Asset } from "expo-asset";
import {
Audio,
InterruptionModeAndroid,
InterruptionModeIOS,
ResizeMode,
Video
} from "expo-av";
import * as Font from "expo-font";
import Slider from "#react-native-community/slider";
import { MaterialIcons } from "#expo/vector-icons";
import { PLAYLIST } from "../data/PlayList";
import { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from "react/cjs/react.development";
const ICON_STOP_BUTTON = () => (<Image source={(require("../assets/images/stop_button.png"))} style={{ width: 22, height: 22 }} />);
const ICON_FORWARD_BUTTON = () => (<Image source={(require("../assets/images/forward_button.png"))} style={{ width: 33, height: 25 }} />);
const ICON_BACK_BUTTON = () => (<Image source={(require("../assets/images/back_button.png"))} style={{ width: 33, height: 25 }} />);
const ICON_LOOP_ALL_BUTTON = require("../assets/images/loop_all_button.png")
const ICON_LOOP_ONE_BUTTON = require("../assets/images/loop_one_button.png")
const ICON_THROUGH_EARPIECE = "speaker-phone";
const ICON_THROUGH_SPEAKER = "speaker";
const LOOPING_TYPE_ALL = 0;
const LOOPING_TYPE_ONE = 1;
const LOOPING_TYPE_ICONS = { 0: ICON_LOOP_ALL_BUTTON, 1: ICON_LOOP_ONE_BUTTON };
const { width: DEVICE_WIDTH, height: DEVICE_HEIGHT } = Dimensions.get("window");
const BACKGROUND_COLOR = "#FFF8ED";
const DISABLED_OPACITY = 0.5;
const FONT_SIZE = 14;
const LOADING_STRING = "... loading ...";
const BUFFERING_STRING = "...buffering...";
const RATE_SCALE = 3.0;
const VIDEO_CONTAINER_HEIGHT = (DEVICE_HEIGHT * 2.0) / 5.0 - FONT_SIZE * 2;
export default function MusicPlayerScreen() {
const [index, setIndex] = useState(0)
const [isSeeking, setIsSeeking] = useState(false)
const [shouldPlayAtEndOfSeek, setShouldPlayAtEndOfSeek] = useState(false)
const [playbackInstance, setPlaybackInstance] = useState(null)
const [state, setState] = useState({
showVideo: false,
playbackInstanceName: LOADING_STRING,
loopingType: LOOPING_TYPE_ALL,
muted: false,
playbackInstancePosition: null,
playbackInstanceDuration: null,
shouldPlay: false,
isPlaying: false,
isBuffering: false,
isLoading: true,
fontLoaded: false,
shouldCorrectPitch: true,
volume: 1.0,
rate: 1.0,
videoWidth: DEVICE_WIDTH,
videoHeight: VIDEO_CONTAINER_HEIGHT,
poster: false,
useNativeControls: false,
fullscreen: false,
throughEarpiece: false
})
componentDidMount = () => {
Audio.setAudioModeAsync({
allowsRecordingIOS: false,
staysActiveInBackground: false,
interruptionModeIOS: InterruptionModeIOS.DoNotMix,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
playThroughEarpieceAndroid: false
});
(async () => {
await Font.loadAsync({
...MaterialIcons.font,
"cutive-mono-regular": require("../assets/fonts/CutiveMono-Regular.ttf")
});
setState({ fontLoaded: true });
})();
}
async function _loadNewPlaybackInstance(playing) {
if (playbackInstance != null) {
await playbackInstance.unloadAsync();
// playbackInstance.setOnPlaybackStatusUpdate(null);
setPlaybackInstance(null);
}
const source = { uri: PLAYLIST[index].uri };
const [initialStatus, setInitialStatus] = useState({
shouldPlay: playing,
rate: state.rate,
shouldCorrectPitch: state.shouldCorrectPitch,
volume: state.volume,
isMuted: state.muted,
isLooping: state.loopingType === LOOPING_TYPE_ONE
// // UNCOMMENT THIS TO TEST THE OLD androidImplementation:
// androidImplementation: 'MediaPlayer',
});
if (PLAYLIST[index].isVideo) {
await _video.loadAsync(source, initialStatus);
// _video.onPlaybackStatusUpdate(_onPlaybackStatusUpdate);
setPlaybackInstance = _video;
const status = await _video.getStatusAsync();
} else {
const { sound, status } = await Audio.Sound.createAsync(
source,
initialStatus,
_onPlaybackStatusUpdate
);
setPlaybackInstance = sound;
}
_updateScreenForLoading(false);
}
_mountVideo = component => {
_video = component;
_loadNewPlaybackInstance(false);
};
_updateScreenForLoading = (isLoading) => {
if (isLoading) {
setState({
showVideo: false,
isPlaying: false,
playbackInstanceName: LOADING_STRING,
playbackInstanceDuration: null,
playbackInstancePosition: null,
isLoading: true
});
} else {
setState({
playbackInstanceName: PLAYLIST[index].name,
showVideo: PLAYLIST[index].isVideo,
isLoading: false
});
}
};
_onPlaybackStatusUpdate = status => {
if (status.isLoaded) {
setState({
playbackInstancePosition: status.positionMillis,
playbackInstanceDuration: status.durationMillis,
shouldPlay: status.shouldPlay,
isPlaying: status.isPlaying,
isBuffering: status.isBuffering,
rate: status.rate,
muted: status.isMuted,
volume: status.volume,
loopingType: status.isLooping ? LOOPING_TYPE_ONE : LOOPING_TYPE_ALL,
shouldCorrectPitch: status.shouldCorrectPitch
});
if (status.didJustFinish && !status.isLooping) {
_advanceIndex(true);
_updatePlaybackInstanceForIndex(true);
}
} else {
if (status.error) {
console.log(`FATAL PLAYER ERROR: ${status.error}`);
}
}
};
_onLoadStart = () => {
console.log(`ON LOAD START`);
};
_onLoad = status => {
console.log(`ON LOAD : ${JSON.stringify(status)}`);
};
_onError = error => {
console.log(`ON ERROR : ${error}`);
};
_onReadyForDisplay = event => {
const widestHeight =
(DEVICE_WIDTH * event.naturalSize.height) / event.naturalSize.width;
if (widestHeight > VIDEO_CONTAINER_HEIGHT) {
setState({
videoWidth:
(VIDEO_CONTAINER_HEIGHT * event.naturalSize.width) /
event.naturalSize.height,
videoHeight: VIDEO_CONTAINER_HEIGHT
});
} else {
setState({
videoWidth: DEVICE_WIDTH,
videoHeight:
(DEVICE_WIDTH * event.naturalSize.height) / event.naturalSize.width
});
}
};
_onFullscreenUpdate = event => {
console.log(
`FULLSCREEN UPDATE : ${JSON.stringify(event.fullscreenUpdate)}`
);
};
_advanceIndex = (forward) => {
setIndex((index + (forward ? 1 : PLAYLIST.length - 1)) % PLAYLIST.length);
}
async function _updatePlaybackInstanceForIndex(playing) {
_updateScreenForLoading(true);
setState({
videoWidth: DEVICE_WIDTH,
videoHeight: VIDEO_CONTAINER_HEIGHT
});
_loadNewPlaybackInstance(playing);
}
_onPlayPausePressed = () => {
if (playbackInstance != null) {
if (state.isPlaying) {
playbackInstance.pauseAsync();
} else {
playbackInstance.playAsync();
}
}
};
_onStopPressed = () => {
if (playbackInstance != null) {
playbackInstance.stopAsync();
}
};
_onForwardPressed = () => {
if (playbackInstance != null) {
_advanceIndex(true);
_updatePlaybackInstanceForIndex(state.shouldPlay);
}
};
_onBackPressed = () => {
if (playbackInstance != null) {
_advanceIndex(false);
_updatePlaybackInstanceForIndex(state.shouldPlay);
}
};
_onMutePressed = () => {
if (playbackInstance != null) {
playbackInstance.setIsMutedAsync(!state.muted);
}
};
_onLoopPressed = () => {
if (playbackInstance != null) {
playbackInstance.setIsLoopingAsync(
state.loopingType !== LOOPING_TYPE_ONE
);
}
};
_onVolumeSliderValueChange = value => {
if (playbackInstance != null) {
playbackInstance.setVolumeAsync(value);
}
};
_trySetRate = async (rate, shouldCorrectPitch) => {
if (playbackInstance != null) {
try {
await playbackInstance.setRateAsync(rate, shouldCorrectPitch);
} catch (error) {
// Rate changing could not be performed, possibly because the client's Android API is too old.
}
}
};
_onRateSliderSlidingComplete = async value => {
_trySetRate(value * RATE_SCALE, state.shouldCorrectPitch);
};
_onPitchCorrectionPressed = async value => {
_trySetRate(state.rate, !state.shouldCorrectPitch);
};
_onSeekSliderValueChange = value => {
if (playbackInstance != null && !isSeeking) {
setIsSeeking(true);
shouldPlayAtEndOfSeek = state.shouldPlay;
playbackInstance.pauseAsync();
}
};
_onSeekSliderSlidingComplete = async value => {
if (playbackInstance != null) {
setIsSeeking(false);
const seekPosition = value * state.playbackInstanceDuration;
if (shouldPlayAtEndOfSeek) {
playbackInstance.playFromPositionAsync(seekPosition);
} else {
playbackInstance.setPositionAsync(seekPosition);
}
}
};
_getSeekSliderPosition = () => {
if (
playbackInstance != null &&
state.playbackInstancePosition != null &&
state.playbackInstanceDuration != null
) {
return (
state.playbackInstancePosition /
state.playbackInstanceDuration
);
}
return 0;
}
_getMMSSFromMillis = (millis) => {
const totalSeconds = millis / 1000;
const seconds = Math.floor(totalSeconds % 60);
const minutes = Math.floor(totalSeconds / 60);
const padWithZero = number => {
const string = number.toString();
if (number < 10) {
return "0" + string;
}
return string;
};
return padWithZero(minutes) + ":" + padWithZero(seconds);
}
_getTimestamp = () => {
if (
playbackInstance != null &&
state.playbackInstancePosition != null &&
state.playbackInstanceDuration != null
) {
return `${_getMMSSFromMillis(
state.playbackInstancePosition
)} / ${_getMMSSFromMillis(state.playbackInstanceDuration)}`;
}
return "";
}
_onPosterPressed = () => {
setState({ poster: !state.poster });
};
_onUseNativeControlsPressed = () => {
setState({ useNativeControls: !state.useNativeControls });
};
_onFullscreenPressed = () => {
try {
_video.presentFullscreenPlayer();
} catch (error) {
console.log(error.toString());
}
};
_onSpeakerPressed = () => {
setState(
state => {
return { throughEarpiece: !state.throughEarpiece };
},
() =>
Audio.setAudioModeAsync({
allowsRecordingIOS: false,
interruptionModeIOS: InterruptionModeIOS.DoNotMix,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
playThroughEarpieceAndroid: state.throughEarpiece
})
);
};
return !state.fontLoaded ? (
<View style={styles.emptyContainer} />
) : (
<View style={styles.container}>
<View />
<View style={styles.nameContainer}>
<Text style={[styles.text, { fontFamily: "cutive-mono-regular" }]}>
{state.playbackInstanceName}
</Text>
</View>
<View style={styles.space} />
<View style={styles.videoContainer}>
<Video
ref={_mountVideo}
style={[
styles.video,
{
opacity: state.showVideo ? 1.0 : 0.0,
width: state.videoWidth,
height: state.videoHeight
}
]}
resizeMode={ResizeMode.CONTAIN}
onPlaybackStatusUpdate={_onPlaybackStatusUpdate}
onLoadStart={_onLoadStart}
onLoad={_onLoad}
onError={_onError}
onFullscreenUpdate={_onFullscreenUpdate}
onReadyForDisplay={_onReadyForDisplay}
useNativeControls={state.useNativeControls}
/>
</View>
<View
style={[
styles.playbackContainer,
{
opacity: state.isLoading ? DISABLED_OPACITY : 1.0
}
]}
>
<Slider
style={styles.playbackSlider}
trackImage={() => { require("../assets/images/track_1.png") }}
thumbImage={() => { require("../assets/images/thumb_1.png") }}
value={_getSeekSliderPosition()}
onValueChange={_onSeekSliderValueChange}
onSlidingComplete={_onSeekSliderSlidingComplete}
disabled={state.isLoading}
/>
<View style={styles.timestampRow}>
<Text
style={[
styles.text,
styles.buffering,
{ fontFamily: "cutive-mono-regular" }
]}
>
{state.isBuffering ? BUFFERING_STRING : ""}
</Text>
<Text
style={[
styles.text,
styles.timestamp,
{ fontFamily: "cutive-mono-regular" }
]}
>
{_getTimestamp()}
</Text>
</View>
</View>
<View
style={[
styles.buttonsContainerBase,
styles.buttonsContainerTopRow,
{
opacity: state.isLoading ? DISABLED_OPACITY : 1.0
}
]}
>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onBackPressed}
disabled={state.isLoading}
>
{ICON_BACK_BUTTON}
</TouchableHighlight>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onPlayPausePressed}
disabled={state.isLoading}
>
<Image
style={styles.button}
source={require(
state.isPlaying
? "../assets/images/pause_button.png"
: "../assets/images/play_button.png"
)}
/>
</TouchableHighlight>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onStopPressed}
disabled={state.isLoading}
>
{ICON_STOP_BUTTON}
</TouchableHighlight>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onForwardPressed}
disabled={state.isLoading}
>
{ICON_FORWARD_BUTTON}
</TouchableHighlight>
</View>
<View
style={[
styles.buttonsContainerBase,
styles.buttonsContainerMiddleRow
]}
>
<View style={styles.volumeContainer}>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onMutePressed}
>
<Image
style={styles.button}
source={require(
state.muted
? "../assets/images/muted_button.png"
: "../assets/images/unmuted_button.png"
)}
/>
</TouchableHighlight>
<Slider
style={styles.volumeSlider}
trackImage={() => { require("../assets/images/track_1.png") }}
thumbImage={() => { require("../assets/images/thumb_2.png") }}
value={1}
onValueChange={_onVolumeSliderValueChange}
/>
</View>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onLoopPressed}
>
<Image
style={styles.button}
source={LOOPING_TYPE_ICONS[state.loopingType]}
/>
</TouchableHighlight>
</View>
<View
style={[
styles.buttonsContainerBase,
styles.buttonsContainerBottomRow
]}
>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={() => _trySetRate(1.0, state.shouldCorrectPitch)}
>
<View style={styles.button}>
<Text
style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
>
Rate:
</Text>
</View>
</TouchableHighlight>
<Slider
style={styles.rateSlider}
trackImage={() => { require("../assets/images/track_1.png") }}
thumbImage={() => { require("../assets/images/thumb_1.png") }}
value={state.rate / RATE_SCALE}
onSlidingComplete={_onRateSliderSlidingComplete}
/>
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onPitchCorrectionPressed}
>
<View style={styles.button}>
<Text
style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
>
PC: {state.shouldCorrectPitch ? "yes" : "no"}
</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
onPress={_onSpeakerPressed}
underlayColor={BACKGROUND_COLOR}
>
<MaterialIcons
name={
state.throughEarpiece
? ICON_THROUGH_EARPIECE
: ICON_THROUGH_SPEAKER
}
size={32}
color="black"
/>
</TouchableHighlight>
</View>
<View />
{state.showVideo ? (
<View>
<View
style={[
styles.buttonsContainerBase,
styles.buttonsContainerTextRow
]}
>
<View />
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onPosterPressed}
>
<View style={styles.button}>
<Text
style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
>
Poster: {state.poster ? "yes" : "no"}
</Text>
</View>
</TouchableHighlight>
<View />
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onFullscreenPressed}
>
<View style={styles.button}>
<Text
style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
>
Fullscreen
</Text>
</View>
</TouchableHighlight>
<View />
</View>
<View style={styles.space} />
<View
style={[
styles.buttonsContainerBase,
styles.buttonsContainerTextRow
]}
>
<View />
<TouchableHighlight
underlayColor={BACKGROUND_COLOR}
style={styles.wrapper}
onPress={_onUseNativeControlsPressed}
>
<View style={styles.button}>
<Text
style={[styles.text, { fontFamily: "cutive-mono-regular" }]}
>
Native Controls:{" "}
{state.useNativeControls ? "yes" : "no"}
</Text>
</View>
</TouchableHighlight>
<View />
</View>
</View>
) : null}
</View>
);
}
const styles = StyleSheet.create({
emptyContainer: {
alignSelf: "stretch",
backgroundColor: BACKGROUND_COLOR
},
container: {
flex: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
alignSelf: "stretch",
backgroundColor: BACKGROUND_COLOR
},
wrapper: {},
nameContainer: {
height: FONT_SIZE
},
space: {
height: FONT_SIZE
},
videoContainer: {
height: VIDEO_CONTAINER_HEIGHT
},
video: {
maxWidth: DEVICE_WIDTH
},
playbackContainer: {
flex: 1,
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
alignSelf: "stretch",
minHeight: 18 * 2.0,
maxHeight: 19 * 2.0
},
playbackSlider: {
alignSelf: "stretch"
},
timestampRow: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
alignSelf: "stretch",
minHeight: FONT_SIZE
},
text: {
fontSize: FONT_SIZE,
minHeight: FONT_SIZE
},
buffering: {
textAlign: "left",
paddingLeft: 20
},
timestamp: {
textAlign: "right",
paddingRight: 20
},
button: {
backgroundColor: BACKGROUND_COLOR
},
buttonsContainerBase: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between"
},
buttonsContainerTopRow: {
maxHeight: 51,
minWidth: DEVICE_WIDTH / 2.0,
maxWidth: DEVICE_WIDTH / 2.0
},
buttonsContainerMiddleRow: {
maxHeight: 58,
alignSelf: "stretch",
paddingRight: 20
},
volumeContainer: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
minWidth: DEVICE_WIDTH / 2.0,
maxWidth: DEVICE_WIDTH / 2.0
},
volumeSlider: {
width: DEVICE_WIDTH / 2.0 - 67
},
buttonsContainerBottomRow: {
maxHeight: 19,
alignSelf: "stretch",
paddingRight: 20,
paddingLeft: 20
},
rateSlider: {
width: DEVICE_WIDTH / 2.0
},
buttonsContainerTextRow: {
maxHeight: FONT_SIZE,
alignItems: "center",
paddingRight: 20,
paddingLeft: 20,
minWidth: DEVICE_WIDTH,
maxWidth: DEVICE_WIDTH
}
});
I was searched all on google, github, and other questions in stackoverflow but I still haven't found the desired answer. I'm a newbie hoping someone can help me!
I am having trouble making this work and I have no Idea why this is happening.
I am using #alentoma/react-native-selectable-text inside a scrollView but when the text get focus or I click on it. The scrollview bounce up and down once.
#alentoma/react-native-selectable-text is using ReactTextView on android.
Here is the code
<ScrollView
ref={(c) => {
if (c) {
ref.current = c;
scrollTo();
}
}}
onScrollBeginDrag={() => {
clearTimeout(onScrollTimeout.current);
scrollable.current = true;
}}
onScrollEndDrag={() => {
clearTimeout(onScrollTimeout.current);
onScrollTimeout.current = setTimeout(() => {
scrollable.current = false;
}, 1500);
}}
onScroll={({ nativeEvent }) => {
if (!viewPlayer && scrollable.current) {
onScroll(nativeEvent);
} else return false
}}
contentOffset={{ y: state.content.scrollPos, x: 0 }} contentContainerStyle={!viewPlayer ? { paddingBottom: 400, paddingTop: 150, minHeight: windowHeight } : {}}
>
{showAds && !viewPlayer ? (
<Banner bannerSize="smartBannerPortrait" />
) : null}
<View style={{ minHeight: windowHeight + (!viewPlayer ? 500 : 0), flexDirection: 'row' }} incBackGround={false}>
<Text
key={state.content.key + "Text"}
style={{
width: windowWidth,
padding: 0,
margin: 0,
minHeight: windowHeight + (!viewPlayer ? 500 : -60),
paddingTop: (viewPlayer ? 60 : 0),
flex: 1,
flexWrap: 'wrap'
}} onPress={txtClick}>
<SelectableText
key={state.content.key}
highLights={detaliItemSettings.detaliItemSettingsEdit?.filter(x => x.editWith.length > 0 && x.highlightColor && x.highlightColor.length > 1).map(x => { return { text: x.editWith, color: x.highlightColor } })}
menuItems={globalContext.currentLanguage.novelReaderTextSelectionMenus.map(x => x.text)}
onSelection={(item) => {
disableClick.current = false;
handleContextMenu(item);
}}
onLongPress={() => {
disableClick.current = true
}}
onPress={txtClick}
contentWidth={windowWidth}
value={state.content} />
</Text>
</View>
</ScrollView>
I have implemented a list like Instagram explore page using this question React native grid like instagram
but it is not performant at all with the layout I have. is there any other way to implement this?
the other problem is in re-rendering different items for some rows when scrolling, I manage to reduce it by changing the conditions in which item should be rendered but there is still a problem.
I have uploaded a video of my app here: https://youtu.be/gP7kbwKDeNA
I chunck my content to 3 item arrays and then pass it to this component this is my code:
const albumIcon = require("../../assets/icons/stream.png");
const videoIcon = require("../../assets/icons/play.png");
const { width } = Dimensions.get("window");
let isLeft = false;
let url = null;
let icon = null;
function CustomGridList(props){
const onRefresh = (loadMore = false) => {
if (loadMore === true) {
loadMore();
} else {
props.refresh(props.query);
}
};
const loadMore = () => {
props.refresh(props.query, true);
};
const getItemKey = (index,item) => {
return "item".concat(index);
};
const _renderPhoto = (item, isLarge) => {
url = null;
icon = null;
if (item && item.videos.length > 0) {
url = item.videos[0].thumbnail;
icon = videoIcon;
} else if (item && item.images.length > 1) {
url = item.images[0].url;
icon = albumIcon;
} else if(item && item.images.length > 0){
url = item.images[0].url;
}else{
url = 'https://castlebba.co.uk/wp-content/uploads/2017/04/default-image-620x600.jpg'
}
return (
<TouchableOpacity style={{marginRight: 1}}>
<CachedImage
source={{uri: url}}
style={{ flex: 1}}
afterClickFunc={() => console.log('clicked')}
useTouchableOpacity
width={isLarge ? (width*(2/3)+4) : width/3}
height={isLarge ? 200: 100}
/>
<View
style={{
position: 'absolute',
top: 4,
right: 4,
}}
>
<CustomIcon boldColor={'#fff'} icon={icon} />
</View>
</TouchableOpacity>
);
};
const _renderRow = (index, item) => {
console.log(index, item );
console.log('indexable -> ', ++index%3 );
let item1 = item[0] !== null? item[0]: null;
let item2 = item[1] !== null? item[1]: null;
let item3 = item[2] !== null? item[2]: null;
if((index+1) % 3 === 0){
if (isLeft){
isLeft = false;
}else{
isLeft = true;
}
return (
<View style={{flexDirection: isLeft? 'row' : 'row-reverse', height: 200}}>
{_renderPhoto(item1, true)}
<View style={{flexDirection: 'column'}}>
<View style={{flex:1, height: 100}}>
{_renderPhoto(item2,false)}
</View>
<View style={{ flex:1, height: 100}}>
{_renderPhoto(item3,false)}
</View>
</View>
</View>
);
}else{
return (
<View style={{flexDirection: 'row', height: 100}}>
{_renderPhoto(item1, false)}
{_renderPhoto(item2, false)}
{_renderPhoto(item3, false)}
</View>
);
}
}
const { loading, content } = props;
if(loading && !content) {
return (
<View style={{ marginTop: 30, height: 100, alignItems: 'center' }}>
<ActivityIndicator
color={getThemeStyle().color_main}
size={'large'}
/>
</View>
);
}
if (content === null) {
return <View />;
}
return (
<View style={styles.flatListUserListWrapper}>
<View style={styles.albumContainer}>
<CustomFlatList
{...props}
showsVerticalScrollIndicator={false}
style={[
styles.albumContainer,
content.length > 0 ? { margin: 1 } : {},
]}
content={content}
renderItem={({ index,item }) => _renderRow(index,item)}
itemSeparator={() => <View style={{ width: '100%', height: 1 }} />}
keyExtractor={(index,item) => getItemKey(item)}
enableLoadMore={true}
loading={loading}
onRefresh={() => onRefresh()}
loadMore={()=>loadMore()}
pagePostType={"search"}
canSendPost={false}
/>
</View>
</View>
);
}
CustomGridList.propTypes = {
error: PropTypes.string,
loading: PropTypes.bool,
refresh: PropTypes.func,
navigation: PropTypes.object,
content: PropTypes.array,
query: PropTypes.string,
navigateTo: PropTypes.func, //TODO implement this one on click
};
export default CustomGridList = React.memo(CustomGridList);
I separated different rows and put them in different files then changed the way I was selecting rows with big tiles, it got a little bit better but still rerendering more than it's supposed to. I'm not doing anything dynamicly.
const albumIcon = require("../../../assets/icons/stream.png");
const videoIcon = require("../../../assets/icons/play.png");
const { width } = Dimensions.get("window");
let isLeft = false;
let url = null;
let icon = null;
function CustomGridList(props){
const onRefresh = (loadMore = false) => {
if (loadMore === true) {
loadMore();
} else {
props.refresh(props.query);
}
};
const loadMore = () => {
props.refresh(props.query, true);
};
const getItemKey = (index,item) => {
return "item".concat(index);
};
const _renderRow = (index, item) => {
if((index+1)%6 === 0){
return(<LargeTiledRow item={item} width={width} reverse={false} navigateTo={props.navigateTo}/>)
}else if((index+4)%6 === 0){
return(<LargeTiledRow item={item} width={width} reverse={true} navigateTo={props.navigateTo}/>)
}else{
return (<GridSimpleRow item={item} width={width} navigateTo={props.navigateTo}/>);
}
}
const { loading, content } = props;
if(loading && !content) {
return (
<View style={{ marginTop: 30, height: 100, alignItems: 'center' }}>
<ActivityIndicator
color={getThemeStyle().color_main}
size={'large'}
/>
</View>
);
}
if (content === null) {
return <View />;
}
return (
<View style={styles.flatListUserListWrapper}>
<View style={styles.albumContainer}>
<CustomFlatList
{...props}
showsVerticalScrollIndicator={false}
style={[
styles.albumContainer,
content.length > 0 ? { margin: 1 } : {},
]}
content={content}
renderItem={({ index,item }) => _renderRow(index,item)}
itemSeparator={() => <View style={{ width: '100%', height: 1 }} />}
keyExtractor={(index,item) => getItemKey(item)}
enableLoadMore={true}
loading={loading}
onRefresh={() => onRefresh()}
loadMore={()=>loadMore()}
pagePostType={"search"}
canSendPost={false}
/>
</View>
</View>
);
}
function areEqual(prevProps, nextProps) {
prevProps.content === nextProps.content
}
export default CustomGridList = React.memo(CustomGridList,areEqual);
Topic: Video Flatlist in React Native. (Normal Videos / YouTube Videos)
Any one will be displayed according to the array data simultaneously. So it will be a long flatlist containing two types of video.
Sometimes many onPress event doesn't work due to performance issue
Major Issues:
App crashes when scrolling bottom to top
Performance is very poor
Above two are the major issue need to solve. Are there any ways to solve these issues?
What solutions can I try to improve Flatlist performance and to solve Video Flatlist crashing issue?
Below is my code in App.js
import React from "react";
import {
StyleSheet,
Text,
View,
SafeAreaView,
FlatList,
TouchableWithoutFeedback,
Dimensions
} from "react-native";
import Video from "react-native-video";
import ProgressBar from "react-native-progress/Bar";
import YouTube from 'react-native-youtube';
import LightVideo from "./sample.mp4";
import Icon from "react-native-vector-icons/FontAwesome";
// console.disableYellowBox = true;
function secondsToTime(time) {
return ~~(time / 60) + ":" + (time % 60 < 10 ? "0" : "") + time % 60;
}
const { width } = Dimensions.get("window");
const height = width * 0.5625;
const list = [
{
'type': 'YouTube'
},
{
'type': 'Normal'
},
{
'type': 'YouTube'
},
{
'type': 'Normal'
},
{
'type': 'Normal'
},
];
export default class RNVideo extends React.PureComponent {
state = {
video: [],
duration: [0, 0, 0, 0, 0],
progress: [0, 0, 0, 0, 0]
};
constructor(props) {
super(props);
this.player = [];
}
componentDidMount() {
this.progress_state();
this.duration_state();
this.video_pause_state(false);
}
play_pause_button = (index) => {
if (this.state.progress >= 1) {
this.player[index].seek(0);
}
var statevisible = this.state.video;
statevisible[index] = !statevisible[index];
this.setState({
video: statevisible
});
this.forceUpdate();
};
progress_bar_touch(e, index) {
const position = e.nativeEvent.locationX;
const progress = (position / 250) * this.state.duration[index];
this.player[index].seek(progress);
};
handleProgress(progress, index) {
var statevisible = this.state.progress;
statevisible[index] = progress.currentTime / this.state.duration[index];
this.setState({
progress: statevisible
});
this.forceUpdate();
};
handleLoad(meta, index) {
var statevisible = this.state.duration;
statevisible[index] = meta.duration;
this.setState({
duration: statevisible
});
this.forceUpdate();
};
onViewableItemsChanged = async ({ viewableItems }) => {
this.video_pause_state(true);
if (viewableItems != '' && viewableItems != null) {
var indexvalue = viewableItems[0]['index'];
var statevisible = this.state.video;
statevisible[indexvalue] = !statevisible[indexvalue];
this.setState({
video: statevisible
});
this.forceUpdate();
}
}
// shouldComponentUpdate() {
// return false
// }
video_pause_state(value) {
var statevisible = [];
for (var i = 0; i < list.length; i++) {
statevisible[i] = value;
}
this.setState({
video: statevisible
});
}
progress_state() {
var statevisible = [];
for (var i = 0; i < list.length; i++) {
statevisible[i] = 0;
}
this.setState({
progress: statevisible
});
}
duration_state() {
var statevisible = [];
for (var i = 0; i < list.length; i++) {
statevisible[i] = 0;
}
this.setState({
duration: statevisible
});
}
video_end(index) {
var statevisible1 = this.state.duration;
statevisible1[index] = 0;
var statevisible2 = this.state.progress;
statevisible2[index] = 0;
this.setState({
duration: statevisible1,
progress: statevisible2
});
this.play_pause_button(index);
}
keyExtractor = (item, index) => index.toString()
renderItem = ({ item, index }) => (
<>
{item.type != 'YouTube' ?
<>
<View style={styles.container}>
<View>
<Video
paused={this.state.video[index]}
muted={this.state.video[index]}
source={LightVideo}
// controls={true}
// repeat={true}
style={styles.videos}
resizeMode="cover"
onLoad={(meta) => this.handleLoad(meta, index)}
onProgress={(progress) => this.handleProgress(progress, index)}
onEnd={() => this.video_end(index)}
ref={(ref) => {
this.player[index] = ref
}}
/>
<View style={styles.controls}>
<TouchableWithoutFeedback onPress={(event) => this.play_pause_button(index)}>
<Icon name={!this.state.video[index] ? "pause" : "play"} size={30} color="#FFF" />
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={(event) => this.progress_bar_touch(event, index, this)}>
<View>
<ProgressBar
progress={this.state.progress[index]}
color="#FFF"
unfilledColor="rgba(255,255,255,.5)"
borderColor="#FFF"
width={230}
height={20}
/>
</View>
</TouchableWithoutFeedback>
<Text style={styles.duration}>
{secondsToTime(Math.floor(this.state.progress[index] * this.state.duration[index]))}
</Text>
</View>
</View>
</View>
</>
:
<>
<View style={{ marginTop: 50 }}>
<YouTube
apiKey="YOUR_API_KEY"
videoId="YOUR_YOUTUBE_VIDEO_ID_FROM_URL"
play={this.state.video[index]}
style={styles.videos}
/>
</View>
</>
}
</>
)
render() {
return (
<>
<SafeAreaView style={{ flex: 1 }}>
<FlatList
keyExtractor={this.keyExtractor}
data={list}
renderItem={this.renderItem}
extraData={this.state}
onViewableItemsChanged={this.onViewableItemsChanged}
removeClippedSubviews={true}
maxToRenderPerBatch={3}
initialNumToRender={3}
legacyImplementation={true}
/>
</SafeAreaView>
</>
);
}
}
const wid = "95%";
const styles = StyleSheet.create({
container: {
flex: 1,
marginBottom: 60,
marginTop: 60,
},
controls: {
backgroundColor: "rgba(0, 0, 0, 0.5)",
height: 48,
left: 0,
bottom: 0,
right: 0,
position: "absolute",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-around",
width: wid,
margin: 8,
borderBottomLeftRadius: 10,
borderBottomRightRadius: 10,
paddingHorizontal: 10,
},
mainButton: {
marginRight: 15,
},
duration: {
color: "#FFF",
marginLeft: 15,
},
videos: {
width: wid,
backgroundColor: '#ccc',
borderRadius: 10,
overflow: 'hidden',
margin: 8,
height
},
});
Update
I have come up with this final solution. But not able to do autoplay. Need ideas on this.
import React from 'react';
import { View, StyleSheet, SafeAreaView } from 'react-native';
import YouTube from 'react-native-youtube';
import { OptimizedFlatList } from 'react-native-optimized-flatlist'
import VideoPlayer from 'react-native-video-player';
const list = [
{
'type': 'Normal'
},
{
'type': 'YouTube'
},
{
'type': 'Normal'
},
{
'type': 'Normal'
},
{
'type': 'YouTube'
},
{
'type': 'Normal'
},
{
'type': 'Normal'
},
{
'type': 'Normal'
},
];
export default class App extends React.PureComponent {
constructor() {
super();
this.player = [];
this.state = {
video: { width: undefined, height: undefined, duration: undefined },
thumbnailUrl: undefined,
videoUrl: undefined,
};
}
keyExtractor = (item, index) => index.toString()
renderItem = ({ item, index }) => (
<>
{item.type != 'YouTube' ?
<>
<View style={styles.videos}>
<VideoPlayer
// autoplay
// pauseOnPress
video={{ uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', type: 'mp4' }}
resizeMode={'cover'}
ref={r => this.player[index] = r}
/>
</View>
</>
:
<>
<View style={{ marginTop: 50 }}>
<YouTube
apiKey="YOUR_API_KEY"
videoId="3NQRhE772b0"
style={{...styles.videos,...styles.videos1}}
/>
</View>
</>
}
</>
)
onViewableItemsChanged = async ({ viewableItems }) => {
if (viewableItems != '' && viewableItems != null) {
var indexvalue = viewableItems[0]['index'];
indexvalue++;
if (indexvalue != 1) {
if (!this.player[indexvalue].state.isPlaying) {
this.player[indexvalue].pause();
} else {
this.player[indexvalue].resume();
}
}
}
}
render() {
return (
<>
<SafeAreaView style={{ flex: 1 }}>
<OptimizedFlatList
keyExtractor={this.keyExtractor}
data={list}
renderItem={this.renderItem}
removeClippedSubviews={true}
onViewableItemsChanged={this.onViewableItemsChanged}
/>
</SafeAreaView>
</>
);
}
}
const styles = StyleSheet.create({
videos: {
width: "95%",
backgroundColor: '#ccc',
borderRadius: 10,
overflow: 'hidden',
margin: 10,
marginBottom: 20,
marginTop: 20
},
videos1: {
height: 250
}
});
I have a Child Component that needs to update when a click event occurs in the Parent Component, Below is the code and i need startLoader function to be called.
Child Component:
class Footer extends Component {
constructor(props) {
super(props);
this.state = {
showText: true,
showLoader:false,
};
}
startLoader = () => {
this.setState({
showText:false,
showLoader:true
});
}
finishLoader = () => {
this.setState({
showText:false,
showLoader:true
});
}
render() {
return (<View style={styles.container}>
{ this.state.showText ? <Text style={styles.text}>Load More</Text> : null }
{ this.state.showLoader ? <Image style={{width: 30, height: 30}} source={require('./images/loader.gif')}/> : null }
</View>);
}
}
Parent Component:
class Social extends Component {
constructor(props) {
super(props);
var tabLabel = this.props.tabLabel;
var dataSource = new ListView.DataSource({rowHasChanged: (row1, row2) => row1 !== row2});
this.state = {
text: 'Search ' + tabLabel + ' here...' ,
textInput: '',
dataSource:dataSource,
height:height,
searchHeight: 40,
nextPageToken: '',
arrVideos: new Array(),
listHeight: height,
};
}
renderRow(rowData: object, sectionID: number, rowID: number) {
return (
<TouchableHighlight
underlayColor = '#ddd'>
<View style ={styles.row}>
<View style={styles.imagecon}>
<Image style={{width: 100, height: 80}} source={{uri: rowData.thumbnail}} />
</View>
<View style={styles.textcon}>
<Text style={{ fontSize: 11, fontWeight: 'bold' }} > {rowData.title} </Text>
<Text style={{ fontSize: 11}} > {rowData.desc} </Text>
</View>
</View>
</TouchableHighlight>
)
}
doneClick = () => {
this.props.showTrans();
fetch("https://www.googleapis.com/youtube/v3/search?part=snippet&q=tupac&key=AIzaSyDFOz50C_XkuEh2T-2XTN9fqm_5xHYzbg8&pageToken=" + this.state.nextPageToken)
.then((response) => response.json())
.then((responseData) => {
console.log('fxbxfbxf' + responseData.toString());
console.log('ghghgh' + responseData["items"].length);
//let doc = new DomParser().parseFromString(html,'text/html')
for(var i = 0; i < responseData["items"].length; i++) {
var title = responseData["items"][i]["snippet"].title;
var thumbnail = responseData["items"][i]["snippet"]["thumbnails"]["default"]["url"];
var desc = responseData["items"][i]["snippet"]["description"];
var video = {
title : title,
thumbnail: thumbnail,
desc: desc
}
this.state.arrVideos.push(video);
}
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.state.arrVideos),
nextPageToken: responseData.nextPageToken,
listHeight: (120 * this.state.arrVideos.length) + 80,
});
this.props.hideTrans();
})
.done();
}
loadMore = () => {
this.refs['footer'].startLoader();
fetch("https://www.googleapis.com/youtube/v3/search?part=snippet&q=tupac&key=AIzaSyDFOz50C_XkuEh2T-2XTN9fqm_5xHYzbg8&pageToken=" + this.state.nextPageToken)
.then((response) => response.json())
.then((responseData) => {
console.log('fxbxfbxf' + responseData.toString());
console.log('ghghgh' + responseData["items"].length);
//let doc = new DomParser().parseFromString(html,'text/html')
for(var i = 0; i < responseData["items"].length; i++) {
var title = responseData["items"][i]["snippet"].title;
var thumbnail = responseData["items"][i]["snippet"]["thumbnails"]["default"]["url"];
var desc = responseData["items"][i]["snippet"]["description"];
var video = {
title : title,
thumbnail: thumbnail,
desc: desc
}
this.state.arrVideos.push(video);
}
this.setState({
dataSource: this.state.dataSource.cloneWithRows(this.state.arrVideos),
nextPageToken: responseData.nextPageToken,
listHeight: (120 * this.state.arrVideos.length) + 80,
});
this.props.hideTrans();
})
.done();
}
render() {
return (
<View style={styles.menu} >
<View style={{alignItems: 'flex-start',
flexDirection: 'row',
backgroundColor: '#90CAF9', height: this.state.searchHeight}} >
<Image style={{marginTop: 10 ,marginLeft: 10 , width: 20, height: 20}} source={require('./images/search.png')} />
<TextInput
onSubmitEditing={ this.doneClick }
style={{marginLeft: 5, color: '#757575', width: width - 80, height: 40, fontSize: 11,}}
onChangeText={(textInput) => this.setState({textInput})}
placeholder={this.state.text}
underlineColorAndroid="transparent"
/>
</View>
<View style={styles.sep} >
</View>
<ScrollView style={{flex:1}}>
<SGListView
stickyHeaderIndices={[]}
onEndReachedThreshold={1}
scrollRenderAheadDistance={1}
pageSize={1}
initialListSize={9}
renderScrollComponent={props => <RecyclerViewBackedScrollView {...props} />}
style = {{flex:1, flexDirection: 'column',
backgroundColor: 'white', height: this.state.listHeight}}
dataSource = {this.state.dataSource}
renderRow = {this.renderRow.bind(this)}
renderFooter={() => <TouchableOpacity onPress={() => this.loadMore() }><Footer ref='footer' /></TouchableOpacity>}
>
</SGListView>
</ScrollView>
</View>
);
}
}
Where Footer is the onpress that needs to update the child component. How can i do this?