【10】React Nativeでテキストエディタを作ってみる!【ナビ・エディタエリア作成編】

こんにちは!かたつむり(@Katatumuri_nyan)です!

Reactを触ってみて、サイト的なものは作れるようになりました(*´ω`)
そこで、次はReactNativeを触ってみようと思い、簡単なテキストエディタを作成しようと企んでおります(笑)

今回は、ナビとエディタエリアを作成していきます(*´ω`)

GitHubでソースコードを管理しています!

最初から見る↓
【1】React Nativeでテキストエディタを作ってみる!【下調べ編】【1】React Nativeでテキストエディタを作ってみる!【下調べ編】

前回を見る↓
【9】React Nativeでテキストエディタを作ってみる!【デザインからアプリの見た目作成編】【9】React Nativeでテキストエディタを作ってみる!【デザインからアプリの見た目作成編】

前回までのできたところ

picture 1
ここまでできました~!

  • App.js
  • TopBar.js
  • Title.js
  • Nav.js
  • その他関数等のファイル

↑を作成ました。

picture 8
↑こちらが目標なので、がんばります!

ナビの編集

picture 2

Nav.jsを編集していきます!
右から左にスワイプで閉じるようにしたいです~!(*´ω`)

React Native Gesture Handlerを使っていきます。

公式ドキュメントが分かり難いので、React Native Gesture Handler: Swipe, long-press, and moreこちらを参考にしました。

サンプル作成

↓とりあえずテストを作りました

↓コードはコチラ

import React from 'react';
import { Text, View, SafeAreaView} from 'react-native';
import { useTheme } from 'react-native-elements';
import Nav from './_components/Nav/Nav';
import Title from './_components/Title';
import Swipeable from 'react-native-gesture-handler/Swipeable';



export default function TopBar(props) {
    const { theme } = useTheme();

    const style ={
        position: 'relative',
        width: '100%',
        justifyContent:'center',
        alignItems: 'center',
    }

    const LeftSwipeActions =()=>{
        return(
            <View
                style={{
                    paddingHorizontal: 30,
                    paddingVertical: 20,
                    backgroundColor: 'red',
                }}
            >
                <Text style={{ fontSize: 24 }} style={{ fontSize: 20 }}>
                    LeftSwipeActions
                </Text>
            </View>
        )
    }

    const rightSwipeActions = () => {
        return (
            <View
                style={{
                    paddingHorizontal: 30,
                    paddingVertical: 20,
                    backgroundColor: 'pink',
                }}
            >
                <Text style={{ fontSize: 24 }} style={{ fontSize: 20 }}>
                    rightSwipeActions
                </Text>
            </View>
        )

    }

    const swipeFromRightOpen = () => {
        alert('Swipe from right');
    }

    const swipeFromLeftOpen = () => {
        alert('Swipe from left');
    }

    return(
        <SafeAreaView style={style}>
            <Nav/>
            <Title title={props.title}/>


            <Swipeable
                renderLeftActions={LeftSwipeActions}
                renderRightActions={rightSwipeActions}
                onSwipeableRightOpen={swipeFromRightOpen}
                onSwipeableLeftOpen={swipeFromLeftOpen}
            >
                <View
                    style={{
                        paddingHorizontal: 30,
                        paddingVertical: 20,
                        backgroundColor: 'white',
                    }}
                >
                    <Text style={{ fontSize: 24 }} style={{ fontSize: 20 }}>
                        text
                    </Text>
                </View>
            </Swipeable>


        </SafeAreaView>
    )
}

スワイプ開閉するナビ完成

最終的にこのようになりました(*´ω`)
Swipeableではなく、PanGestureHandlerをつかいました~!

できたコードは↓こちら

