KeyboardAvoidingView.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. * @flow
  9. */
  10. 'use strict';
  11. const Keyboard = require('./Keyboard');
  12. const LayoutAnimation = require('../../LayoutAnimation/LayoutAnimation');
  13. const Platform = require('../../Utilities/Platform');
  14. const React = require('react');
  15. const StyleSheet = require('../../StyleSheet/StyleSheet');
  16. const View = require('../View/View');
  17. import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
  18. import type EmitterSubscription from '../../vendor/emitter/EmitterSubscription';
  19. import type {
  20. ViewProps,
  21. ViewLayout,
  22. ViewLayoutEvent,
  23. } from '../View/ViewPropTypes';
  24. import type {KeyboardEvent} from './Keyboard';
  25. type Props = $ReadOnly<{|
  26. ...ViewProps,
  27. /**
  28. * Specify how to react to the presence of the keyboard.
  29. */
  30. behavior?: ?('height' | 'position' | 'padding'),
  31. /**
  32. * Style of the content container when `behavior` is 'position'.
  33. */
  34. contentContainerStyle?: ?ViewStyleProp,
  35. /**
  36. * Controls whether this `KeyboardAvoidingView` instance should take effect.
  37. * This is useful when more than one is on the screen. Defaults to true.
  38. */
  39. enabled: ?boolean,
  40. /**
  41. * Distance between the top of the user screen and the React Native view. This
  42. * may be non-zero in some cases. Defaults to 0.
  43. */
  44. keyboardVerticalOffset: number,
  45. |}>;
  46. type State = {|
  47. bottom: number,
  48. |};
  49. /**
  50. * View that moves out of the way when the keyboard appears by automatically
  51. * adjusting its height, position, or bottom padding.
  52. */
  53. class KeyboardAvoidingView extends React.Component<Props, State> {
  54. static defaultProps: {|enabled: boolean, keyboardVerticalOffset: number|} = {
  55. enabled: true,
  56. keyboardVerticalOffset: 0,
  57. };
  58. _frame: ?ViewLayout = null;
  59. _subscriptions: Array<EmitterSubscription> = [];
  60. viewRef: {current: React.ElementRef<any> | null, ...};
  61. _initialFrameHeight: number = 0;
  62. constructor(props: Props) {
  63. super(props);
  64. this.state = {bottom: 0};
  65. this.viewRef = React.createRef();
  66. }
  67. _relativeKeyboardHeight(keyboardFrame): number {
  68. const frame = this._frame;
  69. if (!frame || !keyboardFrame) {
  70. return 0;
  71. }
  72. const keyboardY = keyboardFrame.screenY - this.props.keyboardVerticalOffset;
  73. // Calculate the displacement needed for the view such that it
  74. // no longer overlaps with the keyboard
  75. return Math.max(frame.y + frame.height - keyboardY, 0);
  76. }
  77. _onKeyboardChange = (event: ?KeyboardEvent) => {
  78. if (event == null) {
  79. this.setState({bottom: 0});
  80. return;
  81. }
  82. const {duration, easing, endCoordinates} = event;
  83. const height = this._relativeKeyboardHeight(endCoordinates);
  84. if (this.state.bottom === height) {
  85. return;
  86. }
  87. if (duration && easing) {
  88. LayoutAnimation.configureNext({
  89. // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m
  90. duration: duration > 10 ? duration : 10,
  91. update: {
  92. duration: duration > 10 ? duration : 10,
  93. type: LayoutAnimation.Types[easing] || 'keyboard',
  94. },
  95. });
  96. }
  97. this.setState({bottom: height});
  98. };
  99. _onLayout = (event: ViewLayoutEvent) => {
  100. this._frame = event.nativeEvent.layout;
  101. if (!this._initialFrameHeight) {
  102. // save the initial frame height, before the keyboard is visible
  103. this._initialFrameHeight = this._frame.height;
  104. }
  105. };
  106. componentDidMount(): void {
  107. if (Platform.OS === 'ios') {
  108. this._subscriptions = [
  109. Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange),
  110. ];
  111. } else {
  112. this._subscriptions = [
  113. Keyboard.addListener('keyboardDidHide', this._onKeyboardChange),
  114. Keyboard.addListener('keyboardDidShow', this._onKeyboardChange),
  115. ];
  116. }
  117. }
  118. componentWillUnmount(): void {
  119. this._subscriptions.forEach(subscription => {
  120. subscription.remove();
  121. });
  122. }
  123. render(): React.Node {
  124. const {
  125. behavior,
  126. children,
  127. contentContainerStyle,
  128. enabled,
  129. keyboardVerticalOffset,
  130. style,
  131. ...props
  132. } = this.props;
  133. const bottomHeight = enabled ? this.state.bottom : 0;
  134. switch (behavior) {
  135. case 'height':
  136. let heightStyle;
  137. if (this._frame != null && this.state.bottom > 0) {
  138. // Note that we only apply a height change when there is keyboard present,
  139. // i.e. this.state.bottom is greater than 0. If we remove that condition,
  140. // this.frame.height will never go back to its original value.
  141. // When height changes, we need to disable flex.
  142. heightStyle = {
  143. height: this._initialFrameHeight - bottomHeight,
  144. flex: 0,
  145. };
  146. }
  147. return (
  148. <View
  149. ref={this.viewRef}
  150. style={StyleSheet.compose(
  151. style,
  152. heightStyle,
  153. )}
  154. onLayout={this._onLayout}
  155. {...props}>
  156. {children}
  157. </View>
  158. );
  159. case 'position':
  160. return (
  161. <View
  162. ref={this.viewRef}
  163. style={style}
  164. onLayout={this._onLayout}
  165. {...props}>
  166. <View
  167. style={StyleSheet.compose(
  168. contentContainerStyle,
  169. {
  170. bottom: bottomHeight,
  171. },
  172. )}>
  173. {children}
  174. </View>
  175. </View>
  176. );
  177. case 'padding':
  178. return (
  179. <View
  180. ref={this.viewRef}
  181. style={StyleSheet.compose(
  182. style,
  183. {paddingBottom: bottomHeight},
  184. )}
  185. onLayout={this._onLayout}
  186. {...props}>
  187. {children}
  188. </View>
  189. );
  190. default:
  191. return (
  192. <View
  193. ref={this.viewRef}
  194. onLayout={this._onLayout}
  195. style={style}
  196. {...props}>
  197. {children}
  198. </View>
  199. );
  200. }
  201. }
  202. }
  203. module.exports = KeyboardAvoidingView;