|
1 | | -import React, { useCallback, useEffect, useRef, useState } from 'react'; |
2 | | -import { Animated, Keyboard, View } from 'react-native'; |
3 | | - |
4 | | -import { useKeyboardCompatibleHeight } from './hooks/useKeyboardCompatibleHeight'; |
| 1 | +'use strict'; |
5 | 2 |
|
| 3 | +import React from 'react'; |
| 4 | +import { |
| 5 | + AppState, |
| 6 | + Keyboard, |
| 7 | + LayoutAnimation, |
| 8 | + Platform, |
| 9 | + StyleSheet, |
| 10 | + View, |
| 11 | +} from 'react-native'; |
6 | 12 | import { KeyboardContext } from '../../context'; |
7 | 13 |
|
8 | 14 | /** |
9 | | - * KeyboardCompatibleView is HOC component similar to [KeyboardAvoidingView](https://facebook.github.io/react-native/docs/keyboardavoidingview), |
10 | | - * designed to work with MessageInput and MessageList component. |
11 | | - * |
12 | | - * Main motivation of writing this our own component was to get rid of issues that come with KeyboardAvoidingView from react-native |
13 | | - * when used with components of fixed height. [Channel](https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/ChannelInner.js) component |
14 | | - * uses `KeyboardCompatibleView` internally, so you don't need to explicitly add it. |
| 15 | + * View that moves out of the way when the keyboard appears by automatically |
| 16 | + * adjusting its height, position, or bottom padding. |
15 | 17 | * |
16 | | - * ```json |
17 | | - * <KeyboardCompatibleView> |
18 | | - * <MessageList /> |
19 | | - * <MessageInput /> |
20 | | - * </KeyboardCompatibleView> |
21 | | - * ``` |
| 18 | + * Following piece of code has been mostly copied from KeyboardAvoidingView component, with few additional tweaks. |
22 | 19 | */ |
23 | | -export const KeyboardCompatibleView = ({ |
24 | | - children, |
25 | | - enabled = true, |
26 | | - keyboardDismissAnimationDuration = 500, |
27 | | - keyboardOpenAnimationDuration = 500, |
28 | | -}) => { |
29 | | - const heightAnim = useRef(new Animated.Value(0)).current; |
30 | | - const rootChannelView = useRef(); |
31 | | - |
32 | | - const [initialHeight, setInitialHeight] = useState(0); |
33 | | - |
34 | | - const { height: channelHeight, isKeyboardOpen } = useKeyboardCompatibleHeight( |
35 | | - { |
36 | | - enabled, |
37 | | - initialHeight, |
38 | | - rootChannelView, |
39 | | - }, |
40 | | - ); |
41 | | - |
42 | | - useEffect(() => { |
43 | | - Animated.timing(heightAnim, { |
44 | | - duration: isKeyboardOpen |
45 | | - ? keyboardDismissAnimationDuration |
46 | | - : keyboardOpenAnimationDuration, |
47 | | - toValue: channelHeight, |
48 | | - useNativeDriver: false, |
49 | | - }).start(); |
50 | | - }, [ |
51 | | - channelHeight, |
52 | | - heightAnim, |
53 | | - keyboardDismissAnimationDuration, |
54 | | - keyboardOpenAnimationDuration, |
55 | | - ]); |
56 | | - |
57 | | - const dismissKeyboard = useCallback(() => { |
58 | | - Keyboard.dismiss(); |
| 20 | +class KeyboardCompatibleView extends React.Component { |
| 21 | + static defaultProps = { |
| 22 | + behavior: Platform.OS === 'ios' ? 'padding' : 'position', |
| 23 | + enabled: true, |
| 24 | + keyboardVerticalOffset: 66.5, // default MessageInput height |
| 25 | + }; |
| 26 | + |
| 27 | + _frame = null; |
| 28 | + _keyboardEvent = null; |
| 29 | + _subscriptions = []; |
| 30 | + viewRef; |
| 31 | + _initialFrameHeight = 0; |
| 32 | + dismissKeyboardResolver = null; |
| 33 | + constructor(props) { |
| 34 | + super(props); |
| 35 | + this.state = { appState: '', bottom: 0, isKeyboardOpen: false }; |
| 36 | + this.viewRef = React.createRef(); |
| 37 | + } |
| 38 | + |
| 39 | + _relativeKeyboardHeight(keyboardFrame) { |
| 40 | + const frame = this._frame; |
| 41 | + if (!frame || !keyboardFrame) { |
| 42 | + return 0; |
| 43 | + } |
| 44 | + |
| 45 | + const keyboardY = keyboardFrame.screenY - this.props.keyboardVerticalOffset; |
| 46 | + |
| 47 | + // Calculate the displacement needed for the view such that it |
| 48 | + // no longer overlaps with the keyboard |
| 49 | + return Math.max(frame.y + frame.height - keyboardY, 0); |
| 50 | + } |
| 51 | + |
| 52 | + _onKeyboardChange = (event) => { |
| 53 | + this._keyboardEvent = event; |
| 54 | + this._updateBottomIfNecesarry(); |
| 55 | + }; |
| 56 | + |
| 57 | + _onLayout = (event) => { |
| 58 | + this._frame = event.nativeEvent.layout; |
| 59 | + if (!this._initialFrameHeight) { |
| 60 | + // save the initial frame height, before the keyboard is visible |
| 61 | + this._initialFrameHeight = this._frame.height; |
| 62 | + } |
| 63 | + |
| 64 | + this._updateBottomIfNecesarry(); |
| 65 | + }; |
| 66 | + |
| 67 | + _updateBottomIfNecesarry = () => { |
| 68 | + if (this._keyboardEvent == null) { |
| 69 | + this.setState({ bottom: 0 }); |
| 70 | + return; |
| 71 | + } |
| 72 | + |
| 73 | + const { duration, easing, endCoordinates } = this._keyboardEvent; |
| 74 | + const height = this._relativeKeyboardHeight(endCoordinates); |
| 75 | + |
| 76 | + if (this.state.bottom === height) { |
| 77 | + return; |
| 78 | + } |
| 79 | + |
| 80 | + if (duration && easing) { |
| 81 | + LayoutAnimation.configureNext({ |
| 82 | + // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m |
| 83 | + duration: duration > 10 ? duration : 10, |
| 84 | + update: { |
| 85 | + duration: duration > 10 ? duration : 10, |
| 86 | + type: LayoutAnimation.Types[easing] || 'keyboard', |
| 87 | + }, |
| 88 | + }); |
| 89 | + } |
| 90 | + this.setState({ bottom: height }); |
| 91 | + }; |
| 92 | + |
| 93 | + _handleAppStateChange = (nextAppState) => { |
| 94 | + if ( |
| 95 | + this.state.appState.match(/inactive|background/) && |
| 96 | + nextAppState === 'active' |
| 97 | + ) { |
| 98 | + this.setKeyboardListeners(); |
| 99 | + } |
| 100 | + |
| 101 | + if (nextAppState.match(/inactive|background/)) { |
| 102 | + this.unsetKeyboardListeners(); |
| 103 | + } |
| 104 | + |
| 105 | + this.setState({ appState: nextAppState }); |
| 106 | + }; |
| 107 | + |
| 108 | + setKeyboardListeners = () => { |
| 109 | + if (Platform.OS === 'ios') { |
| 110 | + this._subscriptions = [ |
| 111 | + Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange), |
| 112 | + ]; |
| 113 | + } else { |
| 114 | + this._subscriptions = [ |
| 115 | + Keyboard.addListener('keyboardDidHide', this._onKeyboardChange), |
| 116 | + Keyboard.addListener('keyboardDidShow', this._onKeyboardChange), |
| 117 | + ]; |
| 118 | + } |
| 119 | + |
| 120 | + this._subscriptions.push( |
| 121 | + Keyboard.addListener('keyboardDidHide', () => { |
| 122 | + this.setState({ isKeyboardOpen: false }); |
| 123 | + }), |
| 124 | + Keyboard.addListener('keyboardDidShow', () => { |
| 125 | + this.setState({ isKeyboardOpen: true }); |
| 126 | + }), |
| 127 | + ); |
| 128 | + }; |
| 129 | + |
| 130 | + unsetKeyboardListeners = () => { |
| 131 | + this._subscriptions.forEach((subscription) => { |
| 132 | + subscription.remove(); |
| 133 | + }); |
| 134 | + }; |
| 135 | + |
| 136 | + dismissKeyboard = () => { |
| 137 | + if (!this.state.isKeyboardOpen) { |
| 138 | + return; |
| 139 | + } |
59 | 140 |
|
60 | 141 | return new Promise((resolve) => { |
61 | | - if (!isKeyboardOpen) { |
62 | | - // If channel height is already at full length, then don't do anything. |
| 142 | + const subscription = Keyboard.addListener('keyboardDidHide', () => { |
63 | 143 | resolve(); |
64 | | - } else { |
65 | | - // Bring the channel height to its full length state. |
66 | | - Animated.timing(heightAnim, { |
67 | | - duration: keyboardDismissAnimationDuration, |
68 | | - toValue: initialHeight, |
69 | | - useNativeDriver: false, |
70 | | - }).start(resolve); |
71 | | - } |
| 144 | + subscription.remove(); |
| 145 | + }); |
| 146 | + |
| 147 | + Keyboard.dismiss(); |
72 | 148 | }); |
73 | | - }, [ |
74 | | - channelHeight, |
75 | | - heightAnim, |
76 | | - initialHeight, |
77 | | - isKeyboardOpen, |
78 | | - keyboardDismissAnimationDuration, |
79 | | - ]); |
80 | | - |
81 | | - const onLayout = useCallback( |
82 | | - ({ |
83 | | - nativeEvent: { |
84 | | - layout: { height }, |
85 | | - }, |
86 | | - }) => { |
87 | | - if (!enabled) { |
88 | | - return; |
89 | | - } |
90 | | - |
91 | | - // Not to set initial height again. |
92 | | - if (!initialHeight) { |
93 | | - setInitialHeight(height); |
94 | | - Animated.timing(heightAnim, { |
95 | | - duration: 10, |
96 | | - toValue: height, |
97 | | - useNativeDriver: false, |
98 | | - }).start(); |
99 | | - } |
100 | | - }, |
101 | | - [enabled, heightAnim, initialHeight], |
102 | | - ); |
103 | | - |
104 | | - if (!enabled) { |
105 | | - return ( |
106 | | - <KeyboardContext.Provider |
107 | | - value={{ |
108 | | - dismissKeyboard, |
109 | | - }} |
110 | | - > |
111 | | - {children} |
112 | | - </KeyboardContext.Provider> |
113 | | - ); |
| 149 | + }; |
| 150 | + |
| 151 | + componentDidMount() { |
| 152 | + AppState.addEventListener('change', this._handleAppStateChange); |
| 153 | + this.setKeyboardListeners(); |
| 154 | + } |
| 155 | + |
| 156 | + componentWillUnmount() { |
| 157 | + AppState.removeEventListener('change', this._handleAppStateChange); |
| 158 | + this.unsetKeyboardListeners(); |
114 | 159 | } |
115 | 160 |
|
116 | | - return ( |
117 | | - <Animated.View |
118 | | - onLayout={onLayout} |
119 | | - style={{ |
120 | | - height: initialHeight ? heightAnim : undefined, |
121 | | - }} |
122 | | - > |
123 | | - <KeyboardContext.Provider value={{ dismissKeyboard }}> |
124 | | - <View collapsable={false} ref={rootChannelView}> |
125 | | - {children} |
126 | | - </View> |
127 | | - </KeyboardContext.Provider> |
128 | | - </Animated.View> |
129 | | - ); |
130 | | -}; |
| 161 | + render() { |
| 162 | + const { |
| 163 | + behavior, |
| 164 | + children, |
| 165 | + contentContainerStyle, |
| 166 | + enabled, |
| 167 | + keyboardVerticalOffset, |
| 168 | + style, |
| 169 | + ...props |
| 170 | + } = this.props; |
| 171 | + const bottomHeight = enabled ? this.state.bottom : 0; |
| 172 | + switch (behavior) { |
| 173 | + case 'height': |
| 174 | + // eslint-disable-next-line no-case-declarations |
| 175 | + let heightStyle; |
| 176 | + if (this._frame != null && this.state.bottom > 0) { |
| 177 | + // Note that we only apply a height change when there is keyboard present, |
| 178 | + // i.e. this.state.bottom is greater than 0. If we remove that condition, |
| 179 | + // this.frame.height will never go back to its original value. |
| 180 | + // When height changes, we need to disable flex. |
| 181 | + heightStyle = { |
| 182 | + flex: 0, |
| 183 | + height: this._initialFrameHeight - bottomHeight, |
| 184 | + }; |
| 185 | + } |
| 186 | + return ( |
| 187 | + <KeyboardContext.Provider |
| 188 | + value={{ |
| 189 | + dismissKeyboard: this.dismissKeyboard, |
| 190 | + }} |
| 191 | + > |
| 192 | + <View |
| 193 | + onLayout={this._onLayout} |
| 194 | + ref={this.viewRef} |
| 195 | + style={StyleSheet.compose(style, heightStyle)} |
| 196 | + {...props} |
| 197 | + > |
| 198 | + {children} |
| 199 | + </View> |
| 200 | + </KeyboardContext.Provider> |
| 201 | + ); |
| 202 | + |
| 203 | + case 'position': |
| 204 | + return ( |
| 205 | + <KeyboardContext.Provider |
| 206 | + value={{ |
| 207 | + dismissKeyboard: this.dismissKeyboard, |
| 208 | + }} |
| 209 | + > |
| 210 | + <View |
| 211 | + onLayout={this._onLayout} |
| 212 | + ref={this.viewRef} |
| 213 | + style={style} |
| 214 | + {...props} |
| 215 | + > |
| 216 | + <View |
| 217 | + style={StyleSheet.compose(contentContainerStyle, { |
| 218 | + bottom: bottomHeight, |
| 219 | + })} |
| 220 | + > |
| 221 | + {children} |
| 222 | + </View> |
| 223 | + </View> |
| 224 | + </KeyboardContext.Provider> |
| 225 | + ); |
| 226 | + |
| 227 | + case 'padding': |
| 228 | + return ( |
| 229 | + <KeyboardContext.Provider |
| 230 | + value={{ |
| 231 | + dismissKeyboard: this.dismissKeyboard, |
| 232 | + }} |
| 233 | + > |
| 234 | + <View |
| 235 | + onLayout={this._onLayout} |
| 236 | + ref={this.viewRef} |
| 237 | + style={StyleSheet.compose(style, { paddingBottom: bottomHeight })} |
| 238 | + {...props} |
| 239 | + > |
| 240 | + {children} |
| 241 | + </View> |
| 242 | + </KeyboardContext.Provider> |
| 243 | + ); |
| 244 | + |
| 245 | + default: |
| 246 | + return ( |
| 247 | + <KeyboardContext.Provider |
| 248 | + value={{ |
| 249 | + dismissKeyboard: this.dismissKeyboard, |
| 250 | + }} |
| 251 | + > |
| 252 | + <View |
| 253 | + onLayout={this._onLayout} |
| 254 | + ref={this.viewRef} |
| 255 | + style={style} |
| 256 | + {...props} |
| 257 | + > |
| 258 | + {children} |
| 259 | + </View> |
| 260 | + </KeyboardContext.Provider> |
| 261 | + ); |
| 262 | + } |
| 263 | + } |
| 264 | +} |
131 | 265 |
|
132 | 266 | export default KeyboardCompatibleView; |
0 commit comments