// Nav.js
import { PanGestureHandler} from 'react-native-gesture-handler';
export default function Nav(props) {
  const [isNavOpen, setIsNavOpen] = useState(false)
  let { theme } = useTheme();

  function onNavOpen() {
    setIsNavOpen(true)
  }

  function onNavClose() {
    console.log("swipe");
    setIsNavOpen(false)
  }

  function onSwipeEvent(event){
    const swipeX = event.nativeEvent.translationX

    if (swipeX <= 34) {
      onNavClose()
    } else if (34 < swipeX) {
      onNavOpen()
    }
  }

  const styles = {
    navContainer: {
      position: 'absolute',
      left: 10,
      top: 10,
      backgroundColor: 'pink',
      backgroundColor: theme.main.secondBackgroundColor,
      borderRadius: 20
    }
  }

    return (
      <PanGestureHandler onGestureEvent={(event) => { onSwipeEvent(event) }}>
        <View style={styles.navContainer}>
          {isNavOpen?
          <NavOpened color={theme.nav.iconColor} />:
          <NavClosed color={theme.nav.iconColor} onPress={onNavOpen} />
          }
        </View >
      </PanGestureHandler>
    )
}

エディタエリア(InputArea.js)の作成

InputArea
↑こちらの白いエリアを作ります。

