こんにちは!かたつむり(@Katatumuri_nyan)です!
Reactを触ってみて、サイト的なものは作れるようになりました(*´ω`)
そこで、次はReactNativeを触ってみようと思い、簡単なテキストエディタを作成しようと企んでおります(笑)
今回は、ナビとエディタエリアを作成していきます(*´ω`)
GitHubでソースコードを管理しています!
最初から見る↓
【1】React Nativeでテキストエディタを作ってみる!【下調べ編】
前回を見る↓
【9】React Nativeでテキストエディタを作ってみる!【デザインからアプリの見た目作成編】
目次
前回までのできたところ
ここまでできました~!
- App.js
- TopBar.js
- Title.js
- Nav.js
- その他関数等のファイル
↑を作成ました。
↑こちらが目標なので、がんばります!
ナビの編集
Nav.js
を編集していきます!
右から左にスワイプで閉じるようにしたいです~!(*´ω`)
React Native Gesture Handlerを使っていきます。
公式ドキュメントが分かり難いので、React Native Gesture Handler: Swipe, long-press, and moreこちらを参考にしました。
サンプル作成
↓とりあえずテストを作りました
スワイプテスト✨ pic.twitter.com/XGvIpOT4Sz
— Katatumuri (@Katatumuri_nyan) July 7, 2021
↓コードはコチラ
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> ) }
スワイプ開閉するナビ完成
スワイプでナビの開閉できました✨ pic.twitter.com/HO7FSgFNTk
— Katatumuri (@Katatumuri_nyan) July 7, 2021
最終的にこのようになりました(*´ω`)
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.js
という名前で作成しました(*´ω`)
TextInput
を使っていきます。
↑こんな感じにできました!
↓コードはこちら
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
)の作成
テキストエリアで入力したマークダウンテキストを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でテキストエディタを作ってみる!【テキストエリア作成編】この時に起きたエラーと同様のエラーが起きたので、解決しておきます!
プレビューエリア作成
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
を使ったので、↑こちらの記事を参考にしました✨
テキストエリアとプレビューエリアの連携✨ pic.twitter.com/d90aelr2IR
— Katatumuri (@Katatumuri_nyan) July 7, 2021
↑こんな感じにテキストエリアに入力したものがプレビューエリアに表示されました。
useContext
を使用する関係で、App.js
をMain.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.js
でcreateContext
して、ContextObject.Provider
にContextValue
を渡しています。
こうすることで、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
にて、InputArea
とPreview
を組み込んでいます。
// 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
の値を変更することも可能です。
プレビューエリアの開閉機能の追加
プレビューエリアが左右スワイプで開閉するようにしていきたいと思います(*´ω`)
あと、真ん中がくっついてるので、そこも修正していきます!
どのエリアをスワイプしているのか調べるために、PanGestureHandler
とuseWindowDimensionsを使います!
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> ) }
↑こんな感じで開閉するようにしました(*´ω`)
キーボードをよける設定
↑こんな感じで仮想キーボードが出てきたらテキストエリアとプレビューを上に縮めます。
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でテキストエディタを作ってみる!【メニューエリア作成編】