Skip to content

源码: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;
}

站点构建时间:2026/8/24 23:43:17