InputArea.jsという名前で作成しました(*´ω`)

TextInputを使っていきます。

picture 3
↑こんな感じにできました!
↓コードはこちら

import React from 'react';
import {useState} from 'react';
import { TextInput} from 'react-native';
import { useTheme } from 'react-native-elements';

export default function InputArea(props) {
    const { theme } = useTheme();
    const [value, onChangeText] = useState(props.value);

    function onChange(text) {
        onChangeText(text)
    }

    const style = {
        flex: 1,
        backgroundColor: theme.textView.backgroundColor,
        color: theme.textView.textColor,
        padding: 20,
        paddingTop: 10,
        borderRadius: 20,
    }

    return (
        <TextInput
            style={style}
            multiline={true}
            scrollEnabled={true}
            textAlignVertical='top'
            onChangeText={text => onChange(text)}
            placeholder="Hello World!"
            value={value}
        />
    )
}

他のコンポーネントとの連携はあまり考えずに作っています。
そこはコンポーネントを追加した時に連携させていきます!

App.jsの方でもいくらかスタイル等を設定しました!
↓こちらに一部のせておきます

// App.js
export default function App() {
    /// 省略
  const styles = {
    app: {
      flex: 1,
      flexDirection: 'column',
      height: '100%',
      backgroundColor: theme[appTheme].main.mainBackgroundColor,
      alignItems: 'center'
    },
    editorArea:{
      flex: 1,
      padding: 20,
      paddingTop: 0,
      width: '100%'
    }
    }

  return (
    <ThemeProvider theme={theme[appTheme]}>
      <FileDataGetter.Provider value={fileDataGetterValue}>
        <SafeAreaView style={styles.app}>
          <TopBar
            title={title}
          />
          <EditorArea style={styles.editorArea}/>
        </SafeAreaView>
       </FileDataGetter.Provider>
     </ThemeProvider>
  );
}


function EditorArea(props) {
  return (
    <View style={props.style}>
      <InputArea />
    </View>
  )
}

エディタエリア(Preview.js)の作成

image 2
テキストエリアで入力したマークダウンテキストをHTMLに変換してプレビューするエリアをつくります。
↑上の画像で言うと右側のエリアです。

iamacup/react-native-markdown-displayこちらを使用していきます!

> npm install --save react-native-markdown-display
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! 
npm ERR! While resolving: undefined@undefined
npm ERR! Found: react@17.0.2
npm ERR! node_modules/react
npm ERR!   react@"17.0.2" from the root project
npm ERR! 
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^16.2.0" from react-native-markdown-display@7.0.0-alpha.2
npm ERR! node_modules/react-native-markdown-display
npm ERR!   react-native-markdown-display@"*" from the root project
npm ERR! 
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR! 
npm ERR! See /home/mymai/.npm/eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/mymai/.npm/_logs/2021-07-07T04_44_33_558Z-debug.log

↑この様なエラーが出たのでERESOLVE unable to resolve dependency treeの解決方法をを参考にインストールしました。
npm install react-native-markdown-display --save --legacy-peer-deps

【5】React Nativeでテキストエディタを作ってみる!【テキストエリア作成編】この時に起きたエラーと同様のエラーが起きたので、解決しておきます!

プレビューエリア作成

picture 4

Preview.jsのcodeはとりあえず以下のようになりました。

import React from 'react';
import { useState } from 'react';
import { ScrollView} from 'react-native';
import { useTheme } from 'react-native-elements';
import Markdown from 'react-native-markdown-display';

export default function Preview(props) {
    const { theme } = useTheme();
    const [value, onChangeText] = useState('プレビュー');

    const styles = {
        container: {
            flex: 1,
            backgroundColor: theme.textView.backgroundColor,
            padding: 20,
            paddingTop: 10,
            borderRadius: 20,
        },
        text: {
            body:{
                color: theme.textView.textColor,
            }
        }
    }

    return (
        <ScrollView style={styles.container}>
            <Markdown style={styles.text}>{value}</Markdown>
        </ScrollView>
    )
}

InputArea.jsとの連携

InputArea.jsのテキストエリアに入力した値をPreview.jsに反映するようにします。

createContextしたファイルでProviderを使わないほうが良い – it’s better not to use Provider and use createContext in same file
How to Use Context API with Hooks Efficiently While Avoiding Performance Bottlenecks
useContext + useState 利用時のパフォーマンスはProviderの使い方で決まる!かも。。。?

useContextを使ったので、↑こちらの記事を参考にしました✨

↑こんな感じにテキストエリアに入力したものがプレビューエリアに表示されました。

useContextを使用する関係で、App.jsMain.jsとし、新しくApp.jsを作成しました。

また、context.jsというファイルを作成し、そこでコンテキストの設定をしました。

// context.js
import React,{ createContext, useState} from 'react';

export const ContextObject = createContext()

export function ContextProvider(props) {
    const [appTheme, setAppTheme] = useState("Night")
    const [title, setTitle] = useState("Title")
    const [text, setText] = useState("")

    console.log(text);

    const ContextValue = {
        appTheme,
        setAppTheme,
        title,
        setTitle,
        text,
        setText
    }

    return (
        <ContextObject.Provider value={ContextValue}>
            {props.children}
        </ContextObject.Provider>)
}

context.jscreateContextして、ContextObject.ProviderContextValueを渡しています。
こうすることで、ContextProviderコンポーネントの子孫コンポーネントは、どこからでもContextValueの値を読み取ったり、Stateの変更ができるようになります。

// App.js
import React from 'react';
import { ContextProvider } from './modules/context';
import Main from './components/Main';

export default function App() {
  return (
    <ContextProvider>
      <Main/>
    </ContextProvider>
  );
}

App.jsで、MainコンポーネントをContextProviderコンポーネントで包みました。
これで、Main全体で値を共有できます。

// Main.js
import React from 'react';
// 省略

export default function Main() {
    // 省略
 return (
    <ThemeProvider theme={theme[appTheme]}>
      <StatusBar hidden={false}/>
        <SafeAreaView style={styles.app}>
          <TopBar
            title={title}
          />
          <EditorArea style={styles.editorArea}/>
        </SafeAreaView>
     </ThemeProvider>
  );
}

function EditorArea(props) {
  return (
    <View style={props.style}>
      <InputArea />
      <Preview/>
    </View>
  )
}

Main.jsにて、InputAreaPreviewを組み込んでいます。

// InputArea.js
import React, { useContext} from 'react';
import { ContextObject } from '../../modules/context';

export default function InputArea() {
    const { theme } = useTheme();
    const {
        text,
        setText
    } = useContext(ContextObject)

    function onChange(text) {
        setText(text)
    }

// 省略

    return (
        <TextInput
            style={style}
            multiline={true}
            scrollEnabled={true}
            textAlignVertical='top'
            onChangeText={text => onChange(text)}
            placeholder="Hello World!"
            value={text}
        />
    )
}
// Preview.js
import React, { useContext } from 'react';
import { ContextObject } from '../../modules/context';

export default function Preview() {
    const { theme } = useTheme();
    const { text } = useContext(ContextObject)

  // 省略

    return (
        <ScrollView style={styles.container}>
            <Markdown style={styles.text}>{text}</Markdown>
        </ScrollView>
    )
}

InputArea.js``Preview.jsでは、useContextを使うことでtextの値をとってきています。
setTextで、textの値を変更することも可能です。

プレビューエリアの開閉機能の追加

プレビューエリアが左右スワイプで開閉するようにしていきたいと思います(*´ω`)
あと、真ん中がくっついてるので、そこも修正していきます!

