KeyboardAvoidingView
Demo Video 1
Implementation
When building mobile application, the main headache of using an input field is that the keyboard can hide your form.
The first thing we learn is KeyboardAvoidingView
, which in the past I have created one component to abstract every detail that I don't want to care again:
// KeyboardPushedView import { KeyboardAvoidingView, Platform, View } from "react-native"; import { ReactNode } from "react" import WbView from "./WbView"; import { ScrollView } from "react-native"; import useKeyboardVisible from "../hooks/useKeyboardVisible"; export default ({ formEle, bottomEle, keyboardVerticalOffset = 60, backgroundColor }: { formEle: ReactNode, bottomEle: ReactNode, keyboardVerticalOffset?: number, backgroundColor?: string }) => { const { isKeyboardVisible } = useKeyboardVisible(); const backgroundStyle: { backgroundColor?: string } = {} if (backgroundColor) { backgroundStyle.backgroundColor = backgroundColor; } return ( <KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : "height"} style={{ flex: 1 }} keyboardVerticalOffset={keyboardVerticalOffset} > <WbView style={{ height: "100%", ...backgroundStyle }} > <ScrollView showsVerticalScrollIndicator={false}> <View style={{ flex: 1, justifyContent: "flex-end" }}> {formEle} <View style={{ flex: 1 }} /> </View > </ScrollView> {!isKeyboardVisible && bottomEle} </WbView> </KeyboardAvoidingView> ) }
So the next time I want to make a Stack.Screen
that avoids the input being blocked by the keyboard, I can simply write:
const Login = () => { ... return ( <KeyboardPushedView backgroundColor='white' keyboardVerticalOffset={0} formEle={ <InputFieldAndButtons /> } bottomEle={ <></> } /> ) }
Case When KeyboardAvoidingView Would not Help, Especially in Chat App ...
What if I want the input field to be sticky with the keyboard just like any existing messaging applications? KeyboardAvoidingView
is not an answer here. There are two solutions for this scenario.
Solution 1. InputAccessoryView
Demo Video 2
Implementation with Highlights and Explanation
1const MultiChat = ({ 2 quitRoomAction, 3 sort, 4 sessionId, 5 inverted, 6 showKeyboard, 7 showCachedMessages = false, 8 hasHeader, 9 shouldEstablishSocketConnection, 10 enableDeleteMessage, 11 input, 12}: { 13 quitRoomAction: () => void, 14 sort: "asc" | "desc", 15 sessionId: string, 16 inverted: boolean, 17 showKeyboard: boolean, 18 hasHeader: boolean, 19 showCachedMessages?: boolean, 20 shouldEstablishSocketConnection: boolean, 21 enableDeleteMessage: boolean, 22 input?: ReactNode 23}) => { 24 const keyboardActive = useAppSelector(s => s.chat.ongoingSessionInfo.roomKeyboardActive);
-
In line 24 we need a boolean to control the existence of the input component as shown in our video above.
We save that boolean in the redux store in order to control it wherever we want.
-
A similar example can be found in discord mobile app, in which you swipe to the center to chat, and you swipe to the left to view channels and the keyboard is closed automatically.
25 const dispatch = useAppDispatch(); 26 const { addPageCount, count, resetPageCount } = usePageCount(); 27 28 const { flatListRef, messages, audioStartPlay } = useInitChatroom({ 29 quitRoomAction, 30 sort, 31 sessionId, 32 shouldEstablishSocketConnection, 33 showCachedMessages 34 }); 35 const InputComponent = useCallback(() => input, []) 36 37 useEffect(() => { 38 resetPageCount() 39 return () => { 40 resetPageCount() 41 } 42 }, []) 43 44 useEffect(() => { 45 return () => { 46 dispatch(chatSlice.actions.resetOngoingSession()); 47 } 48 }, []) 49 50 useEffect(() => { 51 if (showKeyboard) { 52 dispatch(chatSlice.actions.setRoomKeyboardActive(showKeyboard)); 53 } 54 return () => { 55 dispatch(chatSlice.actions.setRoomKeyboardActive(true)); 56 } 57 }, []) 58 59 return ( 60 <View style={{ position: "relative", flex: 1 }}> 61 <CompLabel componentName="MultiChat" /> 62 <View style={{ 63 flexDirection: "column", 64 flex: 1, 65 alignItems: "center", 66 }}> 67 {hasHeader && <MultiChatHeader />} 68 <FlatList 69 contentContainerStyle={{ paddingBottom: 60 }} 70 persistentScrollbar 71 keyboardShouldPersistTaps='handled' 72 automaticallyAdjustKeyboardInsets={true} 73 showsVerticalScrollIndicator={true}
- In line 72 we need to set
automaticallyAdjustKeyboardInsets={true}
to let the height ofScrollView
be adjustable when keyboard popups.
74 onEndReachedThreshold={0.2} 75 onEndReached={async() => { 76 await dispatch(ChatThunkAction.getRoomMessagesHistory({ 77 roomId: sessionId || "", 78 page: count, 79 sort: sort, 80 showCachedMessages 81 })).unwrap() 82 addPageCount(); 83 }} 84 onScrollToIndexFailed={info => { 85 const wait = new Promise(resolve => setTimeout(resolve, 500)); 86 wait.then(() => { 87 if (messages.length === 0) { 88 return; 89 } 90 flatListRef.current?.scrollToIndex({ index: info.index, animated: true }); 91 }); 92 }} 93 initialScrollIndex={0} 94 inverted={inverted} 95 style={{ height: "100%", width: "100%", marginBottom: 0, paddingBottom: 0, }} 96 data={messages} 97 keyExtractor={item => item.uuid} 98 renderItem={({ item, index }) => { 99 return ( 100 <View key={item.uuid} > 101 <MessageRow 102 enableDelete={enableDeleteMessage} 103 message={item} 104 playAudio={audioStartPlay} 105 /> 106 </View> 107 ) 108 }} 109 /> 110 {Platform.OS === "ios" && keyboardActive && ( 111 <InputAccessoryView> 112 <InputComponent /> 113 </InputAccessoryView> 114 )} 115 {Platform.OS !== "ios" && keyboardActive && ( 116 <InputComponent /> 117 )} 118 </View > 119 </View> 120 ) 121}
- Lastly, note that
InputAccessoryView
is iOS-specific. The stickyless of inputfield right above the keyboard is natively supported in Android.
Caveat of InputAccessoryView
-
As shown in the demo video, our
InputAccessoryView
cannot be animated byPanGesture
, i.e., not controllable byreact-reanimated
. -
When there is highly customized pan gesture involved for page transition, the logic of closedness and openedness of the custom inputfield will also be error-prone.
-
When the
InputAccessoryView
is embedded into a bottomsheet, thenInputAccessoryView
may still persist while transitioning to second bottomsheet.Unless you kill/dismiss the first bottomsheet (that contains
InputAccessoryView
), but that means you can't return from the second.
Solution 2. Treat Input Element as a Sticky Header of the (Inverted) FlatList
Demo Video 3
(Here I scroll up to remove the keyboard)
Implementation with Highlights
With the same code as above, we replace the FlatList
element with emphasis on the highlighted lines:
68 <FlatList 69 ref={flatListRef} 70 persistentScrollbar 71 ListHeaderComponent={InputComponent} 72 stickyHeaderIndices={[0]} 73 invertStickyHeaders={false} 74 keyboardShouldPersistTaps='handled' 75 automaticallyAdjustKeyboardInsets={true} 76 showsVerticalScrollIndicator={true} 77 onEndReachedThreshold={0.2} 78 onEndReached={onEndReached_} 79 onScrollToIndexFailed={info => { 80 const wait = new Promise(resolve => setTimeout(resolve, 500)); 81 wait.then(() => { 82 if (messages.length === 0) { 83 return; 84 } 85 flatListRef.current?.scrollToIndex({ index: info.index, animated: true }); 86 }); 87 }} 88 initialScrollIndex={0} 89 inverted={inverted} 90 style={{ height: "100%", width: "100%", marginBottom: 0, paddingBottom: 0, }} 91 data={messages} 92 keyExtractor={item => item.uuid} 93 renderItem={({ item, index }) => { 94 return ( 95 <View key={item.uuid} > 96 <MessageRow 97 enableContextMenu={enableDeleteMessage} 98 message={item} 99 playAudio={audioStartPlay} 100 /> 101 </View> 102 ) 103 }} 104 onScroll={e => { 105 const currentOffset = e.nativeEvent.contentOffset.y; 106 const scrollUp = currentOffset > prevOffetRef.current; 107 if (scrollUp) { 108 Keyboard.dismiss(); 109 } 110 prevOffetRef.current = currentOffset; 111 }} 112 />
Now the input element is part of the FlatList
, and it can be animated by the pan-gesture handler. We don't need to worry about the blockage of the keyboard to other views transitted by pan-guesture any more.
Conclusion
-
When your view does not involve highly complicated pan-gesture animation (e.g., every page transition is simply controlled by stack-navigation), go
InputAccessoryView
. -
Otherwise, treat the input element as the sticky header in a
FlatList
/ScrollView
to avoid potential issues caused by the unexpected persistence of the input element.