発生した問題
- 複数行を入力できる TextInput を作成したら、 TextInput がキーボードの後ろの隠れてしまい、入力している内容が見えなくなった
- TextInput の下に置いた View の内容もキーボードに隠れてしまった
うまく動いたコード
ScrollView
の style
に marginBottom
を設定したら、キーボード分の余白が下に設定されました。
抜粋すると以下のような ScrollView
でコンテンツを囲みました。
Sample.tsx
const keyboardHeight = useKeyboardHeight();
return (
<ScrollView style={{ marginBottom: keyboardHeight }}>
<TextInput
こうすると、Viewの下の方に余白ができて、キーボードが表示されてもView全体が見えるようになりました。
キーボードの高さは以下の hook を使って取得しています。
コード全体は以下の通りです。関係ない部分も入っているので、参考までに。
NoteDetailScreen.tsx
import { updateNoteBody, updateNoteTitle } from '../../databases/repositories/noteRepository';
import { useKeyboardHeight } from '../../hooks/useKeyboardHeight';
import { ReloadTabState } from '../../states/atoms/ReloadTabState';
import { MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import * as Clipboard from 'expo-clipboard';
import * as Haptics from 'expo-haptics';
import { useEffect, useState } from 'react';
import {
Keyboard,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableWithoutFeedback,
View,
} from 'react-native';
import 'react-native-get-random-values';
import Toast from 'react-native-toast-message';
import { useSetRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
export default function NoteDetailScreen({ route }: any) {
const setReloadTabState = useSetRecoilState(ReloadTabState);
const noteTitle = route.params.note.title;
const noteBody = route.params.note.body;
const noteDate = route.params.note.date;
const [title, setTitle] = useState<string>(noteTitle);
const [body, setBody] = useState<string>(noteBody);
const [date] = useState<string>(noteDate);
const onChangeTitle = async (title: string) => {
setTitle(title);
updateNoteTitle(route.params.note.id, title).then((note) => {});
};
const onChangeBody = (body: string) => {
setBody(body);
updateNoteBody(route.params.note.id, body).then((note) => {});
};
const countLines = (str: string) => {
return str.split(/\r\n|\r|\n/).length;
};
const navigation = useNavigation();
useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
setReloadTabState(uuidv4());
console.log('Navigating away from NoteDetailScreen');
});
return unsubscribe;
}, [navigation]);
const keyboardHeight = useKeyboardHeight();
return (
<ScrollView style={{ marginBottom: keyboardHeight }}>
<TextInput
onChangeText={onChangeTitle}
value={title}
style={styles.titleInput}
autoFocus={title.length === 0}
placeholder={'Title'}
/>
<View style={styles.bodyContainer}>
<TextInput
onChangeText={onChangeBody}
value={body}
multiline={true}
style={styles.bodyInput}
placeholder={'Note'}
scrollEnabled={false}
autoFocus={title.length > 0}
/>
<MaterialIcons
name="content-copy"
size={24}
color="gray"
style={styles.copyIcon}
onPress={() => {
Clipboard.setStringAsync(body).then(() => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Toast.show({
type: 'success',
text1: 'Copied to clipboard',
visibilityTime: 1000,
});
});
}}
/>
{(countLines(body) > 8 || body.length >= 100) && (
<MaterialCommunityIcons
name="keyboard-close"
size={24}
color="gray"
style={styles.dismissKeyboardIcon}
onPress={Keyboard.dismiss}
/>
)}
<Toast position={'bottom'} />
</View>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.dateAndCharCountContainer}>
<Text style={styles.labelText}>{date}</Text>
<Text style={styles.labelText}>{body.length}</Text>
</View>
</TouchableWithoutFeedback>
</ScrollView>
);
}
const styles = StyleSheet.create({
scrollContainer: {
flex: 1,
},
container: {
flex: 1,
},
titleInput: {
width: '100%',
height: 50,
fontSize: 25,
fontWeight: 'bold',
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
bodyContainer: {
position: 'relative',
},
bodyInput: {
width: '100%',
fontSize: 18,
paddingHorizontal: 16,
paddingVertical: 16,
marginBottom: 24,
},
dismissKeyboardIcon: {
position: 'absolute',
bottom: 10,
right: 10,
},
copyIcon: {
position: 'absolute',
top: 10,
right: 10,
},
dateAndCharCountContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
borderTopWidth: 1,
borderTopColor: '#ccc',
borderBottomWidth: 1,
borderBottomColor: '#ccc',
marginBottom: 16,
},
labelText: {
fontSize: 15,
color: '#666',
},
});
うまく動かなかったコード
KeyboardAvoidingView
を使っても、予想していた「キーボードが出てきたら、キーボードを避けて上の方ににゅるっと動いてくれる」ような動作にはなりませんでした。
Sample.tsx
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<TextInput
onChangeText={onChangeTitle}
value={title}
style={styles.titleInput}
autoFocus={true}
placeholder={'Title'}
/>
<View style={styles.bodyContainer}>
<TextInput
onChangeText={onChangeBody}
value={body}
multiline={true}
style={styles.bodyInput}
placeholder={'Note'}
/>
<MaterialIcons
name="content-copy"
size={24}
color="gray"
style={styles.copyIcon}
onPress={() => {
Clipboard.setStringAsync(body).then(() => {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
Toast.show({
type: 'success',
text1: 'Copied to clipboard',
visibilityTime: 1000,
});
});
}}
/>
{(countLines(body) > 8 || body.length >= 100) && (
<MaterialCommunityIcons
name="keyboard-close"
size={24}
color="gray"
style={styles.dismissKeyboardIcon}
onPress={Keyboard.dismiss}
/>
)}
<Toast position={'bottom'} />
</View>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.dateAndCharCountContainer}>
<Text style={styles.labelText}>{date}</Text>
<Text style={styles.labelText}>{body.length}</Text>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
);
useHeaderHeight でヘッダーの高さを取得してもうまく動かなかった
調べていると、 useHeaderHeight
でヘッダーの高さを取得して、 keyboardVerticalOffset
を設定すればいい、という話もありました。
shell
npm install @react-navigation/elements
以下のようにすれば、 headerHeight が取れるらしいです。
Sample.tsx
import { useHeaderHeight } from '@react-navigation/elements';
// ...
const headerHeight = useHeaderHeight();
それで、 KeyboardAvoidingView
に keyboardVerticalOffset
に headerHeight
を設定しなさい、という記事もありましたが、これはうまく動きませんでした。