Unverified Commit 667f4785 authored by xster's avatar xster Committed by GitHub

CupertinoPicker part 4 - create CupertinoPicker and add gallery demo (#14091)

* controller, position and test

* Make controllers swappable

* WIP

* Create a ListWheelScrollPhysics

* Created picker and gallery demo and testing now

* Works. Ready to document and test.

* Document and add tests. Make the scroll controller more generic.

* minor cleanup

* review

* review

* fix tests

* stop using TransformLayers for now
parent b6c6e365
......@@ -6,5 +6,6 @@ export 'cupertino_activity_indicator_demo.dart';
export 'cupertino_buttons_demo.dart';
export 'cupertino_dialog_demo.dart';
export 'cupertino_navigation_demo.dart';
export 'cupertino_picker_demo.dart';
export 'cupertino_slider_demo.dart';
export 'cupertino_switch_demo.dart';
// 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'cupertino_navigation_demo.dart' show coolColorNames;
const double _kPickerSheetHeight = 216.0;
const double _kPickerItemHeight = 32.0;
class CupertinoPickerDemo extends StatefulWidget {
static const String routeName = '/cupertino/picker';
@override
_CupertinoPickerDemoState createState() => new _CupertinoPickerDemoState();
}
class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
int _selectedItemIndex = 0;
Widget _buildMenu() {
return new Container(
decoration: const BoxDecoration(
color: CupertinoColors.white,
border: const Border(
top: const BorderSide(color: const Color(0xFFBCBBC1), width: 0.0),
bottom: const BorderSide(color: const Color(0xFFBCBBC1), width: 0.0),
),
),
height: 44.0,
child: new Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: new SafeArea(
top: false,
bottom: false,
child: new DefaultTextStyle(
style: const TextStyle(
letterSpacing: -0.24,
fontSize: 17.0,
color: CupertinoColors.black,
),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
const Text('Favorite Color'),
new Text(
coolColorNames[_selectedItemIndex],
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
),
),
),
);
}
Widget _buildBottomPicker() {
final FixedExtentScrollController scrollController =
new FixedExtentScrollController(initialItem: _selectedItemIndex);
return new SizedBox(
height: _kPickerSheetHeight,
child: new DefaultTextStyle(
style: const TextStyle(
color: CupertinoColors.black,
fontSize: 22.0,
),
child: new GestureDetector(
// Blocks taps from propagating to the modal sheet and popping.
onTap: () {},
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]),
);
}),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('Cupertino Picker'),
),
body: new DefaultTextStyle(
style: const TextStyle(
fontFamily: '.SF UI Text',
fontSize: 17.0,
color: CupertinoColors.black,
),
child: new DecoratedBox(
decoration: const BoxDecoration(color: const Color(0xFFEFEFF4)),
child: new ListView(
children: <Widget>[
const Padding(padding: const EdgeInsets.only(top: 32.0)),
new GestureDetector(
onTap: () async {
await showModalBottomSheet<Null>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker();
},
);
},
child: _buildMenu(),
),
],
),
),
),
);
}
}
......@@ -306,6 +306,13 @@ List<GalleryItem> _buildGalleryItems() {
routeName: CupertinoNavigationDemo.routeName,
buildRoute: (BuildContext context) => new CupertinoNavigationDemo(),
),
new GalleryItem(
title: 'Pickers',
subtitle: 'Cupertino styled pickers',
category: 'Cupertino Components',
routeName: CupertinoPickerDemo.routeName,
buildRoute: (BuildContext context) => new CupertinoPickerDemo(),
),
new GalleryItem(
title: 'Sliders',
subtitle: 'Cupertino styled sliders',
......
......@@ -71,6 +71,7 @@ const List<Demo> demos = const <Demo>[
const Demo('Buttons'),
const Demo('Dialogs'),
const Demo('Navigation'),
const Demo('Pickers'),
const Demo('Sliders'),
const Demo('Switches'),
......
......@@ -15,6 +15,7 @@ export 'src/cupertino/dialog.dart';
export 'src/cupertino/icons.dart';
export 'src/cupertino/nav_bar.dart';
export 'src/cupertino/page_scaffold.dart';
export 'src/cupertino/picker.dart';
export 'src/cupertino/route.dart';
export 'src/cupertino/scrollbar.dart';
export 'src/cupertino/slider.dart';
......
......@@ -37,11 +37,11 @@ class CupertinoColors {
/// Used in iOS 10 for light background fills such as the chat bubble background.
static const Color lightBackgroundGray = const Color(0xFFE5E5EA);
/// Used in iOS 10 for unselected selectables such as tab bar items in their
/// inactive state.
/// Used in iOS 11 for unselected selectables such as tab bar items in their
/// inactive state or de-emphasized subtitles and details text.
///
/// Not the same gray as disabled buttons etc.
static const Color inactiveGray = const Color(0xFF929292);
static const Color inactiveGray = const Color(0xFF8E8E93);
/// Used for iOS 10 for destructive actions such as the delete actions in
/// table view cells and dialogs.
......
// 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/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
/// Color of the 'magnifier' lens border.
const Color _kHighlighterBorder = const Color(0xFF7F7F7F);
const Color _kDefaultBackground = const Color(0xFFD2D4DB);
/// Eyeballed value comparing with a native picker.
const double _kDefaultDiameterRatio = 1.1;
/// Opacity fraction value that hides the wheel above and below the 'magnifier'
/// lens with the same color as the background.
const double _kForegroundScreenOpacityFraction = 0.7;
/// An iOS-styled picker.
///
/// Displays the provided [children] widgets on a wheel for selection and
/// calls back when the currently selected item changes.
///
/// Can be used with [showModalBottomSheet] to display the picker modally at the
/// bottom of the screen.
///
/// See also:
///
/// * [ListWheelScrollView], the generic widget backing this picker without
/// the iOS design specific chrome.
/// * <https://developer.apple.com/ios/human-interface-guidelines/controls/pickers/>
class CupertinoPicker extends StatefulWidget {
const CupertinoPicker({
Key key,
this.diameterRatio: _kDefaultDiameterRatio,
this.backgroundColor: _kDefaultBackground,
this.scrollController,
@required this.itemExtent,
@required this.onSelectedItemChanged,
@required this.children,
}) : assert(diameterRatio != null),
assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage),
assert(itemExtent != null),
assert(itemExtent > 0),
super(key: key);
/// Relative ratio between this picker's height and the simulated cylinder's diameter.
///
/// Smaller values creates more pronounced curvatures in the scrollable wheel.
///
/// For more details, see [ListWheelScrollView.diameterRatio].
///
/// Must not be null and defaults to `1.1` to visually mimic iOS.
final double diameterRatio;
/// Background color behind the children.
///
/// Defaults to a gray color in the iOS color palette.
final Color backgroundColor;
/// A [FixedExtentScrollController] to read and control the current item.
///
/// If null, an implicit one will be created internally.
final FixedExtentScrollController scrollController;
/// The uniform height of all children.
///
/// All children will be given the [BoxConstraints] to match this exact
/// height. Must not be null and must be positive.
final double itemExtent;
/// An option callback when the currently centered item changes.
///
/// Value changes when the item closest to the center changes.
///
/// This can be called during scrolls and during ballistic flings. To get the
/// value only when the scrolling settles, use a [NotificationListener],
/// listen for [ScrollEndNotification] and read its [FixedExtentMetrics].
final ValueChanged<int> onSelectedItemChanged;
/// [Widget]s in the picker's scroll wheel.
final List<Widget> children;
@override
State<StatefulWidget> createState() => new _CupertinoPickerState();
}
class _CupertinoPickerState extends State<CupertinoPicker> {
int _lastHapticIndex;
void _handleSelectedItemChanged(int index) {
if (index != _lastHapticIndex) {
// TODO(xster): Insert haptic feedback with lighter knock.
// https://github.com/flutter/flutter/issues/13710.
_lastHapticIndex = index;
}
if (widget.onSelectedItemChanged != null) {
widget.onSelectedItemChanged(index);
}
}
/// Makes the fade to white edge gradients.
Widget _buildGradientScreen() {
return new Positioned.fill(
child: new IgnorePointer(
child: new Container(
decoration: const BoxDecoration(
gradient: const LinearGradient(
colors: const <Color>[
const Color(0xFFFFFFFF),
const Color(0xF2FFFFFF),
const Color(0xDDFFFFFF),
const Color(0x00FFFFFF),
const Color(0x00FFFFFF),
const Color(0xDDFFFFFF),
const Color(0xF2FFFFFF),
const Color(0xFFFFFFFF),
],
stops: const <double>[
0.0, 0.05, 0.09, 0.22, 0.78, 0.91, 0.95, 1.0,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
),
);
}
/// Makes the magnifier lens look so that the colors are normal through
/// the lens and partially grayed out around it.
Widget _buildMagnifierScreen() {
final Color foreground = widget.backgroundColor?.withAlpha(
(widget.backgroundColor?.alpha * _kForegroundScreenOpacityFraction).toInt()
);
return new IgnorePointer(
child: new Column(
children: <Widget>[
new Expanded(
child: new Container(
color: foreground,
),
),
new Container(
decoration: const BoxDecoration(
border: const Border(
top: const BorderSide(width: 0.0, color: _kHighlighterBorder),
bottom: const BorderSide(width: 0.0, color: _kHighlighterBorder),
)
),
constraints: new BoxConstraints.expand(height: widget.itemExtent),
),
new Expanded(
child: new Container(
color: foreground,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return new DecoratedBox(
decoration: new BoxDecoration(
color: widget.backgroundColor,
),
child: new Stack(
children: <Widget>[
new Positioned.fill(
child: new ListWheelScrollView(
controller: widget.scrollController,
physics: const FixedExtentScrollPhysics(),
diameterRatio: widget.diameterRatio,
itemExtent: widget.itemExtent,
onSelectedItemChanged: _handleSelectedItemChanged,
children: widget.children,
),
),
_buildGradientScreen(),
_buildMagnifierScreen(),
],
),
);
}
}
......@@ -35,7 +35,7 @@ class ListWheelParentData extends ContainerBoxParentData<RenderBox> { }
///
/// This class works in 3 coordinate systems:
///
/// 1- The **scrollable layout coordinates**. This coordinate system is used to
/// 1. The **scrollable layout coordinates**. This coordinate system is used to
/// communicate with [ViewportOffset] and describes its children's abstract
/// offset from the beginning of the scrollable list at (0.0, 0.0).
///
......@@ -44,7 +44,7 @@ class ListWheelParentData extends ContainerBoxParentData<RenderBox> { }
///
/// Children's layout coordinates don't change as the viewport scrolls.
///
/// 2- The **untransformed plane's viewport painting coordinates**. Children are
/// 2. The **untransformed plane's viewport painting coordinates**. Children are
/// not painted in this coordinate system. It's an abstract intermediary used
/// before transforming into the next cylindrical coordinate system.
///
......@@ -63,7 +63,7 @@ class ListWheelParentData extends ContainerBoxParentData<RenderBox> { }
/// paint 10-11 visible 10px children if there are enough children in the
/// viewport.
///
/// 3- The **transformed cylindrical space viewport painting coordinates**.
/// 3. The **transformed cylindrical space viewport painting coordinates**.
/// Children from system 2 get their positions transformed into a cylindrical
/// projection matrix instead of its cartesian offset with respect to the
/// scroll offset.
......@@ -130,13 +130,13 @@ class RenderListWheelViewport
/// An arbitrary but aesthetically reasonable default value for [diameterRatio].
static const double defaultDiameterRatio = 2.0;
/// Ar arbitrary but aesthetically reasonable default value for [perspective].
/// An arbitrary but aesthetically reasonable default value for [perspective].
static const double defaultPerspective = 0.003;
/// An error message to show when the provided [diameterRatio] is zero.
static const String diameterRatioZeroMessage = "You can't set a diameterRatio "
'of 0. It would imply a cylinder of 0 in diameter in which case nothing '
'will be drawn.';
'of 0 or of a negative number. It would imply a cylinder of 0 in diameter '
'in which case nothing will be drawn.';
/// An error message to show when the [perspective] value is too high.
static const String perspectiveTooHighMessage = 'A perspective too high will '
......@@ -563,7 +563,9 @@ class RenderListWheelViewport
);
context.pushTransform(
needsCompositing,
// Text with TransformLayers and no cullRects currently have an issue rendering
// https://github.com/flutter/flutter/issues/14224.
false,
offset,
_centerOriginTransform(transform),
// Pre-transform painting function.
......@@ -591,9 +593,12 @@ class RenderListWheelViewport
return result;
}
/// This returns the matrices relative to the **untransformed plane's viewport
/// painting coordinates** system.
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.translate(0.0, _getUntransformedPaintingCoordinateY(0.0));
final ListWheelParentData parentData = child?.parentData;
transform.translate(0.0, _getUntransformedPaintingCoordinateY(parentData.offset.dy));
}
@override
......
......@@ -99,7 +99,8 @@ class PageController extends ScrollController {
);
assert(
positions.length == 1,
'Multiple PageViews cannot be attached to the same PageController.',
'The page property cannot be read when multiple PageViews are attached to '
'the same PageController.',
);
final _PagePosition position = this.position;
return position.page;
......
// 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('layout', () {
testWidgets('selected item is in the middle', (WidgetTester tester) async {
final FixedExtentScrollController controller =
new FixedExtentScrollController(initialItem: 1);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Align(
alignment: Alignment.topLeft,
child: new SizedBox(
height: 300.0,
width: 300.0,
child: new CupertinoPicker(
scrollController: controller,
itemExtent: 50.0,
onSelectedItemChanged: (_) {},
children: new List<Widget>.generate(3, (int index) {
return new Container(
height: 50.0,
width: 300.0,
child: new Text(index.toString()),
);
}),
),
),
),
),
);
expect(
tester.getTopLeft(find.widgetWithText(Container, '1')),
const Offset(0.0, 125.0),
);
controller.jumpToItem(0);
await tester.pump();
expect(
tester.getTopLeft(find.widgetWithText(Container, '1')),
const Offset(0.0, 175.0),
);
expect(
tester.getTopLeft(find.widgetWithText(Container, '0')),
const Offset(0.0, 125.0),
);
});
});
group('scroll', () {
testWidgets('a drag in between items settles back', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
final FixedExtentScrollController controller =
new FixedExtentScrollController(initialItem: 10);
final List<int> selectedItems = <int>[];
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new CupertinoPicker(
scrollController: controller,
itemExtent: 100.0,
onSelectedItemChanged: (int index) { selectedItems.add(index); },
children: new List<Widget>.generate(100, (int index) {
return new Center(
child: new Container(
width: 400.0,
height: 100.0,
child: new Text(index.toString()),
),
);
}),
),
),
);
// Drag it by a bit but not enough to move to the next item.
await tester.drag(find.text('10'), const Offset(0.0, 30.0));
// The item that was in the center now moved a bit.
expect(
tester.getTopLeft(find.widgetWithText(Container, '10')),
const Offset(200.0, 280.0),
);
await tester.pumpAndSettle();
expect(
tester.getTopLeft(find.widgetWithText(Container, '10')).dy,
moreOrLessEquals(250.0, epsilon: 0.5),
);
expect(selectedItems.isEmpty, true);
// Drag it by enough to move to the next item.
await tester.drag(find.text('10'), const Offset(0.0, 70.0));
await tester.pumpAndSettle();
expect(
tester.getTopLeft(find.widgetWithText(Container, '10')).dy,
// It's down by 100.0 now.
moreOrLessEquals(350.0, epsilon: 0.5),
);
expect(selectedItems, <int>[9]);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('a big fling that overscrolls springs back', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
final FixedExtentScrollController controller =
new FixedExtentScrollController(initialItem: 10);
final List<int> selectedItems = <int>[];
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new CupertinoPicker(
scrollController: controller,
itemExtent: 100.0,
onSelectedItemChanged: (int index) { selectedItems.add(index); },
children: new List<Widget>.generate(100, (int index) {
return new Center(
child: new Container(
width: 400.0,
height: 100.0,
child: new Text(index.toString()),
),
);
}),
),
),
);
// A wild throw appears.
await tester.fling(
find.text('10'),
const Offset(0.0, 10000.0),
1000.0,
);
expect(
tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
// Should have been flung far enough to go off screen.
greaterThan(600.0),
);
expect(
selectedItems,
// This specific throw was fast enough that each scroll update landed
// on every second item.
<int>[8, 6, 4, 2, 0],
);
// Let it spring back.
await tester.pumpAndSettle();
expect(
tester.getTopLeft(find.widgetWithText(Container, '0')).dy,
// Should have sprung back to the middle now.
moreOrLessEquals(250.0),
);
expect(
selectedItems,
// Falling back to 0 shouldn't produce more callbacks.
<int>[8, 6, 4, 2, 0],
);
debugDefaultTargetPlatformOverride = null;
});
});
}
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