119 lines
4.2 KiB
Markdown
119 lines
4.2 KiB
Markdown
# 【Demo】响应式布局:一套代码适配手机 / Pad
|
||
|
||
> 源文件:`lib/pages/demo/responsive_demo.dart` | 分类:基础布局
|
||
> 一句话:屏幕适配 ≠ 响应式布局 —— 一个是"等比缩放",一个是"改变结构"。
|
||
|
||
---
|
||
|
||
## 一、这个 Demo 解决什么问题
|
||
|
||
同一个页面,手机单列列表、Pad 双列卡片墙,**一套代码自动切换**。
|
||
新手最容易混淆的概念在这里讲清楚。
|
||
|
||
### 两个维度(务必分清)
|
||
| 概念 | 手段 | 解决什么 |
|
||
|---|---|---|
|
||
| **屏幕适配** | flutter_screenutil(`.w/.h/.sp`) | 不同屏幕比例不变形(尺寸等比缩放) |
|
||
| **响应式布局** | 断点 + LayoutBuilder(本 Demo) | 不同屏幕宽度改变布局**结构**(单列/双列) |
|
||
|
||
> 两者配合使用:尺寸用 `.w/.h/.sp` 缩放,结构用断点切换。
|
||
|
||
---
|
||
|
||
## 二、怎么运行 / 演示内容
|
||
|
||
```dart
|
||
runApp(const MaterialApp(home: ResponsiveDemoPage()));
|
||
```
|
||
|
||
- 顶部模式条实时显示"手机模式 / Pad 模式 + 当前宽度 dp";
|
||
- 手机(<600dp)→ 单列 ListTile 列表;
|
||
- Pad(≥600dp)→ 多列卡片网格(列数按宽度自适应)。
|
||
|
||
---
|
||
|
||
## 三、核心知识点(新手向)
|
||
|
||
### 1. 断点标准(Material 官方)
|
||
| 宽度 | 设备 |
|
||
|---|---|
|
||
| < 600dp | 手机 |
|
||
| 600 ~ 839dp | 小平板 / 折叠屏 |
|
||
| >= 840dp | 大平板 / 桌面 |
|
||
|
||
> ⚠ 关键:判断的是**屏幕宽度**,不是 Platform!
|
||
> iOS 也有 iPad,Android 也有平板 —— 用平台判断永远分不清它们。
|
||
|
||
### 2. 两种拿宽度的方式
|
||
```dart
|
||
MediaQuery.sizeOf(context).width // 整个屏幕宽度
|
||
LayoutBuilder(...) // 父容器实际给你的可用宽度
|
||
```
|
||
- 页面铺满屏幕时两者一样;
|
||
- 页面内有**可伸缩区域**(侧栏收起/展开)时必须用 `LayoutBuilder`;
|
||
- 本 Demo 用 `LayoutBuilder`,因为能拿到最真实的可用宽度。
|
||
|
||
### 3. 核心切换逻辑
|
||
```dart
|
||
LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final width = constraints.maxWidth;
|
||
final isTablet = width >= _tabletBreakpoint; // 600dp
|
||
// 列数自适应:每列至少 280dp 宽,最多 4 列
|
||
final columns = (width / _minCardWidth).floor().clamp(1, 4);
|
||
|
||
return isTablet
|
||
? _buildGrid(columns) // Pad:多列网格
|
||
: _buildList(); // 手机:单列列表
|
||
},
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 四、关键代码剖析
|
||
|
||
```dart
|
||
// 手机列表项
|
||
ListView.separated(
|
||
padding: EdgeInsets.all(12.w),
|
||
itemCount: _items.length,
|
||
separatorBuilder: (_, _) => SizedBox(height: 8.h),
|
||
itemBuilder: (context, i) => _ListTileItem(item: _items[i]),
|
||
)
|
||
|
||
// Pad 卡片网格(同一个数据源 _items,只是换了一种展示容器)
|
||
GridView.builder(
|
||
padding: EdgeInsets.all(12.w),
|
||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: columns, // 由宽度算出来的列数
|
||
mainAxisSpacing: 12.w,
|
||
crossAxisSpacing: 12.w,
|
||
childAspectRatio: 1.4, // 宽高比,数字大了卡片变矮
|
||
),
|
||
itemCount: _items.length,
|
||
itemBuilder: (context, i) => _CardItem(item: _items[i]),
|
||
)
|
||
```
|
||
|
||
**关键思想:数据不变,变的是"容器"** —— 同一份 `_items`,手机渲染成列表项,Pad 渲染成卡片。
|
||
组件只负责"怎么显示一条数据",页面负责"按宽度选容器"。
|
||
|
||
---
|
||
|
||
## 五、踩坑记录
|
||
|
||
1. **别用 `Platform.isAndroid` 判断手机/Pad** —— 永远用宽度断点;
|
||
2. `MediaQuery.sizeOf(context).width` 拿的是全屏宽,页面嵌在可伸缩容器里会算错 —— 用 LayoutBuilder;
|
||
3. `(width / 280).floor()` 在窄屏可能算出 0 列 —— 必须 `.clamp(1, 4)` 兜底;
|
||
4. 断点值用常量集中管理(`_tabletBreakpoint = 600`),别散落在各页面写死魔法数字。
|
||
|
||
---
|
||
|
||
## 六、扩展思考
|
||
|
||
- **官方推荐**:`flutter/foundation` 的 `defaultTargetPlatform` + 宽度断点结合;或引入 `responsive_builder` 包;
|
||
- 平板横竖屏切换:竖屏 8.4" ≈ 800dp,横屏 1280dp —— 断点要覆盖两种情况;
|
||
- 进阶模式:`LayoutBuilder` + `CustomScrollView`([sliver_demo.md](sliver_demo.md))组合做"响应式长列表";
|
||
- 本项目 theme_demo 的 `PageTheme` 按页面层级给配色,响应式按宽度给结构 —— 两个维度正交,可自由组合。
|