どのエリアをスワイプしているのか調べるために、PanGestureHandleruseWindowDimensionsを使います!

EditorArea.jsを作成し、Main.jsにあったEditorAreaコンポーネントを分けました。

// EditorArea.js
import React from 'react';
import { useContext } from 'react';
import { View } from 'react-native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import { ContextObject } from '../../modules/context';
import InputArea from './InputArea/InputArea';
import Preview from './Preview/Preview';



export default function EditorArea(props) {
    const {
        windowWidth,
        isPreviewOpen,
        setIsPreviewOpen,
        setAbsoluteX
    } = useContext(ContextObject)

    const style = {
        flex: 1,
        flexDirection: 'row',
        position: 'relative',
        padding: 20,
        paddingTop: 0,
        width: '100%',
        height: '100%'
    }

    function onSwipeEvent(event) {
        const absoluteX = event.nativeEvent.absoluteX
        const previewWidth = windowWidth / 2
        const swipeX = event.nativeEvent.translationX
        const rightArea = previewWidth <= absoluteX

        if (rightArea && swipeX < 0){
            // (←)画面右半分を右から左にスワイプした時
            setIsPreviewOpen(true)
            setAbsoluteX(absoluteX)
        } else if (rightArea && swipeX > 0){
            // (→)画面右半分を左から右にスワイプした時
            setIsPreviewOpen(false)
            setAbsoluteX(absoluteX)
        }
    }


    return (
        <PanGestureHandler onGestureEvent={(event) => { onSwipeEvent(event) }}>
            <View style={style}>
                <InputArea />
                {isPreviewOpen ? <Preview />:<View/>}
            </View>
        </PanGestureHandler>
    )
}

↑こんな感じで開閉するようにしました(*´ω`)

キーボードをよける設定

picture 5
↑こんな感じで仮想キーボードが出てきたらテキストエリアとプレビューを上に縮めます。

iOSの時、仮想キーボードがあっても勝手に縮んでくれないので、KeyboardAvoidingViewを使用して実装しました。

// Main.js
const os = Device.osName
const [keyboardAvoidingViewEnabled, setKeyboardAvoidingViewEnabled] = useState(true)

useEffect(() => {
  Keyboard.addListener('keyboardWillShow', keyboardWillShow);
  Keyboard.addListener('keyboardWillHide', keyboardWillHide);
}, []);

function keyboardWillHide() {
  setKeyboardAvoidingViewEnabled(false)
}

function keyboardWillShow() {
  setKeyboardAvoidingViewEnabled(true)
}

return (
      <ThemeProvider theme={theme[appTheme]}>
        <StatusBar hidden={false}/>
        <SafeAreaView style={styles.viwe}>
          <Pressable style={styles.viwe} onPress={Keyboard.dismiss}>
            <KeyboardAvoidingView
            behavior={Platform.OS == 'ios' ? 'padding' : 'height'}
            style={styles.app}
            keyboardVerticalOffset={Platform.OS == 'ios' ? '10' : '0'}
            enabled={keyboardAvoidingViewEnabled}
            >
              <TopBar
              title={title}
              />
              <EditorArea/>
            </KeyboardAvoidingView>
          </Pressable>
        </SafeAreaView>
      </ThemeProvider>
);

今回はこんな感じで終わります(*´ω`)
次はメニューコンポーネントを作っていきます!

↓続き
【11】React Nativeでテキストエディタを作ってみる!【メニューエリア作成編】【11】React Nativeでテキストエディタを作ってみる!【メニューエリア作成編】

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です