Appearance
源码:labs/flutter/host/lib/topics/screen_adaptation/adaptive_layout_demo/adaptive_layout_demo_page.dart
- 原始路径:
labs/flutter/host/lib/topics/screen_adaptation/adaptive_layout_demo/adaptive_layout_demo_page.dart - 类型:
dart
dart
import 'package:flutter/material.dart';
class AdaptiveLayoutDemoPage extends StatefulWidget {
const AdaptiveLayoutDemoPage({super.key});
static const routeName = '/adaptive_layout_demo';
@override
State<AdaptiveLayoutDemoPage> createState() => _AdaptiveLayoutDemoPageState();
}
class _AdaptiveLayoutDemoPageState extends State<AdaptiveLayoutDemoPage> {
static const List<double> _presetWidths = [375, 430, 600, 768, 1024];
double _simulatedWidth = 375;
bool _useScaleOnly = false;
@override
Widget build(BuildContext context) {
final breakpoint = _resolveBreakpoint(_simulatedWidth);
final scale = (_simulatedWidth / 375).clamp(1.0, 1.2);
return Scaffold(
appBar: AppBar(
title: const Text('屏幕适配断点对照 Demo'),
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'这个 demo 不依赖真机矩阵,而是通过模拟逻辑宽度,对比“375 基准缩放”和“600/840 断点换布局”的差异。',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('模拟宽度:${_simulatedWidth.toStringAsFixed(0)}dp', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _presetWidths
.map(
(width) => ChoiceChip(
label: Text('${width.toStringAsFixed(0)}dp'),
selected: _simulatedWidth == width,
onSelected: (_) => setState(() => _simulatedWidth = width),
),
)
.toList(),
),
const SizedBox(height: 12),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('只看 375 基准缩放结果'),
subtitle: const Text('打开后故意不切布局,用于观察平板/横屏为什么会“整体变大”'),
value: _useScaleOnly,
onChanged: (value) => setState(() => _useScaleOnly = value),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('断点:${breakpoint.label}')),
Chip(label: Text('375 缩放系数:${scale.toStringAsFixed(2)}x')),
Chip(label: Text(_useScaleOnly ? '模式:纯缩放' : '模式:断点换布局')),
],
),
],
),
),
),
const SizedBox(height: 16),
Text('预览区域', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
LayoutBuilder(
builder: (context, constraints) {
final availableWidth = constraints.maxWidth;
final initialScale = _simulatedWidth > availableWidth ? availableWidth / _simulatedWidth : 1.0;
return SizedBox(
width: availableWidth,
height: 420 * initialScale,
child: InteractiveViewer(
minScale: initialScale,
maxScale: 5.0,
constrained: false,
child: Container(
width: _simulatedWidth,
height: 420,
decoration: BoxDecoration(
border: Border.all(color: Colors.blueGrey.shade200),
borderRadius: BorderRadius.circular(16),
color: Colors.white,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: _useScaleOnly
? _ScaledOnlyPreview(width: _simulatedWidth, scale: scale)
: _BreakpointPreview(width: _simulatedWidth, breakpoint: breakpoint, scale: scale),
),
),
),
);
},
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('观察重点', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
const Text('1. 375/430dp 手机宽度下,纯缩放和断点换布局的差异很小。'),
const Text('2. 到 768/1024dp 后,纯缩放会把按钮、卡片、标题一起放大;断点方案则开始切两栏/多列。'),
const Text('3. 断点方案里卡片尺寸保持稳定,增长的是“信息密度”和“布局结构”,不是整个 UI 比例。'),
],
),
),
),
],
),
);
}
}
class _ScaledOnlyPreview extends StatelessWidget {
const _ScaledOnlyPreview({required this.width, required this.scale});
final double width;
final double scale;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.grey.shade50,
child: Transform.scale(
scale: scale,
alignment: Alignment.topCenter,
child: const SizedBox(
width: 375,
child: _PhoneStyleDashboard(title: '375 基准纯缩放', subtitle: '所有尺寸都乘以同一个缩放系数'),
),
),
);
}
}
class _BreakpointPreview extends StatelessWidget {
const _BreakpointPreview({required this.width, required this.breakpoint, required this.scale});
final double width;
final _Breakpoint breakpoint;
final double scale;
@override
Widget build(BuildContext context) {
switch (breakpoint) {
case _Breakpoint.compact:
return _PhoneStyleDashboard(
title: 'Compact < 600dp',
subtitle: '手机单列,允许 375 基准做轻微缩放(当前 ${scale.toStringAsFixed(2)}x)',
);
case _Breakpoint.medium:
return const _TwoPaneDashboard(title: 'Medium 600~840dp', columns: 2);
case _Breakpoint.expanded:
return const _TwoPaneDashboard(title: 'Expanded > 840dp', columns: 3);
}
}
}
class _PhoneStyleDashboard extends StatelessWidget {
const _PhoneStyleDashboard({required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 16),
_SummaryCard(color: Colors.indigo.shade100, title: '今日任务', value: '12'),
const SizedBox(height: 12),
_SummaryCard(color: Colors.green.shade100, title: '待处理消息', value: '6'),
const SizedBox(height: 12),
Expanded(
child: ListView.separated(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) => const _ListItemCard(),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemCount: 3,
),
),
],
),
);
}
}
class _TwoPaneDashboard extends StatelessWidget {
const _TwoPaneDashboard({required this.title, required this.columns});
final String title;
final int columns;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
SizedBox(
width: 220,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text('开始切换栏位和信息密度,而不是把手机 UI 整体放大。', style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 16),
const _NavRailCard(icon: Icons.inbox_outlined, title: '收件箱'),
const SizedBox(height: 10),
const _NavRailCard(icon: Icons.analytics_outlined, title: '分析面板'),
const SizedBox(height: 10),
const _NavRailCard(icon: Icons.settings_outlined, title: '设置'),
],
),
),
const SizedBox(width: 16),
Expanded(
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.2,
),
itemCount: columns * 3,
itemBuilder: (_, index) => _GridPanel(index: index),
),
),
],
),
);
}
}
class _SummaryCard extends StatelessWidget {
const _SummaryCard({required this.color, required this.title, required this.value});
final Color color;
final String title;
final String value;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(16)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title, style: Theme.of(context).textTheme.titleSmall),
Text(value, style: Theme.of(context).textTheme.headlineSmall),
],
),
);
}
}
class _ListItemCard extends StatelessWidget {
const _ListItemCard();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.blueGrey.shade100),
),
child: const Row(
children: [
CircleAvatar(child: Icon(Icons.person_outline)),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('列表项标题'),
SizedBox(height: 4),
Text('手机单列时保持紧凑,不要让元素被平板宽度放大'),
],
),
),
],
),
);
}
}
class _NavRailCard extends StatelessWidget {
const _NavRailCard({required this.icon, required this.title});
final IconData icon;
final String title;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
color: Colors.indigo.shade50,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
Icon(icon),
const SizedBox(width: 10),
Text(title),
],
),
);
}
}
class _GridPanel extends StatelessWidget {
const _GridPanel({required this.index});
final int index;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.blueGrey.shade100),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('面板 ${index + 1}', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.blueGrey.shade50,
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 8),
const Text('保持卡片本体尺寸稳定,用更多列数承接更大的可用宽度。'),
],
),
);
}
}
enum _Breakpoint {
compact('compact'),
medium('medium'),
expanded('expanded');
const _Breakpoint(this.label);
final String label;
}
_Breakpoint _resolveBreakpoint(double widthDp) {
if (widthDp < 600) {
return _Breakpoint.compact;
}
if (widthDp < 840) {
return _Breakpoint.medium;
}
return _Breakpoint.expanded;
}