Appearance
样式美化
在完成了待办清单应用的核心功能后,我们需要对应用进行样式美化,提升用户体验。在这一节中,我们将学习如何使用 React Native 的样式系统创建美观、现代的用户界面。
1. 主题与色彩方案
首先,让我们定义一个统一的主题和色彩方案,确保应用的视觉一致性:
js
// src/theme.js
export const theme = {
colors: {
primary: '#4F46E5', // 主色调 - 靛蓝色
secondary: '#10B981', // 次要色调 - 绿色
background: '#F3F4F6', // 背景色
surface: '#FFFFFF', // 表面色
text: '#1F2937', // 主要文本色
textSecondary: '#6B7280', // 次要文本色
border: '#E5E7EB', // 边框色
error: '#EF4444', // 错误色
success: '#10B981', // 成功色
warning: '#F59E0B', // 警告色
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
borderRadius: {
sm: 4,
md: 8,
lg: 12,
xl: 16,
},
shadows: {
sm: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
md: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
},
};2. 组件样式优化
2.1 容器样式
js
// src/components/TodoContainer.js
import React from 'react';
import { View, StyleSheet, SafeAreaView } from 'react-native';
import { theme } from '../theme';
const TodoContainer = ({ children }) => {
return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
{children}
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
content: {
flex: 1,
padding: theme.spacing.md,
},
});
export default TodoContainer;2.2 标题样式
js
// src/components/TodoHeader.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { theme } from '../theme';
const TodoHeader = ({ title, subtitle }) => {
return (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
{subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
</View>
);
};
const styles = StyleSheet.create({
header: {
marginBottom: theme.spacing.lg,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: theme.colors.text,
marginBottom: theme.spacing.xs,
},
subtitle: {
fontSize: 16,
color: theme.colors.textSecondary,
},
});
export default TodoHeader;2.3 待办项样式
js
// src/components/TodoItem.js
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { theme } from '../theme';
const TodoItem = ({ todo, onToggle, onDelete }) => {
return (
<TouchableOpacity
style={[styles.item, todo.completed && styles.completedItem]}
onPress={() => onToggle(todo.id)}
>
<View style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}>
{todo.completed && <Text style={styles.checkmark}>✓</Text>}
</View>
<Text
style={[styles.text, todo.completed && styles.completedText]}
numberOfLines={1}
>
{todo.text}
</Text>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => onDelete(todo.id)}
>
<Text style={styles.deleteText}>×</Text>
</TouchableOpacity>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
item: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.surface,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.md,
marginBottom: theme.spacing.sm,
...theme.shadows.sm,
},
completedItem: {
opacity: 0.7,
},
checkbox: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: theme.colors.primary,
marginRight: theme.spacing.sm,
justifyContent: 'center',
alignItems: 'center',
},
checkboxCompleted: {
backgroundColor: theme.colors.primary,
},
checkmark: {
color: theme.colors.surface,
fontWeight: 'bold',
},
text: {
flex: 1,
fontSize: 16,
color: theme.colors.text,
},
completedText: {
textDecorationLine: 'line-through',
color: theme.colors.textSecondary,
},
deleteButton: {
padding: theme.spacing.xs,
borderRadius: theme.borderRadius.sm,
},
deleteText: {
fontSize: 24,
color: theme.colors.error,
fontWeight: 'bold',
},
});
export default TodoItem;2.4 输入表单样式
js
// src/components/TodoForm.js
import React, { useState } from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { theme } from '../theme';
const TodoForm = ({ onAddTodo }) => {
const [text, setText] = useState('');
const handleSubmit = () => {
if (text.trim()) {
onAddTodo(text);
setText('');
}
};
return (
<View style={styles.form}>
<TextInput
style={styles.input}
placeholder="添加新的待办事项..."
placeholderTextColor={theme.colors.textSecondary}
value={text}
onChangeText={setText}
returnKeyType="done"
onSubmitEditing={handleSubmit}
/>
<TouchableOpacity
style={styles.button}
onPress={handleSubmit}
activeOpacity={0.8}
>
<Text style={styles.buttonText}>+</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
form: {
flexDirection: 'row',
marginBottom: theme.spacing.lg,
},
input: {
flex: 1,
backgroundColor: theme.colors.surface,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.md,
marginRight: theme.spacing.sm,
fontSize: 16,
color: theme.colors.text,
...theme.shadows.sm,
},
button: {
width: 50,
height: 50,
borderRadius: theme.borderRadius.md,
backgroundColor: theme.colors.primary,
justifyContent: 'center',
alignItems: 'center',
...theme.shadows.md,
},
buttonText: {
fontSize: 24,
color: theme.colors.surface,
fontWeight: 'bold',
},
});
export default TodoForm;3. 空状态和加载状态
3.1 空状态组件
js
// src/components/EmptyState.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { theme } from '../theme';
const EmptyState = ({ message }) => {
return (
<View style={styles.container}>
<Text style={styles.icon}>📝</Text>
<Text style={styles.message}>{message}</Text>
<Text style={styles.subMessage}>点击上方添加你的第一个待办事项</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: theme.spacing.xl,
},
icon: {
fontSize: 48,
marginBottom: theme.spacing.md,
},
message: {
fontSize: 18,
fontWeight: '600',
color: theme.colors.text,
marginBottom: theme.spacing.sm,
textAlign: 'center',
},
subMessage: {
fontSize: 14,
color: theme.colors.textSecondary,
textAlign: 'center',
},
});
export default EmptyState;3.2 加载状态组件
js
// src/components/LoadingState.js
import React from 'react';
import { View, ActivityIndicator, StyleSheet } from 'react-native';
import { theme } from '../theme';
const LoadingState = () => {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
export default LoadingState;4. 动画效果
为了提升用户体验,我们可以添加一些简单的动画效果:
js
// src/components/AnimatedTodoItem.js
import React, { useRef } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Animated } from 'react-native';
import { theme } from '../theme';
const AnimatedTodoItem = ({ todo, onToggle, onDelete }) => {
const scaleAnim = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scaleAnim, {
toValue: 0.95,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scaleAnim, {
toValue: 1,
friction: 3,
tension: 40,
useNativeDriver: true,
}).start();
};
return (
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
<TouchableOpacity
style={[styles.item, todo.completed && styles.completedItem]}
onPress={() => onToggle(todo.id)}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
>
<View style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}>
{todo.completed && <Text style={styles.checkmark}>✓</Text>}
</View>
<Text
style={[styles.text, todo.completed && styles.completedText]}
numberOfLines={1}
>
{todo.text}
</Text>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => onDelete(todo.id)}
>
<Text style={styles.deleteText}>×</Text>
</TouchableOpacity>
</TouchableOpacity>
</Animated.View>
);
};
const styles = StyleSheet.create({
// 与 TodoItem 相同的样式
item: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.surface,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.md,
marginBottom: theme.spacing.sm,
...theme.shadows.sm,
},
completedItem: {
opacity: 0.7,
},
checkbox: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: theme.colors.primary,
marginRight: theme.spacing.sm,
justifyContent: 'center',
alignItems: 'center',
},
checkboxCompleted: {
backgroundColor: theme.colors.primary,
},
checkmark: {
color: theme.colors.surface,
fontWeight: 'bold',
},
text: {
flex: 1,
fontSize: 16,
color: theme.colors.text,
},
completedText: {
textDecorationLine: 'line-through',
color: theme.colors.textSecondary,
},
deleteButton: {
padding: theme.spacing.xs,
borderRadius: theme.borderRadius.sm,
},
deleteText: {
fontSize: 24,
color: theme.colors.error,
fontWeight: 'bold',
},
});
export default AnimatedTodoItem;5. 主应用组件整合
现在,让我们更新主应用组件,整合所有美化后的组件:
js
// App.js
import React, { useState, useEffect } from 'react';
import { View, FlatList, StyleSheet, Alert } from 'react-native';
import TodoContainer from './src/components/TodoContainer';
import TodoHeader from './src/components/TodoHeader';
import TodoForm from './src/components/TodoForm';
import AnimatedTodoItem from './src/components/AnimatedTodoItem';
import EmptyState from './src/components/EmptyState';
import LoadingState from './src/components/LoadingState';
import { todoStorage } from './src/utils/storage';
import { theme } from './src/theme';
export default function App() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
// 加载存储的待办事项
useEffect(() => {
const loadTodos = async () => {
try {
const storedTodos = await todoStorage.getTodos();
setTodos(storedTodos);
} catch (error) {
console.error('加载待办事项失败:', error);
Alert.alert('错误', '加载待办事项失败');
} finally {
setLoading(false);
}
};
loadTodos();
}, []);
// 保存待办事项到存储
useEffect(() => {
if (!loading) {
todoStorage.saveTodos(todos).catch(error => {
console.error('保存待办事项失败:', error);
});
}
}, [todos, loading]);
// 添加待办事项
const handleAddTodo = (text) => {
const newTodo = {
id: Date.now().toString(),
text,
completed: false,
createdAt: new Date().toISOString(),
};
setTodos(prevTodos => [newTodo, ...prevTodos]);
};
// 切换待办事项状态
const handleToggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// 删除待办事项
const handleDeleteTodo = (id) => {
Alert.alert(
'确认删除',
'确定要删除这个待办事项吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '删除',
style: 'destructive',
onPress: () => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
},
},
],
{ cancelable: true }
);
};
// 渲染待办事项
const renderTodo = ({ item }) => (
<AnimatedTodoItem
todo={item}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
/>
);
// 渲染列表头部
const ListHeaderComponent = () => (
<View>
<TodoHeader
title="我的待办清单"
subtitle={`共 ${todos.length} 项,已完成 ${todos.filter(t => t.completed).length} 项`}
/>
<TodoForm onAddTodo={handleAddTodo} />
</View>
);
if (loading) {
return <LoadingState />;
}
return (
<TodoContainer>
<FlatList
data={todos}
renderItem={renderTodo}
keyExtractor={item => item.id}
ListHeaderComponent={ListHeaderComponent}
ListEmptyComponent={
<EmptyState message="还没有待办事项" />
}
contentContainerStyle={todos.length === 0 ? styles.emptyContainer : styles.listContainer}
showsVerticalScrollIndicator={false}
/>
</TodoContainer>
);
}
const styles = StyleSheet.create({
listContainer: {
paddingBottom: theme.spacing.lg,
},
emptyContainer: {
flexGrow: 1,
},
});6. 响应式设计
为了确保应用在不同尺寸的设备上都能良好显示,我们可以添加响应式设计:
js
// src/utils/responsive.js
import { Dimensions, Platform, StatusBar } from 'react-native';
const { width, height } = Dimensions.get('window');
export const isSmallDevice = width < 375;
export const spacing = {
xs: isSmallDevice ? 4 : 4,
sm: isSmallDevice ? 8 : 8,
md: isSmallDevice ? 12 : 16,
lg: isSmallDevice ? 16 : 24,
xl: isSmallDevice ? 20 : 32,
};
export const fontSize = {
xs: isSmallDevice ? 12 : 12,
sm: isSmallDevice ? 14 : 14,
md: isSmallDevice ? 16 : 16,
lg: isSmallDevice ? 20 : 24,
xl: isSmallDevice ? 24 : 28,
};
export const getStatusBarHeight = () => {
return Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 0;
};7. 主题切换功能
为了提供更好的用户体验,我们可以添加深色模式支持:
js
// src/contexts/ThemeContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import { useColorScheme } from 'react-native';
const ThemeContext = createContext();
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
export const ThemeProvider = ({ children }) => {
const systemColorScheme = useColorScheme();
const [isDarkMode, setIsDarkMode] = useState(systemColorScheme === 'dark');
const toggleTheme = () => {
setIsDarkMode(!isDarkMode);
};
const theme = {
colors: {
primary: '#4F46E5',
secondary: '#10B981',
background: isDarkMode ? '#121212' : '#F3F4F6',
surface: isDarkMode ? '#1E1E1E' : '#FFFFFF',
text: isDarkMode ? '#FFFFFF' : '#1F2937',
textSecondary: isDarkMode ? '#A0AEC0' : '#6B7280',
border: isDarkMode ? '#2D3748' : '#E5E7EB',
error: '#EF4444',
success: '#10B981',
warning: '#F59E0B',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
borderRadius: {
sm: 4,
md: 8,
lg: 12,
xl: 16,
},
shadows: {
sm: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
md: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
},
},
};
return (
<ThemeContext.Provider value={{ theme, isDarkMode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};8. 最终效果
现在,我们的待办清单应用已经完成了样式美化,具有以下特点:
- 现代美观的界面:使用统一的色彩方案和布局
- 流畅的动画效果:添加了按压和过渡动画
- 完善的状态管理:包含空状态和加载状态
- 响应式设计:适配不同尺寸的设备
- 主题切换:支持浅色和深色模式
9. 总结
通过本章节的学习,我们掌握了如何使用 React Native 的样式系统创建美观、现代的用户界面。我们学习了:
- 如何定义和使用主题系统
- 如何优化组件样式
- 如何添加动画效果
- 如何实现响应式设计
- 如何添加主题切换功能
这些技能将帮助我们创建更加专业和用户友好的 React Native 应用。
10. 下一步
现在,我们已经完成了待办清单应用的开发,包括:
- 项目结构搭建
- 增删改查功能
- 本地数据存储
- 样式美化
接下来,我们可以继续学习其他实战项目,如新闻/资讯APP和个人主页/小工具APP,进一步提升我们的 React Native 开发技能。
