8 个你可能忽略了的 Flutter 小部件(四)

📚 BlockSemantics

BlockSemantics 控件是一个可访问性控制工具 ,它的作用是阻止屏幕阅读器访问其子控件树中的单个控件

你可以将其想象成:给一组控件贴上一个请勿打扰 的标志。屏幕阅读器看到这个标志后,就会忽略标志后面的所有内容,不会去读取那些被它包裹住的单个控件。

//video

这个控件特别有用,尤其是在处理以下情况时:

您有一些控件在视觉上位于其他控件的后方 (例如,一个模态对话框叠加层 modal dialog overlay),但它们仍然是控件树的一部分。

如果没有 BlockSemantics ,屏幕阅读器可能会读取那些对用户当前不相关 的控件信息,从而提供令人困惑或多余的信息。

dart 复制代码
BlockSemantics(
  child: Container(
    child: Column(
      children: [
        Text('This text will be ignored by screen readers'),
        ElevatedButton(
          onPressed: () {},
          child: Text('This button will also be ignored'),
        ),
      ],
    ),
  ),
)

BlockSemantics 的底层工作原理

BlockSemantics 控件的工作原理是创建一个语义边界阻止 语义信息从其子控件传播 出去。它有效地创建了一个语义上的"黑洞",可访问性服务无法穿透这个黑洞来读取单个子控件。

dart 复制代码
class BlockSemantics extends SingleChildRenderObjectWidget {
  const BlockSemantics({super.key, this.blocking = true, super.child});

  final bool blocking;

  @override
  RenderBlockSemantics createRenderObject(BuildContext context) =>
      RenderBlockSemantics(blocking: blocking);

  @override
  void updateRenderObject(BuildContext context, RenderBlockSemantics renderObject) {
    renderObject.blocking = blocking;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    // ...
  }
}

渲染对象 RenderBlockSemantics 重写 了语义注解方法,以阻止子控件的语义 被包含在可访问性树中。


BlockSemantics 实用完整示例

以下是一个完整示例 ,展示了 BlockSemantics模态对话框场景中的应用:

dart 复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: BlockSemanticsDemo(),
    );
  }
}

class BlockSemanticsDemo extends StatefulWidget {
  const BlockSemanticsDemo({super.key});

  @override
  State<BlockSemanticsDemo> createState() => _BlockSemanticsDemoState();
}

class _BlockSemanticsDemoState extends State<BlockSemanticsDemo> {
  bool _showDialog = false;
  bool _useBlockSemantics = true;

  void _toggleDialog() {
    setState(() {
      _showDialog = !_showDialog;
    });
  }

  void _toggleBlockSemantics() {
    setState(() {
      _useBlockSemantics = !_useBlockSemantics;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BlockSemantics Demo'),
      ),
      body: Stack(
        children: [
          // Background content
          _buildBackgroundContent(),

          // Modal dialog overlay
          if (_showDialog) _buildModalOverlay(),
        ],
      ),
    );
  }

