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