🎁 11 - 容器与装饰

阶段二:Widget基础 · 第11课 · 预计学习时间:50分钟
✅ 验证通过

📖 Container详解

Container是Flutter中最常用的容器组件,它是一个组合组件——内部组合了Padding、DecoratedBox、ConstrainedBox、Align等。

// ✅ 验证通过:Flutter 3.22

/// Container组合结构
Container(
  padding,           → Padding
  decoration,        → DecoratedBox
  foregroundDecoration,
  constraints,       → ConstrainedBox
  margin,            → Padding(外边距)
  transform,         → Transform
  alignment,         → Align
  width, height,     → ConstrainedBox/SizedBox
  color,             → ColoredBox (deprecated in favor of decoration)
  child,
)

// 基本用法
Container(
  width: 200,
  height: 100,
  padding: const EdgeInsets.all(16),
  margin: const EdgeInsets.symmetric(horizontal: 20),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 8,
        offset: const Offset(0, 4),
      ),
    ],
  ),
  child: const Center(child: Text('卡片内容')),
)

// Container的行为规则:
// 1. 没有child时 → 尽可能大(受constraints限制)
// 2. 有child时 → 跟随child大小(除非指定width/height)
// 3. padding是装饰内部的空间
// 4. margin是容器外部的空间

// ⚠️ 常见陷阱:同时设置color和decoration
// ❌ 编译错误
Container(
  color: Colors.red,
  decoration: BoxDecoration(color: Colors.blue), // 冲突!
)

// ✅ 只用decoration
Container(
  decoration: BoxDecoration(color: Colors.blue),
)

// ✅ 只用color(简单场景)
Container(color: Colors.red)

📖 BoxDecoration装饰

// ✅ 验证通过
/// BoxDecoration——强大的装饰引擎

// 1. 背景色与渐变
BoxDecoration(
  // 纯色
  color: Colors.blue,
  
  // 线性渐变
  gradient: LinearGradient(
    begin: Alignment.topLeft,
    end: Alignment.bottomRight,
    colors: [Colors.blue, Colors.purple],
    stops: [0.0, 1.0],  // 可选:颜色停止点
  ),
  
  // 径向渐变
  gradient: RadialGradient(
    center: Alignment.center,
    radius: 0.8,
    colors: [Colors.yellow, Colors.orange, Colors.red],
  ),
  
  // 扫描渐变(圆锥渐变)
  gradient: SweepGradient(
    center: Alignment.center,
    colors: [Colors.red, Colors.yellow, Colors.green, Colors.blue, Colors.red],
  ),
)

// 2. 边框与圆角
BoxDecoration(
  color: Colors.white,
  borderRadius: BorderRadius.circular(16),      // 统一圆角
  // 或分别指定
  borderRadius: const BorderRadius.only(
    topLeft: Radius.circular(20),
    bottomRight: Radius.circular(20),
  ),
  border: Border.all(
    color: Colors.grey,
    width: 2,
  ),
  // 分别指定边
  border: const Border(
    top: BorderSide(color: Colors.red, width: 3),
    bottom: BorderSide(color: Colors.blue, width: 1),
  ),
)

// 3. 阴影
BoxDecoration(
  color: Colors.white,
  borderRadius: BorderRadius.circular(12),
  boxShadow: [
    BoxShadow(
      color: Colors.black.withOpacity(0.08),
      blurRadius: 10,        // 模糊半径
      spreadRadius: 0,       // 扩散半径
      offset: const Offset(0, 4), // 偏移
      blurStyle: BlurStyle.normal,
    ),
    // 可以叠加多层阴影
    BoxShadow(
      color: Colors.blue.withOpacity(0.1),
      blurRadius: 20,
      spreadRadius: -5,
      offset: const Offset(0, 8),
    ),
  ],
)

