iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

前言:

這個範例教學影片,把套件安裝的流程給省略了
下面會補充套件安裝的清單

React Native 套件:

npm install @react-navigation/native

npm install react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

npm install react-native-vector-icons

npm install @react-navigation/stack

程式畫面預覽:

圖片1

範例影片:

Yes

程式碼:

app.js

import React from 'react';
import "react-native-gesture-handler";
import { View, Text } from 'react-native';
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 HomeScreen from "./src/views/screens/HomeScreen"
import DetailsScreen from "./src/views/screens/DetailsScreen"

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <StatusBar barStyle="light-content" backgroundColor="rgba(0, 0, 0, 0)" translucent />
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Details" component={DetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default App;

src\views\screens\DetailsScreen.js

import React from 'react';
import { StyleSheet, Text, View, ScrollView, Image, ImageBackground } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';

import COLORS from '../../consts/colors';

const DetailScreen = ({ navigation, route }) => {
    const item = route.params
    console.log(item)
    return (
        <ScrollView
            showsVerticalScrollIndicator={false}
            contentContainerStyle={{ backgroundColor: COLORS.white, paddingBottom: 20 }}>

            {/* 第一次用到 */}
            <ImageBackground
                style={styles.HeaderImage}
                source={item.image}
            >
                <View style={styles.Header}>
                    {/* 回上一頁 */}
                    <Icon name="arrow-back" size={28} color={COLORS.white} onPress={navigation.goBack} />
                    {/* 加入書籤 純展示 無作用 */}
                    <Icon name="bookmark-border" size={28} color={COLORS.white} />
                </View>
            </ImageBackground>
            <View>
                {/* 地址 icon */}
                <View style={styles.IconContainer}>
                    <Icon name="place" size={28} color={COLORS.white} />
                </View>
                <View style={{ marginTop: 20, paddingHorizontal: 20, }}>
                    {/* 飯店名稱 */}
                    <Text style={{ fontSize: 20, fontWeight: "bold" }}>{item.name}</Text>
                    <Text style={{
                        fontSize: 12,
                        fontWeight: "400",
                        color: COLORS.grey,
                        marginTop: 5,
                    }}>
                        {/* 飯店地址 */}
                        {item.location}</Text>
                    <View style={{ marginTop: 10, flexDirection: "row", justifyContent: "space-between" }}>
                        <View style={{ flexDirection: 'row' }}>
                            {/* 飯店評價 */}
                            <View style={{ flexDirection: "row" }}>
                                <Icon name="star" size={20} color={COLORS.orange} />
                                <Icon name="star" size={20} color={COLORS.orange} />
                                <Icon name="star" size={20} color={COLORS.orange} />
                                <Icon name="star" size={20} color={COLORS.orange} />
                                <Icon name="star" size={20} color={COLORS.grey} />
                            </View>
                            <Text style={{ fontSize: 18, fontWeight: "bold", marginLeft: 5 }}>4.0</Text>
                        </View>
                        {/* 飯店點閱數 */}
                        <Text style={{ fontSize: 13, color: COLORS.grey }}> 9487reviews</Text>
                    </View>
                    {/* 飯店簡介 */}
                    <View style={{ marginTop: 20 }}>
                        <Text style={{ lineHeight: 20, color: COLORS.grey }}>{item.details}</Text>
                    </View>
                </View>
                {/* 價錢 */}
                <View style={{ marginTop: 20, flexDirection: "row", justifyContent: "space-between", paddingLeft: 20, alignItems: "center" }}>
                    <Text style={{ fontSize: 20, fontWeight: "bold" }}>Price per night</Text>
                    <View style={styles.PriceTag}>
                        <Text style={{ fontSize: 16, fontWeight: "bold", color: COLORS.grey, marginLeft: 5 }}>${item.price}</Text>
                        <Text style={{ fontSize: 12, fontWeight: "bold", color: COLORS.grey, marginLeft: 5 }}>+早餐</Text>
                    </View>
                </View>
                {/* 訂房按鈕 */}
                <View style={styles.Btn}>
                    <Text style={{ fontSize: 24, fontWeight: "bold", color: COLORS.white }}>Book Now</Text>
                </View>
            </View>
        </ScrollView >
    );
};

