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)),
            ),

简单的使用只需实现以上三个参数:

  1. itemExtent :子项高度,选中位置的高度。
  2. children: 子widget组。
  3. 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 SDK版本管理神器

Flutter好用的图片库

强力推荐:一个好用的Flutter与原生应用通讯的开源框架!

Flutter 3.3 之 SelectionArea 好不好用?用 "Bug" 带您全面了解它 | 开发者说·DTalk

Flutter 3.3 之 SelectionArea 好不好用?用 "Bug" 带您全面了解它 | 开发者说·DTalk