mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add semantics traversal order example (#14174)
This commit is contained in:
parent
14309b9311
commit
66febf2663
349
examples/catalog/lib/custom_a11y_traversal.dart
Normal file
349
examples/catalog/lib/custom_a11y_traversal.dart
Normal file
@ -0,0 +1,349 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
|
||||
/// This example shows a set of widgets for changing data fields arranged in a column
|
||||
/// of rows but, in accessibility mode, are traversed in a custom order.
|
||||
///
|
||||
/// This demonstrates how Flutter's accessibility system describes custom traversal
|
||||
/// orders using sort keys.
|
||||
///
|
||||
/// The example app here has three fields that have a title and up/down spinner buttons
|
||||
/// above and below. The traversal order should allow the user to focus on each
|
||||
/// title, the input field next, the up spinner next, and the down spinner last before
|
||||
/// moving to the next input title.
|
||||
///
|
||||
/// Users that do not use a screen reader (e.g. TalkBack on Android and VoiceOver on iOS)
|
||||
/// will just see a regular app with controls.
|
||||
///
|
||||
/// The example's [RowColumnTraversal] widget sets up two [Semantics] objects that wrap the
|
||||
/// given [Widget] child, providing the traversal order they should have in the "row"
|
||||
/// direction, and then the traversal order they should have in the "column" direction.
|
||||
///
|
||||
/// Sort keys, by default, are appended to the sort orders for their parents, but
|
||||
/// they can also override those of their parents (by setting
|
||||
/// [SemanticsSortOrder.discardParentOrder] to true), and an entire sort order can be
|
||||
/// defined with multiple keys, to provide for virtually any ordering.
|
||||
///
|
||||
/// Keys at the same position in the sort order are compared with each other, and
|
||||
/// keys which are of different types, or which have different [SemanticSortKey.name]
|
||||
/// values compare as "equal" so that two different types of keys can co-exist at the
|
||||
/// same level and not interfere with each other, allowing for sorting in groups.
|
||||
/// Keys that evaluate as equal, or when compared with Widgets that don't have
|
||||
/// [Semantics], are given the default upper-start-to-lower-end geometric
|
||||
/// ordering.
|
||||
///
|
||||
/// Since widgets are globally sorted by their sort key, the order does not have
|
||||
/// to conform to the widget hierarchy. Indeed, in this example, we traverse vertically
|
||||
/// first, but the widget hierarchy is a column of rows.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Semantics] for an object that annotates widgets with accessibility semantics
|
||||
/// (including traversal order).
|
||||
/// * [SemanticSortOrder] for the class that manages the sort order of a semantic node.
|
||||
/// * [SemanticSortKey] for the base class of all semantic sort keys.
|
||||
/// * [OrdinalSortKey] for a concrete sort key that sorts based on the given ordinal.
|
||||
class RowColumnTraversal extends StatelessWidget {
|
||||
const RowColumnTraversal({this.rowOrder, this.columnOrder, this.child});
|
||||
|
||||
final int rowOrder;
|
||||
final int columnOrder;
|
||||
final Widget child;
|
||||
|
||||
/// Builds a widget hierarchy that wraps [child].
|
||||
///
|
||||
/// This function expresses the sort keys as a hierarchy, but it could just as
|
||||
/// easily have been a flat list:
|
||||
///
|
||||
/// ```
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return new Semantics(
|
||||
/// sortOrder: new SemanticsSortOrder(
|
||||
/// keys: <SemanticsSortKey>[
|
||||
/// new OrdinalSortKey(columnOrder.toDouble()),
|
||||
/// new OrdinalSortKey(rowOrder.toDouble())
|
||||
/// ],
|
||||
/// ),
|
||||
/// child: child,
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The resulting order is the same.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Semantics(
|
||||
sortKey: new OrdinalSortKey(columnOrder.toDouble()),
|
||||
child: new Semantics(
|
||||
sortKey: new OrdinalSortKey(rowOrder.toDouble()),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- Component widgets ---------------------
|
||||
|
||||
/// A Button class that wraps an [IconButton] with a [RowColumnTraversal] to
|
||||
/// set its traversal order.
|
||||
class SpinnerButton extends StatelessWidget {
|
||||
const SpinnerButton(
|
||||
{Key key,
|
||||
this.onPressed,
|
||||
this.icon,
|
||||
this.rowOrder,
|
||||
this.columnOrder,
|
||||
this.field,
|
||||
this.increment}) : super(key: key);
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final IconData icon;
|
||||
final int rowOrder;
|
||||
final int columnOrder;
|
||||
final Field field;
|
||||
final bool increment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String label = '${increment ? 'Increment' : 'Decrement'} ${_fieldToName(field)}';
|
||||
|
||||
return new RowColumnTraversal(
|
||||
rowOrder: rowOrder,
|
||||
columnOrder: columnOrder,
|
||||
child: new Center(
|
||||
child: new IconButton(
|
||||
icon: new Icon(icon),
|
||||
onPressed: onPressed,
|
||||
tooltip: label,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A text entry field that wraps a [TextField] with a [RowColumnTraversal] to
|
||||
/// set its traversal order.
|
||||
class FieldWidget extends StatelessWidget {
|
||||
const FieldWidget({
|
||||
Key key,
|
||||
this.rowOrder,
|
||||
this.columnOrder,
|
||||
this.onIncrease,
|
||||
this.onDecrease,
|
||||
this.value,
|
||||
this.field,
|
||||
}) : super(key: key);
|
||||
|
||||
final int rowOrder;
|
||||
final int columnOrder;
|
||||
final VoidCallback onDecrease;
|
||||
final VoidCallback onIncrease;
|
||||
final int value;
|
||||
final Field field;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String stringValue = '${_fieldToName(field)} $value';
|
||||
final String increasedValue = '${_fieldToName(field)} ${value + 1}';
|
||||
final String decreasedValue = '${_fieldToName(field)} ${value - 1}';
|
||||
|
||||
return new RowColumnTraversal(
|
||||
rowOrder: rowOrder,
|
||||
columnOrder: columnOrder,
|
||||
child: new Center(
|
||||
child: new Semantics(
|
||||
onDecrease: onDecrease,
|
||||
onIncrease: onIncrease,
|
||||
value: stringValue,
|
||||
increasedValue: increasedValue,
|
||||
decreasedValue: decreasedValue,
|
||||
child: new ExcludeSemantics(child: new Text(value.toString())),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- Field manipulation functions ---------------------
|
||||
|
||||
/// An enum that describes which column we're referring to.
|
||||
enum Field { DOGS, CATS, FISH }
|
||||
|
||||
String _fieldToName(Field field) {
|
||||
switch (field) {
|
||||
case Field.DOGS:
|
||||
return 'Dogs';
|
||||
case Field.CATS:
|
||||
return 'Cats';
|
||||
case Field.FISH:
|
||||
return 'Fish';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --------------- Main app ---------------------
|
||||
|
||||
/// The top-level example widget that serves as the body of the app.
|
||||
class CustomTraversalExample extends StatefulWidget {
|
||||
@override
|
||||
CustomTraversalExampleState createState() => new CustomTraversalExampleState();
|
||||
}
|
||||
|
||||
/// The state object for the top level example widget.
|
||||
class CustomTraversalExampleState extends State<CustomTraversalExample> {
|
||||
/// The fields that we are manipulating. List indices correspond to
|
||||
/// the entries in the [Field] enum.
|
||||
List<int> fields = <int>[0, 0, 0];
|
||||
|
||||
void _addToField(Field field, int delta) {
|
||||
setState(() {
|
||||
fields[field.index] += delta;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _makeFieldHeader(int rowOrder, int columnOrder, Field field) {
|
||||
return new RowColumnTraversal(
|
||||
rowOrder: rowOrder,
|
||||
columnOrder: columnOrder,
|
||||
child: new Text(_fieldToName(field)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _makeSpinnerButton(int rowOrder, int columnOrder, Field field, {bool increment: true}) {
|
||||
return new SpinnerButton(
|
||||
rowOrder: rowOrder,
|
||||
columnOrder: columnOrder,
|
||||
icon: increment ? Icons.arrow_upward : Icons.arrow_downward,
|
||||
onPressed: () => _addToField(field, increment ? 1 : -1),
|
||||
field: field,
|
||||
increment: increment,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _makeEntryField(int rowOrder, int columnOrder, Field field) {
|
||||
return new FieldWidget(
|
||||
rowOrder: rowOrder,
|
||||
columnOrder: columnOrder,
|
||||
onIncrease: () => _addToField(field, 1),
|
||||
onDecrease: () => _addToField(field, -1),
|
||||
value: fields[field.index],
|
||||
field: field,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new MaterialApp(
|
||||
home: new Scaffold(
|
||||
appBar: new AppBar(
|
||||
title: const Text('Pet Inventory'),
|
||||
),
|
||||
body: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
return new DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: 21.0),
|
||||
child: new Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
// Since this is the only sort key that the text has, it
|
||||
// will be compared with the 'column' OrdinalSortKeys of all the
|
||||
// fields, because the column sort keys are first in the other fields.
|
||||
//
|
||||
// An ordinal of "0.0" means that it will be traversed before column 1.
|
||||
sortKey: const OrdinalSortKey(0.0),
|
||||
child: const Text(
|
||||
'How many pets do you own?',
|
||||
),
|
||||
),
|
||||
const Padding(padding: const EdgeInsets.symmetric(vertical: 10.0)),
|
||||
new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
_makeFieldHeader(1, 0, Field.DOGS),
|
||||
_makeFieldHeader(1, 1, Field.CATS),
|
||||
_makeFieldHeader(1, 2, Field.FISH),
|
||||
],
|
||||
),
|
||||
new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
_makeSpinnerButton(3, 0, Field.DOGS, increment: true),
|
||||
_makeSpinnerButton(3, 1, Field.CATS, increment: true),
|
||||
_makeSpinnerButton(3, 2, Field.FISH, increment: true),
|
||||
],
|
||||
),
|
||||
new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
_makeEntryField(2, 0, Field.DOGS),
|
||||
_makeEntryField(2, 1, Field.CATS),
|
||||
_makeEntryField(2, 2, Field.FISH),
|
||||
],
|
||||
),
|
||||
new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: <Widget>[
|
||||
_makeSpinnerButton(4, 0, Field.DOGS, increment: false),
|
||||
_makeSpinnerButton(4, 1, Field.CATS, increment: false),
|
||||
_makeSpinnerButton(4, 2, Field.FISH, increment: false),
|
||||
],
|
||||
),
|
||||
const Padding(padding: const EdgeInsets.symmetric(vertical: 10.0)),
|
||||
new Semantics(
|
||||
// Since this is the only sort key that the reset button has, it
|
||||
// will be compared with the 'column' OrdinalSortKeys of all the
|
||||
// fields, because the column sort keys are first in the other fields.
|
||||
//
|
||||
// an ordinal of "5.0" means that it will be traversed after column 4.
|
||||
sortKey: const OrdinalSortKey(5.0),
|
||||
child: new MaterialButton(
|
||||
child: const Text('RESET'),
|
||||
textTheme: ButtonTextTheme.normal,
|
||||
textColor: Colors.blue,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
fields = <int>[0, 0, 0];
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runApp(new CustomTraversalExample());
|
||||
}
|
||||
|
||||
/*
|
||||
Sample Catalog
|
||||
|
||||
Title: CustomTraversalExample
|
||||
|
||||
Summary: An app that demonstrates a custom semantics traversal order.
|
||||
|
||||
Description:
|
||||
This app presents a value selection interface where the fields can be
|
||||
incremented or decremented using spinner arrows. In accessibility mode, the
|
||||
widgets are traversed in a custom order from one column to the next, starting
|
||||
with the column title, moving to the input field, then to the "up" increment
|
||||
button, and lastly to the "down" decrement button.
|
||||
|
||||
When not in accessibility mode, the app works as one would expect.
|
||||
|
||||
Classes: Semantics
|
||||
|
||||
Sample: CustomTraversalExample
|
||||
*/
|
@ -0,0 +1,6 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="catalog - custom_a11y_traversal" type="FlutterRunConfigurationType" factoryName="Flutter">
|
||||
<option name="filePath" value="$PROJECT_DIR$/examples/catalog/lib/custom_a11y_traversal.dart" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
Loading…
Reference in New Issue
Block a user