Unverified Commit 72517f0a authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Encode scrolling status into tree (#14536)

parent 248919fa
a031239a5d4e44e60d0ebc62b8c544a9f592fc22
8ac6f6efa177fb548dcdc81f1501f060b2ad1115
......@@ -12,7 +12,7 @@ bool nearEqual(double a, double b, double epsilon) {
assert(epsilon >= 0.0);
if (a == null || b == null)
return a == b;
return (a > (b - epsilon)) && (a < (b + epsilon));
return (a > (b - epsilon)) && (a < (b + epsilon)) || a == b;
}
/// Whether a double is within a given distance of zero.
......
......@@ -95,6 +95,9 @@ class SemanticsData extends Diagnosticable {
@required this.nextNodeId,
@required this.rect,
@required this.textSelection,
@required this.scrollPosition,
@required this.scrollExtentMax,
@required this.scrollExtentMin,
this.tags,
this.transform,
}) : assert(flags != null),
......@@ -156,6 +159,38 @@ class SemanticsData extends Diagnosticable {
/// if this node represents a text field.
final TextSelection textSelection;
/// Indicates the current scrolling position in logical pixels if the node is
/// scrollable.
///
/// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid
/// in-range values for this property. The value for [scrollPosition] may
/// (temporarily) be outside that range, e.g. during an overscroll.
///
/// See also:
///
/// * [ScrollPosition.pixels], from where this value is usually taken.
final double scrollPosition;
/// Indicates the maximum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.maxScrollExtent], from where this value is usually taken.
final double scrollExtentMax;
/// Indicates the mimimum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.minScrollExtent], from where this value is usually taken.
final double scrollExtentMin;
/// The bounding box for this node in its coordinate system.
final Rect rect;
......@@ -204,7 +239,10 @@ class SemanticsData extends Diagnosticable {
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new IntProperty('nextNodeId', nextNodeId, defaultValue: null));
if (textSelection?.isValid == true)
properties.add(new MessageProperty('text selection', '[${textSelection.start}, ${textSelection.end}]'));
properties.add(new MessageProperty('textSelection', '[${textSelection.start}, ${textSelection.end}]'));
properties.add(new DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(new DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(new DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
}
@override
......@@ -224,11 +262,14 @@ class SemanticsData extends Diagnosticable {
&& typedOther.rect == rect
&& setEquals(typedOther.tags, tags)
&& typedOther.textSelection == textSelection
&& typedOther.scrollPosition == scrollPosition
&& typedOther.scrollExtentMax == scrollExtentMax
&& typedOther.scrollExtentMin == scrollExtentMin
&& typedOther.transform == transform;
}
@override
int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, nextNodeId, rect, tags, textSelection, transform);
int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, nextNodeId, rect, tags, textSelection, scrollPosition, scrollExtentMax, scrollExtentMin, transform);
}
class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
......@@ -915,6 +956,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_textDirection != config.textDirection ||
_sortOrder != config._sortOrder ||
_textSelection != config._textSelection ||
_scrollPosition != config._scrollPosition ||
_scrollExtentMax != config._scrollExtentMax ||
_scrollExtentMin != config._scrollExtentMin ||
_actionsAsBits != config._actionsAsBits ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
}
......@@ -1011,6 +1055,42 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
TextSelection get textSelection => _textSelection;
TextSelection _textSelection;
/// Indicates the current scrolling position in logical pixels if the node is
/// scrollable.
///
/// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid
/// in-range values for this property. The value for [scrollPosition] may
/// (temporarily) be outside that range, e.g. during an overscroll.
///
/// See also:
///
/// * [ScrollPosition.pixels], from where this value is usually taken.
double get scrollPosition => _scrollPosition;
double _scrollPosition;
/// Indicates the maximum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.maxScrollExtent], from where this value is usually taken.
double get scrollExtentMax => _scrollExtentMax;
double _scrollExtentMax;
/// Indicates the mimimum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.minScrollExtent] from where this value is usually taken.
double get scrollExtentMin => _scrollExtentMin;
double _scrollExtentMin;
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
static final SemanticsConfiguration _kEmptyConfig = new SemanticsConfiguration();
......@@ -1043,6 +1123,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
_actionsAsBits = config._actionsAsBits;
_textSelection = config._textSelection;
_scrollPosition = config._scrollPosition;
_scrollExtentMax = config._scrollExtentMax;
_scrollExtentMin = config._scrollExtentMin;
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
......@@ -1074,6 +1157,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
int nextNodeId = _nextNodeId;
Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
TextSelection textSelection = _textSelection;
double scrollPosition = _scrollPosition;
double scrollExtentMax = _scrollExtentMax;
double scrollExtentMin = _scrollExtentMin;
if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) {
......@@ -1083,6 +1169,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
textDirection ??= node._textDirection;
nextNodeId ??= node._nextNodeId;
textSelection ??= node._textSelection;
scrollPosition ??= node._scrollPosition;
scrollExtentMax ??= node._scrollExtentMax;
scrollExtentMin ??= node._scrollExtentMin;
if (value == '' || value == null)
value = node._value;
if (increasedValue == '' || increasedValue == null)
......@@ -1123,6 +1212,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
transform: transform,
tags: mergedTags,
textSelection: textSelection,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
);
}
......@@ -1160,6 +1252,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
nextNodeId: data.nextNodeId,
textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
scrollPosition: data.scrollPosition != null ? data.scrollPosition : double.nan,
scrollExtentMax: data.scrollExtentMax != null ? data.scrollExtentMax : double.nan,
scrollExtentMin: data.scrollExtentMin != null ? data.scrollExtentMin : double.nan,
transform: data.transform?.storage ?? _kIdentityTransform,
children: children,
);
......@@ -1232,6 +1327,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(new DiagnosticsProperty<SemanticsSortOrder>('sortOrder', sortOrder, defaultValue: null));
if (_textSelection?.isValid == true)
properties.add(new MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]'));
properties.add(new DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(new DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(new DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
}
/// Returns a string representation of this node and its descendants.
......@@ -2088,6 +2186,56 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true;
}
/// Indicates the current scrolling position in logical pixels if the node is
/// scrollable.
///
/// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid
/// in-range values for this property. The value for [scrollPosition] may
/// (temporarily) be outside that range, e.g. during an overscroll.
///
/// See also:
///
/// * [ScrollPosition.pixels], from where this value is usually taken.
double get scrollPosition => _scrollPosition;
double _scrollPosition;
set scrollPosition(double value) {
assert(value != null);
_scrollPosition = value;
_hasBeenAnnotated = true;
}
/// Indicates the maximum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.maxScrollExtent], from where this value is usually taken.
double get scrollExtentMax => _scrollExtentMax;
double _scrollExtentMax;
set scrollExtentMax(double value) {
assert(value != null);
_scrollExtentMax = value;
_hasBeenAnnotated = true;
}
/// Indicates the minimum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.minScrollExtent], from where this value is usually taken.
double get scrollExtentMin => _scrollExtentMin;
double _scrollExtentMin;
set scrollExtentMin(double value) {
assert(value != null);
_scrollExtentMin = value;
_hasBeenAnnotated = true;
}
// TAGS
/// The set of tags that this configuration wants to add to all child
......@@ -2171,6 +2319,9 @@ class SemanticsConfiguration {
_actionsAsBits |= other._actionsAsBits;
_flags |= other._flags;
_textSelection ??= other._textSelection;
_scrollPosition ??= other._scrollPosition;
_scrollExtentMax ??= other._scrollExtentMax;
_scrollExtentMin ??= other._scrollExtentMin;
textDirection ??= other.textDirection;
_sortOrder = _sortOrder?.merge(other._sortOrder);
......@@ -2214,6 +2365,9 @@ class SemanticsConfiguration {
.._flags = _flags
.._tagsForChildren = _tagsForChildren
.._textSelection = _textSelection
.._scrollPosition = _scrollPosition
.._scrollExtentMax = _scrollExtentMax
.._scrollExtentMin = _scrollExtentMin
.._actionsAsBits = _actionsAsBits
.._actions.addAll(_actions);
}
......
......@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
/// An event sent by the application to notify interested listeners that
......@@ -53,75 +52,6 @@ abstract class SemanticsEvent {
}
}
/// 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.
///
/// The parameters [axis], [pixels], [minScrollExtent], and [maxScrollExtent] are
/// required and may not be null.
ScrollCompletedSemanticsEvent({
@required this.axis,
@required this.pixels,
@required this.maxScrollExtent,
@required this.minScrollExtent
}) : assert(axis != null),
assert(pixels != null),
assert(maxScrollExtent != null),
assert(minScrollExtent != null),
super('scroll');
/// The axis in which the scroll view was scrolled.
///
/// See also [ScrollPosition.axis].
final Axis axis;
/// The current scroll position, in logical pixels.
///
/// See also [ScrollPosition.pixels].
final double pixels;
/// The minimum in-range value for [pixels].
///
/// See also [ScrollPosition.minScrollExtent].
final double minScrollExtent;
/// The maximum in-range value for [pixels].
///
/// See also [ScrollPosition.maxScrollExtent].
final double maxScrollExtent;
@override
Map<String, dynamic> getDataMap() {
final Map<String, dynamic> map = <String, dynamic>{
'pixels': pixels.clamp(minScrollExtent, maxScrollExtent),
'minScrollExtent': minScrollExtent,
'maxScrollExtent': maxScrollExtent,
};
switch (axis) {
case Axis.horizontal:
map['axis'] = 'h';
break;
case Axis.vertical:
map['axis'] = 'v';
break;
}
return map;
}
}
/// An event for a semantic announcement.
///
/// This should be used for announcement that are not seamlessly announced by
......
......@@ -59,11 +59,15 @@ abstract class ScrollMetrics {
/// The minimum in-range value for [pixels].
///
/// The actual [pixels] value might be [outOfRange].
///
/// This value can be negative infinity, if the scroll is unbounded.
double get minScrollExtent;
/// The maximum in-range value for [pixels].
///
/// The actual [pixels] value might be [outOfRange].
///
/// This value can be infinity, if the scroll is unbounded.
double get maxScrollExtent;
/// The current scroll position, in logical pixels along the [axisDirection].
......@@ -140,4 +144,4 @@ class FixedScrollMetrics extends ScrollMetrics {
String toString() {
return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)})';
}
}
\ No newline at end of file
}
......@@ -275,7 +275,6 @@ 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.
......@@ -284,30 +283,10 @@ 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) {
final _RenderExcludableScrollSemantics render = _excludableScrollSemanticsKey.currentContext?.findRenderObject();
render?.sendSemanticsEvent(new ScrollCompletedSemanticsEvent(
axis: position.axis,
pixels: position.pixels,
minScrollExtent: position.minScrollExtent,
maxScrollExtent: position.maxScrollExtent,
));
_semanticsScrollEventScheduled = false;
});
_semanticsScrollEventScheduled = true;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
......@@ -529,6 +508,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
result = new _ExcludableScrollSemantics(
key: _excludableScrollSemanticsKey,
child: result,
position: position,
);
}
......@@ -557,28 +537,61 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
/// node, which is annotated with the scrolling actions, will house the
/// scrollable children.
class _ExcludableScrollSemantics extends SingleChildRenderObjectWidget {
const _ExcludableScrollSemantics({ Key key, Widget child }) : super(key: key, child: child);
const _ExcludableScrollSemantics({
Key key,
@required this.position,
Widget child
}) : assert(position != null), super(key: key, child: child);
final ScrollPosition position;
@override
_RenderExcludableScrollSemantics createRenderObject(BuildContext context) => new _RenderExcludableScrollSemantics(position: position);
@override
_RenderExcludableScrollSemantics createRenderObject(BuildContext context) => new _RenderExcludableScrollSemantics();
void updateRenderObject(BuildContext context, _RenderExcludableScrollSemantics renderObject) {
renderObject.position = position;
}
}
class _RenderExcludableScrollSemantics extends RenderProxyBox {
_RenderExcludableScrollSemantics({ RenderBox child }) : super(child);
_RenderExcludableScrollSemantics({
@required ScrollPosition position,
RenderBox child,
}) : _position = position, assert(position != null), super(child) {
position.addListener(markNeedsSemanticsUpdate);
}
/// Whether this render object is excluded from the semantic tree.
ScrollPosition get position => _position;
ScrollPosition _position;
set position(ScrollPosition value) {
assert(value != null);
if (value == _position)
return;
_position.removeListener(markNeedsSemanticsUpdate);
_position = value;
_position.addListener(markNeedsSemanticsUpdate);
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
if (position.haveDimensions) {
config
..scrollPosition = _position.pixels
..scrollExtentMax = _position.maxScrollExtent
..scrollExtentMin = _position.minScrollExtent;
}
}
SemanticsNode _innerNode;
SemanticsNode _annotatedNode;
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) {
_annotatedNode = node;
super.assembleSemanticsNode(node, config, children);
return;
}
......@@ -587,7 +600,6 @@ class _RenderExcludableScrollSemantics extends RenderProxyBox {
_innerNode
..isMergedIntoParent = node.isPartOfNodeMerging
..rect = Offset.zero & node.rect.size;
_annotatedNode = _innerNode;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode];
final List<SemanticsNode> included = <SemanticsNode>[];
......@@ -606,12 +618,5 @@ class _RenderExcludableScrollSemantics extends RenderProxyBox {
void clearSemantics() {
super.clearSemantics();
_innerNode = null;
_annotatedNode = null;
}
/// 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);
}
}
// 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/physics.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('nearEquals', () {
expect(nearEqual(double.infinity, double.infinity, 0.1), isTrue);
expect(nearEqual(double.negativeInfinity, double.negativeInfinity, 0.1), isTrue);
expect(nearEqual(double.infinity, double.negativeInfinity, 0.1), isFalse);
expect(nearEqual(0.1, 0.11, 0.001), isFalse);
expect(nearEqual(0.1, 0.11, 0.1), isTrue);
expect(nearEqual(0.1, 0.1, 0.0000001), isTrue);
});
}
......@@ -348,7 +348,7 @@ void main() {
expect(
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
'SemanticsNode#1(owner: null, isMergedIntoParent: false, mergeAllDescendantsIntoThisNode: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isInMutuallyExcusiveGroup: false, isSelected: false, isFocused: false, isButton: false, isTextField: false, invisible, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null, nextNodeId: null, sortOrder: null)\n'
'SemanticsNode#1(owner: null, isMergedIntoParent: false, mergeAllDescendantsIntoThisNode: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isInMutuallyExcusiveGroup: false, isSelected: false, isFocused: false, isButton: false, isTextField: false, invisible, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null, nextNodeId: null, sortOrder: null, scrollExtentMin: null, scrollPosition: null, scrollExtentMax: null)\n'
);
final SemanticsConfiguration config = new SemanticsConfiguration()
......@@ -516,4 +516,4 @@ class TestRender extends RenderProxyBox {
class CustomSortKey extends OrdinalSortKey {
const CustomSortKey(double order, {String name}) : super(order, name: name);
}
\ No newline at end of file
}
......@@ -5,7 +5,6 @@
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';
......@@ -204,12 +203,7 @@ void main() {
expect(tester.getTopLeft(find.byWidget(children[1])).dy, kToolbarHeight);
});
testWidgets('vertical scrolling sends ScrollCompletedSemanticsEvent', (WidgetTester tester) async {
final List<dynamic> messages = <dynamic>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
messages.add(message);
});
testWidgets('correct scrollProgress', (WidgetTester tester) async {
semantics = new SemanticsTester(tester);
final List<Widget> textWidgets = <Widget>[];
......@@ -220,74 +214,84 @@ void main() {
child: new ListView(children: textWidgets),
));
await flingUp(tester);
expect(messages, isNot(hasLength(0)));
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
scrollPosition: 0.0,
scrollExtentMax: 520.0,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
],
));
Map<Object, Object> message = messages.last['data'];
expect(message['axis'], 'v');
expect(message['pixels'], isPositive);
expect(message['minScrollExtent'], 0.0);
expect(message['maxScrollExtent'], 520.0);
await flingUp(tester);
messages.clear();
await flingDown(tester);
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
scrollPosition: 380.2,
scrollExtentMax: 520.0,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
],
));
expect(messages, isNot(hasLength(0)));
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
await flingUp(tester);
message = messages.last['data'];
expect(message['axis'], 'v');
expect(message['pixels'], isNonNegative);
expect(message['minScrollExtent'], 0.0);
expect(message['maxScrollExtent'], 520.0);
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
scrollPosition: 520.0,
scrollExtentMax: 520.0,
actions: <SemanticsAction>[
SemanticsAction.scrollDown,
],
));
});
testWidgets('horizontal scrolling sends ScrollCompletedSemanticsEvent', (WidgetTester tester) async {
final List<dynamic> messages = <dynamic>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) {
messages.add(message);
});
testWidgets('correct scrollProgress for unbound', (WidgetTester tester) async {
semantics = new SemanticsTester(tester);
final List<Widget> children = <Widget>[];
for (int i = 0; i < 80; i++)
children.add(new Container(
child: new Text('$i'),
width: 100.0,
));
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new ListView(
children: children,
scrollDirection: Axis.horizontal,
child: new ListView.builder(
itemExtent: 20.0,
itemBuilder: (BuildContext context, int index) {
return new Text('entry $index');
},
),
));
await flingLeft(tester);
expect(messages, isNot(hasLength(0)));
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
scrollPosition: 0.0,
scrollExtentMax: double.infinity,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
],
));
Map<Object, Object> message = messages.last['data'];
expect(message['axis'], 'h');
expect(message['pixels'], isPositive);
expect(message['minScrollExtent'], 0.0);
expect(message['maxScrollExtent'], 7200.0);
await flingUp(tester);
messages.clear();
await flingRight(tester);
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
scrollPosition: 380.2,
scrollExtentMax: double.infinity,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
],
));
expect(messages, isNot(hasLength(0)));
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
await flingUp(tester);
message = messages.last['data'];
expect(message['axis'], 'h');
expect(message['pixels'], isNonNegative);
expect(message['minScrollExtent'], 0.0);
expect(message['maxScrollExtent'], 7200.0);
expect(semantics, includesNodeWith(
scrollExtentMin: 0.0,
scrollPosition: 760.4,
scrollExtentMax: double.infinity,
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
],
));
});
testWidgets('Semantics tree is populated mid-scroll', (WidgetTester tester) async {
......
......@@ -5,6 +5,7 @@
import 'dart:ui' show SemanticsFlag;
import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
......@@ -369,6 +370,9 @@ class SemanticsTester {
TextDirection textDirection,
List<SemanticsAction> actions,
List<SemanticsFlag> flags,
double scrollPosition,
double scrollExtentMax,
double scrollExtentMin,
SemanticsNode ancestor,
}) {
bool checkNode(SemanticsNode node) {
......@@ -390,6 +394,12 @@ class SemanticsTester {
if (expectedFlags != actualFlags)
return false;
}
if (scrollPosition != null && !nearEqual(node.scrollPosition, scrollPosition, 0.1))
return false;
if (scrollExtentMax != null && !nearEqual(node.scrollExtentMax, scrollExtentMax, 0.1))
return false;
if (scrollExtentMin != null && !nearEqual(node.scrollExtentMin, scrollExtentMin, 0.1))
return false;
return true;
}
......@@ -578,13 +588,19 @@ class _IncludesNodeWith extends Matcher {
this.textDirection,
this.actions,
this.flags,
}) : assert(label != null || value != null || actions != null || flags != null);
this.scrollPosition,
this.scrollExtentMax,
this.scrollExtentMin,
}) : assert(label != null || value != null || actions != null || flags != null || scrollPosition != null || scrollExtentMax != null || scrollExtentMin != null);
final String label;
final String value;
final TextDirection textDirection;
final List<SemanticsAction> actions;
final List<SemanticsFlag> flags;
final double scrollPosition;
final double scrollExtentMax;
final double scrollExtentMin;
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
......@@ -594,6 +610,9 @@ class _IncludesNodeWith extends Matcher {
textDirection: textDirection,
actions: actions,
flags: flags,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
).isNotEmpty;
}
......@@ -619,6 +638,12 @@ class _IncludesNodeWith extends Matcher {
strings.add('actions "${actions.join(', ')}"');
if (flags != null)
strings.add('flags "${flags.join(', ')}"');
if (scrollPosition != null)
strings.add('scrollPosition "$scrollPosition"');
if (scrollExtentMax != null)
strings.add('scrollExtentMax "$scrollExtentMax"');
if (scrollExtentMin != null)
strings.add('scrollExtentMin "$scrollExtentMin"');
return strings.join(', ');
}
}
......@@ -633,6 +658,9 @@ Matcher includesNodeWith({
TextDirection textDirection,
List<SemanticsAction> actions,
List<SemanticsFlag> flags,
double scrollPosition,
double scrollExtentMax,
double scrollExtentMin,
}) {
return new _IncludesNodeWith(
label: label,
......@@ -640,5 +668,8 @@ Matcher includesNodeWith({
textDirection: textDirection,
actions: actions,
flags: flags,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
);
}
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