Skip to content

Commit 8b78846

Browse files
nicklockwoodFacebook Github Bot 8
authored and
Facebook Github Bot 8
committed
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
1 parent d64368b commit 8b78846

File tree

6 files changed

+313
-0
lines changed

6 files changed

+313
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule KeyboardAvoidingViewExample
10+
*/
11+
'use strict';
12+
13+
const React = require('React');
14+
const ReactNative = require('react-native');
15+
const {
16+
KeyboardAvoidingView,
17+
Modal,
18+
SegmentedControlIOS,
19+
StyleSheet,
20+
Text,
21+
TextInput,
22+
TouchableHighlight,
23+
View,
24+
} = ReactNative;
25+
26+
const UIExplorerBlock = require('./UIExplorerBlock');
27+
const UIExplorerPage = require('./UIExplorerPage');
28+
29+
const KeyboardAvoidingViewExample = React.createClass({
30+
statics: {
31+
title: '<KeyboardAvoidingView>',
32+
description: 'Base component for views that automatically adjust their height or position to move out of the way of the keyboard.',
33+
},
34+
35+
getInitialState() {
36+
return {
37+
behavior: 'padding',
38+
modalOpen: false,
39+
};
40+
},
41+
42+
onSegmentChange(segment: String) {
43+
this.setState({behavior: segment.toLowerCase()});
44+
},
45+
46+
renderExample() {
47+
return (
48+
<View style={styles.outerContainer}>
49+
<Modal animationType="fade" visible={this.state.modalOpen}>
50+
<KeyboardAvoidingView behavior={this.state.behavior} style={styles.container}>
51+
<SegmentedControlIOS
52+
onValueChange={this.onSegmentChange}
53+
selectedIndex={this.state.behavior === 'padding' ? 0 : 1}
54+
style={styles.segment}
55+
values={['Padding', 'Position']} />
56+
<TextInput
57+
placeholder="<TextInput />"
58+
style={styles.textInput} />
59+
</KeyboardAvoidingView>
60+
<TouchableHighlight
61+
onPress={() => this.setState({modalOpen: false})}
62+
style={styles.closeButton}>
63+
<Text>Close</Text>
64+
</TouchableHighlight>
65+
</Modal>
66+
67+
<TouchableHighlight onPress={() => this.setState({modalOpen: true})}>
68+
<Text>Open Example</Text>
69+
</TouchableHighlight>
70+
</View>
71+
);
72+
},
73+
74+
render() {
75+
return (
76+
<UIExplorerPage title="Keyboard Avoiding View">
77+
<UIExplorerBlock title="Keyboard-avoiding views move out of the way of the keyboard.">
78+
{this.renderExample()}
79+
</UIExplorerBlock>
80+
</UIExplorerPage>
81+
);
82+
},
83+
});
84+
85+
const styles = StyleSheet.create({
86+
outerContainer: {
87+
flex: 1,
88+
},
89+
container: {
90+
flex: 1,
91+
justifyContent: 'center',
92+
paddingHorizontal: 20,
93+
paddingTop: 20,
94+
},
95+
textInput: {
96+
borderRadius: 5,
97+
borderWidth: 1,
98+
height: 44,
99+
paddingHorizontal: 10,
100+
},
101+
segment: {
102+
marginBottom: 10,
103+
},
104+
closeButton: {
105+
position: 'absolute',
106+
top: 30,
107+
left: 10,
108+
}
109+
});
110+
111+
module.exports = KeyboardAvoidingViewExample;

Examples/UIExplorer/UIExplorerList.android.js

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
29
* The examples provided by Facebook are for non-commercial testing and
310
* evaluation purposes only.
411
*

