Skip to content
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
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 0 deletions.
@@ -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
Copy link
Contributor

@GantMan GantMan commented on 8b78846 Jun 13, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Looking forward to this!

@mozillo
Copy link

@mozillo mozillo commented on 8b78846 Jul 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love it!

@norfish
Copy link

@norfish norfish commented on 8b78846 Jul 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool

@DylanYasen
Copy link

@DylanYasen DylanYasen commented on 8b78846 Aug 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome!

@bsiddiqui
Copy link

@bsiddiqui bsiddiqui commented on 8b78846 Aug 15, 2016

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?

@wootwoot1234
Copy link

@wootwoot1234 wootwoot1234 commented on 8b78846 Aug 20, 2016

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'?

@GantMan
Copy link
Contributor

@GantMan GantMan commented on 8b78846 Aug 20, 2016

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.

@wootwoot1234
Copy link

@wootwoot1234 wootwoot1234 commented on 8b78846 Aug 21, 2016

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

@DylanYasen
Copy link

@DylanYasen DylanYasen commented on 8b78846 Aug 21, 2016

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!

@AzizAK
Copy link

@AzizAK AzizAK commented on 8b78846 Aug 27, 2016

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!

@Arkanine
Copy link

@Arkanine Arkanine commented on 8b78846 Oct 4, 2016

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.

@chirag04
Copy link
Contributor

@chirag04 chirag04 commented on 8b78846 Oct 4, 2016

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?

@migueloller
Copy link

@migueloller migueloller commented on 8b78846 Oct 8, 2016

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:

  • 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.