| // 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/services.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| /// Color of the 'magnifier' lens border. |
| const Color _kHighlighterBorder = Color(0xFF7F7F7F); |
| const Color _kDefaultBackground = Color(0xFFD2D4DB); |
| // Eyeballed values comparing with a native picker. |
| // Values closer to PI produces denser flatter lists. |
| const double _kDefaultDiameterRatio = 1.35; |
| const double _kDefaultPerspective = 0.004; |
| /// 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 its 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 { |
| /// Creates a picker from a concrete list of children. |
| /// |
| /// The [diameterRatio] and [itemExtent] arguments must not be null. The |
| /// [itemExtent] must be greater than zero. |
| /// |
| /// The [backgroundColor] defaults to light gray. It can be set to null to |
| /// disable the background painting entirely; this is mildly more efficient |
| /// than using [Colors.transparent]. |
| /// |
| /// The [looping] argument decides whether the child list loops and can be |
| /// scrolled infinitely. If set to true, scrolling past the end of the list |
| /// will loop the list back to the beginning. If set to false, the list will |
| /// stop scrolling when you reach the end or the beginning. |
| CupertinoPicker({ |
| 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 List<Widget> children, |
| bool looping = false, |
| }) : assert(children != null), |
| assert(diameterRatio != null), |
| assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage), |
| assert(magnification > 0), |
| assert(itemExtent != null), |
| assert(itemExtent > 0), |
| childDelegate = looping |
| ? ListWheelChildLoopingListDelegate(children: children) |
| : ListWheelChildListDelegate(children: children), |
| super(key: key); |
| |
| /// Creates a picker from an [IndexedWidgetBuilder] callback where the builder |
| /// is dynamically invoked during layout. |
| /// |
| /// A child is lazily created when it starts becoming visible in the viewport. |
| /// All of the children provided by the builder are cached and reused, so |
| /// normally the builder is only called once for each index (except when |
| /// rebuilding - the cache is cleared). |
| /// |
| /// The [itemBuilder] argument must not be null. The [childCount] argument |
| /// reflects the number of children that will be provided by the [itemBuilder]. |
| /// {@macro flutter.widgets.wheelList.childCount} |
| /// |
| /// The [itemExtent] argument must be non-null and positive. |
| /// |
| /// The [backgroundColor] defaults to light gray. It can be set to null to |
| /// disable the background painting entirely; this is mildly more efficient |
| /// than using [Colors.transparent]. |
| CupertinoPicker.builder({ |
| 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 IndexedWidgetBuilder itemBuilder, |
| int childCount, |
| }) : assert(itemBuilder != null), |
| assert(diameterRatio != null), |
| assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage), |
| assert(magnification > 0), |
| assert(itemExtent != null), |
| assert(itemExtent > 0), |
| childDelegate = ListWheelChildBuilderDelegate(builder: itemBuilder, childCount: childCount), |
| 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. |
| /// |
| /// This can be set to null to disable the background painting entirely; this |
| /// 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. |
| 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; |
| |
| /// A delegate that lazily instantiates children. |
| final ListWheelChildDelegate childDelegate; |
| |
| @override |
| State<StatefulWidget> createState() => _CupertinoPickerState(); |
| } |
| |
| class _CupertinoPickerState extends State<CupertinoPicker> { |
| int _lastHapticIndex; |
| |
| void _handleSelectedItemChanged(int index) { |
| // Only the haptic engine hardware on iOS devices would produce the |
| // intended effects. |
| if (defaultTargetPlatform == TargetPlatform.iOS |
| && index != _lastHapticIndex) { |
| _lastHapticIndex = index; |
| HapticFeedback.selectionClick(); |
| } |
| |
| if (widget.onSelectedItemChanged != null) { |
| widget.onSelectedItemChanged(index); |
| } |
| } |
| |
| /// Makes the fade to white edge gradients. |
| Widget _buildGradientScreen() { |
| return Positioned.fill( |
| child: IgnorePointer( |
| child: Container( |
| decoration: const BoxDecoration( |
| gradient: LinearGradient( |
| colors: <Color>[ |
| Color(0xFFFFFFFF), |
| Color(0xF2FFFFFF), |
| Color(0xDDFFFFFF), |
| Color(0x00FFFFFF), |
| Color(0x00FFFFFF), |
| Color(0xDDFFFFFF), |
| Color(0xF2FFFFFF), |
| Color(0xFFFFFFFF), |
| ], |
| stops: <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 IgnorePointer( |
| child: Column( |
| children: <Widget>[ |
| Expanded( |
| child: Container( |
| color: foreground, |
| ), |
| ), |
| Container( |
| decoration: const BoxDecoration( |
| border: Border( |
| top: BorderSide(width: 0.0, color: _kHighlighterBorder), |
| bottom: BorderSide(width: 0.0, color: _kHighlighterBorder), |
| ) |
| ), |
| constraints: BoxConstraints.expand( |
| height: widget.itemExtent * widget.magnification, |
| ), |
| ), |
| Expanded( |
| child: Container( |
| color: foreground, |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| 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, |
| ), |
| ), |
| _buildGradientScreen(), |
| _buildMagnifierScreen(), |
| ], |
| ); |
| if (widget.backgroundColor != null) { |
| result = DecoratedBox( |
| decoration: BoxDecoration( |
| color: widget.backgroundColor, |
| ), |
| child: result, |
| ); |
| } |
| return result; |
| } |
| } |