1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React Native】キーボードへの文字数カウンターの実装(iOS)

Last updated at Posted at 2021-04-11

はじめに

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

実装した機能は以下の2つです。

  • 文字数のカウント
  • 入力画面と全画面表示の切替

同様の機能の実装を考えている方の参考になれば幸いです。
※iOS専用の機能のため、Androidには対応しておりません。あらかじめご了承ください。

実装

機能ごとに実装内容を記載します。

文字数カウント

文字数カウンターを表示する場所として、iOSキーボードの上部をカスタマイズするためのコンポーネントであるInputAccessoryViewを使用します。
文字数のカウントは、wordCountというstateを用意して、入力した文字の長さ(text.lentgh)をsetWordCountで更新して表示するだけです。

InputinputAccessoryViewIDプロパティとInputAccesoryViewnativeIDプロパティの値を共通にすることで、各コンポーネントを対応させています。

また、Androidで画面を開いたときにエラーが出ないよう、Platform.OS === 'ios'iOSのときだけInputAccessoryViewが表示されるようにしています。

index.tsx
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()}でタップ時にキーボードを閉じるようにしました。

index.tsx
      //キーボード開のときに表示
      {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>
      )}

次に、キーボード閉のときに表示する内容を追加し、TouchableOpacityonPress={() => inputRef.current?.focus()}を加えることで、タッチしたときに入力カーソルが当たるようにしました。

このとき、const inputRef = useRef<Input>(null)を定義し、<Input>ref={inputRef}の記述も加えています。

index.tsx
      //キーボード閉のときに表示
      {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を可変させるようなイメージになります。

InputInputAccessoryViewの間にマージンがほしかったので、OFFSET_HEIGHTを加えました。

index.tsx
  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の高さについては、内部に新しく追加したViewonLayoutプロパティを使用しました。

        <InputAccessoryView nativeID={inputAccessoryViewID}>
          <Divider />
          <View
            style={styles.accessoryContainer}
            onLayout={(e) =>
              setAccessoryViewHeight(e.nativeEvent.layout.height) //accessoryViewHeightを設定
            }
          >

コード全文

完成したコードを掲載します。
ご参考になれば幸いです。

index.tsx
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でも同様の機能を実装できるかどうか模索してみます。

参考資料

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?