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; }