const styles = StyleSheet.create({
    HeaderImage: {
        height: 400,
        borderBottomLeftRadius: 40,
        borderBottomRightRadius: 40,
        overflow: "hidden"
    },
    Header: {
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
        marginTop: 40,
        marginHorizontal: 20,
    },
    IconContainer: {
        position: "absolute",
        width: 60,
        height: 60,
        backgroundColor: COLORS.primary,
        top: -30,
        right: 20,
        borderRadius: 30,
        justifyContent: "center",
        alignItems: "center",
    },
    PriceTag: {
        flex: 1,
        flexDirection: "row",
        height: 40,
        alignItems: "center",
        marginLeft: 40,
        paddingLeft: 20,
        backgroundColor: COLORS.secondary,
        borderTopLeftRadius: 20,
        borderBottomLeftRadius: 20,
    },
    Btn: {
        height: 55,
        justifyContent: "center",
        alignItems: "center",
        marginTop: 40,
        backgroundColor: COLORS.primary,
        marginHorizontal: 20,
        borderRadius: 10
    },
});

export default DetailScreen;

src\views\screens\HomeScreen.js

import React, { useState, useRef } from 'react';
import {
    Dimensions,
    FlatList,
    SafeAreaView,
    ScrollView,
    StyleSheet,
    Text,
    TextInput,
    TouchableOpacity,
    View,
    Image,
    Animated, e
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Component } from 'react/cjs/react.development';
import COLORS from '../../consts/colors';
import hotels from '../../consts/hotels';

const { width } = Dimensions.get("screen");
const CardWidth = width / 1.8

