feat: 完成顶部导航组件开发和系统UI适配
- 创建可复用的AppHeader组件,集成搜索栏和导入按钮 - 实现SafeArea系统UI避让,解决状态栏重叠问题 - 优化代码注释,采用简洁专业的文档风格 - 完善Flutter布局系统学习文档,涵盖Container、Row、InkWell等组件 - 更新项目状态和开发进度文档 技术要点: - Material Design主题系统集成 - 条件渲染和组件参数化设计 - InkWell水波纹点击效果 - Flutter代码质量优化(零警告) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8b5fdaa36d
commit
f62a43823c
380
learning_docs/09_Flutter布局系统详解.md
Normal file
380
learning_docs/09_Flutter布局系统详解.md
Normal file
@ -0,0 +1,380 @@
|
||||
# Flutter布局系统详解
|
||||
|
||||
## 📚 学习目标
|
||||
|
||||
深入理解Flutter的布局系统,掌握常用的布局组件和设计原则,能够创建响应式的、可复用的UI组件。
|
||||
|
||||
## 🎯 核心概念
|
||||
|
||||
### 1. Widget树结构
|
||||
Flutter应用由一棵Widget树构成,每个Widget都有父Widget和子Widget的关系:
|
||||
|
||||
```
|
||||
MaterialApp (根节点)
|
||||
└── Scaffold (页面骨架)
|
||||
└── AppHeader (自定义组件)
|
||||
├── Container (搜索栏容器)
|
||||
│ ├── Material (Material容器)
|
||||
│ └── InkWell (点击响应)
|
||||
└── Container (按钮容器)
|
||||
├── Material (Material容器)
|
||||
└── InkWell (点击响应)
|
||||
```
|
||||
|
||||
## 📦 基础布局组件详解
|
||||
|
||||
### Container - 万能容器
|
||||
Container是最常用的布局组件,可以设置尺寸、内边距、装饰等。
|
||||
|
||||
```dart
|
||||
Container({
|
||||
height: 60, // 固定高度
|
||||
width: 100, // 固定宽度
|
||||
padding: EdgeInsets.all(16), // 内边距
|
||||
margin: EdgeInsets.symmetric( // 外边距
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration( // 装饰属性
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey),
|
||||
),
|
||||
child: Text('内容'), // 子组件
|
||||
})
|
||||
```
|
||||
|
||||
**关键知识点:**
|
||||
- **尺寸控制**:width、height控制容器大小
|
||||
- **间距管理**:padding(内边距)、margin(外边距)
|
||||
- **装饰属性**:通过BoxDecoration设置背景色、边框、圆角等
|
||||
- **布局影响**:Container会尽可能小,除非有父容器约束
|
||||
|
||||
### Row - 水平布局
|
||||
Row将其子组件沿水平轴排列。
|
||||
|
||||
```dart
|
||||
Row(
|
||||
children: [
|
||||
Text('左侧'), // 固定宽度
|
||||
Expanded( // 占据剩余空间
|
||||
child: Text('中间'),
|
||||
),
|
||||
Container(width: 50), // 固定宽度
|
||||
Text('右侧'), // 固定宽度
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
**关键知识点:**
|
||||
- **Expanded**:占据剩余可用空间
|
||||
- **Flexible**:按比例分配空间
|
||||
- **MainAxisAlignment**:主轴(水平)对齐方式
|
||||
- **CrossAxisAlignment**:交叉轴(垂直)对齐方式
|
||||
|
||||
### Column - 垂直布局
|
||||
Column将其子组件沿垂直轴排列,与Row功能相同但方向相反。
|
||||
|
||||
```dart
|
||||
Column(
|
||||
children: [
|
||||
Text('顶部'), // 固定高度
|
||||
Expanded( // 占据剩余垂直空间
|
||||
child: Text('中间内容'),
|
||||
),
|
||||
Container(height: 50), // 固定高度
|
||||
Text('底部'), // 固定高度
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### SizedBox - 空白间距
|
||||
用于创建固定大小的空白区域,常用于组件间距。
|
||||
|
||||
```dart
|
||||
SizedBox(width: 16), // 水平间距
|
||||
SizedBox(height: 8), // 垂直间距
|
||||
SizedBox.square(20), // 正方形空白
|
||||
```
|
||||
|
||||
## 🎨 Material Design组件
|
||||
|
||||
### Material - Material容器
|
||||
提供Material Design的基础功能,如墨水纹效果、颜色系统支持。
|
||||
|
||||
```dart
|
||||
Material(
|
||||
color: Colors.blue, // 背景色
|
||||
child: InkWell( // 点击响应
|
||||
onTap: () {},
|
||||
child: Text('按钮'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### InkWell - 点击响应
|
||||
提供水波纹点击效果,是Material Design的推荐交互组件。
|
||||
|
||||
```dart
|
||||
InkWell(
|
||||
onTap: () { // 点击事件
|
||||
print('被点击了');
|
||||
},
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 50,
|
||||
color: Colors.red,
|
||||
child: Text('可点击区域'),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Text - 文本显示
|
||||
用于显示文本,支持丰富的样式定制。
|
||||
|
||||
```dart
|
||||
Text(
|
||||
'Hello World',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## 🎯 AppHeader组件布局分析
|
||||
|
||||
### 整体布局结构
|
||||
|
||||
```dart
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
// 1. 固定高度的顶部导航
|
||||
AppHeader(...),
|
||||
|
||||
// 2. 占据剩余空间的内容区域
|
||||
Expanded(
|
||||
child: 页面内容,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
### AppHeader内部布局
|
||||
|
||||
```dart
|
||||
return Container(
|
||||
height: 60, // 固定高度
|
||||
child: Row(
|
||||
children: [
|
||||
// 1. 可选标题(条件渲染)
|
||||
if (title != null) ...[
|
||||
Text(title),
|
||||
SizedBox(width: 16),
|
||||
],
|
||||
|
||||
// 2. 搜索栏(占据主要空间)
|
||||
if (showSearchBar) ...[
|
||||
Expanded(child: 搜索栏),
|
||||
SizedBox(width: 12),
|
||||
],
|
||||
|
||||
// 3. 导入按钮(固定大小)
|
||||
导入按钮,
|
||||
],
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
**布局设计要点:**
|
||||
1. **Row水平布局**:标题、搜索栏、按钮水平排列
|
||||
2. **Expanded搜索栏**:使用Expanded让搜索栏自适应宽度
|
||||
3. **条件渲染**:通过`if (condition) ...[...]`控制组件显示
|
||||
4. **固定间距**:使用SizedBox创建统一的组件间距
|
||||
|
||||
## 🎨 装饰与样式
|
||||
|
||||
### BoxDecoration详解
|
||||
BoxDecoration提供了丰富的装饰选项:
|
||||
|
||||
```dart
|
||||
BoxDecoration(
|
||||
// 背景色
|
||||
color: Colors.white,
|
||||
|
||||
// 渐变色
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue, Colors.purple],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
|
||||
// 边框
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey),
|
||||
bottom: BorderSide(color: Colors.grey),
|
||||
),
|
||||
|
||||
// 圆角
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
||||
// 阴影
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
|
||||
// 背景图片
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/image.png'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Theme.of(context) 主题系统
|
||||
|
||||
Flutter提供了完整的主题系统,支持亮色和暗色模式:
|
||||
|
||||
```dart
|
||||
// 获取当前主题配置
|
||||
ThemeData theme = Theme.of(context);
|
||||
|
||||
// 使用主题颜色
|
||||
Color primaryColor = theme.colorScheme.primary;
|
||||
Color surfaceColor = theme.colorScheme.surface;
|
||||
|
||||
// 使用文本样式
|
||||
TextStyle headlineStyle = theme.textTheme.headlineLarge;
|
||||
TextStyle bodyStyle = theme.textTheme.bodyLarge;
|
||||
|
||||
// 获取主题图标配色
|
||||
Color iconColor = theme.iconTheme.color;
|
||||
```
|
||||
|
||||
## 🎓 响应式设计原则
|
||||
|
||||
### 尺寸适配
|
||||
```dart
|
||||
// 使用MediaQuery获取屏幕信息
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
// 根据屏幕尺寸调整布局
|
||||
double padding = screenWidth > 600 ? 24.0 : 16.0;
|
||||
|
||||
// 使用LayoutBuilder根据父容器约束调整
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > 600) {
|
||||
return Row(...); // 平板布局
|
||||
} else {
|
||||
return Column(...); // 手机布局
|
||||
}
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Flex布局
|
||||
```dart
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 2, // 占据2份空间
|
||||
child: Container(color: Colors.red),
|
||||
),
|
||||
Flexible(
|
||||
flex: 1, // 占据1份空间
|
||||
child: Container(color: Colors.blue),
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 1. 组件化设计
|
||||
- 将重复的UI抽取为可复用组件
|
||||
- 通过参数配置组件的行为
|
||||
- 保持组件的单一职责原则
|
||||
|
||||
### 2. 布局优化
|
||||
- 合理使用Expanded和Flexible
|
||||
- 避免不必要的嵌套
|
||||
- 使用约束而不是硬编码尺寸
|
||||
|
||||
### 3. 主题适配
|
||||
- 始终使用Theme.of(context)获取主题配置
|
||||
- 避免硬编码颜色值
|
||||
- 支持亮色/暗色模式自动切换
|
||||
|
||||
### 4. 交互设计
|
||||
- 使用InkWell提供Material Design交互
|
||||
- 设置合适的最小触摸目标(44x44dp)
|
||||
- 提供视觉反馈(颜色变化、动画等)
|
||||
|
||||
## 🔧 常见问题解决
|
||||
|
||||
### 问题1:Container大小不符合预期
|
||||
```dart
|
||||
// 错误:Container没有约束时会尽可能小
|
||||
Container(child: Text('文本')) // 可能显示不完整
|
||||
|
||||
// 正确:给Container设置明确的约束或让父容器约束
|
||||
Container(
|
||||
width: 200,
|
||||
height: 100,
|
||||
child: Text('文本'),
|
||||
)
|
||||
```
|
||||
|
||||
### 问题2:Row/Column中的布局异常
|
||||
```dart
|
||||
// 错误:Row中的子组件过大导致溢出
|
||||
Row(children: [Container(width: 500)]) // 可能溢出屏幕
|
||||
|
||||
// 正确:使用Expanded或Flexible自适应空间
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Container()), // 自适应宽度
|
||||
Container(width: 100), // 固定宽度
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### 问题3:文本样式不生效
|
||||
```dart
|
||||
// 错误:在父Widget上设置文本样式不会被子组件继承
|
||||
Container(
|
||||
child: Text('文本'),
|
||||
style: TextStyle(fontSize: 16), // ❌ 这不会生效
|
||||
)
|
||||
|
||||
// 正确:直接在Text组件上设置样式
|
||||
Container(
|
||||
child: Text(
|
||||
'文本',
|
||||
style: TextStyle(fontSize: 16), // ✅ 这会生效
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**学习要点总结:**
|
||||
1. **理解Widget树结构**:掌握父子组件关系和约束传递
|
||||
2. **掌握基础布局组件**:Container、Row、Column、SizedBox
|
||||
3. **学会响应式设计**:使用Expanded、Flexible、MediaQuery
|
||||
4. **遵循Material Design**:使用Material、InkWell、Theme
|
||||
5. **组件化思维**:创建可复用的UI组件
|
||||
|
||||
---
|
||||
|
||||
*文档创建时间:2025年1月*
|
||||
*Flutter版本:>=3.0.0*
|
||||
156
lib/components/app_header.dart
Normal file
156
lib/components/app_header.dart
Normal file
@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 应用顶部导航组件
|
||||
///
|
||||
/// 可复用的顶部导航栏组件,集成在搜索栏内的导入按钮。
|
||||
/// 提供统一的搜索和文件导入功能,支持主题自适应。
|
||||
class AppHeader extends StatelessWidget {
|
||||
/// 标题文本(可选),某些页面可以显示标题而非搜索栏
|
||||
final String? title;
|
||||
|
||||
/// 搜索按钮点击回调函数
|
||||
final VoidCallback? onSearchPressed;
|
||||
|
||||
/// 导入按钮点击回调函数
|
||||
final VoidCallback? onImportPressed;
|
||||
|
||||
/// 是否显示搜索栏(默认值为true)
|
||||
final bool showSearchBar;
|
||||
|
||||
/// 搜索栏占位符文本
|
||||
final String searchHint;
|
||||
|
||||
const AppHeader({
|
||||
super.key,
|
||||
this.title,
|
||||
this.onSearchPressed,
|
||||
this.onImportPressed,
|
||||
this.showSearchBar = true,
|
||||
this.searchHint = '搜索书名或内容...',
|
||||
});
|
||||
|
||||
/// 构建组件的UI结构
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 标题区域(条件渲染)
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
|
||||
// 搜索栏区域(占据剩余空间)
|
||||
if (showSearchBar) ...[
|
||||
Expanded(
|
||||
child: _buildSearchBar(context),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建搜索栏组件,包含搜索图标、占位符文本和导入按钮
|
||||
Widget _buildSearchBar(BuildContext context) {
|
||||
return Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: onSearchPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 24,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
searchHint,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant
|
||||
.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildImportButton(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建导入按钮组件
|
||||
Widget _buildImportButton(BuildContext context) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onImportPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'导入',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:hive_flutter/hive_flutter.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:hive_flutter/hive_flutter.dart';
|
||||
|
||||
// 数据层服务
|
||||
import 'services/database_service.dart';
|
||||
@ -247,7 +247,7 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: "Readful",
|
||||
title: 'Readful',
|
||||
theme: ThemeData(primarySwatch: Colors.blue),
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('readful 电子书阅读器')),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../components/app_header.dart';
|
||||
|
||||
/// 首页页面
|
||||
///
|
||||
@ -14,11 +15,33 @@ class HomePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Text(
|
||||
'首页',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 顶部导航组件
|
||||
SafeArea(
|
||||
bottom: false, // 只处理顶部安全区域
|
||||
child: AppHeader(
|
||||
onSearchPressed: () {
|
||||
// TODO: 实现搜索功能
|
||||
print('搜索按钮被点击');
|
||||
},
|
||||
onImportPressed: () {
|
||||
// TODO: 实现导入功能
|
||||
print('导入按钮被点击');
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 页面内容区域
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'首页内容区域',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../components/app_header.dart';
|
||||
|
||||
/// 书库页面
|
||||
///
|
||||
/// 用户管理和浏览所有导入的电子书
|
||||
/// 功能规划:
|
||||
/// - 书籍列表展示
|
||||
/// - 分类筛选功能
|
||||
/// - 排序选项
|
||||
/// - 批量操作功能
|
||||
class LibraryPage extends StatelessWidget {
|
||||
const LibraryPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Text('书库', style: Theme.of(context).textTheme.headlineMedium),
|
||||
body: Column(
|
||||
children: [
|
||||
// 顶部导航组件
|
||||
SafeArea(
|
||||
bottom: false,
|
||||
child: AppHeader(
|
||||
title: '书库',
|
||||
showSearchBar: true,
|
||||
onSearchPressed: () {
|
||||
print('书库搜索按钮被点击');
|
||||
},
|
||||
onImportPressed: () {
|
||||
print('书库导入按钮被点击');
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 书籍列表内容区域
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'书库列表区域',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user