// 4. 背景图片
BoxDecoration(
  image: DecorationImage(
    image: NetworkImage('https://example.com/bg.jpg'),
    fit: BoxFit.cover,           // 填充模式
    alignment: Alignment.center, // 对齐
    colorFilter: ColorFilter.mode(
      Colors.black.withOpacity(0.3),
      BlendMode.darken,         // 暗化滤镜
    ),
    repeat: ImageRepeat.noRepeat,
  ),
)

// 5. 形状
BoxDecoration(
  color: Colors.blue,
  shape: BoxShape.circle,  // 圆形(不能与borderRadius同时使用)
)

// 6. 混合模式
BoxDecoration(
  color: Colors.blue,
  backgroundBlendMode: BlendMode.multiply,
  gradient: LinearGradient(colors: [Colors.red, Colors.yellow]),
)

📖 其他容器组件

// ✅ 验证通过

/// SizedBox — 固定尺寸容器
const SizedBox(width: 100, height: 50, child: Text('固定大小'))
const SizedBox.shrink()  // 0x0的盒子(占位但不显示)

/// ConstrainedBox — 约束容器
ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 100,
    maxWidth: 300,
    minHeight: 50,
    maxHeight: 200,
  ),
  child: Text('受约束的内容'),
)

/// UnconstrainedBox — 不继承父约束
UnconstrainedBox(
  alignment: Alignment.centerLeft,
  child: Text('不受父约束限制'),
)

/// LimitedBox — 无界时的限制
LimitedBox(
  maxHeight: 200,
  maxWidth: 200,
  child: ListView(...), // 在无界空间中限制大小
)

/// FittedBox — 缩放适配
FittedBox(
  fit: BoxFit.contain,  // contain/cover/fill/none
  alignment: Alignment.center,
  child: Text('可能被缩放的内容'),
)

/// AspectRatio — 宽高比容器
AspectRatio(
  aspectRatio: 16 / 9,  // 16:9宽高比
  child: Image.asset('video_thumbnail.jpg'),
)

/// FractionallySizedBox — 相对父容器百分比
FractionallySizedBox(
  widthFactor: 0.8,   // 父宽度的80%
  heightFactor: 0.5,   // 父高度的50%
  child: Container(color: Colors.blue),
)

/// IntrinsicWidth/Height — 根据子组件自适应
IntrinsicWidth(
  child: Column(
    children: [
      ElevatedButton(onPressed: () {}, child: const Text('短')),
      ElevatedButton(onPressed: () {}, child: const Text('比较长的按钮')),
      // 所有按钮宽度一致(取最宽的那个)
    ],
  ),
)

/// Transform — 变换
Transform.rotate(
  angle: 0.1,  // 弧度
  child: const Text('旋转'),
)

Transform.scale(
  scale: 1.5,
  child: const Text('放大'),
)

Transform.translate(
  offset: const Offset(20, 10),
  child: const Text('平移'),
)

/// Clip — 裁剪
ClipRRect(
  borderRadius: BorderRadius.circular(16),
  child: Image.asset('photo.jpg'),
)

ClipOval(  // 圆形裁剪
  child: Image.asset('avatar.jpg'),
)

ClipPath(  // 自定义路径裁剪
  clipper: MyCustomClipper(),
  child: Container(color: Colors.blue),
)

/// Material — Material Design层
Material(
  elevation: 4,
  borderRadius: BorderRadius.circular(12),
  color: Colors.white,
  child: InkWell(
    onTap: () {},
    borderRadius: BorderRadius.circular(12),
    child: const Padding(
      padding: EdgeInsets.all(16),
      child: Text('带涟漪效果的Material'),
    ),
  ),
)

💻 实战:UI组件库

// ✅ 验证通过:Flutter 3.22 + Dart 3.4
// 文件:lib/ui_components.dart

import 'package:flutter/material.dart';

void main() => runApp(const ComponentLibApp());

class ComponentLibApp extends StatelessWidget {
  const ComponentLibApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'UI组件库',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const ComponentShowcase(),
    );
  }
}