const HomeScreen = ({ navigation }) => {
    const categories = ['All', 'Popular', 'Top Rated', 'Featured', 'Luxury'];
    const [selectedCategoryindex, setSelectedCategoryindex] = useState(0)
    const [activeCardIndex, setActiveCardIndex] = useState(0)
    //
    const scrollX = useRef(new Animated.Value(0)).current;

    // 分類bar Component
    const CategoryList = ({ navigation }) => {
        return (
            <View style={styles.CategoryListContainer}>
                {categories.map((item, index) => (
                    <TouchableOpacity key={index} activeOpacity={0.8} onPress={() => setSelectedCategoryindex(index)}>
                        <View>
                            {/* 第一次看到 點點點+style的用法  這邊的用途是 被點選的分類 顏色會不同*/}
                            <Text style={{ ...styles.CategoryListText, color: selectedCategoryindex == index ? COLORS.primary : COLORS.grey }}>{item}</Text>
                        </View>
                        {selectedCategoryindex == index && (
                            // 下底線
                            < View style={{ height: 3, width: 30, backgroundColor: COLORS.primary, marginTop: 2 }} />
                        )}
                    </TouchableOpacity>
                ))}
            </View>
        )
    }

    // 房間卡片 Component
    const Card = ({ hotel, index }) => {
        const inputRange = [
            (index - 1) * CardWidth,
            index * CardWidth,
            (index + 1) * CardWidth,
        ];
        // 房間卡片 透明度動畫
        const opacity = scrollX.interpolate({
            inputRange,
            outputRange: [0.7, 0, 0.7],
        });
        // 房間卡片大小 縮放動畫
        const scale = scrollX.interpolate({
            inputRange,
            outputRange: [0.8, 1, 0.8],
        });

        return (
            <TouchableOpacity
                activeOpacity={1}
                //控制 目前滑到的頁面 才能點擊 旁邊的卡片點了無作用 (需配合下面 寫的 CardNo判斷式)
                disabled={activeCardIndex != index}
                onPress={() => navigation.navigate("Details", hotel)}>
                <Animated.View style={{ ...styles.Card, transform: [{ scale }] }}>
                    {/* 卡片 半透明變色 遮罩 */}
                    <Animated.View style={{ ...styles.CardOverLay, opacity }} />
                    {/* 右上角 價錢標籤 */}
                    <View style={styles.PriceTag}>
                        <Text style={{ color: COLORS.white, fontSize: 20, fontWeight: "bold" }}>${hotel.price}</Text>
                    </View>
                    {/* 房間圖片 */}
                    <Image
                        source={hotel.image}
                        style={styles.CardImage}
                    />
                    {/* 房間簡介 */}
                    <View style={styles.CardDetails}>
                        <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
                            <View>
                                {/* 飯店名稱 */}
                                <Text style={{ fontSize: 17, fontWeight: "bold" }}>{hotel.name}</Text>
                                {/* 地址 */}
                                <Text style={{ fontSize: 12, color: COLORS.grey }}>{hotel.location}</Text>
                            </View>
                            <Icon name="bookmark-border" size={26} color={COLORS.primary} />
                        </View>
                        {/* 飯店評價星數 點閱數 */}
                        {/* 可改善的地方: 星數 跟瀏覽數 做成api 控制輸出 */}
                        <View style={{
                            flexDirection: 'row',
                            justifyContent: 'space-between',
                            marginTop: 10,
                        }}>
                            <View style={{ flexDirection: 'row' }}>
                                <Icon name="star" size={15} color={COLORS.orange} />
                                <Icon name="star" size={15} color={COLORS.orange} />
                                <Icon name="star" size={15} color={COLORS.orange} />
                                <Icon name="star" size={15} color={COLORS.orange} />
                                <Icon name="star" size={15} color={COLORS.grey} />
                            </View>
                            <Text style={{ fontSize: 10, color: COLORS.grey }}>365reviews</Text>
                        </View>
                    </View>
                </Animated.View>
            </TouchableOpacity>
        )
    }
    // 房間卡片2 top hotel card
    const TopHotelCard = ({ hotel }) => {
        return (
            <View style={styles.TopHotelCard}>
                <View style={{
                    position: "absolute",
                    top: 5,
                    right: 5,
                    zIndex: 1,
                    flexDirection: "row"
                }}>
                    <Icon name="star"
                        size={15}
                        color={COLORS.orange} />
                    <Text style={{ color: COLORS.white, fontSize: 15, fontWeight: "bold" }}>5.0</Text>
                </View>
                <Image style={styles.TopHotelCardImage}
                    source={hotel.image} />
                <View style={{ paddingVertical: 5, paddingHorizontal: 10 }}>
                    <Text style={{ fontSize: 10, fontWeight: "bold" }}>{hotel.name}</Text>
                    <Text style={{ fontSize: 7, color: COLORS.grey }}>{hotel.location}</Text>
                </View>
            </View>
        )
    }
    return (
        <SafeAreaView style={{ flex: 1, backgroundColor: COLORS.white }}>
            {/* 標頭 */}
            <View style={styles.Header}>
                <View style={{ paddingBottom: 15 }}>
                    <Text style={{ fontSize: 30, fontWeight: "bold" }}>Find your hotel</Text>
                    <View style={{ flexDirection: "row" }}>
                        <Text style={{ fontSize: 30, fontWeight: "bold" }}>in</Text>
                        <Text style={{ fontSize: 30, fontWeight: "bold", color: COLORS.primary }}>Taipei</Text>
                    </View>
                </View>
                <Icon name="person-outline" size={38} color={COLORS.grey} />
            </View>
            {/* 標頭以下的區塊 都可滑動 */}
            <ScrollView showsVerticalScrollIndicator={false}>
                {/* 搜尋框 */}
                <View style={styles.SearchInputContainer}>
                    <Icon name="search" size={30} style={{ marginLeft: 20 }} />
                    <TextInput placeholder='Search' style={{ fontSize: 20, paddingLeft: 10 }} />
                </View>
                {/* 分類bar */}
                <CategoryList />
                {/* 房間展示的卡片 */}
                <View>
                    <Animated.FlatList
                        //判斷 目前 滑到第幾張 房間卡片
                        //影片中 只跳一次數字 我的 一次跳好幾筆(重覆的數值)
                        onMomentumScrollEnd={(e) => {
                            // console.log(Math.round(e.nativeEvent.contentOffset.x / CardWidth))
                            const CardNo = e.nativeEvent.contentOffset.x / CardWidth
                            setActiveCardIndex(CardNo)
                        }}
                        //
                        onScroll={Animated.event(
                            [{ nativeEvent: { contentOffset: { x: scrollX } } }],
                            { useNativeDriver: true },
                        )}
                        // FlatList 讓卡片橫排 用 horizontal 而不是 像一般 在外面加 flexDirection: row
                        horizontal
                        contentContainerStyle={{ paddingVertical: 30, paddingLeft: 20, paddingRight: CardWidth / 2 - 40 }}
                        data={hotels}
                        showsHorizontalScrollIndicator={false}
                        renderItem={({ item, index }) => <Card hotel={item} index={index} />}
                        // 定格效果 看不太出來
                        snapToInterval={CardWidth}
                    />
                </View>
                {/* 下方的區塊 */}
                <View style={{ flexDirection: "row", justifyContent: "space-between", marginHorizontal: 20 }}>
                    <Text style={{ fontWeight: "bold", color: COLORS.grey }}>Top Hotels</Text>
                    <Text style={{ color: COLORS.grey }}>Show all</Text>
                </View>
                <FlatList
                    data={hotels}
                    horizontal
                    showsHorizontalScrollIndicator={false}
                    contentContainerStyle={{ paddingLeft: 20, marginTop: 20, paddingBottom: 30 }}
                    // renderItem={({ item }) => <TopHotelCard hotel={item} />}
                    renderItem={({ item }) => <TopHotelCard hotel={item} />}
                />
            </ScrollView>
        </SafeAreaView>
    );
};

