這個範例教學影片,把套件安裝的流程給省略了
下面會補充套件安裝的清單
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
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