Expo.ioMaterial DesignReactReact Native React Native PaperReact Navigation

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

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

Bottom Navigation

それでは、下記の三つのタブを持つ、Tab Navigator を実装します。
Feed タブは先ほど作成した Stack Navigator が入ります。

  • Feed : FeedDetails スクリーンが動作します。
  • Notifications:さらに、AllMentions タブが入ります。
  • Messages:メッセージを作成する Floating Action Button ( FAB ) ボタンを追加します。

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

try🐶everything twitter-clone-example$ touch ./src/BottomTabNavigator.js
try🐶everything twitter-clone-example$ touch ./src/Notifications.js
try🐶everything twitter-clone-example$ touch ./src/Messages.js

src/BottomTabNavigator.js

ここで、FeedNotificationsMessages コンポーネントをレンダリングします。
createMaterialBottomTabNavigator を使用して設定を行います。

// BottomTabNavigator
import React from "react";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";

import { Feed } from "./Feed";
import { Messages } from "./Messages";
import { Notifications } from "./Notifications";

const Tab = createMaterialBottomTabNavigator();

export const BottomTabNavigator = () => {
  return (
    <Tab.Navigator
      initialRouteName="Feed"
      shifting={true}
      sceneAnimationEnabled={false}
    >
      <Tab.Screen
        name="Feed"
        component={Feed}
        options={{
          tabBarIcon: "home-account"
        }}
      />
      <Tab.Screen
        name="Notifications"
        component={Notifications}
        options={{
          tabBarIcon: "bell-outline"
        }}
      />
      <Tab.Screen
        name="Messages"
        component={Messages}
        options={{
          tabBarIcon: "message-text-outline"
        }}
      />
    </Tab.Navigator>
  );
};

src/Notifications.js

// Notifications.js
import React from "react";
import { View, Text, StyleSheet } from "react-native";

export const Notifications = props => {
  return (
    <View style={styles.container}>
      <Text>Notifications</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center"
  }
});
スポンサーリンク

src/Messages.js

// Messages.js
import React from "react";
import { View, Text, StyleSheet } from "react-native";

export const Messages = props => {
  return (
    <View style={styles.container}>
      <Text>Messages</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center"
  }
});

src/StackNavigator.js を下記のように修正します。

  • Feed の代わりに先ほど作成した BottomTabNavigator をインポートします。
// src/StackNavigator.js


– import { Feed } from “./Feed”;
+ import { BottomTabNavigator } from “./BottomTabNavigator”;
import { Details } from “./Details”;

<Stack.Screen
name=”Feed”
– component={Feed}
+ component={BottomTabNavigator}
options={{ headerTitle: “Twitter” }}
/>
<Stack.Screen
name=”Details”

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

▲ いずれのタブを選択しても Header タイトルが Twitterアイコン のままです。

これで、Stack Navigator と Tab Navigator がよく統合され、Feed タブから Stack の Feed と Details スクリーンに移動出来るようになりました。

が、少し見た目を変えておきましょう。

  • 現在のスクリーン名を取得してheader のタイトルとしてレンダリングします。
  • この場合、タイトル名が Feed であれば、Twitter アイコンを表示し、その他はそれぞれのタイトルを表示します。
  • タイトルにテーマのスタイルを適用します。( titleStyle )

src/StackNavigator.js ファイルを再度変更します。

// src/StackNavigator.js

<Appbar.Content
– title={
– previous ? title : <MaterialCommunityIcons name=”twitter” size={40} />
– }

+ title={
+ title === “Feed” ? (
+ <MaterialCommunityIcons
+ style={{ marginRight: 10 }}
+ name=”twitter”
+ size={40}
+ color={theme.colors.primary}
+ />
+ ) : (
+ title
+ )
+ }
+ titleStyle={{
+ fontSize: 18,
+ fontWeight: “bold”,
+ color: theme.colors.primary
+ }}

/>
</Appbar.Header>
);
};

