iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0

程式畫面預覽:

圖片

範例影片:

影片

程式碼:

app.js

import 'react-native-gesture-handler';
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import DrawerNavigator from './src/views/navigators/DrawerNavigator';
import DetailsScreen from './src/views/screens/DetailsScreen';
const Stack = createNativeStackNavigator();

const App = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        <Stack.Screen name="HomeScreen" component={DrawerNavigator} />
        <Stack.Screen name="DetailsScreen" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default App;

src\views\screens\DetailsScreen.js

mport { StyleSheet, Text, View, SafeAreaView, StatusBar, Image, ImageBackground } from 'react-native';
import React from 'react';

import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import COLORS from "../../const/colors"
import { Colors } from 'react-native/Libraries/NewAppScreen';

const DetailsScreen = ({ navigation, route }) => {
    // 這邊注意 怎麼跟上一頁(homescreen) 拿資料到這一頁來. 
    const pet = route.params;
    console.log("pet= ", pet)

    return (
        <SafeAreaView style={{ flex: 1, backgroundColor: COLORS.white }}>
            <StatusBar backgroundColor={COLORS.background} />
            {/* 標頭 */}
            <View style={{ height: 400, backgroundColor: COLORS.background }}>
                {/* 背景圖 */}
                <ImageBackground
                    source={pet?.image}
                    resizeMode="contain" style={{ height: 280, top: 20 }}
                >
                    <View style={styles.Header}>
                        <Icon name="arrow-left" size={28} color={COLORS.dark} onPress={navigation.goBack} />
                        <Icon name="dots-vertical" size={28} color={COLORS.dark} />
                    </View>
                </ImageBackground>
                {/* 中間的資訊欄 */}
                <View style={styles.DetailContainer}>
                    <View style={{
                        flexDirection: "row",
                        justifyContent: "space-between"
                    }}>
                        <Text style={{
                            fontSize: 20,
                            color: COLORS.dark,
                            fontWeight: "bold",
                        }}>
                            {pet?.name}</Text>
                        <Icon name="gender-male" size={25} color={COLORS.dark} />
                    </View>
                    <View style={{
                        flexDirection: "row",
                        justifyContent: "space-between",
                        marginTop: 5,
                    }}>
                        <Text style={{
                            fontSize: 12,
                            color: COLORS.dark,
                        }}>{pet?.type}</Text>
                        <Text style={{
                            fontSize: 13,
                            color: COLORS.dark,
                        }}>{pet?.age}</Text>
                    </View>
                    <View style={{ flexDirection: "row", marginTop: 5 }}>
                        <Icon name='map-marker' size={20} color={COLORS.primary} />
                        <Text style={{ fontSize: 14, marginLeft: 5, color: COLORS.grey }}>5 Green Swamp Road, Nyora, New South Wales, 2646 Australia</Text>
                    </View>
                </View>
            </View>
            {/* 下方資訊欄 */}
            <View style={{ flex: 1, marginTop: 80, justifyContent: "space-between" }}>
                <View>
                    <View style={{ flexDirection: "row", paddingHorizontal: 20 }}>
                        {/* 左邊:頭像  */}
                        <Image source={require("../../assets/person.png")}
                            style={{
                                width: 40,
                                height: 40,
                                borderRadius: 20
                            }}
                        />
                        {/* 右邊: 個人資訊 */}
                        <View style={{ flex: 1, paddingLeft: 10, height: 20 }}>
                            <Text style={{ fontSize: 12, color: COLORS.dark, fontWeight: "bold" }}>Smile Hsu</Text>
                            <Text style={{ fontSize: 11, color: COLORS.grey, fontWeight: "bold", marginTop: 2 }}>Owner</Text>
                        </View>
                        {/* 注意: 這邊文字一開始無法顯示 是要去 最上層 SafeAreaView 把 flex:1 加上去才正常 */}
                        <Text style={{ fontSize: 12, color: COLORS.grey }}>Feb 10, 2022</Text>
                    </View>
                    <Text style={styles.Comment}>Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima id exercitationem impedit eveniet nobis molestias nostrum dolorem officiis?</Text>
                </View>
                {/* 頁尾 按鈕 */}
                <View style={styles.Footer}>
                    <View style={styles.IconContainer}>
                        <Icon name='heart-outline' size={22} color={COLORS.white} />
                    </View>
                    <View style={styles.Btn}>
                        <Text style={{ color: COLORS.white, fontWeight: "bold" }}>ADOPTION</Text>
                    </View>
                </View>
            </View>
        </SafeAreaView>
    );
};

