Commit 3bf3df33 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by Ian Hickson

Add SemanticsEvents and send them for scrolling (#11909)

* ++

* ++

* ++

* ++

* ++

* dart docs

* test fix

* undo unintended change

* fix test

* fix test

* review feedback
parent fad36baa
......@@ -7,6 +7,7 @@ import 'dart:ui' as ui show ImageFilter, Gradient;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';
......@@ -2981,6 +2982,13 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null;
SemanticsNode _innerNode;
SemanticsNode _annotatedNode;
/// Sends a [SemanticsEvent] in the context of the [SemanticsNode] that is
/// annotated with this object's semantics information.
void sendSemanticsEvent(SemanticsEvent event) {
_annotatedNode?.sendEvent(event);
}
@override
void assembleSemanticsNode(SemanticsNode node, Iterable<SemanticsNode> children) {
......@@ -3017,6 +3025,7 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
}
void _annotate(SemanticsNode node) {
_annotatedNode = node;
List<SemanticsAction> actions = <SemanticsAction>[];
if (onTap != null)
actions.add(SemanticsAction.tap);
......
......@@ -8,12 +8,15 @@ import 'dart:ui' show Rect, SemanticsAction, SemanticsFlags;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';
import 'debug.dart';
import 'node.dart';
import 'semantics_event.dart';
export 'dart:ui' show SemanticsAction;
export 'semantics_event.dart';
/// Interface for [RenderObject]s to implement when they want to support
/// being tapped, etc.
......@@ -697,6 +700,27 @@ class SemanticsNode extends AbstractNode {
_dirty = false;
}
/// Sends a [SemanticsEvent] associated with this [SemanticsNode].
///
/// Semantics events should be sent to inform interested parties (like
/// the accessibility system of the operating system) about changes to the UI.
///
/// For example, if this semantics node represents a scrollable list, a
/// [ScrollCompletedSemanticsEvent] should be sent after a scroll action is completed.
/// That way, the operating system can give additional feedback to the user
/// about the state of the UI (e.g. on Android a ping sound is played to
/// indicate a successful scroll in accessibility mode).
void sendEvent(SemanticsEvent event) {
if (!attached)
return;
final Map<String, dynamic> annotatedEvent = <String, dynamic>{
'nodeId': id,
'type': event.type,
'data': event.toMap(),
};
SystemChannels.accessibility.send(annotatedEvent);
}
@override
String toString() {
final StringBuffer buffer = new StringBuffer();
......
// Copyright 2017 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.
/// An event that can be send by the application to notify interested listeners
/// that something happened to the user interface (e.g. a view scrolled).
///
/// These events are usually interpreted by assistive technologies to give the
/// user additional clues about the current state of the UI.
abstract class SemanticsEvent {
/// Initializes internal fields.
///
/// [type] is a string that identifies this class of [SemanticsEvent]s.
SemanticsEvent(this.type);
/// The type of this event.
///
/// The type is used by the engine to translate this event into the
/// appropriate native event (`UIAccessibility*Notification` on iOS and
/// `AccessibilityEvent` on Android).
final String type;
/// Converts this event to a Map that can be encoded with
/// [StandardMessageCodec].
Map<String, dynamic> toMap();
}
/// Notifies that a scroll action has been completed.
///
/// This event translates into a `AccessibilityEvent.TYPE_VIEW_SCROLLED` on
/// Android and a `UIAccessibilityPageScrolledNotification` on iOS. It is
/// processed by the accessibility systems of the operating system to provide
/// additional feedback to the user about the state of a scrollable view (e.g.
/// on Android, a ping sound is played to indicate that a scroll action was
/// successful).
class ScrollCompletedSemanticsEvent extends SemanticsEvent {
/// Creates a [ScrollCompletedSemanticsEvent].
///
/// This event should be sent after a scroll action is completed. It is
/// interpreted by assistive technologies to provide additional feedback about
/// the just completed scroll action to the user.
// TODO(goderbauer): add more metadata to this event (e.g. how far are we scrolled?).
ScrollCompletedSemanticsEvent() : super('scroll');
@override
Map<String, dynamic> toMap() => <String, dynamic>{};
}
......@@ -54,4 +54,15 @@ class SystemChannels {
const JSONMessageCodec(),
);
/// A [BasicMessageChannel] for accessibility events.
///
/// See also:
///
/// * [SemanticsEvents] and its subclasses for a list of valid accessibility
/// events that can be sent over this channel.
static const BasicMessageChannel<dynamic> accessibility = const BasicMessageChannel<dynamic>(
'flutter/accessibility',
const StandardMessageCodec(),
);
}
......@@ -557,10 +557,21 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
});
if (!widget.excludeFromSemantics) {
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
context.visitChildElements((Element element) {
final _GestureSemantics widget = element.widget;
widget._updateSemanticsActions(semanticsGestureHandler, actions);
});
semanticsGestureHandler.validActions = actions;
}
}
/// Sends a [SemanticsEvent] in the context of the [SemanticsNode] that is
/// annotated with this object's semantics information.
///
/// The event can be interpreted by assistive technologies to provide
/// additional feedback to the user about the state of the UI.
///
/// The event will not be sent if [excludeFromSemantics] is set to `true`.
void sendSemanticsEvent(SemanticsEvent event) {
if (!widget.excludeFromSemantics) {
final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject();
semanticsGestureHandler.sendSemanticsEvent(event);
}
}
......@@ -735,10 +746,6 @@ class _GestureSemantics extends SingleChildRenderObjectWidget {
recognizers.containsKey(PanGestureRecognizer) ? owner._handleSemanticsVerticalDragUpdate : null;
}
void _updateSemanticsActions(RenderSemanticsGestureHandler renderObject, Set<SemanticsAction> actions) {
renderObject.validActions = actions;
}
@override
void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) {
_updateHandlers(renderObject);
......
......@@ -260,6 +260,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
final ScrollPosition oldPosition = position;
if (oldPosition != null) {
controller?.detach(oldPosition);
oldPosition.removeListener(_sendSemanticsScrollEvent);
// It's important that we not dispose the old position until after the
// viewport has had a chance to unregister its listeners from the old
// position. So, schedule a microtask to do it.
......@@ -268,11 +269,24 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
_position = controller?.createScrollPosition(_physics, this, oldPosition)
?? new ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: oldPosition);
_position.addListener(_sendSemanticsScrollEvent);
assert(position != null);
controller?.attach(position);
}
bool _semanticsScrollEventScheduled = false;
void _sendSemanticsScrollEvent() {
if (_semanticsScrollEventScheduled)
return;
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
_gestureDetectorKey.currentState?.sendSemanticsEvent(new ScrollCompletedSemanticsEvent());
_semanticsScrollEventScheduled = false;
});
_semanticsScrollEventScheduled = true;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
......
......@@ -5,6 +5,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'semantics_tester.dart';
......@@ -195,6 +196,36 @@ void main() {
await tester.pump(const Duration(seconds: 5));
expect(tester.getTopLeft(find.byWidget(semantics[1])).dy, kToolbarHeight);
});
testWidgets('scrolling sends ScrollCompletedSemanticsEvent', (WidgetTester tester) async {
final List<dynamic> messages = <dynamic>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
messages.add(message);
});
final SemanticsTester semantics = new SemanticsTester(tester);
final List<Widget> textWidgets = <Widget>[];
for (int i = 0; i < 80; i++)
textWidgets.add(new Text('$i'));
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new ListView(children: textWidgets),
));
await flingUp(tester);
expect(messages, isNot(hasLength(0)));
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
messages.clear();
await flingDown(tester);
expect(messages, isNot(hasLength(0)));
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
semantics.dispose();
});
}
Future<Null> flingUp(WidgetTester tester, { int repetitions: 1 }) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment