flutter中好用的Widget-CupertinoPicker
Posted 一叶飘舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了flutter中好用的Widget-CupertinoPicker相关的知识,希望对你有一定的参考价值。
简介
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),
],
),
),
),
);
以上是关于flutter中好用的Widget-CupertinoPicker的主要内容,如果未能解决你的问题,请参考以下文章
强力推荐:一个好用的Flutter与原生应用通讯的开源框架!
Flutter 3.3 之 SelectionArea 好不好用?用 "Bug" 带您全面了解它 | 开发者说·DTalk
Flutter 3.3 之 SelectionArea 好不好用?用 "Bug" 带您全面了解它 | 开发者说·DTalk