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中找到。您可以在那里探索、实验,并在您自己的项目中以这些概念为基础进行构建。

相关推荐
学Linux的语莫3 小时前
Vue前端知识
前端·javascript·vue.js
BUG创建者3 小时前
thee.js完成线上展厅demo
开发语言·前端·javascript·css·html·css3·three.js
LYFlied3 小时前
前端开发者需要掌握的编译原理相关知识及优化点
前端·javascript·webpack·性能优化·编译原理·babel·打包编译
BlackWolfSky3 小时前
ES6 学习笔记3—7数值的扩展、8函数的扩展
前端·javascript·笔记·学习·es6
未来之窗软件服务3 小时前
幽冥大陆(四十四)源码找回之Vue——东方仙盟筑基期
前端·javascript·vue.js·仙盟创梦ide·东方仙盟·源码提取·源码丢失
我有一棵树3 小时前
css 的回溯机制、CSS 层级过深的选择器会影响浏览器的性能
前端·css
|晴 天|3 小时前
企业级中后台管理系统前端架构设计:从单体到模块化的演进之路
前端
AI云原生3 小时前
《开箱即用的高性能:openEuler 默认配置下的 Web 服务性能评测》
运维·前端·docker·云原生·开源·开源软件·开源协议