Unverified Commit 953db753 authored by Viet Do's avatar Viet Do Committed by GitHub

Adding multicolumn cupertino picker to the Gallery. (#19284)

* Add sample of multicolumn picker to Flutter Gallery
* Modify CupertinoPicker to be able to be rendered off-center.
parent ba1a18f4
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'cupertino_navigation_demo.dart' show coolColorNames;
......@@ -17,9 +18,12 @@ class CupertinoPickerDemo extends StatefulWidget {
}
class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
int _selectedItemIndex = 0;
int _selectedColorIndex = 0;
Widget _buildMenu() {
int _selectedHour = 0;
int _selectedMinute = 0;
Widget _buildMenu(List<Widget> children) {
return new Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
......@@ -42,13 +46,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const Text('Favorite Color'),
new Text(
coolColorNames[_selectedItemIndex],
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
children: children,
),
),
),
......@@ -56,10 +54,82 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
);
}
Widget _buildBottomPicker() {
Widget _buildColorPicker() {
final FixedExtentScrollController scrollController =
new FixedExtentScrollController(initialItem: _selectedItemIndex);
new FixedExtentScrollController(initialItem: _selectedColorIndex);
return new CupertinoPicker(
scrollController: scrollController,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.white,
onSelectedItemChanged: (int index) {
setState(() {
_selectedColorIndex = index;
});
},
children: new List<Widget>.generate(coolColorNames.length, (int index) {
return new Center(child:
new Text(coolColorNames[index]),
);
}),
);
}
Widget _buildAlarmPicker() {
return new Row(
children: <Widget>[
new Expanded(
child: new CupertinoPicker(
scrollController: new FixedExtentScrollController(
initialItem: _selectedHour,
),
offAxisFraction: -0.5,
useMagnifier: true,
magnification: 1.1,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.white,
onSelectedItemChanged: (int index) {
setState(() {
_selectedHour = index;
});
},
children: new List<Widget>.generate(24, (int index) {
return new Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 32.0),
child: new Text(index.toString()),
);
}),
),
),
new Expanded(
child: new CupertinoPicker(
scrollController: new FixedExtentScrollController(
initialItem: _selectedMinute,
),
offAxisFraction: 0.5,
useMagnifier: true,
magnification: 1.1,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.white,
onSelectedItemChanged: (int index) {
setState(() {
_selectedMinute = index;
});
},
children: new List<Widget>.generate(60, (int index) {
return new Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 32.0),
child: new Text(index.toString()),
);
}),
),
),
],
);
}
Widget _buildBottomPicker(Widget picker) {
return new Container(
height: _kPickerSheetHeight,
color: CupertinoColors.white,
......@@ -72,21 +142,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
// Blocks taps from propagating to the modal sheet and popping.
onTap: () {},
child: new SafeArea(
child: new CupertinoPicker(
scrollController: scrollController,
itemExtent: _kPickerItemHeight,
backgroundColor: CupertinoColors.white,
onSelectedItemChanged: (int index) {
setState(() {
_selectedItemIndex = index;
});
},
children: new List<Widget>.generate(coolColorNames.length, (int index) {
return new Center(child:
new Text(coolColorNames[index]),
);
}),
),
child: picker,
),
),
),
......@@ -95,6 +151,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
@override
Widget build(BuildContext context) {
final String time = new DateFormat.Hm().format(new DateTime(2018, 1, 1, _selectedHour, _selectedMinute));
return new Scaffold(
appBar: new AppBar(
title: const Text('Cupertino Picker'),
......@@ -115,11 +172,42 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
await showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker();
return _buildBottomPicker(_buildColorPicker());
},
);
},
child: _buildMenu(
<Widget>[
const Text('Favorite Color'),
new Text(
coolColorNames[_selectedColorIndex],
style: const TextStyle(
color: CupertinoColors.inactiveGray
),
),
]
),
),
new GestureDetector(
onTap: () async {
await showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(_buildAlarmPicker());
},
);
},
child: _buildMenu(),
child: _buildMenu(
<Widget>[
const Text('Alarm'),
new Text(
time,
style: const TextStyle(
color: CupertinoColors.inactiveGray
),
),
]
),
),
],
),
......
......@@ -42,12 +42,16 @@ class CupertinoPicker extends StatefulWidget {
Key key,
this.diameterRatio = _kDefaultDiameterRatio,
this.backgroundColor = _kDefaultBackground,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
this.scrollController,
@required this.itemExtent,
@required this.onSelectedItemChanged,
@required this.children,
}) : assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
super(key: key);
......@@ -69,6 +73,15 @@ class CupertinoPicker extends StatefulWidget {
/// is mildly more efficient than using [Colors.transparent].
final Color backgroundColor;
/// {@macro flutter.rendering.wheelList.offAxisFraction}
final double offAxisFraction;
/// {@macro flutter.rendering.wheelList.useMagnifier}
final bool useMagnifier;
/// {@macro flutter.rendering.wheelList.magnification}
final double magnification;
/// A [FixedExtentScrollController] to read and control the current item.
///
/// If null, an implicit one will be created internally.
......@@ -164,7 +177,9 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
bottom: const BorderSide(width: 0.0, color: _kHighlighterBorder),
)
),
constraints: new BoxConstraints.expand(height: widget.itemExtent),
constraints: new BoxConstraints.expand(
height: widget.itemExtent * widget.magnification,
),
),
new Expanded(
child: new Container(
......@@ -185,6 +200,9 @@ class _CupertinoPickerState extends State<CupertinoPicker> {
controller: widget.scrollController,
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio,
offAxisFraction: widget.offAxisFraction,
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged,
children: widget.children,
......
......@@ -100,6 +100,9 @@ class RenderListWheelViewport
@required ViewportOffset offset,
double diameterRatio = defaultDiameterRatio,
double perspective = defaultPerspective,
double offAxisFraction = 0.0,
bool useMagnifier = false,
double magnification = 1.0,
@required double itemExtent,
bool clipToSize = true,
bool renderChildrenOutsideViewport = false,
......@@ -110,6 +113,10 @@ class RenderListWheelViewport
assert(perspective != null),
assert(perspective > 0),
assert(perspective <= 0.01, perspectiveTooHighMessage),
assert(offAxisFraction != null),
assert(useMagnifier != null),
assert(magnification != null),
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(clipToSize != null),
......@@ -121,6 +128,9 @@ class RenderListWheelViewport
_offset = offset,
_diameterRatio = diameterRatio,
_perspective = perspective,
_offAxisFraction = offAxisFraction,
_useMagnifier = useMagnifier,
_magnification = magnification,
_itemExtent = itemExtent,
_clipToSize = clipToSize,
_renderChildrenOutsideViewport = renderChildrenOutsideViewport {
......@@ -243,6 +253,74 @@ class RenderListWheelViewport
_hasScrolled();
}
/// {@template flutter.rendering.wheelList.offAxisFraction}
/// How much the wheel is horizontally off-center, as a fraction of its width.
/// This property creates the visual effect of looking at a vertical wheel from
/// its side where its vanishing points at the edge curves to one side instead
/// of looking at the wheel head-on.
///
/// The value is horizontal distance between the wheel's center and the vertical
/// vanishing line at the edges of the wheel, represented as a fraction of the
/// wheel's width.
///
/// The value `0.0` means the wheel is looked at head-on and its vanishing
/// line runs through the center of the wheel. Negative values means moving
/// the wheel to the left of the observer, thus the edges curve to the right.
/// Positive values means moving the wheel to the right of the observer,
/// thus the edges curve to the left.
///
/// The visual effect causes the wheel's edges to curve rather than moving
/// the center. So a value of `0.5` means the edges' vanishing line will touch
/// the wheel's size's left edge.
///
/// Defaults to 0.0, which means looking at the wheel head-on.
/// The visual effect can be unaesthetic if this value is too far from the
/// range [-0.5, 0.5].
/// {@endtemplate}
double get offAxisFraction => _offAxisFraction;
double _offAxisFraction = 0.0;
set offAxisFraction(double value) {
assert(value != null);
if (value == _offAxisFraction)
return;
_offAxisFraction = value;
markNeedsPaint();
}
/// {@template flutter.rendering.wheelList.useMagnifier}
/// Whether to use the magnifier for the center item of the wheel.
/// {@endtemplate}
bool get useMagnifier => _useMagnifier;
bool _useMagnifier = false;
set useMagnifier(bool value) {
assert(value != null);
if (value == _useMagnifier)
return;
_useMagnifier = value;
markNeedsPaint();
}
/// {@template flutter.rendering.wheelList.magnification}
/// The zoomed-in rate of the magnifier, if it is used.
///
/// The default value is 1.0, which will not change anything.
/// If the value is > 1.0, the center item will be zoomed in by that rate, and
/// it will also be rendered as flat, not cylindrical like the rest of the list.
/// The item will be zoomed out if magnification < 1.0.
///
/// Must be positive.
/// {@endtemplate}
double get magnification => _magnification;
double _magnification = 1.0;
set magnification(double value) {
assert(value != null);
assert(value > 0);
if (value == _magnification)
return;
_magnification = value;
markNeedsPaint();
}
/// {@template flutter.rendering.wheelList.itemExtent}
/// The size of the children along the main axis. Children [RenderBox]es will
/// be given the [BoxConstraints] of this exact size.
......@@ -562,34 +640,150 @@ class RenderListWheelViewport
perspective: _perspective,
);
// Offset that helps painting everything in the center (e.g. angle = 0).
final Offset offsetToCenter = new Offset(
untransformedPaintingCoordinates.dx,
-_topScrollMarginExtent);
if (!useMagnifier)
_paintChildCylindrically(context, offset, child, transform, offsetToCenter);
else
_paintChildWithMagnifier(
context,
offset,
child,
transform,
offsetToCenter,
untransformedPaintingCoordinates,
);
}
/// Paint child with the magnifier active - the child will be rendered
/// differently if it intersects with the magnifier.
void _paintChildWithMagnifier(
PaintingContext context,
Offset offset,
RenderBox child,
Matrix4 cylindricalTransform,
Offset offsetToCenter,
Offset untransformedPaintingCoordinates,
) {
final double magnifierTopLinePosition =
size.height / 2 - _itemExtent * _magnification / 2;
final double magnifierBottomLinePosition =
size.height / 2 + _itemExtent * _magnification / 2;
final bool isAfterMagnifierTopLine = untransformedPaintingCoordinates.dy
>= magnifierTopLinePosition - _itemExtent * _magnification;
final bool isBeforeMagnifierBottomLine = untransformedPaintingCoordinates.dy
<= magnifierBottomLinePosition;
// Some part of the child is in the center magnifier.
if (isAfterMagnifierTopLine && isBeforeMagnifierBottomLine) {
final Rect centerRect = new Rect.fromLTWH(
0.0,
magnifierTopLinePosition,
size.width,
_itemExtent * _magnification);
final Rect topHalfRect = new Rect.fromLTWH(
0.0,
0.0,
size.width,
magnifierTopLinePosition);
final Rect bottomHalfRect = new Rect.fromLTWH(
0.0,
magnifierBottomLinePosition,
size.width,
magnifierTopLinePosition);
// Clipping the part in the center.
context.pushClipRect(
false,
offset,
centerRect,
(PaintingContext context, Offset offset) {
context.pushTransform(
false,
offset,
_magnifyTransform(),
(PaintingContext context, Offset offset) {
context.paintChild(
child,
offset + untransformedPaintingCoordinates);
});
});
// Clipping the part in either the top-half or bottom-half of the wheel.
context.pushClipRect(
false,
offset,
untransformedPaintingCoordinates.dy <= magnifierTopLinePosition
? topHalfRect
: bottomHalfRect,
(PaintingContext context, Offset offset) {
_paintChildCylindrically(
context,
offset,
child,
cylindricalTransform,
offsetToCenter);
}
);
} else {
_paintChildCylindrically(
context,
offset,
child,
cylindricalTransform,
offsetToCenter);
}
}
// / Paint the child cylindrically at given offset.
void _paintChildCylindrically(
PaintingContext context,
Offset offset,
RenderBox child,
Matrix4 cylindricalTransform,
Offset offsetToCenter,
) {
context.pushTransform(
// Text with TransformLayers and no cullRects currently have an issue rendering
// https://github.com/flutter/flutter/issues/14224.
false,
offset,
_centerOriginTransform(transform),
_centerOriginTransform(cylindricalTransform),
// Pre-transform painting function.
(PaintingContext context, Offset offset) {
context.paintChild(
child,
offset + new Offset(
untransformedPaintingCoordinates.dx,
// Paint everything in the center (e.g. angle = 0), then transform.
-_topScrollMarginExtent,
),
// Paint everything in the center (e.g. angle = 0), then transform.
offset + offsetToCenter,
);
},
);
}
/// Return the Matrix4 transformation that would zoom in content in the
/// magnified area.
Matrix4 _magnifyTransform() {
final Matrix4 magnify = Matrix4.identity();
magnify.translate(size.width * (-_offAxisFraction + 0.5), size.height / 2);
magnify.scale(_magnification, _magnification, _magnification);
magnify.translate(-size.width * (-_offAxisFraction + 0.5), -size.height / 2);
return magnify;
}
/// Apply incoming transformation with the transformation's origin at the
/// viewport's center.
/// viewport's center or horizontally off to the side based on offAxisFraction.
Matrix4 _centerOriginTransform(Matrix4 originalMatrix) {
final Matrix4 result = new Matrix4.identity();
final Offset centerOriginTranslation = Alignment.center.alongSize(size);
result.translate(centerOriginTranslation.dx, centerOriginTranslation.dy);
result.translate(centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1),
centerOriginTranslation.dy);
result.multiply(originalMatrix);
result.translate(-centerOriginTranslation.dx, -centerOriginTranslation.dy);
result.translate(-centerOriginTranslation.dx * (-_offAxisFraction * 2 + 1),
-centerOriginTranslation.dy);
return result;
}
......
......@@ -395,6 +395,9 @@ class ListWheelScrollView extends StatefulWidget {
this.physics,
this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio,
this.perspective = RenderListWheelViewport.defaultPerspective,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
@required this.itemExtent,
this.onSelectedItemChanged,
this.clipToSize = true,
......@@ -405,6 +408,7 @@ class ListWheelScrollView extends StatefulWidget {
assert(perspective != null),
assert(perspective > 0),
assert(perspective <= 0.01, RenderListWheelViewport.perspectiveTooHighMessage),
assert(magnification > 0),
assert(itemExtent != null),
assert(itemExtent > 0),
assert(clipToSize != null),
......@@ -446,6 +450,15 @@ class ListWheelScrollView extends StatefulWidget {
/// {@macro flutter.rendering.wheelList.perspective}
final double perspective;
/// {@macro flutter.rendering.wheelList.offAxisFraction}
final double offAxisFraction;
/// {@macro flutter.rendering.wheelList.useMagnifier}
final bool useMagnifier;
/// {@macro RenderListWheelViewport.magnification}
final double magnification;
/// Size of each child in the main axis. Must not be null and must be
/// positive.
final double itemExtent;
......@@ -517,6 +530,9 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
return new ListWheelViewport(
diameterRatio: widget.diameterRatio,
perspective: widget.perspective,
offAxisFraction: widget.offAxisFraction,
useMagnifier: widget.useMagnifier,
magnification: widget.magnification,
itemExtent: widget.itemExtent,
clipToSize: widget.clipToSize,
renderChildrenOutsideViewport: widget.renderChildrenOutsideViewport,
......@@ -560,6 +576,9 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
Key key,
this.diameterRatio = RenderListWheelViewport.defaultDiameterRatio,
this.perspective = RenderListWheelViewport.defaultPerspective,
this.offAxisFraction = 0.0,
this.useMagnifier = false,
this.magnification = 1.0,
@required this.itemExtent,
this.clipToSize = true,
this.renderChildrenOutsideViewport = false,
......@@ -587,6 +606,15 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
/// {@macro flutter.rendering.wheelList.perspective}
final double perspective;
/// {@macro flutter.rendering.wheelList.offAxisFraction}
final double offAxisFraction;
/// {@macro flutter.rendering.wheelList.useMagnifier}
final bool useMagnifier;
/// {@macro flutter.rendering.wheelList.magnification}
final double magnification;
/// {@macro flutter.rendering.wheelList.itemExtent}
final double itemExtent;
......@@ -605,6 +633,9 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
return new RenderListWheelViewport(
diameterRatio: diameterRatio,
perspective: perspective,
offAxisFraction: offAxisFraction,
useMagnifier: useMagnifier,
magnification: magnification,
itemExtent: itemExtent,
clipToSize: clipToSize,
renderChildrenOutsideViewport: renderChildrenOutsideViewport,
......@@ -617,6 +648,9 @@ class ListWheelViewport extends MultiChildRenderObjectWidget {
renderObject
..diameterRatio = diameterRatio
..perspective = perspective
..offAxisFraction = offAxisFraction
..useMagnifier = useMagnifier
..magnification = magnification
..itemExtent = itemExtent
..clipToSize = clipToSize
..renderChildrenOutsideViewport = renderChildrenOutsideViewport
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
......@@ -49,6 +51,21 @@ void main() {
);
expect(tester.getSize(find.byType(ListWheelScrollView)), const Size(800.0, 600.0));
});
testWidgets('ListWheelScrollView needs positive magnification', (WidgetTester tester) async {
expect(
() {
new ListWheelScrollView(
useMagnifier: true,
magnification: -1.0,
itemExtent: 20.0,
children: <Widget>[new Container()],
);
},
throwsAssertionError,
);
});
});
group('layout', () {
......@@ -238,6 +255,31 @@ void main() {
});
group('viewport transformation', () {
testWidgets('Center child is magnified', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new RepaintBoundary(
key: const Key('list_wheel_scroll_view'),
child: new ListWheelScrollView(
useMagnifier: true,
magnification: 2.0,
itemExtent: 50.0,
children: List<Widget>.generate(10, (int index) {
return const Placeholder();
}),
),
),
),
);
await expectLater(
find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile('list_wheel_scroll_view.center_child.magnified.png'),
skip: !Platform.isLinux,
);
});
testWidgets('Default middle transform', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
......@@ -267,6 +309,32 @@ void main() {
));
});
testWidgets('Curve the wheel to the left', (WidgetTester tester) async {
final ScrollController controller = new ScrollController(initialScrollOffset: 300.0);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new RepaintBoundary(
key: const Key('list_wheel_scroll_view'),
child: new ListWheelScrollView(
controller: controller,
offAxisFraction: 0.5,
itemExtent: 50.0,
children: List<Widget>.generate(32, (int index) {
return const Placeholder();
}),
),
),
),
);
await expectLater(
find.byKey(const Key('list_wheel_scroll_view')),
matchesGoldenFile('list_wheel_scroll_view.curved_wheel.left.png'),
skip: !Platform.isLinux,
);
});
testWidgets('Scrolling, diameterRatio, perspective all changes matrix', (WidgetTester tester) async {
final ScrollController controller = new ScrollController(initialScrollOffset: 200.0);
......@@ -385,6 +453,99 @@ void main() {
]),
));
});
testWidgets('offAxisFraction, magnification changes matrix', (WidgetTester tester) async {
final ScrollController controller = new ScrollController(
initialScrollOffset: 200.0);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
offAxisFraction: 0.5,
children: <Widget>[
new Container(
width: 200.0,
child: const Center(
child: const Text('blah'),
),
),
],
),
),
);
final RenderListWheelViewport viewport = tester.firstRenderObject(find.byType(Container)).parent;
expect(viewport, paints
..transform(
matrix4: equals(<dynamic>[
1.0,
0.0,
0.0,
0.0,
0.0,
moreOrLessEquals(0.6318744917928063),
moreOrLessEquals(0.3420201433256688),
moreOrLessEquals(-0.0010260604299770066),
0.0,
moreOrLessEquals(-1.1877435020329863),
moreOrLessEquals(0.9396926207859083),
moreOrLessEquals(-0.002819077862357725),
0.0,
moreOrLessEquals(-62.20844875763376),
moreOrLessEquals(-138.79047052615562),
moreOrLessEquals(1.4163714115784667),
]),
));
controller.jumpTo(0.0);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new ListWheelScrollView(
controller: controller,
itemExtent: 100.0,
offAxisFraction: 0.5,
useMagnifier: true,
magnification: 1.5,
children: <Widget>[
new Container(
width: 200.0,
child: const Center(
child: const Text('blah'),
),
),
],
),
),
);
expect(viewport, paints
..transform(
matrix4: equals(<dynamic>[
1.5,
0.0,
0.0,
0.0,
0.0,
1.5,
0.0,
0.0,
0.0,
0.0,
1.5,
0.0,
0.0,
-150.0,
0.0,
1.0,
]),
));
});
});
group('scroll notifications', () {
......
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