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
This commit is contained in:
parent
d64368b9e2
commit
8b78846a95
|
@ -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'),
|
||||
|
|
Loading…
Reference in New Issue