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 {
class _CupertinoPickerState extends State<CupertinoPicker> {
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) {
// Only the haptic engine hardware on iOS devices would produce the
......@@ -246,17 +272,20 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
Widget result = Stack(
children: <Widget>[
Positioned.fill(
child: ListWheelScrollView.useDelegate(
controller: widget.scrollController,
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio,
perspective: _kDefaultPerspective,
offAxisFraction: widget.offAxisFraction,
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged,
childDelegate: widget.childDelegate,
child: _CupertinoPickerSemantics(
scrollController: widget.scrollController ?? _controller,
child: ListWheelScrollView.useDelegate(
controller: widget.scrollController ?? _controller,
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio,
perspective: _kDefaultPerspective,
offAxisFraction: widget.offAxisFraction,
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged,
childDelegate: widget.childDelegate,
),
),
),
_buildGradientScreen(),
......@@ -274,3 +303,110 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
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 {
super.visitChildrenForSemantics(visitor);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
......
......@@ -10,6 +10,7 @@ import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'scroll_context.dart';
......@@ -91,7 +92,7 @@ class ListWheelChildListDelegate extends ListWheelChildDelegate {
Widget build(BuildContext context, int index) {
if (index < 0 || index >= children.length)
return null;
return children[index];
return IndexedSemantics(child: children[index], index: index);
}
@override
......@@ -137,7 +138,7 @@ class ListWheelChildLoopingListDelegate extends ListWheelChildDelegate {
Widget build(BuildContext context, int index) {
if (children.isEmpty)
return null;
return children[index % children.length];
return IndexedSemantics(child: children[index % children.length], index: index);
}
@override
......@@ -178,11 +179,13 @@ class ListWheelChildBuilderDelegate extends ListWheelChildDelegate {
@override
Widget build(BuildContext context, int index) {
if (childCount == null)
return builder(context, index);
if (childCount == null) {
final Widget child = builder(context, index);
return child == null ? null : IndexedSemantics(child: child, index: index);
}
if (index < 0 || index >= childCount)
return null;
return builder(context, index);
return IndexedSemantics(child: builder(context, index), index: index);
}
@override
......
// 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 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
......@@ -581,4 +584,98 @@ void main() {
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() {
)
);
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
// value of childCount should be 4.
......@@ -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(
matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0,
......@@ -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(
matrix4: equals(<dynamic>[
1.0, 0.0, 0.0, 0.0,
......@@ -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
..transform(
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