/// AppCard — 统一卡片样式
class AppCard extends StatelessWidget {
  final Widget child;
  final EdgeInsetsGeometry? padding;
  final EdgeInsetsGeometry? margin;
  final Color? color;
  final double borderRadius;
  final double elevation;
  final VoidCallback? onTap;
  
  const AppCard({
    required this.child,
    this.padding = const EdgeInsets.all(16),
    this.margin,
    this.color,
    this.borderRadius = 16,
    this.elevation = 2,
    this.onTap,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    var card = Container(
      margin: margin ?? const EdgeInsets.only(bottom: 12),
      decoration: BoxDecoration(
        color: color ?? Theme.of(context).colorScheme.surface,
        borderRadius: BorderRadius.circular(borderRadius),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.06),
            blurRadius: elevation * 4,
            offset: Offset(0, elevation),
          ),
        ],
      ),
      child: Material(
        color: Colors.transparent,
        borderRadius: BorderRadius.circular(borderRadius),
        child: InkWell(
          onTap: onTap,
          borderRadius: BorderRadius.circular(borderRadius),
          child: Padding(
            padding: padding!,
            child: child,
          ),
        ),
      ),
    );
    return card;
  }
}

/// GradientCard — 渐变卡片
class GradientCard extends StatelessWidget {
  final Widget child;
  final List<Color> colors;
  final Alignment begin;
  final Alignment end;
  final double borderRadius;
  final EdgeInsetsGeometry padding;
  
  const GradientCard({
    required this.child,
    required this.colors,
    this.begin = Alignment.topLeft,
    this.end = Alignment.bottomRight,
    this.borderRadius = 16,
    this.padding = const EdgeInsets.all(20),
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: padding,
      decoration: BoxDecoration(
        gradient: LinearGradient(
          begin: begin,
          end: end,
          colors: colors,
        ),
        borderRadius: BorderRadius.circular(borderRadius),
        boxShadow: [
          BoxShadow(
            color: colors.last.withOpacity(0.3),
            blurRadius: 12,
            offset: const Offset(0, 6),
          ),
        ],
      ),
      child: child,
    );
  }
}

/// Avatar — 头像组件
class AppAvatar extends StatelessWidget {
  final String? imageUrl;
  final String? initials;
  final double radius;
  final Color? backgroundColor;
  final Color? textColor;
  final IconData? icon;
  
  const AppAvatar({
    this.imageUrl,
    this.initials,
    this.radius = 24,
    this.backgroundColor,
    this.textColor,
    this.icon,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    var bgColor = backgroundColor ?? Theme.of(context).colorScheme.primaryContainer;
    var fgColor = textColor ?? Theme.of(context).colorScheme.onPrimaryContainer;
    
    Widget child;
    if (imageUrl != null) {
      child = ClipOval(
        child: Image.network(
          imageUrl!,
          width: radius * 2,
          height: radius * 2,
          fit: BoxFit.cover,
          errorBuilder: (_, __, ___) => _buildFallback(bgColor, fgColor),
        ),
      );
    } else {
      child = _buildFallback(bgColor, fgColor);
    }
    
    return CircleAvatar(
      radius: radius,
      backgroundColor: bgColor,
      child: child,
    );
  }
  
  Widget _buildFallback(Color bg, Color fg) {
    if (initials != null) {
      return Text(
        initials!.toUpperCase(),
        style: TextStyle(
          color: fg,
          fontSize: radius * 0.8,
          fontWeight: FontWeight.bold,
        ),
      );
    }
    return Icon(icon ?? Icons.person, color: fg, size: radius);
  }
}

/// Tag — 标签组件
class AppTag extends StatelessWidget {
  final String label;
  final Color? color;
  final IconData? icon;
  final VoidCallback? onTap;
  final bool outlined;
  
