Encode scrolling status into tree (#14536)
This commit is contained in:
parent
248919fa9f
commit
72517f0a0a
@ -1 +1 @@
|
||||
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)})';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
_RenderExcludableScrollSemantics createRenderObject(BuildContext context) => new _RenderExcludableScrollSemantics(position: position);
|
||||
|
||||
@override
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
19
packages/flutter/test/physics/utils_test.dart
Normal file
19
packages/flutter/test/physics/utils_test.dart
Normal file
@ -0,0 +1,19 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
));
|
||||
|
||||
expect(semantics, includesNodeWith(
|
||||
scrollExtentMin: 0.0,
|
||||
scrollPosition: 0.0,
|
||||
scrollExtentMax: 520.0,
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
],
|
||||
));
|
||||
|
||||
await flingUp(tester);
|
||||
|
||||
expect(messages, isNot(hasLength(0)));
|
||||
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
|
||||
expect(semantics, includesNodeWith(
|
||||
scrollExtentMin: 0.0,
|
||||
scrollPosition: 380.2,
|
||||
scrollExtentMax: 520.0,
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
],
|
||||
));
|
||||
|
||||
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(messages, isNot(hasLength(0)));
|
||||
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
|
||||
|
||||
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(semantics, includesNodeWith(
|
||||
scrollExtentMin: 0.0,
|
||||
scrollPosition: 0.0,
|
||||
scrollExtentMax: double.infinity,
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
],
|
||||
));
|
||||
|
||||
expect(messages, isNot(hasLength(0)));
|
||||
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
|
||||
await flingUp(tester);
|
||||
|
||||
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);
|
||||
expect(semantics, includesNodeWith(
|
||||
scrollExtentMin: 0.0,
|
||||
scrollPosition: 380.2,
|
||||
scrollExtentMax: double.infinity,
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.scrollUp,
|
||||
SemanticsAction.scrollDown,
|
||||
],
|
||||
));
|
||||
|
||||
messages.clear();
|
||||
await flingRight(tester);
|
||||
await flingUp(tester);
|
||||
|
||||
expect(messages, isNot(hasLength(0)));
|
||||
expect(messages.every((dynamic message) => message['type'] == 'scroll'), isTrue);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user