ScrollView jump up and down on first click or focus - react-native

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>

Related

how to prevent absolutely positioned element moving up with the keyboard in react native

I have a container which contains a code editor and a collapsed live preview like this
<View style={style.editorWithLivePreview}>
{
questionObject
&& <CodeEditorsTab.Navigator>
{
Object.keys(questionObject.questionSetup.files)
.map((filePath, idx) => {
const { files } = memorizedWebkataQuestionState.questionObject.questionSetup;
const { name } = files[filePath];
return <CodeEditorsTab.Screen name={`${name}Tab`} key={idx} options={{ tabBarLabel: name }}>
{(props) => <CodeEditor {...props}
codeEditorWebViewRef={(el) => {
codeEditorsRefs.current[name] = el;
}}
style={style}
onload={onCodeEditorLoad}
styleStringForDocumentInsideWebView={styleStringForDocumentInsideWebView}
onCodeEditorChanged={(code) => onCodeEditorChange(name, code)}
/>}
</CodeEditorsTab.Screen>;
})
}
</CodeEditorsTab.Navigator>
}
<View style={[style.containerWithRevealerBtn, {
height: orientation === 'PORTRAIT' ? Dimensions.get('window').height - 130 : Dimensions.get('window').height - 105,
transform:
[{ translateX: livePreviewOpen ? 0 : Dimensions.get('window').width - 50 }],
zIndex: livePreviewOpen ? 1 : undefined,
}]}>
<RevealerButton
style={style}
revealed={livePreviewOpen}
toggleReveal={() => {
setLocalState((prev) => ({
...prev,
livePreviewOpen: !prev.livePreviewOpen,
}));
}}
/>
<View style={style.container}>
<SecondaryTabBar
style={style}
activeTab={secondaryActiveTab}
changeTab={(tabToSet) => {
setSecondaryActiveTab(tabToSet);
}}
/>
{
secondaryActiveTab === 'livePreview'
&& <LivePreview
style={style}
livePreviewWebViewRef={livePreviewWebViewRef}
codeState={codeState}
onMessageFromLivePreviewWebView={onMessageFromLivePreviewWebView}
/>
}
{
secondaryActiveTab === 'validatedResult'
&& <ValidatedResult
style={style}
evaluationDetails={evaluationDetails}
/>
}
</View>
</View>
</View>
The code is a bit too much due to the implementation details but this is how the app looks
marked in red is the container with the style.editorWithLivePreview.
marked in blue is the container with the style.containerWithRevealerBtn (the container which holds the revealer button and the live preview).
here are the styles
const style = {
editorWithLivePreview: {
flex:1,
position:'relative',
},
containerWithRevealerBtn: {
flex: 1,
position: 'absolute',
width: '100%',
bottom: 0,
flexDirection: 'row',
},
}
the live preview container is absolutely positioned relative to editorWithLivePreview.
When the keyboard opens the live preview container is also pushed up with the keyboard.
How to prevent this

Convert react-native class component to functional component with react-hooks

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!

react native flatlist is slow with dynamic items and a big data

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);

Correct way to create a react native component with 5 input text