  Widget _buildBackgroundContent() {
    Widget backgroundContent = Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Card(
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Background Content',
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 8),
                  Text(
                    'This content should be ignored by screen readers when the dialog is open.',
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          SwitchListTile(
            title: const Text('Use BlockSemantics'),
            subtitle: const Text('Toggle to see accessibility difference'),
            value: _useBlockSemantics,
            onChanged: (value) => _toggleBlockSemantics(),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: _toggleDialog,
            child: Text(_showDialog ? 'Hide Dialog' : 'Show Dialog'),
          ),
          const SizedBox(height: 20),
          const Text(
            'Additional Background Elements',
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: () {},
            child: const Text('Background Action 1'),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: () {},
            child: const Text('Background Action 2'),
          ),
          const SizedBox(height: 8),
          const TextField(
            decoration: InputDecoration(
              labelText: 'Background Input',
              hintText: 'This should be blocked when dialog is open',
            ),
          ),
        ],
      ),
    );

    // Apply BlockSemantics when dialog is shown and option is enabled
    if (_showDialog && _useBlockSemantics) {
      return BlockSemantics(child: backgroundContent);
    }
    return backgroundContent;
  }

  Widget _buildModalOverlay() {
    return Container(
      color: Colors.black54,
      child: Center(
        child: Card(
          margin: const EdgeInsets.all(32.0),
          child: Padding(
            padding: const EdgeInsets.all(24.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Text(
                  'Modal Dialog',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 16),
                const Text(
                  'This dialog should be the focus of screen readers. '
                  'Background content should be blocked from accessibility.',
                ),
                const SizedBox(height: 20),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    ElevatedButton(
                      onPressed: _toggleDialog,
                      child: const Text('Cancel'),
                    ),
                    ElevatedButton(
                      onPressed: _toggleDialog,
                      child: const Text('Confirm'),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

BlockSemantics 对于创建可访问的模态体验 至关重要,它能确保屏幕阅读器用户只关注 相关内容,而不会被背景元素分散注意力


BeveledRectangleBorder

BeveledRectangleBorder 是一个形状类 ,它创建的矩形形状的角是斜切的、"被切掉的" ,而不是通常的圆角或直角。

这赋予了控件一种独特的、棱角分明的外观 ,非常适合创建具有几何、切口风格的独特设计元素。

//video

你通常会通过按钮、卡片和容器等控件的 shape 属性来使用 BeveledRectangleBorder ,从而实现那种独特的斜切(beveled)外观

dart 复制代码
Container(
  decoration: ShapeDecoration(
    color: Colors.blue,
    shape: BeveledRectangleBorder(
      borderRadius: BorderRadius.circular(16),
      side: BorderSide(color: Colors.purple, width: 2),
    ),
  ),
  child: Text('Beveled Container'),
)

BeveledRectangleBorder 的两个关键属性

有两个关键属性

  • borderRadius : 接受一个 BorderRadius ,用于决定斜切(bevel)从角落开始的距离半径值越大 ,创建的切角就越引人注目(更戏剧化)。
  • side : 允许您定义形状周围的描边(border stroke) ,包括颜色、宽度和样式。

BeveledRectangleBorder 的底层工作原理

BeveledRectangleBorder 继承自 OutlinedBorder ,并实现ShapeBorder 接口

以下是来自 GitHub 上的 Flutter 仓库的完整源代码

dart 复制代码
// ...
class BeveledRectangleBorder extends OutlinedBorder {
  final BorderRadiusGeometry borderRadius;

  ShapeBorder scale(t) => BeveledRectangleBorder(
    side: side.scale(t), borderRadius: borderRadius * t);

  ShapeBorder? lerpFrom/To(other, t) => sameType
    ? BeveledRectangleBorder(
        side: BorderSide.lerp(...),
        borderRadius: BorderRadiusGeometry.lerp(...))
    : super.lerpFrom/To(...);

  BeveledRectangleBorder copyWith({side?, borderRadius?}) => ...;

  Path _getPath(RRect r) {
    // Clamp radii to >= 0
    // Build 8 vertices by "cutting" each corner toward the side center
    final verts = <Offset>[
      // TL edge points toward left & top centers
      // TR, BR, BL similarly...
    ];
    return Path()..addPolygon(verts, true);
  }

  Path getOuterPath(Rect rect, {TextDirection? td}) =>
    _getPath(borderRadius.resolve(td).toRRect(rect));

  Path getInnerPath(Rect rect, {TextDirection? td}) =>
    _getPath(borderRadius.resolve(td).toRRect(rect).deflate(side.strokeInset));

  void paint(Canvas c, Rect rect, {TextDirection? td}) {
    if (side.style == BorderStyle.solid && !rect.isEmpty) {
      final RRect r = borderRadius.resolve(td).toRRect(rect);
      final RRect inflated = r.inflate(side.strokeOutset);
      final Path stroke = _getPath(inflated)
        ..addPath(getInnerPath(rect, textDirection: td), Offset.zero);
      c.drawPath(stroke, side.toPaint());
    }
  }
}
// ...

这个边框(BeveledRectangleBorder)类似于 RoundedRectangleBorder ,但不同之处在于,它的角是直切(straight cuts) ,而不是圆弧。

  • _getPath 方法: 它计算出一个八边形(octagon) :即8个顶点

    • 这些顶点是通过沿着每条边向其侧边中心滑动创建的。
    • 滑动的距离受限于 所提供的每个角的半径(radii (并被限制为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ≥ 0 \ge 0 </math>≥0)。
    • 如果半径(radii)非常大 ,斜切的对角线会在中心相交,形状最终会变成一个菱形(diamond)
  • getOuterPath/getInnerPath 方法:

    • 首先,它们利用 TextDirection (文本方向)来解析 BorderRadiusGeometry ,并将其转换为 RRect 对象。
    • 接着,它们构建外部/内部斜切的多边形 (内部路径使用 deflate 来考虑描边的厚度)。
  • paint 方法:

    • 首先,它为描边的外展(stroke outset)进行膨胀(inflates)。
    • 然后,它构建一个**"外部路径减去内部路径"**的路径(即轮廓区域)。
    • 最后,使用 side.toPaint() 方法将该路径绘制出来。

💻 BeveledRectangleBorder 实用完整示例

以下是一个综合示例 ,展示了 BeveledRectangleBorder不同变体

dart 复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('BeveledRectangleBorder Demo'),
        ),
        body: const SingleChildScrollView(
          padding: EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'BeveledRectangleBorder Examples',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 20),
              BeveledExample(
                title: 'Small Bevel (8px)',
                borderRadius: 8,
                color: Colors.red,
              ),
              SizedBox(height: 16),
              BeveledExample(
                title: 'Medium Bevel (16px)',
                borderRadius: 16,
                color: Colors.green,
              ),
              SizedBox(height: 16),
              BeveledExample(
                title: 'Large Bevel (24px)',
                borderRadius: 24,
                color: Colors.blue,
              ),
              SizedBox(height: 16),
              BeveledExample(
                title: 'With Border',
                borderRadius: 20,
                color: Colors.amber,
                hasBorder: true,
              ),
              SizedBox(height: 16),
              BeveledButtonExample(),
              SizedBox(height: 16),
              BeveledCardExample(),
            ],
          ),
        ),
      ),
    );
  }
}