const styles = StyleSheet.create({
    Header: {
        flexDirection: 'row',
        justifyContent: "space-between",
        padding: 20,
    }, DetailContainer: {
        flex: 1,
        height: 120,
        backgroundColor: COLORS.white,
        padding: 20,
        marginHorizontal: 20,
        bottom: -60,
        elevation: 10,
        borderRadius: 20,
        justifyContent: "center",
    },
    Comment: {
        marginTop: 10,
        fontSize: 12.5,
        color: COLORS.dark,
        marginHorizontal: 20,
        lineHeight: 20,
    },
    Footer: {
        flexDirection: "row",
        height: 100,
        backgroundColor: COLORS.light,
        borderTopRightRadius: 30,
        borderTopLeftRadius: 30,
        alignItems: "center",
        paddingHorizontal: 20,
    },
    IconContainer: {
        backgroundColor: COLORS.primary,
        width: 50,
        height: 50,
        borderRadius: 12,
        justifyContent: "center",
        alignItems: "center",
        marginTop: 15
    },
    Btn: {
        flex: 1,
        backgroundColor: COLORS.primary,
        height: 50,
        borderRadius: 12,
        justifyContent: "center",
        alignItems: "center",
        marginTop: 15,
        marginLeft: 20,
    },
});

export default DetailsScreen;

src\views\navigators\DrawerNavigator.js

import { StatusBar, View, Image, Text } from 'react-native';
import React from 'react';
import {
    createDrawerNavigator,
    DrawerContentScrollView,
    DrawerItemList,
    useDrawerProgress,
    useDrawerStatus
} from '@react-navigation/drawer';
import HomeScreen from "../screens/HomeScreen"
import COLORS from '../../const/colors';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import Animated from 'react-native-reanimated';

const Drawer = createDrawerNavigator();

//
const CustomDrawerContent = props => {
    return (
        <DrawerContentScrollView style={{ paddingVertical: 30 }}>
            <View style={{ marginLeft: 20, marginVertical: 40 }}>
                <Image source={require("../../assets/person.png")}
                    style={{
                        width: 70,
                        height: 70,
                        borderRadius: 20
                    }}
                />
                <Text style={{ color: COLORS.white, fontSize: 13, fontWeight: "bold", marginTop: 10 }}> Smile Hsu </Text>
            </View>
            <DrawerItemList
                {...props}
            />
        </DrawerContentScrollView>
    )
}

const DrawerScreenContainer = ({ children }) => {
    // 判斷 Drawer 頁面是否作用中
    const isDrawerOpen = useDrawerStatus()
    //
    const progress = useDrawerProgress()
    //
    const scale = Animated.interpolateNode(progress, { inputRange: [0, 1], outputRange: [1, 0.8] })
    //
    const borderRadius = Animated.interpolateNode(progress, { inputRange: [0, 1], outputRange: [0, 25] })
    return (
        <Animated.View style={{ backgroundColor: COLORS.white, flex: 1, transform: [{ scale }], overflow: "hidden", borderRadius }}>
            <StatusBar barStyle='dark-content'
                // 根據 Drawer 頁面是否作用中 改變 statusbar 的顏色
                backgroundColor={isDrawerOpen == "open" ? COLORS.primary : COLORS.white} />
            {children}
        </Animated.View>
    )
}

