-
Notifications
You must be signed in to change notification settings - Fork 24.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Summary: KeyboardAvoidingView is a component we built internally to solve the common problem of views that need to move out of the way of the virtual keyboard. KeyboardAvoidingView can automatically adjust either its position or bottom padding based on the position of the keyboard. Reviewed By: javache Differential Revision: D3398238 fbshipit-source-id: 493f2d2dec76667996250c011a1c5b7a14f245eb
- Loading branch information
1 parent
d64368b
commit 8b78846
Showing
6 changed files
with
313 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/** | ||
* Copyright (c) 2013-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
* | ||
* @providesModule KeyboardAvoidingViewExample | ||
*/ | ||
'use strict'; | ||
|
||
const React = require('React'); | ||
const ReactNative = require('react-native'); | ||
const { | ||
KeyboardAvoidingView, | ||
Modal, | ||
SegmentedControlIOS, | ||
StyleSheet, | ||
Text, | ||
TextInput, | ||
TouchableHighlight, | ||
View, | ||
} = ReactNative; | ||
|
||
const UIExplorerBlock = require('./UIExplorerBlock'); | ||
const UIExplorerPage = require('./UIExplorerPage'); | ||
|
||
const KeyboardAvoidingViewExample = React.createClass({ | ||
statics: { | ||
title: '<KeyboardAvoidingView>', | ||
description: 'Base component for views that automatically adjust their height or position to move out of the way of the keyboard.', | ||
}, | ||
|
||
getInitialState() { | ||
return { | ||
behavior: 'padding', | ||
modalOpen: false, | ||
}; | ||
}, | ||
|
||
onSegmentChange(segment: String) { | ||
this.setState({behavior: segment.toLowerCase()}); | ||
}, | ||
|
||
renderExample() { | ||
return ( | ||
<View style={styles.outerContainer}> | ||
<Modal animationType="fade" visible={this.state.modalOpen}> | ||
<KeyboardAvoidingView behavior={this.state.behavior} style={styles.container}> | ||
<SegmentedControlIOS | ||
onValueChange={this.onSegmentChange} | ||
selectedIndex={this.state.behavior === 'padding' ? 0 : 1} | ||
style={styles.segment} | ||
values={['Padding', 'Position']} /> | ||
<TextInput | ||
placeholder="<TextInput />" | ||
style={styles.textInput} /> | ||
</KeyboardAvoidingView> | ||
<TouchableHighlight | ||
onPress={() => this.setState({modalOpen: false})} | ||
style={styles.closeButton}> | ||
<Text>Close</Text> | ||
</TouchableHighlight> | ||
</Modal> | ||
|
||
<TouchableHighlight onPress={() => this.setState({modalOpen: true})}> | ||
<Text>Open Example</Text> | ||
</TouchableHighlight> | ||
</View> | ||
); | ||
}, | ||
|
||
render() { | ||
return ( | ||
<UIExplorerPage title="Keyboard Avoiding View"> | ||
<UIExplorerBlock title="Keyboard-avoiding views move out of the way of the keyboard."> | ||
{this.renderExample()} | ||
</UIExplorerBlock> | ||
</UIExplorerPage> | ||
); | ||
}, | ||
}); | ||
|
||
const styles = StyleSheet.create({ | ||
outerContainer: { | ||
flex: 1, | ||
}, | ||
container: { | ||
flex: 1, | ||
justifyContent: 'center', | ||
paddingHorizontal: 20, | ||
paddingTop: 20, | ||
}, | ||
textInput: { | ||
borderRadius: 5, | ||
borderWidth: 1, | ||
height: 44, | ||
paddingHorizontal: 10, | ||
}, | ||
segment: { | ||
marginBottom: 10, | ||
}, | ||
closeButton: { | ||
position: 'absolute', | ||
top: 30, | ||
left: 10, | ||
} | ||
}); | ||
|
||
module.exports = KeyboardAvoidingViewExample; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
/** | ||
* Copyright (c) 2015-present, Facebook, Inc. | ||
* All rights reserved. | ||
* | ||
* This source code is licensed under the BSD-style license found in the | ||
* LICENSE file in the root directory of this source tree. An additional grant | ||
* of patent rights can be found in the PATENTS file in the same directory. | ||
* | ||
* @providesModule KeyboardAvoidingView | ||
* @flow | ||
*/ | ||
'use strict'; | ||
|
||
const Keyboard = require('Keyboard'); | ||
const LayoutAnimation = require('LayoutAnimation'); | ||
const Platform = require('Platform'); | ||
const PropTypes = require('ReactPropTypes'); | ||
const React = require('React'); | ||
const TimerMixin = require('react-timer-mixin'); | ||
const View = require('View'); | ||
|
||
import type EmitterSubscription from 'EmitterSubscription'; | ||
|
||
type Rect = { | ||
x: number; | ||
y: number; | ||
width: number; | ||
height: number; | ||
}; | ||
type ScreenRect = { | ||
screenX: number; | ||
screenY: number; | ||
width: number; | ||
height: number; | ||
}; | ||
type KeyboardChangeEvent = { | ||
startCoordinates?: ScreenRect; | ||
endCoordinates: ScreenRect; | ||
duration?: number; | ||
easing?: string; | ||
}; | ||
type LayoutEvent = { | ||
nativeEvent: { | ||
layout: Rect; | ||
} | ||
}; | ||
|
||
const viewRef = 'VIEW'; | ||
|
||
const KeyboardAvoidingView = React.createClass({ | ||
mixins: [TimerMixin], | ||
|
||
propTypes: { | ||
...View.propTypes, | ||
behavior: PropTypes.oneOf(['height', 'position', 'padding']), | ||
|
||
/** | ||
* This is the distance between the top of the user screen and the react native view, | ||
* may be non-zero in some use cases. | ||
*/ | ||
keyboardVerticalOffset: PropTypes.number.isRequired, | ||
}, | ||
|
||
getDefaultProps() { | ||
return { | ||
keyboardVerticalOffset: 0, | ||
}; | ||
}, | ||
|
||
getInitialState() { | ||
return { | ||
bottom: 0, | ||
}; | ||
}, | ||
|
||
subscriptions: ([]: Array<EmitterSubscription>), | ||
frame: (null: ?Rect), | ||
|
||
relativeKeyboardHeight(keyboardFrame: ScreenRect): number { | ||
const frame = this.frame; | ||
if (!frame) { | ||
return 0; | ||
} | ||
|
||
const y1 = Math.max(frame.y, keyboardFrame.screenY - this.props.keyboardVerticalOffset); | ||
const y2 = Math.min(frame.y + frame.height, keyboardFrame.screenY + keyboardFrame.height - this.props.keyboardVerticalOffset); | ||
return Math.max(y2 - y1, 0); | ||
}, | ||
|
||
onKeyboardChange(event: ?KeyboardChangeEvent) { | ||
if (!event) { | ||
this.setState({bottom: 0}); | ||
return; | ||
} | ||
|
||
const {duration, easing, endCoordinates} = event; | ||
const height = this.relativeKeyboardHeight(endCoordinates); | ||
|
||
if (duration && easing) { | ||
LayoutAnimation.configureNext({ | ||
duration: duration, | ||
update: { | ||
duration: duration, | ||
type: LayoutAnimation.Types[easing] || 'keyboard', | ||
}, | ||
}); | ||
} | ||
this.setState({bottom: height}); | ||
}, | ||
|
||
onLayout(event: LayoutEvent) { | ||
this.frame = event.nativeEvent.layout; | ||
}, | ||
|
||
componentWillUpdate(nextProps: Object, nextState: Object, nextContext?: Object): void { | ||
if (nextState.bottom === this.state.bottom && | ||
this.props.behavior === 'height' && | ||
nextProps.behavior === 'height') { | ||
// If the component rerenders without an internal state change, e.g. | ||
// triggered by parent component re-rendering, no need for bottom to change. | ||
nextState.bottom = 0; | ||
} | ||
}, | ||
|
||
componentWillMount() { | ||
if (Platform.OS === 'ios') { | ||
this.subscriptions = [ | ||
Keyboard.addListener('keyboardWillChangeFrame', this.onKeyboardChange), | ||
]; | ||
} else { | ||
this.subscriptions = [ | ||
Keyboard.addListener('keyboardDidHide', this.onKeyboardChange), | ||
Keyboard.addListener('keyboardDidShow', this.onKeyboardChange), | ||
]; | ||
} | ||
}, | ||
|
||
componentWillUnmount() { | ||
this.subscriptions.forEach((sub) => sub.remove()); | ||
}, | ||
|
||
render(): ReactElement<any> { | ||
const {behavior, children, style, ...props} = this.props; | ||
|
||
switch (behavior) { | ||
case 'height': | ||
let heightStyle; | ||
if (this.frame) { | ||
// Note that we only apply a height change when there is keyboard present, | ||
// i.e. this.state.bottom is greater than 0. If we remove that condition, | ||
// this.frame.height will never go back to its original value. | ||
// When height changes, we need to disable flex. | ||
heightStyle = {height: this.frame.height - this.state.bottom, flex: 0}; | ||
} | ||
return ( | ||
<View ref={viewRef} style={[style, heightStyle]} onLayout={this.onLayout} {...props}> | ||
{children} | ||
</View> | ||
); | ||
|
||
case 'position': | ||
const positionStyle = {bottom: this.state.bottom}; | ||
return ( | ||
<View ref={viewRef} style={style} onLayout={this.onLayout} {...props}> | ||
<View style={positionStyle}> | ||
{children} | ||
</View> | ||
</View> | ||
); | ||
|
||
case 'padding': | ||
const paddingStyle = {paddingBottom: this.state.bottom}; | ||
return ( | ||
<View ref={viewRef} style={[style, paddingStyle]} onLayout={this.onLayout} {...props}> | ||
{children} | ||
</View> | ||
); | ||
|
||
default: | ||
return ( | ||
<View ref={viewRef} onLayout={this.onLayout} style={style} {...props}> | ||
{children} | ||
</View> | ||
); | ||
} | ||
}, | ||
}); | ||
|
||
module.exports = KeyboardAvoidingView; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Looking forward to this!
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love it!
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome!
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome but I've been having trouble using it - any plans on docs?
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference between setting
behavior
to 'height', 'position' and, 'padding'?8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
padding is the only one that works for me. Not sure the differences.
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They all work for me but it's not clear to me when to use each one. I will say, they work work great. Thanks @nicklockwood
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm currently using 'position' mode in my project. It works well enough, thanks for this!
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @nicklockwood, i'm using "position" and its very helpful!
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @nicklockwood, how to use it with ScrollView? When keyboard disappear, the space still exists.
Thank you for the answer.
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been using this in production: https://gist.github.com/chirag04/d7a7d58f4afc9520a51f511ce7f67788
There is this which didn't work well for me: https://github.com/APSL/react-native-keyboard-aware-scroll-view
@Arkanine can you give feedback if either of them work for you?
8b78846
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey!
I've been using this component in production now and there are 2 improvements that I'm wondering if they would help other people. If so, I wouldn't mind doing a PR implementing the improvements.
They are:
keyboardVerticalOffset
prop. It is possible to calculate this value using themeasure
method fromNativeMethodsMixin
.keyboardDidShow
and respond accordingly. This is important because a component could be mounted after the keyboard is shown and you still would want the view to avoid the keyboard.I've implemented these two improvements on my end so I know that they work 100%.
Thoughts?