Unverified Commit aac5b048 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

CupertinoPicker semantics (#23551)

parent e92ac484
...@@ -161,6 +161,32 @@ class CupertinoPicker extends StatefulWidget { ...@@ -161,6 +161,32 @@ class CupertinoPicker extends StatefulWidget {
class _CupertinoPickerState extends State<CupertinoPicker> { class _CupertinoPickerState extends State<CupertinoPicker> {
int _lastHapticIndex; int _lastHapticIndex;
FixedExtentScrollController _controller;
@override
void initState() {
super.initState();
if (widget.scrollController == null) {
_controller = FixedExtentScrollController();
}
}
@override
void didUpdateWidget(CupertinoPicker oldWidget) {
if (widget.scrollController != null && oldWidget.scrollController == null) {
_controller = null;
} else if (widget.scrollController == null && oldWidget.scrollController != null) {
assert(_controller == null);
_controller = FixedExtentScrollController();
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
void _handleSelectedItemChanged(int index) { void _handleSelectedItemChanged(int index) {
// Only the haptic engine hardware on iOS devices would produce the // Only the haptic engine hardware on iOS devices would produce the
...@@ -246,17 +272,20 @@ class _CupertinoPickerState extends State<CupertinoPicker> { ...@@ -246,17 +272,20 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
Widget result = Stack( Widget result = Stack(
children: <Widget>[ children: <Widget>[
Positioned.fill( Positioned.fill(
child: ListWheelScrollView.useDelegate( child: _CupertinoPickerSemantics(
controller: widget.scrollController, scrollController: widget.scrollController ?? _controller,
physics: const FixedExtentScrollPhysics(), child: ListWheelScrollView.useDelegate(
diameterRatio: widget.diameterRatio, controller: widget.scrollController ?? _controller,
perspective: _kDefaultPerspective, physics: const FixedExtentScrollPhysics(),
offAxisFraction: widget.offAxisFraction, diameterRatio: widget.diameterRatio,
useMagnifier: widget.useMagnifier, perspective: _kDefaultPerspective,
magnification: widget.magnification, offAxisFraction: widget.offAxisFraction,
itemExtent: widget.itemExtent, useMagnifier: widget.useMagnifier,
onSelectedItemChanged: _handleSelectedItemChanged, magnification: widget.magnification,
childDelegate: widget.childDelegate, itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged,
childDelegate: widget.childDelegate,
),
), ),
), ),
_buildGradientScreen(), _buildGradientScreen(),
...@@ -274,3 +303,110 @@ class _CupertinoPickerState extends State<CupertinoPicker> { ...@@ -274,3 +303,110 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
return result; return result;
} }
} }
// Turns the scroll semantics of the ListView into a single adjustable semantics
// node. This is done by removing all of the child semantics of the scroll
// wheel and using the scroll indexes to look up the current, previous, and
// next semantic label. This label is then turned into the value of a new
// adjustable semantic node, with adjustment callbacks wired to move the
// scroll controller.
class _CupertinoPickerSemantics extends SingleChildRenderObjectWidget {
const _CupertinoPickerSemantics({
Key key,
Widget child,
@required this.scrollController,
}) : super(key: key, child: child);
final FixedExtentScrollController scrollController;
@override
RenderObject createRenderObject(BuildContext context) => _RenderCupertinoPickerSemantics(scrollController, Directionality.of(context));
@override
void updateRenderObject(BuildContext context, covariant _RenderCupertinoPickerSemantics renderObject) {
renderObject
..textDirection = Directionality.of(context)
..controller = scrollController;
}
}
class _RenderCupertinoPickerSemantics extends RenderProxyBox {
_RenderCupertinoPickerSemantics(FixedExtentScrollController controller, this._textDirection) {
this.controller = controller;
}
FixedExtentScrollController get controller => _controller;
FixedExtentScrollController _controller;
set controller(FixedExtentScrollController value) {
if (value == _controller)
return;
if (_controller != null)
_controller.removeListener(_handleScrollUpdate);
else
_currentIndex = value.initialItem ?? 0;
value.addListener(_handleScrollUpdate);
_controller = value;
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (textDirection == value)
return;
_textDirection = value;
markNeedsSemanticsUpdate();
}
int _currentIndex = 0;
void _handleIncrease() {
controller.jumpToItem(_currentIndex + 1);
}
void _handleDecrease() {
if (_currentIndex == 0)
return;
controller.jumpToItem(_currentIndex - 1);
}
void _handleScrollUpdate() {
if (controller.selectedItem == _currentIndex)
return;
_currentIndex = controller.selectedItem;
markNeedsSemanticsUpdate();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.isSemanticBoundary = true;
config.textDirection = textDirection;
}
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
if (children.isEmpty)
return super.assembleSemanticsNode(node, config, children);
final SemanticsNode scrollable = children.first;
final Map<int, SemanticsNode> indexedChildren = <int, SemanticsNode>{};
scrollable.visitChildren((SemanticsNode child) {
assert(child.indexInParent != null);
indexedChildren[child.indexInParent] = child;
return true;
});
if (indexedChildren[_currentIndex] == null) {
return node.updateWith(config: config);
}
config.value = indexedChildren[_currentIndex].label;
final SemanticsNode previousChild = indexedChildren[_currentIndex - 1];
final SemanticsNode nextChild = indexedChildren[_currentIndex + 1];
if (nextChild != null) {
config.increasedValue = nextChild.label;
config.onIncrease = _handleIncrease;
}
if (previousChild != null) {
config.decreasedValue = previousChild.label;
config.onDecrease = _handleDecrease;
}
node.updateWith(config: config);
}
}
...@@ -4032,7 +4032,6 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -4032,7 +4032,6 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
super.visitChildrenForSemantics(visitor); super.visitChildrenForSemantics(visitor);
} }
@override @override
void describeSemanticsConfiguration(SemanticsConfiguration config) { void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
......
...@@ -10,6 +10,7 @@ import 'package:flutter/physics.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'scroll_context.dart'; import 'scroll_context.dart';
...@@ -91,7 +92,7 @@ class ListWheelChildListDelegate extends ListWheelChildDelegate { ...@@ -91,7 +92,7 @@ class ListWheelChildListDelegate extends ListWheelChildDelegate {
Widget build(BuildContext context, int index) { Widget build(BuildContext context, int index) {
if (index < 0 || index >= children.length) if (index < 0 || index >= children.length)
return null; return null;
return children[index]; return IndexedSemantics(child: children[index], index: index);
} }
@override @override
...@@ -137,7 +138,7 @@ class ListWheelChildLoopingListDelegate extends ListWheelChildDelegate { ...@@ -137,7 +138,7 @@ class ListWheelChildLoopingListDelegate extends ListWheelChildDelegate {
Widget build(BuildContext context, int index) { Widget build(BuildContext context, int index) {
if (children.isEmpty) if (children.isEmpty)
return null; return null;
return children[index % children.length]; return IndexedSemantics(child: children[index % children.length], index: index);
} }
@override @override
...@@ -178,11 +179,13 @@ class ListWheelChildBuilderDelegate extends ListWheelChildDelegate { ...@@ -178,11 +179,13 @@ class ListWheelChildBuilderDelegate extends ListWheelChildDelegate {
@override @override
Widget build(BuildContext context, int index) { Widget build(BuildContext context, int index) {
if (childCount == null) if (childCount == null) {
return builder(context, index); final Widget child = builder(context, index);
return child == null ? null : IndexedSemantics(child: child, index: index);
}
if (index < 0 || index >= childCount) if (index < 0 || index >= childCount)
return null; return null;
return builder(context, index); return IndexedSemantics(child: builder(context, index), index: index);
} }
@override @override
......
// Copyright 2017 The Chromium Authors. All rights reserved. // Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
...@@ -581,4 +584,98 @@ void main() { ...@@ -581,4 +584,98 @@ void main() {
expect(date, DateTime(2018, 1, 1, 15, 59)); expect(date, DateTime(2018, 1, 1, 15, 59));
}); });
}); });
}
\ No newline at end of file testWidgets('scrollController can be removed or added', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
int lastSelectedItem;
void onSelectedItemChanged(int index) {
lastSelectedItem = index;
}
await tester.pumpWidget(_buildPicker(
controller: FixedExtentScrollController(),
onSelectedItemChanged: onSelectedItemChanged,
));
tester.binding.pipelineOwner.semanticsOwner.performAction(1, SemanticsAction.increase);
await tester.pumpAndSettle();
expect(lastSelectedItem, 1);
await tester.pumpWidget(_buildPicker(
onSelectedItemChanged: onSelectedItemChanged,
));
tester.binding.pipelineOwner.semanticsOwner.performAction(1, SemanticsAction.increase);
await tester.pumpAndSettle();
expect(lastSelectedItem, 2);
await tester.pumpWidget(_buildPicker(
controller: FixedExtentScrollController(),
onSelectedItemChanged: onSelectedItemChanged,
));
tester.binding.pipelineOwner.semanticsOwner.performAction(1, SemanticsAction.increase);
await tester.pumpAndSettle();
expect(lastSelectedItem, 3);
handle.dispose();
});
testWidgets('picker exports semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
debugResetSemanticsIdCounter();
int lastSelectedItem;
await tester.pumpWidget(_buildPicker(onSelectedItemChanged: (int index) {
lastSelectedItem = index;
}));
expect(tester.getSemantics(find.byType(CupertinoPicker)), matchesSemantics(
children: <Matcher>[
matchesSemantics(
hasIncreaseAction: true,
hasDecreaseAction: false,
increasedValue: '1',
value: '0',
textDirection: TextDirection.ltr,
),
],
));
tester.binding.pipelineOwner.semanticsOwner.performAction(1, SemanticsAction.increase);
await tester.pumpAndSettle();
expect(tester.getSemantics(find.byType(CupertinoPicker)), matchesSemantics(
children: <Matcher>[
matchesSemantics(
hasIncreaseAction: true,
hasDecreaseAction: true,
increasedValue: '2',
decreasedValue: '0',
value: '1',
textDirection: TextDirection.ltr,
),
],
));
expect(lastSelectedItem, 1);
handle.dispose();
});
}
Widget _buildPicker({FixedExtentScrollController controller, ValueChanged<int> onSelectedItemChanged}) {
return Directionality(
textDirection: TextDirection.ltr,
child: CupertinoPicker(
scrollController: controller,
itemExtent: 100.0,
onSelectedItemChanged: onSelectedItemChanged,
children: List<Widget>.generate(100, (int index) {
return Center(
child: Container(
width: 400.0,
height: 100.0,
child: Text(index.toString()),
),
);
}),
),
);
}
...@@ -331,7 +331,7 @@ void main() { ...@@ -331,7 +331,7 @@ void main() {
) )
); );
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Text)).parent; final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Text)).parent.parent;
// Item 0 is in the middle. There are 3 children visible after it, so the // Item 0 is in the middle. There are 3 children visible after it, so the
// value of childCount should be 4. // value of childCount should be 4.
...@@ -515,7 +515,7 @@ void main() { ...@@ -515,7 +515,7 @@ void main() {
), ),
); );
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent; final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent.parent;
expect(viewport, paints..transform( expect(viewport, paints..transform(
matrix4: equals(<dynamic>[ matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
...@@ -573,7 +573,7 @@ void main() { ...@@ -573,7 +573,7 @@ void main() {
), ),
); );
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent; final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent.parent;
expect(viewport, paints..transform( expect(viewport, paints..transform(
matrix4: equals(<dynamic>[ matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
...@@ -694,7 +694,7 @@ void main() { ...@@ -694,7 +694,7 @@ void main() {
), ),
); );
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent; final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent.parent;
expect(viewport, paints expect(viewport, paints
..transform( ..transform(
matrix4: equals(<dynamic>[ matrix4: equals(<dynamic>[
......
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