const DrawerNavigator = () => {
    return (
        <Drawer.Navigator screenOptions={{
            headerShown: false,
            drawerType: "slide",
            drawerStyle: {
                width: 200,
                backgroundColor: COLORS.primary,
            },
            overlayColor: null,
            sceneContainerStyle: {
                // 此設定 在 android無效? 影片是用iso
                backgroundColor: COLORS.primary
            },
            // 作用中的顏色
            drawerActiveTintColor: COLORS.white,
            //非作用中的顏色
            drawerInactiveTintColor: COLORS.secondary,
            //取消 選項的 背景顏色
            drawerItemStyle: { backgroundColor: null },
            //
            drawerLabelStyle: { fontWeight: "bold" },
        }}
            drawerContent={props => <CustomDrawerContent {...props} />}
        >
            {/* ==================== 選單項目 ==================== */}
            <Drawer.Screen name="Home" options={{
                title: "ADOPTION", drawerIcon: ({ color }) =>
                    <Icon name="paw"
                        size={25}
                        color={color}
                        style={{ marginRight: -20 }}
                    />
            }} >
                {(props) =>
                (<DrawerScreenContainer>
                    <HomeScreen {...props} />
                </DrawerScreenContainer>)
                }
            </Drawer.Screen>
            {/* ==================== 選單項目 ==================== */}
            <Drawer.Screen name="DONATION" options={{
                drawerIcon: ({ color }) =>
                    <Icon name="gift"
                        size={25}
                        color={color}
                        style={{ marginRight: -20 }}
                    />
            }} >
                {(props) =>
                (<DrawerScreenContainer>
                    <HomeScreen {...props} />
                </DrawerScreenContainer>)
                }
            </Drawer.Screen>
            {/* ==================== 選單項目 ==================== */}
            <Drawer.Screen name="ADD PET" options={{
                drawerIcon: ({ color }) =>
                    <Icon name="plus-box"
                        size={25}
                        color={color}
                        style={{ marginRight: -20 }}
                    />
            }} >
                {(props) =>
                (<DrawerScreenContainer>
                    <HomeScreen {...props} />
                </DrawerScreenContainer>)
                }
            </Drawer.Screen>
            {/* ==================== 選單項目 ==================== */}
            <Drawer.Screen name="FAVORITES" options={{
                drawerIcon: ({ color }) =>
                    <Icon name="heart"
                        size={25}
                        color={color}
                        style={{ marginRight: -20 }}
                    />
            }} >
                {(props) =>
                (<DrawerScreenContainer>
                    <HomeScreen {...props} />
                </DrawerScreenContainer>)
                }
            </Drawer.Screen>
            {/* ==================== 選單項目 ==================== */}
            <Drawer.Screen name="PROFILE" options={{
                drawerIcon: ({ color }) =>
                    <Icon name="account"
                        size={25}
                        color={color}
                        style={{ marginRight: -20 }}
                    />
            }} >
                {(props) =>
                (<DrawerScreenContainer>
                    <HomeScreen {...props} />
                </DrawerScreenContainer>)
                }
            </Drawer.Screen>

        </Drawer.Navigator>
    );
};

export default DrawerNavigator;

src\views\screens\HomeScreen.js

import React, { useEffect, useState } from 'react';
import {
    Dimensions,
    StyleSheet,
    Text,
    View,
    SafeAreaView,
    Image,
    TextInput,
    TouchableOpacity,
    FlatList
} from 'react-native';
import { ScrollView } from 'react-native-virtualized-view';

import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import COLORS from "../../const/colors"
//寵物資料的api
import pets from '../../const/pets';
//
const { height } = Dimensions.get("window")
//寵物分類的選單項目
const petCategories = [
    { name: 'CATS', icon: 'cat' },
    { name: 'DOGS', icon: 'dog' },
    { name: 'BIRDS', icon: 'ladybug' },
    { name: 'BUNNIES', icon: 'rabbit' },
];

