From 1744e8e0aadd8f4115140a29c407d689338bd297 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 19 Jul 2017 12:21:36 -0700 Subject: [PATCH] Expose the currently available semantic scroll actions (#11286) * Expose the currently available semantic scroll actions * review comments * add test * refactor to set --- .../flutter/lib/src/rendering/proxy_box.dart | 33 +++++++++--- .../lib/src/widgets/gesture_detector.dart | 23 +++++++++ .../lib/src/widgets/scroll_context.dart | 3 ++ .../lib/src/widgets/scroll_position.dart | 31 +++++++++++ .../flutter/lib/src/widgets/scrollable.dart | 10 ++++ .../widgets/scrollable_semantics_test.dart | 51 +++++++++++++++++++ 6 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 packages/flutter/test/widgets/scrollable_semantics_test.dart diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 90843e1160..d90f4be195 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/painting.dart'; +import 'package:collection/collection.dart'; import 'package:vector_math/vector_math_64.dart'; import 'box.dart'; @@ -2731,6 +2732,15 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA _onVerticalDragUpdate = onVerticalDragUpdate, super(child); + Set get validActions => _validActions; + Set _validActions; + set validActions(Set value) { + if (const SetEquality().equals(value, _validActions)) + return; + _validActions = value; + markNeedsSemanticsUpdate(onlyChanges: true); + } + /// Called when the user taps on the render object. GestureTapCallback get onTap => _onTap; GestureTapCallback _onTap; @@ -2802,14 +2812,25 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA SemanticsAnnotator get semanticsAnnotator => isSemanticBoundary ? _annotate : null; void _annotate(SemanticsNode node) { + List actions = []; if (onTap != null) - node.addAction(SemanticsAction.tap); + actions.add(SemanticsAction.tap); if (onLongPress != null) - node.addAction(SemanticsAction.longPress); - if (onHorizontalDragUpdate != null) - node.addHorizontalScrollingActions(); - if (onVerticalDragUpdate != null) - node.addVerticalScrollingActions(); + actions.add(SemanticsAction.longPress); + if (onHorizontalDragUpdate != null) { + actions.add(SemanticsAction.scrollRight); + actions.add(SemanticsAction.scrollLeft); + } + if (onVerticalDragUpdate != null) { + actions.add(SemanticsAction.scrollUp); + actions.add(SemanticsAction.scrollDown); + } + + // If a set of validActions has been provided only expose those. + if (validActions != null) + actions = actions.where((SemanticsAction action) => validActions.contains(action)).toList(); + + actions.forEach(node.addAction); } @override diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 5c681f35a2..ad3db0e259 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -535,6 +535,25 @@ class RawGestureDetectorState extends State { } } + void replaceSemanticsActions(Set actions) { + assert(() { + if (!context.findRenderObject().owner.debugDoingLayout) { + throw new FlutterError( + 'Unexpected call to replaceSemanticsActions() method of RawGestureDetectorState.\n' + 'The replaceSemanticsActions() method can only be called during the layout phase.' + ); + } + return true; + }); + if (!widget.excludeFromSemantics) { + final RenderSemanticsGestureHandler semanticsGestureHandler = context.findRenderObject(); + context.visitChildElements((Element element) { + final _GestureSemantics widget = element.widget; + widget._updateSemanticsActions(semanticsGestureHandler, actions); + }); + } + } + @override void dispose() { for (GestureRecognizer recognizer in _recognizers.values) @@ -714,6 +733,10 @@ class _GestureSemantics extends SingleChildRenderObjectWidget { recognizers.containsKey(PanGestureRecognizer) ? _handleVerticalDragUpdate : null; } + void _updateSemanticsActions(RenderSemanticsGestureHandler renderObject, Set actions) { + renderObject.validActions = actions; + } + @override void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) { _updateHandlers(renderObject, owner._recognizers); diff --git a/packages/flutter/lib/src/widgets/scroll_context.dart b/packages/flutter/lib/src/widgets/scroll_context.dart index 71fec6d219..ef62f6b765 100644 --- a/packages/flutter/lib/src/widgets/scroll_context.dart +++ b/packages/flutter/lib/src/widgets/scroll_context.dart @@ -56,4 +56,7 @@ abstract class ScrollContext { /// Whether the user can drag the widget, for example to initiate a scroll. void setCanDrag(bool value); + + /// Set the [SemanticsAction]s that should be expose to the semantics tree. + void setSemanticsActions(Set actions); } diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index c96ee6ea2f..0519f91131 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; @@ -367,6 +368,35 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { return true; } + Set _semanticActions; + + void _updateSemanticActions() { + SemanticsAction forward; + SemanticsAction backward; + switch (axis) { + case Axis.vertical: + forward = SemanticsAction.scrollUp; + backward = SemanticsAction.scrollDown; + break; + case Axis.horizontal: + forward = SemanticsAction.scrollLeft; + backward = SemanticsAction.scrollRight; + break; + } + + final Set actions = new Set(); + if (pixels > minScrollExtent) + actions.add(backward); + if (pixels < maxScrollExtent) + actions.add(forward); + + if (const SetEquality().equals(actions, _semanticActions)) + return; + + _semanticActions = actions; + context.setSemanticsActions(_semanticActions); + } + @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { if (_minScrollExtent != minScrollExtent || @@ -378,6 +408,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { applyNewDimensions(); _didChangeViewportDimension = false; } + _updateSemanticActions(); return true; } diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 45e06e99cf..1284b0e817 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -304,6 +304,16 @@ class ScrollableState extends State with TickerProviderStateMixin } + // SEMANTICS ACTIONS + + @override + @protected + void setSemanticsActions(Set actions) { + if (_gestureDetectorKey.currentState != null) + _gestureDetectorKey.currentState.replaceSemanticsActions(actions); + } + + // GESTURE RECOGNITION AND POINTER IGNORING final GlobalKey _gestureDetectorKey = new GlobalKey(); diff --git a/packages/flutter/test/widgets/scrollable_semantics_test.dart b/packages/flutter/test/widgets/scrollable_semantics_test.dart new file mode 100644 index 0000000000..1de20fe3a4 --- /dev/null +++ b/packages/flutter/test/widgets/scrollable_semantics_test.dart @@ -0,0 +1,51 @@ +// 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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'semantics_tester.dart'; + +void main() { + testWidgets('scrollable exposes the correct semantic actions', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + final List textWidgets = []; + for (int i = 0; i < 80; i++) + textWidgets.add(new Text('$i')); + await tester.pumpWidget(new ListView(children: textWidgets)); + + expect(semantics,includesNodeWith(actions: [SemanticsAction.scrollUp])); + + await flingUp(tester); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown])); + + await flingDown(tester, repetitions: 2); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp])); + + await flingUp(tester, repetitions: 5); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollDown])); + + await flingDown(tester); + expect(semantics, includesNodeWith(actions: [SemanticsAction.scrollUp, SemanticsAction.scrollDown])); + + }); +} + +Future flingUp(WidgetTester tester, { int repetitions: 1 }) async { + while (repetitions-- > 0) { + await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 1000.0); + await tester.pump(); + await tester.pump(const Duration(seconds: 5)); + } +} + +Future flingDown(WidgetTester tester, { int repetitions: 1 }) async { + while (repetitions-- > 0) { + await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 1000.0); + await tester.pump(); + await tester.pump(const Duration(seconds: 5)); + } +} \ No newline at end of file