Commit 1744e8e0 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Expose the currently available semantic scroll actions (#11286)

* Expose the currently available semantic scroll actions

* review comments

* add test

* refactor to set
parent bc4a3f17
......@@ -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<SemanticsAction> get validActions => _validActions;
Set<SemanticsAction> _validActions;
set validActions(Set<SemanticsAction> value) {
if (const SetEquality<SemanticsAction>().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<SemanticsAction> actions = <SemanticsAction>[];
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
......
......@@ -535,6 +535,25 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
}
}
void replaceSemanticsActions(Set<SemanticsAction> 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<SemanticsAction> actions) {
renderObject.validActions = actions;
}
@override
void updateRenderObject(BuildContext context, RenderSemanticsGestureHandler renderObject) {
_updateHandlers(renderObject, owner._recognizers);
......
......@@ -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<SemanticsAction> actions);
}
......@@ -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<SemanticsAction> _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<SemanticsAction> actions = new Set<SemanticsAction>();
if (pixels > minScrollExtent)
actions.add(backward);
if (pixels < maxScrollExtent)
actions.add(forward);
if (const SetEquality<SemanticsAction>().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;
}
......
......@@ -304,6 +304,16 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
}
// SEMANTICS ACTIONS
@override
@protected
void setSemanticsActions(Set<SemanticsAction> actions) {
if (_gestureDetectorKey.currentState != null)
_gestureDetectorKey.currentState.replaceSemanticsActions(actions);
}
// GESTURE RECOGNITION AND POINTER IGNORING
final GlobalKey<RawGestureDetectorState> _gestureDetectorKey = new GlobalKey<RawGestureDetectorState>();
......
// 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<Widget> textWidgets = <Widget>[];
for (int i = 0; i < 80; i++)
textWidgets.add(new Text('$i'));
await tester.pumpWidget(new ListView(children: textWidgets));
expect(semantics,includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));
await flingUp(tester);
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));
await flingDown(tester, repetitions: 2);
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp]));
await flingUp(tester, repetitions: 5);
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollDown]));
await flingDown(tester);
expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown]));
});
}
Future<Null> 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<Null> 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
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