export const StackNavigator = () => {


<Stack.Screen
name=”Feed”
component={BottomTabNavigator}
– options={{ headerTitle: “Twitter” }}
+ options={({ route }) => {
+ const routeName = route.state
+ ? route.state.routes[route.state.index].name
+ : “Feed”;
+ return { headerTitle: routeName };
+ }}

/>
<Stack.Screen
name=”Details”

Bottom Tab の色もテーマカラーに変更します。

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

try🐶everything twitter-clone-example$ mkdir ./src/utils
try🐶everything twitter-clone-example$ touch ./src/utils/overlay.js
スポンサーリンク

src/BottomTabNavigator.js

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

// BottomTabNavigator
// このようなコードをコピペする際には " ' などが正しく動作するか要確認!
import React from “react”;
+ import { useTheme } from “react-native-paper”;
+ import color from “color”;

+ import overlay from “./utils/overlay”;
import { createMaterialBottomTabNavigator } from “@react-navigation/material-bottom-tabs”;


export const BottomTabNavigator = () => {
+ const theme = useTheme();
+ const tabBarColor = theme.dark
+ ? overlay(6, theme.colors.surface)
+ : theme.colors.surface;

return (
<Tab.Navigator
initialRouteName=”Feed”
shifting={true}
sceneAnimationEnabled={false}
+ backBehavior=”initialRoute”
+ activeColor={theme.colors.primary}
+ inactiveColor={color(theme.colors.text)
+ .alpha(0.6)
+.rgb()
+.string()}

>
<Tab.Screen

options={{
+ tabBarIcon: “home-account”, tabBarColor

options={{
+ tabBarIcon: “bell-outline”, tabBarColor

options={{
+ tabBarIcon: “message-text-outline”, tabBarColor
}}

src/utils/overlay.js ファイルを作成します。

▼ は Paper の overlay.ts ファイルを .js に変換したものです。( コピペ可能 ▼ )

スポンサーリンク
// overlay.js
import color from "color";
import { Animated } from "react-native";
import { DarkTheme } from "react-native-paper";

export default function overlay(
  elevation = 1,
  surfaceColor = DarkTheme.colors.surface
) {
  if (elevation instanceof Animated.Value) {
    const inputRange = [0, 1, 2, 3, 8, 24];
    return elevation.interpolate({
      inputRange,
      outputRange: inputRange.map(elevation => {
        return calculateColor(surfaceColor, elevation);
      })
    });
  }
  return calculateColor(surfaceColor, elevation);
}

function calculateColor(surfaceColor, elevation) {
  let overlayTransparency;
  if (elevation >= 1 && elevation <= 24) {
    overlayTransparency = elevationOverlayTransparency[elevation];
  } else if (elevation > 24) {
    overlayTransparency = elevationOverlayTransparency[24];
  } else {
    overlayTransparency = elevationOverlayTransparency[1];
  }
  return color(surfaceColor)
    .mix(color("white"), overlayTransparency * 0.01)
    .hex();
}
const elevationOverlayTransparency = {
  1: 5,
  2: 7,
  3: 8,
  4: 9,
  5: 10,
  6: 11,
  7: 11.5,
  8: 12,
  9: 12.5,
  10: 13,
  11: 13.5,
  12: 14,
  13: 14.25,
  14: 14.5,
  15: 14.75,
  16: 15,
  17: 15.12,
  18: 15.24,
  19: 15.36,
  20: 15.48,
  21: 15.6,
  22: 15.72,
  23: 15.84,
  24: 16
};

下記のように動作します。

スポンサーリンク

次は Notifications.js を作成します。

TabViewSceneMapTabBar などを利用するため、react-native-tab-view をインストールします。

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

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

try🐶everything twitter-clone-example$ touch ./src/All.js

src/Notifications.js

// Notifications.js
import React from "react";
import { useTheme } from "react-native-paper";
import { Dimensions } from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import color from "color";

import { Feed } from "./Feed";
import { AllNotifications } from "./All";

const initialLayout = { width: Dimensions.get("window").width };
const All = () => <AllNotifications />;
const Mentions = () => <Feed />;

export const Notifications = () => {
  const [index, setIndex] = React.useState(0);
  const [routes] = React.useState([
    { key: "all", title: "All" },
    { key: "mentions", title: "Mentions" }
  ]);
  const theme = useTheme();
  const renderScene = SceneMap({
    all: All,
    mentions: Mentions
  });

  const tabBarColor = theme.colors.surface;

  const rippleColor = theme.dark
    ? color(tabBarColor).lighten(0.5)
    : color(tabBarColor).darken(0.2);

  const renderTabBar = props => (
    <TabBar
      {...props}
      indicatorStyle={{ backgroundColor: theme.colors.primary }}
      style={{ backgroundColor: tabBarColor, shadowColor: theme.colors.text }}
      labelStyle={{ color: theme.colors.primary }}
      pressColor={rippleColor}
    />
  );
  return (
    <React.Fragment>
      <TabView
        navigationState={{ index, routes }}
        renderScene={renderScene}
        onIndexChange={setIndex}
        initialLayout={initialLayout}
        renderTabBar={renderTabBar}
      />
    </React.Fragment>
  );
};
スポンサーリンク

src/All.js

// All.js

import React from "react";
import { View, Text, StyleSheet } from "react-native";

export const AllNotifications = props => {
  return (
    <View style={styles.container}>
      <Text>AllNotifications</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center"
  }
});

All.js の詳細を作成します。

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

try🐶everything twitter-clone-example$ touch ./src/components/NotificationTweet.js

src/All.js

// ALl.js
import React from "react";
import { View, FlatList, StyleSheet } from "react-native";
import { useTheme } from "react-native-paper";
import { NotificationTweet } from "./components/NotificationTweet";
import { notificationTweets } from "./data";

function renderItem({ item }) {
  return <NotificationTweet {...item} />;
}
function keyExtractor(item) {
  return item.id.toString();
}
export const AllNotifications = () => {
  const theme = useTheme();
  return (
    <FlatList
      contentContainerStyle={{ backgroundColor: theme.colors.background }}
      style={{ backgroundColor: theme.colors.background }}
      data={notificationTweets}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      ItemSeparatorComponent={() => (
        <View style={{ height: StyleSheet.hairlineWidth }} />
      )}
    />
  );
};

src/components/NotificationTweet.jsを作成します。

スポンサーリンク

src/components/NotificationTweet.js

// NotificationTweet.js
import React from "react";
import { View, StyleSheet } from "react-native";
import { Surface, Text, Avatar, useTheme } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import color from "color";

export const NotificationTweet = props => {
  const theme = useTheme();
  const contentColor = color(theme.colors.text)
    .alpha(0.8)
    .rgb()
    .string();

  return (
    <Surface style={styles.container}>
      <View style={styles.leftColum}>
        <MaterialCommunityIcons
          name="star-four-points"
          size={30}
          color="#8d38e8"
        />
      </View>
      <View style={styles.rightColumn}>
        <View style={styles.topRow}>
          {props.people.map(({ name, image }) => (
            <Avatar.Image
              style={{ marginRight: 10 }}
              key={name}
              source={{ uri: image }}
              size={40}
            />
          ))}
        </View>
        <Text style={{ marginBottom: 10 }}>
          {props.people.map(({ name }) => name).join(" and ")} likes{" "}
          {props.name} tweet.
        </Text>
        <Text stle={{ color: contentColor }}>{props.content}</Text>
      </View>
    </Surface>
  );
};

const styles = StyleSheet.create({
  container: {
    flexDirection: "row",
    paddingTop: 15,
    paddingRight: 15,
    paddingBottom: 15
  },
  leftColum: {
    width: 50,
    marginRight: 10,
    alignItems: "flex-end"
  },
  rightColumn: {
    flex: 1
  },
  topRow: {
    flexDirection: "row",
    alignItems: "center",
    marginBottom: 10
  }
});

src/data/index.js ファイルを開いて、ファイルの最後のところに下記の内容を追加します。

スポンサーリンク

src/data/index.js

 avatar:
      "https://pbs.twimg.com/profile_images/1203624639538302976/h-rvrjWy_400x400.jpg",
    comments: 23,
    retweets: 21,
    hearts: 300
  }
];

// ↓↓↓ここから追加します。
export const notificationTweets = [
  {
    id: 1,
    content:
      "In any case, the focus is not react navigation, but the possibility of writing your app once and running it on several different platforms.  Then you use the technology you want, for example for the interface, I choose @rn_paper",
    name: "Leandro Fevre",
    people: [
      {
        name: "Evan Bacon 🥓",
        image:
          "https://pbs.twimg.com/profile_images/1203624639538302976/h-rvrjWy_400x400.jpg"
      },
      {
        name: "Leandro Favre",
        image:
          "https://pbs.twimg.com/profile_images/1181019042557173760/a1C7MHkM_400x400.jpg"
      }
    ]
  },
  {
    id: 2,
    content: "It's finally somewhat bright on my way to work 🥳",
    name: "Tomasz Łakomy",
    people: [
      {
        name: "Wojteg1337",
        image:
          "https://pbs.twimg.com/profile_images/1164452902913675264/cn3bEqJp_400x400.jpg"
      }
    ]
  },
  {
    id: 3,
    content:
      'What they say during code review:\n\n"I see your point, but this is extra work - how about we create a ticket for it and get to it next sprint?"\n\nWhat they mean:\n\n"I literally don\'t give a single shit about it and this ticket will rot in the backlog for eternity"',
    name: "Tomasz Łakomy",
    people: [
      {
        name: "Nader Dabit",
        image:
          "https://pbs.twimg.com/profile_images/1167093599600816129/APWfpd5O_400x400.jpg"
      }
    ]
  },
  {
    id: 4,
    content:
      "In any case, the focus is not react navigation, but the possibility of writing your app once and running it on several different platforms.  Then you use the technology you want, for example for the interface, I choose @rn_paper",
    name: "Leandro Fevre",
    people: [
      {
        name: "Evan Bacon 🥓",
        image:
          "https://pbs.twimg.com/profile_images/1203624639538302976/h-rvrjWy_400x400.jpg"
      },
      {
        name: "Leandro Favre",
        image:
          "https://pbs.twimg.com/profile_images/1181019042557173760/a1C7MHkM_400x400.jpg"
      }
    ]
  },
  {
    id: 5,
    content: "It's finally somewhat bright on my way to work 🥳",
    name: "Tomasz Łakomy",
    people: [
      {
        name: "Wojteg1337",
        image:
          "https://pbs.twimg.com/profile_images/1164452902913675264/cn3bEqJp_400x400.jpg"
      }
    ]
  },
  {
    id: 6,
    content:
      'What they say during code review:\n\n"I see your point, but this is extra work - how about we create a ticket for it and get to it next sprint?"\n\nWhat they mean:\n\n"I literally don\'t give a single shit about it and this ticket will rot in the backlog for eternity"',
    name: "Tomasz Łakomy",
    people: [
      {
        name: "Nader Dabit",
        image:
          "https://pbs.twimg.com/profile_images/1167093599600816129/APWfpd5O_400x400.jpg"
      }
    ]
  }
];

下記のように動作します。

スポンサーリンク

src/Messages.js

Messages も作成します。

// Messages.js
import React from "react";
import { ScrollView, StyleSheet } from "react-native";
import { Headline, Caption, useTheme, Button } from "react-native-paper";
import overlay from "./utils/overlay";

export const Messages = props => {
  const theme = useTheme();
  const backgroundColor = overlay(2, theme.colors.surface);

  return (
    <ScrollView
      style={backgroundColor}
      contentContainerStyle={[styles.scrollViewContent, { backgroundColor }]}
    >
      <Headline style={styles.centerText}>
        Send a message, get a message
      </Headline>
      <Caption style={styles.centerText}>
        Private Messages are private conversations between you and other people
        on Twitter. Share Tweets, media, and more!
      </Caption>
      <Button
        onPress={() => {}}
        style={styles.button}
        mode="contained"
        labelStyle={{ color: "white" }}
      >
        Writer a message
      </Button>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  scrollViewContent: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    paddingHorizontal: 30
  },
  centerText: {
    textAlign: "center"
  },
  button: {
    marginTop: 20
  }
});

再度動作を確認すると、▼のようになります。

これで、主な機能が実装されました!

あとは、Messages タブに FAB を適用してテーマを切り替えたり、RTL を変えたりする機能を実装して行きます。


お名前.com

https://www.xserver.ne.jp/lp/service01/

コメント

  1. ふぁびょん より:

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

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

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