Permalink
Browse files

Open sourced KeyboardAvoidingView

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...
nicklockwood authored and Facebook Github Bot 8 committed Jun 7, 2016
1 parent d64368b commit 8b78846a9501ef9c5ce9d1e18ee104bfae76af2e
@@ -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;
@@ -1,4 +1,11 @@
/**
* 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.
*
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
@@ -40,6 +40,10 @@ const ComponentExamples: Array<UIExplorerExample> = [
key: 'ImageExample',
module: require('./ImageExample'),
},
{
key: 'KeyboardAvoidingViewExample',
module: require('./KeyboardAvoidingViewExample'),
},
{
key: 'LayoutEventsExample',
module: require('./LayoutEventsExample'),
@@ -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;
@@ -35,6 +35,7 @@ const ReactNative = {
get Image() { return require('Image'); },
get ImageEditor() { return require('ImageEditor'); },
get ImageStore() { return require('ImageStore'); },
get KeyboardAvoidingView() { return require('KeyboardAvoidingView'); },
get ListView() { return require('ListView'); },
get MapView() { return require('MapView'); },
get Modal() { return require('Modal'); },
@@ -33,6 +33,7 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), {
Image: require('Image'),
ImageEditor: require('ImageEditor'),
ImageStore: require('ImageStore'),
KeyboardAvoidingView: require('KeyboardAvoidingView'),
ListView: require('ListView'),
MapView: require('MapView'),
Modal: require('Modal'),

13 comments on commit 8b78846

@GantMan

This comment has been minimized.

Show comment
Hide comment
@GantMan

GantMan Jun 13, 2016

Contributor

👍 Looking forward to this!

Contributor

GantMan replied Jun 13, 2016

👍 Looking forward to this!

@mozillo

This comment has been minimized.

Show comment
Hide comment
@mozillo

mozillo Jul 4, 2016

I love it!

mozillo replied Jul 4, 2016

I love it!

@norfish

This comment has been minimized.

Show comment
Hide comment
@norfish

norfish replied Jul 14, 2016

cool

@DylanYasen

This comment has been minimized.

Show comment
Hide comment
@DylanYasen

DylanYasen replied Aug 6, 2016

awesome!

@bsiddiqui

This comment has been minimized.

Show comment
Hide comment
@bsiddiqui

bsiddiqui Aug 15, 2016

This is awesome but I've been having trouble using it - any plans on docs?

bsiddiqui replied Aug 15, 2016

This is awesome but I've been having trouble using it - any plans on docs?

@wootwoot1234

This comment has been minimized.

Show comment
Hide comment
@wootwoot1234

wootwoot1234 Aug 20, 2016

What's the difference between setting behavior to 'height', 'position' and, 'padding'?

wootwoot1234 replied Aug 20, 2016

What's the difference between setting behavior to 'height', 'position' and, 'padding'?

@GantMan

This comment has been minimized.

Show comment
Hide comment
@GantMan

GantMan Aug 20, 2016

Contributor

padding is the only one that works for me. Not sure the differences.

Contributor

GantMan replied Aug 20, 2016

padding is the only one that works for me. Not sure the differences.

@wootwoot1234

This comment has been minimized.

Show comment
Hide comment
@wootwoot1234

wootwoot1234 Aug 21, 2016

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

wootwoot1234 replied Aug 21, 2016

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

@DylanYasen

This comment has been minimized.

Show comment
Hide comment
@DylanYasen

DylanYasen Aug 21, 2016

I'm currently using 'position' mode in my project. It works well enough, thanks for this!

DylanYasen replied Aug 21, 2016

I'm currently using 'position' mode in my project. It works well enough, thanks for this!

@AzizAK

This comment has been minimized.

Show comment
Hide comment
@AzizAK

AzizAK Aug 27, 2016

Thank you @nicklockwood, i'm using "position" and its very helpful!

AzizAK replied Aug 27, 2016

Thank you @nicklockwood, i'm using "position" and its very helpful!

@Arkanine

This comment has been minimized.

Show comment
Hide comment
@Arkanine

Arkanine Oct 4, 2016

Hey @nicklockwood, how to use it with ScrollView? When keyboard disappear, the space still exists.
Thank you for the answer.

Arkanine replied Oct 4, 2016

Hey @nicklockwood, how to use it with ScrollView? When keyboard disappear, the space still exists.
Thank you for the answer.

@chirag04

This comment has been minimized.

Show comment
Hide comment
@chirag04

chirag04 Oct 4, 2016

Collaborator

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?

Collaborator

chirag04 replied Oct 4, 2016

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?

@migueloller

This comment has been minimized.

Show comment
Hide comment
@migueloller

migueloller Oct 8, 2016

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:

  • Remove the need to specify the keyboardVerticalOffset prop. It is possible to calculate this value using the measure method from NativeMethodsMixin.
  • Listen for 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?

migueloller replied Oct 8, 2016

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:

  • Remove the need to specify the keyboardVerticalOffset prop. It is possible to calculate this value using the measure method from NativeMethodsMixin.
  • Listen for 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?

Please sign in to comment.