mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
350 lines
12 KiB
Dart
350 lines
12 KiB
Dart
// 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
|
|
*/
|