Examples/UIExplorer/UIExplorerList.ios.js

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const ComponentExamples: Array<UIExplorerExample> = [
4040
key: 'ImageExample',
4141
module: require('./ImageExample'),
4242
},
43+
{
44+
key: 'KeyboardAvoidingViewExample',
45+
module: require('./KeyboardAvoidingViewExample'),
46+
},
4347
{
4448
key: 'LayoutEventsExample',
4549
module: require('./LayoutEventsExample'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule KeyboardAvoidingView
10+
* @flow
11+
*/
12+
'use strict';
13+
14+
const Keyboard = require('Keyboard');
15+
const LayoutAnimation = require('LayoutAnimation');
16+
const Platform = require('Platform');
17+
const PropTypes = require('ReactPropTypes');
18+
const React = require('React');
19+
const TimerMixin = require('react-timer-mixin');
20+
const View = require('View');
21+
22+
import type EmitterSubscription from 'EmitterSubscription';
23+
24+
type Rect = {
25+
x: number;
26+
y: number;
27+
width: number;
28+
height: number;
29+
};
30+
type ScreenRect = {
31+
screenX: number;
32+
screenY: number;
33+
width: number;
34+
height: number;
35+
};
36+
type KeyboardChangeEvent = {
37+
startCoordinates?: ScreenRect;
38+
endCoordinates: ScreenRect;
39+
duration?: number;
40+
easing?: string;
41+
};
42+
type LayoutEvent = {
43+
nativeEvent: {
44+
layout: Rect;
45+
}
46+
};
47+
48+
const viewRef = 'VIEW';
49+
50+
const KeyboardAvoidingView = React.createClass({
51+
mixins: [TimerMixin],
52+
53+
propTypes: {
54+
...View.propTypes,
55+
behavior: PropTypes.oneOf(['height', 'position', 'padding']),
56+
57+
/**
58+
* This is the distance between the top of the user screen and the react native view,
59+
* may be non-zero in some use cases.
60+
*/
61+
keyboardVerticalOffset: PropTypes.number.isRequired,
62+
},
63+
64+
getDefaultProps() {
65+
return {
66+
keyboardVerticalOffset: 0,
67+
};
68+
},
69+
70+
getInitialState() {
71+
return {
72+
bottom: 0,
73+
};
74+
},
75+
76+
subscriptions: ([]: Array<EmitterSubscription>),
77+
frame: (null: ?Rect),
78+
79+
relativeKeyboardHeight(keyboardFrame: ScreenRect): number {
80+
const frame = this.frame;
81+
if (!frame) {
82+
return 0;
83+
}
84+
85+
const y1 = Math.max(frame.y, keyboardFrame.screenY - this.props.keyboardVerticalOffset);
86+
const y2 = Math.min(frame.y + frame.height, keyboardFrame.screenY + keyboardFrame.height - this.props.keyboardVerticalOffset);
87+
return Math.max(y2 - y1, 0);
88+
},
89+
90+
onKeyboardChange(event: ?KeyboardChangeEvent) {
91+
if (!event) {
92+
this.setState({bottom: 0});
93+
return;
94+
}
95+
96+
const {duration, easing, endCoordinates} = event;
97+
const height = this.relativeKeyboardHeight(endCoordinates);
98+
99+
if (duration && easing) {
100+
LayoutAnimation.configureNext({
101+
duration: duration,
102+
update: {
103+
duration: duration,
104+
type: LayoutAnimation.Types[easing] || 'keyboard',
105+
},
106+
});
107+
}
108+
this.setState({bottom: height});
109+
},
110+
111+
onLayout(event: LayoutEvent) {
112+
this.frame = event.nativeEvent.layout;
113+
},
114+
115+
componentWillUpdate(nextProps: Object, nextState: Object, nextContext?: Object): void {
116+
if (nextState.bottom === this.state.bottom &&
117+
this.props.behavior === 'height' &&
118+
nextProps.behavior === 'height') {
119+
// If the component rerenders without an internal state change, e.g.
120+
// triggered by parent component re-rendering, no need for bottom to change.
121+
nextState.bottom = 0;
122+
}
123+
},
124+
125+
componentWillMount() {
126+
if (Platform.OS === 'ios') {
127+
this.subscriptions = [
128+
Keyboard.addListener('keyboardWillChangeFrame', this.onKeyboardChange),
129+
];
130+
} else {
131+
this.subscriptions = [
132+
Keyboard.addListener('keyboardDidHide', this.onKeyboardChange),
133+
Keyboard.addListener('keyboardDidShow', this.onKeyboardChange),
134+
];
135+
}
136+
},
137+
138+
componentWillUnmount() {
139+
this.subscriptions.forEach((sub) => sub.remove());
140+
},
141+
142+
render(): ReactElement<any> {
143+
const {behavior, children, style, ...props} = this.props;
144+
145+
switch (behavior) {
146+
case 'height':
147+
let heightStyle;
148+
if (this.frame) {
149+
// Note that we only apply a height change when there is keyboard present,
150+
// i.e. this.state.bottom is greater than 0. If we remove that condition,
151+
// this.frame.height will never go back to its original value.
152+
// When height changes, we need to disable flex.
153+
heightStyle = {height: this.frame.height - this.state.bottom, flex: 0};
154+
}
155+
return (
156+
<View ref={viewRef} style={[style, heightStyle]} onLayout={this.onLayout} {...props}>
157+
{children}
158+
</View>
159+
);
160+
161+
case 'position':
162+
const positionStyle = {bottom: this.state.bottom};
163+
return (
164+
<View ref={viewRef} style={style} onLayout={this.onLayout} {...props}>
165+
<View style={positionStyle}>
166+
{children}
167+
</View>
168+
</View>
169+
);
170+
171+
case 'padding':
172+
const paddingStyle = {paddingBottom: this.state.bottom};
173+
return (
174+
<View ref={viewRef} style={[style, paddingStyle]} onLayout={this.onLayout} {...props}>
175+
{children}
176+
</View>
177+
);
178+
179+
default:
180+
return (
181+
<View ref={viewRef} onLayout={this.onLayout} style={style} {...props}>
182+
{children}
183+
</View>
184+
);
185+
}
186+
},
187+
});
188+
189+
module.exports = KeyboardAvoidingView;

Libraries/react-native/react-native.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const ReactNative = {
3535
get Image() { return require('Image'); },
3636
get ImageEditor() { return require('ImageEditor'); },
3737
get ImageStore() { return require('ImageStore'); },
38+
get KeyboardAvoidingView() { return require('KeyboardAvoidingView'); },
3839
get ListView() { return require('ListView'); },
3940
get MapView() { return require('MapView'); },
4041
get Modal() { return require('Modal'); },

Libraries/react-native/react-native.js.flow

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ var ReactNative = Object.assign(Object.create(require('ReactNative')), {
3333
Image: require('Image'),
3434
ImageEditor: require('ImageEditor'),
3535
ImageStore: require('ImageStore'),
36+
KeyboardAvoidingView: require('KeyboardAvoidingView'),
3637
ListView: require('ListView'),
3738
MapView: require('MapView'),
3839
Modal: require('Modal'),

0 commit comments

Comments
 (0)