I'm new to react native, so I could be taking the wrong approach, feel free to indicate a better architecture if there is one.
I'm building a component that displays 5 InputText. Each of those can only have a single digit, so when the text changed, I use the onChangeText event to move on to the next TextInput.
The problem where I get stuck is how to style the TextInput which is currently focused. I get the onFocus and onBlur events to fire, but when I setState for the style, nothing happens.
Here is the code:
class DigitFramedControl extends React.Component<
{},
{ showingError: boolean, errorString: string, value: string, backgroundColor: string },
> {
constructor(props: {}) {
super(props);
this.state = {
showingError: false,
errorString: 'Votre réponse est erroné. Veuillez réessayer.',
value: ' ',
};
}
handleDigitChanged(index: number, character: string) {
if (index > this.state.value.length) {
const error = new Error('index out of range');
throw error;
} else if (character.length === 0) {
// user pressed backspace, don't change the end value
// the digit is updated in the text input though
} else {
console.log('setting state');
this.setState((prevState, props) => ({
value: prevState.value.substr(0, index) + character + prevState.value.substr(index + 1),
}));
// go to next field
if (index < 4) {
this.digitTextInputAtIndex(index + 1).focus();
}
}
}
onDigitFocus = (index: number) => {
const textInput = this.digitTextInputAtIndex(index);
// this.style = [styles.digitFramedControlDigit, { backgroundColor: 'green' }];
textInput.setState({ style: [styles.digitFramedControlDigit, { backgroundColor: 'green' }] });
};
onDigitBlur = (index: number) => {
const textInput = this.digitTextInputAtIndex(index);
// this.style = [styles.digitFramedControlDigit, { backgroundColor: 'green' }];
textInput.setState({ style: [styles.digitFramedControlDigit, { backgroundColor: 'red' }] });
// this.style = [styles.digitFramedControlDigit, { backgroundColor: 'red' }];
};
digitTextInputAtIndex: TextInput = (index) => {
let returnValue = null;
switch (index) {
case 0:
returnValue = this.refs.digit0;
break;
case 1:
returnValue = this.refs.digit1;
break;
case 2:
returnValue = this.refs.digit2;
break;
case 3:
returnValue = this.refs.digit3;
break;
case 4:
returnValue = this.refs.digit4;
break;
}
return returnValue;
};
render() {
const sharedTextInputProps = {
maxLength: 1,
selectTextOnFocus: true,
selectionColor: '#ffffff00',
autoCapitalize: 'none',
};
return (
<View
style={{
flexDirection: 'column',
height: 100,
width: 300,
borderWidth: 1,
borderColor: '#090',
}}
>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
borderWidth: 1,
borderColor: '#600',
}}
>
<TextInput
{...sharedTextInputProps}
ref="digit0"
onChangeText={this.handleDigitChanged.bind(this, 0)}
onFocus={this.onDigitFocus.bind(this, 0)}
onBlur={this.onDigitBlur.bind(this, 0)}
style={[
styles.digitFramedControlDigit,
{ backgroundColor: this.state.backgroundColor },
]}
returnKeyType="next"
/>
<TextInput
ref="digit1"
style={styles.digitFramedControlDigit}
onChangeText={this.handleDigitChanged.bind(this, 1)}
returnKeyType="next"
onFocus={this.onDigitFocus.bind(this, 1)}
onBlur={this.onDigitBlur.bind(this, 1)}
style={[
styles.digitFramedControlDigit,
{ backgroundColor: this.state.backgroundColor },
]}
/>
<TextInput
ref="digit2"
style={styles.digitFramedControlDigit}
onChangeText={this.handleDigitChanged.bind(this, 2)}
onFocus={this.onDigitFocus.bind(this, 2)}
onBlur={this.onDigitBlur.bind(this, 2)}
style={[
styles.digitFramedControlDigit,
{ backgroundColor: this.state.backgroundColor },
]}
returnKeyType="next"
/>
<TextInput
ref="digit3"
style={styles.digitFramedControlDigit}
onChangeText={this.handleDigitChanged.bind(this, 3)}
onFocus={this.onDigitFocus.bind(this, 3)}
onBlur={this.onDigitBlur.bind(this, 3)}
returnKeyType="next"
/>
<TextInput
ref="digit4"
style={styles.digitFramedControlDigit}
onChangeText={this.handleDigitChanged.bind(this, 4)}
onFocus={this.onDigitFocus.bind(this, 4)}
onBlur={this.onDigitBlur.bind(this, 4)}
returnKeyType="done"
/>
</View>
<Text style={styles.digitFrameErrorString}>{this.state.errorString}</Text>
<Text style={styles.digitFrameErrorString}>{this.state.value}</Text>
</View>
);
}
}
Check out the below code.
Basically, it is storing the focused field's index in state as focusedInput.
Then when applying the styles to each Input, it conditionally applies the green background colour if the index matches the focused index.
Note: The code is untested, so expect syntax errors etc!
I've also refactored digitTextInputAtIndex to vastly simplify it ;)
class DigitFramedControl extends React.Component<
{},
{ showingError: boolean, errorString: string, value: string, backgroundColor: string },
> {
constructor(props: {}) {
super(props);
this.state = {
showingError: false,
errorString: 'Votre réponse est erroné. Veuillez réessayer.',
value: ' ',
focusedInput: null,
};
}
handleDigitChanged(index: number, character: string) {
if (index > this.state.value.length) {
const error = new Error('index out of range');
throw error;
} else if (character.length === 0) {
// user pressed backspace, don't change the end value
// the digit is updated in the text input though
} else {
console.log('setting state');
this.setState((prevState, props) => ({
value: prevState.value.substr(0, index) + character + prevState.value.substr(index + 1),
}));
// go to next field
if (index < 4) {
this.digitTextInputAtIndex(index + 1).focus();
}
}
}
onDigitFocus = (index: number) => {
this.setState({ focusedInput: index })
};
onDigitBlur = (index: number) => {
this.setState({ focusedInput: null })
};
digitTextInputAtIndex: TextInput = (index) => {
return this.refs[`digit${index}`]
};
render() {
const sharedTextInputProps = {
maxLength: 1,
selectTextOnFocus: true,
selectionColor: '#ffffff00',
autoCapitalize: 'none',
};
return (
<View
style={{
flexDirection: 'column',
height: 100,
width: 300,
borderWidth: 1,
borderColor: '#090',
}}
>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
borderWidth: 1,
borderColor: '#600',
}}
>
<TextInput
{...sharedTextInputProps}
ref="digit0"
onChangeText={this.handleDigitChanged.bind(this, 0)}
onFocus={this.onDigitFocus.bind(this, 0)}
onBlur={this.onDigitBlur.bind(this, 0)}
style={[
styles.digitFramedControlDigit,
this.state.focusedInput === 0 && { backgroundColor: 'green' }
]}
returnKeyType="next"
/>
<TextInput
ref="digit1"
onChangeText={this.handleDigitChanged.bind(this, 1)}
returnKeyType="next"
onFocus={this.onDigitFocus.bind(this, 1)}
onBlur={this.onDigitBlur.bind(this, 1)}
style={[
styles.digitFramedControlDigit,
this.state.focusedInput === 1 && { backgroundColor: 'green' }
]}
/>
<TextInput
ref="digit2"
onChangeText={this.handleDigitChanged.bind(this, 2)}
onFocus={this.onDigitFocus.bind(this, 2)}
onBlur={this.onDigitBlur.bind(this, 2)}
style={[
styles.digitFramedControlDigit,
this.state.focusedInput === 2 && { backgroundColor: 'green' }
]}
returnKeyType="next"
/>
<TextInput
ref="digit3"
onChangeText={this.handleDigitChanged.bind(this, 3)}
onFocus={this.onDigitFocus.bind(this, 3)}
onBlur={this.onDigitBlur.bind(this, 3)}
style={[
styles.digitFramedControlDigit,
this.state.focusedInput === 3 && { backgroundColor: 'green' }
]}
returnKeyType="next"
/>
<TextInput
ref="digit4"
onChangeText={this.handleDigitChanged.bind(this, 4)}
onFocus={this.onDigitFocus.bind(this, 4)}
onBlur={this.onDigitBlur.bind(this, 4)}
style={[
styles.digitFramedControlDigit,
this.state.focusedInput === 4 && { backgroundColor: 'green' }
]}
returnKeyType="done"
/>
</View>
<Text style={styles.digitFrameErrorString}>{this.state.errorString}</Text>
<Text style={styles.digitFrameErrorString}>{this.state.value}</Text>
</View>
);
}
}

center active tab in react-native-navbar

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.