Files
2026-09-04 15:22:20 +08:00

247 lines
8.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}