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が作りづらく苦労していたので参考になりました。
技術革新も早くコピペしてもまともに動かないサイトも多く非常に参考になりました。
動かない部分もありますが自分で調べてみようと思います。