I currently have a KeyboardAvoidingView with a hard-coded keyboardVerticalOffset of 64. This works fine on the iPhone but is about 20px short on the iPhone X.
The component looks like this:
<KeyboardAvoidingView behavior='padding' keyboardVerticalOffset={ 64 }>
<View style={ styles.messageList }>
...
</View>
<View style={ styles.messageInput }>
...
</View>
</KeyboardAvoidingView>
Is there a better way to determine what keyboardVerticalOffset should be than hard coding a value? Is there something else I could be doing differently with component placement? I'm open to any suggestions.
iPhone 8
iPhone X
This is caused by the status bar height being different for iphoneX. (you also get the same issue with other iphones if you toggle the 'in-call' status bar using ⌘Y in the simulator).
You can get the status bar height and use this to set the keyboardVerticalOffset value of the KeyboardAvoidingView. (in our case this was 44 + statusBarHeight)
import React, {Component} from 'react';
import {KeyboardAvoidingView, NativeModules, StatusBarIOS} from 'react-native';
const {StatusBarManager} = NativeModules;
export class IOSKeyboardAvoidingView extends Component {
state = {statusBarHeight: 0};
componentDidMount() {
StatusBarManager.getHeight((statusBarFrameData) => {
this.setState({statusBarHeight: statusBarFrameData.height});
});
this.statusBarListener = StatusBarIOS.addListener('statusBarFrameWillChange', (statusBarData) => {
this.setState({statusBarHeight: statusBarData.frame.height});
});
}
componentWillUnmount() {
this.statusBarListener.remove();
}
render() {
const {style, children} = this.props;
return (
<KeyboardAvoidingView
behavior="padding"
keyboardVerticalOffset={44 + this.state.statusBarHeight}
style={style}
>{children}
</KeyboardAvoidingView>
);
}
}
Please refer to : https://stackoverflow.com/a/51169574/10031014 for similar issues
I have used a custom component to overcome this situation.
import React from "react";
import {Animated, Keyboard} from "react-native";
export default class KeyboardAwareComponent extends React.Component {
constructor(props) {
super(props)
this.keyboardHeight = new Animated.Value(0);
}
componentWillMount () {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillHideSub.remove();
}
keyboardWillShow = (event) => {
Animated.parallel([
Animated.timing(this.keyboardHeight, {
duration: event.duration,
toValue: event.endCoordinates.height,
})
]).start();
};
keyboardWillHide = (event) => {
Animated.parallel([
Animated.timing(this.keyboardHeight, {
duration: event.duration,
toValue: 0,
})
]).start();
};
render(){
const {children, style, ...props} = this.props
return(
<Animated.View style={[{flex:1,alignItems:'center',paddingBottom: this.keyboardHeight},style]} {...props}>
{children}
</Animated.View>
);
}
}
Just use the component "KeyboardAwareComponent" as a root component of any page. It will automatically adjust the view when keyboard will show or hide.
Example:---
YourComponent extends React.Component{
render(){
<KeyboardAwareComponent>
{Your child views}
</KeyboardAwareComponent>
}
}
So I did a quick check, given my understanding of how to do this in native iOS, and it seems like in newer versions of react native, you can do this relatively easily.
There do seem to be a couple of options, depending on your flexibility needs.
First, have you tried using KeyboardAvoidView instead of a standard container View without specifying keyboardVerticalOffset?
Another option that gives you much more control (similar to what I would do in a native iOS app) is to use the Keyboard module to create listeners on the keyboard events.
componentWillMount () {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillChangeSub = Keyboard.addListener('keyboardWillChangeFrame', this.keyboardWillChange);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillChangeSub.remove();
this.keyboardWillHideSub.remove();
}
This would allow you to get the keyboard height from the event parameter:
keyboardWillShow = (event) => {
Animated.parallel([
Animated.timing(this.keyboardHeight, {
duration: event.duration,
toValue: event.endCoordinates.height,
}),
Animated.timing(this.imageHeight, {
duration: event.duration,
toValue: IMAGE_HEIGHT_SMALL,
}),
]).start();
};
Repeat something similar for keyboardWillChange and keyboardWillHide.
For a better, more detailed explanation of your options, see this page:
https://medium.freecodecamp.org/how-to-make-your-react-native-app-respond-gracefully-when-the-keyboard-pops-up-7442c1535580
I think the best first test would be to try to remove the keyboardVerticalOffset before trying to add code to handle the keboard events.
Related
const viewableWindowHeight = Dimensions.get('window').height - Header.HEIGHT - ???
How do I get the TabBar height?
What if the iPhone is X? How can I take that into account?
Solution 1
If you want to calculate viewable window height directly, then you can use the onLayout callback, for eg, on tab navigation each page,
render() {
return (
<View style={{ flex: 1}} onLayout={(event) => {
var {x, y, width, height} = event.nativeEvent.layout;
this.viewableWindowHeight=height;
// use height as viewableWindowHeight
}} />
<ScollView>
//Your scrollable contant
</ScrollView>
</View>
);
Solution 2
According to an issue in react navigation, you can't directly calculate the height of the bottom tab Bar. But if you wrap bottom tab bar into a view and then you can calculate that views height as bottom tab bar. Consider the example below
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { View } from 'react-native';
import { BottomTabBar } from 'react-navigation';
class TabBarComponent extends Component {
measure = () => {
if (this.tabBar) {
this.tabBar.measureInWindow(this.props.setTabMeasurement);
}
}
render() {
return (
<View
ref={(el) => { this.tabBar = el; }}
onLayout={this.measure}
>
<BottomTabBar {...this.props} />
</View>
);
}
}
function mapDispatchToProps(dispatch) {
return {
setTabMeasurement: (x, y, width, height) => dispatch({
type: 'SET_TAB_MEASUREMENT',
measurement: {
x, y, width, height,
},
}),
};
}
export default connect(null, mapDispatchToProps)(TabBarComponent);
Try this:
import { Dimensions, Platform } from 'react-native';
import {
getStatusBarHeight,
getBottomSpace,
} from 'react-native-iphone-x-helper';
import { Header } from 'react-navigation';
const { height } = Dimensions.get('window');
const stackHeaderHeight = Header.HEIGHT;
/* Taken from source code of react-navigation-tabs*/
const TAB_BAR_DEFAULT_HEIGHT = 49;
const TAB_BAR_COMPACT_HEIGHT = 29;
const TAB_BAR_HEIGHT = this.bottomTabBarRef._shouldUseHorizontalLabels() && !Platform.isPad
? TAB_BAR_COMPACT_HEIGHT
: TAB_BAR_DEFAULT_HEIGHT;
const marginTop = getStatusBarHeight() + stackHeaderHeight;
const marginBottom = getBottomSpace() + TAB_BAR_HEIGHT;
// < What you're after
const viewableWindowHight = height - marginTop - marginBottom;
FOR TBBAR
Height is changing between these two values >> TAB_BAR_COMPACT_HEIGHT, and TAB_BAR_DEFAULT_HEIGHT, according to a condition determined by this method:
According to react-navigation-tabs source code.
OR
You could set initialLayout to your TabNavigatorConfig as mentioned in the documentation:
initialLayout - Optional object containing the initial height and
width, can be passed to prevent the one frame delay in
react-native-tab-view rendering.
FOR IPHONE-X
You can access statusBar height, bottomSpace in Iphone-X safely though react-native-iphone-x-helper npm module
You can simply use SafeAreaView which will automatically set topBarHeight mainly for iPhoneX phones.
In the new version use
import { useHeaderHeight } from "react-navigation-stack";
console.log(useHeaderHeight());
I am trying to create a simple and standard chat window just like WhatsApp, Telegram, etc. Where when the inputbox on the bottom of the screen got focus, the keyboard appear and the inputbox goes right about the keyboard like this...
This is my code...
import React from 'react'
import {Actions} from 'react-native-router-flux'
import {ScrollView, View, TextInput, Text} from 'react-native'
import style from './style'
class Chat extends React.Component {
componentDidMount() {
Actions.refresh({title: 'Chat'})
}
render() {
return (
<View style={{flex:1}}>
<ScrollView>
<View style={{flex: 1}}>
<Text>Hello !</Text>
</View>
</ScrollView>
<View style={{borderWidth: 1, padding:15}}>
<TextInput/>
</View>
</View>
)
}
}
export default Chat
The result is this very simple one:
But when my inputbox got focus, the inputbox still got stucked in the bottom of the screen, behind the keyboard. Any suggestion for this ?
One way would be to change your input's position when keyboard shows and hides.
You need to add two listeners for when keyboard shows and hides:
import { Keyboard } from 'react-native';
constructor(props) {
super(props);
this.state = {
keyboardHeight: 0,
inputHeight: 40
}
}
componentDidMount() {
Keyboard.addListener('keyboardDidShow', this._keyboardDidShow.bind(this));
Keyboard.addListener('keyboardDidHide', this._keyboardDidHide.bind(this));
}
_keyboardDidShow(e) {
this.setState({keyboardHeight: e.endCoordinates.height});
}
_keyboardDidHide(e) {
this.setState({keyboardHeight: 0});
}
render() {
return (
<TextInput style={{marginBottom: keyboardHeight + inputHeight}} />
)
}
You can also add some animation to make it move smoothly.
Another Suggestion would be :
React-Native-Keyboard-Aware-Scroll-View
This is great when you have multiples text inputs and does handle some animation. Not perfect but did the job for me.
Its my first time animating with react native's animated library. Is it possible to animate in the center of the screen? If so, how can I do that?
This is my code below:
I basically had a 60x60 box on the top left corner of my screen to go down 1/2 the screen and over 1/2 the width. I want it to expand horizontally about 150 px across. The problem is that the box is not expanding in the center of the screen
/**
* Sample React Native App
* https://github.com/facebook/react-native
* #flow
*/
import React, { Component } from 'react';
import {
View,
Animated,
Dimensions,
Easing
} from 'react-native';
const {width,height} = Dimensions.get('window')
export default class App extends Component {
constructor(){
super()
this.state = {
width: new Animated.Value(60),//Animated.Value/ValueXY used to hook up animation attributes, also for single values
height: new Animated.Value(60),
animateXY : new Animated.ValueXY({x:0,y:0})//for vectors
}
}
componentWillMount(){
//called when an instance of a component is being created and inserted into the DOM
Animated.sequence([
//function where we add the ending results
Animated.timing(
this.state.animateXY,
{
toValue: {x:0, y:height/2},
duration: 2000
}),
Animated.timing(
this.state.animateXY,
{
toValue: {x:width/2, y:height/2},
duration: 2000,
}),
Animated.timing(
this.state.width,
{
toValue: 150,
duration: 3000
}),
]).start()
}
render() {
return (
<View>
<Animated.View
style={
{
width: this.state.width,
height:this.state.height,
backgroundColor:"teal",
position:'absolute',
top: this.state.animateXY.y,
left: this.state.animateXY.x,
}
}
/>
</View>
);
}
}
Any advice will do! thank you!
Jemma
You are not updating the height. Maybe in your case you can use the width only since it’s a square
I am working on tutorial for React Native navigation. I found out that all layout starts loading from top of screen instead of below of the status bar. This causes most layouts to overlap with the status bar. I can fix this by adding a padding to the view when loading them. Is this the actual way to do it? I don' think manually adding padding is an actual way to solve it. Is there a more elegant way to fix this?
import React, { Component } from 'react';
import { View, Text, Navigator } from 'react-native';
export default class MyScene extends Component {
static get defaultProps() {
return {
title : 'MyScene'
};
}
render() {
return (
<View style={{padding: 20}}> //padding to prevent overlap
<Text>Hi! My name is {this.props.title}.</Text>
</View>
)
}
}
Below shows the screenshots before and after the padding is added.
Now you can use SafeAreaView which is included in React Navigation:
<SafeAreaView>
... your content ...
</SafeAreaView>
There is a very simple way to fix this. Make a component.
You can create a StatusBar component and call it first after the first view wrapper in your parent components.
Here is the code for the one I use:
'use strict'
import React, {Component} from 'react';
import {View, Text, StyleSheet, Platform} from 'react-native';
class StatusBarBackground extends Component{
render(){
return(
<View style={[styles.statusBarBackground, this.props.style || {}]}> //This part is just so you can change the color of the status bar from the parents by passing it as a prop
</View>
);
}
}
const styles = StyleSheet.create({
statusBarBackground: {
height: (Platform.OS === 'ios') ? 18 : 0, //this is just to test if the platform is iOS to give it a height of 18, else, no height (Android apps have their own status bar)
backgroundColor: "white",
}
})
module.exports= StatusBarBackground
After doing this and exporting it to your main component, call it like this:
import StatusBarBackground from './YourPath/StatusBarBackground'
export default class MyScene extends Component {
render(){
return(
<View>
<StatusBarBackground style={{backgroundColor:'midnightblue'}}/>
</View>
)
}
}
I tried a more simple way for this.
We can get the height of Status Bar on android and use SafeAreaView along with it to make the code work on both platforms.
import { SafeAreaView, StatusBar, Platform } from 'react-native';
If we log out Platform.OS and StatusBar.currentHeight we get the logs,
console.log('Height on: ', Platform.OS, StatusBar.currentHeight);
Height on: android 24 and
Height on: android 24
We can now optionally add margin/padding to our container view using
paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : 0
The final code in App.js is below:
export default class App extends React.Component {
render() {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#fff" }}>
<View style={styles.container}>
<Text>Hello World</Text>
</View>
</SafeAreaView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : 0
}
});
#philipheinser solution does work indeed.
However, I would expect that React Native's StatusBar component will handle that for us.
It doesn't, unfortunately, but we can abstract that away quite easily by creating our own component around it:
./StatusBar.js
import React from 'react';
import { View, StatusBar, Platform } from 'react-native';
// here, we add the spacing for iOS
// and pass the rest of the props to React Native's StatusBar
export default function (props) {
const height = (Platform.OS === 'ios') ? 20 : 0;
const { backgroundColor } = props;
return (
<View style={{ height, backgroundColor }}>
<StatusBar { ...props } />
</View>
);
}
./index.js
import React from 'react';
import { View } from 'react-native';
import StatusBar from './StatusBar';
export default function App () {
return (
<View>
<StatusBar backgroundColor="#2EBD6B" barStyle="light-content" />
{ /* rest of our app */ }
</View>
)
}
Before:
After:
The react-navigation docs have a great solution for this. First off, they recommend not to use the SafeAreaView included with React Native because:
While React Native exports a SafeAreaView component, it has some
inherent issues, i.e. if a screen containing safe area is animating,
it causes jumpy behavior. In addition, this component only supports
iOS 10+ with no support for older iOS versions or Android. We
recommend to use the react-native-safe-area-context library to handle
safe areas in a more reliable way.
Instead, they recommend react-native-safe-area-context - with which it would look like this:
import React, { Component } from 'react';
import { View, Text, Navigator } from 'react-native';
import { useSafeArea } from 'react-native-safe-area-context';
export default function MyScene({title = 'MyScene'}) {
const insets = useSafeArea();
return (
<View style={{paddingTop: insets.top}}>
<Text>Hi! My name is {title}.</Text>
</View>
)
}
I would like to note that it's probably a better idea to use the SafeAreaView that this library offers though, since phones these days may also have elements at the bottom that can overlap UI elements. It all depends on your app of course. (For more detail on that, see the react-navigation docs I linked to in the beginning.)
Here is a way that works for iOS:
<View style={{height: 20, backgroundColor: 'white', marginTop: -20, zIndex: 2}}>
<StatusBar barStyle="dark-content"/></View>
You can handle this by adding a padding to you navigation bar component or just ad a view that has the same hight as the statusbar at the top of your view tree with a backgroundcolor like the facebook app does this.
Just Simple User React native Default StatusBar to achieve this funcationality.
<View style={styles.container}>
<StatusBar backgroundColor={Color.TRANSPARENT} translucent={true} />
<MapView
provider={PROVIDER_GOOGLE} // remove if not using Google Maps
style={styles.map}
region={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.015,
longitudeDelta: 0.0121,
}}
/>
</View>
If you combine SaveAreaView and StatusBar, you get it.
https://reactnative.dev/docs/statusbar
https://reactnative.dev/docs/safeareaview
Just do this:
<SafeAreaView>
<View style={{flex: 1}}>
<StatusBar translucent={false} backgroundColor="#fff" />
// Your dark magic here
</View>
</SafeAreaView>
[This answer is applicable to Android emulators]
Hi, I have imported status bar from "react-native" and called it at the end of block with status bar style set to auto and it worked for me, the code below is for reference:
import { SafeAreaView,Button, StyleSheet, Text, TextInput, View } from 'react-native';
import { StatusBar } from 'react-native';
export default function App() {
return (
<SafeAreaView style={styles.appContainer}>
<View >
<TextInput placeholder='Add your course goal' />
<Button title="Add Goals" />
</View>
<View>
<Text>List of goals..</Text>
</View>
<StatusBar style="auto" />
</SafeAreaView>
);
}
I am using a ScrollView inside of a PanResponder. On Android it works fine but on iOS the ScrollView will not scroll. I did some investigation and here are some facts:
If I put a break point in PanResponder.onMoveShouldSetPanResponder(), before I step over, the scrollView will scroll as normal but once I release the break point, the scrollView stops working.
If I modify ScrollResponder.js, and return true in scrollResponderHandleStartShouldSetResponderCapture() - it used to return false at runtime; and return false in scrollResponderHandleTerminationRequest(), the scrollView works OK but of course, since it swallows the event the outer PanResponder will not get the event.
So the questions are:
I want to make the scrollview to work, and not to swallow the event. Any one know what's the approach?
How the responding system works on iOS? The react-native responder system doc does not explain that to me.
To enable scrolling in a ScrollView that is a child of a parent with PanResponder, you have to make sure the ScrollView is the responder to any gesture inside of it. By default, gesture events bubble up from the deepest component to the parent component. In order to capture the event by the ScrollView, you can add a View with a PanResponder inside of it. See the (pseudo) example below, where ChildComponent is a child of a parent with PanResponder.
const ChildComponent = ({ theme }) => {
const panResponder = React.useRef(
PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
})
).current;
return (
<ScrollView style={{ height: 500 }}>
<View {...panResponder.panHandlers}>
...
</View>
</ScrollView>
);
};
Within PanResponder is an event that returns the current touch position. You can use that to compare 2 values to perform a scroll.
I finally solve this by wrap the scrollview inside a view ,and set the style of scrollview a limited height.
import * as React from 'react';
import { Text, View, StyleSheet,PanResponder,Animated,ScrollView,Dimensions} from 'react-native';
import { Constants } from 'expo';
const WINDOW_WIDTH = Dimensions.get("window").width;
const WINDOW_HEIGHT = Dimensions.get("window").height;
// You can import from local files
export default class App extends React.Component {
constructor(props){
super(props);
this.minTop = 100;
this.maxTop = 500;
this.state={
AnimatedTop:new Animated.Value(0),
}
}
componentWillMount(){
let that = this;
this._previousTop = 100;
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder(){
return true;
},
onPanResponderGrant(){
that._previousTop = that.state.AnimatedTop.__getValue();
return true;
},
onPanResponderMove(evt,gestureState){
let currentTop = that._previousTop + gestureState.dy;
that.state.AnimatedTop.setValue(that._previousTop+gestureState.dy);
},
onPanResponderRelease(){
}
})
}
render() {
return (
<View style={styles.container}>
<Animated.View
style={[styles.overlay,{top:this.state.AnimatedTop}]}
{...this._panResponder.panHandlers}
>
<View style={{height:200,backgroundColor:"black"}}></View>
<View>
<ScrollView
style={{height:500}}
>
<View style={{backgroundColor:"blue",height:200}}></View>
<View style={{backgroundColor:"yellow",height:200}}></View>
<View style={{backgroundColor:"pink",height:200}}></View>
<View style={{backgroundColor:"red",height:200}}></View>
</ScrollView>
</View>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
overlay:{
position:"absolute",
width:WINDOW_WIDTH,
height:WINDOW_HEIGHT-100,
}
});
enter link description here
I spent plenty of time to solve this.Hope this will help someone confused with the same problem.
I don't know if it's usefull right now but you can add that line,
return !(gestureState.dx === 0 && gestureState.dy === 0)
in 'onMoveShouldSetPanResponder' property of PanResponder with evt and gestureState as parameters.
Your PanResponder should look like this :
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder(evt, gestureState){
return !(gestureState.dx === 0 && gestureState.dy === 0)
},
onPanResponderGrant(){
that._previousTop = that.state.AnimatedTop.__getValue();
return true;
},
onPanResponderMove(evt,gestureState){
let currentTop = that._previousTop + gestureState.dy;
that.state.AnimatedTop.setValue(that._previousTop+gestureState.dy);
},
onPanResponderRelease(){
}
})
I have solved this by adding onPress handlers to all of the contents of the ScrollView - doesn't matter if the handler is a no-op, the scroll view still works fine.
I'm not sure you still need this or not but I'll put to help others as well
If you put the panResponder on a sibling view of the ScrollView, the ScrollView behaves properly. then you can position that sibling view
here is an example of the workaround.
I solved this issue by doing it
onMoveShouldSetPanResponder: (event, gesture) => {
if (gesture?.moveX > gesture?.moveY) {
return false;
}
return true;
},
On my case, I have a horizontal Flatlist inside a PanResponder...