はじめに
react-native-elements
のInput
に文字数カウンターがあれば便利だなーと思ったので自分で実装してみました。

実装した機能は以下の2つです。
- 文字数のカウント
- 入力画面と全画面表示の切替
同様の機能の実装を考えている方の参考になれば幸いです。
※iOS専用の機能のため、Androidには対応しておりません。あらかじめご了承ください。
実装
機能ごとに実装内容を記載します。
文字数カウント
文字数カウンターを表示する場所として、iOSキーボードの上部をカスタマイズするためのコンポーネントであるInputAccessoryView
を使用します。
文字数のカウントは、wordCount
というstateを用意して、入力した文字の長さ(text.lentgh
)をsetWordCount
で更新して表示するだけです。
Input
のinputAccessoryViewID
プロパティとInputAccesoryView
のnativeID
プロパティの値を共通にすることで、各コンポーネントを対応させています。
また、Androidで画面を開いたときにエラーが出ないよう、Platform.OS === 'ios'
でiOS
のときだけInputAccessoryView
が表示されるようにしています。
export const useInput = (initialValue: string) => {
const [value, setValue] = useState(initialValue);
return { value, setValue };
};
export const InputWithWordCount = () => {
const inputAccessoryViewID = '1';
const bodyProps = useInput('');
const [wordCount, setWordCount] = useState<number>(0);
const onChangeText = (text: string) => {
bodyProps.setValue(text);
setWordCount(text.length);
};
return (
<>
<ScrollView style={styles.container}>
<Input
multiline
value={bodyProps.value}
onChangeText={onChangeText}
placeholder="ここに入力してください"
inputAccessoryViewID={inputAccessoryViewID}
inputContainerStyle={{ borderBottomWidth: 0 }}
autoFocus
/>
</ScrollView>
{Platform.OS === 'ios' && (
<InputAccessoryView nativeID={inputAccessoryViewID}>
<Divider />
<View style={styles.accessoryContainer}>
<View style={styles.keyboardClose}>
<MaterialCommunityIcons
size={24}
name="keyboard-close"
color="grey"
onPress={() => Keyboard.dismiss()}
/>
</View>
<View style={styles.wordCount}>
<Text style={styles.wordText}>{wordCount}文字</Text>
</View>
</View>
</InputAccessoryView>
)}
</>
);
};
入力画面と全画面表示の切替
入力内容が長文になってしまったときに、全画面表示で確認できるようにしました。
この機能を実装するにあたって、以下の部分を工夫しました。
- 全画面表示でスクロールしたときに誤作動でキーボードが開かないこと
- 入力内容がキーボードに隠れないこと(Inputの高さも切り替える)
全画面表示でスクロールしたときに誤作動でキーボードが開かないこと
全画面表示で入力内容を確認するとき、画面スクロールでキーボードが開いてしまう挙動がありました。
ユーザにストレスがかかる挙動だったので、キーボードの開閉をアイコンタップで行うようにしました。
まず、InputAccessoryView
にアイコンを追加し、onPress={() => Keyboard.dismiss()}
でタップ時にキーボードを閉じるようにしました。
//キーボード開のときに表示
{Platform.OS === 'ios' && (
<InputAccessoryView nativeID={inputAccessoryViewID}>
<Divider />
<View
style={styles.accessoryContainer}
onLayout={(e) =>
setAccessoryViewHeight(e.nativeEvent.layout.height)
}
>
<View style={styles.keyboardClose}>
<MaterialCommunityIcons
size={24}
name="keyboard-close"
color="grey"
onPress={() => Keyboard.dismiss()} //キーボード閉じる
/>
</View>
<View style={styles.wordCount}>
<Text style={styles.wordText}>{wordCount}文字</Text>
</View>
</View>
</InputAccessoryView>
)}
次に、キーボード閉のときに表示する内容を追加し、TouchableOpacity
にonPress={() => inputRef.current?.focus()}
を加えることで、タッチしたときに入力カーソルが当たるようにしました。
このとき、const inputRef = useRef<Input>(null)
を定義し、<Input>
にref={inputRef}
の記述も加えています。
//キーボード閉のときに表示
{Platform.OS === 'ios' && keyboardHeight === 0 && (
<TouchableOpacity
onPress={() => inputRef.current?.focus()} //キーボード開く
style={{ marginHorizontal: 10, marginBottom: 30 }}
>
<Divider />
<MaterialCommunityIcons size={24} name="keyboard" color="grey" />
</TouchableOpacity>
)}
入力内容がキーボードに隠れないこと
内容が長文になってくると、入力した文字がキーボードにかかって隠れてしまいます。
とはいえ、キーボードの隠れないように<Input>
の高さを固定してしまうと、キーボードを閉じたときに内容が全画面に表示されなくなってしまいます。
これらの問題を起こさないために、キーボードの開閉で<Input>
の高さが可変するような工夫をしました。
以下の図のようにそれぞれのパーツの高さを設定して、キーボードの開閉ごとにcontentHeight
を可変させるようなイメージになります。

