Expo.ioMaterial DesignReactReact Native React Native PaperReact Navigation

[ReactNative/Expo] Paper と Navigation v5 で Twitter Clone アプリを作成する〜初心者向け〜

scarf-4849441_1920 Expo.io
スポンサーリンク
スポンサーリンク

FAB と Portal

ポータルで他の要素の上に表示されるコンテンツをレンダリングできるため、FAB と組み合わせてタブを変更したときに FAB のアイコンをスムーズにアニメーション化し、特定のスクリーンで FAB を完全に非表示にします。

まず、すべてのタブに FAB をレンダリングします。

src/BottomTabNavigator.js

下記のように変更します。

// BottomTabNavigator
import React from “react”;
import { createMaterialBottomTabNavigator } from “@react-navigation/material-bottom-tabs”;
+ import { useTheme, Portal, FAB } from “react-native-paper”;

return (
+ <React.Fragment>
<Tab.Navigator
initialRouteName=”Feed”

</Tab.Navigator>
+ <Portal>
+ <FAB
+ icon=”feather”
+ style={{
+ position: “absolute”,
+ bottom: 100,
+ right: 16
+ }}
+ />
+ </Portal>
+ </React.Fragment>

);
};

FAB はすべてのタブで表示されてしまいます。

次は、Feed → Details スクリーンに移動するたびに FAB ボタンを非表示に変更します。

現在のナビゲーションの構造は下記の通りです。

  • 二つのスクリーンを持つ Stack Navigator
  • Stack Navigator の最初のスクリーンは、3つのタブを持つ Tab Navigator をレンダリング
  • Stack Navigator の 2 番目のスクリーンは、ツイートの詳細を表示

それでは、useIsFocused フックを使用して Tab Navigator がフォーカスされた場合のみ、FAB ボタンを表示するように BottomTabNavigator.js に設定を追加します。

// src/BottomTabNavigator.js

import { createMaterialBottomTabNavigator } from “@react-navigation/material-bottom-tabs”;
import { useTheme, Portal, FAB } from “react-native-paper”;
+ import { useIsFocused } from “@react-navigation/native”;
import { Feed } from “./Feed”;

