flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
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;
}