FAB と Portal
ポータルで他の要素の上に表示されるコンテンツをレンダリングできるため、FAB
と組み合わせてタブを変更したときに FAB
のアイコンをスムーズにアニメーション化し、特定のスクリーンで FAB
を完全に非表示にします。
まず、すべてのタブに FAB
をレンダリングします。
src/BottomTabNavigator.js
下記のように変更します。
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
に設定を追加します。
…
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
から指定 & テーマカラーを適用します。 Notifications
やMessages
スクリーンのそれぞれのheader
名を表示出来るようにします。( 現在 Twitter アイコンが表示中 )
…
+ 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 v5
と React 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
に渡して、スイッチを押すたびにテーマを切り替えます。
これでスイッチを切り替えることができ、Paper
の Provider
と React Navigation
のNativeNavigationContainer
の両方が正しい色をコンポーネントに自動的に適用されます。
RTL
は I18nManager
によって管理されます。
下記のように変更します。
// 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
ファイルに toggleTheme
と toggleRTL
の設定を追加することです。( コピぺ可能 ▼ )
// 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は ここ ) を書いたつもりです。一人の方でも役に立てれば幸いです!
個人的にマテリアルデザインが好きなので、当分はこの構成で開発を進めても良いかと思いました。
コメント
参考になりました。ありがとうございます。
React NativeはネイティブアプリらしいUIが作りづらく苦労していたので参考になりました。
技術革新も早くコピペしてもまともに動かないサイトも多く非常に参考になりました。
動かない部分もありますが自分で調べてみようと思います。