Bottom Navigation
それでは、下記の三つのタブを持つ、Tab Navigator を実装します。
Feed タブは先ほど作成した Stack Navigator が入ります。
- Feed :
Feed
とDetails
スクリーンが動作します。 - Notifications:さらに、
All
とMentions
タブが入ります。 - 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
ここで、Feed
、Notifications
、Messages
コンポーネントをレンダリングします。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
をインポートします。
…
– 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
ファイルを再度変更します。
…
<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
下記のように変更します。
// このようなコードをコピペする際には
"
'
などが正しく動作するか要確認!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
を作成します。
TabView
、SceneMap
、TabBar
などを利用するため、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
を変えたりする機能を実装して行きます。
コメント
参考になりました。ありがとうございます。
React NativeはネイティブアプリらしいUIが作りづらく苦労していたので参考になりました。
技術革新も早くコピペしてもまともに動かないサイトも多く非常に参考になりました。
動かない部分もありますが自分で調べてみようと思います。