// 寵物卡片
const Card = ({ pet, navigation }) => {
    return (
        <TouchableOpacity activeOpacity={0.8} onPress={() => navigation.navigate("DetailsScreen", pet)}>
            <View style={styles.CardContainer}>
                {/* 寵物的圖片 */}
                <View style={styles.CardImageContainer}>
                    <Image source={pet.image}
                        style={{ width: "100%", height: "100%", resizeMode: "contain" }}
                    />
                </View>
                {/* 寵物的簡介 */}
                <View style={styles.CardDetailContainer}>
                    <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
                        <Text style={{ color: COLORS.dark, fontSize: 20, fontWeight: "bold" }}>{pet?.name}</Text>
                        <Icon name='gender-male' size={22} color={COLORS.grey}
                        />
                    </View>
                    <Text style={{ fontSize: 12, marginTop: 5, color: COLORS.dark }}>{pet?.type}</Text>
                    <Text style={{ fontSize: 10, marginTop: 5, color: COLORS.grey }}>{pet?.age}</Text>
                    <View style={{ flexDirection: "row", marginTop: 5 }}>
                        <Icon name='map-marker' size={18} color={COLORS.primary} />
                        <Text style={{ fontSize: 12, marginLeft: 5, color: COLORS.primary }}>Distance:7.8km</Text>
                    </View>
                </View>
            </View>
        </TouchableOpacity >
    )
}

const HomeScreen = ({ navigation }) => {
    //
    const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0)
    const [filteredPets, setFilteredPet] = useState([])

    //篩選寵物
    const filterPet = (index) => {
        const currentPets = pets.filter((item) => item?.pet?.toLocaleUpperCase() == petCategories[index].name)[0].pets
        // console.log("currentPets =", currentPets[0])
        setFilteredPet(currentPets)
    }

    //
    useEffect(() => {
        filterPet(0)
    }, [])

    return (
        <SafeAreaView style={{ flex: 1, backgroundColor: COLORS.white }}>
            {/* 標頭  header */}
            <View style={styles.Header}>
                <Icon name="sort-variant" size={28} onPress={navigation.toggleDrawer} />
                <Text style={{ color: COLORS.primary, fontSize: 16, fontWeight: "bold" }}>Smile Hsu</Text>
                <Image source={require(".././../assets/person.png")} style={{ width: 30, height: 30, borderRadius: 15 }} />
            </View>
            {/* 主要內容 main */}
            <ScrollView showsVerticalScrollIndicator={false}>
                <View style={styles.MainContainer}>
                    {/* 搜尋列 search bar */}
                    <View style={styles.SearchBarContainer}>
                        <Icon name='magnify' size={24} color={COLORS.grey} />
                        {/* flex:1 讓字靠右 */}
                        <TextInput
                            placeholder='Search pet to adopt'
                            placeholderTextColor={COLORS.grey}
                            style={{ flex: 1 }} />
                        <Icon name="sort-ascending" size={24} color={COLORS.grey} />
                    </View>
                    {/* 寵物分類選單 */}
                    <View style={{
                        flexDirection: "row",
                        justifyContent: "space-between",
                        marginTop: 20,
                    }}>
                        {/* 用map輸出 寵物分類選單的項目 */}
                        {petCategories.map((item, index) => (
                            // 讓icon跟下方文字 置中對齊 要寫在這邊
                            <View key={"pet" + index} style={{ alignItems: "center" }}>
                                {/* 這邊注意 點選icon 會改變顏色 的寫法 */}
                                {/* 注意 onpress 加入 filterPet(index) 第一次用到 大刮號 加兩個函式 */}
                                <TouchableOpacity style={[styles.CategoryBtn, { backgroundColor: selectedCategoryIndex == index ? COLORS.primary : COLORS.white }]}
                                    onPress={() => {
                                        filterPet(index)
                                        setSelectedCategoryIndex(index)
                                    }}
                                >
                                    <Icon name={item.icon} size={30} color={selectedCategoryIndex == index ? COLORS.white : COLORS.primary} />
                                </TouchableOpacity>
                                <Text style={styles.CategoryBtnName}>{item.name}</Text>
                            </View>
                        ))}
                    </View>
                    {/* 下方的寵物展示 內容區塊 */}
                    <View style={{ marginTop: 20 }}>
                        <FlatList
                            showsVerticalScrollIndicator={false}

                            data={filteredPets}
                            renderItem={({ item }) => <Card pet={item} navigation={navigation} />}
                        />
                    </View>
                </View>
            </ScrollView>
        </SafeAreaView>
    );
};