Input
とInputAccessoryView
の間にマージンがほしかったので、OFFSET_HEIGHTを加えました。
const SCREEN_HEIGHT = Dimensions.get('window').height; //スクリーンの高さ
const OFFSET_HEIGHT = 90; //オフセットの高さ
const [contentHeight, setContentHeight] = useState<number>(SCREEN_HEIGHT); //Inputの高さ
const [keyboardHeight, setKeyboardHeight] = useState<number>(0); //キーボードの高さ
const [accessoryViewHeight, setAccessoryViewHeight] = useState<number>(0); //InputAccessoryViewの高さ
const onKeyboardDidShow = (e: KeyboardEvent): void => {
setKeyboardHeight(e.endCoordinates.height); //キーボードの高さ(開いているとき)を設定
};
const onKeyboardDidHide = (): void => {
setKeyboardHeight(0); //キーボードの高さ(閉じているとき)を設定
};
useEffect(() => {
Keyboard.addListener('keyboardDidShow', onKeyboardDidShow);
Keyboard.addListener('keyboardDidHide', onKeyboardDidHide);
return (): void => {
Keyboard.removeListener('keyboardDidShow', onKeyboardDidShow);
Keyboard.removeListener('keyboardDidHide', onKeyboardDidHide);
};
}, []);
キーボード開閉のたびにcontentHeightを設定
useEffect(() => {
setContentHeight(
SCREEN_HEIGHT - keyboardHeight - accessoryViewHeight - OFFSET_HEIGHT
);
}, [keyboardHeight]);
Input
の高さ指定は、inputStyle
プロパティで行っています。
キーボードを閉じているときの高さはauto
にします。
<Input
inputStyle={{ height: keyboardHeight === 0 ? 'auto' : contentHeight }} //キーボード開閉のたびにheightを設定
pointerEvents={keyboardHeight === 0 ? 'none' : 'auto'} //キーボードが開いたときにカーソルをあてる
/>
また、InputAccessoryView
の高さについては、内部に新しく追加したView
のonLayout
プロパティを使用しました。
<InputAccessoryView nativeID={inputAccessoryViewID}>
<Divider />
<View
style={styles.accessoryContainer}
onLayout={(e) =>
setAccessoryViewHeight(e.nativeEvent.layout.height) //accessoryViewHeightを設定
}
>
コード全文
完成したコードを掲載します。
ご参考になれば幸いです。
import { MaterialCommunityIcons } from '@expo/vector-icons';
import React, { useState, useEffect, useRef } from 'react';
import {
View,
StyleSheet,
Text,
Keyboard,
KeyboardEvent,
InputAccessoryView,
Platform,
TouchableOpacity,
ScrollView,
Dimensions,
} from 'react-native';
import { Divider, Input } from 'react-native-elements';
export const useInput = (initialValue: string) => {
const [value, setValue] = useState(initialValue);
return { value, setValue };
};
export const InputWithWordCount = () => {
const SCREEN_HEIGHT = Dimensions.get('window').height;
const OFFSET_HEIGHT = 90;
const inputAccessoryViewID = '1';
const bodyProps = useInput('');
const [wordCount, setWordCount] = useState<number>(0);
const [contentHeight, setContentHeight] = useState<number>(SCREEN_HEIGHT);
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
const [accessoryViewHeight, setAccessoryViewHeight] = useState<number>(0);
const inputRef = useRef<Input>(null);
const onKeyboardDidShow = (e: KeyboardEvent): void => {
setKeyboardHeight(e.endCoordinates.height);
};
const onKeyboardDidHide = (): void => {
setKeyboardHeight(0);
};
useEffect(() => {
Keyboard.addListener('keyboardDidShow', onKeyboardDidShow);
Keyboard.addListener('keyboardDidHide', onKeyboardDidHide);
return (): void => {
Keyboard.removeListener('keyboardDidShow', onKeyboardDidShow);
Keyboard.removeListener('keyboardDidHide', onKeyboardDidHide);
};
}, []);
useEffect(() => {
setContentHeight(
SCREEN_HEIGHT - keyboardHeight - accessoryViewHeight - OFFSET_HEIGHT
);
}, [keyboardHeight]);
const onChangeText = (text: string) => {
bodyProps.setValue(text);
setWordCount(text.length);
};
return (
<>
<ScrollView style={styles.container}>
<Input
multiline
value={bodyProps.value}
inputStyle={{ height: keyboardHeight === 0 ? 'auto' : contentHeight }}
onChangeText={onChangeText}
placeholder="ここに入力してください"
inputAccessoryViewID={inputAccessoryViewID}
inputContainerStyle={{ borderBottomWidth: 0 }}
autoFocus
ref={inputRef}
pointerEvents={keyboardHeight === 0 ? 'none' : 'auto'}
/>
</ScrollView>
{Platform.OS === 'ios' && keyboardHeight === 0 && (
<TouchableOpacity
onPress={() => inputRef.current?.focus()}
style={{ marginHorizontal: 10, marginBottom: 30 }}
>
<Divider />
<MaterialCommunityIcons size={24} name="keyboard" color="grey" />
</TouchableOpacity>
)}
{Platform.OS === 'ios' && (
<InputAccessoryView nativeID={inputAccessoryViewID}>
<Divider />
<View
style={styles.accessoryContainer}
onLayout={(e) =>
setAccessoryViewHeight(e.nativeEvent.layout.height)
}
>
<View style={styles.keyboardClose}>
<MaterialCommunityIcons
size={24}
name="keyboard-close"
color="grey"
onPress={() => Keyboard.dismiss()}
/>
</View>
<View style={styles.wordCount}>
<Text style={styles.wordText}>{wordCount}文字</Text>
</View>
</View>
</InputAccessoryView>
)}
</>
);
};
const styles = StyleSheet.create({
container: {
marginTop: 100,
backgroundColor: '#fff',
},
accessoryContainer: {
display: 'flex',
flex: 1,
flexDirection: 'row',
marginVertical: 5,
marginHorizontal: 10,
},
keyboardClose: {
flex: 1,
},
wordCount: {
flex: 1,
alignItems: 'flex-end',
marginRight: 20,
},
wordText: {
fontSize: 16,
color: 'grey',
},
});
おわりに
サードパーティのライブラリで、Androidでも同様の機能を実装できるかどうか模索してみます。
参考資料