├── .gitignore ├── .pubignore ├── CHANGELOG.md ├── LICENSE ├── README-en.md ├── README.md ├── city_pickers.iml ├── doc └── index.md ├── example ├── .gitignore ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── city_pickers │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ └── wip.jpeg ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ ├── main.dart │ ├── meta │ │ ├── province.dart │ │ └── province_nm.dart │ ├── src │ │ ├── attr_item_container.dart │ │ ├── color_picker.dart │ │ ├── item_container.dart │ │ ├── location_selector.dart │ │ └── picker.dart │ └── view │ │ ├── show_city_picker.dart │ │ ├── show_full_page_picker.dart │ │ ├── util_getLocationInfo.dart │ │ └── wip.dart ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png │ ├── index.html │ └── manifest.json ├── lib ├── city_pickers.dart ├── meta │ ├── _province.dart │ └── province.dart ├── modal │ ├── base_citys.dart │ ├── point.dart │ └── result.dart └── src │ ├── base │ ├── base.dart │ └── pickers.dart │ ├── cities_selector │ ├── alpha.dart │ ├── cities_selector.dart │ ├── cities_style.dart │ ├── types.dart │ └── utils.dart │ ├── city_picker.dart │ ├── full_page │ └── full_page.dart │ ├── mod │ ├── inherit_process.dart │ └── picker_popup_route.dart │ ├── show_types.dart │ ├── util.dart │ └── utils │ ├── index.dart │ └── location.dart ├── pubspec.yaml └── test └── unit_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | pubspec.lock 7 | 8 | build/ 9 | .idea/ 10 | .metadata 11 | #city_pickers.iml 12 | #lib/meta/_province.dart 13 | example/ios/Flutter/flutter_export_environment.sh 14 | android/local.properties 15 | /file.txt 16 | 17 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | ./city_pickers.iml 2 | ./lib/meta/_province.dart 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.3.0 2 | - 升级demo, 支持flutter3.0 3 | - 废弃 accentColor 属性 4 | - 修正readme 5 | 6 | # 1.2.0 7 | - 将地区码处理过程从int变成string, 方便用户自定义如 "01100" 被误成成 1100 8 | - 城市选择器增加了搜索栏目, 支持名称与拼音查询 9 | - 优化根目录android中的内容 10 | 11 | # 1.1.3 12 | - 修复IOS选择器. 滑动city时卡顿报错. 13 | 14 | # 1.1.2 15 | - 修复重庆字母 16 | 17 | # 1.1.1 18 | - 增加webdemo 19 | - 修正使用demo 20 | - 更新依赖版本 21 | # 1.1.0 22 | - 修复全屏下的三级联动中主题样式不正确的问题 23 | - 城市选择器 增加自定义AppBar 24 | - 增加showCityPicker下的容器圆角配置. 25 | - 适配 flutter 3.0 删掉api 26 | - 修复快速点击. 导致页面黑屏挂掉的bug. 27 | - 省市县三级选择器 增加页面背景色 28 | # 1.0.1 29 | - unsafe包正式发布 30 | - 更新unsafe的问题 31 | - 修正因为更新unsafe导致的null等逻辑问题 32 | - 更新unsafe导致升级大版本的问题 33 | 34 | # 1.0.0 35 | - 这是一个beta版本的包. 需要废弃 36 | - 将plugin 转为 package 包 37 | - 更新unsafe的问题 38 | - base方法兼容二级情况. 39 | - 放开放有兼容的逻辑报错. 比如台湾的特殊处理 40 | - 删除src/android & src/ios等资源 41 | 42 | # 0.2.0 43 | - 更新四级联动, 更新依赖 44 | - 更新最新flutter与dart依赖 45 | 46 | ## 0.1.30 47 | - 解决无法编译打包的问题. 48 | - 解决example无法构建的问题 49 | 50 | 51 | ## 0.1.29 52 | - 更新 compileSdkVersion 到28 53 | 54 | ## 0.1.28 55 | - 解决多个地方使用时选择内容为第一次打开的那个内容, 去除缓存 56 | 57 | ## 0.1.27 58 | - 解决 `getAreaResultByCode` 中. 错误赋值导致的bug. #65 59 | 60 | ## 0.1.26 61 | - 暴露utils函数接口. 提供 `getAreaResultByCode` 方法 62 | ## 0.1.25 63 | - 更新文档, hotTags => hotCities 64 | 65 | ## 0.1.24 66 | - 解决ios风格的城市选择器, 在自定义顶部按钮状态下的文字溢出问题 67 | - 为字母定位城市选择器, 增加属性 **hotCities (List)** 68 | ## 0.1.23 69 | - 解决ios风格模拟器. 在某些主题色下, 无法正常显示数据的问题 70 | 71 | ## 0.1.22 72 | - 更新最新的城市数据. 73 | 74 | ## 0.1.21 75 | - 解决字母级城市选择器, 无法自定义城市数据的问题 76 | 77 | ## 0.1.20 78 | - showCityPicker增加isSort属性. 将省份数据是否排序做配置 79 | 80 | ## 0.1.19 81 | - 处理代码规范问题 82 | ## 0.1.18 83 | - 解决ios风格城市选择器字体无法自定义的问题 84 | - 开放ios风格城市选择器, 头部按钮可能会随主题色显示不出的问题, 支持用户自定义头部按钮 85 | 86 | ## 0.1.17 87 | - 暴露城市与省份数据到CityPickers上 88 | 89 | ## 0.1.16 90 | - 增加 **showCitiesSelector** 函数方法, 城市级选择器. 支持右侧拼音首字母定位 91 | ## 0.1.15 92 | - 解决使用非标准数据源, 导致的省份数据报错的问题 93 | ## 0.1.14 94 | - 解决用户给出的location不正确, 导致的报错 95 | ## 0.1.13 96 | - 更新文档 97 | ## 0.1.12 98 | - 解决自定义数据源, 报错的问题 99 | - 加入自定义数据源的Example配置 100 | ## 0.1.10 101 | - 优化当市级无选项时的显示逻辑 102 | ## 0.1.9 103 | - 修复 https://github.com/hanxu317317/city_pickers/issues/5 初始化child为空的Point时,会报错 104 | 105 | ## 0.1.3 106 | - 更新readme中的效果图 107 | ## 0.1.2 108 | - 加入省市县三级全屏效果 109 | - 更新文档介绍 110 | - 完善ios选择器的参数 111 | 112 | ## 0.0.1 113 | 114 | * 加入三级联动 115 | - 支持配置高度 116 | - 支持初始化地理位置 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 for hanxu317317 2 | Permission is hereby granted, free of charge, to any person obtaining a copy of 3 | this software and associated documentation files (the "Software"), to deal in 4 | the Software without restriction, including without limitation the rights to 5 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 6 | of the Software, and to permit persons to whom the Software is furnished to do 7 | so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | # city_pickers 2 | 3 | this is china area selector. 4 | 5 | # Demo 6 | 7 | 8 | # Getting Started 9 | 10 | 11 | In your flutter project add the dependency: 12 | 13 | ``` 14 | dependencies: 15 | ... 16 | city_pickers:^0.0.1 17 | ``` 18 | 19 | For help getting started with Flutter, view the online [documentation](https://flutter.io/). 20 | 21 | # Usage example 22 | Import city_pickers.dart 23 | 24 | ``` 25 | import 'package:city_pickers/city_pickers.dart'; 26 | ``` 27 | 28 | Demo code to show selector 29 | 30 | ``` 31 | Result result = await CityPickers.showCityPicker( 32 | context: context, 33 | ); 34 | ``` 35 | 36 | 37 | 38 | ### CityPickers attributes 39 | 40 | |Name|Type|Desc| 41 | |:---------------|:--------|:----------| 42 | |showCityPicker|Function|the handle show selector | 43 | |showFullPageCityPicker|Function|the handle show full screen selector | 44 | 45 | 46 | ### showCityPicker params 47 | 48 | |Name|Type|Default|Desc| 49 | |:---------------|:--------|:----|:----------| 50 | |context|BuildContext|null|context| 51 | |theme|ThemeData|Theme.of(context)| theme| 52 | |locationCode|String|110000| initial location| 53 | |height|double|300| container height| 54 | |showType|ShowType|ShowType.pca| selector show type| 55 | |barrierOpacity|double|0.5|pop modal opacity| 56 | |barrierDismissible|bool|true| dismiss this route by tapping the modal barrier| 57 | 58 | 59 | ### showFullPageCityPicker params 60 | 61 | |Name|Type|Default|Desc| 62 | |:---------------|:--------|:----|:----------| 63 | |context|BuildContext|null|context| 64 | |theme|ThemeData|Theme.of(context)| theme| 65 | |locationCode|String|110000| initial location| 66 | |showType|ShowType|ShowType.pca| selector show type| 67 | 68 | 69 | 70 | # contributors 71 | 72 | 73 | 74 | 75 | 76 | # Date Statement 77 | 78 | The data information comes from [National Bureau of Statistics](http://www.stats.gov.cn/sj/tjbz/tjyqhdmhcxhfdm/2018/index.html) 79 | 80 | # Statement 81 | 82 | This project's example code and style, reference with[Flutter Go](https://github.com/alibaba/flutter-go/), **flutter go** help developers get started quickly Flutter 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Language: [English](https://github.com/hanxu317317/city_pickers/blob/master/README-en.md) 2 | # city_pickers 3 | 4 | 中国的城市三级联动选择器 5 | 6 | # Demo 7 | demo: https://pmbee.alicdn.com/flutter/city_pickers/index.html 8 | 9 | 10 | 11 | 12 | # 沟通QQ群 13 | QQ 群号码: 935915271 14 | 15 | ![image](https://user-images.githubusercontent.com/8518989/132689722-d9e9a4a0-d3af-44b1-aecc-34cdd36daaa6.png) 16 | 17 | 18 | # 开始 19 | 20 | 在flutter的项目文件中增加依赖 21 | 22 | ``` 23 | dependencies: 24 | ... 25 | city_pickers:^1.1.0 26 | ``` 27 | 28 | 关于如何运行flutter项目, 参考官方文档[documentation](https://flutter.io/). 29 | 30 | # 使用方法 31 | 32 | 简单使用方法 33 | 34 | ``` 35 | import 'package:city_pickers/city_pickers.dart'; 36 | ... 37 | // 通过钩子事件, 主动唤起浮层. 38 | Future getResult () async { 39 | Result? result = await CityPickers.showFullPageCityPicker( 40 | context: context 41 | ); 42 | print('result $a'); 43 | 44 | // type 1 45 | Result? result = await CityPickers.showCityPicker( 46 | context: context, 47 | ); 48 | // type 2 49 | Result? result2 = await CityPickers.showFullPageCityPicker( 50 | context: context, 51 | ); 52 | 53 | // type 3 54 | Result? result2 = await CityPickers.showCitiesSelector( 55 | context: context, 56 | ); 57 | return result; 58 | } 59 | ``` 60 | 61 | 62 | ## CityPickers 静态方法 63 | 64 | |Name|Type|Desc| 65 | |:---------------|:--------|:----------| 66 | |showCityPicker|Function|呼出弹出层,显示多级选择器 | 67 | |showFullPageCityPicker|Function|呼出一层界面, 显示多级选择器| 68 | |showCitiesSelector |Function|呼出一层, 显示支持字母定位城市选择器| 69 | |utils|Function|获取utils接口的钩子| 70 | 71 | 72 | 73 | ### showCityPicker 参数说明 74 | 75 | |Name|Type|Default|Desc| 76 | |:---------------|:--------|:----|:----------| 77 | |context|BuildContext||上下文对象| 78 | |theme|ThemeData|Theme.of(context)| 主题, 可以自定义| 79 | |locationCode|String|110000| 初始化地址信息, 可以是省, 市, 区的地区码| 80 | |height|double|300| 弹出层的高度, 过高或者过低会导致容器报错| 81 | |showType|ShowType|ShowType.pca| 三级联动, 显示类型| 82 | |barrierOpacity|double|0.5|弹出层的背景透明度, 应该是大于0, 小于1| 83 | |barrierDismissible|bool|true| 是否可以通过点击弹出层背景, 关闭弹出层| 84 | |cancelWidget|Widget||用户自定义取消按钮| 85 | |confirmWidget| Widget || 用户自定义确认按钮 | 86 | |itemExtent|double||目标框高度| 87 | |itemBuilder|Widget||item生成器, function(String value, List lists, item){}, 当itemBuilder不为空的时候. 必须设置itemExtent| 88 | |citiesData|Map|[城市数据](https://github.com/hanxu317317/city_pickers/blob/master/lib/meta/province.dart)|选择器的城市与区的数据源| 89 | |provincesData|Map|[省份数据](https://github.com/hanxu317317/city_pickers/blob/master/lib/meta/province.dart)|选择器的省份数据源| 90 | 91 | 92 | 93 | ### showFullPageCityPicker 参数说明 94 | 95 | |Name|Type|Default|Desc| 96 | |:---------------|:--------|:----|:----------| 97 | |context|BuildContext|null|上下文对象| 98 | |theme|ThemeData|Theme.of(context)| 主题, 可以自定义| 99 | |locationCode|String|110000| 初始化地址信息, 可以是省, 市, 区的地区码| 100 | |showType|ShowType|ShowType.pca| 三级联动, 显示类型| 101 | 102 | |citiesData|Map|[城市数据](https://github.com/hanxu317317/city_pickers/blob/master/lib/meta/province.dart)|选择器的城市与区的数据源| 103 | |provincesData|Map|[省份数据](https://github.com/hanxu317317/city_pickers/blob/master/lib/meta/province.dart)|选择器的省份数据源| 104 | 105 | ### showCitiesSelector 参数说明 106 | 107 | |Name|Type|Default|Desc| 108 | |:---------------|:--------|:----|:----------| 109 | |context|BuildContext|null|上下文对象| 110 | |theme|ThemeData|Theme.of(context)| 主题, 可以自定义| 111 | |locationCode|String|110000| 初始化地址信息, 可以是省, 市, 区的地区码| 112 | |title|String|城市选择器|弹出层界面标题| 113 | |citiesData|Map|[城市数据](https://github.com/hanxu317317/city_pickers/blob/master/lib/meta/province.dart)|选择器的城市与区的数据源| 114 | |provincesData|Map|[省份数据](https://github.com/hanxu317317/city_pickers/blob/master/lib/meta/province.dart)|选择器的省份数据源| 115 | |hotCities|List\|null|热门城市| 116 | |sideBarStyle|[BaseStyle](https://github.com/hanxu317317/city_pickers/blob/develop/lib/src/cities_selector/cities_style.dart)|初始默认样式| 右侧字母索引集样式| 117 | |cityItemStyle|[BaseStyle](https://github.com/hanxu317317/city_pickers/blob/develop/lib/src/cities_selector/cities_style.dart)|初始默认样式| 城市选项样式| 118 | |topStickStyle|[BaseStyle](https://github.com/hanxu317317/city_pickers/blob/develop/lib/src/cities_selector/cities_style.dart)|初始默认样式| 顶部索引吸顶样式| 119 | |scaffoldBackgroundColor|Colors|Colors.white| 页面背景色| 120 | |useSearchAppBar|bool|false|当AppBarBuilder=null的时候, 控制是否开启搜索框 | 121 | |appBarBuilder|Function|null|用户自定义AppBar| 122 | 123 | 124 | ### utils 说明 125 | utils 是用来封装常用的一些方法, 方便使用者能更好的使用该插件. 使用者通过以下方式声明实例, 可以**获取所有的工具类方法** 126 | 127 | ``` 128 | // 声明实例 129 | CityPickerUtil cityPickerUtils = CityPickers.utils(); 130 | ``` 131 | 132 | #### Result getAreaResultByCode(String code) 133 | 134 | 使用者通过地区ID, 获取所在区域的省市县等相关信息. 当未查询到具体信息. 返回空的Result对象. 135 | 136 | ``` 137 | print('result>>> ${cityPickerUtils.getAreaResultByCode('100100)}'); 138 | 139 | // 输出为: result>>>> {"provinceName":"北京市","provinceId":"110000","cityName":"东城区","cityId":"110101"} 140 | ``` 141 | 142 | # 数据来源 143 | 144 | - [国家统计局](http://www.stats.gov.cn/sj/tjbz/tjyqhdmhcxhfdm/2018/index.html) 145 | 146 | # 贡献者 147 | 148 | 149 | 150 | 151 | 152 | # 声明 153 | 154 | 本项目Example部份代码与样式, 参考借鉴[Flutter Go](https://github.com/alibaba/flutter-go/), **flutter go** 是flutter 开发者帮助 APP,包含 flutter 常用 140+ 组件的demo 演示与中文文档 155 | 156 | ### To Do List 157 | 158 | - [x] 城市选择器, 借鉴点评 159 | - [x] 支持拼音等模糊搜索 160 | - [ ] 加入单元测试 161 | -------------------------------------------------------------------------------- /city_pickers.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/doc/index.md -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # city_pickers_example 2 | Demonstrates how to use the city_pickers. 3 | 4 | # Quick test 5 | ``` 6 | flutter run 7 | ``` 8 | 9 | # Specific app entry point 10 | 11 | ``` 12 | flutter run -t lib/main.dart 13 | ``` 14 | 15 | # Getting Started 16 | 17 | For help getting started with Flutter, view the online [documentation](https://flutter.io). -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace "com.example.city_pickers" 30 | compileSdkVersion flutter.compileSdkVersion 31 | ndkVersion flutter.ndkVersion 32 | 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | 42 | sourceSets { 43 | main.java.srcDirs += 'src/main/kotlin' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.example.city_pickers" 49 | // You can update the following values to match your application needs. 50 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 51 | minSdkVersion flutter.minSdkVersion 52 | targetSdkVersion flutter.targetSdkVersion 53 | versionCode flutterVersionCode.toInteger() 54 | versionName flutterVersionName 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | } 73 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/city_pickers/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.city_pickers 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/assets/wip.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/assets/wip.jpeg -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | City Pickers 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | city_pickers 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'src/item_container.dart'; 3 | import 'view/show_city_picker.dart'; 4 | import 'view/wip.dart'; 5 | import 'view/show_full_page_picker.dart'; 6 | import 'view/util_getLocationInfo.dart'; 7 | 8 | void main() => runApp(const MyApp()); 9 | 10 | class MyApp extends StatefulWidget { 11 | const MyApp({Key? key}) : super(key: key); 12 | 13 | @override 14 | State createState() => _MyAppState(); 15 | } 16 | 17 | class _MyAppState extends State { 18 | var useMaterial3 = false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return MaterialApp( 23 | theme: ThemeData( 24 | colorScheme: const ColorScheme.light( 25 | primary: Color(0xFFC91B3A), 26 | background: Color(0xFFEFEFEF), 27 | secondary: Color(0xFF888888), 28 | ), 29 | textTheme: const TextTheme( 30 | //设置Material的默认字体样式 31 | bodyMedium: TextStyle(color: Color(0xFF888888), fontSize: 16.0), 32 | ), 33 | useMaterial3: useMaterial3, 34 | ), 35 | darkTheme: ThemeData.dark().copyWith( 36 | primaryColor: const Color(0xFFC91B3A), 37 | ), 38 | themeMode: ThemeMode.system, 39 | title: 'Welcome to Flutt2er', 40 | debugShowCheckedModeBanner: false, 41 | routes: { 42 | '/name': (_) => const ShowCityPicker(), 43 | '/full_page': (_) => const ShowFullPageCityPicker(), 44 | '/city_select': (_) => const WorkInProgress(), 45 | '/util_getLocationInfo': (_) => const UtilGetLocationInfo() 46 | }, 47 | home: Body( 48 | useMaterial3: useMaterial3, 49 | onUseMaterial3Changed: (v) => setState( 50 | () { 51 | useMaterial3 = v; 52 | }, 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | 59 | class Body extends StatelessWidget { 60 | const Body({ 61 | Key? key, 62 | required this.useMaterial3, 63 | required this.onUseMaterial3Changed, 64 | }) : super(key: key); 65 | 66 | final bool useMaterial3; 67 | final ValueChanged onUseMaterial3Changed; 68 | 69 | static final List _demoList = [ 70 | {"icon": Icons.place, "name": "ios选择器", "routerName": '/name'}, 71 | {"icon": Icons.fullscreen, "name": "三级全屏选择器", "routerName": '/full_page'}, 72 | { 73 | "icon": Icons.location_city, 74 | "name": "城市选择器", 75 | "routerName": '/city_select' 76 | }, 77 | { 78 | "icon": Icons.location_city, 79 | "name": "内置工具类", 80 | "routerName": '/util_getLocationInfo' 81 | }, 82 | ]; 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | return Scaffold( 87 | appBar: AppBar( 88 | title: const Text('CityPickers Examples'), 89 | actions: [ 90 | Tooltip( 91 | message: 'useMaterial3', 92 | child: Switch( 93 | value: useMaterial3, 94 | onChanged: onUseMaterial3Changed, 95 | ), 96 | ) 97 | ], 98 | ), 99 | body: ItemContainer( 100 | itemList: _demoList, 101 | )); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /example/lib/meta/province.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 26/02/2019 5 | // Time: 19:34 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: 自定义数据源 8 | // 9 | const Map provincesData = { 10 | "10": "北京市", 11 | "120": "天津市", 12 | }; 13 | 14 | const Map citiesData = { 15 | "710000": { 16 | "710100": {"name": "台湾", "alpha": "t"} 17 | }, 18 | "10": { 19 | "110100": {"name": "北京城区", "alpha": "b"} 20 | }, 21 | "110100": { 22 | "110101": {"name": "东城区", "alpha": "d"}, 23 | "110102": {"name": "西城区", "alpha": "x"}, 24 | "110105": {"name": "朝阳区", "alpha": "c"}, 25 | "110106": {"name": "丰台区", "alpha": "f"}, 26 | "110107": {"name": "石景山区", "alpha": "s"}, 27 | "110108": {"name": "海淀区", "alpha": "h"}, 28 | "110109": {"name": "门头沟区", "alpha": "m"}, 29 | "110111": {"name": "房山区", "alpha": "f"}, 30 | "110112": {"name": "通州区", "alpha": "t"}, 31 | "110113": {"name": "顺义区", "alpha": "s"}, 32 | "110114": {"name": "昌平区", "alpha": "c"}, 33 | "110115": {"name": "大兴区", "alpha": "d"}, 34 | "110116": {"name": "怀柔区", "alpha": "h"}, 35 | "110117": {"name": "平谷区", "alpha": "p"}, 36 | "110118": {"name": "密云区", "alpha": "m"}, 37 | "110119": {"name": "延庆区", "alpha": "y"} 38 | }, 39 | "120": { 40 | "120100": {"name": "天津城区", "alpha": "t"} 41 | }, 42 | "120100": { 43 | "120101": {"name": "和平区", "alpha": "h"}, 44 | "120102": {"name": "河东区", "alpha": "h"}, 45 | "120103": {"name": "河西区", "alpha": "h"}, 46 | "120104": {"name": "南开区", "alpha": "n"}, 47 | "120105": {"name": "河北区", "alpha": "h"}, 48 | "120106": {"name": "红桥区", "alpha": "h"}, 49 | "120110": {"name": "东丽区", "alpha": "d"}, 50 | "120111": {"name": "西青区", "alpha": "x"}, 51 | "120112": {"name": "津南区", "alpha": "j"}, 52 | "120113": {"name": "北辰区", "alpha": "b"}, 53 | "120114": {"name": "武清区", "alpha": "w"}, 54 | "120115": {"name": "宝坻区", "alpha": "b"}, 55 | "120116": {"name": "滨海新区", "alpha": "b"}, 56 | "120117": {"name": "宁河区", "alpha": "n"}, 57 | "120118": {"name": "静海区", "alpha": "j"}, 58 | "120119": {"name": "蓟州区", "alpha": "j"} 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /example/lib/meta/province_nm.dart: -------------------------------------------------------------------------------- 1 | // 随机选一组名字比较长的地名和一组名字比较短的地名测试4级显示 2 | const Map provincesDataNm = { 3 | "150000": "内蒙古自治区", 4 | "440000": "广东省", 5 | }; 6 | 7 | const Map citiesDataNm = { 8 | "150000": { 9 | "150600": {"name": "鄂尔多斯市", "alpha": "e"}, 10 | }, 11 | "150600": { 12 | "150602": {"name": "东胜区", "alpha": "d"}, 13 | "150603": {"name": "康巴什区", "alpha": "k"} 14 | }, 15 | "150602": { 16 | "150602001": {"name": "交通街道办事处", "alpha": "j"}, 17 | "150602002": {"name": "公园街道办事处", "alpha": "g"}, 18 | "150602003": {"name": "林荫街道办事处", "alpha": "l"}, 19 | "150602004": {"name": "建设街道办事处", "alpha": "j"}, 20 | "150602005": {"name": "富兴街道办事处", "alpha": "f"}, 21 | "150602006": {"name": "天骄街道办事处", "alpha": "t"}, 22 | "150602007": {"name": "诃额伦街道办事处", "alpha": "k"}, 23 | "150602008": {"name": "巴音门克街道办事处", "alpha": "b"}, 24 | "150602010": {"name": "幸福街道办事处", "alpha": "x"}, 25 | "150602011": {"name": "纺织街道办事处", "alpha": "f"}, 26 | "150602012": {"name": "兴盛街道办事处", "alpha": "x"}, 27 | "150602013": {"name": "民族街道办事处", "alpha": "m"}, 28 | "150602100": {"name": "泊尔江海子镇", "alpha": "b"}, 29 | "150602101": {"name": "罕台镇", "alpha": "h"}, 30 | "150602102": {"name": "铜川镇", "alpha": "t"}, 31 | "150602400": {"name": "鄂尔多斯市装备制造基地", "alpha": "e"}, 32 | "150602401": {"name": "东胜区轻纺工业园区", "alpha": "d"}, 33 | "150602404": {"name": "铜川汽车博览园", "alpha": "t"} 34 | }, 35 | "150603": { 36 | "150603001": {"name": "哈巴格希街道办事处", "alpha": "h"}, 37 | "150603002": {"name": "青春山街道办事处", "alpha": "q"}, 38 | "150603003": {"name": "滨河街道办事处", "alpha": "b"}, 39 | "150603008": {"name": "康新街道办事处", "alpha": "k"}, 40 | "150603401": {"name": "鄂尔多斯市高新技术产业园区", "alpha": "e"}, 41 | "150603402": {"name": "鄂尔多斯市云计算产业园区", "alpha": "e"} 42 | }, 43 | 44 | "440000": { 45 | "440800": {"name": "湛江市", "alpha": "z"} 46 | }, 47 | 48 | "440800": { 49 | "440883": {"name": "吴川市", "alpha": "w"} 50 | }, 51 | 52 | "440883": { 53 | "440883001": {"name": "梅录街道", "alpha": "m"}, 54 | "440883002": {"name": "塘尾街道", "alpha": "t"}, 55 | "440883003": {"name": "大山江街道", "alpha": "d"}, 56 | "440883004": {"name": "博铺街道", "alpha": "b"}, 57 | "440883005": {"name": "海滨街道", "alpha": "h"}, 58 | "440883100": {"name": "浅水镇", "alpha": "q"}, 59 | "440883101": {"name": "长岐镇", "alpha": "c"}, 60 | "440883102": {"name": "覃巴镇", "alpha": "q"}, 61 | "440883103": {"name": "王村港镇", "alpha": "w"}, 62 | "440883104": {"name": "振文镇", "alpha": "z"}, 63 | "440883105": {"name": "樟铺镇", "alpha": "z"}, 64 | "440883106": {"name": "吴阳镇", "alpha": "w"}, 65 | "440883107": {"name": "塘缀镇", "alpha": "t"}, 66 | "440883109": {"name": "黄坡镇", "alpha": "h"}, 67 | "440883111": {"name": "兰石镇", "alpha": "l"} 68 | }, 69 | 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /example/lib/src/attr_item_container.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 12/02/2019 5 | // Time: 18:06 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | 12 | class AttrItemContainer extends StatefulWidget { 13 | final String title; 14 | final Widget editor; 15 | 16 | const AttrItemContainer({Key? key, required this.title, required this.editor}) 17 | : super(key: key); 18 | 19 | @override 20 | State createState() => _AttrItemContainerState(); 21 | } 22 | 23 | class _AttrItemContainerState extends State { 24 | @override 25 | Widget build(BuildContext context) { 26 | return Padding( 27 | padding: const EdgeInsets.fromLTRB(24, 0, 24, 0), 28 | child: Container( 29 | padding: const EdgeInsets.fromLTRB(0, 5, 0, 5), 30 | decoration: BoxDecoration( 31 | border: Border( 32 | bottom: BorderSide( 33 | width: 1, color: Theme.of(context).dividerColor))), 34 | child: Row( 35 | children: [ 36 | Text(widget.title), 37 | Expanded( 38 | child: Padding( 39 | padding: const EdgeInsets.only(left: 30), 40 | child: widget.editor)) 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/lib/src/color_picker.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 12/02/2019 5 | // Time: 18:22 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/cupertino.dart'; 12 | import 'package:flutter_colorpicker/flutter_colorpicker.dart'; 13 | 14 | class ColorPickers extends StatefulWidget { 15 | final Widget target; 16 | final Color? initColor; 17 | final ValueChanged onConfirm; 18 | 19 | const ColorPickers({ 20 | Key? key, 21 | required this.initColor, 22 | required this.onConfirm, 23 | required this.target, 24 | }) : super(key: key); 25 | 26 | @override 27 | State createState() => _ColorPickersState(); 28 | } 29 | 30 | class _ColorPickersState extends State { 31 | late Color pickerColor; 32 | 33 | late Color currentColor; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | pickerColor = widget.initColor ?? Colors.red; 39 | currentColor = widget.initColor ?? Colors.red; 40 | } 41 | 42 | onChangeColor(Color color) { 43 | setState(() { 44 | pickerColor = color; 45 | }); 46 | } 47 | 48 | // showColorPicker(BuildContext context) async { 49 | // showDialog( 50 | // context: context, 51 | // child: AlertDialog( 52 | // title: const Text('Pick a color!'), 53 | // content: SingleChildScrollView( 54 | // child: ColorPicker( 55 | // pickerColor: pickerColor, 56 | // onColorChanged: onChangeColor, 57 | // enableLabel: true, 58 | // pickerAreaHeightPercent: 0.8, 59 | // ), 60 | // ), 61 | // actions: [ 62 | // FlatButton( 63 | // child: const Text('Got it'), 64 | // onPressed: () { 65 | // setState(() => currentColor = pickerColor); 66 | // Navigator.of(context).pop(currentColor); 67 | // }, 68 | // ), 69 | // ], 70 | // ), 71 | // ); 72 | // } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return Container( 77 | color: Colors.white, 78 | padding: const EdgeInsets.all(6.0), 79 | alignment: Alignment.center, 80 | child: GestureDetector( 81 | onTap: () async { 82 | Color color = await showDialog( 83 | context: context, 84 | builder: (context) { 85 | return AlertDialog( 86 | title: const Text('Pick a color!'), 87 | content: SingleChildScrollView( 88 | child: ColorPicker( 89 | pickerColor: pickerColor, 90 | onColorChanged: onChangeColor, 91 | pickerAreaHeightPercent: 0.8, 92 | ), 93 | ), 94 | actions: [ 95 | TextButton( 96 | child: const Text('Got it'), 97 | onPressed: () { 98 | setState(() => currentColor = pickerColor); 99 | Navigator.of(context).pop(currentColor); 100 | }, 101 | ), 102 | ], 103 | ); 104 | }); 105 | widget.onConfirm(color); 106 | }, 107 | child: widget.target, 108 | )); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /example/lib/src/item_container.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 12/02/2019 5 | // Time: 16:29 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | 12 | class ItemContainer extends StatefulWidget { 13 | final List itemList; 14 | 15 | const ItemContainer({Key? key, required this.itemList}) : super(key: key); 16 | 17 | @override 18 | State createState() => _ItemContainerState(); 19 | } 20 | 21 | class _ItemContainerState extends State { 22 | _buildWidgetContainer() { 23 | List lists = widget.itemList; 24 | return GridView.builder( 25 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 26 | crossAxisCount: 3, //每行3个 27 | mainAxisSpacing: 10.0, //主轴(竖直)方向间距 28 | crossAxisSpacing: 0.0, //纵轴(水平)方向间距 29 | childAspectRatio: 1.0 //纵轴缩放比例 30 | ), 31 | itemCount: lists.length, 32 | itemBuilder: (BuildContext context, int index) { 33 | return InkWell( 34 | onTap: () { 35 | Navigator.pushNamed(context, lists[index]['routerName']); 36 | }, 37 | child: Container( 38 | decoration: BoxDecoration( 39 | border: (index + 1) % 3 != 0 40 | ? Border( 41 | right: BorderSide( 42 | color: Theme.of(context).dividerColor, width: 1.0)) 43 | : null), 44 | child: Column( 45 | mainAxisAlignment: MainAxisAlignment.center, 46 | children: [ 47 | Icon(lists[index]['icon'], 48 | color: Theme.of(context).primaryColor), 49 | const SizedBox( 50 | height: 8.0, 51 | ), 52 | Text(lists[index]['name']), 53 | ], 54 | ), 55 | ), 56 | ); 57 | }, 58 | ); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | var screenWidth = MediaQuery.of(context).size.width; 64 | var screenHeight = MediaQuery.of(context).size.height; 65 | return Container( 66 | padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), 67 | child: Stack( 68 | children: [ 69 | Container( 70 | width: screenWidth - 20, 71 | height: screenHeight, 72 | margin: const EdgeInsets.only(top: 30.0, bottom: 0.0), 73 | decoration: BoxDecoration( 74 | color: Colors.white, 75 | borderRadius: BorderRadius.circular(4.0), 76 | ), 77 | child: Column( 78 | children: [ 79 | Container( 80 | width: screenWidth - 20, 81 | padding: const EdgeInsets.only(left: 65.0, top: 3.0), 82 | height: 30.0, 83 | child: Text( 84 | "Citypicers", 85 | style: TextStyle( 86 | color: Theme.of(context).primaryColor, 87 | fontSize: 18.0, 88 | ), 89 | ), 90 | ), 91 | Expanded( 92 | child: _buildWidgetContainer(), 93 | ), 94 | ], 95 | ), 96 | ), 97 | Positioned( 98 | left: 0.0, 99 | top: 0.0, 100 | child: Container( 101 | height: 60.0, 102 | width: 60.0, 103 | decoration: BoxDecoration( 104 | color: Colors.white, 105 | borderRadius: BorderRadius.circular(30.0), 106 | ), 107 | child: Center( 108 | child: Container( 109 | decoration: BoxDecoration( 110 | color: Theme.of(context).primaryColor, 111 | borderRadius: BorderRadius.circular(23.0), 112 | ), 113 | height: 46.0, 114 | width: 46.0, 115 | child: const Icon( 116 | Icons.streetview, 117 | color: Colors.white, 118 | size: 30.0, 119 | ), 120 | ), 121 | ), 122 | ), 123 | ) 124 | ], 125 | ), 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /example/lib/src/location_selector.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 13/02/2019 5 | // Time: 20:48 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | import 'package:city_pickers/city_pickers.dart'; 12 | 13 | class LocationSelector extends StatelessWidget { 14 | /// 触发对象 15 | final Widget target; 16 | 17 | /// 显示类型 18 | final ShowType showType; 19 | 20 | /// 确认 21 | final ValueChanged onConfirm; 22 | 23 | /// initResult type[Result] 24 | final Result? initResult; 25 | 26 | const LocationSelector( 27 | {Key? key, 28 | this.showType = ShowType.pca, 29 | this.initResult, 30 | required this.target, 31 | required this.onConfirm}) 32 | : super(key: key); 33 | 34 | show(BuildContext context) async { 35 | Result? result = await CityPickers.showCityPicker( 36 | context: context, 37 | locationCode: initResult != null 38 | ? initResult!.areaId ?? 39 | initResult!.cityId ?? 40 | initResult!.provinceId ?? 41 | '110000' 42 | : '110000', 43 | showType: showType); 44 | if (result != null) { 45 | onConfirm(result); 46 | } 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return InkWell( 52 | onTap: () { 53 | show(context); 54 | }, 55 | child: Container( 56 | color: Colors.black12, 57 | alignment: Alignment.center, 58 | margin: const EdgeInsets.all(3.0), 59 | child: target, 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/lib/src/picker.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 12/02/2019 5 | // Time: 18:22 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/cupertino.dart'; 12 | 13 | const double _pickerHeight = 400.0; 14 | 15 | class PickerItem { 16 | String name; 17 | dynamic value; 18 | 19 | PickerItem({required this.name, this.value}); 20 | } 21 | 22 | class Picker extends StatefulWidget { 23 | final List items; 24 | final Widget target; 25 | final ValueChanged onConfirm; 26 | 27 | const Picker( 28 | {Key? key, 29 | required this.onConfirm, 30 | required this.target, 31 | required this.items}) 32 | : super(key: key); 33 | 34 | @override 35 | State createState() => _PickerState(); 36 | } 37 | 38 | class _PickerState extends State { 39 | FixedExtentScrollController scrollController = 40 | FixedExtentScrollController(initialItem: 0); 41 | late PickerItem result; 42 | 43 | @override 44 | void initState() { 45 | // TODO: implement initState 46 | result = widget.items[0]; 47 | super.initState(); 48 | } 49 | 50 | onChange(int index) { 51 | setState(() { 52 | result = widget.items[index]; 53 | }); 54 | } 55 | 56 | buildPicker() { 57 | return CupertinoPicker.builder( 58 | magnification: 1.0, 59 | scrollController: scrollController, 60 | itemExtent: 40.0, 61 | backgroundColor: Colors.white, 62 | onSelectedItemChanged: onChange, 63 | itemBuilder: (context, index) { 64 | return Center( 65 | child: Text( 66 | widget.items[index].name, 67 | maxLines: 1, 68 | overflow: TextOverflow.fade, 69 | textAlign: TextAlign.center, 70 | ), 71 | ); 72 | }, 73 | childCount: widget.items.length); 74 | } 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return Container( 79 | color: Colors.white, 80 | padding: const EdgeInsets.all(6.0), 81 | alignment: Alignment.center, 82 | child: GestureDetector( 83 | onTap: () async { 84 | await showCupertinoModalPopup( 85 | context: context, 86 | builder: (BuildContext context) { 87 | return Container( 88 | height: _pickerHeight, 89 | padding: const EdgeInsets.only(top: 6.0), 90 | color: CupertinoColors.white, 91 | child: DefaultTextStyle( 92 | style: const TextStyle( 93 | color: CupertinoColors.black, 94 | fontSize: 22.0, 95 | ), 96 | child: Column( 97 | children: [ 98 | Row( 99 | mainAxisSize: MainAxisSize.max, 100 | crossAxisAlignment: CrossAxisAlignment.start, 101 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 102 | children: [ 103 | TextButton( 104 | onPressed: () { 105 | Navigator.of(context).pop(); 106 | }, 107 | child: Text( 108 | 'cancle', 109 | style: TextStyle( 110 | color: Theme.of(context).primaryColor, 111 | ), 112 | ), 113 | ), 114 | TextButton( 115 | onPressed: () { 116 | Navigator.of(context).pop(result); 117 | }, 118 | child: Text( 119 | 'confirm', 120 | style: TextStyle( 121 | color: Theme.of(context).primaryColor, 122 | ), 123 | ), 124 | ), 125 | ], 126 | ), 127 | SizedBox( 128 | height: _pickerHeight - 100, 129 | child: buildPicker(), 130 | ) 131 | ], 132 | )), 133 | ); 134 | }, 135 | ); 136 | widget.onConfirm(result); 137 | }, 138 | child: widget.target, 139 | )); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /example/lib/view/show_city_picker.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 12/02/2019 5 | // Time: 16:53 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: 基本用法 8 | // 9 | 10 | import 'package:city_pickers_example/meta/province_nm.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter/cupertino.dart'; 13 | import 'package:city_pickers/city_pickers.dart'; 14 | import '../src/attr_item_container.dart'; 15 | import '../src/location_selector.dart'; 16 | import '../src/picker.dart'; 17 | 18 | var emptyResult = Result(); 19 | 20 | class ShowCityPicker extends StatefulWidget { 21 | const ShowCityPicker({Key? key}) : super(key: key); 22 | 23 | @override 24 | State createState() => _ShowCityPickerState(); 25 | } 26 | 27 | class _ShowCityPickerState extends State { 28 | PickerItem showTypeAttr = PickerItem(name: '省+市+县+乡', value: ShowType.pcav); 29 | Result resultAttr = Result(); 30 | Result result = Result(); 31 | double barrierOpacityAttr = 0.5; 32 | bool barrierDismissibleAttr = false; 33 | bool customerMeta = true; 34 | bool customerItemBuilder = false; 35 | double customerItemExtent = 40; 36 | bool customerButtons = false; 37 | bool isSort = false; 38 | double borderRadius = 0; 39 | 40 | PickerItem? themeAttr; 41 | 42 | Widget _buildShowTypes() { 43 | return Picker( 44 | target: showTypeAttr.name != null 45 | ? Text(showTypeAttr.name) 46 | : const Text("显示几级联动"), 47 | onConfirm: (PickerItem item) { 48 | setState(() { 49 | showTypeAttr = item; 50 | }); 51 | }, 52 | items: [ 53 | PickerItem(name: '省', value: ShowType.p), 54 | PickerItem(name: '市', value: ShowType.c), 55 | PickerItem(name: '县', value: ShowType.a), 56 | PickerItem(name: '省+市', value: ShowType.pc), 57 | PickerItem(name: '省+市+县', value: ShowType.pca), 58 | PickerItem(name: '省+市+县+乡', value: ShowType.pcav), 59 | PickerItem(name: '市+县+乡', value: ShowType.cav), 60 | ], 61 | ); 62 | } 63 | 64 | Widget _buildDefaultLocation() { 65 | return Row( 66 | children: [ 67 | Expanded( 68 | flex: 1, 69 | child: LocationSelector( 70 | target: Text(resultAttr.provinceName ?? '省', 71 | maxLines: 1, overflow: TextOverflow.ellipsis), 72 | showType: ShowType.p, 73 | initResult: resultAttr, 74 | onConfirm: (Result result) { 75 | if (result.provinceId != null) { 76 | setState(() { 77 | resultAttr = result; 78 | }); 79 | } 80 | }, 81 | ), 82 | ), 83 | Expanded( 84 | flex: 1, 85 | child: LocationSelector( 86 | target: Text(resultAttr.cityName ?? '市', 87 | maxLines: 1, overflow: TextOverflow.ellipsis), 88 | showType: ShowType.c, 89 | initResult: resultAttr, 90 | onConfirm: (Result result) { 91 | if (result.cityId != null) { 92 | setState(() { 93 | resultAttr = result; 94 | }); 95 | } 96 | }, 97 | ), 98 | ), 99 | Expanded( 100 | flex: 1, 101 | child: LocationSelector( 102 | target: Text(resultAttr.areaName ?? '区', 103 | maxLines: 1, overflow: TextOverflow.ellipsis), 104 | showType: ShowType.a, 105 | initResult: resultAttr, 106 | onConfirm: (Result result) { 107 | if (result.areaId != null) { 108 | setState(() { 109 | resultAttr = result; 110 | }); 111 | } 112 | }, 113 | ), 114 | ), 115 | Expanded( 116 | flex: 1, 117 | child: LocationSelector( 118 | target: Text(resultAttr.villageName ?? '乡', 119 | maxLines: 1, overflow: TextOverflow.ellipsis), 120 | showType: ShowType.a, 121 | initResult: resultAttr, 122 | onConfirm: (Result result) { 123 | if (result.villageId != null) { 124 | setState(() { 125 | resultAttr = result; 126 | }); 127 | } 128 | }, 129 | ), 130 | ), 131 | ], 132 | ); 133 | } 134 | 135 | Widget _buildBarrierDismissible() { 136 | return Container( 137 | alignment: Alignment.centerRight, 138 | child: CupertinoSwitch( 139 | value: barrierDismissibleAttr, 140 | onChanged: (bool val) { 141 | setState(() { 142 | barrierDismissibleAttr = !barrierDismissibleAttr; 143 | }); 144 | }, 145 | )); 146 | } 147 | 148 | Widget _buildBarrierOpacity() { 149 | return Row( 150 | children: [ 151 | Expanded( 152 | flex: 1, 153 | child: CupertinoSlider( 154 | value: barrierOpacityAttr, 155 | //实际进度的位置 156 | min: 0.01, 157 | max: 1.0, 158 | divisions: 100, 159 | activeColor: Colors.blue, 160 | //进度中活动部分的颜色 161 | onChanged: (value) { 162 | setState(() { 163 | barrierOpacityAttr = value.toDouble(); 164 | }); 165 | }, 166 | ), 167 | ), 168 | Text(barrierOpacityAttr.toStringAsFixed(2)) 169 | ], 170 | ); 171 | } 172 | 173 | Widget _buildItemExtent() { 174 | return Row( 175 | children: [ 176 | Expanded( 177 | flex: 1, 178 | child: CupertinoSlider( 179 | value: customerItemExtent, 180 | //实际进度的位置 181 | min: 00, 182 | max: 100, 183 | divisions: 60, 184 | activeColor: Colors.blue, 185 | //进度中活动部分的颜色 186 | onChanged: (value) { 187 | setState(() { 188 | customerItemExtent = value.toDouble(); 189 | }); 190 | }, 191 | ), 192 | ), 193 | Text(customerItemExtent.toStringAsFixed(2)) 194 | ], 195 | ); 196 | } 197 | 198 | Widget _buildItemBorderRadius() { 199 | return Row( 200 | children: [ 201 | Expanded( 202 | flex: 1, 203 | child: CupertinoSlider( 204 | value: borderRadius, 205 | //实际进度的位置 206 | min: 00, 207 | max: 40, 208 | divisions: 60, 209 | activeColor: Colors.blue, 210 | //进度中活动部分的颜色 211 | onChanged: (value) { 212 | setState(() { 213 | borderRadius = value.toDouble(); 214 | }); 215 | }, 216 | ), 217 | ), 218 | Text(borderRadius.toStringAsFixed(2)) 219 | ], 220 | ); 221 | } 222 | 223 | Widget _buildCustomerMeta() { 224 | return Container( 225 | alignment: Alignment.centerRight, 226 | child: CupertinoSwitch( 227 | value: customerMeta, 228 | onChanged: (bool val) { 229 | setState(() { 230 | customerMeta = !customerMeta; 231 | }); 232 | }, 233 | )); 234 | } 235 | 236 | Widget _buildCustomerButtons() { 237 | return Container( 238 | alignment: Alignment.centerRight, 239 | child: CupertinoSwitch( 240 | value: customerButtons, 241 | onChanged: (bool val) { 242 | setState(() { 243 | customerButtons = !customerButtons; 244 | }); 245 | }, 246 | )); 247 | } 248 | 249 | Widget _buildCustomerItem() { 250 | return Container( 251 | alignment: Alignment.centerRight, 252 | child: CupertinoSwitch( 253 | value: customerItemBuilder, 254 | onChanged: (bool val) { 255 | setState(() { 256 | customerItemBuilder = !customerItemBuilder; 257 | }); 258 | }, 259 | )); 260 | } 261 | 262 | Widget _buildSortItem() { 263 | return Container( 264 | alignment: Alignment.centerRight, 265 | child: CupertinoSwitch( 266 | value: isSort, 267 | onChanged: (bool val) { 268 | setState(() { 269 | isSort = !isSort; 270 | }); 271 | }, 272 | )); 273 | } 274 | 275 | Widget _buildTheme() { 276 | return Picker( 277 | target: Text(themeAttr?.name ?? "主题切换"), 278 | onConfirm: (PickerItem item) { 279 | setState(() { 280 | themeAttr = item; 281 | }); 282 | }, 283 | items: [ 284 | PickerItem(name: 'ThemeData.light()', value: ThemeData.light()), 285 | PickerItem(name: 'ThemeData.fallback()', value: ThemeData.fallback()), 286 | PickerItem(name: 'ThemeData.dark()', value: ThemeData.dark()), 287 | PickerItem(name: 'ThemeData.of(context)', value: null), 288 | ]); 289 | } 290 | 291 | getItemBuilder() { 292 | if (customerItemBuilder) { 293 | return (item, list, index) { 294 | return Center( 295 | child: 296 | Text(item, maxLines: 1, style: const TextStyle(fontSize: 55))); 297 | }; 298 | } else { 299 | return null; 300 | } 301 | } 302 | 303 | @override 304 | Widget build(BuildContext context) { 305 | return Scaffold( 306 | appBar: AppBar( 307 | title: const Text("ios风格城市选择器"), 308 | ), 309 | body: SingleChildScrollView( 310 | // 防止边界超出 311 | child: Column( 312 | children: [ 313 | AttrItemContainer(title: '级联方式', editor: _buildShowTypes()), 314 | AttrItemContainer(title: '默认地址', editor: _buildDefaultLocation()), 315 | AttrItemContainer(title: '背景透明度', editor: _buildBarrierOpacity()), 316 | AttrItemContainer(title: '选中区域高度', editor: _buildItemExtent()), 317 | AttrItemContainer(title: '顶部圆角值', editor: _buildItemBorderRadius()), 318 | AttrItemContainer( 319 | title: '背景点击关闭', editor: _buildBarrierDismissible()), 320 | AttrItemContainer(title: '是否采用自定义数据', editor: _buildCustomerMeta()), 321 | AttrItemContainer( 322 | title: '是否采用自定义的头部按钮', editor: _buildCustomerButtons()), 323 | AttrItemContainer(title: '自定义item渲染', editor: _buildCustomerItem()), 324 | AttrItemContainer(title: '数据是否排序', editor: _buildSortItem()), 325 | AttrItemContainer(title: '主题选择', editor: _buildTheme()), 326 | AttrItemContainer(title: '选择结果', editor: Text(result.toString())), 327 | ElevatedButton( 328 | onPressed: () async { 329 | print("locationCode $resultAttr"); 330 | Result? tempResult = await CityPickers.showCityPicker( 331 | context: context, 332 | theme: themeAttr?.value, 333 | locationCode: resultAttr != null 334 | ? resultAttr.areaId ?? 335 | resultAttr.cityId ?? 336 | resultAttr.provinceId ?? 337 | '110000' 338 | : '110000', 339 | borderRadius: borderRadius, 340 | showType: showTypeAttr.value, 341 | isSort: isSort, 342 | barrierOpacity: barrierOpacityAttr, 343 | barrierDismissible: barrierDismissibleAttr, 344 | citiesData: customerMeta == true ? citiesDataNm : null, 345 | provincesData: 346 | customerMeta == true ? provincesDataNm : null, 347 | itemExtent: customerItemExtent, 348 | cancelWidget: customerButtons ? const Text('cancle') : null, 349 | confirmWidget: 350 | customerButtons ? const Text('confirm') : null, 351 | itemBuilder: getItemBuilder()); 352 | if (tempResult == null) { 353 | return; 354 | } 355 | setState(() { 356 | result = tempResult; 357 | }); 358 | }, 359 | child: const Text("展示city picker"), 360 | ) 361 | ], 362 | ), 363 | ), 364 | ); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /example/lib/view/show_full_page_picker.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 12/02/2019 5 | // Time: 16:53 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/cupertino.dart'; 12 | import 'package:city_pickers/city_pickers.dart'; 13 | import '../src/attr_item_container.dart'; 14 | import '../src/location_selector.dart'; 15 | import '../src/picker.dart'; 16 | import '../meta/province.dart'; 17 | 18 | var emptyResult = Result(); 19 | 20 | class ShowFullPageCityPicker extends StatefulWidget { 21 | const ShowFullPageCityPicker({Key? key}) : super(key: key); 22 | 23 | @override 24 | State createState() => _ShowFullPageCityPickerState(); 25 | } 26 | 27 | class _ShowFullPageCityPickerState extends State { 28 | PickerItem showTypeAttr = PickerItem(name: '省+市+县', value: ShowType.pca); 29 | Result resultAttr = Result(); 30 | Result result = Result(); 31 | double barrierOpacityAttr = 0.5; 32 | bool barrierDismissibleAttr = false; 33 | bool customerMeta = false; 34 | PickerItem? themeAttr; 35 | 36 | Widget _buildShowTypes() { 37 | return Picker( 38 | target: showTypeAttr.name != null 39 | ? Text(showTypeAttr.name) 40 | : const Text("显示几级联动"), 41 | onConfirm: (PickerItem item) { 42 | setState(() { 43 | showTypeAttr = item; 44 | }); 45 | }, 46 | items: [ 47 | PickerItem(name: '省', value: ShowType.p), 48 | PickerItem(name: '市', value: ShowType.c), 49 | PickerItem(name: '县', value: ShowType.a), 50 | PickerItem(name: '省+市', value: ShowType.pc), 51 | PickerItem(name: '省+市+县', value: ShowType.pca), 52 | PickerItem(name: '市+县', value: ShowType.ca), 53 | ], 54 | ); 55 | } 56 | 57 | Widget _buildDefaultLocation() { 58 | return Row( 59 | children: [ 60 | Expanded( 61 | flex: 1, 62 | child: LocationSelector( 63 | target: Text(resultAttr.provinceName ?? '省', 64 | maxLines: 1, overflow: TextOverflow.ellipsis), 65 | showType: ShowType.p, 66 | initResult: resultAttr, 67 | onConfirm: (Result result) { 68 | if (result.provinceId != null) { 69 | setState(() { 70 | resultAttr = result; 71 | }); 72 | } 73 | }, 74 | ), 75 | ), 76 | Expanded( 77 | flex: 1, 78 | child: LocationSelector( 79 | target: Text(resultAttr.cityName ?? '市', 80 | maxLines: 1, overflow: TextOverflow.ellipsis), 81 | showType: ShowType.c, 82 | initResult: resultAttr, 83 | onConfirm: (Result result) { 84 | if (result.cityId != null) { 85 | setState(() { 86 | resultAttr = result; 87 | }); 88 | } 89 | }, 90 | ), 91 | ), 92 | Expanded( 93 | flex: 1, 94 | child: LocationSelector( 95 | target: Text(resultAttr.areaName ?? '区', 96 | maxLines: 1, overflow: TextOverflow.ellipsis), 97 | showType: ShowType.a, 98 | initResult: resultAttr, 99 | onConfirm: (Result result) { 100 | if (result.areaId != null) { 101 | setState(() { 102 | resultAttr = result; 103 | }); 104 | } 105 | }, 106 | ), 107 | ) 108 | ], 109 | ); 110 | } 111 | 112 | Widget _buildTheme() { 113 | return Picker( 114 | target: Text(themeAttr?.name ?? "主题切换"), 115 | onConfirm: (PickerItem item) { 116 | setState(() { 117 | themeAttr = item; 118 | }); 119 | }, 120 | items: [ 121 | PickerItem(name: 'ThemeData.light()', value: ThemeData.light()), 122 | PickerItem(name: 'ThemeData.fallback()', value: ThemeData.fallback()), 123 | PickerItem(name: 'ThemeData.dark()', value: ThemeData.dark()), 124 | PickerItem(name: 'ThemeData.of(context)', value: null), 125 | ]); 126 | } 127 | 128 | Widget _buildCustomerMeta() { 129 | return Container( 130 | alignment: Alignment.centerRight, 131 | child: CupertinoSwitch( 132 | value: customerMeta, 133 | onChanged: (bool val) { 134 | setState(() { 135 | customerMeta = !customerMeta; 136 | }); 137 | }, 138 | )); 139 | } 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | return Scaffold( 144 | appBar: AppBar( 145 | title: const Text("省市县三级全屏联动"), 146 | ), 147 | body: Column( 148 | children: [ 149 | AttrItemContainer(title: '级联方式', editor: _buildShowTypes()), 150 | AttrItemContainer(title: '默认地址', editor: _buildDefaultLocation()), 151 | AttrItemContainer(title: '主题选择', editor: _buildTheme()), 152 | AttrItemContainer(title: '是否采用自定义数据', editor: _buildCustomerMeta()), 153 | AttrItemContainer(title: '选择结果', editor: Text(result.toString())), 154 | ElevatedButton( 155 | onPressed: () async { 156 | print("locationCode $resultAttr"); 157 | Result? tempResult = await CityPickers.showFullPageCityPicker( 158 | context: context, 159 | theme: themeAttr?.value, 160 | locationCode: resultAttr.areaId ?? 161 | resultAttr.cityId ?? 162 | resultAttr.provinceId ?? 163 | "110000", 164 | showType: showTypeAttr.value, 165 | citiesData: customerMeta ? citiesData : null, 166 | provincesData: customerMeta ? provincesData : null); 167 | if (tempResult == null) { 168 | return; 169 | } 170 | setState(() { 171 | result = tempResult; 172 | }); 173 | }, 174 | child: const Text("展示city picker"), 175 | ) 176 | ], 177 | ), 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /example/lib/view/util_getLocationInfo.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 09/05/2019 5 | // Time: 20:08 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | import 'package:flutter/material.dart'; 10 | import 'package:city_pickers/city_pickers.dart'; 11 | 12 | import '../src/attr_item_container.dart'; 13 | 14 | class UtilGetLocationInfo extends StatefulWidget { 15 | const UtilGetLocationInfo({Key? key}) : super(key: key); 16 | 17 | @override 18 | State createState() => _Demo(); 19 | } 20 | 21 | class _Demo extends State { 22 | CityPickerUtil cityPickerUtils = CityPickers.utils(); 23 | Result result = Result(); 24 | String code = '110101'; 25 | 26 | buttonHandle() { 27 | print("code::: $code"); 28 | setState(() { 29 | result = cityPickerUtils.getAreaResultByCode(code); 30 | print("result>>>> ${result.toString()}"); 31 | }); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: const Text("解析locationCode"), 39 | ), 40 | body: Column( 41 | children: [ 42 | AttrItemContainer( 43 | title: '标题1111', 44 | editor: TextField( 45 | keyboardType: TextInputType.number, 46 | autofocus: false, 47 | controller: TextEditingController(text: code), 48 | onChanged: (String value) { 49 | setState(() { 50 | code = value; 51 | }); 52 | }, 53 | ), 54 | ), 55 | Text("地址信息为: ${result.toString()}"), 56 | ElevatedButton( 57 | onPressed: buttonHandle, 58 | child: Text('touch me 解析 $code '), 59 | ) 60 | ], 61 | )); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /example/lib/view/wip.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 13/02/2019 5 | // Time: 21:02 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:city_pickers/city_pickers.dart'; 11 | import 'package:flutter/cupertino.dart'; 12 | import 'package:flutter/material.dart'; 13 | 14 | import '../src/attr_item_container.dart'; 15 | import '../src/color_picker.dart'; 16 | import '../meta/province.dart'; 17 | 18 | //showCitiesSelector 19 | class WorkInProgress extends StatefulWidget { 20 | const WorkInProgress({Key? key}) : super(key: key); 21 | 22 | @override 23 | WorkInProgressState createState() { 24 | return WorkInProgressState(); 25 | } 26 | } 27 | 28 | class WorkInProgressState extends State { 29 | String title = '城市选择'; 30 | Result result = Result(); 31 | Color tagBgColor = const Color.fromRGBO(255, 255, 255, 1); 32 | Color pageBgColor = Colors.white; 33 | 34 | Color tagBgActiveColor = const Color(0xffeeeeee); 35 | Color tagFontColor = const Color(0xff666666); 36 | Color tagFontActiveColor = const Color(0xff242424); 37 | 38 | double tagBarFontSize = 12; 39 | 40 | double cityItemFontSize = 16; 41 | 42 | double topIndexHeight = 40; 43 | 44 | double topIndexFontSize = 13; 45 | 46 | Color topIndexFontColor = const Color(0xffc0c0c0); 47 | 48 | Color topIndexBgColor = const Color(0xfff3f4f5); 49 | 50 | Color itemSelectFontColor = Colors.cyan; 51 | 52 | Color itemSelectBgColor = Colors.blueGrey; 53 | 54 | Color itemFontColor = Colors.black; 55 | bool useSearchBar = false; 56 | AppBarBuilder appBarBuilder = (String title) { 57 | return AppBar( 58 | title: const Text('用户自定义AppBar'), 59 | actions: [ 60 | IconButton(icon: const Icon(Icons.add), onPressed: () {}), 61 | IconButton(icon: const Icon(Icons.dashboard), onPressed: () {}), 62 | IconButton(icon: const Icon(Icons.cached), onPressed: () {}), 63 | ], 64 | ); 65 | }; 66 | 67 | bool userSelfMeta = false; 68 | 69 | Widget _buildTopIndexFontSize() { 70 | return Row( 71 | children: [ 72 | Expanded( 73 | flex: 1, 74 | child: CupertinoSlider( 75 | value: topIndexFontSize, 76 | //实际进度的位置 77 | min: 10, 78 | max: 30, 79 | divisions: 20, 80 | activeColor: Colors.blue, 81 | //进度中活动部分的颜色 82 | onChanged: (value) { 83 | setState(() { 84 | topIndexFontSize = value; 85 | }); 86 | }, 87 | ), 88 | ), 89 | Text("$topIndexFontSize") 90 | ], 91 | ); 92 | } 93 | 94 | Widget _buildSwitch({ 95 | required bool value, 96 | required ValueChanged onChanged, 97 | }) { 98 | return Container( 99 | alignment: Alignment.centerRight, 100 | child: CupertinoSwitch( 101 | value: value, 102 | onChanged: onChanged, 103 | )); 104 | } 105 | 106 | Widget _buildTopIndexHeight() { 107 | return Row( 108 | children: [ 109 | Expanded( 110 | flex: 1, 111 | child: CupertinoSlider( 112 | value: topIndexHeight, 113 | //实际进度的位置 114 | min: 40, 115 | max: 100, 116 | divisions: 60, 117 | activeColor: Colors.blue, 118 | //进度中活动部分的颜色 119 | onChanged: (value) { 120 | setState(() { 121 | topIndexHeight = value; 122 | }); 123 | }, 124 | ), 125 | ), 126 | Text("$topIndexHeight") 127 | ], 128 | ); 129 | } 130 | 131 | Widget _buildBarFontSize() { 132 | return Row( 133 | children: [ 134 | Expanded( 135 | flex: 1, 136 | child: CupertinoSlider( 137 | value: tagBarFontSize, 138 | //实际进度的位置 139 | min: 12, 140 | max: 40, 141 | divisions: 28, 142 | activeColor: Colors.blue, 143 | //进度中活动部分的颜色 144 | onChanged: (value) { 145 | setState(() { 146 | tagBarFontSize = value; 147 | }); 148 | }, 149 | ), 150 | ), 151 | Text("$tagBarFontSize") 152 | ], 153 | ); 154 | } 155 | 156 | Widget _buildCityItemFontSize() { 157 | return Row( 158 | children: [ 159 | Expanded( 160 | flex: 1, 161 | child: CupertinoSlider( 162 | value: cityItemFontSize, 163 | //实际进度的位置 164 | min: 12, 165 | max: 50, 166 | divisions: 38, 167 | activeColor: Colors.blue, 168 | //进度中活动部分的颜色 169 | onChanged: (value) { 170 | setState(() { 171 | cityItemFontSize = value; 172 | }); 173 | }, 174 | ), 175 | ), 176 | Text("$cityItemFontSize") 177 | ], 178 | ); 179 | } 180 | 181 | toggle(BuildContext context) async { 182 | Result? tempResult = await CityPickers.showCitiesSelector( 183 | context: context, 184 | title: title, 185 | locationCode: result.cityId, 186 | scaffoldBackgroundColor: pageBgColor, 187 | provincesData: 188 | !userSelfMeta ? CityPickers.metaProvinces : provincesData, 189 | citiesData: !userSelfMeta ? CityPickers.metaCities : citiesData, 190 | hotCities: [ 191 | HotCity(id: '0', name: '北京'), 192 | HotCity(id: '1', name: '沈阳'), 193 | HotCity(id: '2', name: '天津'), 194 | ], 195 | tagBarTextPadding: const EdgeInsets.symmetric( 196 | horizontal: 4.0, 197 | vertical: 2.0, 198 | ), 199 | appBarBuilder: useSearchBar ? null : appBarBuilder, 200 | useSearchAppBar: useSearchBar, 201 | sideBarStyle: BaseStyle( 202 | fontSize: tagBarFontSize, 203 | color: tagFontColor, 204 | backgroundColor: tagBgColor, 205 | backgroundActiveColor: tagBgActiveColor, 206 | activeColor: tagFontActiveColor), 207 | cityItemStyle: BaseStyle( 208 | fontSize: cityItemFontSize, 209 | color: itemFontColor, 210 | activeColor: itemSelectFontColor), 211 | topStickStyle: BaseStyle( 212 | fontSize: topIndexFontSize, 213 | color: topIndexFontColor, 214 | backgroundColor: topIndexBgColor, 215 | height: topIndexHeight)); 216 | 217 | if (tempResult == null) { 218 | return; 219 | } 220 | setState(() { 221 | result = tempResult; 222 | }); 223 | } 224 | 225 | handleOnTitleChanged(String value) { 226 | setState(() { 227 | title = value; 228 | }); 229 | } 230 | 231 | @override 232 | Widget build(BuildContext context) { 233 | return Scaffold( 234 | appBar: AppBar( 235 | title: const Text("省市县三级全屏联动"), 236 | ), 237 | body: SingleChildScrollView( 238 | child: Column( 239 | children: [ 240 | AttrItemContainer( 241 | title: '标题', 242 | editor: TextField( 243 | controller: TextEditingController(text: title), 244 | onChanged: (String value) { 245 | setState(() { 246 | title = value; 247 | }); 248 | }, 249 | ), 250 | ), 251 | AttrItemContainer( 252 | title: '边栏背景色', 253 | editor: ColorPickers( 254 | target: Text('选择颜色', style: TextStyle(color: tagBgColor)), 255 | initColor: tagBgColor, 256 | onConfirm: (Color color) { 257 | setState(() { 258 | tagBgColor = color; 259 | }); 260 | }, 261 | ), 262 | ), 263 | AttrItemContainer( 264 | title: '页面背景色', 265 | editor: ColorPickers( 266 | target: Text('选择颜色', style: TextStyle(color: pageBgColor)), 267 | initColor: pageBgColor, 268 | onConfirm: (Color color) { 269 | print("color::: $color"); 270 | setState(() { 271 | pageBgColor = color; 272 | }); 273 | }, 274 | ), 275 | ), 276 | AttrItemContainer( 277 | title: '边栏背景激活颜色', 278 | editor: ColorPickers( 279 | target: Text('选择颜色', style: TextStyle(color: tagBgActiveColor)), 280 | initColor: tagBgActiveColor, 281 | onConfirm: (Color color) { 282 | setState(() { 283 | tagBgActiveColor = color; 284 | }); 285 | }, 286 | ), 287 | ), 288 | AttrItemContainer( 289 | title: '边栏字体颜色', 290 | editor: ColorPickers( 291 | target: Text('选择颜色', style: TextStyle(color: tagFontColor)), 292 | initColor: tagFontColor, 293 | onConfirm: (Color color) { 294 | setState(() { 295 | tagFontColor = color; 296 | }); 297 | }, 298 | ), 299 | ), 300 | AttrItemContainer( 301 | title: '边栏字体激活颜色', 302 | editor: ColorPickers( 303 | target: 304 | Text('选择颜色', style: TextStyle(color: tagFontActiveColor)), 305 | initColor: tagFontActiveColor, 306 | onConfirm: (Color color) { 307 | setState(() { 308 | tagFontActiveColor = color; 309 | }); 310 | }, 311 | ), 312 | ), 313 | AttrItemContainer(title: 'tag集字体大小', editor: _buildBarFontSize()), 314 | AttrItemContainer( 315 | title: '城市item字体大小', editor: _buildCityItemFontSize()), 316 | AttrItemContainer( 317 | title: '顶部tag分类高度', editor: _buildTopIndexHeight()), 318 | AttrItemContainer( 319 | // topIndexFontSize 320 | title: '顶部tag分类字体大小', 321 | editor: _buildTopIndexFontSize()), 322 | AttrItemContainer( 323 | // topIndexFontSize 324 | title: '顶部tag分类字体颜色', 325 | editor: ColorPickers( 326 | target: 327 | Text('选择颜色', style: TextStyle(color: topIndexFontColor)), 328 | initColor: topIndexFontColor, 329 | onConfirm: (Color color) { 330 | setState(() { 331 | topIndexFontColor = color; 332 | }); 333 | }, 334 | ), 335 | ), 336 | AttrItemContainer( 337 | title: '顶部tag分类背景颜色', 338 | editor: ColorPickers( 339 | target: Text('选择颜色', style: TextStyle(color: topIndexBgColor)), 340 | initColor: topIndexBgColor, 341 | onConfirm: (Color color) { 342 | setState(() { 343 | topIndexBgColor = color; 344 | }); 345 | }, 346 | ), 347 | ), 348 | AttrItemContainer( 349 | title: '城市item字体颜色', 350 | editor: ColorPickers( 351 | target: Text('选择颜色', style: TextStyle(color: itemFontColor)), 352 | initColor: itemFontColor, 353 | onConfirm: (Color color) { 354 | setState(() { 355 | itemFontColor = color; 356 | }); 357 | }, 358 | ), 359 | ), 360 | AttrItemContainer( 361 | title: '城市item选中字体颜色', 362 | editor: ColorPickers( 363 | target: 364 | Text('选择颜色', style: TextStyle(color: itemSelectFontColor)), 365 | initColor: itemSelectFontColor, 366 | onConfirm: (Color color) { 367 | setState(() { 368 | itemSelectFontColor = color; 369 | }); 370 | }, 371 | ), 372 | ), 373 | AttrItemContainer( 374 | title: '城市item选中背景颜色', 375 | editor: ColorPickers( 376 | target: 377 | Text('选择颜色', style: TextStyle(color: itemSelectBgColor)), 378 | initColor: itemSelectBgColor, 379 | onConfirm: (Color color) { 380 | setState(() { 381 | itemSelectBgColor = color; 382 | }); 383 | }, 384 | ), 385 | ), 386 | AttrItemContainer( 387 | title: '使用自定义数据', 388 | editor: _buildSwitch( 389 | value: userSelfMeta, 390 | onChanged: (bool val) => setState(() { 391 | userSelfMeta = val; 392 | }), 393 | ), 394 | ), 395 | AttrItemContainer( 396 | title: '使用搜索栏', 397 | editor: _buildSwitch( 398 | value: useSearchBar, 399 | onChanged: (value) => setState(() { 400 | useSearchBar = value; 401 | }), 402 | ), 403 | ), 404 | AttrItemContainer( 405 | title: '选择结果', editor: Text(result.toString())), 406 | ElevatedButton( 407 | child: const Text('呼出'), 408 | onPressed: () { 409 | toggle(context); 410 | }, 411 | ) 412 | ], 413 | ), 414 | ), 415 | ); 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: city_pickers_example 2 | description: Demonstrates how to use the city_pickers plugin. 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | # The following adds the Cupertino Icons font to your application. 14 | # Use with the CupertinoIcons class for iOS style icons. 15 | cupertino_icons: ^1.0.2 16 | city_pickers: 17 | path: ../ 18 | flutter_colorpicker: ^1.0.3 19 | 20 | dev_dependencies: 21 | flutter_test: 22 | sdk: flutter 23 | flutter_lints: ^2.0.0 24 | 25 | # For information on the generic Dart part of this file, see the 26 | # following page: https://www.dartlang.org/tools/pub/pubspec 27 | 28 | # The following section is specific to Flutter. 29 | flutter: 30 | 31 | # The following line ensures that the Material Icons font is 32 | # included with your application, so that you can use the icons in 33 | # the material Icons class. 34 | uses-material-design: true 35 | 36 | # To add assets to your application, add an assets section, like this: 37 | assets: 38 | - assets/wip.jpeg 39 | 40 | # An image asset can refer to one or more resolution-specific "variants", see 41 | # https://flutter.io/assets-and-images/#resolution-aware. 42 | 43 | # For details regarding adding assets from package dependencies, see 44 | # https://flutter.io/assets-and-images/#from-packages 45 | 46 | # To add custom fonts to your application, add a fonts section here, 47 | # in this "flutter" section. Each entry in this list should have a 48 | # "family" key with the font family name, and a "fonts" key with a 49 | # list giving the asset and other descriptors for the font. For 50 | # example: 51 | # fonts: 52 | # - family: Schyler 53 | # fonts: 54 | # - asset: fonts/Schyler-Regular.ttf 55 | # - asset: fonts/Schyler-Italic.ttf 56 | # style: italic 57 | # - family: Trajan Pro 58 | # fonts: 59 | # - asset: fonts/TrajanPro.ttf 60 | # - asset: fonts/TrajanPro_Bold.ttf 61 | # weight: 700 62 | # 63 | # For details regarding fonts from package dependencies, 64 | # see https://flutter.io/custom-fonts/#from-packages 65 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | //// This is a basic Flutter widget test. 2 | //// 3 | //// To perform an interaction with a widget in your test, use the WidgetTester 4 | //// utility that Flutter provides. For example, you can send tap and scroll 5 | //// gestures. You can also use WidgetTester to find child widgets in the widget 6 | //// tree, read text, and verify that the values of widget properties are correct. 7 | // 8 | //import 'package:flutter/material.dart'; 9 | //import 'package:flutter_test/flutter_test.dart'; 10 | // 11 | //import 'package:city_pickers_example/main.dart'; 12 | // 13 | //void main() { 14 | // testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // // Build our app and trigger a frame. 16 | // await tester.pumpWidget(MyApp()); 17 | // 18 | // // Verify that platform version is retrieved. 19 | // expect( 20 | // find.byWidgetPredicate( 21 | // (Widget widget) => widget is Text && 22 | // widget.data.startsWith('Running on:'), 23 | // ), 24 | // findsOneWidget, 25 | // ); 26 | // }); 27 | //} 28 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanxu317317/city_pickers/b964b28339a8174cc3660206d4054ad4314a96d6/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | city_pickers 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "city_pickers", 3 | "short_name": "city_pickers", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /lib/city_pickers.dart: -------------------------------------------------------------------------------- 1 | // A Flutter plugin for china city selections 2 | 3 | library city_pickers; 4 | 5 | export 'modal/point.dart' show Point; 6 | export 'src/cities_selector/cities_selector.dart' show CitiesSelector; 7 | export 'src/cities_selector/cities_style.dart'; 8 | export 'src/cities_selector/utils.dart'; 9 | export 'modal/result.dart'; 10 | export 'src/city_picker.dart'; 11 | export 'src/utils/index.dart'; 12 | export 'src/show_types.dart'; 13 | export 'src/util.dart'; 14 | -------------------------------------------------------------------------------- /lib/modal/base_citys.dart: -------------------------------------------------------------------------------- 1 | import 'package:city_pickers/modal/point.dart'; 2 | import 'package:lpinyin/lpinyin.dart'; 3 | 4 | import '../meta/province.dart'; 5 | import '../src/util.dart'; 6 | 7 | /// tree point 8 | 9 | class CityTree { 10 | /// build cityTrees's meta, it can be changed bu developers 11 | Map metaInfo; 12 | 13 | /// provData user self-defining data 14 | Map? provincesInfo; 15 | Cache _cache = new Cache(); 16 | 17 | /// the tree's modal 18 | /// data = Point( 19 | /// letter, 20 | /// name 21 | /// code, 22 | /// letter, 23 | /// child: [ 24 | /// Point 25 | /// ] 26 | /// ) 27 | /// data = [ 28 | /// { 29 | /// letter: 'Z', 30 | /// name: '浙江', 31 | /// code: 330000 32 | /// child: [ 33 | /// letter: 'h', 34 | /// name: '杭州', 35 | /// child 36 | /// ] 37 | /// } 38 | /// ] 39 | late Point tree; 40 | 41 | /// @param metaInfo city and areas meta describe 42 | CityTree({this.metaInfo = citiesData, this.provincesInfo}); 43 | 44 | Map get _provincesData => this.provincesInfo ?? provincesData; 45 | 46 | /// build tree by int provinceId, 47 | /// @param provinceId this is province id 48 | /// @return tree 49 | Point initTree(String provinceId) { 50 | String _cacheKey = provinceId; 51 | // 这里为了避免 https://github.com/hanxu317317/city_pickers/issues/68 52 | // if (_cache.has(_cacheKey)) { 53 | // return tree = _cache.get(_cacheKey); 54 | // } 55 | 56 | String name = this._provincesData[provinceId]!; 57 | String letter = PinyinHelper.getFirstWordPinyin(name).substring(0, 1); 58 | var root = 59 | new Point(code: provinceId, letter: letter, children: [], name: name); 60 | tree = _buildTree(root, metaInfo[provinceId], metaInfo); 61 | _cache.set(_cacheKey, tree); 62 | return tree; 63 | } 64 | 65 | /// this is a private function, used the return to get a correct tree contain cities and areas 66 | /// @param code one of province city or area id; 67 | /// @return provinceId return id which province's child contain code 68 | String? _getProvinceByCode(String code) { 69 | String _code = code.toString(); 70 | List keys = metaInfo.keys.toList(); 71 | for (int i = 0; i < keys.length; i++) { 72 | String key = keys[i]; 73 | Map child = metaInfo[key]; 74 | if (child.containsKey(_code)) { 75 | // 当前元素的父key在省份内 76 | if (this._provincesData.containsKey(key)) { 77 | return key; 78 | } 79 | return _getProvinceByCode(key); 80 | } 81 | } 82 | return null; 83 | } 84 | 85 | /// build tree by any code provinceId or cityCode or areaCode 86 | /// @param code build a tree 87 | /// @return Point a province with its cities and areas tree 88 | Point initTreeByCode(String code) { 89 | String _code = code.toString(); 90 | if (this._provincesData[_code] != null) { 91 | return initTree(code); 92 | } 93 | String? provinceId = _getProvinceByCode(code); 94 | if (provinceId != null) { 95 | return initTree(provinceId); 96 | } 97 | return Point.nullPoint(); 98 | // return Point.nullPoint; 99 | } 100 | 101 | /// private function 102 | /// recursion to build tree 103 | Point _buildTree(Point target, Map? citys, Map meta) { 104 | if (citys == null || citys.isEmpty) { 105 | return target; 106 | } else { 107 | List keys = citys.keys.toList(); 108 | 109 | for (int i = 0; i < keys.length; i++) { 110 | String key = keys[i]; 111 | Map value = citys[key]; 112 | Point _point = new Point( 113 | code: key, 114 | letter: value['alpha'], 115 | children: [], 116 | name: value['name'], 117 | isClassificationNode: value['isClassificationNode'] ?? false, 118 | ); 119 | 120 | // for avoid the data error that such as 121 | // "469027": { 122 | // "469027": { 123 | // "name": "乐东黎族自治县", 124 | // "alpha": "l" 125 | // } 126 | // } 127 | if (citys.keys.length == 1) { 128 | if (target.code.toString() == citys.keys.first) { 129 | continue; 130 | } 131 | } 132 | 133 | _point = _buildTree(_point, meta[key], meta); 134 | target.addChild(_point); 135 | } 136 | } 137 | return target; 138 | } 139 | } 140 | 141 | /// Province Class 142 | class Provinces { 143 | Map metaInfo; 144 | 145 | // 是否将省份排序, 进行排序 146 | bool sort; 147 | 148 | Provinces({this.metaInfo = provincesData, this.sort = true}); 149 | 150 | // 获取省份数据 151 | get provinces { 152 | List provList = []; 153 | List keys = metaInfo.keys.toList(); 154 | for (int i = 0; i < keys.length; i++) { 155 | String name = metaInfo[keys[i]]!; 156 | provList.add(Point( 157 | code: keys[i], 158 | children: [], 159 | letter: PinyinHelper.getFirstWordPinyin(name).substring(0, 1), 160 | name: name)); 161 | } 162 | if (this.sort == true) { 163 | provList.sort((Point a, Point b) { 164 | if (a.letter == null && b.letter == null) { 165 | return 0; 166 | } 167 | 168 | if (a.letter == null) { 169 | return 1; 170 | } 171 | 172 | return a.letter!.compareTo(b.letter!); 173 | }); 174 | } 175 | 176 | return provList; 177 | } 178 | } 179 | //main() { 180 | // var tree = new CityTree(); 181 | // tree.initTree(460000); 182 | // print("treePo>>> ${tree.tree.toString()}"); 183 | //} 184 | // 185 | 186 | //main() { 187 | // var p = new Provinces( 188 | //// metaInfo: provincesData 189 | // ); 190 | // print("p.provinces ${p.provinces}"); 191 | //} 192 | -------------------------------------------------------------------------------- /lib/modal/point.dart: -------------------------------------------------------------------------------- 1 | import 'package:lpinyin/lpinyin.dart'; 2 | 3 | /// use National Bureau of Statistics's data, build tree, the [point] is trees's node 4 | class Point { 5 | static final _pinyinPlaceholder = new Pinyin._([], '', ''); 6 | 7 | final String? code; 8 | final List children; 9 | final int? depth; 10 | final String? letter; 11 | final String name; 12 | 13 | /// Just a classification node, not corresponding to the actual region. 14 | final bool isClassificationNode; 15 | 16 | Point.nullPoint() 17 | : children = [], 18 | name = '', 19 | isClassificationNode = false, 20 | code = null, 21 | depth = null, 22 | letter = null; 23 | 24 | bool get isNull => this.code == null; 25 | 26 | Point({ 27 | this.code = '0', 28 | required this.children, 29 | this.depth, 30 | String? letter, 31 | this.name = '', 32 | this.isClassificationNode = false, 33 | }) : letter = letter?.toUpperCase(); 34 | 35 | String? _lowerCaseName; 36 | String get lowerCaseName => _lowerCaseName ??= name.toLowerCase(); 37 | 38 | Pinyin? _pinyin = _pinyinPlaceholder; 39 | Pinyin? get pinyin { 40 | if (identical(_pinyin, _pinyinPlaceholder)) { 41 | _pinyin = Pinyin.tryParse(name); 42 | } 43 | return _pinyin; 44 | } 45 | 46 | /// add node for Point, the node's type must is [Point] 47 | addChild(Point node) { 48 | this.children.add(node); 49 | } 50 | 51 | @override 52 | String toString() { 53 | return "Point {code: $code, name: $name, letter: $letter, child: Array & length = ${children.length}"; 54 | } 55 | } 56 | 57 | class Pinyin { 58 | static Pinyin? tryParse(String text) { 59 | // TODO: 2022/11/8 ipcjs 处理搜索英文首字母... 60 | if (text.isEmpty || !ChineseHelper.containsChinese(text)) { 61 | return null; 62 | } 63 | final pinyin = PinyinHelper.getPinyinE(text, separator: ' ', defPinyin: '?') 64 | .split(' '); 65 | 66 | return Pinyin._( 67 | pinyin, 68 | pinyin.join(''), 69 | pinyin.map((e) => e[0]).join(''), 70 | ); 71 | } 72 | 73 | final List pinyin; 74 | final String short; 75 | final String full; 76 | const Pinyin._(this.pinyin, this.full, this.short); 77 | } 78 | -------------------------------------------------------------------------------- /lib/modal/result.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 03/02/2019 5 | // Time: 22:43 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: xxx 8 | // 9 | 10 | import 'dart:convert'; 11 | 12 | /// CityPicker 返回的 **Result** 结果函数 13 | class Result { 14 | /// provinceId 15 | String? provinceId; 16 | 17 | /// cityId 18 | String? cityId; 19 | 20 | /// areaId 21 | String? areaId; 22 | 23 | String? villageId; // 增加第4级(村/镇)选择 24 | 25 | /// provinceName 26 | String? provinceName; 27 | 28 | /// cityName 29 | String? cityName; 30 | 31 | /// areaName 32 | String? areaName; 33 | 34 | String? villageName; // 增加第4级(村/镇)选择 35 | 36 | Result({ 37 | this.provinceId, 38 | this.cityId, 39 | this.areaId, 40 | // 增加第4级(村/镇)选择 41 | this.villageId, 42 | this.provinceName, 43 | this.cityName, 44 | this.areaName, 45 | // 增加第4级(村/镇)选择 46 | this.villageName, 47 | }); 48 | 49 | /// string json 50 | @override 51 | String toString() { 52 | //TODO: implement toString 53 | Map obj = { 54 | 'provinceName': provinceName, 55 | 'provinceId': provinceId, 56 | 'cityName': cityName, 57 | 'villageName': villageName, // 增加第4级(村/镇)选择 58 | 'cityId': cityId, 59 | 'areaName': areaName, 60 | 'areaId': areaId, 61 | 'villageId': villageId // 增加第4级(村/镇)选择 62 | }; 63 | obj.removeWhere((key, value) => value == null || value == 'null'); 64 | 65 | return json.encode(obj); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/base/base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:city_pickers/modal/base_citys.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | import '../../modal/point.dart'; 7 | import '../../modal/result.dart'; 8 | import '../mod/inherit_process.dart'; 9 | import '../show_types.dart'; 10 | import '../util.dart'; 11 | import './pickers.dart'; 12 | 13 | class BaseView extends StatefulWidget { 14 | final double? progress; 15 | final String locationCode; 16 | final ShowType showType; 17 | final Map provincesData; 18 | final Map citiesData; 19 | final ItemWidgetBuilder? itemBuilder; 20 | 21 | /// 是否对数据进行排序 22 | final bool isSort; 23 | 24 | /// ios选择框的高度. 配合 itemBuilder中的字体使用. 25 | final double? itemExtent; 26 | 27 | /// 容器高度 28 | final double height; 29 | 30 | /// 取消按钮的Widget 31 | /// 当用户设置该属性. 会优先使用用户设置的widget, 否则使用代码中默认的文本, 使用primary主题色 32 | final Widget? cancelWidget; 33 | 34 | /// 确认按钮的widget 35 | /// 当用户设置该属性. 会优先使用用户设置的widget, 否则使用代码中默认的文本, 使用primary主题色 36 | final Widget? confirmWidget; 37 | 38 | final double borderRadius; 39 | 40 | /// 是否开启全球化数据 41 | final bool? global; 42 | 43 | BaseView({ 44 | this.progress, 45 | required this.showType, 46 | required this.height, 47 | required this.locationCode, 48 | required this.citiesData, 49 | required this.provincesData, 50 | this.itemBuilder, 51 | this.itemExtent, 52 | this.cancelWidget, 53 | this.confirmWidget, 54 | this.isSort = false, 55 | this.borderRadius = 0, 56 | this.global = false, 57 | }) : assert(!(itemBuilder != null && itemExtent == null), 58 | "\ritemExtent could't be null if itemBuilder exits"); 59 | 60 | _BaseView createState() => _BaseView(); 61 | } 62 | 63 | class _BaseView extends State { 64 | Timer? _changeTimer; 65 | bool _resetControllerOnce = false; 66 | 67 | FixedExtentScrollController provinceController = 68 | new FixedExtentScrollController(); 69 | FixedExtentScrollController cityController = 70 | new FixedExtentScrollController(); 71 | FixedExtentScrollController areaController = 72 | new FixedExtentScrollController(); 73 | FixedExtentScrollController villageController = 74 | new FixedExtentScrollController(); // 增加第4级(村/镇)选择 75 | 76 | // 所有省的列表. 因为性能等综合原因, 77 | // 没有一次性构建整个以国为根的树. 动态的构建以省为根的树, 效率高. 78 | late List provinces; 79 | late CityTree cityTree; 80 | 81 | late Point targetProvince; 82 | Point? targetCity; 83 | Point? targetArea; 84 | Point? targetVillage; // 增加第4级(村/镇)选择 85 | 86 | @override 87 | void initState() { 88 | super.initState(); 89 | 90 | provinces = 91 | new Provinces(metaInfo: widget.provincesData, sort: widget.isSort) 92 | .provinces; 93 | 94 | cityTree = new CityTree( 95 | metaInfo: widget.citiesData, provincesInfo: widget.provincesData); 96 | 97 | try { 98 | _initLocation(widget.locationCode); 99 | _initController(); 100 | } catch (e) { 101 | print('Exception details:\n 初始化地理位置信息失败, 请检查省分城市数据 \n $e'); 102 | } 103 | } 104 | 105 | void dispose() { 106 | provinceController.dispose(); 107 | cityController.dispose(); 108 | areaController.dispose(); 109 | villageController.dispose(); 110 | 111 | // 增加第4级(村/镇)选择 112 | if (_changeTimer?.isActive ?? false) { 113 | _changeTimer!.cancel(); 114 | } 115 | super.dispose(); 116 | } 117 | 118 | // 初始化controller, 为了使给定的默认值, 在选框的中心位置 119 | void _initController() { 120 | ShowType showType = widget.showType; 121 | if (showType.contain(ShowType.p)) { 122 | provinceController = new FixedExtentScrollController( 123 | initialItem: 124 | provinces.indexWhere((Point p) => p.code == targetProvince.code)); 125 | } 126 | if (showType.contain(ShowType.c)) { 127 | cityController = new FixedExtentScrollController( 128 | initialItem: targetProvince.children 129 | .indexWhere((Point p) => p.code == targetCity!.code)); 130 | } 131 | if (showType.contain(ShowType.a)) { 132 | areaController = new FixedExtentScrollController( 133 | initialItem: targetCity!.children 134 | .indexWhere((Point p) => p.code == targetArea!.code)); 135 | } 136 | // 增加第4级(村/镇)选择 137 | if (showType.contain(ShowType.v)) { 138 | villageController = new FixedExtentScrollController( 139 | initialItem: targetArea!.children 140 | .indexWhere((Point p) => p.code == targetVillage!.code)); 141 | } 142 | } 143 | 144 | // 重置Controller的原因在于, 无法手动去更改initialItem, 也无法通过 145 | // jumpTo or animateTo去更改, 强行更改, 会触发 _onProvinceChange _onCityChange 与 _onAreacChange 146 | // 只为覆盖初始化化的参数initialItem 147 | void _resetController() { 148 | if (_resetControllerOnce) return; 149 | provinceController = new FixedExtentScrollController(initialItem: 0); 150 | 151 | cityController = new FixedExtentScrollController(initialItem: 0); 152 | areaController = new FixedExtentScrollController(initialItem: 0); 153 | villageController = 154 | new FixedExtentScrollController(initialItem: 0); // 增加第4级(村/镇)选择 155 | _resetControllerOnce = true; 156 | } 157 | 158 | // initialize tree by locationCode 159 | void _initLocation(String? locationCode) { 160 | String _locationCode; 161 | if (locationCode != null) { 162 | try { 163 | _locationCode = locationCode; 164 | } catch (e) { 165 | print(ArgumentError( 166 | "The Argument locationCode must be valid like: '100000' but get '$locationCode' ")); 167 | return; 168 | } 169 | 170 | targetProvince = cityTree.initTreeByCode(_locationCode); 171 | 172 | /// 为用户给出的locationCode不正确做一个容错 173 | if (targetProvince.isNull) { 174 | targetProvince = cityTree.initTreeByCode(provinces.first.code!); 175 | } 176 | targetProvince.children.forEach((Point _city) { 177 | if (_city.code == _locationCode) { 178 | targetCity = _city; 179 | targetArea = _getTargetChildFirst(_city); 180 | // 增加第4级(村/镇)选择 181 | targetVillage = _getTargetChildFirst(targetArea!); 182 | } 183 | _city.children.forEach((Point _area) { 184 | if (_area.code == _locationCode) { 185 | targetCity = _city; 186 | targetArea = _area; 187 | // 增加第4级(村/镇)选择 188 | targetVillage = _getTargetChildFirst(_area); 189 | } 190 | _area.children.forEach((Point _village) { 191 | if (_village.code == _locationCode) { 192 | targetCity = _city; 193 | targetArea = _area; 194 | // 增加第4级(村/镇)选择 195 | targetVillage = _village; 196 | } 197 | }); 198 | }); 199 | }); 200 | } else { 201 | /// 本来默认想定在北京, 但是由于有可能出现用户的省份数据为不包含北京, 所以采用第一个省份做为初始 202 | targetProvince = cityTree.initTreeByCode(widget.provincesData.keys.first); 203 | } 204 | // 尝试试图匹配到下一个级别的第一个, 205 | if (targetCity == null) { 206 | targetCity = _getTargetChildFirst(targetProvince); 207 | } 208 | // 尝试试图匹配到下一个级别的第一个, 209 | if (targetArea == null) { 210 | targetArea = _getTargetChildFirst(targetCity!); 211 | } 212 | // 增加第4级(村/镇)选择 213 | // 尝试试图匹配到下一个级别的第一个, 214 | if (targetVillage == null) { 215 | targetVillage = _getTargetChildFirst(targetArea!); 216 | } 217 | } 218 | 219 | Point? _getTargetChildFirst(Point target) { 220 | if (target == Point.nullPoint()) { 221 | return Point.nullPoint(); 222 | } 223 | if (target.children.isNotEmpty && target.children.isNotEmpty) { 224 | return target.children.first; 225 | } 226 | return Point.nullPoint(); 227 | } 228 | 229 | // 通过选中的省份, 构建以省份为根节点的树型结构 230 | List getCityItemList() { 231 | List result = []; 232 | result.addAll(targetProvince.children.toList().map((p) => p.name).toList()); 233 | return result; 234 | } 235 | 236 | List getAreaItemList() { 237 | List result = []; 238 | 239 | if (targetCity != null) { 240 | result.addAll(targetCity!.children.toList().map((p) => p.name).toList()); 241 | } 242 | return result; 243 | } 244 | 245 | // 增加第4级(村/镇)选择 246 | List getVillageItemList() { 247 | List result = []; 248 | 249 | if (targetArea != null) { 250 | result.addAll(targetArea!.children.toList().map((p) => p.name).toList()); 251 | } 252 | return result; 253 | } 254 | 255 | // province change handle 256 | // 加入延时处理, 减少构建树的消耗 257 | _onProvinceChange(Point _province) { 258 | if (_changeTimer != null && _changeTimer!.isActive) { 259 | _changeTimer!.cancel(); 260 | } 261 | _changeTimer = new Timer(Duration(milliseconds: 100), () { 262 | Point _provinceTree = cityTree.initTree(_province.code.toString()); 263 | setState(() { 264 | targetProvince = _provinceTree; 265 | targetCity = _getTargetChildFirst(_provinceTree); 266 | if (!targetCity!.isNull) { 267 | targetArea = _getTargetChildFirst(targetCity!); 268 | targetVillage = _getTargetChildFirst(targetArea!); // 增加第4级(村/镇)选择 269 | } else { 270 | targetArea = Point.nullPoint(); 271 | targetVillage = Point.nullPoint(); 272 | } 273 | _resetController(); 274 | }); 275 | }); 276 | } 277 | 278 | _onCityChange(Point _targetCity) { 279 | print('_onCityChange'); 280 | if (_changeTimer != null && _changeTimer!.isActive) { 281 | _changeTimer!.cancel(); 282 | } 283 | _changeTimer = new Timer(Duration(milliseconds: 100), () { 284 | if (!mounted) return; 285 | setState(() { 286 | targetCity = _targetCity; 287 | targetArea = _getTargetChildFirst(targetCity!); 288 | if (!targetArea!.isNull) { 289 | targetVillage = _getTargetChildFirst(targetArea!); // 增加第4级(村/镇)选择 290 | } else { 291 | targetVillage = Point.nullPoint(); 292 | } 293 | }); 294 | }); 295 | _resetController(); 296 | } 297 | 298 | _onAreaChange(Point _targetArea) { 299 | if (_changeTimer != null && _changeTimer!.isActive) { 300 | _changeTimer!.cancel(); 301 | } 302 | _changeTimer = new Timer(Duration(milliseconds: 100), () { 303 | if (!mounted) return; 304 | setState(() { 305 | targetArea = _targetArea; 306 | targetVillage = _getTargetChildFirst(targetArea!); 307 | }); 308 | }); 309 | } 310 | 311 | // 增加第4级(村/镇)选择 312 | _onVillageChange(Point _targetVillage) { 313 | if (_changeTimer != null && _changeTimer!.isActive) { 314 | _changeTimer!.cancel(); 315 | } 316 | _changeTimer = new Timer(Duration(milliseconds: 100), () { 317 | if (!mounted) return; 318 | setState(() { 319 | targetVillage = _targetVillage; 320 | }); 321 | }); 322 | } 323 | 324 | Result _buildResult() { 325 | Result result = Result(); 326 | ShowType showType = widget.showType; 327 | if (showType.contain(ShowType.p)) { 328 | result.provinceId = targetProvince.code.toString(); 329 | result.provinceName = targetProvince.name; 330 | } 331 | if (showType.contain(ShowType.c)) { 332 | result.provinceId = targetProvince.code.toString(); 333 | result.provinceName = targetProvince.name; 334 | result.cityId = targetCity?.code.toString(); 335 | result.cityName = targetCity?.name; 336 | } 337 | if (showType.contain(ShowType.a)) { 338 | result.provinceId = targetProvince.code.toString(); 339 | result.provinceName = targetProvince.name; 340 | result.cityId = targetCity?.code.toString(); 341 | result.cityName = targetCity?.name; 342 | result.areaId = targetArea?.code.toString(); 343 | result.areaName = targetArea?.name; 344 | } 345 | 346 | // 增加第4级(村/镇)选择 347 | if (showType.contain(ShowType.v)) { 348 | result.provinceId = targetProvince.code.toString(); 349 | result.provinceName = targetProvince.name; 350 | result.cityId = targetCity!.code.toString(); 351 | result.cityName = targetCity?.name; 352 | result.areaId = targetArea?.code.toString(); 353 | result.areaName = targetArea?.name; 354 | result.villageId = targetVillage?.code.toString(); 355 | result.villageName = targetVillage?.name; 356 | } 357 | // 台湾异常数据. 需要过滤 358 | // if (result.provinceId == "710000") { 359 | // result.cityId = null; 360 | // result.cityName = null; 361 | // result.areaId = null; 362 | // result.areaName = null; 363 | // result.villageId = null; 364 | // result.villageName = null; 365 | // } 366 | 367 | return result; 368 | } 369 | 370 | Widget _bottomBuild() { 371 | List pickerRows = []; 372 | if (widget.showType.contain(ShowType.p)) { 373 | pickerRows.add(new ScrollPicker( 374 | key: Key('province'), 375 | isShow: widget.showType.contain(ShowType.p), 376 | controller: provinceController, 377 | itemBuilder: widget.itemBuilder, 378 | itemExtent: widget.itemExtent, 379 | value: targetProvince.name, 380 | itemList: provinces.toList().map((v) => v.name).toList(), 381 | changed: (index) { 382 | _onProvinceChange(provinces[index]); 383 | }, 384 | )); 385 | } 386 | if (widget.showType.contain(ShowType.c)) { 387 | pickerRows.add(new ScrollPicker( 388 | key: Key('citys'), 389 | // 这个属性是为了强制刷新 390 | isShow: widget.showType.contain(ShowType.c), 391 | controller: cityController, 392 | itemBuilder: widget.itemBuilder, 393 | itemExtent: widget.itemExtent, 394 | value: targetCity?.name, 395 | itemList: getCityItemList(), 396 | changed: (index) { 397 | _onCityChange(targetProvince.children[index]); 398 | }, 399 | )); 400 | } 401 | if (widget.showType.contain(ShowType.a)) { 402 | pickerRows.add(new ScrollPicker( 403 | key: Key('towns'), 404 | isShow: widget.showType.contain(ShowType.a), 405 | controller: areaController, 406 | itemBuilder: widget.itemBuilder, 407 | itemExtent: widget.itemExtent, 408 | value: targetArea?.name, 409 | itemList: getAreaItemList(), 410 | changed: (index) { 411 | _onAreaChange(targetCity!.children[index]); 412 | }, 413 | )); 414 | } 415 | if (widget.showType.contain(ShowType.v)) { 416 | pickerRows.add(new ScrollPicker( 417 | // 增加第4级(村/镇)选择 418 | // key: Key('villages'), 419 | isShow: widget.showType.contain(ShowType.v), 420 | controller: villageController, 421 | itemBuilder: widget.itemBuilder, 422 | itemExtent: widget.itemExtent, 423 | value: targetVillage?.name, 424 | itemList: getVillageItemList(), 425 | changed: (index) { 426 | _onVillageChange(targetArea!.children[index]); 427 | }, 428 | )); 429 | } 430 | return new Container( 431 | width: double.infinity, 432 | decoration: BoxDecoration( 433 | color: Theme.of(context).scaffoldBackgroundColor, 434 | borderRadius: BorderRadius.only( 435 | topLeft: Radius.circular(widget.borderRadius), 436 | topRight: Radius.circular(widget.borderRadius)), 437 | ), 438 | child: new Column( 439 | crossAxisAlignment: CrossAxisAlignment.start, 440 | mainAxisAlignment: MainAxisAlignment.start, 441 | children: [ 442 | new Row( 443 | children: [ 444 | TextButton( 445 | onPressed: () { 446 | Navigator.pop(context); 447 | }, 448 | child: widget.cancelWidget ?? 449 | new Text( 450 | '取消', 451 | style: new TextStyle( 452 | color: Theme.of(context).primaryColor, 453 | ), 454 | ), 455 | ), 456 | TextButton( 457 | onPressed: () { 458 | Navigator.pop(context, _buildResult()); 459 | }, 460 | child: widget.confirmWidget ?? 461 | new Text( 462 | '确定', 463 | style: new TextStyle( 464 | color: Theme.of(context).primaryColor, 465 | ), 466 | ), 467 | ), 468 | ], 469 | mainAxisSize: MainAxisSize.max, 470 | crossAxisAlignment: CrossAxisAlignment.start, 471 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 472 | ), 473 | Expanded( 474 | child: new Row( 475 | children: pickerRows, 476 | ), 477 | ) 478 | ], 479 | )); 480 | } 481 | 482 | Widget build(BuildContext context) { 483 | final route = InheritRouteWidget.of(context)!.router; 484 | return new AnimatedBuilder( 485 | animation: route.animation!, 486 | builder: (BuildContext context, Widget? child) { 487 | return new CustomSingleChildLayout( 488 | delegate: _WrapLayout( 489 | progress: route.animation!.value, height: widget.height), 490 | child: new GestureDetector( 491 | child: new Material( 492 | color: Colors.transparent, 493 | child: 494 | new Container(width: double.infinity, child: _bottomBuild()), 495 | ), 496 | ), 497 | ); 498 | }, 499 | ); 500 | } 501 | } 502 | 503 | class _WrapLayout extends SingleChildLayoutDelegate { 504 | _WrapLayout({ 505 | required this.progress, 506 | required this.height, 507 | }); 508 | 509 | final double progress; 510 | final double height; 511 | 512 | @override 513 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 514 | double maxHeight = height; 515 | 516 | return new BoxConstraints( 517 | minWidth: constraints.maxWidth, 518 | maxWidth: constraints.maxWidth, 519 | minHeight: 0.0, 520 | maxHeight: maxHeight, 521 | ); 522 | } 523 | 524 | @override 525 | Offset getPositionForChild(Size size, Size childSize) { 526 | double height = size.height - childSize.height * progress; 527 | return new Offset(0.0, height); 528 | } 529 | 530 | @override 531 | bool shouldRelayout(_WrapLayout oldDelegate) { 532 | return progress != oldDelegate.progress; 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /lib/src/base/pickers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import '../util.dart'; 4 | 5 | class ScrollPicker extends StatelessWidget { 6 | final List? itemList; 7 | final Key? key; 8 | final String? value; 9 | final bool isShow; 10 | final FixedExtentScrollController? controller; 11 | final ValueChanged changed; 12 | final ItemWidgetBuilder? itemBuilder; 13 | 14 | // ios选择框的高度. 配合 itemBuilder中的字体使用. 15 | final double? itemExtent; 16 | // Constructor. {} here denote that they are optional values i.e you can use as: new MyCard() 17 | ScrollPicker( 18 | {this.key, 19 | this.controller, 20 | this.isShow = false, 21 | required this.changed, 22 | this.itemList, 23 | this.itemExtent, 24 | this.itemBuilder, 25 | this.value}); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | if (!this.isShow) { 30 | return Container(); 31 | } 32 | if (this.itemList == null || this.itemList!.isEmpty) { 33 | return new Expanded( 34 | child: Container(), 35 | ); 36 | } 37 | return new Expanded( 38 | child: new Container( 39 | color: Theme.of(context).scaffoldBackgroundColor, 40 | padding: const EdgeInsets.all(6.0), 41 | alignment: Alignment.center, 42 | child: CupertinoPicker.builder( 43 | magnification: 1.0, 44 | itemExtent: this.itemExtent ?? 40.0, 45 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 46 | scrollController: this.controller, 47 | onSelectedItemChanged: (index) { 48 | this.changed(index); 49 | }, 50 | itemBuilder: (context, index) { 51 | if (this.itemBuilder != null) { 52 | return this.itemBuilder!( 53 | this.itemList![index], this.itemList!, index); 54 | } 55 | 56 | String text = this.itemList![index]; 57 | 58 | // TODO 根据字数调整字体大小,不够优雅,可以改为根据函数计算字体大小 59 | double fontSize = 13; 60 | if (text != '') { 61 | int len = text.length; 62 | if (len >= 1 && len <= 3) { 63 | fontSize = 20; 64 | } else if (len > 3 && len <= 4) { 65 | fontSize = 18; 66 | } else if (len > 4 && len <= 5) { 67 | fontSize = 16; 68 | } else if (len > 5 && len <= 6) { 69 | fontSize = 12; 70 | } else if (len > 6 && len <= 9) { 71 | fontSize = 10; 72 | } else if (len > 9) { 73 | fontSize = 7; 74 | } 75 | } 76 | return Center( 77 | child: Text( 78 | '$text', 79 | overflow: TextOverflow.ellipsis, // 字数过多时显示省略号 80 | maxLines: 1, 81 | style: TextStyle(fontSize: fontSize), 82 | ), 83 | ); 84 | }, 85 | childCount: this.itemList!.length)), 86 | flex: 1, 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/cities_selector/alpha.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 20/02/2019 5 | // Time: 17:28 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | //import 'dart:async'; 11 | 12 | import 'package:flutter/material.dart'; 13 | 14 | typedef void AlphaChanged(String? alpha); 15 | typedef void OnTouchStart(); 16 | typedef void OnTouchMove(); 17 | typedef void OnTouchEnd(); 18 | 19 | ///Default Index data. 20 | const List ALPHAS_INDEX = const [ 21 | "A", 22 | "B", 23 | "C", 24 | "D", 25 | "E", 26 | "F", 27 | "G", 28 | "H", 29 | "I", 30 | "J", 31 | "K", 32 | "L", 33 | "M", 34 | "N", 35 | "O", 36 | "P", 37 | "Q", 38 | "R", 39 | "S", 40 | "T", 41 | "U", 42 | "V", 43 | "W", 44 | "X", 45 | "Y", 46 | "Z", 47 | "#" 48 | ]; 49 | 50 | class Alpha extends StatefulWidget { 51 | double get alphaItemSize => alphaFontSize + alphaPadding.vertical; 52 | 53 | /// 单个字母的字体大小 54 | final double alphaFontSize; 55 | final EdgeInsetsGeometry alphaPadding; 56 | final List alphas; 57 | 58 | /// 当选中的字母发生改变 59 | final AlphaChanged? onAlphaChange; 60 | 61 | final OnTouchStart? onTouchStart; 62 | final OnTouchMove? onTouchMove; 63 | final OnTouchEnd? onTouchEnd; 64 | 65 | /// 激活状态下的背景色 66 | final Color activeBgColor; 67 | 68 | /// 未激活状态下的背景色 69 | final Color bgColor; 70 | 71 | /// 未激活状态下字体的颜色 72 | final Color fontColor; 73 | 74 | /// 激活状态下字体的颜色 75 | final Color fontActiveColor; 76 | 77 | Alpha( 78 | { 79 | 80 | /// 字母列表的高度大小与字体大小 81 | this.alphaFontSize = 14, 82 | this.alphaPadding = const EdgeInsets.symmetric(horizontal: 4.0), 83 | 84 | /// 可供选择的字母集 85 | this.alphas = ALPHAS_INDEX, 86 | 87 | /// 当右侧字母集, 因触摸而产生的回调 88 | this.onAlphaChange, 89 | this.onTouchStart, 90 | this.onTouchMove, 91 | this.onTouchEnd, 92 | this.activeBgColor = Colors.green, 93 | this.bgColor = Colors.yellow, 94 | this.fontColor = Colors.black, 95 | this.fontActiveColor = Colors.yellow}); 96 | 97 | @override 98 | AlphaState createState() { 99 | return new AlphaState(); 100 | } 101 | } 102 | 103 | class AlphaState extends State { 104 | // Timer _changeTimer; 105 | 106 | bool isTouched = false; 107 | 108 | List indexRange = []; 109 | 110 | /// 第一个字母或者分类距离global坐标系的高度 111 | double? _distance2Top; 112 | 113 | // 当触摸结束前, 最后一个字母; 114 | String? _lastTag; 115 | 116 | @override 117 | void initState() { 118 | super.initState(); 119 | _init(); 120 | } 121 | 122 | _init() { 123 | List alphas = widget.alphas; 124 | for (int i = 0; i <= alphas.length; i++) { 125 | indexRange.add((i) * widget.alphaItemSize); 126 | } 127 | } 128 | 129 | String? _getHitAlpha(offset) { 130 | int hit = (offset / widget.alphaItemSize).toInt(); 131 | if (hit < 0) { 132 | return null; 133 | } 134 | if (hit >= widget.alphas.length) { 135 | return null; 136 | } 137 | return widget.alphas[hit]; 138 | } 139 | 140 | _onAlphaChange([String? tag]) { 141 | if (widget.onAlphaChange != null && tag != _lastTag) { 142 | _lastTag = tag; 143 | widget.onAlphaChange!(tag); 144 | } 145 | } 146 | 147 | _touchStartEvent(String tag) { 148 | this.setState(() { 149 | isTouched = true; 150 | }); 151 | if (tag != null) { 152 | _onAlphaChange(tag); 153 | } 154 | 155 | if (widget.onTouchStart != null && tag != null) { 156 | widget.onTouchStart!(); 157 | } 158 | } 159 | 160 | _touchMoveEvent(String tag) { 161 | if (tag != null) { 162 | _onAlphaChange(tag); 163 | } 164 | if (widget.onTouchMove != null) { 165 | widget.onTouchMove!(); 166 | } 167 | } 168 | 169 | _touchEndEvent() { 170 | this.setState(() { 171 | isTouched = false; 172 | }); 173 | // 这里本可以不用再触发一次的. 但是为了数据的准备, 最后再触发一次 174 | if (_lastTag != null) { 175 | _onAlphaChange(_lastTag); 176 | } 177 | if (widget.onTouchEnd != null) { 178 | widget.onTouchEnd!(); 179 | } 180 | } 181 | 182 | _buildAlpha() { 183 | List result = []; 184 | for (var alpha in widget.alphas) { 185 | result.add(new Container( 186 | key: Key(alpha), 187 | height: widget.alphaItemSize, 188 | padding: widget.alphaPadding, 189 | child: new Text( 190 | alpha, 191 | textAlign: TextAlign.center, 192 | style: TextStyle( 193 | fontSize: widget.alphaFontSize, 194 | color: isTouched ? widget.fontActiveColor : widget.fontColor, 195 | height: 1.0, 196 | ), 197 | ), 198 | )); 199 | } 200 | return Align( 201 | alignment: Alignment.centerRight, 202 | child: Container( 203 | alignment: Alignment.center, 204 | color: isTouched ? widget.activeBgColor : widget.bgColor, 205 | padding: EdgeInsets.symmetric(horizontal: 4), 206 | child: Column(mainAxisSize: MainAxisSize.min, children: result), 207 | )); 208 | } 209 | 210 | @override 211 | Widget build(BuildContext context) { 212 | return GestureDetector( 213 | onVerticalDragDown: (DragDownDetails details) { 214 | if (_distance2Top == null) { 215 | RenderBox? renderBox = context.findRenderObject() as RenderBox; 216 | _distance2Top = renderBox.localToGlobal(Offset.zero).dy.toInt() + 217 | (renderBox.size.height - 218 | widget.alphaItemSize * widget.alphas.length) / 219 | 2; 220 | } 221 | 222 | int touchOffset2Begin = 223 | details.globalPosition.dy.toInt() - _distance2Top!.toInt(); 224 | String? tag = _getHitAlpha(touchOffset2Begin); 225 | if (tag != null) { 226 | _touchStartEvent(tag); 227 | } 228 | }, 229 | onVerticalDragUpdate: (DragUpdateDetails details) { 230 | int touchOffset2Begin = 231 | details.globalPosition.dy.toInt() - _distance2Top!.toInt(); 232 | String? tag = _getHitAlpha(touchOffset2Begin); 233 | if (tag != null) { 234 | _touchMoveEvent(tag); 235 | } 236 | }, 237 | onVerticalDragEnd: (DragEndDetails details) { 238 | _touchEndEvent(); 239 | }, 240 | child: _buildAlpha()); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /lib/src/cities_selector/cities_selector.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 18/02/2019 5 | // Time: 17:57 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: 城市级别选择器. 支持搜索. 与字母排序 8 | // 9 | 10 | import 'dart:async'; 11 | 12 | import 'dart:math' as math; 13 | import 'package:collection/collection.dart'; 14 | import 'package:flutter/cupertino.dart'; 15 | import 'package:flutter/material.dart'; 16 | import 'package:flutter/services.dart'; 17 | import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; 18 | 19 | import '../../meta/province.dart'; 20 | import '../../modal/point.dart'; 21 | import '../../modal/result.dart'; 22 | import '../util.dart'; 23 | import 'alpha.dart'; 24 | import 'utils.dart'; 25 | 26 | export 'cities_style.dart'; 27 | 28 | const defaultTagBgColor = Color.fromRGBO(0, 0, 0, 0); 29 | const defaultTagActiveBgColor = Color(0xffeeeeee); 30 | const defaultTagActiveFontColor = Color(0xff242424); 31 | const defaultTagFontColor = Color(0xff666666); 32 | const defaultTopIndexFontColor = Color(0xffc0c0c0); 33 | const defaultTopIndexBgColor = Color(0xfff3f4f5); 34 | const defaultScaffoldBackgroundColor = Colors.white; 35 | 36 | class CitiesSelector extends StatefulWidget { 37 | static Result _createResult(Point city) { 38 | Result result = Result(); 39 | result.cityId = city.code; 40 | result.cityName = city.name; 41 | return result; 42 | } 43 | 44 | final String? locationCode; 45 | final List cities; 46 | 47 | final List? hotCities; 48 | 49 | /// 定义右侧bar的激活与普通状态的颜色 50 | final Color tagBarBgColor; 51 | final Color tagBarActiveColor; 52 | 53 | /// 定义右侧bar的字体的激活与普通状态的颜色 54 | final Color tagBarFontColor; 55 | final Color tagBarFontActiveColor; 56 | 57 | /// 右侧Bar字体的大小 58 | final double tagBarFontSize; 59 | 60 | /// 右侧Bar文字的Padding 61 | final EdgeInsetsGeometry tagBarTextPadding; 62 | 63 | /// 是否显示顶部的tag提示标签 64 | final bool showTopIndex; 65 | 66 | /// 每一个类别的城市顶部的标题的高度 67 | final double topIndexHeight; 68 | 69 | /// 每一个类别的城市顶部的标题的字体大小 70 | final double topIndexFontSize; 71 | 72 | /// 每一个类别的城市顶部的标题的样式 73 | final Color topIndexFontColor; 74 | final Color topIndexBgColor; 75 | 76 | /// 城市列表每一个Item的字体大小 77 | final double itemFontSize; 78 | 79 | final Color? itemSelectFontColor; 80 | 81 | final Color? itemFontColor; 82 | 83 | final ValueSetter onSelected; 84 | 85 | CitiesSelector({ 86 | this.locationCode, 87 | required this.cities, 88 | this.hotCities, 89 | this.tagBarActiveColor = Colors.yellow, 90 | this.tagBarFontActiveColor = Colors.red, 91 | this.tagBarBgColor = Colors.cyanAccent, 92 | this.tagBarFontColor = Colors.white, 93 | this.tagBarFontSize = 14.0, 94 | this.tagBarTextPadding = const EdgeInsets.symmetric(horizontal: 4.0), 95 | this.showTopIndex = true, 96 | this.topIndexFontSize = 16, 97 | this.topIndexHeight = 40, 98 | this.topIndexFontColor = Colors.green, 99 | this.topIndexBgColor = Colors.blueGrey, 100 | this.itemFontSize = 12.0, 101 | this.itemFontColor = Colors.black, 102 | this.itemSelectFontColor = Colors.red, 103 | required this.onSelected, 104 | }); 105 | 106 | Widget buildCityItem(BuildContext context, Point city) { 107 | CitiesSelector widget = this; 108 | // 这里使用code判断是否选择, 因为[widget.hotCities]和[widget.citiesData]有可能有相同code的城市 109 | // 此时他们应该都是选中状态 110 | bool selected = 111 | widget.locationCode != null && widget.locationCode == city.code; 112 | final theme = Theme.of(context); 113 | 114 | return ListTileTheme( 115 | selectedColor: widget.itemSelectFontColor ?? theme.colorScheme.primary, 116 | textColor: widget.itemFontColor ?? theme.colorScheme.secondary, 117 | child: ListTile( 118 | selected: selected, 119 | title: Text(city.name, style: TextStyle(fontSize: widget.itemFontSize)), 120 | onTap: () { 121 | widget.onSelected(_createResult(city)); 122 | }, 123 | ), 124 | ); 125 | } 126 | 127 | @override 128 | _CitiesSelectorState createState() => _CitiesSelectorState(); 129 | } 130 | 131 | class _CitiesSelectorState extends State { 132 | String? _touchedTagName; 133 | Timer? _changeTimer; 134 | 135 | int _initialScrollIndex = -1; 136 | bool _isTouchTagBar = false; 137 | 138 | /// 城市列表数组 139 | List _cities = []; 140 | late ItemScrollController _scrollController; 141 | late ItemPositionsListener _positionsListener; 142 | 143 | late Map _tagToIndexMap; 144 | 145 | /// 有效的tag标签列表, 对应右侧标签 146 | late List _tagList; 147 | 148 | @override 149 | void initState() { 150 | super.initState(); 151 | _cities = [ 152 | if (widget.hotCities != null) 153 | ...widget.hotCities!.map((e) => Point( 154 | code: e.id, 155 | letter: e.tag, 156 | name: e.name, 157 | children: [], 158 | )), 159 | ...widget.cities, 160 | ]; 161 | 162 | _tagToIndexMap = _generateTagToIndexMap(_cities); 163 | _initialScrollIndex = getInitialCityCodeIndex(); 164 | _tagList = CitiesUtils.getValidTagsByCityList(_cities); 165 | 166 | _scrollController = new ItemScrollController(); 167 | _positionsListener = ItemPositionsListener.create(); 168 | } 169 | 170 | @override 171 | void dispose() { 172 | _changeTimer?.cancel(); 173 | super.dispose(); 174 | } 175 | 176 | int getInitialCityCodeIndex() { 177 | final code = widget.locationCode; 178 | if (code == null) { 179 | return -1; 180 | } 181 | return _cities.indexWhere((Point point) { 182 | return point.code == code; 183 | }); 184 | } 185 | 186 | Map _generateTagToIndexMap(List cities) { 187 | final map = {}; 188 | String? prevLetter; 189 | for (var i = 0; i < cities.length; i++) { 190 | var letter = cities[i].letter; 191 | if (letter != prevLetter) { 192 | map[letter ?? ''] = i; 193 | prevLetter = letter; 194 | } 195 | } 196 | return map; 197 | } 198 | 199 | /// 当右侧的类型. 因为触摸而发生改变 200 | _onTagChange(String alpha) { 201 | if (_changeTimer?.isActive ?? false) { 202 | _changeTimer!.cancel(); 203 | } 204 | HapticFeedback.selectionClick(); 205 | _changeTimer = new Timer(Duration(milliseconds: 30), () { 206 | final index = _tagToIndexMap[alpha]; 207 | if (index != null) { 208 | _scrollController.jumpTo(index: index); 209 | } 210 | }); 211 | } 212 | 213 | @override 214 | Widget build(BuildContext context) { 215 | return LayoutBuilder( 216 | builder: (context, c) => Stack( 217 | children: _buildChildren(context, c.maxHeight), 218 | ), 219 | ); 220 | } 221 | 222 | /// 生成中间的字母提示Modal 223 | Widget _buildCenterModal() { 224 | return Center( 225 | child: Card( 226 | color: Colors.black54, 227 | child: Container( 228 | alignment: Alignment.center, 229 | width: 80.0, 230 | height: 80.0, 231 | child: Text( 232 | _touchedTagName ?? _tagList.first, 233 | style: TextStyle( 234 | fontSize: 32.0, 235 | color: Colors.white, 236 | ), 237 | ), 238 | ), 239 | ), 240 | ); 241 | } 242 | 243 | Widget _buildAlphaAndTags() { 244 | return Alpha( 245 | alphas: _tagList, 246 | activeBgColor: widget.tagBarActiveColor, 247 | bgColor: widget.tagBarBgColor, 248 | fontColor: widget.tagBarFontColor, 249 | fontActiveColor: widget.tagBarFontActiveColor, 250 | alphaFontSize: widget.tagBarFontSize, 251 | alphaPadding: widget.tagBarTextPadding, 252 | onTouchStart: () { 253 | this.setState(() { 254 | _isTouchTagBar = true; 255 | }); 256 | }, 257 | onTouchEnd: () { 258 | this.setState(() { 259 | _isTouchTagBar = false; 260 | }); 261 | }, 262 | onAlphaChange: (String? alpha) { 263 | this.setState(() { 264 | if (!_isTouchTagBar) { 265 | _isTouchTagBar = true; 266 | } 267 | _touchedTagName = alpha; 268 | }); 269 | if (alpha != null) { 270 | _onTagChange(alpha); 271 | } 272 | }, 273 | ); 274 | } 275 | 276 | List _buildChildren(BuildContext context, double height) { 277 | List children = []; 278 | 279 | bool hideTag(int index) => 280 | index != 0 && _cities[index - 1].letter == _cities[index].letter; 281 | 282 | children.add(ScrollablePositionedList.builder( 283 | initialScrollIndex: math.max(0, _initialScrollIndex), 284 | initialAlignment: _initialScrollIndex > 0 && 285 | widget.showTopIndex && 286 | !_tagToIndexMap.containsValue(_initialScrollIndex) 287 | // 不显示tag的item, 顶部会被常显的topTag挡住, 需要往下偏移 288 | ? widget.topIndexHeight / height 289 | : 0, 290 | itemScrollController: _scrollController, 291 | itemPositionsListener: _positionsListener, 292 | itemCount: _cities.length, 293 | itemBuilder: (context, index) { 294 | return Column( 295 | children: [ 296 | Offstage( 297 | offstage: hideTag(index), 298 | child: Container( 299 | height: widget.topIndexHeight, 300 | alignment: Alignment.centerLeft, 301 | padding: const EdgeInsets.only(left: 15.0), 302 | color: widget.topIndexBgColor, 303 | child: Text( 304 | _cities[index].letter ?? "", 305 | softWrap: true, 306 | style: TextStyle( 307 | fontSize: widget.topIndexFontSize, 308 | color: widget.topIndexFontColor), 309 | ), 310 | ), 311 | ), 312 | Container( 313 | alignment: Alignment.centerLeft, 314 | child: Center( 315 | child: widget.buildCityItem(context, _cities[index]), 316 | ), 317 | ) 318 | ], 319 | ); 320 | }, 321 | )); 322 | if (widget.showTopIndex) { 323 | children.add(ValueListenableBuilder>( 324 | valueListenable: _positionsListener.itemPositions, 325 | builder: (context, value, child) { 326 | // value不是有序的, 需要排序 327 | final positions = value.sortedBy((it) => it.index); 328 | 329 | final firstPosition = positions.firstOrNull; 330 | final tagName = firstPosition == null 331 | ? null 332 | : _cities[firstPosition.index].letter; 333 | 334 | final firstFullyVisibleTagPosition = positions.firstWhereOrNull( 335 | (it) => 336 | it.itemLeadingEdge > 0 && 337 | _tagToIndexMap.containsValue(it.index)); 338 | final top = firstFullyVisibleTagPosition != null 339 | ? math.min( 340 | 0.0, 341 | firstFullyVisibleTagPosition.itemLeadingEdge * height - 342 | widget.topIndexHeight) 343 | : 0.0; 344 | 345 | return Positioned( 346 | top: top, 347 | left: 0, 348 | right: 0, 349 | child: Opacity( 350 | opacity: firstFullyVisibleTagPosition?.index == 0 ? 0 : 1, 351 | child: Container( 352 | height: widget.topIndexHeight, 353 | alignment: Alignment.centerLeft, 354 | padding: const EdgeInsets.only(left: 15.0), 355 | color: widget.topIndexBgColor, 356 | child: Text( 357 | tagName ?? _tagList.first, 358 | softWrap: true, 359 | style: TextStyle( 360 | fontSize: widget.topIndexFontSize, 361 | color: widget.topIndexFontColor, 362 | ), 363 | ), 364 | ), 365 | ), 366 | ); 367 | }, 368 | )); 369 | } 370 | children.add(Offstage( 371 | offstage: !_isTouchTagBar, 372 | child: _buildCenterModal(), 373 | )); 374 | 375 | /// 加入字母 376 | children.add(Positioned( 377 | right: 0, 378 | top: 0, 379 | bottom: 0, 380 | child: _buildAlphaAndTags(), 381 | )); 382 | return children; 383 | } 384 | } 385 | 386 | class CitiesSelectorPage extends StatefulWidget { 387 | const CitiesSelectorPage({ 388 | Key? key, 389 | required this.buildCitiesSelector, 390 | this.title = '城市选择器', 391 | this.scaffoldBackgroundColor, 392 | this.appBarBuilder, 393 | this.useSearchAppBar = false, 394 | this.provincesData, 395 | this.citiesData, 396 | }) : assert(!(useSearchAppBar && appBarBuilder != null)), 397 | super(key: key); 398 | final Map? provincesData; 399 | final Map? citiesData; 400 | 401 | final CitiesSelector Function( 402 | BuildContext context, 403 | List cities, 404 | ) buildCitiesSelector; 405 | final Color? scaffoldBackgroundColor; 406 | final String title; 407 | final AppBarBuilder? appBarBuilder; 408 | final bool useSearchAppBar; 409 | 410 | @override 411 | State createState() => _CitiesSelectorPageState(); 412 | } 413 | 414 | class _CitiesSelectorPageState extends State { 415 | late final cities = CitiesUtils.getAllCitiesByMeta( 416 | widget.provincesData ?? provincesData, 417 | widget.citiesData ?? citiesData, 418 | ); 419 | late final _citiesSearcher = CitiesSearcher(cities); 420 | 421 | String _query = ''; 422 | AppBar _buildAppBar() { 423 | if (widget.appBarBuilder != null) { 424 | return widget.appBarBuilder!(widget.title); 425 | } 426 | return AppBar( 427 | title: Text(widget.title), 428 | ); 429 | } 430 | 431 | @override 432 | Widget build(BuildContext context) { 433 | return Scaffold( 434 | backgroundColor: widget.scaffoldBackgroundColor, 435 | appBar: widget.useSearchAppBar 436 | ? AppBar( 437 | automaticallyImplyLeading: false, 438 | elevation: 0, 439 | backgroundColor: Theme.of(context).colorScheme.surface, 440 | titleSpacing: 0, 441 | title: Padding( 442 | padding: EdgeInsetsDirectional.only(start: 16.0), 443 | child: CupertinoSearchTextField( 444 | prefixInsets: EdgeInsetsDirectional.only(start: 6), 445 | placeholder: '输入城市名或拼音查询', 446 | onChanged: (value) { 447 | setState(() { 448 | _query = value; 449 | }); 450 | }, 451 | ), 452 | ), 453 | actions: [ 454 | CupertinoButton( 455 | child: Text('取消'), 456 | onPressed: () => Navigator.of(context).pop(), 457 | ), 458 | ], 459 | ) 460 | : _buildAppBar(), 461 | resizeToAvoidBottomInset: !widget.useSearchAppBar, 462 | body: SafeArea( 463 | bottom: true, 464 | child: widget.useSearchAppBar 465 | ? _buildSearchBody(context) 466 | : widget.buildCitiesSelector(context, cities), 467 | ), 468 | ); 469 | } 470 | 471 | Widget _buildSearchBody(BuildContext context) { 472 | final citiesSelector = widget.buildCitiesSelector(context, cities); 473 | Widget? queryResult; 474 | if (_query.trim().isNotEmpty) { 475 | final cities = _citiesSearcher.search(_query); 476 | queryResult = ColoredBox( 477 | color: widget.scaffoldBackgroundColor ?? 478 | Theme.of(context).scaffoldBackgroundColor, 479 | child: ListView.builder( 480 | itemCount: cities.length, 481 | itemBuilder: (context, index) => citiesSelector.buildCityItem( 482 | context, 483 | cities[index], 484 | ), 485 | ), 486 | ); 487 | } 488 | return NotificationListener( 489 | onNotification: (notification) { 490 | final focusScope = FocusScope.of(context); 491 | if ((notification is OverscrollNotification || 492 | notification is ScrollUpdateNotification) && 493 | focusScope.hasFocus) { 494 | focusScope.unfocus(); 495 | } 496 | return false; 497 | }, 498 | child: Stack( 499 | children: [ 500 | citiesSelector, 501 | if (queryResult != null) queryResult, 502 | ], 503 | ), 504 | ); 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /lib/src/cities_selector/cities_style.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // 公共的基本样式 4 | class BaseStyle { 5 | double fontSize; 6 | Color color; 7 | Color? activeColor; 8 | Color? backgroundColor; 9 | double? height; 10 | Color? backgroundActiveColor; 11 | 12 | BaseStyle({ 13 | required this.color, 14 | required this.fontSize, 15 | this.height, 16 | this.activeColor, 17 | this.backgroundActiveColor, 18 | this.backgroundColor, 19 | }); 20 | 21 | BaseStyle copyWith({ 22 | double? fontSize, 23 | Color? color, 24 | double? height, 25 | Color? activeColor, 26 | Color? backgroundColor, 27 | Color? backgroundActiveColor, 28 | }) { 29 | // print("copyWidth >>> fontSize: ${fontSize ?? this.fontSize}"); 30 | return BaseStyle( 31 | fontSize: fontSize ?? this.fontSize, 32 | color: color ?? this.color, 33 | height: height ?? this.height, 34 | activeColor: activeColor ?? this.activeColor, 35 | backgroundColor: backgroundColor ?? this.backgroundColor, 36 | backgroundActiveColor: 37 | backgroundActiveColor ?? this.backgroundActiveColor); 38 | } 39 | 40 | BaseStyle merge(BaseStyle? other) { 41 | if (other == null) return this; 42 | return copyWith( 43 | fontSize: other.fontSize, 44 | color: other.color, 45 | height: other.height, 46 | activeColor: other.activeColor, 47 | backgroundColor: other.backgroundColor, 48 | backgroundActiveColor: other.backgroundActiveColor); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/cities_selector/types.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 06/03/2019 5 | // Time: 22:52 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: 暂时未使用到. 目的是将builder暴露给开发者, 方便其自定义样式 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | 12 | typedef Widget CityItemWidgetBuilder(BuildContext context); 13 | 14 | /// Called to build IndexBar. 15 | typedef Widget IndexBarBuilder(BuildContext context, List tags); 16 | 17 | /// Called to build index hint. 18 | typedef Widget IndexHintBuilder(BuildContext context, String hint); 19 | -------------------------------------------------------------------------------- /lib/src/cities_selector/utils.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 21/02/2019 5 | // Time: 10:37 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:collection/collection.dart'; 11 | import 'package:lpinyin/lpinyin.dart'; 12 | 13 | import '../../modal/base_citys.dart'; 14 | import '../../modal/point.dart'; 15 | 16 | class CitiesSearcher { 17 | final List _cities; 18 | CitiesSearcher(this._cities); 19 | 20 | String _prevQuery = ''; 21 | late List _prevQueryResult = _cities; 22 | 23 | List search(String text) { 24 | final query = text.trim().toLowerCase(); 25 | if (query == _prevQuery) { 26 | // 查询条件相同, 结果相同 27 | return _prevQueryResult; 28 | } 29 | 30 | final cities = query.startsWith(_prevQuery) 31 | // 查询条件范围变窄, 可以直接在上次的查询结果基础上过滤 32 | ? _prevQueryResult 33 | : _cities; 34 | 35 | final result = []; 36 | final queryPinyin = ChineseHelper.containsChinese(query) 37 | ? null 38 | : query.replaceAll(RegExp(r'\s'), ''); 39 | 40 | for (final city in cities) { 41 | if (queryPinyin != null) { 42 | final pinyin = city.pinyin; 43 | if (pinyin != null) { 44 | if (pinyin.short.startsWith(queryPinyin) || 45 | pinyin.full.startsWith(queryPinyin)) { 46 | result.add(city); 47 | continue; 48 | } 49 | } 50 | } 51 | if ((city.letter?.toLowerCase().startsWith(query) == true) || 52 | city.lowerCaseName.contains(query)) { 53 | result.add(city); 54 | } 55 | } 56 | return result; 57 | } 58 | } 59 | 60 | class CitiesUtils { 61 | /// 获取城市选择器所有的数据 62 | static List getAllCitiesByMeta( 63 | Map provinceMeta, 64 | Map citiesMeta, 65 | ) { 66 | CityTree citiesTreeBuilder = new CityTree( 67 | metaInfo: citiesMeta, 68 | provincesInfo: provinceMeta, 69 | ); 70 | 71 | final provinces = provinceMeta.keys 72 | .map((provinceId) => citiesTreeBuilder.initTree(provinceId)) 73 | .toList(); 74 | 75 | List cities = []; 76 | for (final province in provinces) { 77 | for (final city in province.children) { 78 | if (city.isClassificationNode) { 79 | // city级的"分类节点", 下面是"省直辖县级行政区", 这里也把他们看作是一个city 80 | cities.addAll(city.children); 81 | } else { 82 | cities.add(city); 83 | } 84 | } 85 | } 86 | // 归并排序, 结果稳定 87 | mergeSort( 88 | cities, 89 | compare: (p0, p1) { 90 | final c0 = p0.letter!.codeUnitAt(0); 91 | final c1 = p1.letter!.codeUnitAt(0); 92 | return c0.compareTo(c1); 93 | }, 94 | ); 95 | return cities; 96 | } 97 | 98 | static List getValidTagsByCityList(List citiesList) { 99 | List validTags = []; 100 | 101 | /// 先分类 102 | String lastTag = ''; 103 | citiesList.forEach((Point item) { 104 | if (item.letter != lastTag) { 105 | validTags.add(item.letter!); 106 | lastTag = item.letter!; 107 | } 108 | }); 109 | return validTags; 110 | } 111 | } 112 | 113 | /// 热闹城市对象 114 | class HotCity { 115 | final String name; 116 | final String id; 117 | final String tag; 118 | 119 | HotCity({required this.name, required this.id, this.tag = "★"}); 120 | } 121 | -------------------------------------------------------------------------------- /lib/src/city_picker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import 'package:city_pickers/src/base/base.dart'; 3 | import 'package:city_pickers/src/cities_selector/cities_selector.dart'; 4 | import 'package:city_pickers/src/cities_selector/utils.dart'; 5 | import 'package:city_pickers/src/full_page/full_page.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:city_pickers/src/utils/index.dart'; 8 | 9 | import '../meta/province.dart' as meta; 10 | import '../modal/result.dart'; 11 | import './util.dart'; 12 | import 'mod/picker_popup_route.dart'; 13 | import 'show_types.dart'; 14 | 15 | /// ios city pickers 16 | /// provide config height, initLocation and so on 17 | /// 18 | /// Sample:flutter format 19 | /// ``` 20 | /// await CityPicker.showPicker( 21 | /// location: String, 22 | /// height: double 23 | /// ); 24 | /// 25 | /// ``` 26 | 27 | class CityPickers { 28 | /// static original city data for this plugin 29 | static Map metaCities = meta.citiesData; 30 | 31 | /// static original province data for this plugin 32 | static Map metaProvinces = meta.provincesData; 33 | 34 | static utils( 35 | {Map? provinceData, Map? citiesData}) { 36 | print("CityPickers.metaProvinces::: ${CityPickers.metaCities}"); 37 | return CityPickerUtil( 38 | provincesData: provinceData ?? CityPickers.metaProvinces, 39 | citiesData: citiesData ?? CityPickers.metaCities, 40 | ); 41 | } 42 | 43 | /// use 44 | /// @param context BuildContext for navigator 45 | /// @param locationCode initial select, one of province area or city id 46 | /// if given id is provinceId, the city and area id will be this province's first city and first area in metadata 47 | /// @param height Container's height 48 | /// 49 | /// @param Theme used it's primaryColor 50 | /// 51 | /// @param barrierDismissible whether user can dismiss the modal by touch background 52 | /// 53 | /// @param cancelWidget customer widget for building cancel button 54 | /// 55 | /// @param confirmWidget customer widget for building confirm button 56 | /// 57 | /// @param itemBuilder customer widget for building item 58 | /// 59 | /// @parma borderRadius Container topleft and topRight radius default 0. 60 | /// @return Result see [Result] 61 | /// 62 | static Future showCityPicker( 63 | {required BuildContext context, 64 | showType = ShowType.pca, 65 | double height = 400.0, 66 | String locationCode = '110000', 67 | ThemeData? theme, 68 | Map? citiesData, 69 | Map? provincesData, 70 | // CityPickerRoute params 71 | bool barrierDismissible = true, 72 | double barrierOpacity = 0.5, 73 | ItemWidgetBuilder? itemBuilder, 74 | double? itemExtent, 75 | Widget? cancelWidget, 76 | Widget? confirmWidget, 77 | double borderRadius = 0, 78 | bool isSort = false}) { 79 | return Navigator.of(context, rootNavigator: true).push( 80 | new CityPickerRoute( 81 | theme: theme ?? Theme.of(context), 82 | canBarrierDismiss: barrierDismissible, 83 | barrierOpacity: barrierOpacity, 84 | barrierLabel: 85 | MaterialLocalizations.of(context).modalBarrierDismissLabel, 86 | child: BaseView( 87 | isSort: isSort, 88 | showType: showType, 89 | height: height, 90 | itemExtent: itemExtent, 91 | itemBuilder: itemBuilder, 92 | cancelWidget: cancelWidget, 93 | confirmWidget: confirmWidget, 94 | citiesData: citiesData ?? meta.citiesData, 95 | provincesData: provincesData ?? meta.provincesData, 96 | locationCode: locationCode, 97 | borderRadius: borderRadius, 98 | ), 99 | ), 100 | ); 101 | } 102 | 103 | /// @theme Theme used it's primaryColor 104 | static Future showFullPageCityPicker({ 105 | required BuildContext context, 106 | ThemeData? theme, 107 | ShowType showType = ShowType.pca, 108 | String locationCode = '110000', 109 | Map? citiesData, 110 | Map? provincesData, 111 | }) { 112 | return Navigator.push( 113 | context, 114 | new PageRouteBuilder( 115 | transitionDuration: const Duration(milliseconds: 250), 116 | pageBuilder: (context, _, __) => new Theme( 117 | data: theme ?? Theme.of(context), 118 | child: FullPage( 119 | showType: showType, 120 | locationCode: locationCode, 121 | citiesData: citiesData ?? meta.citiesData, 122 | provincesData: provincesData ?? meta.provincesData, 123 | ), 124 | ), 125 | transitionsBuilder: 126 | (_, Animation animation, __, Widget child) => 127 | new SlideTransition( 128 | position: new Tween( 129 | begin: Offset(0.0, 1.0), 130 | end: Offset(0.0, 0.0), 131 | ).animate(animation), 132 | child: child, 133 | ), 134 | ), 135 | ); 136 | } 137 | 138 | static Future showCitiesSelector({ 139 | required BuildContext context, 140 | ThemeData? theme, 141 | String? locationCode, 142 | String title = '城市选择器', 143 | Map citiesData = meta.citiesData, 144 | Map provincesData = meta.provincesData, 145 | AppBarBuilder? appBarBuilder, 146 | List? hotCities, 147 | BaseStyle? sideBarStyle, 148 | BaseStyle? cityItemStyle, 149 | BaseStyle? topStickStyle, 150 | Color? scaffoldBackgroundColor, 151 | bool useSearchAppBar = false, 152 | EdgeInsetsGeometry tagBarTextPadding = 153 | const EdgeInsets.symmetric(horizontal: 4.0), 154 | }) { 155 | BaseStyle _sideBarStyle = BaseStyle( 156 | fontSize: 14, 157 | color: defaultTagFontColor, 158 | activeColor: defaultTagActiveBgColor, 159 | backgroundColor: defaultTagBgColor, 160 | backgroundActiveColor: defaultTagActiveBgColor, 161 | ); 162 | if (sideBarStyle != null) { 163 | _sideBarStyle = _sideBarStyle.merge(sideBarStyle); 164 | } 165 | 166 | BaseStyle _cityItemStyle = BaseStyle( 167 | fontSize: 12, 168 | color: Colors.black, 169 | activeColor: Colors.red, 170 | ); 171 | if (cityItemStyle != null) { 172 | _cityItemStyle = _cityItemStyle.merge(cityItemStyle); 173 | } 174 | 175 | BaseStyle _topStickStyle = BaseStyle( 176 | fontSize: 16, 177 | height: 40, 178 | color: defaultTopIndexFontColor, 179 | backgroundColor: defaultTopIndexBgColor, 180 | ); 181 | if (topStickStyle != null) { 182 | _topStickStyle = _topStickStyle.merge(topStickStyle); 183 | } 184 | return Navigator.push( 185 | context, 186 | new PageRouteBuilder( 187 | transitionDuration: const Duration(milliseconds: 250), 188 | pageBuilder: (context, _, __) => new Theme( 189 | data: theme ?? Theme.of(context), 190 | child: CitiesSelectorPage( 191 | title: title, 192 | appBarBuilder: appBarBuilder, 193 | scaffoldBackgroundColor: 194 | scaffoldBackgroundColor ?? defaultScaffoldBackgroundColor, 195 | useSearchAppBar: useSearchAppBar, 196 | provincesData: provincesData, 197 | citiesData: citiesData, 198 | buildCitiesSelector: (context, cities) => CitiesSelector( 199 | cities: cities, 200 | hotCities: hotCities, 201 | locationCode: locationCode, 202 | tagBarActiveColor: _sideBarStyle.backgroundActiveColor!, 203 | tagBarFontActiveColor: _sideBarStyle.activeColor!, 204 | tagBarBgColor: _sideBarStyle.backgroundColor!, 205 | tagBarFontColor: _sideBarStyle.color, 206 | tagBarFontSize: _sideBarStyle.fontSize, 207 | tagBarTextPadding: tagBarTextPadding, 208 | topIndexFontSize: _topStickStyle.fontSize, 209 | topIndexHeight: _topStickStyle.height!, 210 | topIndexFontColor: _topStickStyle.color, 211 | topIndexBgColor: _topStickStyle.backgroundColor!, 212 | itemFontColor: _cityItemStyle.color, 213 | itemFontSize: _cityItemStyle.fontSize, 214 | itemSelectFontColor: _cityItemStyle.activeColor, 215 | onSelected: (value) => Navigator.pop(context, value), 216 | ), 217 | ), 218 | ), 219 | transitionsBuilder: 220 | (_, Animation animation, __, Widget child) => 221 | new SlideTransition( 222 | position: new Tween( 223 | begin: Offset(0.0, 1.0), 224 | end: Offset(0.0, 0.0), 225 | ).animate(animation), 226 | child: child, 227 | ), 228 | ), 229 | ); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /lib/src/full_page/full_page.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 10/02/2019 5 | // Time: 21:52 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'dart:async'; 11 | 12 | import 'package:city_pickers/modal/base_citys.dart'; 13 | import 'package:city_pickers/modal/point.dart'; 14 | import 'package:city_pickers/modal/result.dart'; 15 | import 'package:city_pickers/src/show_types.dart'; 16 | import 'package:city_pickers/src/util.dart'; 17 | import 'package:flutter/material.dart'; 18 | 19 | class FullPage extends StatefulWidget { 20 | final String? locationCode; 21 | final ShowType showType; 22 | final Map provincesData; 23 | final Map citiesData; 24 | 25 | FullPage({ 26 | this.locationCode, 27 | required this.showType, 28 | required this.provincesData, 29 | required this.citiesData, 30 | }); 31 | 32 | @override 33 | _FullPageState createState() => _FullPageState(); 34 | } 35 | 36 | // 界面状态 37 | enum Status { 38 | Province, 39 | City, 40 | Area, 41 | Over, 42 | } 43 | 44 | class HistoryPageInfo { 45 | Status status; 46 | List itemList; 47 | 48 | HistoryPageInfo({required this.status, required this.itemList}); 49 | } 50 | 51 | class _FullPageState extends State { 52 | /// if pophome has been called once, this should bee true; 53 | /// in initState func shound set false; fixed: https://github.com/hanxu317317/city_pickers/issues/121 54 | bool hasPop = false; 55 | 56 | /// list scroll control 57 | late ScrollController scrollController; 58 | 59 | /// provinces object [Point] 60 | late List provinces; 61 | 62 | /// cityTree modal ,for building tree that root is province 63 | late CityTree cityTree; 64 | 65 | /// page current statue, show p or a or c or over 66 | late Status pageStatus; 67 | 68 | /// show items maybe province city or area; 69 | 70 | late List itemList; 71 | 72 | /// body history, the max length is three 73 | List _history = []; 74 | 75 | /// the target province user selected 76 | late Point targetProvince; 77 | 78 | /// the target city user selected 79 | Point? targetCity; 80 | 81 | /// the target area user selected 82 | Point? targetArea; 83 | 84 | @override 85 | void initState() { 86 | super.initState(); 87 | hasPop = false; 88 | scrollController = new ScrollController(); 89 | provinces = new Provinces(metaInfo: widget.provincesData).provinces; 90 | cityTree = new CityTree( 91 | metaInfo: widget.citiesData, provincesInfo: widget.provincesData); 92 | itemList = provinces; 93 | pageStatus = Status.Province; 94 | try { 95 | _initLocation(widget.locationCode); 96 | } catch (e) { 97 | print('Exception details:\n 初始化地理位置信息失败, 请检查省分城市数据 \n $e'); 98 | } 99 | } 100 | 101 | Future back() { 102 | HistoryPageInfo? last = _history.length > 0 ? _history.last : null; 103 | if (last != null && mounted) { 104 | this.setState(() { 105 | pageStatus = last.status; 106 | itemList = last.itemList; 107 | }); 108 | _history.removeLast(); 109 | return Future.value(false); 110 | } 111 | return Future.value(true); 112 | } 113 | 114 | void _initLocation(String? locationCode) { 115 | String _locationCode; 116 | if (locationCode != null) { 117 | try { 118 | _locationCode = locationCode; 119 | } catch (e) { 120 | print(ArgumentError( 121 | "The Argument locationCode must be valid like: '100000' but get '$locationCode' ")); 122 | return; 123 | } 124 | 125 | targetProvince = cityTree.initTreeByCode(_locationCode); 126 | if (targetProvince.isNull) { 127 | targetProvince = cityTree.initTreeByCode(provinces.first.code!); 128 | } 129 | targetProvince.children.forEach((Point _city) { 130 | if (_city.code == _locationCode) { 131 | targetCity = _city; 132 | targetArea = _getTargetChildFirst(_city) ?? null; 133 | } 134 | _city.children.forEach((Point _area) { 135 | if (_area.code == _locationCode) { 136 | targetCity = _city; 137 | targetArea = _area; 138 | } 139 | }); 140 | }); 141 | } else { 142 | targetProvince = cityTree.initTreeByCode(widget.provincesData.keys.first); 143 | } 144 | 145 | if (targetCity == null) { 146 | targetCity = _getTargetChildFirst(targetProvince); 147 | } 148 | if (targetArea == null) { 149 | targetArea = _getTargetChildFirst(targetCity!); 150 | } 151 | } 152 | 153 | Result _buildResult() { 154 | Result result = Result(); 155 | ShowType showType = widget.showType; 156 | try { 157 | if (showType.contain(ShowType.p)) { 158 | result.provinceId = targetProvince.code.toString(); 159 | result.provinceName = targetProvince.name; 160 | } 161 | if (showType.contain(ShowType.c)) { 162 | result.provinceId = targetProvince.code.toString(); 163 | result.provinceName = targetProvince.name; 164 | result.cityId = targetCity?.code.toString(); 165 | result.cityName = targetCity?.name; 166 | } 167 | if (showType.contain(ShowType.a)) { 168 | result.provinceId = targetProvince.code.toString(); 169 | result.provinceName = targetProvince.name; 170 | result.cityId = targetCity?.code.toString(); 171 | result.cityName = targetCity?.name; 172 | result.areaId = targetArea?.code.toString(); 173 | result.areaName = targetArea?.name; 174 | } 175 | } catch (e) { 176 | print('Exception details:\n _buildResult error \n $e'); 177 | // 此处兼容, 部分城市下无地区信息的情况 178 | } 179 | 180 | // 台湾异常数据. 需要过滤 181 | // if (result.provinceId == "710000") { 182 | // result.cityId = null; 183 | // result.cityName = null; 184 | // result.areaId = null; 185 | // result.areaName = null; 186 | // } 187 | return result; 188 | } 189 | 190 | Point? _getTargetChildFirst(Point target) { 191 | if (target == null) { 192 | return null; 193 | } 194 | if (target.children != null && target.children.isNotEmpty) { 195 | return target.children.first; 196 | } 197 | return null; 198 | } 199 | 200 | popHome() { 201 | if (!hasPop) { 202 | setState(() { 203 | hasPop = true; 204 | }); 205 | Navigator.of(context).pop(_buildResult()); 206 | } 207 | } 208 | 209 | _onProvinceSelect(Point province) { 210 | this.setState(() { 211 | targetProvince = cityTree.initTree(province.code!); 212 | }); 213 | } 214 | 215 | _onAreaSelect(Point area) { 216 | this.setState(() { 217 | targetArea = area; 218 | }); 219 | } 220 | 221 | _onCitySelect(Point city) { 222 | this.setState(() { 223 | targetCity = city; 224 | }); 225 | } 226 | 227 | String _getSelectedId() { 228 | String? selectId; 229 | switch (pageStatus) { 230 | case Status.Province: 231 | selectId = targetProvince.code; 232 | break; 233 | case Status.City: 234 | selectId = targetCity?.code; 235 | break; 236 | case Status.Area: 237 | selectId = targetArea?.code; 238 | break; 239 | case Status.Over: 240 | break; 241 | } 242 | return selectId ?? '0'; 243 | } 244 | 245 | /// 所有选项的点击事件入口 246 | /// @param targetPoint 被点击对象的point对象 247 | _onItemSelect(Point targetPoint) { 248 | _history.add(HistoryPageInfo(itemList: itemList, status: pageStatus)); 249 | Status nextStatus = Status.Over; 250 | List? nextItemList; 251 | switch (pageStatus) { 252 | case Status.Province: 253 | _onProvinceSelect(targetPoint); 254 | nextStatus = Status.City; 255 | nextItemList = targetProvince.children; 256 | if (!widget.showType.contain(ShowType.c)) { 257 | nextStatus = Status.Over; 258 | } 259 | if (nextItemList.isEmpty) { 260 | targetCity = null; 261 | targetArea = null; 262 | nextStatus = Status.Over; 263 | } 264 | break; 265 | case Status.City: 266 | _onCitySelect(targetPoint); 267 | nextStatus = Status.Area; 268 | nextItemList = targetCity?.children; 269 | if (!widget.showType.contain(ShowType.a)) { 270 | nextStatus = Status.Over; 271 | } 272 | if (nextItemList == null || nextItemList.isEmpty) { 273 | targetArea = null; 274 | nextStatus = Status.Over; 275 | } 276 | break; 277 | case Status.Area: 278 | nextStatus = Status.Over; 279 | _onAreaSelect(targetPoint); 280 | break; 281 | case Status.Over: 282 | break; 283 | } 284 | 285 | setTimeout( 286 | milliseconds: 300, 287 | callback: () { 288 | if (nextItemList == null || nextStatus == Status.Over) { 289 | return popHome(); 290 | } 291 | if (mounted) { 292 | this.setState(() { 293 | itemList = nextItemList!; 294 | pageStatus = nextStatus; 295 | }); 296 | scrollController.jumpTo(0.0); 297 | } 298 | }); 299 | } 300 | 301 | Widget _buildHead() { 302 | String title = '请选择城市'; 303 | switch (pageStatus) { 304 | case Status.Province: 305 | break; 306 | case Status.City: 307 | title = targetProvince.name; 308 | break; 309 | case Status.Area: 310 | title = targetCity!.name; 311 | break; 312 | case Status.Over: 313 | break; 314 | } 315 | return Text(title); 316 | } 317 | 318 | @override 319 | Widget build(BuildContext context) { 320 | ThemeData theme = Theme.of(context); 321 | return WillPopScope( 322 | onWillPop: back, 323 | child: Scaffold( 324 | backgroundColor: theme.colorScheme.background, 325 | appBar: AppBar( 326 | title: _buildHead(), 327 | ), 328 | body: SafeArea( 329 | bottom: true, 330 | child: ListWidget( 331 | itemList: itemList, 332 | controller: scrollController, 333 | onSelect: _onItemSelect, 334 | selectedId: _getSelectedId(), 335 | ), 336 | ), 337 | ), 338 | ); 339 | } 340 | } 341 | 342 | class ListWidget extends StatelessWidget { 343 | final List itemList; 344 | final ScrollController controller; 345 | final String selectedId; 346 | final ValueChanged onSelect; 347 | 348 | ListWidget( 349 | {required this.itemList, 350 | required this.onSelect, 351 | required this.controller, 352 | required this.selectedId}); 353 | 354 | @override 355 | Widget build(BuildContext context) { 356 | ThemeData theme = Theme.of(context); 357 | return ListView.builder( 358 | controller: controller, 359 | itemBuilder: (BuildContext context, int index) { 360 | Point item = itemList[index]; 361 | return Container( 362 | decoration: BoxDecoration( 363 | // color: theme.backgroundColor, 364 | border: Border( 365 | bottom: BorderSide(color: theme.dividerColor, width: 1.0))), 366 | child: ListTileTheme( 367 | child: ListTile( 368 | title: Text(item.name), 369 | // 这里还是不敢放开. 容易引发兼容问题 370 | // title: Text(item.name, style: TextStyle(color: theme.textTheme.bodyText1!.color)), 371 | // item 标题 372 | dense: true, 373 | // tileColor:theme.textTheme.bodyText1!.color, 374 | // item 直观感受是整体大小 375 | trailing: selectedId == item.code 376 | ? Icon(Icons.check, color: theme.primaryColor) 377 | : null, 378 | contentPadding: EdgeInsets.fromLTRB(24.0, .0, 24.0, 3.0), 379 | // item 内容内边距 380 | enabled: true, 381 | onTap: () { 382 | onSelect(itemList[index]); 383 | }, 384 | // item onTap 点击事件 385 | onLongPress: () {}, 386 | // item onLongPress 长按事件 387 | selected: selectedId == item.code, // item 是否选中状态 388 | ), 389 | ), 390 | ); 391 | }, 392 | itemCount: itemList.length, 393 | ); 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /lib/src/mod/inherit_process.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 07/02/2019 5 | // Time: 21:14 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | import 'picker_popup_route.dart'; 12 | 13 | class InheritRouteWidget extends InheritedWidget { 14 | final CityPickerRoute router; 15 | 16 | InheritRouteWidget({Key? key, required this.router, required Widget child}) 17 | : super(key: key, child: child); 18 | 19 | static InheritRouteWidget? of(BuildContext context) { 20 | return context.dependOnInheritedWidgetOfExactType(); 21 | } 22 | 23 | @override 24 | bool updateShouldNotify(InheritRouteWidget oldWidget) { 25 | // TODO: implement updateShouldNotify 26 | return oldWidget.router != router; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/mod/picker_popup_route.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 07/02/2019 5 | // Time: 21:55 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: xxx 8 | // 9 | 10 | import 'package:flutter/material.dart'; 11 | import 'package:city_pickers/src/mod/inherit_process.dart'; 12 | 13 | class CityPickerRoute extends PopupRoute { 14 | final ThemeData? theme; 15 | final String? barrierLabel; 16 | final bool canBarrierDismiss; 17 | final Widget child; 18 | final double barrierOpacity; 19 | 20 | CityPickerRoute({ 21 | this.theme, 22 | required this.child, 23 | this.canBarrierDismiss = true, 24 | this.barrierOpacity = 0.5, 25 | this.barrierLabel, 26 | }); 27 | 28 | @override 29 | Duration get transitionDuration => Duration(milliseconds: 2000); 30 | 31 | @override 32 | @override 33 | Color get barrierColor => Color.fromRGBO(0, 0, 0, barrierOpacity); 34 | 35 | @override 36 | bool get barrierDismissible => canBarrierDismiss; 37 | 38 | AnimationController? _animationController; 39 | 40 | @override 41 | AnimationController createAnimationController() { 42 | assert(_animationController == null); 43 | _animationController = 44 | BottomSheet.createAnimationController(navigator!.overlay!); 45 | return _animationController!; 46 | } 47 | 48 | @override 49 | Widget buildPage(BuildContext context, Animation animation, 50 | Animation secondaryAnimation) { 51 | Widget bottomSheet = new MediaQuery.removePadding( 52 | removeTop: true, 53 | context: context, 54 | child: InheritRouteWidget(router: this, child: child)); 55 | if (theme != null) { 56 | bottomSheet = new Theme(data: theme!, child: bottomSheet); 57 | } 58 | return bottomSheet; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/show_types.dart: -------------------------------------------------------------------------------- 1 | // 显示类型 2 | enum Mods { 3 | Province, 4 | Area, 5 | City, 6 | Village, // 增加第4级(村/镇)选择 7 | } 8 | 9 | abstract class ShowTypeGeometry { 10 | const ShowTypeGeometry(); 11 | } 12 | 13 | class ShowType extends ShowTypeGeometry { 14 | final List typesList; 15 | 16 | const ShowType(this.typesList); 17 | 18 | static const ShowType p = ShowType([Mods.Province]); 19 | static const ShowType c = ShowType([Mods.City]); 20 | static const ShowType a = ShowType([Mods.Area]); 21 | static const ShowType v = ShowType([Mods.Village]); // 增加第4级(村/镇)选择 22 | static const ShowType pc = ShowType([Mods.Province, Mods.City]); 23 | static const ShowType pca = ShowType([Mods.Province, Mods.City, Mods.Area]); 24 | static const ShowType pcav = ShowType( 25 | [Mods.Province, Mods.City, Mods.Area, Mods.Village]); // 增加第4级(村/镇)选择 26 | static const ShowType ca = ShowType([Mods.Area, Mods.City]); 27 | static const ShowType cav = 28 | ShowType([Mods.Area, Mods.City, Mods.Village]); // 增加第4级(村/镇)选择 29 | 30 | ShowType operator +(ShowType others) { 31 | typesList.addAll(others.typesList); 32 | return ShowType(typesList); 33 | } 34 | 35 | bool contain(ShowType other) { 36 | for (Mods m in other.typesList) { 37 | if (!typesList.contains(m)) { 38 | return false; 39 | } 40 | } 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/util.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 28/01/2019 5 | // Time: 21:38 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: 组件内部使用的工具方法 8 | import 'package:flutter/material.dart'; 9 | import 'dart:async'; 10 | 11 | /// it's a cache class, aim to reduce calculations; 12 | class Cache { 13 | Map _cache = {}; 14 | 15 | // factory 16 | factory Cache() { 17 | return _getInstance(); 18 | } 19 | 20 | static Cache get instance => _getInstance(); 21 | static Cache? _instance; 22 | 23 | Cache._(); 24 | 25 | void set(String key, dynamic value) { 26 | _cache[key] = value; 27 | } 28 | 29 | bool has(String key) { 30 | return _cache.containsKey(key); 31 | } 32 | 33 | dynamic get(String key) { 34 | if (has(key)) { 35 | return _cache[key]; 36 | } 37 | return null; 38 | } 39 | 40 | dynamic remove(String key) { 41 | if (has(key)) { 42 | _cache.remove(key); 43 | } 44 | return null; 45 | } 46 | 47 | static Cache _getInstance() { 48 | if (_instance == null) { 49 | _instance = new Cache._(); 50 | } 51 | return _instance!; 52 | } 53 | } 54 | 55 | void setTimeout({required int milliseconds, callback = VoidCallback}) { 56 | new Timer(Duration(milliseconds: milliseconds), () { 57 | callback(); 58 | }); 59 | } 60 | 61 | typedef ItemWidgetBuilder = Widget Function( 62 | dynamic item, List list, int index); 63 | 64 | /// 自定义 城市选择器的头 65 | typedef AppBarBuilder = AppBar Function(String title); 66 | -------------------------------------------------------------------------------- /lib/src/utils/index.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 09/05/2019 5 | // Time: 15:42 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: 开放给city_pickers直接调用的工具方法 8 | // 9 | 10 | import 'location.dart'; 11 | import 'package:city_pickers/modal/result.dart'; 12 | 13 | class CityPickerUtil { 14 | Map citiesData; 15 | Map provincesData; 16 | 17 | CityPickerUtil({required this.citiesData, required this.provincesData}); 18 | 19 | Result getAreaResultByCode(String code) { 20 | Location location = 21 | new Location(citiesData: citiesData, provincesData: provincesData); 22 | return location.initLocation(code); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/utils/location.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 09/05/2019 5 | // Time: 15:47 6 | // email: sanfan.hx@alibaba-inc.com 7 | // target: 处理locationCode相关 8 | // 9 | import '../../modal/result.dart'; 10 | import '../../modal/point.dart'; 11 | import '../../modal/base_citys.dart'; 12 | 13 | class Location { 14 | Map citiesData; 15 | 16 | Map? provincesData; 17 | 18 | /// the target province user selected 19 | Point? provincePoint; 20 | 21 | /// the target city user selected 22 | Point? cityPoint; 23 | 24 | /// the target area user selected 25 | Point? areaPoint; 26 | 27 | // standby 28 | // Point village; 29 | 30 | // 没有一次性构建整个以国为根的树. 动态的构建以省为根的树, 效率高. 31 | // List provinces; 32 | 33 | Location({required this.citiesData, required this.provincesData}); 34 | 35 | Result initLocation(String _locationCode) { 36 | // print("initLocation >>>> $_locationCode"); 37 | 38 | CityTree cityTree = 39 | new CityTree(metaInfo: citiesData, provincesInfo: provincesData); 40 | 41 | String locationCode; 42 | Result locationInfo = new Result(); 43 | try { 44 | locationCode = _locationCode; 45 | } catch (e) { 46 | print(ArgumentError( 47 | "The Argument locationCode must be valid like: '100000' but get '$_locationCode' ")); 48 | return locationInfo; 49 | } 50 | provincePoint = cityTree.initTreeByCode(locationCode); 51 | 52 | if (provincePoint?.isNull ?? true) { 53 | return locationInfo; 54 | } 55 | locationInfo.provinceName = provincePoint!.name; 56 | locationInfo.provinceId = provincePoint!.code.toString(); 57 | 58 | provincePoint!.children.forEach((Point _city) { 59 | if (_city.code == locationCode) { 60 | cityPoint = _city; 61 | } 62 | 63 | /// 正常不应该在一个循环中, 如此操作, 但是考虑到地区码的唯一性, 可以在一次双层循环中完成操作. 避免第二层的循环查找 64 | _city.children.forEach((Point _area) { 65 | if (_area.code == locationCode) { 66 | cityPoint = _city; 67 | areaPoint = _area; 68 | } 69 | }); 70 | }); 71 | 72 | if (cityPoint != null && !cityPoint!.isNull) { 73 | locationInfo.cityName = cityPoint!.name; 74 | locationInfo.cityId = cityPoint!.code.toString(); 75 | } 76 | 77 | if (areaPoint != null && !areaPoint!.isNull) { 78 | locationInfo.areaName = areaPoint!.name; 79 | locationInfo.areaId = areaPoint!.code.toString(); 80 | } 81 | 82 | return locationInfo; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: city_pickers 2 | description: Flutter plugin for city picker, Popups widgets, call by function, support china. 3 | version: 1.3.0 4 | homepage: https://github.com/hanxu317317/city_pickers 5 | 6 | dev_dependencies: 7 | flutter_test: 8 | sdk: flutter 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | scrollable_positioned_list: ^0.3.5 15 | collection: ^1.16.0 16 | lpinyin: ^2.0.3 17 | # For information on the generic Dart part of this file, see the 18 | # following page: https://www.dartlang.org/tools/pub/pubspec 19 | 20 | # The following section is specific to Flutter. 21 | 22 | environment: 23 | sdk: '>=2.12.0 <4.0.0' 24 | flutter: ">=2.0.0" 25 | # To add assets to your plugin package, add an assets section, like this: 26 | # assets: 27 | # - images/a_dot_burr.jpeg 28 | # - images/a_dot_ham.jpeg 29 | # 30 | # For details regarding assets in packages, see 31 | # https://flutter.io/assets-and-images/#from-packages 32 | # 33 | # An image asset can refer to one or more resolution-specific "variants", see 34 | # https://flutter.io/assets-and-images/#resolution-aware. 35 | 36 | # To add custom fonts to your plugin package, add a fonts section here, 37 | # in this "flutter" section. Each entry in this list should have a 38 | # "family" key with the font family name, and a "fonts" key with a 39 | # list giving the asset and other descriptors for the font. For 40 | # example: 41 | # fonts: 42 | # - family: Schyler 43 | # fonts: 44 | # - asset: fonts/Schyler-Regular.ttf 45 | # - asset: fonts/Schyler-Italic.ttf 46 | # style: italic 47 | # - family: Trajan Pro 48 | # fonts: 49 | # - asset: fonts/TrajanPro.ttf 50 | # - asset: fonts/TrajanPro_Bold.ttf 51 | # weight: 700 52 | # 53 | # For details regarding fonts in packages, see 54 | # https://flutter.io/custom-fonts/#from-packages 55 | -------------------------------------------------------------------------------- /test/unit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:city_pickers/city_pickers.dart'; 2 | import 'package:city_pickers/meta/province.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | /// Created by ipcjs on 2022/11/8. 6 | 7 | void main() { 8 | test('Point.pinyin', () { 9 | final zhongGuo = Point(children: [], name: '中国').pinyin; 10 | expect(zhongGuo?.short, 'zg'); 11 | expect(zhongGuo?.full, 'zhongguo'); 12 | 13 | final china = Point(children: [], name: 'china').pinyin; 14 | expect(china, null); 15 | }); 16 | group('CitiesUtils', () { 17 | test('getAllCitiesByMeta', () { 18 | final p1 = CitiesUtils.getAllCitiesByMeta(provincesData, citiesData) 19 | .map((e) => e.toString()); 20 | final p2 = CitiesUtils.getAllCitiesByMeta(provincesData, citiesData) 21 | .map((e) => e.toString()); 22 | 23 | expect(p1, p2); 24 | }); 25 | }); 26 | } 27 | --------------------------------------------------------------------------------