react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js

232 lines
5.8 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
const Keyboard = require('Keyboard');
const LayoutAnimation = require('LayoutAnimation');
const Platform = require('Platform');
const React = require('React');
const StyleSheet = require('StyleSheet');
const View = require('View');
import type EmitterSubscription from 'EmitterSubscription';
import type {ViewStyleProp} from 'StyleSheet';
import type {ViewProps, ViewLayout, ViewLayoutEvent} from 'ViewPropTypes';
import type {KeyboardEvent} from 'Keyboard';
type Props = $ReadOnly<{|
...ViewProps,
/**
* Specify how to react to the presence of the keyboard.
*/
behavior?: ?('height' | 'position' | 'padding'),
/**
* Style of the content container when `behavior` is 'position'.
*/
contentContainerStyle?: ?ViewStyleProp,
/**
* Controls whether this `KeyboardAvoidingView` instance should take effect.
* This is useful when more than one is on the screen. Defaults to true.
*/
enabled: ?boolean,
/**
* Distance between the top of the user screen and the React Native view. This
* may be non-zero in some cases. Defaults to 0.
*/
keyboardVerticalOffset: number,
|}>;
type State = {|
bottom: number,
|};
const viewRef = 'VIEW';
/**
* View that moves out of the way when the keyboard appears by automatically
* adjusting its height, position, or bottom padding.
*/
class KeyboardAvoidingView extends React.Component<Props, State> {
static defaultProps = {
enabled: true,
keyboardVerticalOffset: 0,
};
_frame: ?ViewLayout = null;
_subscriptions: Array<EmitterSubscription> = [];
state = {
bottom: 0,
};
_relativeKeyboardHeight(keyboardFrame): number {
const frame = this._frame;
if (!frame || !keyboardFrame) {
return 0;
}
const keyboardY = keyboardFrame.screenY - this.props.keyboardVerticalOffset;
// Calculate the displacement needed for the view such that it
// no longer overlaps with the keyboard
return Math.max(frame.y + frame.height - keyboardY, 0);
}
_onKeyboardChange = (event: ?KeyboardEvent) => {
if (event == null) {
this.setState({bottom: 0});
return;
}
const {duration, easing, endCoordinates} = event;
const height = this._relativeKeyboardHeight(endCoordinates);
prevent scheduling unnecessary layoutanimation Summary: when a hardware keyboard is connected, the virtual keyboard can be hidden (this can easily be demonstrated in the simulator), which means the height of the keyboard is 0. When in this case a `LayoutAnimation` is scheduled, the `KeyboardAvoidingView` won't be affected, but the next layout change will be animated, which can have unintended side-effects. This can also trigger the `Overriding previous layout animation with new one before the first began` warning. <details> <summary>Screenshot</summary> ![image](https://user-images.githubusercontent.com/351038/33261130-22cf2e0c-d362-11e7-8629-0cc70cda67d8.png) </details> Open the `KeyboardAvoidingView` example in the `RNTester` project, import `LayoutAnimation` and add something rendered conditionally to the content of the `Modal`, e.g.; ```jsx {this.state.behavior === 'position' && <Text>We're using position now</Text> } ``` Then update the `onSegmentChange` handler with a `LayoutAnimation`; ```js onSegmentChange = (segment: String) => { LayoutAnimation.easeInEaseOut(); this.setState({behavior: segment.toLowerCase()}); }; ``` Now open the example in the simulator and play with the "Toggle Software Keyboard" option; ![image](https://user-images.githubusercontent.com/351038/33262149-9ba182fa-d365-11e7-9491-890928656f5d.png) Now when you focus the input, no keyboard should appear, and when you then press an option of the segmented control, you should get the beforementioned warning. After this change this warning will no longer appear, but the component still behaves the same as before. [IOS] [BUGFIX] [KeyboardAvoidingView] - prevent scheduling unnecessary `LayoutAnimation` Closes https://github.com/facebook/react-native/pull/16984 Differential Revision: D6472300 Pulled By: shergin fbshipit-source-id: c4041dfdd846cdc88b2e9d281517ed79da99dfe7
2017-12-04 05:05:45 +00:00
if (this.state.bottom === height) {
return;
}
if (duration && easing) {
LayoutAnimation.configureNext({
duration: duration,
update: {
duration: duration,
type: LayoutAnimation.Types[easing] || 'keyboard',
},
});
}
this.setState({bottom: height});
};
_onLayout = (event: ViewLayoutEvent) => {
this._frame = event.nativeEvent.layout;
};
UNSAFE_componentWillUpdate(nextProps: Props, nextState: State): 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;
}
}
componentDidMount(): void {
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(): void {
this._subscriptions.forEach(subscription => {
subscription.remove();
});
}
render(): React.Node {
const {
behavior,
children,
contentContainerStyle,
enabled,
keyboardVerticalOffset, // eslint-disable-line no-unused-vars
style,
...props
} = this.props;
const bottomHeight = enabled ? this.state.bottom : 0;
switch (behavior) {
case 'height':
let heightStyle;
if (this._frame != null) {
// 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 - bottomHeight,
flex: 0,
};
}
return (
<View
ref={viewRef}
style={StyleSheet.compose(
style,
heightStyle,
)}
onLayout={this._onLayout}
{...props}>
{children}
</View>
);
case 'position':
return (
<View
ref={viewRef}
style={style}
onLayout={this._onLayout}
{...props}>
<View
style={StyleSheet.compose(
contentContainerStyle,
{
bottom: bottomHeight,
},
)}>
{children}
</View>
</View>
);
case 'padding':
return (
<View
ref={viewRef}
style={StyleSheet.compose(
style,
{paddingBottom: bottomHeight},
)}
onLayout={this._onLayout}
{...props}>
{children}
</View>
);
default:
return (
<View
ref={viewRef}
onLayout={this._onLayout}
style={style}
{...props}>
{children}
</View>
);
}
}
}
module.exports = KeyboardAvoidingView;