Skip to content

Commit 462b635

Browse files
Revamp of KeyboardCompatibleView
1 parent ec9dd47 commit 462b635

File tree

4 files changed

+275
-126
lines changed

4 files changed

+275
-126
lines changed
Lines changed: 252 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,266 @@
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';
52

3+
import React from 'react';
4+
import {
5+
AppState,
6+
Keyboard,
7+
LayoutAnimation,
8+
Platform,
9+
StyleSheet,
10+
View,
11+
} from 'react-native';
612
import { KeyboardContext } from '../../context';
713

814
/**
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.
1517
*
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.
2219
*/
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+
}
59140

60141
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', () => {
63143
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();
72148
});
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();
114159
}
115160

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+
}
131265

132266
export default KeyboardCompatibleView;

0 commit comments

Comments
 (0)