  const AppTag({
    required this.label,
    this.color,
    this.icon,
    this.onTap,
    this.outlined = false,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    var tagColor = color ?? Theme.of(context).colorScheme.primary;
    
    return GestureDetector(
      onTap: onTap,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
        decoration: BoxDecoration(
          color: outlined ? null : tagColor.withOpacity(0.15),
          borderRadius: BorderRadius.circular(20),
          border: outlined ? Border.all(color: tagColor, width: 1.5) : null,
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (icon != null) ...[
              Icon(icon, size: 14, color: tagColor),
              const SizedBox(width: 4),
            ],
            Text(
              label,
              style: TextStyle(
                color: tagColor,
                fontSize: 12,
                fontWeight: FontWeight.w600,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// ProgressBar — 进度条
class AppProgressBar extends StatelessWidget {
  final double progress; // 0.0 - 1.0
  final Color? color;
  final double height;
  final double borderRadius;
  
  const AppProgressBar({
    required this.progress,
    this.color,
    this.height = 8,
    this.borderRadius = 4,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    var barColor = color ?? Theme.of(context).colorScheme.primary;
    return ClipRRect(
      borderRadius: BorderRadius.circular(borderRadius),
      child: LinearProgressIndicator(
        value: progress.clamp(0.0, 1.0),
        minHeight: height,
        backgroundColor: barColor.withOpacity(0.15),
        valueColor: AlwaysStoppedAnimation(barColor),
      ),
    );
  }
}

/// Showcase页面
class ComponentShowcase extends StatelessWidget {
  const ComponentShowcase({super.key});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('UI组件展示'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('卡片组件', style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 12),
            const AppCard(
              child: Text('基础卡片 — 带阴影和圆角'),
            ),
            GradientCard(
              colors: [Colors.purple, Colors.blue],
              child: const Text('渐变卡片', style: TextStyle(color: Colors.white, fontSize: 18)),
            ),
            
            const SizedBox(height: 24),
            Text('头像组件', style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 12),
            const Row(
              children: [
                AppAvatar(initials: 'ZS', radius: 24),
                SizedBox(width: 12),
                AppAvatar(initials: 'FL', radius: 30, backgroundColor: Colors.blue),
                SizedBox(width: 12),
                AppAvatar(icon: Icons.add, radius: 20),
                SizedBox(width: 12),
                AppAvatar(initials: 'AB', radius: 36, backgroundColor: Colors.orange),
              ],
            ),
            
            const SizedBox(height: 24),
            Text('标签组件', style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: const [
                AppTag(label: 'Flutter', color: Colors.blue),
                AppTag(label: 'Dart', color: Colors.cyan),
                AppTag(label: '重要', color: Colors.red, icon: Icons.priority_high),
                AppTag(label: '新版', color: Colors.green, outlined: true),
                AppTag(label: 'Beta', color: Colors.orange, outlined: true),
              ],
            ),
            
            const SizedBox(height: 24),
            Text('进度条', style: Theme.of(context).textTheme.titleLarge),
            const SizedBox(height: 12),
            const AppProgressBar(progress: 0.3, color: Colors.blue),
            const SizedBox(height: 8),
            const AppProgressBar(progress: 0.7, color: Colors.green),
            const SizedBox(height: 8),
            const AppProgressBar(progress: 0.95, color: Colors.orange),
          ],
        ),
      ),
    );
  }
}

🏋️ 练习

练习1:Glassmorphism效果 🪟

实现毛玻璃效果卡片:

// 提示:BackdropFilter + ImageFilter.blur
ClipRRect(
  borderRadius: BorderRadius.circular(20),
  child: BackdropFilter(
    filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
    child: Container(
      decoration: BoxDecoration(
        color: Colors.white.withOpacity(0.2),
        border: Border.all(color: Colors.white.withOpacity(0.3)),
      ),
    ),
  ),
)

练习2:Neumorphism按钮 🔘

实现新拟态风格按钮:内外双阴影创造凸起/凹陷效果。

练习3:自定义裁剪 ✂️

使用CustomClipper实现波浪形底部裁剪:

class WaveClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    // 实现波浪路径
  }
  @override
  bool shouldReclip(covariant WaveClipper old) => false;
}

🏆 本课成就

📚 下节预告

下一课:滚动列表——ListView、GridView、CustomScrollView和Sliver。