iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0

程式畫面預覽:

圖片

範例影片:

影片

程式碼:

app.js

import 'react-native-gesture-handler';
import React from 'react';
import { StatusBar } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import COLORS from './src/consts/colors';
import DetailsScreen from './src/views/screens/DetailsScreen';
import BottomNavigator from './src/views/navigation/BottomNavigator';
import OnBoardScreen from './src/views/screens/OnBoardScreen';

const Stack = createNativeStackNavigator();

const App = () => {
  return (
    <NavigationContainer>
      <StatusBar backgroundColor={COLORS.white} barStyle="dark-content" />
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        <Stack.Screen name="BoardScreen" component={OnBoardScreen} />
        <Stack.Screen name="Home" component={BottomNavigator} />
        <Stack.Screen name="DetailsScreen" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default App;

src\views\screens\HomeScreen.js

import { SafeAreaView, StyleSheet, Text, View, Image, TextInput, ScrollView, TouchableOpacity, FlatList, Dimensions } from 'react-native'
import React, { useState } from 'react'
import Icon from 'react-native-vector-icons/MaterialIcons';
import COLORS from "../../consts/colors"
import categories from '../../consts/categories'
import foods from '../../consts/foods';

const { width } = Dimensions.get("screen")
const cardWidth = width / 2 - 20

const HomeScreen = ({ navigation }) => {

    const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0)

    // 項目清單
    const ListCategories = () => {
        return (
            <ScrollView
                horizontal
                showsHorizontalScrollIndicator={false}
                contentContainerStyle={styles.CategoriesListContainer}
            >
                {/* 注意這邊的寫法 常用 但都記不住 XD */}
                {categories.map((category, index) => (
                    <TouchableOpacity key={index}
                        activeOpacity={0.8}
                        // 點選後 給index的值 (與下面的判斷式配合使用)
                        onPress={() => setSelectedCategoryIndex(index)}
                    >
                        <View style={{
                            // 選取中的分類 與未選中的 背景色 判斷式
                            backgroundColor: selectedCategoryIndex == index
                                ? COLORS.primary
                                : COLORS.secondary
                            , ...styles.CategoryBtn
                        }}>
                            <View style={styles.CategoryBtnImg}>
                                <Image source={category.image}
                                    style={{ width: 35, height: 35, resizeMode: "cover" }}
                                />
                            </View>
                            {/* 被選中的顏色改變 判斷式 */}
                            <Text style={{
                                fontSize: 15, fontWeight: "bold", marginLeft: 10,
                                color: selectedCategoryIndex == index
                                    ? COLORS.white
                                    : COLORS.primary
                            }}>{category.name}</Text>
                        </View>
                    </TouchableOpacity>
                ))
                }
            </ScrollView >
        )
    }
    // 食物的卡片
    const Card = ({ food }) => {
        return (
            // navigation.navigate 這邊傳遞的 DetailsScreen 要先在App.js裡面設定
            <TouchableOpacity underlayColor={COLORS.white} activeOpacity={0.9} onPress={() => navigation.navigate("DetailsScreen", food)}>
                <View style={styles.Card}>
                    <View style={{ alignItems: "center", top: -40 }}>
                        <Image source={food.image}
                            style={{
                                width: 120,
                                height: 120,
                            }}
                        />
                    </View>
                    <View style={{ marginHorizontal: 20 }}>
                        <Text style={{ fontSize: 17, fontWeight: "bold" }}>{food.name}</Text>
                        <Text style={{ fontSize: 14, color: COLORS.grey, marginTop: 2 }}>{food.ingredients}</Text>
                    </View>
                    <View style={{ flexDirection: "row", marginTop: 10, marginHorizontal: 20, justifyContent: "space-between" }}>
                        <Text style={{ fontSize: 18, fontWeight: "bold" }}>${food.price}</Text>
                        <View style={styles.AddToCartBtn}>
                            <Icon name="add" size={20} color={COLORS.white} />
                        </View>
                    </View>
                </View>
            </TouchableOpacity>
        )
    }

    return (
        <SafeAreaView style={{ flex: 1, backgroundColor: COLORS.white }}>
            <View style={styles.Header}>
                <View>
                    <View style={{ flexDirection: "row" }}>
                        <Text style={{ fontSize: 28, }}>Hello,</Text>
                        <Text style={{ fontSize: 28, fontWeight: "bold", marginLeft: 10 }}>Hsu</Text>
                    </View>
                    <Text style={{ marginTop: 5, fontSize: 22, color: COLORS.grey }}>What do ypu want today?</Text>
                </View>
                {/* 頭像 */}
                <Image source={require("../../assets/person.png")}
                    style={{ width: 50, height: 50, borderRadius: 25 }}
                />
            </View>
            {/* 搜尋列 */}
            <View style={{ marginTop: 40, flexDirection: "row", paddingHorizontal: 20 }}>
                <View style={styles.InputContainer}>
                    <Icon name="search" size={28} />
                    <TextInput
                        placeholder='請輸入餐點名稱'
                        style={{ flex: 1, fontSize: 18, marginLeft: 5 }}
                    />
                </View>
                {/* 排序按鈕 純展示 無作用 */}
                <View style={styles.SortBtn}>
                    <Icon name="tune" size={28} color={COLORS.white} />
                </View>
            </View>
            {/* 菜單項目的分類欄 */}
            <View>
                <ListCategories />
            </View>
            {/* 卡片 */}
            <FlatList
                showsVerticalScrollIndicator={false}
                numColumns={2}
                data={foods}
                renderItem={({ item }) => <Card food={item} />}
            />
        </SafeAreaView>
    )
}

const styles = StyleSheet.create({
    Header: {
        marginTop: 20,
        flexDirection: "row",
        justifyContent: "space-between",
        paddingHorizontal: 20,

    },
    InputContainer: {
        flex: 1,
        flexDirection: "row",
        height: 50,
        borderRadius: 10,
        backgroundColor: COLORS.light,
        alignItems: "center",
        paddingHorizontal: 20,
    }, SortBtn: {
        width: 50,
        height: 50,
        marginLeft: 10,
        backgroundColor: COLORS.primary,
        borderRadius: 10,
        justifyContent: "center",
        alignItems: "center"
    },
    CategoriesListContainer: {
        paddingVertical: 30,
        paddingHorizontal: 20,
        alignItems: "center",
    },
    CategoryBtn: {
        flexDirection: "row",
        width: 120,
        height: 50,
        marginRight: 7,
        borderRadius: 30,
        alignItems: "center",
        paddingHorizontal: 5,

    },
    CategoryBtnImg: {
        width: 35,
        height: 35,
        backgroundColor: COLORS.white,
        borderRadius: 20,
        justifyContent: "center",
        alignItems: "center"
    },
    Card: {
        width: cardWidth,
        height: 220,
        marginHorizontal: 10,
        marginTop: 50,
        marginBottom: 20,
        borderRadius: 15,
        elevation: 13,
        backgroundColor: COLORS.white,
    },
    AddToCartBtn: {
        width: 30,
        height: 30,
        borderRadius: 15,
        backgroundColor: COLORS.primary,
        justifyContent: "center",
        alignItems: "center",
    },
})

export default HomeScreen

src\views\screens\OnBoardScreen.js

import { StyleSheet, Text, View, Image, Button } from 'react-native';
import React from 'react';
import { SafeAreaView } from 'react-native-safe-area-context';
import COLORS from '../../consts/colors';

import { PrimaryButton } from '../components/Button';

// 登入畫面
const OnBoardScreen = ({ navigation }) => {
    return (
        <SafeAreaView style={{ flex: 1, backgroundColor: COLORS.white }}>
            <View style={{ height: 400, }}>
                <Image
                    source={require("../../assets/onboardImage.png")}
                    style={{ width: "100%", resizeMode: "contain", top: -180 }}
                />
            </View>
            <View style={styles.TextContainer}>
                <View>
                    <Text style={{ fontSize: 32, fontWeight: "bold", textAlign: "center" }}>Delicious Food</Text>
                    <Text style={{
                        marginTop: 20,
                        fontSize: 18,
                        textAlign: "center",
                        color: COLORS.grey
                    }}> We help you to find best and delicious food</Text>
                </View>
                {/* indicator container */}
                <View style={styles.IndicatorContainer}>
                    <View style={styles.CurrentIndicator}></View>
                    <View style={styles.Indicator}></View>
                    <View style={styles.Indicator}></View>
                </View>
                <PrimaryButton title="Get Started"
                    onPress={() => navigation.navigate("Home")}
                />

            </View>
        </SafeAreaView>
    )
}

const styles = StyleSheet.create({
    TextContainer: {
        flex: 1,
        paddingHorizontal: 50,
        justifyContent: "space-between",
        paddingBottom: 40,

    },
    IndicatorContainer: {
        flex: 1,
        height: 50,
        justifyContent: "center",
        flexDirection: "row",
        alignItems: "center",
    },
    CurrentIndicator: {
        width: 30,
        height: 12,
        borderRadius: 10,
        backgroundColor: COLORS.primary,
        marginHorizontal: 5,
    },
    Indicator: {
        width: 12,
        height: 12,
        borderRadius: 6,
        backgroundColor: COLORS.grey,
        marginHorizontal: 5,
    }
})

export default OnBoardScreen

src\views\navigation\BottomNavigator.js

import "react-native-gesture-handler"
import React from 'react'
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"
import Icon from 'react-native-vector-icons/MaterialIcons';
import COLORS from "../../consts/colors"
import { View } from "react-native"

import HomeScreen from '../screens/HomeScreen';
import CartScreen from '../screens/CartScreen';

const Tab = createBottomTabNavigator();

// tab 的 options 影片是舊版的 大部分已失效 
// 原本是 tabBarOptions  改為 screenOptions 
// 定義屬性的方式 也改變
// 參考官網文件 https://reactnavigation.org/docs/bottom-tab-navigator/#options
// 影片教學 Bottom Tab Navigation with Animation | React-Navigation v6/5 | Part-1 https://youtu.be/XiutL0uLICg

const BottomNavigator = () => {
    return (
        <Tab.Navigator
            screenOptions={{
                tabBarStyle: {
                    hight: 55,
                    //目前只有這個看得出有效果 上下兩個看不出來...
                    borderTopWidth: 0,
                    elevation: 1,
                },
                // 隱藏 標題列
                headerShown: false,

                tabBarActiveTintColor: COLORS.primary

            }}>
            {/* 頁籤1 HomeScreen */}
            <Tab.Screen name="HomeScreen" component={HomeScreen} options={{ tabBarShowLabel: false, tabBarIcon: ({ color }) => (<Icon name="home" color={color} size={28} />) }} />
            {/* 頁籤2 LocalMall */}
            <Tab.Screen name="LocalMall" component={HomeScreen} options={{ tabBarShowLabel: false, tabBarIcon: ({ color }) => (<Icon name="local-mall" color={color} size={28} />) }} />
            {/* 頁籤3 Search */}
            <Tab.Screen name="Search" component={HomeScreen} options={{
                tabBarShowLabel: false, tabBarIcon: ({ color }) => (
                    <View style={{
                        width: 60,
                        height: 60,
                        justifyContent: "center",
                        alignItems: "center",
                        backgroundColor: COLORS.white,
                        borderColor: COLORS.primary,
                        borderWidth: 2,
                        borderRadius: 30,
                        top: -25,
                        elevation: 5,
                    }}>
                        <Icon name="search" color={COLORS.primary} size={28} />
                    </View>
                )
            }} />
            {/* 頁籤4 Favorite */}
            <Tab.Screen name="Favorite" component={HomeScreen} options={{ tabBarShowLabel: false, tabBarIcon: ({ color }) => (<Icon name="favorite" color={color} size={28} />) }} />
            {/* 頁籤5 Cart */}
            <Tab.Screen name="Cart" component={CartScreen} options={{ tabBarShowLabel: false, tabBarIcon: ({ color }) => (<Icon name="shopping-cart" color={color} size={28} />) }} />
        </Tab.Navigator>
    )
}

export default BottomNavigator

src\views\components\Button.js

import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'
import React from 'react'
import COLORS from '../../consts/colors'

const PrimaryButton = ({ title, onPress = () => { } }) => {
    return (
        <TouchableOpacity
            activeOpacity={0.8}
            onPress={onPress}
        >
            <View style={styles.BtnContainer}>
                <Text style={styles.Title}>{title}</Text>
            </View>
        </TouchableOpacity>
    )
}

const OrderButton = ({ title, onPress = () => { } }) => {
    return (
        <TouchableOpacity
            activeOpacity={0.8}
            onPress={onPress}
        >
            {/* 這邊注意 套用 多重樣式的寫法 */}
            <View style={{ ...styles.BtnContainer, backgroundColor: COLORS.white }}>
                <Text style={{ ...styles.Title, color: COLORS.primary }}>{title}</Text>
            </View>
        </TouchableOpacity>
    )
}

const styles = StyleSheet.create({
    Title: { color: COLORS.white, fontWeight: 'bold', fontSize: 18 },
    BtnContainer: {
        backgroundColor: COLORS.primary,
        height: 60,
        borderRadius: 30,
        justifyContent: "center",
        alignItems: "center",
    },

})

// export default Button
//注意這邊 的匯出方式
export { PrimaryButton, OrderButton }

src\views\screens\CartScreen.js

import { StyleSheet, Text, View, SafeAreaView, ScrollView, Image, FlatList } from 'react-native'
import React from 'react'
import Icon from 'react-native-vector-icons/MaterialIcons';
import COLORS from "../../consts/colors"
import { PrimaryButton } from '../components/Button';
import foods from '../../consts/foods';

const CartScreen = ({ navigation }) => {
    // 購物車資料
    const CartCard = ({ item }) => {
        return (
            <View style={styles.CartCard}>
                {/* 圖片 */}
                <Image source={item.image}
                    style={{ width: 80, height: 80 }}
                />
                {/* 名稱跟價錢 */}
                <View style={{ flex: 1, height: 100, marginLeft: 10, paddingVertical: 20 }}>
                    <Text style={{ fontSize: 16, fontWeight: "bold" }}>{item.name}</Text>
                    <Text style={{ fontSize: 13, color: COLORS.grey }}>{item.ingredients}</Text>
                    <Text style={{ fontSize: 18, fontWeight: "bold" }}>${item.price}</Text>
                </View>
                {/* 訂購數量跟按鈕 */}
                <View style={{ marginLeft: 20, alignItems: "center" }}>
                    <Text style={{ fontSize: 18, fontWeight: "bold" }}>3</Text>
                    <View style={styles.ActionBtn}>
                        <Icon name="remove" size={25} color={COLORS.white} />
                        <Icon name="add" size={25} color={COLORS.white} />
                    </View>
                </View>
            </View>
        )
    }

    return (
        <SafeAreaView style={{ flex: 1, backgroundColor: COLORS.white }}>
            <View style={styles.Header}>
                <Icon name="arrow-back-ios" size={28} onPress={navigation.goBack} />
                <Text style={{ fontSize: 20, fontWeight: "bold" }}>Cart</Text>
            </View>
            {/* 購物車清單 */}
            <FlatList showsVerticalScrollIndicator={false}
                contentContainerStyle={{ paddingBottom: 80 }}
                data={foods}
                renderItem={({ item }) => <CartCard item={item} />}
                ListFooterComponentStyle={{ paddingHorizontal: 20, marginTop: 20 }}
                ListFooterComponent={() => (
                    <View>
                        <View style={{ flexDirection: "row", justifyContent: "space-between", marginVertical: 15 }}>
                            <Text style={{ fontSize: 18, fontWeight: "bold" }}>Total Price</Text>
                            <Text style={{ fontSize: 18, fontWeight: "bold" }}>$50</Text>
                        </View>
                        <View style={{ marginTop: 30 }}>
                            <PrimaryButton title="CHECKOUT" />
                        </View>
                    </View>
                )}
            />
        </SafeAreaView >
    )
}

{/* <PrimaryButton title="Get Started"
onPress={() => navigation.navigate("Home")}
/> */}

const styles = StyleSheet.create({
    Header: {
        flexDirection: "row",
        paddingHorizontal: 20,
        alignItems: "center",
        marginHorizontal: 20,
    },
    CartCard: {
        flexDirection: "row",
        height: 100,
        elevation: 15,
        borderRadius: 10,
        backgroundColor: COLORS.white,
        marginVertical: 10,
        marginHorizontal: 20,
        paddingHorizontal: 10,
        alignItems: "center",
    },
    ActionBtn: {
        flexDirection: "row",
        width: 80,
        height: 30,
        backgroundColor: COLORS.primary,
        borderRadius: 30,
        paddingHorizontal: 5,
        justifyContent: "space-between",
        alignItems: "center"
    },
})

export default CartScreen
  1. src\consts\categories.js
const categories = [
  {id: '1', name: 'pizza', image: require('../assets/catergories/pizza.png')},
  {id: '2', name: 'Burger', image: require('../assets/catergories/burger.png')},
  {id: '3', name: 'Sushi', image: require('../assets/catergories/sushi.png')},
  {id: '4', name: 'Salad', image: require('../assets/catergories/salad.png')},
];

export default categories;

src\views\screens\DetailsScreen.js

import { StyleSheet, Text, View, SafeAreaView, ScrollView, Image } from 'react-native'
import React from 'react'
import Icon from 'react-native-vector-icons/MaterialIcons';
import COLORS from "../../consts/colors"
import { OrderButton } from '../components/Button';

const DetailsScreen = ({ navigation, route }) => {
    // 設定 item變數 來接收 上一頁傳來的參數
    const item = route.params;
    // 印出 上一頁傳來的參數 測試用
    // console.log(item)

    return (
        <SafeAreaView style={{ backgroundColor: COLORS.white }}>
            <View style={styles.Header}>
                <Icon name="arrow-back-ios" size={28} onPress={() => navigation.goBack()} />
                <Text style={{ fontSize: 20, fontWeight: "bold" }}>Details</Text>
            </View>
            <ScrollView showsVerticalScrollIndicator={false}>
                {/* 食物大圖 */}
                <View style={{ justifyContent: "center", alignItems: "center", height: 280 }}>
                    <Image source={item.image} style={{ width: 220, height: 220, }} />
                </View>
                {/* 食物簡介 */}
                <View style={styles.Details}>
                    {/* 標題 */}
                    <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
                        <Text style={{ fontSize: 20, fontWeight: "bold", color: COLORS.white }}>{item.name}</Text>
                        <View style={styles.IconContainer}>
                            <Icon name="favorite-border" size={24} style={{ color: COLORS.primary }} />
                        </View>
                    </View>
                    {/* 文字區塊 */}
                    <Text style={styles.DetailsText}>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Veritatis id labore architecto eius necessitatibus hic beatae, esse quia provident doloribus. Minima nesciunt a sint eaque dolor illo blanditiis aut corporis.</Text>
                    {/* 訂購按鈕 */}
                    <View style={{ marginTop: 40, marginBottom: 40 }}>
                        <OrderButton title="Add to Cart" />
                    </View>
                </View>
            </ScrollView>
        </SafeAreaView >
    )
}

export default DetailsScreen

const styles = StyleSheet.create({
    Header: {
        flexDirection: "row",
        paddingHorizontal: 20,
        alignItems: "center",
        marginHorizontal: 20,
    },
    Details: {
        paddingHorizontal: 20,
        paddingTop: 40,
        paddingBottom: 60,
        backgroundColor: COLORS.primary,
        borderTopLeftRadius: 40,
        borderTopRightRadius: 40,
    },
    IconContainer: {
        width: 50,
        height: 50,
        borderRadius: 25,
        backgroundColor: COLORS.white,
        justifyContent: "center",
        alignItems: "center"
    },
    DetailsText: {
        marginTop: 10,
        lineHeight: 22,
        fontSize: 16,
        color: COLORS.white
    }
})

src\consts\foods.js

const foods = [
  {
    id: '1',
    name: 'Meat Pizza',
    ingredients: 'Mixed Pizza',
    price: '8.30',
    image: require('../assets/meatPizza.png'),
  },
  {
    id: '2',
    name: 'Cheese Pizza',
    ingredients: 'Cheese Pizza',
    price: '7.10',
    image: require('../assets/cheesePizza.png'),
  },
  {
    id: '3',
    name: 'Chicken Burger',
    ingredients: 'Fried Chicken',
    price: '5.10',
    image: require('../assets/chickenBurger.png'),
  },
  {
    id: '4',
    name: 'Sushi Makizushi',
    ingredients: 'Salmon Meat',
    price: '9.55',
    image: require('../assets/sushiMakizushi.png'),
  },
];

export default foods;

src\consts\colors.js

const COLORS = {
  white: '#FFF',
  dark: '#000',
  primary: '#F9813A',
  secondary: '#fedac5',
  light: '#E5E5E5',
  grey: '#908e8c',
};

範例 Source Code:

git clone https://smilehsu@bitbucket.org/smilehsu/yt_example_foodui0218.git

參考資料:

  1. 影片內容的程式碼: hakymz/FoodAppUIReactNative
  2. React Navigation 官網
  3. Bottom Tabs Navigator
  4. Bottom Tab Navigation with Animation | React-Navigation v6/5

上一篇
DAY13 - 看YT學React Native - UI 範例2
下一篇
DAY15 - 看YT學React Native - UI 範例4
系列文
總是學不來的中年大叔,孤獨的自學之旅第二年30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言