Expose the currently available semantic scroll actions (#11286)
* Expose the currently available semantic scroll actions * review comments * add test * refactor to set
This commit is contained in:
parent
bc4a3f1703
commit
1744e8e0aa
@ -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>();
|
||||
|
51
packages/flutter/test/widgets/scrollable_semantics_test.dart
Normal file
51
packages/flutter/test/widgets/scrollable_semantics_test.dart
Normal file
@ -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<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));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user