class BeveledExample extends StatelessWidget {
  final String title;
  final double borderRadius;
  final Color color;
  final bool hasBorder;

  const BeveledExample({
    super.key,
    required this.title,
    required this.borderRadius,
    required this.color,
    this.hasBorder = false,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          title,
          style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
        ),
        const SizedBox(height: 8),
        Container(
          height: 80,
          decoration: ShapeDecoration(
            color: color.withOpacity(0.3),
            shape: BeveledRectangleBorder(
              borderRadius: BorderRadius.circular(borderRadius),
              side: hasBorder
                  ? BorderSide(color: color, width: 3)
                  : BorderSide.none,
            ),
          ),
          child: Center(
            child: Text(
              'Radius: ${borderRadius}px',
              style: TextStyle(
                color: color.withOpacity(0.8),
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

class BeveledButtonExample extends StatelessWidget {
  const BeveledButtonExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          'Beveled Button',
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
        ),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: () {},
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.purple,
            foregroundColor: Colors.white,
            shape: const BeveledRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(12)),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
          ),
          child: const Text('Beveled Button'),
        ),
      ],
    );
  }
}

class BeveledCardExample extends StatelessWidget {
  const BeveledCardExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          'Beveled Card',
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
        ),
        const SizedBox(height: 8),
        Card(
          shape: const BeveledRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(16)),
            side: BorderSide(color: Colors.grey, width: 1),
          ),
          child: const Padding(
            padding: EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Card with Beveled Corners',
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 8),
                Text(
                  'This card uses BeveledRectangleBorder to create unique, '
                  'angular corners instead of the typical rounded corners.',
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

🌟 总结 (Conclusion)

在这篇博客中,我们探讨了 8 个强大但经常被忽视的 Flutter 控件或类,它们能够解决特定的开发难题。

这些控件的精妙之处不仅在于它们的功能,更在于它们如何展示了 Flutter 一致的设计模式 。无论是性能优化可访问性安全性 ,还是独特的视觉效果 ,Flutter 都提供了优雅的、基于控件的解决方案,它们能无缝地融入您的应用程序架构中。


🚀 继续您的 Flutter 之旅

在您的 Flutter 之旅中,请记住这个框架充满了这些"隐藏的珍宝"。

官方文档、源代码探索和社区讨论都是等待您去发现的解决方案的宝库 。请保持探索精神,持续构建,并且在您想要理解这些强大工具如何在幕后工作 时,请毫不犹豫地深入研究源代码


🔗 获取完整代码

本系列中所有示例的完整源代码 都可以在flutter test cases repository中找到。您可以在那里探索、实验,并在您自己的项目中以这些概念为基础进行构建。

相关推荐
C_心欲无痕3 分钟前
react - Suspense异步加载组件
前端·react.js·前端框架
JosieBook10 分钟前
【Vue】05 Vue技术——Vue 数据绑定的两种方式:单向绑定、双向绑定
前端·javascript·vue.js
想学后端的前端工程师40 分钟前
【浏览器工作原理与性能优化指南:深入理解Web性能】
前端·性能优化
程序员爱钓鱼1 小时前
Node.js 编程实战:错误处理与安全防护
前端·后端·node.js
Geoffwo1 小时前
Electron 打包后 exe 对应的 asar 解压 / 打包完整流程
前端·javascript·electron
柒@宝儿姐1 小时前
vue3中使用element-plus的el-scrollbar实现自动滚动(横向/纵横滚动)
前端·javascript·vue.js
程序员爱钓鱼1 小时前
Node.js 编程实战:模板引擎与静态资源
前端·后端·node.js
Geoffwo1 小时前
Electron打包的软件如何使用浏览器插件
前端·javascript·electron
Sui_Network1 小时前
Sui 2025→2026 直播回顾中文版
大数据·前端·人工智能·深度学习·区块链
打小就很皮...1 小时前
网页包装为桌面应用(electron版)
前端·electron