const styles = StyleSheet.create({
    Header: {
        padding: 20,
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
    },
    MainContainer: {
        minHeight: height,
        backgroundColor: COLORS.light,
        marginTop: 20,
        borderTopLeftRadius: 40,
        borderTopRightRadius: 40,
        paddingHorizontal: 20,
        paddingVertical: 40,
    },
    SearchBarContainer: {
        flexDirection: "row",
        height: 50,
        backgroundColor: COLORS.white,
        borderRadius: 7,
        paddingHorizontal: 20,
        justifyContent: "space-between",
        alignItems: "center",
    },
    CategoryBtn: {
        width: 50,
        height: 50,
        justifyContent: "center",
        alignItems: "center",
        borderRadius: 10,
        backgroundColor: COLORS.primary,
    },
    CategoryBtnName: {
        color: COLORS.dark,
        fontSize: 10,
        fontWeight: "bold",
        marginTop: 5,
    },
    CardContainer: {
        flexDirection: "row",
        alignItems: "center",
        marginBottom: 20,

    }, CardImageContainer: {
        width: 140,
        height: 150,
        backgroundColor: COLORS.background,
        borderRadius: 20,
    },
    CardDetailContainer: {
        flex: 1,
        height: 120,
        backgroundColor: COLORS.white,
        borderTopRightRadius: 10,
        borderBottomRightRadius: 10,
        padding: 20,
        justifyContent: "center"
    },
});

export default HomeScreen;

src\const\pet.js

const pets = [
  {
    pet: 'cats',
    pets: [
      {
        id: '1',
        name: 'Lily',
        image: require('../assets/cat1.png'),
        type: 'Chausie',
        age: '5 years old',
      },
      {
        id: '2',
        name: 'Lucy',
        image: require('../assets/cat2.png'),
        type: 'Bobtail',
        age: '2 years old',
      },
      {
        id: '3',
        name: 'Nala',
        image: require('../assets/cat3.png'),
        type: 'Ragamuffin',
        age: '2 years old',
      },
    ],
  },
  {
    pet: 'dogs',
    pets: [
      {
        id: '1',
        name: 'Bally',
        image: require('../assets/dog1.png'),
        type: 'German Shepherd',
        age: '2 years old',
      },
      {
        id: '2',
        name: 'Max',
        image: require('../assets/dog2.png'),
        type: 'Foxhound',
        age: '2 years old',
      },
    ],
  },
  {
    pet: 'birds',
    pets: [
      {
        id: '1',
        name: 'Coco',
        image: require('../assets/bird1.png'),
        type: 'Parrot',
        age: '2 years old',
      },
      {
        id: '2',
        name: 'Alfie',
        image: require('../assets/bird2.png'),
        type: 'Parrot',
        age: '4 years old',
      },
    ],
  },
  {
    pet: 'bunnies',
    pets: [
      {
        id: '1',
        name: 'Boots',
        image: require('../assets/bunny1.png'),
        type: 'Angora',
        age: '1 years old',
      },
      {
        id: '2',
        name: 'Pookie',
        image: require('../assets/bunny2.png'),
        type: 'Angora',
        age: '1 years old',
      },
    ],
  },
];

export default pets;

src\const\colors.js

const COLORS = {
  primary: '#306060',
  secondary: '#88b3b5',
  white: '#FFF',
  dark: '#616161',
  light: '#f5f5f5',
  grey: '#a8a8a8',
  background: '#d0d8dc',
  orange: '#f5a623',
  green: '#00B761',
};

export default COLORS;

範例 Source Code:

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

參考資料:

  1. 影片內容的程式碼: hakymz/PetAdoptionAppReactNative
  2. React Navigation 官網
  3. [ 卡卡 DAY 13 ] - React Native 頁面導覽 Navigation (上)
  4. React Native Reanimated的使用
  5. 解Bug- Animated.js引入錯誤
  6. 解Bug- react-native-virtualized-view

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

尚未有邦友留言

立即登入留言