export const BottomTabNavigator = () => {
+ const isFocused = useIsFocused();
const theme = useTheme();
const tabBarColor = theme.dark

</Tab.Navigator>
<Portal>
<FAB
+ visible={isFocused}
icon=”feather”

次は、route オブジェクトを利用して、Messages タブが選択された場合のみ、FAB ボタンを別のアイコンに変更します。

  • routeName に現在のスクリーンの情報を取得し、switch 文を使用して、現在スクリーンが Messages の場合、アイコンを別のものに変更します。
  • アイコンの位置を safeArea.bottom から指定 & テーマカラーを適用します。
  • NotificationsMessages スクリーンのそれぞれの header 名を表示出来るようにします。( 現在 Twitter アイコンが表示中 )
// src/BottomTabNavigator.js


+ import { useSafeArea } from “react-native-safe-area-context”;


– export const BottomTabNavigator = () => {
+ export const BottomTabNavigator = props => {
+ const safeArea = useSafeArea();
const isFocused = useIsFocused();

const tabBarColor = theme.dark
? overlay(6, theme.colors.surface)
: theme.colors.surface;

+ const routeName = props.route.state
+ ? props.route.state.routes[props.route.state.index].name
+ : “Feed”;

+ let icon = “feather”;
+ switch (routeName) {
+ case “Messages”:
+ icon = “email-plus-outline”;
+ break;
+ default:
+ icon = “feather”;
+ break;
+ }

return (
<React.Fragment>
..

</Tab.Navigator>
<Portal>
<FAB
visible={isFocused}
– icon=”feather”
+ icon={icon}
style={{
position: “absolute”,
bottom: 100,
+ bottom: safeArea.bottom + 65,
right: 16
}}
+ color=”white”
+ theme={{
+ colors: {
+ accent: theme.colors.primary
+ }
+ }}
+ onPress={() => {}}

/>
</Portal>

出来ました。

スポンサーリンク

テーマを設定

React Navigation v5React Native Pape の両方が Light / Dark テーマ をサポートしています。ここでは、テーマの設定RTL 設定を行います。

iOS、Android、Web のカラースキーム ( Light / Dark ) 情報にアクセス出来る react-native-appearance をインストールします。

try🐶everything twitter-clone-example$ expo install react-native-appearance

必要なファイルを作成します。

try🐶everything twitter-clone-example$ mkdir ./src/context
try🐶everything twitter-clone-example$ touch ./src/context/PreferencesContext.js

src/Main.js

まず、Paper の useTheme フックを使用して現在のテーマを取得し、Dark プロパティをチェックし、正しい値をスイッチに渡します。次に、ToggleTheme 関数を TouchableRipple に渡して、スイッチを押すたびにテーマを切り替えます。

これでスイッチを切り替えることができ、PaperProviderReact NavigationNativeNavigationContainer の両方が正しい色をコンポーネントに自動的に適用されます。

RTLI18nManager によって管理されます。

下記のように変更します。

// Main.js

import React from "react";
import {
  Provider as PaperProvider,
  DefaultTheme,
  DarkTheme
} from "react-native-paper";
import { I18nManager } from "react-native";
import { Updates } from "expo";
import { useColorScheme } from "react-native-appearance";

import { RootNavigator } from "./RootNavigator";
import { PreferencesContext } from "./context/PreferencesContext";

export const Main = () => {
  const colorScheme = useColorScheme();
  const [theme, setTheme] = React.useState(
    colorScheme === "dark" ? "dark" : "light"
  );
  const [rtl] = React.useState(I18nManager.isRTL);
  function toggleTheme() {
    setTheme(theme => (theme === "light" ? "dark" : "light"));
  }
  const toggleRTL = React.useCallback(() => {
    I18nManager.forceRTL(!rtl);
    Updates.reloadFromCache();
  }, [rtl]);
  const preferences = React.useMemo(
    () => ({
      toggleTheme,
      toggleRTL,
      theme,
      rtl: rtl ? "right" : "left"
    }),
    [rtl, theme, toggleRTL, toggleTheme]
  );
  return (
    <PreferencesContext.Provider value={preferences}>
      <PaperProvider
        theme={
          theme === "light"
            ? {
                ...DefaultTheme,
                colors: { ...DefaultTheme.colors, primary: "#1ba1f2" }
              }
            : {
                ...DarkTheme,
                colors: { ...DarkTheme.colors, primary: "#1ba1f2" }
              }
        }
      >
        <RootNavigator />
      </PaperProvider>
    </PreferencesContext.Provider>
  );
};

src/context/PreferencesContext.js ファイルを作成します。

// PreferencesContext.js
import React from "react";

export const PreferencesContext = React.createContext({
  rtl: "left",
  theme: "light",
  toggleTheme: () => {},
  toggleRTL: () => {}
});
スポンサーリンク

src/App.js

下記のように変更します。

import React from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { AppearanceProvider } from "react-native-appearance";

import { Main } from "main";

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

src/RootNavigator.js

下記のように修正してテーマを適用します。

import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createDrawerNavigator } from "@react-navigation/drawer";
import { DefaultTheme, DarkTheme } from "@react-navigation/native";
import { useTheme } from "react-native-paper";

import { StackNavigator } from "./StackNavigator";
import { DrawerContent } from "./DrawerContent";

const Drawer = createDrawerNavigator();

export const RootNavigator = () => {
  const theme = useTheme();
  const navigationTheme = theme.dark ? DarkTheme : DefaultTheme;

  return (
    <NavigationContainer theme={navigationTheme}>
      <Drawer.Navigator drawerContent={props => <DrawerContent {...props} />}>
        <Drawer.Screen name="Home" component={StackNavigator} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
};

Twitter のようなカラーになりました!

スポンサーリンク

最後のステップは、src/DrawerContent.js ファイルに toggleThemetoggleRTL の設定を追加することです。( コピぺ可能 ▼ )

// DrawerContent.js

import React from "react";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { DrawerContentScrollView, DrawerItem } from "@react-navigation/drawer";
import { StyleSheet, TouchableOpacity, View } from "react-native";
import {
  Avatar,
  Caption,
  Drawer,
  Paragraph,
  Switch,
  Text,
  Title,
  TouchableRipple,
  useTheme
} from "react-native-paper";
import Animated from "react-native-reanimated";
import { PreferencesContext } from "./context/PreferencesContext";

export function DrawerContent(props) {
  const paperTheme = useTheme();
  const { rtl, theme, toggleRTL, toggleTheme } = React.useContext(
    PreferencesContext
  );
  const translateX = Animated.interpolate(props.progress, {
    inputRange: [0, 0.5, 0.7, 0.8, 1],
    outputRange: [-100, -85, -70, -45, 0]
  });
  return (
    <DrawerContentScrollView {...props}>
      <Animated.View
        style={[
          styles.drawerContent,
          {
            backgroundColor: paperTheme.colors.surface,
            transform: [{ translateX }]
          }
        ]}
      >
        <View style={styles.userInfoSection}>
          <TouchableOpacity
            style={{ marginLeft: 10 }}
            onPress={() => {
              props.navigation.toggleDrawer();
            }}
          >
            <Avatar.Image
              source={{
                uri:
                  "https://pbs.twimg.com/profile_images/952545910990495744/b59hSXUd_400x400.jpg"
              }}
              size={50}
            />
          </TouchableOpacity>
          <Title style={styles.title}>Dawid Urbaniak</Title>
          <Caption style={styles.caption}>@trensik</Caption>
          <View style={styles.row}>
            <View style={styles.section}>
              <Paragraph style={[styles.paragraph, styles.caption]}>
                202
              </Paragraph>
              <Caption style={styles.caption}>Obserwuje</Caption>
            </View>
            <View style={styles.section}>
              <Paragraph style={[styles.paragraph, styles.caption]}>
                159
              </Paragraph>
              <Caption style={styles.caption}>Obserwujący</Caption>
            </View>
          </View>
        </View>
        <Drawer.Section style={styles.drawerSection}>
          <DrawerItem
            icon={({ color, size }) => (
              <MaterialCommunityIcons
                name="account-outline"
                color={color}
                size={size}
              />
            )}
            label="Profile"
            onPress={() => {}}
          />
          <DrawerItem
            icon={({ color, size }) => (
              <MaterialCommunityIcons name="tune" color={color} size={size} />
            )}
            label="Preferences"
            onPress={() => {}}
          />
          <DrawerItem
            icon={({ color, size }) => (
              <MaterialCommunityIcons
                name="bookmark-outline"
                color={color}
                size={size}
              />
            )}
            label="Bookmarks"
            onPress={() => {}}
          />
        </Drawer.Section>
        <Drawer.Section title="Preferences">
          <TouchableRipple onPress={toggleTheme}>
            <View style={styles.preference}>
              <Text>Dark Theme</Text>
              <View pointerEvents="none">
                <Switch value={theme === "dark"} />
              </View>
            </View>
          </TouchableRipple>
          <TouchableRipple onPress={toggleRTL}>
            <View style={styles.preference}>
              <Text>RTL</Text>
              <View pointerEvents="none">
                <Switch value={rtl === "right"} />
              </View>
            </View>
          </TouchableRipple>
        </Drawer.Section>
      </Animated.View>
    </DrawerContentScrollView>
  );
}
const styles = StyleSheet.create({
  drawerContent: {
    flex: 1
  },
  userInfoSection: {
    paddingLeft: 20
  },
  title: {
    marginTop: 20,
    fontWeight: "bold"
  },
  caption: {
    fontSize: 14,
    lineHeight: 14
  },
  row: {
    marginTop: 20,
    flexDirection: "row",
    alignItems: "center"
  },
  section: {
    flexDirection: "row",
    alignItems: "center",
    marginRight: 15
  },
  paragraph: {
    fontWeight: "bold",
    marginRight: 3
  },
  drawerSection: {
    marginTop: 15
  },
  preference: {
    flexDirection: "row",
    justifyContent: "space-between",
    paddingVertical: 12,
    paddingHorizontal: 16
  }
});

iOS シミュレータを再起動 ( ⌘ R ) して確認します。▼

お疲れ様でした。

これで、Twitter Clone アプリが完成されました。

スポンサーリンク

おまけ:Splash スクリーンを設定します。

twitter.png ファイルを assets フォルダーに保存します。

app.json ファイルを下記のように変更します。


"splash": {
- "image": "./assets/splash.png",
+ "image": "./assets/twitter.png",
- "resizeMode": "contain",
+ "resizeMode": "cover",
"backgroundColor": "#ffffff"
},

Expo サーバを再起動してから、iOS シミュレータを再起動 ( ⌘ R ) して確認すれば、
冒頭で紹介した Youtube 動画のように Twitter のスプラッシュスクリーン ( (再)起動、RTLの切替の時など ) が表示されます。

あとがき

今回は React Native + Expo + React Navigation + React Native Paper の組み合わせでアプリを作成してみましたが、良い感じに出来ましたし、あまり難しいとは感じなかったため他の初心者の方にも取り敢えず作って、素早くアプリの動作を自分の目で確かめるように、この記事 ( GitHub repoは ここ ) を書いたつもりです。一人の方でも役に立てれば幸いです!

個人的にマテリアルデザインが好きなので、当分はこの構成で開発を進めても良いかと思いました。

コメント

  1. ふぁびょん より:

    参考になりました。ありがとうございます。
    React NativeはネイティブアプリらしいUIが作りづらく苦労していたので参考になりました。
    技術革新も早くコピペしてもまともに動かないサイトも多く非常に参考になりました。

    動かない部分もありますが自分で調べてみようと思います。

タイトルとURLをコピーしました