const styles = StyleSheet.create({
    Header: {
        flexDirection: "row",
        marginTop: 20,
        // 畫面兩個物件要左右頭尾放 用space-between
        justifyContent: "space-between",
        paddingHorizontal: 20,
    },
    SearchInputContainer: {
        height: 50,
        backgroundColor: COLORS.light,
        marginTop: 15,
        marginLeft: 20,
        borderBottomLeftRadius: 30,
        flexDirection: "row",
        alignItems: "center"
    },
    CategoryListContainer: {
        flexDirection: "row",
        justifyContent: "space-between",
        marginHorizontal: 20,
        marginTop: 30,
    },
    CategoryListText: {
        fontSize: 17,
        fontWeight: "bold",
    },
    Card: {
        width: CardWidth,
        height: 280,
        elevation: 15,
        marginRight: 20,
        borderRadius: 15,
        backgroundColor: COLORS.white,
    },
    PriceTag: {
        width: 80,
        height: 60,
        backgroundColor: COLORS.primary,
        position: "absolute",
        zIndex: 1,
        right: 0,
        borderTopRightRadius: 15,
        borderBottomLeftRadius: 15,
        justifyContent: "center",
        alignItems: "center",

    },
    CardImage: {
        width: "100%",
        height: 200,
        borderTopLeftRadius: 15,
        borderTopRightRadius: 15,
    },
    CardDetails: {
        width: "100%",
        height: 100,
        borderRadius: 15,
        backgroundColor: COLORS.light,
        position: "absolute",
        bottom: 0,
        padding: 20,
    },
    CardOverLay: {
        height: 280,
        backgroundColor: COLORS.white,
        position: 'absolute',
        zIndex: 100,
        width: CardWidth
    },
    TopHotelCard: {
        width: 120,
        height: 120,
        backgroundColor: COLORS.white,
        elevation: 15,
        marginHorizontal: 10,
        borderRadius: 10,
    },
    TopHotelCardImage: {
        width: "100%",
        height: 80,
        borderTopLeftRadius: 10,
        borderTopRightRadius: 10,
    }
});

export default HomeScreen;

src\consts\hotels.js

const hotels = [
  {
    id: '1',
    name: 'Silver Hotel & SPA',
    location: 'Green street,Central district',
    price: 120,
    image: require('../images/hotel1.jpg'),
    details: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consequat nisl vel pretium lectus quam id leo. Velit euismod in pellentesque massa placerat duis ultricies lacus sed. Justo laoreet sit amet cursus sit`,
  },
  {
    id: '2',
    name: 'Bring Hotel',
    location: 'Yuki street',
    price: 70,
    image: require('../images/hotel2.jpg'),
    details: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consequat nisl vel pretium lectus quam id leo. Velit euismod in pellentesque massa placerat duis ultricies lacus sed. Justo laoreet sit amet cursus sit`,
  },
  {
    id: '3',
    name: 'Aluna Hotel',
    location: 'Almond street',
    price: 90,
    image: require('../images/hotel3.jpg'),
    details: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consequat nisl vel pretium lectus quam id leo. Velit euismod in pellentesque massa placerat duis ultricies lacus sed. Justo laoreet sit amet cursus sit`,
  },
  {
    id: '4',
    name: 'Green Hotel',
    location: 'Main street',
    price: 100,
    image: require('../images/hotel4.jpg'),
    details: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Consequat nisl vel pretium lectus quam id leo. Velit euismod in pellentesque massa placerat duis ultricies lacus sed. Justo laoreet sit amet cursus sit`,
  },
];

export default hotels;

src\consts\colors.js

const COLORS = {
  white: '#FFF',
  dark: '#000',
  primary: '#52c0b4',
  secondary: '#e0f4f1',
  light: '#f0f0f0',
  grey: '#908e8c',
  orange: '#f5a623',
  green: '#00B761',
};

export default COLORS;

範例 Source Code:

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

參考資料:

  1. 影片內容的程式碼: hakymz/HotelAppUiReactNative
  2. UI source
  3. React Navigation 官網

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

尚未有邦友留言

立即登入留言