
文章目录
-
- [🤔 第一节:为什么需要第三方适配插件?](#🤔 第一节:为什么需要第三方适配插件?)
-
- [📱 **原生响应式的挑战**](#📱 原生响应式的挑战)
-
- [😰 **实际开发中的痛点**](#😰 实际开发中的痛点)
- [💡 **第三方插件的价值**](#💡 第三方插件的价值)
- [🛠️ 第二节:主流适配插件对比](#🛠️ 第二节:主流适配插件对比)
-
- [📊 **三大适配方案详解**](#📊 三大适配方案详解)
-
- [1. **flutter_screenutil - 设计稿还原专家**](#1. flutter_screenutil - 设计稿还原专家)
- [2. **responsive_framework - 响应式布局框架**](#2. responsive_framework - 响应式布局框架)
- [3. **flutter_adaptive_scaffold - 官方自适应方案**](#3. flutter_adaptive_scaffold - 官方自适应方案)
- [📊 **方案对比与选择指南**](#📊 方案对比与选择指南)
- [🎯 **如何选择适配方案?**](#🎯 如何选择适配方案?)
- [💡 **混合使用策略**](#💡 混合使用策略)
- [📱 第三节:flutter_screenutil 实战详解](#📱 第三节:flutter_screenutil 实战详解)
-
- [🚀 **快速上手**](#🚀 快速上手)
-
- [1. **安装配置**](#1. 安装配置)
- [2. **初始化设置**](#2. 初始化设置)
- [🎨 **核心API使用**](#🎨 核心API使用)
- [💡 **常用适配技巧**](#💡 常用适配技巧)
- [⚠️ **注意事项**](#⚠️ 注意事项)
- [📱 第四节:折叠屏适配策略](#📱 第四节:折叠屏适配策略)
-
- [🔄 **折叠屏的挑战**](#🔄 折叠屏的挑战)
- [🎯 **折叠屏设备类型**](#🎯 折叠屏设备类型)
- [🛠️ **折叠屏适配实现**](#🛠️ 折叠屏适配实现)
-
- [1. **折叠屏检测原理**](#1. 折叠屏检测原理)
- [2. **自适应布局组件设计**](#2. 自适应布局组件设计)
- [3. **屏幕切换监听机制**](#3. 屏幕切换监听机制)
- [🎨 **布局切换动画优化**](#🎨 布局切换动画优化)
- [🛠️ 第五节:实用工具扩展](#🛠️ 第五节:实用工具扩展)
-
- [📏 **自定义屏幕工具类**](#📏 自定义屏幕工具类)
- [🎨 **响应式样式系统**](#🎨 响应式样式系统)
- [💡 第六节:开发技巧与最佳实践](#💡 第六节:开发技巧与最佳实践)
-
- [✅ **推荐的开发技巧**](#✅ 推荐的开发技巧)
-
- [1. **🎯 创建统一的适配工具类**](#1. 🎯 创建统一的适配工具类)
- [2. **🧪 开发时的调试工具**](#2. 🧪 开发时的调试工具)
- [3. **📱 设备预览工具**](#3. 📱 设备预览工具)
- [4. **💾 布局偏好持久化**](#4. 💾 布局偏好持久化)
- [❌ **需要避免的常见错误**](#❌ 需要避免的常见错误)
- [🎯 **性能优化建议**](#🎯 性能优化建议)
-
- [1. **⚡ 避免频繁重建**](#1. ⚡ 避免频繁重建)
- [2. **🎨 使用const构造函数**](#2. 🎨 使用const构造函数)
- [3. **📊 监控性能**](#3. 📊 监控性能)
- [🎉 第七节:总结](#🎉 第七节:总结)
-
- [📚 **知识点总结**](#📚 知识点总结)
-
- [🎯 **核心知识**](#🎯 核心知识)
- [🛠️ **实战技能**](#🛠️ 实战技能)
- [🎯 **方案选择速查表**](#🎯 方案选择速查表)
- [📖 **推荐学习资源**](#📖 推荐学习资源)
- [🎉 **恭喜完成学习!**](#🎉 恭喜完成学习!)
🤔 第一节:为什么需要第三方适配插件?
📱 原生响应式的挑战
在上一章中,我们学习了使用MediaQuery和LayoutBuilder实现响应式设计。但在实际开发中,你可能会遇到这些问题:
😰 实际开发中的痛点
-
🎨 设计稿还原困难
dart// ❌ 设计师给的是375x812的设计稿,但你的代码要这样写: Container( width: MediaQuery.of(context).size.width * 0.8, // 这是多少像素? height: 200, // 在不同设备上看起来大小不一样 child: Text( '标题', style: TextStyle(fontSize: 16), // 在大屏上显得太小 ), )问题:设计稿上标注的是300px宽度,你却要手动计算比例
-
📐 重复的适配代码
dart// ❌ 每个页面都要写这样的代码 final screenWidth = MediaQuery.of(context).size.width; final adaptiveWidth = screenWidth < 600 ? 100.0 : 150.0; final adaptiveFontSize = screenWidth < 600 ? 14.0 : 18.0; final adaptivePadding = screenWidth < 600 ? 16.0 : 24.0;问题:代码重复,维护困难,容易出错
-
🔢 字体大小不统一
dart// ❌ 不同开发者写的字体大小不一致 Text('标题', style: TextStyle(fontSize: 18)) // 开发者A Text('标题', style: TextStyle(fontSize: 20)) // 开发者B Text('标题', style: TextStyle(fontSize: 16)) // 开发者C问题:团队协作时,UI不统一
-
📱 极端设备适配复杂
dart// ❌ 要考虑各种极端情况 if (width < 320) { // 超小屏幕 } else if (width < 375) { // 小屏幕 } else if (width < 414) { // 中等屏幕 } else if (width < 768) { // 大屏幕 } else { // 平板 }问题:判断逻辑复杂,容易遗漏
💡 第三方插件的价值
为了解决这些痛点,社区开发了各种适配插件:
dart
// ✅ 使用flutter_screenutil后的代码
Container(
width: 300.w, // 直接使用设计稿标注的300px
height: 200.h, // 直接使用设计稿标注的200px
child: Text(
'标题',
style: TextStyle(fontSize: 16.sp), // 自动适配字体大小
),
)
优势:
- 🎯 直接使用设计稿尺寸:300px就写300.w,不用计算
- 🔄 自动等比缩放:在不同设备上自动适配
- 📝 代码简洁:减少重复代码
- 👥 团队统一:统一的适配标准
🛠️ 第二节:主流适配插件对比
📊 三大适配方案详解
1. flutter_screenutil - 设计稿还原专家
核心理念:以设计稿为基准,等比缩放
适用场景:
- ✅ 需要高度还原UI设计稿
- ✅ 团队有专业UI设计师
- ✅ 主要面向手机端应用
工作原理:
dart
// 🎯 设置设计稿尺寸为375x812
ScreenUtil.init(
context,
designSize: Size(375, 812), // iPhone X的设计稿尺寸
);
// 📱 在iPhone SE (320宽度)上:
300.w // 实际显示 = 300 * (320 / 375) = 256px
// 📱 在iPad (768宽度)上:
300.w // 实际显示 = 300 * (768 / 375) = 614px
优点:
- 🎨 设计稿1:1还原
- 📝 代码简洁直观
- ⚡ 性能开销小
缺点:
- ⚠️ 在平板/桌面上可能过度放大
- ⚠️ 不适合真正的响应式设计
2. responsive_framework - 响应式布局框架
核心理念:基于断点的响应式设计
适用场景:
- ✅ 需要跨平台适配(手机/平板/桌面)
- ✅ 不同设备显示不同布局
- ✅ 复杂的响应式需求
工作原理:
dart
ResponsiveWrapper.builder(
child: MyApp(),
breakpoints: [
ResponsiveBreakpoint.resize(450, name: MOBILE),
ResponsiveBreakpoint.autoScale(800, name: TABLET),
ResponsiveBreakpoint.resize(1000, name: DESKTOP),
],
)
优点:
- 🎯 真正的响应式设计
- 🔧 灵活的断点配置
- 📱 适合多平台应用
缺点:
- 📚 学习曲线较陡
- ⚙️ 配置相对复杂
3. flutter_adaptive_scaffold - 官方自适应方案
核心理念:Material Design 3的自适应规范
适用场景:
- ✅ 遵循Material Design规范
- ✅ 需要标准的导航模式
- ✅ 快速搭建应用框架
工作原理:
dart
AdaptiveScaffold(
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
],
body: (_) => MainContent(),
)
// 自动在手机上显示底部导航,平板上显示侧边栏
优点:
- ✅ Google官方支持
- 🎨 符合Material Design规范
- 🚀 开箱即用
缺点:
- 🎭 样式相对固定
- 🔒 自定义程度有限
📊 方案对比与选择指南
| 方案 | 适用场景 | 学习成本 | 灵活性 | 性能 | 推荐指数 |
|---|---|---|---|---|---|
| 原生响应式 | 复杂响应式需求 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| flutter_screenutil | 手机端UI还原 | ⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| responsive_framework | 跨平台响应式 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| adaptive_scaffold | Material应用 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
🎯 如何选择适配方案?
dart
// 🤔 决策树:根据项目特点选择方案
// 场景1:电商App,需要高度还原设计稿
// ✅ 推荐:flutter_screenutil
class EcommerceApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: Size(375, 812), // 设计稿尺寸
minTextAdapt: true, // 文字自适应
splitScreenMode: true, // 支持分屏
child: MaterialApp(
home: ProductListPage(),
),
);
}
}
// 场景2:新闻App,需要跨平台适配
// ✅ 推荐:responsive_framework
class NewsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => ResponsiveWrapper.builder(
child,
breakpoints: [
ResponsiveBreakpoint.resize(450, name: MOBILE),
ResponsiveBreakpoint.autoScale(800, name: TABLET),
ResponsiveBreakpoint.resize(1000, name: DESKTOP),
],
),
home: NewsHomePage(),
);
}
}
// 场景3:工具类App,遵循Material Design
// ✅ 推荐:adaptive_scaffold
class ToolApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AdaptiveScaffold(
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: '首页'),
NavigationDestination(icon: Icon(Icons.settings), label: '设置'),
],
body: (_) => ToolContent(),
),
);
}
}
// 场景4:复杂业务系统,需要精细控制
// ✅ 推荐:原生响应式(第一章学习的方法)
class BusinessApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: LayoutBuilder(
builder: (context, constraints) {
// 自定义响应式逻辑
return CustomResponsiveLayout(constraints: constraints);
},
),
);
}
}
💡 混合使用策略
实际项目中,可以结合多种方案:
dart
class HybridApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: Size(375, 812),
child: MaterialApp(
builder: (context, child) {
// 外层使用responsive_framework处理断点
return ResponsiveWrapper.builder(
child,
breakpoints: [
ResponsiveBreakpoint.resize(450, name: MOBILE),
ResponsiveBreakpoint.autoScale(800, name: TABLET),
],
);
},
home: LayoutBuilder(
builder: (context, constraints) {
// 内层使用原生响应式处理复杂逻辑
if (constraints.maxWidth < 600) {
return MobileLayout(); // 使用screenutil的.w .h
} else {
return TabletLayout(); // 使用原生响应式
}
},
),
),
);
}
}
📱 第三节:flutter_screenutil 实战详解
🚀 快速上手
1. 安装配置
yaml
# pubspec.yaml
dependencies:
flutter_screenutil: ^5.9.0
2. 初始化设置
dart
import 'package:flutter_screenutil/flutter_screenutil.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 🎯 方式1:使用ScreenUtilInit包裹
return ScreenUtilInit(
designSize: Size(375, 812), // 设计稿尺寸(iPhone X)
minTextAdapt: true, // 文字大小根据系统设置自适应
splitScreenMode: true, // 支持分屏模式
builder: (context, child) {
return MaterialApp(
title: 'ScreenUtil Demo',
home: HomePage(),
);
},
);
}
}
🎨 核心API使用
dart
class ScreenUtilDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ScreenUtil示例'),
),
body: Column(
children: [
// 📏 宽度适配
Container(
width: 300.w, // 设计稿宽度300px
height: 200.h, // 设计稿高度200px
color: Colors.blue,
child: Center(
child: Text(
'适配容器',
style: TextStyle(
fontSize: 16.sp, // 字体大小16sp
color: Colors.white,
),
),
),
),
SizedBox(height: 20.h), // 间距也要适配
// 🎯 使用最小边适配(推荐用于正方形元素)
Container(
width: 100.r, // 使用屏幕最小边适配
height: 100.r, // 保证在不同设备上都是正方形
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10.r), // 圆角也要适配
),
),
SizedBox(height: 20.h),
// 📱 获取屏幕信息
Text('屏幕宽度: ${ScreenUtil().screenWidth}'),
Text('屏幕高度: ${ScreenUtil().screenHeight}'),
Text('状态栏高度: ${ScreenUtil().statusBarHeight}'),
Text('底部安全区: ${ScreenUtil().bottomBarHeight}'),
Text('像素密度: ${ScreenUtil().pixelRatio}'),
],
),
);
}
}
💡 常用适配技巧
dart
// 🎯 技巧1:响应式边距
EdgeInsets.symmetric(
horizontal: 16.w, // 水平边距
vertical: 12.h, // 垂直边距
)
// 🎯 技巧2:响应式圆角
BorderRadius.circular(8.r) // 使用.r保证圆角比例
// 🎯 技巧3:响应式图标大小
Icon(Icons.home, size: 24.sp)
// 🎯 技巧4:响应式阴影
BoxShadow(
blurRadius: 10.r,
spreadRadius: 2.r,
offset: Offset(0, 2.h),
)
// 🎯 技巧5:设置最小字体
Text(
'标题',
style: TextStyle(
fontSize: 14.sp.clamp(12, 18), // 最小12,最大18
),
)
⚠️ 注意事项
dart
// ❌ 错误用法
Container(
width: 300, // 忘记使用.w
height: 200, // 忘记使用.h
padding: EdgeInsets.all(16), // 忘记适配
)
// ✅ 正确用法
Container(
width: 300.w,
height: 200.h,
padding: EdgeInsets.all(16.w),
)
// ⚠️ 特殊情况:某些值不需要适配
Container(
width: double.infinity, // 占满父容器,不需要适配
height: 1, // 1像素分割线,不需要适配
)
📱 第四节:折叠屏适配策略
🔄 折叠屏的挑战
折叠屏设备带来了新的适配挑战:
dart
// 🤔 问题场景
// 用户正在使用Galaxy Fold外屏(6.2寸)浏览商品列表
// 突然展开内屏(7.6寸),应用应该如何响应?
// 场景1:商品列表
// 外屏:单列显示
// 内屏:双列显示
// 场景2:视频播放
// 外屏:竖屏播放
// 内屏:横屏全屏播放
// 场景3:聊天界面
// 外屏:只显示聊天内容
// 内屏:左侧显示会话列表,右侧显示聊天内容
🎯 折叠屏设备类型
-
📱➡️📟 双折屏(如Galaxy Fold)
- 外屏:6.2寸手机模式
- 内屏:7.6寸平板模式
- 需要处理屏幕切换
-
📱➡️📟➡️🖥️ 三折屏(如华为Mate XT)
- 单屏:6.4寸手机模式
- 双屏:7.9寸小平板模式
- 三屏:10.2寸大平板模式
- 需要处理多种状态切换
🛠️ 折叠屏适配实现
1. 折叠屏检测原理
🤔 核心思路:
折叠屏设备的特点是屏幕尺寸会动态变化。我们需要:
- 定义状态:将设备分为手机、平板、桌面三种模式
- 检测尺寸:通过屏幕宽度判断当前处于哪种模式
- 识别设备:通过屏幕比例判断是否为折叠屏设备
📐 判断标准:
- 手机模式:宽度 < 600px(外屏或普通手机)
- 平板模式:600px ≤ 宽度 < 900px(折叠屏展开)
- 桌面模式:宽度 ≥ 900px(超大屏或桌面)
🔍 折叠屏识别:
- 普通手机屏幕比例:约 0.46(9:19.5 的窄长屏)
- 折叠屏内屏比例:约 0.85(接近正方形)
- 通过比例 + 宽度双重判断,准确识别折叠屏
💡 实现代码:
dart
/// 📱 折叠屏状态枚举
enum FoldableState {
phone, // 📱 手机模式(外屏或小屏)
tablet, // 📟 平板模式(内屏展开)
desktop, // 🖥️ 桌面模式(超大屏)
}
/// 🔍 折叠屏检测工具
class FoldableDetector {
/// 检测当前折叠状态
static FoldableState detect(BuildContext context) {
final width = MediaQuery.of(context).size.width;
// 根据宽度判断设备模式
if (width < 600) return FoldableState.phone;
if (width < 900) return FoldableState.tablet;
return FoldableState.desktop;
}
/// 判断是否为折叠屏设备
static bool isFoldable(BuildContext context) {
final size = MediaQuery.of(context).size;
final aspectRatio = size.width / size.height;
// 折叠屏特征:
// 1. 屏幕比例在 0.7-1.0 之间(接近正方形)
// 2. 宽度大于 600px(排除小屏手机)
return aspectRatio > 0.7 && aspectRatio < 1.0 && size.width > 600;
}
}
2. 自适应布局组件设计
🎯 设计思路:
创建一个通用的自适应组件,让开发者只需要提供不同屏幕的布局,组件自动根据设备状态切换。
核心特点:
- 声明式API:传入不同状态的布局Widget
- 自动检测:使用LayoutBuilder实时监听尺寸变化
- 灵活配置:桌面布局可选,默认使用平板布局
- 性能优化:只在尺寸变化时重建,避免不必要的渲染
💡 实现代码:
dart
/// 🎯 折叠屏自适应组件
class FoldableAdaptiveLayout extends StatelessWidget {
final Widget phoneLayout; // 手机布局(必需)
final Widget tabletLayout; // 平板布局(必需)
final Widget? desktopLayout; // 桌面布局(可选)
const FoldableAdaptiveLayout({
Key? key,
required this.phoneLayout,
required this.tabletLayout,
this.desktopLayout,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// 使用LayoutBuilder监听尺寸变化
return LayoutBuilder(
builder: (context, constraints) {
// 检测当前设备状态
final state = FoldableDetector.detect(context);
// 根据状态返回对应布局
switch (state) {
case FoldableState.phone:
return phoneLayout;
case FoldableState.tablet:
return tabletLayout;
case FoldableState.desktop:
return desktopLayout ?? tabletLayout; // 桌面布局可选
}
},
);
}
}
**🎯 实战示例:电商商品列表**
**场景说明:**
- 手机上:单列显示,每个商品占满宽度
- 平板上:双列显示,充分利用空间
- 桌面上:三列显示,展示更多商品
```dart
/// 📱 商品列表页面
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('商品列表')),
body: FoldableAdaptiveLayout(
// 📱 手机布局:单列显示
phoneLayout: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1, // 单列
childAspectRatio: 3, // 宽高比3:1(横向卡片)
),
itemBuilder: (context, index) => ProductCard(index),
),
// 📟 平板布局:双列显示
tabletLayout: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 双列
childAspectRatio: 1.5, // 宽高比1.5:1
),
itemBuilder: (context, index) => ProductCard(index),
),
// 🖥️ 桌面布局:三列显示
desktopLayout: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 三列
childAspectRatio: 1.2, // 宽高比1.2:1
),
itemBuilder: (context, index) => ProductCard(index),
),
),
);
}
}
/// 🎴 商品卡片组件
class ProductCard extends StatelessWidget {
final int index;
const ProductCard(this.index);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(8.w), // 使用screenutil适配边距
child: Column(
children: [
Expanded(
child: Container(
color: Colors.grey[300],
child: Icon(Icons.shopping_bag, size: 48.sp), // 适配图标大小
),
),
Padding(
padding: EdgeInsets.all(8.w),
child: Text(
'商品 ${index + 1}',
style: TextStyle(fontSize: 14.sp), // 适配字体大小
),
),
],
),
);
}
}
3. 屏幕切换监听机制
🔄 为什么需要监听?
当用户折叠或展开设备时,我们可能需要:
- 📹 暂停/恢复视频播放
- 💾 保存当前状态
- 🔄 切换全屏模式
- 📊 调整数据加载策略
核心原理:
- 使用WidgetsBindingObserver:监听系统级的屏幕变化事件
- didChangeMetrics回调:当屏幕尺寸改变时触发
- 状态对比:只在状态真正改变时才触发回调
- 生命周期管理:正确添加和移除观察者
💡 实现代码:
dart
/// 🔄 折叠状态监听器
class FoldableStateObserver extends StatefulWidget {
final Widget child;
final Function(FoldableState)? onStateChanged; // 状态变化回调
const FoldableStateObserver({
Key? key,
required this.child,
this.onStateChanged,
}) : super(key: key);
@override
_FoldableStateObserverState createState() => _FoldableStateObserverState();
}
class _FoldableStateObserverState extends State<FoldableStateObserver>
with WidgetsBindingObserver {
FoldableState? _previousState; // 记录上一次的状态
@override
void initState() {
super.initState();
// 注册观察者
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
// 移除观察者,避免内存泄漏
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
// 屏幕尺寸变化时触发
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
final currentState = FoldableDetector.detect(context);
// 只在状态真正改变时才触发回调
if (_previousState != currentState) {
_previousState = currentState;
widget.onStateChanged?.call(currentState);
print('📱 折叠状态变化: $currentState');
}
}
});
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
**🎯 实战示例:视频播放器自动全屏**
**场景说明:**
- 手机模式:普通播放模式
- 展开到平板:自动切换全屏播放
- 折叠回手机:恢复普通模式
```dart
/// 📹 视频播放器页面
class VideoPlayerPage extends StatefulWidget {
@override
_VideoPlayerPageState createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<VideoPlayerPage> {
bool _isFullScreen = false;
/// 处理折叠状态变化
void _handleFoldableStateChange(FoldableState state) {
setState(() {
// 展开到平板模式时自动全屏
_isFullScreen = state == FoldableState.tablet;
// 这里可以添加更多逻辑
if (_isFullScreen) {
print('📱 切换到全屏模式');
// 可以调用视频播放器的全屏API
} else {
print('📱 退出全屏模式');
}
});
}
@override
Widget build(BuildContext context) {
return FoldableStateObserver(
onStateChanged: _handleFoldableStateChange, // 监听状态变化
child: Scaffold(
appBar: _isFullScreen ? null : AppBar(title: Text('视频播放')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_isFullScreen ? Icons.fullscreen : Icons.fullscreen_exit,
size: 64.sp,
),
SizedBox(height: 16.h),
Text(
_isFullScreen ? '🎬 全屏播放模式' : '📱 普通播放模式',
style: TextStyle(fontSize: 24.sp),
),
],
),
),
),
);
}
}
🎨 布局切换动画优化
✨ 为什么需要过渡动画?
当折叠屏展开或折叠时,布局会突然切换,可能让用户感到突兀。添加平滑的过渡动画可以:
- 🎭 提升用户体验:让切换更自然流畅
- 👁️ 视觉连续性:保持界面的连贯感
- 💫 专业感:体现应用的精致度
核心原理:
- AnimatedSwitcher:Flutter内置的切换动画组件
- FadeTransition:淡入淡出效果
- ScaleTransition:缩放效果
- ValueKey:确保Flutter识别不同的布局状态
💡 实现代码:
dart
/// ✨ 平滑的布局过渡组件
class AnimatedFoldableLayout extends StatelessWidget {
final Widget child;
const AnimatedFoldableLayout({Key? key, required this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: Duration(milliseconds: 300), // 动画时长300毫秒
transitionBuilder: (child, animation) {
// 组合淡入淡出和缩放效果
return FadeTransition(
opacity: animation, // 透明度动画
child: ScaleTransition(
scale: animation, // 缩放动画
child: child,
),
);
},
child: child,
);
}
}
/// 🎯 使用示例:带动画的商品列表
class AnimatedProductList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FoldableAdaptiveLayout(
// 📱 手机布局:单列 + 动画
phoneLayout: AnimatedFoldableLayout(
key: ValueKey('phone'), // 关键:用于区分不同状态
child: _buildSingleColumn(),
),
// 📟 平板布局:双列 + 动画
tabletLayout: AnimatedFoldableLayout(
key: ValueKey('tablet'), // 关键:用于区分不同状态
child: _buildDoubleColumn(),
),
);
}
/// 单列布局
Widget _buildSingleColumn() => ListView.builder(
itemBuilder: (context, index) => ListTile(
leading: Icon(Icons.shopping_bag),
title: Text('商品 $index'),
),
);
/// 双列布局
Widget _buildDoubleColumn() => GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
),
itemBuilder: (context, index) => Card(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_bag, size: 48),
SizedBox(height: 8),
Text('商品 $index'),
],
),
),
),
);
}
💡 动画效果说明:
- 当从手机切换到平板时,旧布局会淡出并缩小,新布局会淡入并放大
- 整个过程耗时300毫秒,流畅自然
- ValueKey确保Flutter能正确识别布局变化并触发动画
🛠️ 第五节:实用工具扩展
📏 自定义屏幕工具类
🤔 为什么要自己实现?
虽然flutter_screenutil很好用,但有时我们需要:
- 🎯 更灵活的控制:自定义适配逻辑
- 📊 更多的信息:获取更详细的屏幕数据
- 🔧 团队规范:统一团队的适配标准
- 💡 学习原理:理解适配的底层实现
核心功能:
- 屏幕信息获取:宽度、高度、像素比等
- 尺寸适配:基于设计稿的等比缩放
- 设备判断:手机、平板、桌面识别
- 方向判断:横屏、竖屏检测
- 安全区域:状态栏、底部栏高度
💡 实现代码:
dart
/// 🔧 自定义屏幕适配工具类
class ScreenUtil {
static late MediaQueryData _mediaQueryData;
static late double _screenWidth;
static late double _screenHeight;
static late double _pixelRatio;
static late double _statusBarHeight;
static late double _bottomBarHeight;
static late double _textScaleFactor;
/// 🎯 初始化屏幕工具
static void init(BuildContext context) {
_mediaQueryData = MediaQuery.of(context);
_screenWidth = _mediaQueryData.size.width;
_screenHeight = _mediaQueryData.size.height;
_pixelRatio = _mediaQueryData.devicePixelRatio;
_statusBarHeight = _mediaQueryData.padding.top;
_bottomBarHeight = _mediaQueryData.padding.bottom;
_textScaleFactor = _mediaQueryData.textScaleFactor;
}
/// 📱 获取屏幕宽度
static double get screenWidth => _screenWidth;
/// 📱 获取屏幕高度
static double get screenHeight => _screenHeight;
/// 🎯 根据屏幕宽度适配
static double setWidth(num width) => width * _screenWidth / 375;
/// 🎯 根据屏幕高度适配
static double setHeight(num height) => height * _screenHeight / 812;
/// 🎯 根据较小边适配(确保不变形)
static double setSp(num fontSize) {
return fontSize * min(_screenWidth / 375, _screenHeight / 812);
}
/// 🎯 设备类型判断
static bool get isMobile => _screenWidth < 600;
static bool get isTablet => _screenWidth >= 600 && _screenWidth < 1200;
static bool get isDesktop => _screenWidth >= 1200;
/// 🎯 设备方向判断
static bool get isLandscape => _screenWidth > _screenHeight;
static bool get isPortrait => _screenHeight >= _screenWidth;
/// 🎯 安全区域高度
static double get safeAreaTop => _statusBarHeight;
static double get safeAreaBottom => _bottomBarHeight;
static double get safeAreaHeight => _screenHeight - _statusBarHeight - _bottomBarHeight;
/// 🎯 响应式数值
static T responsive<T>({
required T mobile,
required T tablet,
required T desktop,
}) {
if (isMobile) return mobile;
if (isTablet) return tablet;
return desktop;
}
}
/// 🎯 使用示例
class ResponsiveWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
ScreenUtil.init(context);
return Container(
width: ScreenUtil.setWidth(200),
height: ScreenUtil.setHeight(100),
padding: EdgeInsets.all(ScreenUtil.responsive(
mobile: 16.0,
tablet: 24.0,
desktop: 32.0,
)),
child: Text(
'响应式文本',
style: TextStyle(
fontSize: ScreenUtil.setSp(16),
),
),
);
}
}
🎨 响应式样式系统
🎯 设计思路:
创建统一的样式系统,让整个应用的字体、间距保持一致,并能自动适配不同设备。
优势:
- 📝 统一规范:团队使用相同的样式标准
- 🔄 易于维护:修改一处,全局生效
- 📱 自动适配:根据设备自动调整大小
- 🎨 设计系统:符合Material Design等规范
💡 实现代码:
dart
/// 📝 响应式文本样式系统
class ResponsiveTextStyles {
static TextStyle heading1(BuildContext context) {
return TextStyle(
fontSize: ScreenUtil.responsive(
mobile: 24.0,
tablet: 28.0,
desktop: 32.0,
),
fontWeight: FontWeight.bold,
height: 1.2,
);
}
static TextStyle heading2(BuildContext context) {
return TextStyle(
fontSize: ScreenUtil.responsive(
mobile: 20.0,
tablet: 24.0,
desktop: 28.0,
),
fontWeight: FontWeight.w600,
height: 1.3,
);
}
static TextStyle body(BuildContext context) {
return TextStyle(
fontSize: ScreenUtil.responsive(
mobile: 14.0,
tablet: 16.0,
desktop: 18.0,
),
height: 1.5,
);
}
static TextStyle caption(BuildContext context) {
return TextStyle(
fontSize: ScreenUtil.responsive(
mobile: 12.0,
tablet: 14.0,
desktop: 16.0,
),
color: Colors.grey[600],
height: 1.4,
);
}
}
/// 📏 响应式间距系统
class ResponsiveSpacing {
// 超小间距:用于紧密排列的元素
static double get xs => ScreenUtil.responsive(mobile: 4.0, tablet: 6.0, desktop: 8.0);
// 小间距:用于相关元素之间
static double get sm => ScreenUtil.responsive(mobile: 8.0, tablet: 12.0, desktop: 16.0);
// 中等间距:用于组件之间
static double get md => ScreenUtil.responsive(mobile: 16.0, tablet: 20.0, desktop: 24.0);
// 大间距:用于区块之间
static double get lg => ScreenUtil.responsive(mobile: 24.0, tablet: 32.0, desktop: 40.0);
// 超大间距:用于页面级分隔
static double get xl => ScreenUtil.responsive(mobile: 32.0, tablet: 48.0, desktop: 64.0);
}
🎯 综合使用示例:
dart
/// 📱 使用响应式样式系统的完整页面
class StyledProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 初始化ScreenUtil
ScreenUtil.init(context);
return Scaffold(
appBar: AppBar(
title: Text(
'个人资料',
style: ResponsiveTextStyles.heading1(context),
),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(ResponsiveSpacing.md), // 使用响应式间距
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 头像区域
Center(
child: CircleAvatar(
radius: ScreenUtil.setWidth(50), // 响应式头像大小
child: Icon(Icons.person, size: ScreenUtil.setSp(40)),
),
),
SizedBox(height: ResponsiveSpacing.lg), // 大间距
// 用户名
Text(
'张三',
style: ResponsiveTextStyles.heading1(context),
),
SizedBox(height: ResponsiveSpacing.sm), // 小间距
// 简介
Text(
'Flutter开发工程师',
style: ResponsiveTextStyles.body(context),
),
SizedBox(height: ResponsiveSpacing.xl), // 超大间距
// 信息卡片
_buildInfoCard(
context,
title: '联系方式',
items: [
_buildInfoItem(context, '邮箱', 'zhangsan@example.com'),
_buildInfoItem(context, '电话', '138****8888'),
],
),
SizedBox(height: ResponsiveSpacing.md),
_buildInfoCard(
context,
title: '个人信息',
items: [
_buildInfoItem(context, '城市', '北京'),
_buildInfoItem(context, '职位', 'Senior Developer'),
],
),
],
),
),
);
}
/// 信息卡片
Widget _buildInfoCard(BuildContext context, {
required String title,
required List<Widget> items,
}) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(ResponsiveSpacing.md),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(ScreenUtil.setWidth(12)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: ResponsiveTextStyles.heading2(context)),
SizedBox(height: ResponsiveSpacing.sm),
...items,
],
),
);
}
/// 信息项
Widget _buildInfoItem(BuildContext context, String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: ResponsiveSpacing.xs),
child: Row(
children: [
Text(
'$label: ',
style: ResponsiveTextStyles.caption(context),
),
Text(
value,
style: ResponsiveTextStyles.body(context),
),
],
),
);
}
}
💡 使用效果:
- 在手机上:紧凑的布局,适中的字体和间距
- 在平板上:更宽松的布局,稍大的字体和间距
- 在桌面上:舒适的布局,更大的字体和间距
- 所有尺寸都自动适配,无需手动调整
💡 第六节:开发技巧与最佳实践
✅ 推荐的开发技巧
1. 🎯 创建统一的适配工具类
dart
/// 🔧 项目级适配工具(结合screenutil和原生响应式)
class AppAdaptive {
/// 初始化
static void init(BuildContext context) {
ScreenUtil.init(context);
}
/// 🎯 智能宽度适配
static double width(double designWidth) {
// 在平板和桌面上限制最大宽度,避免过度放大
if (ScreenUtil().screenWidth > 600) {
return designWidth * 1.5; // 平板上适度放大
}
return designWidth.w; // 手机上使用screenutil
}
/// 🎯 智能高度适配
static double height(double designHeight) {
if (ScreenUtil().screenWidth > 600) {
return designHeight * 1.5;
}
return designHeight.h;
}
/// 🎯 智能字体适配
static double fontSize(double designSize) {
if (ScreenUtil().screenWidth > 600) {
return designSize * 1.2; // 平板上字体适度增大
}
return designSize.sp;
}
/// 🎯 响应式间距
static double spacing(SpacingSize size) {
final base = {
SpacingSize.xs: 4.0,
SpacingSize.sm: 8.0,
SpacingSize.md: 16.0,
SpacingSize.lg: 24.0,
SpacingSize.xl: 32.0,
}[size]!;
return ScreenUtil().screenWidth > 600 ? base * 1.5 : base.w;
}
}
enum SpacingSize { xs, sm, md, lg, xl }
/// 🎯 使用示例
class AdaptiveCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
width: AppAdaptive.width(300),
height: AppAdaptive.height(200),
padding: EdgeInsets.all(AppAdaptive.spacing(SpacingSize.md)),
child: Text(
'自适应卡片',
style: TextStyle(fontSize: AppAdaptive.fontSize(16)),
),
);
}
}
2. 🧪 开发时的调试工具
dart
/// 🔍 屏幕信息调试工具
class ScreenDebugger extends StatelessWidget {
final Widget child;
final bool showDebugInfo;
const ScreenDebugger({
Key? key,
required this.child,
this.showDebugInfo = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
children: [
child,
if (showDebugInfo)
Positioned(
top: 50,
right: 10,
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDebugText('宽度: ${ScreenUtil().screenWidth.toInt()}'),
_buildDebugText('高度: ${ScreenUtil().screenHeight.toInt()}'),
_buildDebugText('像素比: ${ScreenUtil().pixelRatio.toStringAsFixed(2)}'),
_buildDebugText('状态: ${FoldableDetector.detect(context).toString().split('.').last}'),
_buildDebugText('折叠屏: ${FoldableDetector.isFoldable(context) ? "是" : "否"}'),
],
),
),
),
],
);
}
Widget _buildDebugText(String text) {
return Text(
text,
style: TextStyle(color: Colors.white, fontSize: 12),
);
}
}
/// 🎯 使用方式
void main() {
runApp(
ScreenDebugger(
showDebugInfo: true, // 开发时设为true,发布时设为false
child: MyApp(),
),
);
}
3. 📱 设备预览工具
dart
/// 🎨 设备预览切换器(用于开发测试)
class DevicePreview extends StatefulWidget {
final Widget child;
const DevicePreview({Key? key, required this.child}) : super(key: key);
@override
_DevicePreviewState createState() => _DevicePreviewState();
}
class _DevicePreviewState extends State<DevicePreview> {
Size _currentSize = Size(375, 812); // 默认iPhone X
final Map<String, Size> _presets = {
'iPhone SE': Size(375, 667),
'iPhone 12': Size(390, 844),
'iPhone 14 Pro Max': Size(430, 932),
'iPad': Size(768, 1024),
'iPad Pro': Size(1024, 1366),
'Galaxy Fold (外屏)': Size(280, 653),
'Galaxy Fold (内屏)': Size(717, 884),
};
@override
Widget build(BuildContext context) {
return Column(
children: [
// 设备选择器
Container(
height: 50,
color: Colors.grey[200],
child: ListView(
scrollDirection: Axis.horizontal,
children: _presets.entries.map((entry) {
return Padding(
padding: EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () => setState(() => _currentSize = entry.value),
child: Text(entry.key),
),
);
}).toList(),
),
),
// 预览区域
Expanded(
child: Center(
child: Container(
width: _currentSize.width,
height: _currentSize.height,
decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 2),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10)],
),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
size: _currentSize,
),
child: widget.child,
),
),
),
),
],
);
}
}
4. 💾 布局偏好持久化
dart
/// 🔄 保存用户的布局偏好
class LayoutPreferences {
static const String _key = 'layout_mode';
/// 保存布局模式
static Future<void> saveMode(String mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_key, mode);
}
/// 获取布局模式
static Future<String> getMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_key) ?? 'auto';
}
/// 保存字体缩放偏好
static Future<void> saveFontScale(double scale) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble('font_scale', scale);
}
/// 获取字体缩放偏好
static Future<double> getFontScale() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getDouble('font_scale') ?? 1.0;
}
}
/// 🎯 使用示例:让用户选择布局模式
class LayoutSettingsPage extends StatefulWidget {
@override
_LayoutSettingsPageState createState() => _LayoutSettingsPageState();
}
class _LayoutSettingsPageState extends State<LayoutSettingsPage> {
String _layoutMode = 'auto';
double _fontScale = 1.0;
@override
void initState() {
super.initState();
_loadPreferences();
}
Future<void> _loadPreferences() async {
final mode = await LayoutPreferences.getMode();
final scale = await LayoutPreferences.getFontScale();
setState(() {
_layoutMode = mode;
_fontScale = scale;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('布局设置')),
body: ListView(
children: [
ListTile(
title: Text('布局模式'),
subtitle: Text(_layoutMode),
trailing: DropdownButton<String>(
value: _layoutMode,
items: [
DropdownMenuItem(value: 'auto', child: Text('自动')),
DropdownMenuItem(value: 'compact', child: Text('紧凑')),
DropdownMenuItem(value: 'comfortable', child: Text('舒适')),
],
onChanged: (value) async {
if (value != null) {
await LayoutPreferences.saveMode(value);
setState(() => _layoutMode = value);
}
},
),
),
ListTile(
title: Text('字体大小'),
subtitle: Slider(
value: _fontScale,
min: 0.8,
max: 1.5,
divisions: 7,
label: '${(_fontScale * 100).toInt()}%',
onChanged: (value) async {
await LayoutPreferences.saveFontScale(value);
setState(() => _fontScale = value);
},
),
),
],
),
);
}
}
❌ 需要避免的常见错误
-
🚫 忽略极端尺寸
dart// ❌ 错误:只考虑常见尺寸 if (width < 600) return mobileLayout(); else return desktopLayout(); // ✅ 正确:考虑所有可能的尺寸 if (width < 600) return mobileLayout(); else if (width < 900) return tabletLayout(); else if (width < 1200) return desktopLayout(); else return ultraWideLayout(); -
🚫 硬编码断点
dart// ❌ 错误:硬编码断点 final isMobile = MediaQuery.of(context).size.width < 600; // ✅ 正确:使用配置化断点 final isMobile = MediaQuery.of(context).size.width < Breakpoints.mobile;
🎯 性能优化建议
1. ⚡ 避免频繁重建
dart
/// ✅ 使用缓存避免重复计算
class AdaptiveCache {
static final Map<String, dynamic> _cache = {};
/// 缓存计算结果
static T getCached<T>(String key, T Function() builder) {
if (!_cache.containsKey(key)) {
_cache[key] = builder();
}
return _cache[key] as T;
}
/// 清除缓存(屏幕尺寸变化时调用)
static void clear() {
_cache.clear();
}
}
/// 🎯 使用示例
class CachedResponsiveWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 缓存计算结果,避免每次build都重新计算
final width = AdaptiveCache.getCached(
'container_width',
() => AppAdaptive.width(300),
);
return Container(width: width, child: Text('优化后的组件'));
}
}
2. 🎨 使用const构造函数
dart
// ❌ 错误:每次build都创建新对象
Text('标题', style: TextStyle(fontSize: 16.sp))
// ✅ 正确:使用const减少重建
const Text('标题', style: TextStyle(fontSize: 16))
// ✅ 更好:提取为常量
class AppTextStyles {
static const heading = TextStyle(fontSize: 24, fontWeight: FontWeight.bold);
static const body = TextStyle(fontSize: 16);
static const caption = TextStyle(fontSize: 12, color: Colors.grey);
}
3. 📊 监控性能
dart
/// 🔍 性能监控工具
class PerformanceMonitor {
static void measureBuildTime(String widgetName, VoidCallback build) {
final stopwatch = Stopwatch()..start();
build();
stopwatch.stop();
if (stopwatch.elapsedMilliseconds > 16) { // 超过一帧时间
print('⚠️ $widgetName 构建耗时: ${stopwatch.elapsedMilliseconds}ms');
}
}
}
🎉 第七节:总结
📚 知识点总结
通过本章学习,你已经掌握了:
🎯 核心知识
- ✅ 理解为什么需要第三方适配插件(解决设计稿还原、代码重复等痛点)
- ✅ 掌握三大主流适配方案的特点和选择标准
- ✅ 学会使用flutter_screenutil进行快速适配
- ✅ 了解折叠屏设备的适配策略
- ✅ 掌握实用的开发工具和调试技巧
🛠️ 实战技能
- ✅ 能够根据项目需求选择合适的适配方案
- ✅ 能够创建统一的适配工具类
- ✅ 能够处理折叠屏的状态切换
- ✅ 能够优化适配性能
- ✅ 能够调试和测试不同设备
🎯 方案选择速查表
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| 电商/社交App | flutter_screenutil | 需要高度还原UI设计稿 |
| 新闻/阅读App | responsive_framework | 需要跨平台响应式布局 |
| 工具/效率App | adaptive_scaffold | 遵循Material Design规范 |
| 企业级应用 | 原生响应式 | 需要精细控制和灵活性 |
| 混合场景 | 组合使用 | 结合多种方案的优势 |
📖 推荐学习资源
-
官方文档
- flutter_screenutil: https://pub.dev/packages/flutter_screenutil
- responsive_framework: https://pub.dev/packages/responsive_framework
- adaptive_scaffold: https://pub.dev/packages/flutter_adaptive_scaffold
-
进阶阅读
- Material Design 3 自适应指南
- Flutter性能优化最佳实践
- 折叠屏设备开发指南
🎉 恭喜完成学习!
现在你已经:
- 🎯 理解了屏幕适配的核心原理和痛点
- 🛠️ 掌握了多种适配方案的使用方法
- 📱 学会了处理折叠屏等特殊设备
- ⚡ 了解了性能优化和最佳实践
🚀 虽然介绍了这么多适配方案,实际开发中需要我们根据自己的项目去做选择。继续加油,成为Flutter UI大师! ✨