247 lines
8.2 KiB
Dart
247 lines
8.2 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||
|
||
/// ============================================================
|
||
/// 【响应式布局 Demo】一套代码同时适配 手机 / Pad
|
||
///
|
||
/// 一、核心概念:屏幕适配 ≠ 响应式布局(两个维度!)
|
||
/// 屏幕适配(flutter_screenutil):
|
||
/// 所有尺寸按比例缩放,解决"不同屏幕比例不变形";
|
||
/// 响应式布局(本 Demo):
|
||
/// 根据屏幕宽度【改变布局结构】——手机单列、Pad 双列。
|
||
/// 两者配合使用:尺寸用 .w/.h/.sp 缩放,结构用断点切换。
|
||
///
|
||
/// 二、区分手机 / Pad 的标准(Material 官方断点):
|
||
/// 宽度 < 600dp 手机
|
||
/// 宽度 600 ~ 839dp 小平板 / 折叠屏
|
||
/// 宽度 >= 840dp 大平板 / 桌面
|
||
/// ⚠ 关键:判断的是【屏幕宽度】,不是 Platform(Android/iOS)!
|
||
/// iOS 也有 iPad,Android 也有平板,平台判断永远分不清它们。
|
||
///
|
||
/// 三、两种拿宽度/可用空间的方式:
|
||
/// MediaQuery.sizeOf(context).width → 整个屏幕宽度
|
||
/// LayoutBuilder 的 constraints → 父容器实际给你的可用宽度
|
||
/// 区别:页面铺满屏幕时两者一样;
|
||
/// 页面内有可伸缩区域(侧栏收起/展开)时,必须用 LayoutBuilder。
|
||
/// 本 Demo 用 LayoutBuilder,因为它能拿到最真实的可用宽度。
|
||
///
|
||
/// 四、本 Demo 演示:
|
||
/// 同一个数据列表:
|
||
/// 手机 → 单列 ListTile
|
||
/// Pad → 双列卡片网格(列数按宽度自适应)
|
||
/// 顶部实时显示当前模式和宽度,旋转 Pad 或换设备即可看到切换。
|
||
/// ============================================================
|
||
class ResponsiveDemoPage extends StatelessWidget {
|
||
const ResponsiveDemoPage({super.key});
|
||
|
||
/// Material 断点:宽度 >= 600dp 视为 Pad
|
||
static const double _tabletBreakpoint = 600;
|
||
|
||
/// 每列卡片的最小宽度(用于自适应列数)
|
||
static const double _minCardWidth = 280;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('响应式:手机 vs Pad')),
|
||
body: LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final width = constraints.maxWidth;
|
||
final isTablet = width >= _tabletBreakpoint;
|
||
// 列数自适应:每列至少 280dp 宽
|
||
final columns = (width / _minCardWidth).floor().clamp(1, 4);
|
||
|
||
return Column(
|
||
children: [
|
||
// 顶部模式指示条(实时反馈,方便调试)
|
||
_ModeBar(width: width, isTablet: isTablet),
|
||
Expanded(
|
||
child: isTablet
|
||
? _buildGrid(columns) // Pad:多列网格
|
||
: _buildList(), // 手机:单列列表
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Pad 布局:双列(或更多列)卡片网格
|
||
Widget _buildGrid(int columns) {
|
||
return 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]),
|
||
);
|
||
}
|
||
|
||
/// 手机布局:单列列表
|
||
Widget _buildList() {
|
||
return ListView.separated(
|
||
padding: EdgeInsets.all(12.w),
|
||
itemCount: _items.length,
|
||
separatorBuilder: (_, _) => SizedBox(height: 8.h),
|
||
itemBuilder: (context, i) => _ListTileItem(item: _items[i]),
|
||
);
|
||
}
|
||
|
||
/// 演示数据
|
||
static const List<_Item> _items = [
|
||
_Item('单词本', '每日 20 个新词,按记忆曲线复习', Color(0xFF3B82F6)),
|
||
_Item('语法练习', '时态、从句专项训练', Color(0xFF8E6FE8)),
|
||
_Item('听力磨耳朵', '每日一段真题听力', Color(0xFFF05252)),
|
||
_Item('错题本', '自动收录做错的题', Color(0xFFFF9800)),
|
||
_Item('阅读打卡', '精选短文,每日一篇', Color(0xFF4CAF50)),
|
||
_Item('口语跟读', 'AI 评分纠正发音', Color(0xFF00ACC1)),
|
||
_Item('作文批改', '真题范文与点评', Color(0xFFEC407A)),
|
||
_Item('模拟考试', '历年真题限时训练', Color(0xFF5C6BC0)),
|
||
];
|
||
}
|
||
|
||
/// 模式指示条:显示当前是手机还是 Pad 模式 + 实时宽度
|
||
class _ModeBar extends StatelessWidget {
|
||
const _ModeBar({required this.width, required this.isTablet});
|
||
|
||
final double width;
|
||
final bool isTablet;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
||
color: isTablet ? const Color(0xFFE8F5E9) : const Color(0xFFE3F2FD),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
isTablet ? Icons.tablet : Icons.phone_android,
|
||
size: 18.w,
|
||
color: isTablet ? const Color(0xFF2E7D32) : const Color(0xFF1565C0),
|
||
),
|
||
SizedBox(width: 8.w),
|
||
Text(
|
||
isTablet ? 'Pad 模式(多列网格)' : '手机模式(单列列表)',
|
||
style: TextStyle(
|
||
fontSize: 14.sp,
|
||
fontWeight: FontWeight.w600,
|
||
color: isTablet ? const Color(0xFF2E7D32) : const Color(0xFF1565C0),
|
||
),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
'宽 ${width.toStringAsFixed(0)}dp',
|
||
style: TextStyle(fontSize: 12.sp, color: const Color(0xFF888888)),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Pad 用的卡片
|
||
class _CardItem extends StatelessWidget {
|
||
const _CardItem({required this.item});
|
||
|
||
final _Item item;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12.r),
|
||
border: Border.all(color: const Color(0xFFEEF1F5)),
|
||
),
|
||
padding: EdgeInsets.all(14.w),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
width: 40.w,
|
||
height: 40.h,
|
||
decoration: BoxDecoration(
|
||
color: item.color.withValues(alpha: 0.12),
|
||
borderRadius: BorderRadius.circular(10.r),
|
||
),
|
||
child: Icon(Icons.auto_stories, color: item.color, size: 22.w),
|
||
),
|
||
const Spacer(),
|
||
Text(
|
||
item.title,
|
||
style: TextStyle(
|
||
fontSize: 16.sp,
|
||
fontWeight: FontWeight.bold,
|
||
color: const Color(0xFF333333),
|
||
),
|
||
),
|
||
SizedBox(height: 4.h),
|
||
Text(
|
||
item.desc,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(fontSize: 12.sp, color: const Color(0xFF999999)),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 手机用的列表项
|
||
class _ListTileItem extends StatelessWidget {
|
||
const _ListTileItem({required this.item});
|
||
|
||
final _Item item;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12.r),
|
||
border: Border.all(color: const Color(0xFFEEF1F5)),
|
||
),
|
||
child: ListTile(
|
||
leading: Container(
|
||
width: 40.w,
|
||
height: 40.h,
|
||
decoration: BoxDecoration(
|
||
color: item.color.withValues(alpha: 0.12),
|
||
borderRadius: BorderRadius.circular(10.r),
|
||
),
|
||
child: Icon(Icons.auto_stories, color: item.color, size: 22.w),
|
||
),
|
||
title: Text(
|
||
item.title,
|
||
style: TextStyle(
|
||
fontSize: 15.sp,
|
||
fontWeight: FontWeight.w600,
|
||
color: const Color(0xFF333333),
|
||
),
|
||
),
|
||
subtitle: Text(
|
||
item.desc,
|
||
style: TextStyle(fontSize: 12.sp, color: const Color(0xFF999999)),
|
||
),
|
||
trailing: const Icon(Icons.chevron_right, color: Color(0xFFC0C6CF)),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 演示数据模型
|
||
class _Item {
|
||
const _Item(this.title, this.desc, this.color);
|
||
|
||
final String title;
|
||
final String desc;
|
||
final Color color;
|
||
}
|