简介
Cupertino (库比蒂诺)是一个地名,苹果电脑的全球总公司所在地。CupertinoPicker是一个ios风格的齿轮滚动的选择器,常用于日期地址选择。
效果图
用法
CupertinoPicker( itemExtent: 28, onSelectedItemChanged: (position) { print('The position is $position'); }, children: getListWidgets(10,Constants.default_min_cycle_day)), ),
简单的使用只需实现以上三个参数:
- itemExtent :子项高度,选中位置的高度。
- children: 子widget组。
- onSelectedItemChanged: 滚动选择的回调,每次滚动,都会触发此回调,会将选中的子widget的position返回。
CupertinoPicker.builder({ Key key, this.diameterRatio = _kDefaultDiameterRatio, this.backgroundColor, this.offAxisFraction = 0.0, this.useMagnifier = false, this.magnification = 1.0, this.scrollController, this.squeeze = _kSqueeze, @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), assert(squeeze != null), assert(squeeze > 0), childDelegate = ListWheelChildBuilderDelegate(builder: itemBuilder, childCount: childCount), super(key: key);
其他参数:
1.diameterRatio:直径比,double类型。
2. backgroundColor,背景颜色。
3. offAxisFraction,轴偏移,默认是0.0。控制选中的子widget的左右偏移
4. useMagnifier: 放大效果,默认false。
5. magnification: 放大倍数,需先开启放大效果,此参数才有作用。
6. scrollController:控制器
7.squeeze:压缩,这个控制的children之间的空隙,和diameterRatio的效果有相似之处。
flutter作为跨平台UI框架,最出色的莫过于快速构建出想要的UI效果。这个CupertinoPicker使用简单,操作方便。
升级到flutter 2.0之后,CupertinoPicker的item样式有所改变,不再是默认的上下横线分割样式,而是变成了圆角灰色背景。
CupertinoPicker 在Flutter 2.0 中的源码如下:
CupertinoPicker.builder({ Key? key, this.diameterRatio = _kDefaultDiameterRatio, this.backgroundColor, this.offAxisFraction = 0.0, this.useMagnifier = false, this.magnification = 1.0, this.scrollController, this.squeeze = _kSqueeze, required this.itemExtent, required this.onSelectedItemChanged, required NullableIndexedWidgetBuilder itemBuilder, int? childCount, this.selectionOverlay = const CupertinoPickerDefaultSelectionOverlay(), }) : assert(itemBuilder != null), assert(diameterRatio != null), assert(diameterRatio > 0.0, RenderListWheelViewport.diameterRatioZeroMessage), assert(magnification > 0), assert(itemExtent != null), assert(itemExtent > 0), assert(squeeze != null), assert(squeeze > 0), childDelegate = ListWheelChildBuilderDelegate(builder: itemBuilder, childCount: childCount), super(key: key);
与1.*版本不同的是,多了个 selectionOverlay 字段,也就是选中样式。2.0中选中样式默认是CupertinoPickerDefaultSelectionOverlay。
this.selectionOverlay = const CupertinoPickerDefaultSelectionOverlay()
CupertinoPickerDefaultSelectionOverlay也是个widget,代码如下:
class CupertinoPickerDefaultSelectionOverlay extends StatelessWidget { const CupertinoPickerDefaultSelectionOverlay({ Key? key, this.background = CupertinoColors.tertiarySystemFill, this.capLeftEdge = true, this.capRightEdge = true, }) : assert(background != null), assert(capLeftEdge != null), assert(capRightEdge != null), super(key: key); ...... @override Widget build(BuildContext context) { const Radius radius = Radius.circular(_defaultSelectionOverlayRadius); return Container( margin: EdgeInsets.only( left: capLeftEdge ? _defaultSelectionOverlayHorizontalMargin : 0, right: capRightEdge ? _defaultSelectionOverlayHorizontalMargin : 0, ), decoration: BoxDecoration( borderRadius: BorderRadius.horizontal( left: capLeftEdge ? radius : Radius.zero, right: capRightEdge ? radius : Radius.zero, ), color: CupertinoDynamicColor.resolve(background, context), ), ); } }
如果还想要1.0的上下横线分割的样式,可以参考1.0中CupertinoPicker的源码,关键代码如下所示:
/// Draws the magnifier borders. Widget _buildMagnifierScreen() { final Color resolvedBorderColor = CupertinoDynamicColor.resolve(_kHighlighterBorder, context); return IgnorePointer( child: Center( child: Container( decoration: BoxDecoration( border: Border( top: BorderSide(width: 0.0, color: resolvedBorderColor), bottom: BorderSide(width: 0.0, color: resolvedBorderColor), ), ), constraints: BoxConstraints.expand( height: widget.itemExtent * widget.magnification, ), ), ), ); }
当然还可以自定义,代码如下所示:
// 中间分割线 Widget _selectionOverlayWidget(){ return Padding( padding: EdgeInsets.only(left: 0, right: 0), child: Column( children: [ Divider( height: 1, color: AppColor.green86Color, ), Expanded(child: Container()), Divider( height: 1, color: AppColor.green86Color, ), ], ), ); }
使用:
CupertinoPicker( key: key, useMagnifier: true, magnification: 1.2, selectionOverlay: _selectionOverlayWidget(), itemExtent: 34, onSelectedItemChanged: (v){}, children: models.map((e) => _itemsWidget(e.name)).toList()), ))
延伸:
flutter 自定义城市选择器
因为城市选择的数据是从服务器上拿的的,在pub上面也没有找到合适插件,索性就自己写了一个,在写的过程也遇到很多问题,其实就是三个 CupertinoPicker 组合在一起的,当时写的过程中发现 CupertinoPicker setState不更新 以及onSelectedItemChanged 调用的问题
CupertinoPicker 不更新可以通过 GlobalKey 来解决 onSelectedItemChanged 调用问题可以通过 NotificationListener监听来拿到当前的索引
这里分享一下实现代码
import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:zhengda_health/app/custom_widgets/custom_text.dart'; import 'package:zhengda_health/app/http_util/http_api.dart'; import 'package:zhengda_health/app/http_util/http_util.dart'; import 'package:zhengda_health/app/support/app_color.dart'; //省市区类型 enum CityType { province, city, area } class CityAlertView extends StatefulWidget { CityAlertViewDelegate delegate; CityAlertView({Key key,this.delegate}) : super(key: key); @override _CityAlertViewState createState() => _CityAlertViewState(); } class _CityAlertViewState extends State<CityAlertView> { List <CityAlertModel> _provinceList = []; List <CityAlertModel> _cityList = []; List <CityAlertModel> _areaList = []; GlobalKey _provinceGlobalKey = GlobalKey(); GlobalKey _cityGlobalKey = GlobalKey(); GlobalKey _areaGlobalKey = GlobalKey(); int _provinceIndex = 0; int _cityIndex = 0; int _areaIndex = 0; @override void initState() { // TODO: implement initState super.initState(); _getAreaData(cityType:CityType.province,pid: '0' ,onSuccess: (){ _getAreaData(cityType: CityType.city,pid: _provinceList.first.adcode,onSuccess: (){ _getAreaData(cityType: CityType.area,pid: _cityList.first.adcode,onSuccess: (){ }); }); }); } void _getAreaData({CityType cityType,String pid,Function onSuccess}){ HttpUtil.getHttp('${HttpApi.areaInfo}?pid=$pid',onSuccess: (res){ List<CityAlertModel> list =List<CityAlertModel>.from(res['areaLists'].map((it) => CityAlertModel.fromJson(it))); if(cityType == CityType.province){ _provinceGlobalKey =GlobalKey();; _provinceList = list ; }else if(cityType == CityType.city){ _cityGlobalKey =GlobalKey();; _cityList =list; }else{ _areaGlobalKey =GlobalKey();; _areaList = list; } setState(() {}); onSuccess(); }); } //确定生成回调 void _confirmClick(BuildContext context ){ if(widget.delegate != null){ widget.delegate.confirmClick([_provinceList[_provinceIndex],_cityList[_cityIndex],_areaList[_areaIndex]]); } Navigator.of(context).pop(); } //取消 void _canlClick(BuildContext context){ Navigator.of(context).pop(); } @override Widget build(BuildContext context) { return SafeArea(child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 8,), _headerWidget(context), Row( children: [ _pickerViewWidget(models:_provinceList, key:_provinceGlobalKey , onSelectedItemChanged: (v){ _provinceIndex = v; _getAreaData(cityType: CityType.city,pid: _provinceList[v].adcode,onSuccess: (){ _getAreaData(cityType: CityType.area,pid: _cityList.first.adcode,onSuccess: (){ }); }); }), _pickerViewWidget(models: _cityList,key:_cityGlobalKey,onSelectedItemChanged: (v){ _cityIndex = v; _getAreaData(cityType: CityType.area,pid: _cityList[v].adcode,onSuccess: (){}); } ), _pickerViewWidget(models: _areaList,key:_areaGlobalKey,onSelectedItemChanged: (v){ _areaIndex =v; } ), ], ) ], )); } Widget _headerWidget(BuildContext context){ return Row( children: [ _buttonWidget(title: '取消',textColor: Colors.black38,callback: (){ _canlClick(context); }), Expanded(child: Container()), _buttonWidget(title: '确定',textColor: Colors.black,callback: (){ _confirmClick(context); }), ], ); } //piceerView Widget _pickerViewWidget({List<CityAlertModel> models,Key key, ValueChanged<int> onSelectedItemChanged,}){ return Expanded( child: SizedBox( height: 200, child: NotificationListener( onNotification: (Notification scrollNotification) { if (scrollNotification is ScrollEndNotification && scrollNotification.metrics is FixedExtentMetrics) { print((scrollNotification.metrics as FixedExtentMetrics).itemIndex); // Index of the list onSelectedItemChanged((scrollNotification.metrics as FixedExtentMetrics).itemIndex); return true; } else { return false; } }, child: CupertinoPicker( key: key, useMagnifier: true, magnification: 1.2, selectionOverlay: _selectionOverlayWidget(), itemExtent: 34, onSelectedItemChanged: (v){}, children: models.map((e) => _itemsWidget(e.name)).toList()), )) ); } // 中间分割线 Widget _selectionOverlayWidget(){ return Padding( padding: EdgeInsets.only(left: 0, right: 0), child: Column( children: [ Divider( height: 1, color: AppColor.green86Color, ), Expanded(child: Container()), Divider( height: 1, color: AppColor.green86Color, ), ], ), ); } // cellItems Widget _itemsWidget(e){ return Container( alignment: Alignment.center, child: CustomText(e,fontSize: 14,), ); } //公共button Widget _buttonWidget({String title ,Color textColor ,VoidCallback callback}){ return InkWell( onTap: callback, child: Container( alignment: Alignment.center, padding:EdgeInsets.only(left: 16,right: 16), height: 40, child: CustomText(title,color: textColor,), ), ); } } abstract class CityAlertViewDelegate{ void confirmClick(List<CityAlertModel> models){} } class CityAlertModel { int id; String pAdcode; String adcode; String name; String level; String pinyin; String first; String lng; String lat; CityAlertModel( {this.id, this.pAdcode, this.adcode, this.name, this.level, this.pinyin, this.first, this.lng, this.lat}); CityAlertModel.fromJson(Map<String, dynamic> json) { id = json['id']; pAdcode = json['p_adcode']; adcode = json['adcode']; name = json['name']; level = json['level']; pinyin = json['pinyin']; first = json['first']; lng = json['lng']; lat = json['lat']; } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['id'] = this.id; data['p_adcode'] = this.pAdcode; data['adcode'] = this.adcode; data['name'] = this.name; data['level'] = this.level; data['pinyin'] = this.pinyin; data['first'] = this.first; data['lng'] = this.lng; data['lat'] = this.lat; return data; } }
调用
class _AddAddressPageState extends State<AddAddressPage> implements CityAlertViewDelegate 要implements 实现回调协议 //回调省市区 @override void confirmClick(List<CityAlertModel> models) {} //调用弹框 showModalBottomSheet( context: context, shape: RoundedRectangleBorder( borderRadius: BorderRadiusDirectional.circular(10)), builder: (BuildContext context) { return CityAlertView(delegate: this,); });
CupertinoPicker组件的二次封装
SinglePickerWidget
SinglePickerWidget是我封装的组件之一,主要是为了实现UI设计的picker效果,效果图如下,需要单位和值分开:
正常的Flutter CupertinoPicker组件是没办法实现的:
所以对CupertinoPicker组件进行了二次封装。
功能实现思路
首先说下我的实现思路,是把单位用Position组件包裹,然后进行定位到选中的一行位置。这样滑动单位的区域也会滑动SinglePickerWidget组件。
代码
由于是为了实现UI设计需求,所以组件没有做的很灵活,具体使用时还需要更改部分代码。
import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; /// /// Author: chengzan /// Date: 2020-6-8 /// Describe: 单个选择picker /// class SinglePickerWidget extends StatefulWidget { final List<Map> values; final value; final double itemHeight; final double height; final double width; final String unit; final Function onChanged; final Color backgroundColor; const SinglePickerWidget( {Key key, @required this.values, @required this.value, @required this.onChanged, this.unit, this.itemHeight = 37.5, this.backgroundColor = const Color(0xffffffff), this.height = 150.0, this.width = 150.0}) : super(key: key); @override _SinglePickerWidgetState createState() => _SinglePickerWidgetState(); } class _SinglePickerWidgetState extends State<SinglePickerWidget> { int _selectedColorIndex = 0; FixedExtentScrollController scrollController; var values; var value; //设置防抖周期为300毫秒 Duration durationTime = Duration(milliseconds: 300); Timer timer; @override void initState() { super.initState(); values = widget.values; value = widget.value; getDefaultValue(); scrollController = FixedExtentScrollController(initialItem: _selectedColorIndex); } @override void dispose() { super.dispose(); scrollController.dispose(); timer?.cancel(); } // 获取默认选择值 getDefaultValue() { // 查找要选择的默认值 for (var i = 0; i < values.length; i++) { if (values[i]["value"] == value) { setState(() { _selectedColorIndex = i; }); break; } } } // 触发值改变 void _changed(index) { timer?.cancel(); timer = new Timer(durationTime, () { // 触发回调函数 widget.onChanged(values[index]["value"]); }); } Widget _buildColorPicker(BuildContext context) { return Container( height: widget.height, color: Colors.white, child: Stack( alignment: Alignment.center, children: [ widget.unit != null ? Positioned( top: widget.height / 2 - (widget.itemHeight / 2), left: widget.width / 2 + 18.0, child: Container( alignment: Alignment.center, height: widget.itemHeight, child: Text( widget.unit, style: TextStyle( color: Color(0xff333333), fontSize: 16.0, height: 1.5, fontWeight: FontWeight.w500, ), ), ), ) : Offstage( offstage: true, ), CupertinoPicker( magnification: 1.0, // 整体放大率 scrollController: scrollController, // 用于读取和控制当前项的FixedxtentScrollController itemExtent: widget.itemHeight, // 所有子节点 统一高度 useMagnifier: true, // 是否使用放大效果 backgroundColor: Colors.transparent, onSelectedItemChanged: (int index) { // 当正中间选项改变时的回调 if (mounted) { print('index--------------$index'); _changed(index); } }, children: List<Widget>.generate(values.length, (int index) { return Container( alignment: Alignment.center, height: widget.itemHeight, child: Text( values[index]["label"], style: TextStyle( color: Color(0xff333333), fontSize: 21.0, height: 1.2, fontWeight: FontWeight.w500, ), ), ); }), ), ], ), ); } @override Widget build(BuildContext context) { return SizedBox( height: widget.height, width: widget.width, child: CupertinoPageScaffold( child: Container( child: ListView( padding: EdgeInsets.all(0), children: <Widget>[ _buildColorPicker(context), ], ), ), ), ); } }
使用
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. 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:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { List<Map> values = [ {"label": "1", "value": 1}, {"label": "2", "value": 2}, {"label": "3", "value": 3}, {"label": "4", "value": 4}, {"label": "5", "value": 5} ]; @override Widget build(BuildContext context) { return Row( children: [ Container( constraints: BoxConstraints(maxWidth: 160), alignment: Alignment.centerLeft, child: SinglePickerWidget( values: values, value: 2, width: 150, itemHeight: 50, height: 250, unit: 'm', onChanged: (val) { print('val----$val'); }, ), ) ], ); } } /// /// Author: chengzan /// Date: 2020-6-8 /// Describe: 单个选择picker /// class SinglePickerWidget extends StatefulWidget { final List<Map> values; final value; final double itemHeight; final double height; final double width; final String unit; final Function onChanged; final Color backgroundColor; const SinglePickerWidget( {Key key, @required this.values, @required this.value, @required this.onChanged, this.unit, this.itemHeight = 37.5, this.backgroundColor = const Color(0xffffffff), this.height = 150.0, this.width = 150.0}) : super(key: key); @override _SinglePickerWidgetState createState() => _SinglePickerWidgetState(); } class _SinglePickerWidgetState extends State<SinglePickerWidget> { int _selectedColorIndex = 0; FixedExtentScrollController scrollController; var values; var value; //设置防抖周期为300毫秒 Duration durationTime = Duration(milliseconds: 300); Timer timer; @override void initState() { super.initState(); values = widget.values; value = widget.value; getDefaultValue(); scrollController = FixedExtentScrollController(initialItem: _selectedColorIndex); } @override void dispose() { super.dispose(); scrollController.dispose(); timer?.cancel(); } // 获取默认选择值 getDefaultValue() { // 查找要选择的默认值 for (var i = 0; i < values.length; i++) { if (values[i]["value"] == value) { setState(() { _selectedColorIndex = i; }); break; } } } // 触发值改变 void _changed(index) { timer?.cancel(); timer = new Timer(durationTime, () { // 回调函数 widget.onChanged(values[index]["value"]); }); } Widget _buildColorPicker(BuildContext context) { return Container( height: widget.height, color: Colors.white, child: Stack( alignment: Alignment.center, children: [ widget.unit != null ? Positioned( top: widget.height / 2 - (widget.itemHeight / 2), left: widget.width / 2 + 18.0, child: Container( alignment: Alignment.center, height: widget.itemHeight, child: Text( widget.unit, style: TextStyle( color: Color(0xff333333), fontSize: 16.0, height: 1.5, fontWeight: FontWeight.w500, ), ), ), ) : Offstage( offstage: true, ), CupertinoPicker( magnification: 1.0, // 整体放大率 scrollController: scrollController, // 用于读取和控制当前项的FixedxtentScrollController itemExtent: widget.itemHeight, // 所以子节点 统一高度 useMagnifier: true, // 是否使用放大效果 backgroundColor: Colors.transparent, onSelectedItemChanged: (int index) { // 当正中间选项改变时的回调 if (mounted) { print('index--------------$index'); _changed(index); } }, children: List<Widget>.generate(values.length, (int index) { return Container( alignment: Alignment.center, height: widget.itemHeight, child: Text( values[index]["label"], style: TextStyle( color: Color(0xff333333), fontSize: 21.0, height: 1.2, fontWeight: FontWeight.w500, ), ), ); }), ), ], ), ); } @override Widget build(BuildContext context) { return SizedBox( height: widget.height, width: widget.width, child: CupertinoPageScaffold( child: Container( child: ListView( padding: EdgeInsets.all(0), children: <Widget>[ _buildColorPicker(context), ], ), ), ), ); } }