ReactNativeについて学んだことをメモしていく
Modal
モーダルコンポーネントでモーダルにしたいところを囲む
<Modal visible={} animationType="slide">
...
</Modal>
visibleは開閉状態を真偽値で設定する必要があるので、useStateで初期値をfalseにしてonPressでfalseにするようにすれば実装可能
animationTypeは他にもnone, fadeがある。
影の当てかた
Android
elevationプロパティで影の深さを数値で指定
{
elevation: 4
}
ios
iosでは複数のプロパティを追加して影を実装する
• shadowColor: 影の色を指定。
• shadowOffset: 影のオフセットを指定。これはオブジェクトで、widthとheightを持つ。
• shadowOpacity: 影の不透明度を指定。値は0から1の範囲で指定。
• shadowRadius: 影の半径を指定。
{
shadowColor: 'black',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 6
}
iosとAndroid両方に対応するためにPlatform.selectででプラットフォームごとに記述
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
...Platform.select({
ios: {
shadowColor: 'black',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 6,
},
android: {
elevation: 8,
},
}),
},
});
グラデーションの実装
- expoからグラデーションをインポート
npx expo install expo-linear-gradient
- LinerGradientコンポーネントで囲む
<ImageBackground
source={require("../assets/images/background.png")}
style={styles.rootScreen}
imageStyle={styles.backgroundImage}
>
...
</ImageBackground>
アイコンの実装
アイコンはexpoが既に持っているので新たにパッケージをインストールする必要はない。
こちらから使いたいアイコンを選んでimportとcomponentをcopyして貼り付ける
フォントの実装
パッケージのインストール
npx expo install expo-font
ファイルのインポート
import { useFonts } from "expo-font";
フォントの読み込み
const [fontsLoaded] = useFonts({
'open-sans': require('./assets/fonts/OpenSans-Regular.ttf'),
'open-sans-bold': require('./assets/fonts/OpenSans-Bold.ttf')
});
ローディングのパッケージをインポート
AppLoadingパッケージのインストール:
フォントがロードされるまでのローディング状態の設定
if(!fontsLoaded) {
return <AppLoading />
}
これによりフォントが読み込まれていない場合、AppLoading コンポーネントを使用してローディング状態を表示します。
あとはuesFontsで定義したキーの部分を使ってスタイルでfontFamilyの値に指定すればフォントが適用されます。
fontFamily: 'open-sans-bold',
プラットフォームごとにスタイルを当てる方法
Platform.OSを使って条件分岐する方法
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
borderWidth: Platform.OS === 'android' ? 2 : 0,
},
});
Platform.selectを使ってスタイルを定義する方法
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
borderWidth: Platform.select({
ios: 0,
android: 2,
}),
},
});
ファイル名にプラットフォーム名を付けて分ける方法
Title.jsが存在する場合
- Title.ios.js : ios専用ファイル
- Title.android.js : android専用ファイル
デバイスの横向き対応
app.jsonの変更
"orientation": "portrait",
から
"orientation": "default",
に変更
useWindowDimesionsで動的に対応
importします。
import { useWindowDimensions } from "react-native";
コンポーネントのトップレベルで使用します。
const { width, height } = useWindowDimensions();
ここで取得したwidthはheightはデバイスの横幅と縦幅を動的に取得します。
useWindowDimensionsに似たDimensionsも存在するが、Dimensionsは縦画面でのデバイスの横幅・縦幅を取得するがレンダリングした際にその値が固定されてしまうので途中で横向きにしてもスタイルが変わらないからレイアウトが崩れる可能性大
react-navigationの導入
まずはimportします。
npm install @react-navigation/native
npx expo install react-native-screens react-native-safe-area-context
npm install @react-navigation/native-stack
App.jsに記述
import { StatusBar } from "expo-status-bar";
import { NavigationContainer } from "@react-navigation/native";
import CategoryScreen from "./screens/CategoryScreen";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
export default function App() {
return (
<>
<StatusBar style="dark" />
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="MealsCategories" component={CategoryScreen} />
</Stack.Navigator>
</NavigationContainer>
</>
);
}
- NavigationContainer : アプリケーション全体をナビゲーションコンテキストとして定義
- Stack.Navigator : 画面の遷移が積み重なって管理していくスタック型のナビゲーションを定義
- Stack.Screen : 実際にナビゲーションさせたいコンポーネントを指定し、それに名前をつけてどのコンポーネントに遷移するのかを指定
headerの設定
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "#351401",
},
headerTintColor: "white",
contentStyle: { backgroundColor: "#3f3f25" },
}}
>
<Stack.Screen
name="MealsCategories"
component={CategoryScreen}
options={{
title: "All Categories",
}}
/>
<Stack.Screen name="MealsOverview" component={MealsOverviewScreen} />
Stack.ScreenのoptionでtitleやheaderStyleでスタイルを当てることができるが、アプリケーション全体に適用するにはStack.NavigatorにたいしてscreenOptionsで全体の設定をすることができる。
動的な設定方法
import { useLayoutEffect } from "react";
useLayoutEffect(() => {
const categoryTitle = CATEGORIES.find(
(category) => category.id === catId
).title;
navigation.setOptions({
title: categoryTitle,
});
}, [catId, navigation]);
コンポーネントに遷移したときにtitleが表示されるまでラグがある場合はuseLayoutEffectを使用する。
Stack.Screenのoptionsでも設定できる。
options={({ route, navigation }) => {
const catId = route.params.categoryId;
return {
title: catId,
};
}}
スタイルをコンポーネントにpropsで渡すこともできる
<MealDetails
duration={selectedMeal.duration}
complexity={selectedMeal.complexity}
affordability={selectedMeal.affordability}
textStyle={styles.detailText}
/>
const styles = StyleSheet.create({
detailText: {
color: 'white'
}
});
function MealDetails({duration, affordability, complexity, style, textStyle}) {
return (
<View style={[styles.details, style]}>
<Text style={[styles.detailItem, textStyle]}>{duration}m</Text>
<Text style={[styles.detailItem, textStyle]}>{complexity.toUpperCase()}</Text>
<Text style={[styles.detailItem, textStyle]}>{affordability.toUpperCase()}</Text>
</View>
);
}
propsで受け取って配列にして記述することで上書きできる。
headerにボタンを追加する
headerの右にボタンを追加する実装はoptionsの中でheaderRightもしくはheaderLeftで定義可能。
<Stack.Screen name="MealDetail" component={MealDetailScreen} options={{
headerRight: () => {
return <Button title="ボタン" />
}
}}/>
コンポーネントに直接的に関係ことであればAppで記述しているStack.screenの中で記述しても問題ないが、コンポーネントの中と関係してくる実装をする場合はpropsでnavigationを受け取り、navigation.setOptionsでStack.screenで記述できるoptionを使うことができるようになる。
function MealDetailScreen({ navigation }) {
function headerButtonPressHandeler() {
console.log('Button pressed')
}
useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => {
return <Button title='Tap me!' onPress={headerButtonPressHandeler} />
}
})
}, [navigation, headerButtonPressHandeler])
return (
...省略します
);
}
export default MealDetailScreen;
styleの中にコールバック関数
style属性のコールバック関数の引数でタップしているかどうかを真偽値で受け取ることができるので、状況に応じてスタイルを当てることができる。
style={({pressed}) => pressed && styles.pressed}
ドロワーの実装
準備
パッケージをインストール
npm install @react-navigation/drawer
expoを使っていたら下記もインストール
npx expo install react-native-gesture-handler react-native-reanimated
雛形
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function MyDrawer() {
return (
<Drawer.Navigator>
<Drawer.Screen name="Feed" component={Feed} />
<Drawer.Screen name="Article" component={Article} />
</Drawer.Navigator>
);
}
DrawerItemのカスタマイズ
オプションでドロワーメニューのアイコンもカスタマイズできたりします。
詳しいオプションについてはこちら
<DrawerItem
name="WelcomeScreen"
options={{
drawerlabel: 'WelcomScreen',
drawerIcon: ({color, size}) => (
<Ionicons name="home" color={color} size={18} />
)
}}
/>
任意の文字やボタンでドロワーを開閉したい時
propsでnavgationを受け取りonPressでタップしたときにnavigation.toggleDrawer()を実行するようにすると可能
function UserScreen({ navigation }) {
function openDrawerHandler() {
navigation.toggleDrawer()
}
return(
<Button title="Open Drawer" onPress={openDrawerHandler} />
)
}
下部タブ実装
import
npm install @react-navigation/bottom-tabs
雛形
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
function MyTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarIcon: ({color, size}) => <Ionicons name="home" color={color} size={size}>
}}
/>
</Tab.Navigator>
);
}
ナビゲーションにドロワーをネストする
Stack.Navigatorの最初の子要素のStack.Screenのcomponentに関数を記述し、その関数内でDrawerを定義してアプリ開いた時に表示させたいコンポーネントを最初に持ってくる。
function Drawernavigator() {
return (
<Drawer.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "#351401",
},
headerTintColor: "white",
sceneContainerStyle: { backgroundColor: "#3f3f25" },
drawerContentStyle: { backgroundColor: "#351401" },
drawerInactiveTintColor: "white",
drawerActiveTintColor: "#e4baa1",
}}
>
<Drawer.Screen
name="Categories"
component={CategoryScreen}
options={{
title: "All Categories",
drawerIcon: ({ color, size }) => (
<Ionicons name="list" color={color} size={size} />
),
}}
/>
<Drawer.Screen
name="Favorites"
component={FavoritesScreen}
options={{
drawerIcon: ({ color, size }) => (
<Ionicons name="star" color={color} size={size} />
),
}}
/>
</Drawer.Navigator>
);
}
export default function App() {
return (
<>
<StatusBar style="light" />
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "#351401",
},
headerTintColor: "white",
contentStyle: { backgroundColor: "#3f3f25" },
}}
>
<Stack.Screen
name="Drawer"
component={Drawernavigator}
options={{
title: "All Categories",
headerShown: false,
}}
/>
<Stack.Screen name="MealsOverview" component={MealsOverviewScreen} />
<Stack.Screen
name="MealDetail"
component={MealDetailScreen}
options={{
title: "About the Meal",
}}
/>
</Stack.Navigator>
</NavigationContainer>
</>
);
}
これで実装するとheaderが二重になってしまうので、
Navigatorのoptionで headerShown: falseを指定すると非表示になる。
コンテキストでstateをグローバルで管理
import { createContext, useState } from "react";
export const FavoritesContext = createContext({
ids: [],
addFavorite: (id) => {},
removeFavorite: (id) => {},
});
function FavoritesContextProvider({children}) {
const [favoriteMealIds, setFavoriteMealIds] = useState([]);
function addFavorite(id) {
setFavoriteMealIds((currentFavIds) => [...currentFavIds, id] )
}
//
function removeFavorite(id) {
setFavoriteMealIds((currentFavIds) => currentFavIds.filter(mealId => mealId !== id))
}
const value = {
ids: favoriteMealIds,
addFavorite:addFavorite,
removeFavorite: removeFavorite
}
return <FavoritesContext.Provider value={value}>{children}</FavoritesContext.Provider>;
}
export default FavoritesContextProvider;
コンテキストの作成
export const FavoritesContext = createContext({
ids: [],
addFavorite: (id) => {},
removeFavorite: (id) => {},
});
コンテキストの構造を定義します。ここではidsという配列とaddFavoriteとremoveFavoriteとして空の関数を設定
コンテキストプロバイダーの定義
function FavoritesContextProvider({ children }) {
const [favoriteMealIds, setFavoriteMealIds] = useState([]);
アプリケーション全体の状態を更新するための状態を提供
追加と削除
引数のidから追加と削除を実装するための記述
function addFavorite(id) {
setFavoriteMealIds((currentFavIds) => [...currentFavIds, id] )
}
function removeFavorite(id) {
setFavoriteMealIds((currentFavIds) => currentFavIds.filter(mealId => mealId !== id))
}
コンテキストプロバイダーへの返却
valueオブジェクトにコンテキストプロバイダーに渡す値を定義し、FavoritesContext.Providerにvalueとして渡す
const value = {
ids: favoriteMealIds,
addFavorite:addFavorite,
removeFavorite: removeFavorite
}
return <FavoritesContext.Provider value={value}>{children}</FavoritesContext.Provider>;
クリックでモーダルを閉じる
propsのnavigatoinからgoBack関数を使えば閉じることができる。
function cancelHandler() {
navigation.goBack();
}
ローディング
ActivityIndicatorコンポーネントでデバイスに最適なローディングアニメーションを表示させる事ができる。
function LoadingOverlay() {
return (
<View style={styles.container}>
<ActivityIndicator size={"large"} color={"white"} />
</View>
);
}
export default LoadingOverlay;
imagePickerの実装
expoが用意しているimagePickerでカメラ機能を実装する事ができるのでメモ
インポート
npx expo install expo-image-picker
app.jsonで設定
{
"expo": {
+ "plugins": [
+ [
+ "expo-image-picker",
+ {
+ "photosPermission": "The app accesses your photos to let you share them with your friends."
+ }
+ ]
+ ]
}
}
実装
import {
launchCameraAsync,
useCameraPermissions,
PermissionStatus,
} from "expo-image-picker";
function ImagePicker() {
const [pickedImage, setPickedImage] = useState();
const [cameraPermissionInformation, requestPermission] =
useCameraPermissions();
async function verifyPermissions() {
if (cameraPermissionInformation.status === PermissionStatus.UNDETERMINED) {
const permissionResponse = await requestPermission();
return permissionResponse.granted;
}
// 拒否
if (cameraPermissionInformation.status === PermissionStatus.DENIED) {
Alert.alert("権限が不足しています。");
return false;
}
return true;
}
async function takeImageHandler() {
const hasPermission = await verifyPermissions();
if (!hasPermission) {
return;
}
const image = await launchCameraAsync({
allowsEditing: true, // 写真を編集
aspect: [16, 9], // アスペクト比を設定
quality: 0.5, // 少し小さめの画像を取得
});
setPickedImage(image.assets[0].uri);
}
let imagePreview = <Text>画像はまだ撮影されていません。</Text>;
if (imagePreview) {
imagePreview = <Image style={styles.image} source={{ uri: pickedImage }} />;
}
return (
<View>
<View style={styles.imagePreview}>{imagePreview}</View>
<OutlineButton icon={"camera"} onPress={takeImageHandler}>take Image</OutlineButton>
</View>
);
}
useCameraPermissions
const [cameraPermissionInformation, requestPermission] = useCameraPermissions();
importしたuseCameraPermissionsから2つの値を取ります。
cameraPermissionInformation : カメラの権限に関する現在の情報をオブジェクトです。プロパティはstatus
を持っており、PermissionStatus型の列挙型であり、下記のステータスを持っています。
Head | Head |
---|---|
UNDETERMINED | まだユーザーにカメラの権限を求めていない状態 |
DENIED | ユーザーがカメラ権限のリクエストを拒否した状態 |
GRANTED | ユーザーがカメラ権限のリクエストを許可した状態 |
タップされているかどうかを真偽値判定
<View style={styles.goalItem}>
<Pressable
onPress={() => onDeleteItem(itemData.item.id)}
android_ripple={{ color: "#210644" }}
style={({pressed}) => pressed && styles.pressedItem }
>
<Text style={styles.goalText}>{itemData.item.text}</Text>
</Pressable>
</View>
スタイルでタップしているかどうかを真偽値判定
style属性の関数を渡すことでpressDataのpressedプロパティにアクセスできます。
これはタップした時にtrue, タップしていない時にfalseを返すという意味があります。
それによりスタイルを柔軟に管理することができます。
また、配列を渡しカンマ区切りで記述すると複数のスタイルを当てることもできます。
export default function PrimaryButton({ children }: PrimaryButtonProps) {
function pressHandler() {
console.log("Button pressed");
}
return (
<View style={styles.buttonOuterContainer}>
<Pressable
style={({pressed}) => pressed ? [styles.buttonInputcontainer, styles.pressed] : styles.buttonInputcontainer}
onPress={pressHandler}
android_ripple={{ color: "#640233" }}
>
<Text style={styles.buttonText}>{children}</Text>
</Pressable>
</View>
);
}