diff --git a/.gitignore b/.gitignore index 28e4133..387d192 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ build/ **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral/ **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ @@ -73,7 +74,7 @@ build/ !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 -/example/ios/Podfile +!/example/ios/Podfile !/example/ios/Runner.xcodeproj/ /web/ /web/.idea/ @@ -82,3 +83,9 @@ build/ /web/.idea/vcs.xml /web/.idea/web.iml /web/.idea/ +.agents +.claude +.vscode +AGENTS.md +CONTEXT.md +skills-lock.json diff --git a/README-EN.md b/README-EN.md index fc66138..e0aad99 100644 --- a/README-EN.md +++ b/README-EN.md @@ -253,6 +253,8 @@ void _showDemo(){ | pickerStyle | See [Styles](#styles) | DefaultPickerStyle()| | onChanged | Callback when picker changes, returns (List data, List position) | null| | onConfirm | Callback when picker confirms, returns (List data, List position)| null| +| editorBuilder | Custom editor below the wheels; call `updateSelection` for two-way synchronization | null| +| editorHeight | Custom editor height | 56.0| | onCancel | Callback when picker cancels, returns (bool isCancel) whether closed by cancel button | null|
diff --git a/README.md b/README.md index 30fc0ca..4c756a0 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,8 @@ Widget _demo() { | onChanged | 选择器发生变动 return (List data, List position) | null| | onConfirm | 选择器提交 return (List data, List position)| null| | onCancel | 选择器取消 return (bool isCancel) 是否通过点击取消按钮关闭 | null| +| editorBuilder | 选择器下方的自定义编辑区,调用 `updateSelection` 可与各列双向联动 | null| +| editorHeight | 自定义编辑区高度 | 56.0|
diff --git a/analysis_options.yaml b/analysis_options.yaml index 07d234d..8e5ba8c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,63 +1,11 @@ include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options - -linter: - rules: - # 禁用一些过于严格的规则,避免对现有代码造成太多警告 - # prefer_single_quotes: false # 地址数据文件有大量双引号 - # prefer_const_constructors: false # 需要大量修改现有代码 - - # 保留重要的规则 - always_declare_return_types: true - annotate_overrides: true - avoid_empty_else: true - avoid_init_to_null: true - avoid_null_checks_in_equality_operators: true - avoid_relative_lib_imports: true - avoid_return_types_on_setters: true - await_only_futures: true - camel_case_types: true - cancel_subscriptions: true - close_sinks: true - empty_catches: true - empty_constructor_bodies: true - library_names: true - library_prefixes: true - non_constant_identifier_names: true - prefer_collection_literals: true - prefer_conditional_assignment: true - prefer_contains: true - prefer_final_fields: true - prefer_for_elements_to_map_fromIterable: true - prefer_if_null_operators: true - prefer_is_empty: true - prefer_is_not_empty: true - prefer_iterable_whereType: true - prefer_spread_collections: true - recursive_getters: true - type_init_formals: true - unnecessary_brace_in_string_interps: true - unnecessary_getters_setters: true - unnecessary_new: true - unnecessary_null_aware_assignments: true - unnecessary_null_in_if_null_operators: true - unnecessary_overrides: true - unnecessary_this: true - use_function_type_syntax_for_parameters: true - use_rethrow_when_possible: true - valid_regexps: true - analyzer: exclude: - - "**/*.g.dart" - - "**/*.freezed.dart" - # 地址数据文件包含大量自动生成的数据,忽略其 lint 警告 - - "lib/address_picker/locations_data.dart" + - lib/address_picker/locations_data.dart + - lib/l10n/generated/** errors: - invalid_annotation_target: ignore - # 将一些 info 级别的警告降级,避免干扰 - prefer_single_quotes: ignore + # Keep the published package compatible while legacy call sites migrate. prefer_const_constructors: ignore + prefer_single_quotes: ignore unnecessary_parenthesis: ignore diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 0000000..f7c8ebd --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,42 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Layout + +This repository uses the single-context domain documentation layout: + +- `CONTEXT.md` at the repository root +- Architecture decision records under `docs/adr/` + +These files are created lazily when domain terminology or architectural decisions need to be recorded. + +## Before exploring, read these + +- **`CONTEXT.md`** at the repo root. +- **`docs/adr/`** — read ADRs that touch the area you're about to work in. + +If either location doesn't exist, **proceed silently**. Don't flag its absence or suggest creating it upfront. The `/domain-modeling` skill creates these files lazily when terms or decisions actually get resolved. + +## File structure + +```text +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +├── lib/ +└── test/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept—in an issue title, refactor proposal, hypothesis, or test name—use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use, or there's a real gap to note for `/domain-modeling`. + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders)—but worth reopening because…_ diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 0000000..82cfbf5 --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,45 @@ +# Issue tracker: GitHub + +Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations. + +## Conventions + +- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. +- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. +- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. +- **Comment on an issue**: `gh issue comment --body "..."` +- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` +- **Close**: `gh issue close --comment "..."` + +Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. + +## Pull requests as a triage surface + +**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_ + +When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents: + +- **Read a PR**: `gh pr view --comments` and `gh pr diff ` for the diff. +- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`). +- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`. + +GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`. + +## When a skill says "publish to the issue tracker" + +Create a GitHub issue. + +## When a skill says "fetch the relevant ticket" + +Run `gh issue view --comments`. + +## Wayfinding operations + +Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets. + +- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`. +- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #` at the top of the child body. Labels: `wayfinder:` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev. +- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos///issues//dependencies/blocked_by -F issue_id=`, where `` is the blocker's numeric **database id** (`gh api repos///issues/ --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #, #` line at the top of the child body. A ticket is unblocked when every blocker is closed. +- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins. +- **Claim**: `gh issue edit --add-assignee @me` — the session's first write. +- **Resolve**: `gh issue comment --body ""`, then `gh issue close `, then append a context pointer (gist + link) to the map's Decisions-so-far. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md new file mode 100644 index 0000000..b716855 --- /dev/null +++ b/docs/agents/triage-labels.md @@ -0,0 +1,15 @@ +# Triage Labels + +The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. + +| Label in mattpocock/skills | Label in our tracker | Meaning | +| -------------------------- | -------------------- | ---------------------------------------- | +| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | +| `needs-info` | `needs-info` | Waiting on reporter for more information | +| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | +| `ready-for-human` | `ready-for-human` | Requires human implementation | +| `wontfix` | `wontfix` | Will not be actioned | + +When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. + +Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/docs/superpowers/plans/2026-03-17-i18n-implementation.md b/docs/superpowers/plans/2026-03-17-i18n-implementation.md new file mode 100644 index 0000000..4b2f5a5 --- /dev/null +++ b/docs/superpowers/plans/2026-03-17-i18n-implementation.md @@ -0,0 +1,1201 @@ +# flutter_pickers 国际化实现计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 flutter_pickers 库添加中英文国际化支持,实现系统语言跟随和API手动控制。 + +**Architecture:** 使用 Flutter 官方 l10n 方案,通过 arb 文件管理翻译,生成 AppLocalizations 类。PickersLocale 类管理语言设置,各组件通过 BuildContext 获取本地化文本。 + +**Tech Stack:** Flutter, flutter_localizations, intl + +--- + +## Chunk 1: 基础设施搭建 + +### Task 1: 更新依赖配置 + +**Files:** +- Modify: `pubspec.yaml` + +- [ ] **Step 1: 添加国际化依赖** + +```yaml +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.18.0 +``` + +- [ ] **Step 2: 运行 flutter pub get** + +Run: `flutter pub get` +Expected: Dependencies resolved successfully + +- [ ] **Step 3: 提交** + +```bash +git add pubspec.yaml pubspec.lock +git commit -m "chore: 添加国际化依赖" +``` + +### Task 2: 创建 l10n 配置 + +**Files:** +- Create: `lib/l10n/l10n.yaml` + +- [ ] **Step 1: 创建 l10n.yaml 文件** + +```yaml +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +output-dir: lib/l10n/generated +synthetic-package: false +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/l10n/l10n.yaml +git commit -m "chore: 添加l10n配置文件" +``` + +### Task 3: 创建英文 ARB 文件(模板文件) + +**Files:** +- Create: `lib/l10n/app_en.arb` + +- [ ] **Step 1: 创建英文翻译文件** + +```json +{ + "@@locale": "en", + + "cancel": "Cancel", + "@cancel": { + "description": "Cancel button text" + }, + "confirm": "Confirm", + "@confirm": { + "description": "Confirm button text" + }, + "year": "Year", + "@year": { + "description": "Year unit" + }, + "month": "Month", + "@month": { + "description": "Month unit" + }, + "day": "Day", + "@day": { + "description": "Day unit" + }, + "hour": "Hour", + "@hour": { + "description": "Hour unit" + }, + "minute": "Minute", + "@minute": { + "description": "Minute unit" + }, + "second": "Second", + "@second": { + "description": "Second unit" + }, + "all": "All", + "@all": { + "description": "All option in address picker" + }, + + "sexAny": "Any", + "@sexAny": { + "description": "Any gender option" + }, + "sexMale": "Male", + "@sexMale": { + "description": "Male gender option" + }, + "sexFemale": "Female", + "@sexFemale": { + "description": "Female gender option" + }, + + "educationBelowHighSchool": "Below High School", + "@educationBelowHighSchool": { + "description": "Education level below high school" + }, + "educationHighSchool": "High School", + "@educationHighSchool": { + "description": "High school education" + }, + "educationAssociate": "Associate", + "@educationAssociate": { + "description": "Associate degree" + }, + "educationBachelor": "Bachelor", + "@educationBachelor": { + "description": "Bachelor degree" + }, + "educationMaster": "Master", + "@educationMaster": { + "description": "Master degree" + }, + "educationPhd": "PhD", + "@educationPhd": { + "description": "Doctorate degree" + }, + "educationPostdoc": "Postdoc", + "@educationPostdoc": { + "description": "Postdoctoral" + }, + "educationOther": "Other", + "@educationOther": { + "description": "Other education" + }, + + "subjectChinese": "Chinese", + "subjectMath": "Math", + "subjectEnglish": "English", + "subjectPhysics": "Physics", + "subjectChemistry": "Chemistry", + "subjectBiology": "Biology", + "subjectPolitics": "Politics", + "subjectGeography": "Geography", + "subjectHistory": "History", + + "constellationAquarius": "Aquarius", + "constellationPisces": "Pisces", + "constellationAries": "Aries", + "constellationTaurus": "Taurus", + "constellationGemini": "Gemini", + "constellationCancer": "Cancer", + "constellationLeo": "Leo", + "constellationVirgo": "Virgo", + "constellationLibra": "Libra", + "constellationScorpio": "Scorpio", + "constellationSagittarius": "Sagittarius", + "constellationCapricorn": "Capricorn", + + "zodiacRat": "Rat", + "zodiacOx": "Ox", + "zodiacTiger": "Tiger", + "zodiacRabbit": "Rabbit", + "zodiacDragon": "Dragon", + "zodiacSnake": "Snake", + "zodiacHorse": "Horse", + "zodiacGoat": "Goat", + "zodiacMonkey": "Monkey", + "zodiacRooster": "Rooster", + "zodiacDog": "Dog", + "zodiacPig": "Pig", + + "ethnicityHan": "Han", + "ethnicityMongol": "Mongol", + "ethnicityHui": "Hui", + "ethnicityTibetan": "Tibetan", + "ethnicityUygur": "Uygur", + "ethnicityMiao": "Miao", + "ethnicityYi": "Yi", + "ethnicityZhuang": "Zhuang", + "ethnicityBouyei": "Bouyei", + "ethnicityKorean": "Korean", + "ethnicityManchu": "Manchu", + "ethnicityDong": "Dong", + "ethnicityYao": "Yao", + "ethnicityBai": "Bai", + "ethnicityTujia": "Tujia", + "ethnicityHani": "Hani", + "ethnicityKazak": "Kazak", + "ethnicityDai": "Dai", + "ethnicityLi": "Li", + "ethnicityLisu": "Lisu", + "ethnicityVa": "Va", + "ethnicityShe": "She", + "ethnicityGaoshan": "Gaoshan", + "ethnicityLahu": "Lahu", + "ethnicityShui": "Shui", + "ethnicityDongxiang": "Dongxiang", + "ethnicityNaxi": "Naxi", + "ethnicityJingpo": "Jingpo", + "ethnicityKirgiz": "Kirgiz", + "ethnicityTu": "Tu", + "ethnicityDaur": "Daur", + "ethnicityMulao": "Mulao", + "ethnicityQiang": "Qiang", + "ethnicityBlang": "Blang", + "ethnicitySalar": "Salar", + "ethnicityMaonan": "Maonan", + "ethnicityGelao": "Gelao", + "ethnicityXibe": "Xibe", + "ethnicityAchang": "Achang", + "ethnicityPumi": "Pumi", + "ethnicityTajik": "Tajik", + "ethnicityNu": "Nu", + "ethnicityUzbek": "Uzbek", + "ethnicityRussian": "Russian", + "ethnicityEwenki": "Ewenki", + "ethnicityDeang": "Deang", + "ethnicityBonan": "Bonan", + "ethnicityYugur": "Yugur", + "ethnicityGin": "Gin", + "ethnicityTatar": "Tatar", + "ethnicityDerung": "Derung", + "ethnicityOroqen": "Oroqen", + "ethnicityHezhen": "Hezhen", + "ethnicityMonba": "Monba", + "ethnicityLhoba": "Lhoba", + "ethnicityJino": "Jino", + "ethnicityOther": "Other", + "ethnicityForeignBornChinese": "Foreign-born Chinese" +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/l10n/app_en.arb +git commit -m "feat: 添加英文翻译文件" +``` + +### Task 4: 创建中文 ARB 文件 + +**Files:** +- Create: `lib/l10n/app_zh.arb` + +- [ ] **Step 1: 创建中文翻译文件** + +```json +{ + "@@locale": "zh", + + "cancel": "取消", + "confirm": "确定", + "year": "年", + "month": "月", + "day": "日", + "hour": "时", + "minute": "分", + "second": "秒", + "all": "全部", + + "sexAny": "不限", + "sexMale": "男", + "sexFemale": "女", + + "educationBelowHighSchool": "高中以下", + "educationHighSchool": "高中", + "educationAssociate": "大专", + "educationBachelor": "本科", + "educationMaster": "硕士", + "educationPhd": "博士", + "educationPostdoc": "博士后", + "educationOther": "其它", + + "subjectChinese": "语文", + "subjectMath": "数学", + "subjectEnglish": "英语", + "subjectPhysics": "物理", + "subjectChemistry": "化学", + "subjectBiology": "生物", + "subjectPolitics": "政治", + "subjectGeography": "地理", + "subjectHistory": "历史", + + "constellationAquarius": "水瓶座", + "constellationPisces": "双鱼座", + "constellationAries": "白羊座", + "constellationTaurus": "金牛座", + "constellationGemini": "双子座", + "constellationCancer": "巨蟹座", + "constellationLeo": "狮子座", + "constellationVirgo": "处女座", + "constellationLibra": "天秤座", + "constellationScorpio": "天蝎座", + "constellationSagittarius": "射手座", + "constellationCapricorn": "摩羯座", + + "zodiacRat": "鼠", + "zodiacOx": "牛", + "zodiacTiger": "虎", + "zodiacRabbit": "兔", + "zodiacDragon": "龙", + "zodiacSnake": "蛇", + "zodiacHorse": "马", + "zodiacGoat": "羊", + "zodiacMonkey": "猴", + "zodiacRooster": "鸡", + "zodiacDog": "狗", + "zodiacPig": "猪", + + "ethnicityHan": "汉族", + "ethnicityMongol": "蒙古族", + "ethnicityHui": "回族", + "ethnicityTibetan": "藏族", + "ethnicityUygur": "维吾尔族", + "ethnicityMiao": "苗族", + "ethnicityYi": "彝族", + "ethnicityZhuang": "壮族", + "ethnicityBouyei": "布依族", + "ethnicityKorean": "朝鲜族", + "ethnicityManchu": "满族", + "ethnicityDong": "侗族", + "ethnicityYao": "瑶族", + "ethnicityBai": "白族", + "ethnicityTujia": "土家族", + "ethnicityHani": "哈尼族", + "ethnicityKazak": "哈萨克族", + "ethnicityDai": "傣族", + "ethnicityLi": "黎族", + "ethnicityLisu": "傈僳族", + "ethnicityVa": "佤族", + "ethnicityShe": "畲族", + "ethnicityGaoshan": "高山族", + "ethnicityLahu": "拉祜族", + "ethnicityShui": "水族", + "ethnicityDongxiang": "东乡族", + "ethnicityNaxi": "纳西族", + "ethnicityJingpo": "景颇族", + "ethnicityKirgiz": "柯尔克孜族", + "ethnicityTu": "土族", + "ethnicityDaur": "达斡尔族", + "ethnicityMulao": "仫佬族", + "ethnicityQiang": "羌族", + "ethnicityBlang": "布朗族", + "ethnicitySalar": "撒拉族", + "ethnicityMaonan": "毛难族", + "ethnicityGelao": "仡佬族", + "ethnicityXibe": "锡伯族", + "ethnicityAchang": "阿昌族", + "ethnicityPumi": "普米族", + "ethnicityTajik": "塔吉克族", + "ethnicityNu": "怒族", + "ethnicityUzbek": "乌孜别克族", + "ethnicityRussian": "俄罗斯族", + "ethnicityEwenki": "鄂温克族", + "ethnicityDeang": "崩龙族", + "ethnicityBonan": "保安族", + "ethnicityYugur": "裕固族", + "ethnicityGin": "京族", + "ethnicityTatar": "塔塔尔族", + "ethnicityDerung": "独龙族", + "ethnicityOroqen": "鄂伦春族", + "ethnicityHezhen": "赫哲族", + "ethnicityMonba": "门巴族", + "ethnicityLhoba": "珞巴族", + "ethnicityJino": "基诺族", + "ethnicityOther": "其他", + "ethnicityForeignBornChinese": "外国血统中国人士" +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/l10n/app_zh.arb +git commit -m "feat: 添加中文翻译文件" +``` + +### Task 5: 生成国际化代码 + +**Files:** +- Generate: `lib/l10n/generated/app_localizations.dart` +- Generate: `lib/l10n/generated/app_localizations_en.dart` +- Generate: `lib/l10n/generated/app_localizations_zh.dart` + +- [ ] **Step 1: 创建 generated 目录** + +```bash +mkdir -p lib/l10n/generated +``` + +- [ ] **Step 2: 运行代码生成** + +Run: `flutter gen-l10n` +Expected: Generated files in lib/l10n/generated/ + +- [ ] **Step 3: 验证生成文件存在** + +Run: `ls lib/l10n/generated/` +Expected: app_localizations.dart, app_localizations_en.dart, app_localizations_zh.dart + +- [ ] **Step 4: 提交** + +```bash +git add lib/l10n/generated/ +git commit -m "feat: 生成国际化代码" +``` + +--- + +## Chunk 2: PickersLocale 类和 getData 方法 + +### Task 6: 创建 PickersLocale 类 + +**Files:** +- Create: `lib/pickers_locale.dart` + +- [ ] **Step 1: 创建 PickersLocale 类** + +```dart +import 'dart:ui'; + +class PickersLocale { + static Locale? _overrideLocale; + + PickersLocale._(); + + static void setLocale(Locale? locale) { + if (locale != null && !_isSupported(locale)) { + return; + } + _overrideLocale = locale; + } + + static Locale? get currentLocale => _overrideLocale; + + static List get supportedLocales => [ + const Locale('zh'), + const Locale('en'), + ]; + + static bool _isSupported(Locale locale) { + return locale.languageCode == 'zh' || locale.languageCode == 'en'; + } +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/pickers_locale.dart +git commit -m "feat: 添加 PickersLocale 语言管理类" +``` + +### Task 7: 在 pickers.dart 中添加 getData 方法 + +**Files:** +- Modify: `lib/pickers.dart` + +- [ ] **Step 1: 验证 init_data.dart 中的类型** + +确认 `pickerData` Map 和 `PickerDataType` 枚举存在且格式正确。 + +Run: `grep -n "pickerData\|PickerDataType" lib/more_pickers/init_data.dart` +Expected: 找到 `PickerDataType` 枚举定义和 `pickerData` Map 定义 + +- [ ] **Step 2: 添加导入和 getData 方法** + +在文件顶部添加导入: + +```dart +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; +import 'package:flutter_pickers/more_pickers/init_data.dart'; +``` + +在 `Pickers` 类中添加方法: + +```dart +static List getData(PickerDataType type, BuildContext context) { + final l10n = AppLocalizations.of(context); + if (l10n == null) { + return _getDefaultData(type); + } + return _getLocalizedData(type, l10n); +} + +static List _getDefaultData(PickerDataType type) { + return pickerData[type] ?? []; +} + +static List _getLocalizedData(PickerDataType type, AppLocalizations l10n) { + switch (type) { + case PickerDataType.sex: + return [l10n.sexAny, l10n.sexMale, l10n.sexFemale]; + case PickerDataType.education: + return [ + l10n.educationBelowHighSchool, + l10n.educationHighSchool, + l10n.educationAssociate, + l10n.educationBachelor, + l10n.educationMaster, + l10n.educationPhd, + l10n.educationPostdoc, + l10n.educationOther, + ]; + case PickerDataType.subject: + return [ + l10n.subjectChinese, + l10n.subjectMath, + l10n.subjectEnglish, + l10n.subjectPhysics, + l10n.subjectChemistry, + l10n.subjectBiology, + l10n.subjectPolitics, + l10n.subjectGeography, + l10n.subjectHistory, + ]; + case PickerDataType.constellation: + return [ + l10n.constellationAquarius, + l10n.constellationPisces, + l10n.constellationAries, + l10n.constellationTaurus, + l10n.constellationGemini, + l10n.constellationCancer, + l10n.constellationLeo, + l10n.constellationVirgo, + l10n.constellationLibra, + l10n.constellationScorpio, + l10n.constellationSagittarius, + l10n.constellationCapricorn, + ]; + case PickerDataType.zodiac: + return [ + l10n.zodiacRat, + l10n.zodiacOx, + l10n.zodiacTiger, + l10n.zodiacRabbit, + l10n.zodiacDragon, + l10n.zodiacSnake, + l10n.zodiacHorse, + l10n.zodiacGoat, + l10n.zodiacMonkey, + l10n.zodiacRooster, + l10n.zodiacDog, + l10n.zodiacPig, + ]; + case PickerDataType.ethnicity: + return [ + l10n.ethnicityHan, + l10n.ethnicityMongol, + l10n.ethnicityHui, + l10n.ethnicityTibetan, + l10n.ethnicityUygur, + l10n.ethnicityMiao, + l10n.ethnicityYi, + l10n.ethnicityZhuang, + l10n.ethnicityBouyei, + l10n.ethnicityKorean, + l10n.ethnicityManchu, + l10n.ethnicityDong, + l10n.ethnicityYao, + l10n.ethnicityBai, + l10n.ethnicityTujia, + l10n.ethnicityHani, + l10n.ethnicityKazak, + l10n.ethnicityDai, + l10n.ethnicityLi, + l10n.ethnicityLisu, + l10n.ethnicityVa, + l10n.ethnicityShe, + l10n.ethnicityGaoshan, + l10n.ethnicityLahu, + l10n.ethnicityShui, + l10n.ethnicityDongxiang, + l10n.ethnicityNaxi, + l10n.ethnicityJingpo, + l10n.ethnicityKirgiz, + l10n.ethnicityTu, + l10n.ethnicityDaur, + l10n.ethnicityMulao, + l10n.ethnicityQiang, + l10n.ethnicityBlang, + l10n.ethnicitySalar, + l10n.ethnicityMaonan, + l10n.ethnicityGelao, + l10n.ethnicityXibe, + l10n.ethnicityAchang, + l10n.ethnicityPumi, + l10n.ethnicityTajik, + l10n.ethnicityNu, + l10n.ethnicityUzbek, + l10n.ethnicityRussian, + l10n.ethnicityEwenki, + l10n.ethnicityDeang, + l10n.ethnicityBonan, + l10n.ethnicityYugur, + l10n.ethnicityGin, + l10n.ethnicityTatar, + l10n.ethnicityDerung, + l10n.ethnicityOroqen, + l10n.ethnicityHezhen, + l10n.ethnicityMonba, + l10n.ethnicityLhoba, + l10n.ethnicityJino, + l10n.ethnicityOther, + l10n.ethnicityForeignBornChinese, + ]; + } +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/pickers.dart +git commit -m "feat: 添加 getData 方法支持国际化内置数据" +``` + +--- + +## Chunk 3: Route 文件国际化改造 + +### Task 8: 改造 Suffix 类支持国际化 + +**Files:** +- Modify: `lib/time_picker/model/suffix.dart` + +- [ ] **Step 1: 添加 fromContext 工厂构造函数** + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; +import 'package:flutter_pickers/time_picker/model/date_type.dart'; + +class Suffix { + late String years; + late String month; + late String days; + late String hours; + late String minutes; + late String seconds; + + Suffix.normal() { + years = '年'; + month = '月'; + days = '日'; + hours = '时'; + minutes = '分'; + seconds = '秒'; + } + + Suffix({ + this.years = '', + this.month = '', + this.days = '', + this.hours = '', + this.minutes = '', + this.seconds = '', + }); + + factory Suffix.fromContext(BuildContext context) { + final l10n = AppLocalizations.of(context); + if (l10n == null) { + return Suffix.normal(); + } + return Suffix( + years: l10n.year, + month: l10n.month, + days: l10n.day, + hours: l10n.hour, + minutes: l10n.minute, + seconds: l10n.second, + ); + } + + String getSingle(DateType dateType) { + switch (dateType) { + case DateType.year: + return years; + case DateType.month: + return month; + case DateType.day: + return days; + case DateType.hour: + return hours; + case DateType.minute: + return minutes; + case DateType.second: + return seconds; + } + } +} +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/time_picker/model/suffix.dart +git commit -m "feat: Suffix 类支持国际化" +``` + +### Task 9: 改造 date_picker_route.dart + +**Files:** +- Modify: `lib/time_picker/route/date_picker_route.dart` + +- [ ] **Step 1: 在 _PickerState 中添加 late Suffix _suffix 属性** + +在 `_PickerState` 类中(约 line 127),找到其他 late 属性定义位置,添加: + +```dart +late Suffix _suffix; +``` + +- [ ] **Step 2: 在 initState 中初始化 _suffix** + +在 `initState` 方法中添加: + +```dart +_suffix = widget.route.suffix ?? Suffix.fromContext(context); +``` + +- [ ] **Step 3: 修改 line 805 使用 _suffix** + +将: +```dart +'${_dateTimeData.getListByName(dateType)[index]}${widget.route.suffix?.getSingle(dateType)}' +``` + +改为: +```dart +'${_dateTimeData.getListByName(dateType)[index]}${_suffix.getSingle(dateType)}' +``` + +- [ ] **Step 4: 提交** + +```bash +git add lib/time_picker/route/date_picker_route.dart +git commit -m "feat: 时间选择器支持国际化" +``` + +### Task 10: 改造 address_picker_route.dart + +**Files:** +- Modify: `lib/address_picker/route/address_picker_route.dart` + +**说明:** 根据设计要求,城市名称始终显示中文,不做国际化。只有"全部"选项需要国际化。 + +- [ ] **Step 1: 添加导入** + +```dart +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; +``` + +- [ ] **Step 2: 修改"全部"文本获取** + +找到使用"全部"文本的地方,改为: + +```dart +final l10n = AppLocalizations.of(context); +final allText = l10n?.all ?? '全部'; +``` + +- [ ] **Step 3: 提交** + +```bash +git add lib/address_picker/route/address_picker_route.dart +git commit -m "feat: 地址选择器支持国际化" +``` + +--- + +## Chunk 4: 样式国际化 + +### Task 11: 改造 picker_style.dart + +**Files:** +- Modify: `lib/style/picker_style.dart` + +- [ ] **Step 1: 添加导入** + +```dart +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; +``` + +- [ ] **Step 2: 修改 getCommitButton 方法** + +```dart +Widget getCommitButton() { + if (_commitButton != null) return _commitButton!; + + final l10n = context != null ? AppLocalizations.of(context!) : null; + final text = l10n?.confirm ?? '确定'; + + final primaryColor = context != null + ? Theme.of(context!).primaryColor + : Colors.blue; + + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(left: 12, right: 22), + child: Text( + text, + style: TextStyle( + color: primaryColor, + fontSize: 16.0, + ), + ), + ); +} +``` + +- [ ] **Step 3: 修改 getCancelButton 方法** + +```dart +Widget getCancelButton() { + if (_cancelButton != null) return _cancelButton!; + + final l10n = context != null ? AppLocalizations.of(context!) : null; + final text = l10n?.cancel ?? '取消'; + + final unselectedColor = context != null + ? Theme.of(context!).unselectedWidgetColor + : Colors.grey; + + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(left: 22, right: 12), + child: Text( + text, + style: TextStyle( + color: unselectedColor, + fontSize: 16.0, + ), + ), + ); +} +``` + +- [ ] **Step 4: 提交** + +```bash +git add lib/style/picker_style.dart +git commit -m "feat: 选择器样式支持国际化" +``` + +### Task 12: 改造 default_style.dart + +**Files:** +- Modify: `lib/style/default_style.dart` + +**说明:** `default_style.dart` 中的样式类(DefaultPickerStyle.dark、RaisedPickerStyle 等)直接设置了 `commitButton` 和 `cancelButton`,绕过了 `getCommitButton()` 方法。有两种处理方式: + +1. **推荐方案**:保持现状,因为这些样式是预设样式,用户如果需要国际化,可以不使用这些预设样式,而是自定义 `PickerStyle` 或使用默认样式。 + +2. **替代方案**:移除所有预设样式中直接设置 `commitButton`/`cancelButton` 的代码,让它们通过 `getCommitButton()`/`getCancelButton()` 获取国际化文本。 + +采用**推荐方案**:保持现状,在文档中说明预设样式不支持国际化。 + +- [ ] **Step 1: 添加注释说明** + +在 `default_style.dart` 文件顶部添加: + +```dart +// 注意:预设样式(如 DefaultPickerStyle.dark、RaisedPickerStyle 等) +// 中的按钮文本为固定中文,不支持国际化。 +// 如需国际化,请使用默认样式或自定义 PickerStyle。 +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/style/default_style.dart +git commit -m "docs: 添加预设样式国际化说明" +``` + +--- + +## Chunk 5: 导出和文档更新 + +### Task 13: 更新 pickers.dart 导出 + +**Files:** +- Modify: `lib/pickers.dart` + +- [ ] **Step 1: 添加 PickersLocale 导出** + +```dart +export 'pickers_locale.dart'; +export 'l10n/generated/app_localizations.dart'; +``` + +- [ ] **Step 2: 提交** + +```bash +git add lib/pickers.dart +git commit -m "feat: 导出 PickersLocale 和 AppLocalizations" +``` + +### Task 14: 更新 README.md + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: 在 README.md 中添加国际化说明** + +在"用法"部分后添加: + +```markdown +## 国际化 + +本库支持中英文国际化。 + +### 使用方式 + +#### 1. 在 MaterialApp 中配置 + +```dart +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +MaterialApp( + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, +); +``` + +#### 2. API 控制语言(可选) + +```dart +// 设置英文 +PickersLocale.setLocale(Locale('en')); + +// 设置中文 +PickersLocale.setLocale(Locale('zh')); + +// 恢复跟随系统 +PickersLocale.setLocale(null); +``` + +#### 3. 获取国际化内置数据 + +```dart +// 旧方式(返回中文) +PickerDataType.sex + +// 新方式(支持国际化) +final sexData = Pickers.getData(PickerDataType.sex, context); +``` + +**注意:** 预设样式(如 `DefaultPickerStyle.dark()`、`RaisedPickerStyle()` 等)中的按钮文本为固定中文,不支持国际化。如需国际化,请使用默认样式或自定义 `PickerStyle`。 +``` + +- [ ] **Step 2: 提交** + +```bash +git add README.md +git commit -m "docs: 更新中文国际化文档" +``` + +### Task 15: 更新 README-EN.md + +**Files:** +- Modify: `README-EN.md` + +- [ ] **Step 1: 添加国际化说明(英文版)** + +```markdown +## Internationalization + +This library supports Chinese and English internationalization. + +### Usage + +#### 1. Configure in MaterialApp + +```dart +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +MaterialApp( + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, +); +``` + +#### 2. API Control (Optional) + +```dart +// Set English +PickersLocale.setLocale(Locale('en')); + +// Set Chinese +PickersLocale.setLocale(Locale('zh')); + +// Reset to follow system +PickersLocale.setLocale(null); +``` + +#### 3. Get Localized Built-in Data + +```dart +// Old way (returns Chinese) +PickerDataType.sex + +// New way (supports i18n) +final sexData = Pickers.getData(PickerDataType.sex, context); +``` + +**Note:** Preset styles (e.g., `DefaultPickerStyle.dark()`, `RaisedPickerStyle()`) have fixed Chinese button text and do not support i18n. For i18n support, use the default style or customize `PickerStyle`. +``` + +- [ ] **Step 2: 提交** + +```bash +git add README-EN.md +git commit -m "docs: update i18n documentation in English" +``` + +### Task 16: 更新 CHANGELOG + +**Files:** +- Modify: `CHANGELOG.md` + +- [ ] **Step 1: 添加版本更新记录** + +```markdown +## 2.3.0 + +- feat: 添加国际化支持(中英文) +- feat: 新增 PickersLocale 类管理语言设置 +- feat: 新增 Pickers.getData() 方法获取国际化内置数据 +- feat: 时间选择器、地址选择器、样式支持国际化 +- feat: Suffix 类新增 fromContext 工厂构造函数支持国际化 +``` + +- [ ] **Step 2: 提交** + +```bash +git add CHANGELOG.md +git commit -m "docs: 更新 CHANGELOG" +``` + +--- + +## Chunk 6: 测试 + +**说明:** 本计划包含单元测试覆盖核心功能。Widget 测试和集成测试可作为后续任务,因为它们需要更完整的 Flutter 测试环境设置。 + +### Task 17: 编写 PickersLocale 单元测试 + +**Files:** +- Modify: `test/flutter_picker_test.dart` + +- [ ] **Step 1: 添加 PickersLocale 测试** + +```dart +import 'dart:ui'; +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PickersLocale', () { + setUp(() { + PickersLocale.setLocale(null); + }); + + test('setLocale with zh returns zh', () { + PickersLocale.setLocale(const Locale('zh')); + expect(PickersLocale.currentLocale?.languageCode, 'zh'); + }); + + test('setLocale with en returns en', () { + PickersLocale.setLocale(const Locale('en')); + expect(PickersLocale.currentLocale?.languageCode, 'en'); + }); + + test('setLocale with unsupported locale is ignored', () { + PickersLocale.setLocale(const Locale('ja')); + expect(PickersLocale.currentLocale, isNull); + }); + + test('setLocale with null resets to null', () { + PickersLocale.setLocale(const Locale('en')); + PickersLocale.setLocale(null); + expect(PickersLocale.currentLocale, isNull); + }); + + test('supportedLocales contains zh and en', () { + final locales = PickersLocale.supportedLocales; + expect(locales.any((l) => l.languageCode == 'zh'), isTrue); + expect(locales.any((l) => l.languageCode == 'en'), isTrue); + }); + }); +} +``` + +- [ ] **Step 2: 运行测试** + +Run: `flutter test test/flutter_picker_test.dart` +Expected: All tests pass + +- [ ] **Step 3: 提交** + +```bash +git add test/flutter_picker_test.dart +git commit -m "test: 添加 PickersLocale 单元测试" +``` + +--- + +## Chunk 7: 最终验证 + +### Task 18: 运行静态分析 + +- [ ] **Step 1: 运行 flutter analyze** + +Run: `flutter analyze` +Expected: No issues found + +如果发现问题,修复后再继续。 + +### Task 19: 运行所有测试 + +- [ ] **Step 1: 运行测试** + +Run: `flutter test` +Expected: All tests pass + +### Task 20: 更新 pubspec.yaml 版本 + +**Files:** +- Modify: `pubspec.yaml` + +- [ ] **Step 1: 更新版本号** + +将版本从 `2.2.0` 改为 `2.3.0` + +- [ ] **Step 2: 提交** + +```bash +git add pubspec.yaml +git commit -m "chore: 更新版本到 2.3.0" +``` + +### Task 21: 最终提交 + +- [ ] **Step 1: 查看所有更改** + +Run: `git log --oneline -20` +Expected: 所有任务都已提交 + +- [ ] **Step 2: 推送到远程(可选)** + +```bash +git push origin master +``` + +--- + +## 验收清单 + +- [ ] flutter pub get 成功 +- [ ] flutter gen-l10n 成功生成国际化代码 +- [ ] 所有选择器在中英文环境下正常显示 +- [ ] PickersLocale.setLocale() 正常工作 +- [ ] Pickers.getData() 返回正确的国际化数据 +- [ ] 现有 API 保持向后兼容 +- [ ] 无国际化配置时,行为与之前一致 \ No newline at end of file diff --git a/docs/superpowers/specs/2026-03-17-i18n-design.md b/docs/superpowers/specs/2026-03-17-i18n-design.md new file mode 100644 index 0000000..8fd984e --- /dev/null +++ b/docs/superpowers/specs/2026-03-17-i18n-design.md @@ -0,0 +1,418 @@ +# flutter_pickers 国际化设计文档 + +## 概述 + +为 flutter_pickers 库添加国际化支持,实现中英文双语,支持系统语言跟随和API手动控制两种方式。 + +## 目标 + +- 支持中文(zh)和英文(en) +- 翻译UI文本和内置数据 +- 支持系统语言跟随 + API控制 +- 城市名称保持中文 + +## 技术方案 + +使用 Flutter 官方国际化方案:`flutter_localizations` + `intl` 包。 + +### 依赖变更 + +```yaml +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.18.0 +``` + +### l10n.yaml 配置 + +```yaml +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +output-dir: lib/l10n/generated +synthetic-package: false +``` + +## 目录结构 + +``` +lib/ +├── l10n/ +│ ├── l10n.yaml +│ ├── app_zh.arb +│ ├── app_en.arb +│ └── generated/ +│ ├── app_localizations.dart +│ ├── app_localizations_zh.dart +│ └── app_localizations_en.dart +├── pickers.dart +└── ... +``` + +## 国际化内容 + +### UI文本 + +| Key | 中文 | 英文 | +|-----|------|------| +| cancel | 取消 | Cancel | +| confirm | 确定 | Confirm | +| year | 年 | Year | +| month | 月 | Month | +| day | 日 | Day | +| hour | 时 | Hour | +| minute | 分 | Minute | +| second | 秒 | Second | +| all | 全部 | All | + +### 内置数据 + +#### sex (性别) +| 中文 | 英文 | +|------|------| +| 不限 | Any | +| 男 | Male | +| 女 | Female | + +#### education (学历) +| 中文 | 英文 | +|------|------| +| 高中以下 | Below High School | +| 高中 | High School | +| 大专 | Associate | +| 本科 | Bachelor | +| 硕士 | Master | +| 博士 | PhD | +| 博士后 | Postdoc | +| 其它 | Other | + +#### subject (学科) +| 中文 | 英文 | +|------|------| +| 语文 | Chinese | +| 数学 | Math | +| 英语 | English | +| 物理 | Physics | +| 化学 | Chemistry | +| 生物 | Biology | +| 政治 | Politics | +| 地理 | Geography | +| 历史 | History | + +#### constellation (星座) +| 中文 | 英文 | +|------|------| +| 水瓶座 | Aquarius | +| 双鱼座 | Pisces | +| 白羊座 | Aries | +| 金牛座 | Taurus | +| 双子座 | Gemini | +| 巨蟹座 | Cancer | +| 狮子座 | Leo | +| 处女座 | Virgo | +| 天秤座 | Libra | +| 天蝎座 | Scorpio | +| 射手座 | Sagittarius | +| 摩羯座 | Capricorn | + +#### zodiac (生肖) +| 中文 | 英文 | +|------|------| +| 鼠 | Rat | +| 牛 | Ox | +| 虎 | Tiger | +| 兔 | Rabbit | +| 龙 | Dragon | +| 蛇 | Snake | +| 马 | Horse | +| 羊 | Goat | +| 猴 | Monkey | +| 鸡 | Rooster | +| 狗 | Dog | +| 猪 | Pig | + +#### ethnicity (民族) +| 中文 | 英文 | +|------|------| +| 汉族 | Han | +| 蒙古族 | Mongol | +| 回族 | Hui | +| 藏族 | Tibetan | +| 维吾尔族 | Uygur | +| 苗族 | Miao | +| 彝族 | Yi | +| 壮族 | Zhuang | +| 布依族 | Bouyei | +| 朝鲜族 | Korean | +| 满族 | Manchu | +| 侗族 | Dong | +| 瑶族 | Yao | +| 白族 | Bai | +| 土家族 | Tujia | +| 哈尼族 | Hani | +| 哈萨克族 | Kazak | +| 傣族 | Dai | +| 黎族 | Li | +| 傈僳族 | Lisu | +| 佤族 | Va | +| 畲族 | She | +| 高山族 | Gaoshan | +| 拉祜族 | Lahu | +| 水族 | Shui | +| 东乡族 | Dongxiang | +| 纳西族 | Naxi | +| 景颇族 | Jingpo | +| 柯尔克孜族 | Kirgiz | +| 土族 | Tu | +| 达斡尔族 | Daur | +| 仫佬族 | Mulao | +| 羌族 | Qiang | +| 布朗族 | Blang | +| 撒拉族 | Salar | +| 毛难族 | Maonan | +| 仡佬族 | Gelao | +| 锡伯族 | Xibe | +| 阿昌族 | Achang | +| 普米族 | Pumi | +| 塔吉克族 | Tajik | +| 怒族 | Nu | +| 乌孜别克族 | Uzbek | +| 俄罗斯族 | Russian | +| 鄂温克族 | Ewenki | +| 崩龙族 | Deang | +| 保安族 | Bonan | +| 裕固族 | Yugur | +| 京族 | Gin | +| 塔塔尔族 | Tatar | +| 独龙族 | Derung | +| 鄂伦春族 | Oroqen | +| 赫哲族 | Hezhen | +| 门巴族 | Monba | +| 珞巴族 | Lhoba | +| 基诺族 | Jino | +| 其他 | Other | +| 外国血统中国人士 | Foreign-born Chinese + +## API设计 + +### PickersLocale 类 + +```dart +class PickersLocale { + static Locale? _overrideLocale; + + /// 设置语言(优先级高于系统设置) + /// 仅支持 'zh' 和 'en',传入其他值将被忽略 + static void setLocale(Locale? locale) { + if (locale != null && !_isSupported(locale)) { + return; // 忽略不支持的语言 + } + _overrideLocale = locale; + } + + /// 获取当前语言 + static Locale? get currentLocale => _overrideLocale; + + /// 支持的语言列表 + static List get supportedLocales => [ + Locale('zh'), + Locale('en'), + ]; + + /// 检查是否为支持的语言 + static bool _isSupported(Locale locale) { + return locale.languageCode == 'zh' || locale.languageCode == 'en'; + } +} +``` + +### Fallback 策略 + +当系统语言非中英文时,默认使用中文(zh)作为 fallback 语言。 + +优先级顺序: +1. API 设置的语言(PickersLocale.setLocale) +2. 系统语言(如果支持) +3. 默认中文(zh) + +### 使用方式 + +#### 跟随系统(默认) + +```dart +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +MaterialApp( + localizationsDelegates: [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, +); +``` + +#### API控制 + +```dart +// 设置英文 +PickersLocale.setLocale(Locale('en')); + +// 设置中文 +PickersLocale.setLocale(Locale('zh')); + +// 恢复跟随系统 +PickersLocale.setLocale(null); +``` + +### 内置数据国际化 + +`PickerDataType` 枚举已存在于 `lib/more_pickers/init_data.dart`,无需新增。 + +`getData` 方法定义在 `Pickers` 类中: + +```dart +class Pickers { + /// 获取国际化后的内置数据 + /// [type] 数据类型 + /// [context] BuildContext,用于获取本地化实例 + /// 返回 List 类型的数据列表 + /// 如果 context 无法获取 AppLocalizations,返回中文数据 + static List getData(PickerDataType type, BuildContext context) { + final l10n = AppLocalizations.of(context); + if (l10n == null) { + return _getDefaultData(type); // fallback 到中文 + } + return _getLocalizedData(type, l10n); + } + + static List _getDefaultData(PickerDataType type) { + return pickerData[type] ?? []; + } + + static List _getLocalizedData(PickerDataType type, AppLocalizations l10n) { + switch (type) { + case PickerDataType.sex: + return [l10n.sexAny, l10n.sexMale, l10n.sexFemale]; + case PickerDataType.education: + return [l10n.educationBelowHighSchool, l10n.educationHighSchool, ...]; + case PickerDataType.constellation: + return [l10n.constellationAquarius, l10n.constellationPisces, ...]; + // ... 其他类型 + } + } +} + +// 旧方式(保持兼容,返回中文) +PickerDataType.sex + +// 新方式(支持国际化) +final sexData = Pickers.getData(PickerDataType.sex, context); +// 返回: ['Any', 'Male', 'Female'] 或 ['不限', '男', '女'] +``` + +## 文件改造 + +### pickers.dart +- 添加 `PickersLocale` 类(语言管理) +- 添加 `getData()` 静态方法(获取国际化内置数据) +- 添加 `_getDefaultData()` 和 `_getLocalizedData()` 私有方法 + +### init_data.dart +- 保留现有 `pickerData` Map(保持向后兼容) +- 不新增 `_localizedPickerData` Map,翻译数据直接通过 arb 文件管理 +- `PickerDataType` 枚举已存在,无需修改 +- 不废弃现有静态列表,`PickerDataType.sex` 仍可使用 + +### 各 Route 文件改造 + +由于 Route 类在 Navigator.push 时创建,已有 `BuildContext` 可用: + +**date_picker_route.dart** +- 时间单位通过 `AppLocalizations.of(context)` 获取 +- 新增私有方法 `_getTimeUnitText()` 处理国际化 + +**picker_style.dart / default_style.dart** +- `PickerStyle` 类已有 `context` 属性(可空),无需新增 +- 按钮文本(取消/确定)通过 BuildContext 获取国际化文本 +- 在 `pickers.dart` 的 `_initPickerStyle` 方法中设置 context +- 保持现有 API 兼容,context 为 null 时使用中文默认值 + +```dart +// 现有 PickerStyle 类已有 context 属性 +class PickerStyle { + BuildContext? context; + // ... 其他属性 +} + +// 在 pickers.dart 中 +static PickerStyle _initPickerStyle(PickerStyle? pickerStyle, BuildContext context) { + final style = pickerStyle ?? DefaultPickerStyle(); + style.context ??= context; // 设置 context + return style; +} +``` + +**address_picker_route.dart** +- "全部" 文本通过 `AppLocalizations.of(context)` 获取 + +### 错误处理 + +所有组件在获取 `AppLocalizations` 时都进行 null 检查: +- 如果 `AppLocalizations.of(context)` 返回 null,使用中文作为 fallback +- 选择器打开期间 locale 变化:由于 Route 已创建,不会重新构建,保持打开时的语言 +- 不支持的语言代码:在 `PickersLocale.setLocale()` 中已忽略 +- **城市选择器**:无论语言设置如何,城市名称始终显示中文(按设计要求) + +## 兼容性 + +- 保持现有API完全兼容 +- 新增国际化功能为可选特性 +- 不使用国际化时,行为与之前一致 + +## 测试计划 + +### 单元测试 + +1. **PickersLocale.setLocale()** + - 传入 'zh',currentLocale 返回 Locale('zh') + - 传入 'en',currentLocale 返回 Locale('en') + - 传入不支持的语言(如 'ja'),currentLocale 不变 + - 传入 null,currentLocale 变为 null + - 连续设置多次,最后一次生效 + +2. **Pickers.getData()** + - context 为英文环境,传入 PickerDataType.sex,返回 ['Any', 'Male', 'Female'] + - context 为中文环境,传入 PickerDataType.sex,返回 ['不限', '男', '女'] + - AppLocalizations 为 null,返回中文数据 + +### Widget 测试 + +3. **时间选择器国际化** + - 系统语言为英文,时间单位显示 Year/Month/Day + - 系统语言为中文,时间单位显示 年/月/日 + +4. **地址选择器国际化** + - "全部" 文本随语言切换 + +5. **样式国际化** + - 取消/确定按钮文本随语言切换 + +### 集成测试 + +6. **语言切换流程** + - 设置 API 语言为英文,所有选择器显示英文 + - 设置 API 语言为 null,跟随系统语言 + - 系统语言切换后,重新打开选择器显示新语言 + +### 验收标准 + +- 所有测试用例通过 +- 现有 API 保持向后兼容 +- 无国际化配置时,行为与之前一致(显示中文) \ No newline at end of file diff --git a/example/android/gradle.properties b/example/android/gradle.properties index f018a61..475a628 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/example/ios/.gitignore b/example/ios/.gitignore index 9f11a5b..e96ef60 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -30,4 +30,3 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 -/Runner.xcodeproj/ diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..a8c080f --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,47 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + end + end +end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d986541 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,737 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 936AD8B762D0B7746248B4C6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD02AA4252F6D63587595D1B /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + E8394CD6B10D6B364AF59AD6 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DBD45DB584648BEF16EC55E /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C3F2D8E630BC5765F154567 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1DBD45DB584648BEF16EC55E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 6C766335C6DA3EAA1619D5AC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 805D4531A7303949573A383D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 94EFB0D6BB68C5D01C397FDF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9CC52BB438C1AAE77CAE442E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + B639D19BA5B15101784A490B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + FD02AA4252F6D63587595D1B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + 936AD8B762D0B7746248B4C6 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A0815BA584983028A1101793 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E8394CD6B10D6B364AF59AD6 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1C1E5EABD4EAD7429BDF1AB0 /* Pods */ = { + isa = PBXGroup; + children = ( + 6C766335C6DA3EAA1619D5AC /* Pods-Runner.debug.xcconfig */, + 9CC52BB438C1AAE77CAE442E /* Pods-Runner.release.xcconfig */, + 0C3F2D8E630BC5765F154567 /* Pods-Runner.profile.xcconfig */, + 94EFB0D6BB68C5D01C397FDF /* Pods-RunnerTests.debug.xcconfig */, + 805D4531A7303949573A383D /* Pods-RunnerTests.release.xcconfig */, + B639D19BA5B15101784A490B /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 908A672943D4C3DC1D4924FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + FD02AA4252F6D63587595D1B /* Pods_Runner.framework */, + 1DBD45DB584648BEF16EC55E /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 1C1E5EABD4EAD7429BDF1AB0 /* Pods */, + 908A672943D4C3DC1D4924FC /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 81EDBF5189CF71DF171791E6 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + A0815BA584983028A1101793 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 6E30F8EFFC79C9F0FE3E472F /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 6E30F8EFFC79C9F0FE3E472F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 81EDBF5189CF71DF171791E6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 3GX68PPUE7; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 94EFB0D6BB68C5D01C397FDF /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 805D4531A7303949573A383D /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B639D19BA5B15101784A490B /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 3GX68PPUE7; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 3GX68PPUE7; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 1579fb3..4f68a2c 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -43,5 +43,7 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/date_picker_page.dart b/example/lib/date_picker_page.dart index 5aaef95..9e4aaac 100644 --- a/example/lib/date_picker_page.dart +++ b/example/lib/date_picker_page.dart @@ -75,7 +75,7 @@ class _DatePickerPageState extends State { Widget _item(title, model) { return Column( children: [ - Container( + Material( color: Colors.white, child: ListTile( title: Text(title), diff --git a/example/lib/multiple_picker_page.dart b/example/lib/multiple_picker_page.dart index 8751595..ee81973 100644 --- a/example/lib/multiple_picker_page.dart +++ b/example/lib/multiple_picker_page.dart @@ -15,6 +15,12 @@ class MultiplePickerPage extends StatefulWidget { class _MultiplePickerPageState extends State { var hourse = 13; String minute = '58'; + final weightController = TextEditingController(text: '60.0'); + List selectedWeight = [60, 0]; + final weightData = [ + List.generate(81, (index) => index + 40), + List.generate(10, (index) => index), + ]; // 时间多列 选中的数据 var listTime = []; @@ -54,6 +60,7 @@ class _MultiplePickerPageState extends State { _item2('时间(多列)'), _item3('时间段'), _item4('自定义样式'), + _editableWeightItem(), ElevatedButton(onPressed: _showDemo, child: Text('Demo')), SizedBox(height: 80), ], @@ -61,6 +68,104 @@ class _MultiplePickerPageState extends State { ); } + @override + void dispose() { + weightController.dispose(); + super.dispose(); + } + + Widget _editableWeightItem() { + return Column( + children: [ + Material( + color: Colors.white, + child: ListTile( + title: const Text('体重(滚轮与输入框双向联动)'), + onTap: _showEditableWeightPicker, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + MyText( + '${selectedWeight[0]}.${selectedWeight[1]} kg', + color: Colors.grey, + rightpadding: 18, + ), + rightIcon, + ], + ), + ), + ), + divider, + ], + ); + } + + void _showEditableWeightPicker() { + Pickers.showMultiPicker( + context, + data: weightData, + selectData: selectedWeight, + suffix: const ['', ''], + editorHeight: 64, + editorBuilder: (context, selection, updateSelection) { + final value = '${selection[0]}.${selection[1]}'; + if (weightController.text != value) { + weightController.value = TextEditingValue( + text: value, + selection: TextSelection.collapsed(offset: value.length), + ); + } + + return Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 76, + child: TextField( + controller: weightController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w500, + ), + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.only(bottom: 4), + ), + onChanged: (value) { + final match = RegExp(r'^(\d+)\.(\d)$').firstMatch(value); + if (match == null) return; + updateSelection([ + int.parse(match.group(1)!), + int.parse(match.group(2)!), + ]); + }, + ), + ), + const Text( + ' kg', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w500), + ), + const Padding( + padding: EdgeInsets.only(left: 8), + child: Icon(Icons.edit, size: 20, color: Colors.grey), + ), + ], + ), + ); + }, + onConfirm: (selection, positions) { + setState(() { + selectedWeight = [selection[0] as int, selection[1] as int]; + }); + }, + ); + } + void _showDemo() { Pickers.showMultiPicker( context, @@ -77,7 +182,7 @@ class _MultiplePickerPageState extends State { Widget _item(title) { return Column( children: [ - Container( + Material( color: Colors.white, child: ListTile( title: Text(title), @@ -103,7 +208,7 @@ class _MultiplePickerPageState extends State { Widget _item2(title) { return Column( children: [ - Container( + Material( color: Colors.white, child: ListTile( title: Text(title), @@ -129,7 +234,7 @@ class _MultiplePickerPageState extends State { Widget _item3(title) { return Column( children: [ - Container( + Material( color: Colors.white, child: ListTile( title: Text(title), @@ -155,7 +260,7 @@ class _MultiplePickerPageState extends State { Widget _item4(title) { return Column( children: [ - Container( + Material( color: Colors.white, child: ListTile( title: Text(title), diff --git a/example/lib/single_picker_page.dart b/example/lib/single_picker_page.dart index a212b3b..257e67a 100644 --- a/example/lib/single_picker_page.dart +++ b/example/lib/single_picker_page.dart @@ -104,7 +104,7 @@ class _SinglePickerPageState extends State { Widget _item(title, var data, var selectData, {String? label}) { return Column( children: [ - Container( + Material( color: Colors.white, child: ListTile( title: Text(title), @@ -160,7 +160,7 @@ class _SinglePickerPageState extends State { Widget _item2(title) { return Column( children: [ - Container( + Material( color: Colors.white, child: ListTile( title: Text(title), diff --git a/example/pubspec.lock b/example/pubspec.lock index 32e79c8..1f42146 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,32 +5,32 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 - url: "https://pub.flutter-io.cn" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.flutter-io.cn" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" collection: @@ -38,25 +38,25 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.flutter-io.cn" + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" fake_async: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" - url: "https://pub.flutter-io.cn" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -67,83 +67,96 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_pickers: dependency: "direct main" description: path: ".." relative: true source: path - version: "2.1.9" + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec - url: "https://pub.flutter-io.cn" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.flutter-io.cn" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.flutter-io.cn" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.flutter-io.cn" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.flutter-io.cn" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.18.0" path: dependency: transitive description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" sky_engine: @@ -155,16 +168,16 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.flutter-io.cn" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.1" stream_channel: @@ -172,7 +185,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" string_scanner: @@ -180,7 +193,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" term_glyph: @@ -188,33 +201,33 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.flutter-io.cn" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.11" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" - url: "https://pub.flutter-io.cn" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.2.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/address_picker/locations_data.dart b/lib/address_picker/locations_data.dart index 2b64b50..76136b0 100644 --- a/lib/address_picker/locations_data.dart +++ b/lib/address_picker/locations_data.dart @@ -1,5 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; import '../utils/check.dart'; +/// 获取国际化的"全部"文本 +String getAllText(BuildContext context) { + final l10n = AppLocalizations.of(context); + return l10n?.all ?? '全部'; +} + /// 数据地址:https://github.com/airyland/china-area-data/blob/master/data.json const locations = { "86": { @@ -354,7 +362,14 @@ const locations = { "140226": "左云县", "140271": "山西大同经济开发区" }, - "140300": {"140301": "市辖区", "140302": "城区", "140303": "矿区", "140311": "郊区", "140321": "平定县", "140322": "盂县"}, + "140300": { + "140301": "市辖区", + "140302": "城区", + "140303": "矿区", + "140311": "郊区", + "140321": "平定县", + "140322": "盂县" + }, "140400": { "140401": "市辖区", "140403": "潞州区", @@ -515,7 +530,12 @@ const locations = { "150223": "达尔罕茂明安联合旗", "150271": "包头稀土高新技术产业开发区" }, - "150300": {"150301": "市辖区", "150302": "海勃湾区", "150303": "海南区", "150304": "乌达区"}, + "150300": { + "150301": "市辖区", + "150302": "海勃湾区", + "150303": "海南区", + "150304": "乌达区" + }, "150400": { "150401": "市辖区", "150402": "红山区", @@ -619,7 +639,12 @@ const locations = { "152531": "多伦县", "152571": "乌拉盖管委会" }, - "152900": {"152921": "阿拉善左旗", "152922": "阿拉善右旗", "152923": "额济纳旗", "152971": "内蒙古阿拉善经济开发区"}, + "152900": { + "152921": "阿拉善左旗", + "152922": "阿拉善右旗", + "152923": "额济纳旗", + "152971": "内蒙古阿拉善经济开发区" + }, "210000": { "210100": "沈阳市", "210200": "大连市", @@ -742,7 +767,13 @@ const locations = { "211021": "辽阳县", "211081": "灯塔市" }, - "211100": {"211101": "市辖区", "211102": "双台子区", "211103": "兴隆台区", "211104": "大洼区", "211122": "盘山县"}, + "211100": { + "211101": "市辖区", + "211102": "双台子区", + "211103": "兴隆台区", + "211104": "大洼区", + "211122": "盘山县" + }, "211200": { "211201": "市辖区", "211202": "银州区", @@ -824,7 +855,13 @@ const locations = { "220381": "公主岭市", "220382": "双辽市" }, - "220400": {"220401": "市辖区", "220402": "龙山区", "220403": "西安区", "220421": "东丰县", "220422": "东辽县"}, + "220400": { + "220401": "市辖区", + "220402": "龙山区", + "220403": "西安区", + "220421": "东丰县", + "220422": "东辽县" + }, "220500": { "220501": "市辖区", "220502": "东昌区", @@ -1000,7 +1037,13 @@ const locations = { "230882": "富锦市", "230883": "抚远市" }, - "230900": {"230901": "市辖区", "230902": "新兴区", "230903": "桃山区", "230904": "茄子河区", "230921": "勃利县"}, + "230900": { + "230901": "市辖区", + "230902": "新兴区", + "230903": "桃山区", + "230904": "茄子河区", + "230921": "勃利县" + }, "231000": { "231001": "市辖区", "231002": "东安区", @@ -1294,7 +1337,14 @@ const locations = { "330482": "平湖市", "330483": "桐乡市" }, - "330500": {"330501": "市辖区", "330502": "吴兴区", "330503": "南浔区", "330521": "德清县", "330522": "长兴县", "330523": "安吉县"}, + "330500": { + "330501": "市辖区", + "330502": "吴兴区", + "330503": "南浔区", + "330521": "德清县", + "330522": "长兴县", + "330523": "安吉县" + }, "330600": { "330601": "市辖区", "330602": "越城区", @@ -1325,7 +1375,13 @@ const locations = { "330825": "龙游县", "330881": "江山市" }, - "330900": {"330901": "市辖区", "330902": "定海区", "330903": "普陀区", "330921": "岱山县", "330922": "嵊泗县"}, + "330900": { + "330901": "市辖区", + "330902": "定海区", + "330903": "普陀区", + "330921": "岱山县", + "330922": "嵊泗县" + }, "331000": { "331001": "市辖区", "331002": "椒江区", @@ -1427,8 +1483,20 @@ const locations = { "340522": "含山县", "340523": "和县" }, - "340600": {"340601": "市辖区", "340602": "杜集区", "340603": "相山区", "340604": "烈山区", "340621": "濉溪县"}, - "340700": {"340701": "市辖区", "340705": "铜官区", "340706": "义安区", "340711": "郊区", "340722": "枞阳县"}, + "340600": { + "340601": "市辖区", + "340602": "杜集区", + "340603": "相山区", + "340604": "烈山区", + "340621": "濉溪县" + }, + "340700": { + "340701": "市辖区", + "340705": "铜官区", + "340706": "义安区", + "340711": "郊区", + "340722": "枞阳县" + }, "340800": { "340801": "市辖区", "340802": "迎江区", @@ -1499,8 +1567,20 @@ const locations = { "341524": "金寨县", "341525": "霍山县" }, - "341600": {"341601": "市辖区", "341602": "谯城区", "341621": "涡阳县", "341622": "蒙城县", "341623": "利辛县"}, - "341700": {"341701": "市辖区", "341702": "贵池区", "341721": "东至县", "341722": "石台县", "341723": "青阳县"}, + "341600": { + "341601": "市辖区", + "341602": "谯城区", + "341621": "涡阳县", + "341622": "蒙城县", + "341623": "利辛县" + }, + "341700": { + "341701": "市辖区", + "341702": "贵池区", + "341721": "东至县", + "341722": "石台县", + "341723": "青阳县" + }, "341800": { "341801": "市辖区", "341802": "宣州区", @@ -1548,7 +1628,14 @@ const locations = { "350212": "同安区", "350213": "翔安区" }, - "350300": {"350301": "市辖区", "350302": "城厢区", "350303": "涵江区", "350304": "荔城区", "350305": "秀屿区", "350322": "仙游县"}, + "350300": { + "350301": "市辖区", + "350302": "城厢区", + "350303": "涵江区", + "350304": "荔城区", + "350305": "秀屿区", + "350322": "仙游县" + }, "350400": { "350401": "市辖区", "350402": "梅列区", @@ -1653,8 +1740,21 @@ const locations = { "360123": "安义县", "360124": "进贤县" }, - "360200": {"360201": "市辖区", "360202": "昌江区", "360203": "珠山区", "360222": "浮梁县", "360281": "乐平市"}, - "360300": {"360301": "市辖区", "360302": "安源区", "360313": "湘东区", "360321": "莲花县", "360322": "上栗县", "360323": "芦溪县"}, + "360200": { + "360201": "市辖区", + "360202": "昌江区", + "360203": "珠山区", + "360222": "浮梁县", + "360281": "乐平市" + }, + "360300": { + "360301": "市辖区", + "360302": "安源区", + "360313": "湘东区", + "360321": "莲花县", + "360322": "上栗县", + "360323": "芦溪县" + }, "360400": { "360401": "市辖区", "360402": "濂溪区", @@ -1672,7 +1772,12 @@ const locations = { "360483": "庐山市" }, "360500": {"360501": "市辖区", "360502": "渝水区", "360521": "分宜县"}, - "360600": {"360601": "市辖区", "360602": "月湖区", "360603": "余江区", "360681": "贵溪市"}, + "360600": { + "360601": "市辖区", + "360602": "月湖区", + "360603": "余江区", + "360681": "贵溪市" + }, "360700": { "360701": "市辖区", "360702": "章贡区", @@ -1897,7 +2002,14 @@ const locations = { "371082": "荣成市", "371083": "乳山市" }, - "371100": {"371101": "市辖区", "371102": "东港区", "371103": "岚山区", "371121": "五莲县", "371122": "莒县", "371171": "日照经济技术开发区"}, + "371100": { + "371101": "市辖区", + "371102": "东港区", + "371103": "岚山区", + "371121": "五莲县", + "371122": "莒县", + "371171": "日照经济技术开发区" + }, "371300": { "371301": "市辖区", "371302": "兰山区", @@ -2300,8 +2412,20 @@ const locations = { "420683": "枣阳市", "420684": "宜城市" }, - "420700": {"420701": "市辖区", "420702": "梁子湖区", "420703": "华容区", "420704": "鄂城区"}, - "420800": {"420801": "市辖区", "420802": "东宝区", "420804": "掇刀区", "420822": "沙洋县", "420881": "钟祥市", "420882": "京山市"}, + "420700": { + "420701": "市辖区", + "420702": "梁子湖区", + "420703": "华容区", + "420704": "鄂城区" + }, + "420800": { + "420801": "市辖区", + "420802": "东宝区", + "420804": "掇刀区", + "420822": "沙洋县", + "420881": "钟祥市", + "420882": "京山市" + }, "420900": { "420901": "市辖区", "420902": "孝南区", @@ -2358,7 +2482,12 @@ const locations = { "422827": "来凤县", "422828": "鹤峰县" }, - "429000": {"429004": "仙桃市", "429005": "潜江市", "429006": "天门市", "429021": "神农架林区"}, + "429000": { + "429004": "仙桃市", + "429005": "潜江市", + "429006": "天门市", + "429021": "神农架林区" + }, "430000": { "430100": "长沙市", "430200": "株洲市", @@ -2470,7 +2599,13 @@ const locations = { "430771": "常德市西洞庭管理区", "430781": "津市市" }, - "430800": {"430801": "市辖区", "430802": "永定区", "430811": "武陵源区", "430821": "慈利县", "430822": "桑植县"}, + "430800": { + "430801": "市辖区", + "430802": "永定区", + "430811": "武陵源区", + "430821": "慈利县", + "430822": "桑植县" + }, "430900": { "430901": "市辖区", "430902": "资阳区", @@ -2529,7 +2664,14 @@ const locations = { "431271": "怀化市洪江管理区", "431281": "洪江市" }, - "431300": {"431301": "市辖区", "431302": "娄星区", "431321": "双峰县", "431322": "新化县", "431381": "冷水江市", "431382": "涟源市"}, + "431300": { + "431301": "市辖区", + "431302": "娄星区", + "431321": "双峰县", + "431322": "新化县", + "431381": "冷水江市", + "431382": "涟源市" + }, "433100": { "433101": "吉首市", "433122": "泸溪县", @@ -2604,7 +2746,12 @@ const locations = { "440311": "光明区", "440312": "大鹏新区" }, - "440400": {"440401": "市辖区", "440402": "香洲区", "440403": "斗门区", "440404": "金湾区"}, + "440400": { + "440401": "市辖区", + "440402": "香洲区", + "440403": "斗门区", + "440404": "金湾区" + }, "440500": { "440501": "市辖区", "440507": "龙湖区", @@ -2615,7 +2762,14 @@ const locations = { "440515": "澄海区", "440523": "南澳县" }, - "440600": {"440601": "市辖区", "440604": "禅城区", "440605": "南海区", "440606": "顺德区", "440607": "三水区", "440608": "高明区"}, + "440600": { + "440601": "市辖区", + "440604": "禅城区", + "440605": "南海区", + "440606": "顺德区", + "440607": "三水区", + "440608": "高明区" + }, "440700": { "440701": "市辖区", "440703": "蓬江区", @@ -2638,7 +2792,14 @@ const locations = { "440882": "雷州市", "440883": "吴川市" }, - "440900": {"440901": "市辖区", "440902": "茂南区", "440904": "电白区", "440981": "高州市", "440982": "化州市", "440983": "信宜市"}, + "440900": { + "440901": "市辖区", + "440902": "茂南区", + "440904": "电白区", + "440981": "高州市", + "440982": "化州市", + "440983": "信宜市" + }, "441200": { "441201": "市辖区", "441202": "端州区", @@ -2650,7 +2811,14 @@ const locations = { "441226": "德庆县", "441284": "四会市" }, - "441300": {"441301": "市辖区", "441302": "惠城区", "441303": "惠阳区", "441322": "博罗县", "441323": "惠东县", "441324": "龙门县"}, + "441300": { + "441301": "市辖区", + "441302": "惠城区", + "441303": "惠阳区", + "441322": "博罗县", + "441323": "惠东县", + "441324": "龙门县" + }, "441400": { "441401": "市辖区", "441402": "梅江区", @@ -2662,7 +2830,13 @@ const locations = { "441427": "蕉岭县", "441481": "兴宁市" }, - "441500": {"441501": "市辖区", "441502": "城区", "441521": "海丰县", "441523": "陆河县", "441581": "陆丰市"}, + "441500": { + "441501": "市辖区", + "441502": "城区", + "441521": "海丰县", + "441523": "陆河县", + "441581": "陆丰市" + }, "441600": { "441601": "市辖区", "441602": "源城区", @@ -2672,7 +2846,13 @@ const locations = { "441624": "和平县", "441625": "东源县" }, - "441700": {"441701": "市辖区", "441702": "江城区", "441704": "阳东区", "441721": "阳西县", "441781": "阳春市"}, + "441700": { + "441701": "市辖区", + "441702": "江城区", + "441704": "阳东区", + "441721": "阳西县", + "441781": "阳春市" + }, "441800": { "441801": "市辖区", "441802": "清城区", @@ -2747,9 +2927,28 @@ const locations = { "442000116": "大涌镇", "442000117": "神湾镇" }, - "445100": {"445101": "市辖区", "445102": "湘桥区", "445103": "潮安区", "445122": "饶平县"}, - "445200": {"445201": "市辖区", "445202": "榕城区", "445203": "揭东区", "445222": "揭西县", "445224": "惠来县", "445281": "普宁市"}, - "445300": {"445301": "市辖区", "445302": "云城区", "445303": "云安区", "445321": "新兴县", "445322": "郁南县", "445381": "罗定市"}, + "445100": { + "445101": "市辖区", + "445102": "湘桥区", + "445103": "潮安区", + "445122": "饶平县" + }, + "445200": { + "445201": "市辖区", + "445202": "榕城区", + "445203": "揭东区", + "445222": "揭西县", + "445224": "惠来县", + "445281": "普宁市" + }, + "445300": { + "445301": "市辖区", + "445302": "云城区", + "445303": "云安区", + "445321": "新兴县", + "445322": "郁南县", + "445381": "罗定市" + }, "450000": { "450100": "南宁市", "450200": "柳州市", @@ -2824,10 +3023,35 @@ const locations = { "450423": "蒙山县", "450481": "岑溪市" }, - "450500": {"450501": "市辖区", "450502": "海城区", "450503": "银海区", "450512": "铁山港区", "450521": "合浦县"}, - "450600": {"450601": "市辖区", "450602": "港口区", "450603": "防城区", "450621": "上思县", "450681": "东兴市"}, - "450700": {"450701": "市辖区", "450702": "钦南区", "450703": "钦北区", "450721": "灵山县", "450722": "浦北县"}, - "450800": {"450801": "市辖区", "450802": "港北区", "450803": "港南区", "450804": "覃塘区", "450821": "平南县", "450881": "桂平市"}, + "450500": { + "450501": "市辖区", + "450502": "海城区", + "450503": "银海区", + "450512": "铁山港区", + "450521": "合浦县" + }, + "450600": { + "450601": "市辖区", + "450602": "港口区", + "450603": "防城区", + "450621": "上思县", + "450681": "东兴市" + }, + "450700": { + "450701": "市辖区", + "450702": "钦南区", + "450703": "钦北区", + "450721": "灵山县", + "450722": "浦北县" + }, + "450800": { + "450801": "市辖区", + "450802": "港北区", + "450803": "港南区", + "450804": "覃塘区", + "450821": "平南县", + "450881": "桂平市" + }, "450900": { "450901": "市辖区", "450902": "玉州区", @@ -2853,7 +3077,14 @@ const locations = { "451031": "隆林各族自治县", "451081": "靖西市" }, - "451100": {"451101": "市辖区", "451102": "八步区", "451103": "平桂区", "451121": "昭平县", "451122": "钟山县", "451123": "富川瑶族自治县"}, + "451100": { + "451101": "市辖区", + "451102": "八步区", + "451103": "平桂区", + "451121": "昭平县", + "451122": "钟山县", + "451123": "富川瑶族自治县" + }, "451200": { "451201": "市辖区", "451202": "金城江区", @@ -2887,9 +3118,27 @@ const locations = { "451425": "天等县", "451481": "凭祥市" }, - "460000": {"460100": "海口市", "460200": "三亚市", "460300": "三沙市", "460400": "儋州市", "469000": "省直辖县级行政区划"}, - "460100": {"460101": "市辖区", "460105": "秀英区", "460106": "龙华区", "460107": "琼山区", "460108": "美兰区"}, - "460200": {"460201": "市辖区", "460202": "海棠区", "460203": "吉阳区", "460204": "天涯区", "460205": "崖州区"}, + "460000": { + "460100": "海口市", + "460200": "三亚市", + "460300": "三沙市", + "460400": "儋州市", + "469000": "省直辖县级行政区划" + }, + "460100": { + "460101": "市辖区", + "460105": "秀英区", + "460106": "龙华区", + "460107": "琼山区", + "460108": "美兰区" + }, + "460200": { + "460201": "市辖区", + "460202": "海棠区", + "460203": "吉阳区", + "460204": "天涯区", + "460205": "崖州区" + }, "460300": {"460321": "西沙群岛", "460322": "南沙群岛", "460323": "中沙群岛的岛礁及其海域"}, "460400": { "460400100": "那大镇", @@ -3026,7 +3275,14 @@ const locations = { "510321": "荣县", "510322": "富顺县" }, - "510400": {"510401": "市辖区", "510402": "东区", "510403": "西区", "510411": "仁和区", "510421": "米易县", "510422": "盐边县"}, + "510400": { + "510401": "市辖区", + "510402": "东区", + "510403": "西区", + "510411": "仁和区", + "510421": "米易县", + "510422": "盐边县" + }, "510500": { "510501": "市辖区", "510502": "江阳区", @@ -3068,7 +3324,14 @@ const locations = { "510823": "剑阁县", "510824": "苍溪县" }, - "510900": {"510901": "市辖区", "510903": "船山区", "510904": "安居区", "510921": "蓬溪县", "510923": "大英县", "510981": "射洪市"}, + "510900": { + "510901": "市辖区", + "510903": "船山区", + "510904": "安居区", + "510921": "蓬溪县", + "510923": "大英县", + "510981": "射洪市" + }, "511000": { "511001": "市辖区", "511002": "市中区", @@ -3166,7 +3429,12 @@ const locations = { "511923": "平昌县", "511971": "巴中经济开发区" }, - "512000": {"512001": "市辖区", "512002": "雁江区", "512021": "安岳县", "512022": "乐至县"}, + "512000": { + "512001": "市辖区", + "512002": "雁江区", + "512021": "安岳县", + "512022": "乐至县" + }, "513200": { "513201": "马尔康市", "513221": "汶川县", @@ -3245,7 +3513,12 @@ const locations = { "520123": "修文县", "520181": "清镇市" }, - "520200": {"520201": "钟山区", "520203": "六枝特区", "520221": "水城县", "520281": "盘州市"}, + "520200": { + "520201": "钟山区", + "520203": "六枝特区", + "520221": "水城县", + "520281": "盘州市" + }, "520300": { "520301": "市辖区", "520302": "红花岗区", @@ -3397,7 +3670,14 @@ const locations = { "530427": "新平彝族傣族自治县", "530428": "元江哈尼族彝族傣族自治县" }, - "530500": {"530501": "市辖区", "530502": "隆阳区", "530521": "施甸县", "530523": "龙陵县", "530524": "昌宁县", "530581": "腾冲市"}, + "530500": { + "530501": "市辖区", + "530502": "隆阳区", + "530521": "施甸县", + "530523": "龙陵县", + "530524": "昌宁县", + "530581": "腾冲市" + }, "530600": { "530601": "市辖区", "530602": "昭阳区", @@ -3496,8 +3776,19 @@ const locations = { "532931": "剑川县", "532932": "鹤庆县" }, - "533100": {"533102": "瑞丽市", "533103": "芒市", "533122": "梁河县", "533123": "盈江县", "533124": "陇川县"}, - "533300": {"533301": "泸水市", "533323": "福贡县", "533324": "贡山独龙族怒族自治县", "533325": "兰坪白族普米族自治县"}, + "533100": { + "533102": "瑞丽市", + "533103": "芒市", + "533122": "梁河县", + "533123": "盈江县", + "533124": "陇川县" + }, + "533300": { + "533301": "泸水市", + "533323": "福贡县", + "533324": "贡山独龙族怒族自治县", + "533325": "兰坪白族普米族自治县" + }, "533400": {"533401": "香格里拉市", "533422": "德钦县", "533423": "维西傈僳族自治县"}, "540000": { "540100": "拉萨市", @@ -3630,7 +3921,13 @@ const locations = { "610122": "蓝田县", "610124": "周至县" }, - "610200": {"610201": "市辖区", "610202": "王益区", "610203": "印台区", "610204": "耀州区", "610222": "宜君县"}, + "610200": { + "610201": "市辖区", + "610202": "王益区", + "610203": "印台区", + "610204": "耀州区", + "610222": "宜君县" + }, "610300": { "610301": "市辖区", "610302": "渭滨区", @@ -3775,7 +4072,14 @@ const locations = { }, "620200": {"620201": "市辖区"}, "620300": {"620301": "市辖区", "620302": "金川区", "620321": "永昌县"}, - "620400": {"620401": "市辖区", "620402": "白银区", "620403": "平川区", "620421": "靖远县", "620422": "会宁县", "620423": "景泰县"}, + "620400": { + "620401": "市辖区", + "620402": "白银区", + "620403": "平川区", + "620421": "靖远县", + "620422": "会宁县", + "620423": "景泰县" + }, "620500": { "620501": "市辖区", "620502": "秦州区", @@ -3786,7 +4090,13 @@ const locations = { "620524": "武山县", "620525": "张家川回族自治县" }, - "620600": {"620601": "市辖区", "620602": "凉州区", "620621": "民勤县", "620622": "古浪县", "620623": "天祝藏族自治县"}, + "620600": { + "620601": "市辖区", + "620602": "凉州区", + "620621": "民勤县", + "620622": "古浪县", + "620623": "天祝藏族自治县" + }, "620700": { "620701": "市辖区", "620702": "甘州区", @@ -3897,11 +4207,41 @@ const locations = { "630224": "化隆回族自治县", "630225": "循化撒拉族自治县" }, - "632200": {"632221": "门源回族自治县", "632222": "祁连县", "632223": "海晏县", "632224": "刚察县"}, - "632300": {"632321": "同仁县", "632322": "尖扎县", "632323": "泽库县", "632324": "河南蒙古族自治县"}, - "632500": {"632521": "共和县", "632522": "同德县", "632523": "贵德县", "632524": "兴海县", "632525": "贵南县"}, - "632600": {"632621": "玛沁县", "632622": "班玛县", "632623": "甘德县", "632624": "达日县", "632625": "久治县", "632626": "玛多县"}, - "632700": {"632701": "玉树市", "632722": "杂多县", "632723": "称多县", "632724": "治多县", "632725": "囊谦县", "632726": "曲麻莱县"}, + "632200": { + "632221": "门源回族自治县", + "632222": "祁连县", + "632223": "海晏县", + "632224": "刚察县" + }, + "632300": { + "632321": "同仁县", + "632322": "尖扎县", + "632323": "泽库县", + "632324": "河南蒙古族自治县" + }, + "632500": { + "632521": "共和县", + "632522": "同德县", + "632523": "贵德县", + "632524": "兴海县", + "632525": "贵南县" + }, + "632600": { + "632621": "玛沁县", + "632622": "班玛县", + "632623": "甘德县", + "632624": "达日县", + "632625": "久治县", + "632626": "玛多县" + }, + "632700": { + "632701": "玉树市", + "632722": "杂多县", + "632723": "称多县", + "632724": "治多县", + "632725": "囊谦县", + "632726": "曲麻莱县" + }, "632800": { "632801": "格尔木市", "632802": "德令哈市", @@ -3911,7 +4251,13 @@ const locations = { "632823": "天峻县", "632857": "大柴旦行政委员会" }, - "640000": {"640100": "银川市", "640200": "石嘴山市", "640300": "吴忠市", "640400": "固原市", "640500": "中卫市"}, + "640000": { + "640100": "银川市", + "640200": "石嘴山市", + "640300": "吴忠市", + "640400": "固原市", + "640500": "中卫市" + }, "640100": { "640101": "市辖区", "640104": "兴庆区", @@ -3921,10 +4267,34 @@ const locations = { "640122": "贺兰县", "640181": "灵武市" }, - "640200": {"640201": "市辖区", "640202": "大武口区", "640205": "惠农区", "640221": "平罗县"}, - "640300": {"640301": "市辖区", "640302": "利通区", "640303": "红寺堡区", "640323": "盐池县", "640324": "同心县", "640381": "青铜峡市"}, - "640400": {"640401": "市辖区", "640402": "原州区", "640422": "西吉县", "640423": "隆德县", "640424": "泾源县", "640425": "彭阳县"}, - "640500": {"640501": "市辖区", "640502": "沙坡头区", "640521": "中宁县", "640522": "海原县"}, + "640200": { + "640201": "市辖区", + "640202": "大武口区", + "640205": "惠农区", + "640221": "平罗县" + }, + "640300": { + "640301": "市辖区", + "640302": "利通区", + "640303": "红寺堡区", + "640323": "盐池县", + "640324": "同心县", + "640381": "青铜峡市" + }, + "640400": { + "640401": "市辖区", + "640402": "原州区", + "640422": "西吉县", + "640423": "隆德县", + "640424": "泾源县", + "640425": "彭阳县" + }, + "640500": { + "640501": "市辖区", + "640502": "沙坡头区", + "640521": "中宁县", + "640522": "海原县" + }, "650000": { "650100": "乌鲁木齐市", "650200": "克拉玛依市", @@ -3953,7 +4323,13 @@ const locations = { "650109": "米东区", "650121": "乌鲁木齐县" }, - "650200": {"650201": "市辖区", "650202": "独山子区", "650203": "克拉玛依区", "650204": "白碱滩区", "650205": "乌尔禾区"}, + "650200": { + "650201": "市辖区", + "650202": "独山子区", + "650203": "克拉玛依区", + "650204": "白碱滩区", + "650205": "乌尔禾区" + }, "650400": {"650402": "高昌区", "650421": "鄯善县", "650422": "托克逊县"}, "650500": {"650502": "伊州区", "650521": "巴里坤哈萨克自治县", "650522": "伊吾县"}, "652300": { @@ -3965,7 +4341,12 @@ const locations = { "652327": "吉木萨尔县", "652328": "木垒哈萨克自治县" }, - "652700": {"652701": "博乐市", "652702": "阿拉山口市", "652722": "精河县", "652723": "温泉县"}, + "652700": { + "652701": "博乐市", + "652702": "阿拉山口市", + "652722": "精河县", + "652723": "温泉县" + }, "652800": { "652801": "库尔勒市", "652822": "轮台县", @@ -3989,7 +4370,12 @@ const locations = { "652928": "阿瓦提县", "652929": "柯坪县" }, - "653000": {"653001": "阿图什市", "653022": "阿克陶县", "653023": "阿合奇县", "653024": "乌恰县"}, + "653000": { + "653001": "阿图什市", + "653022": "阿克陶县", + "653023": "阿合奇县", + "653024": "乌恰县" + }, "653100": { "653101": "喀什市", "653121": "疏附县", @@ -4045,7 +4431,13 @@ const locations = { "654325": "青河县", "654326": "吉木乃县" }, - "659000": {"659001": "石河子市", "659002": "阿拉尔市", "659003": "图木舒克市", "659004": "五家渠市", "659006": "铁门关市"}, + "659000": { + "659001": "石河子市", + "659002": "阿拉尔市", + "659003": "图木舒克市", + "659004": "五家渠市", + "659006": "铁门关市" + }, "710000": { "710100": "台北市", "710200": "高雄市", @@ -4082,7 +4474,14 @@ const locations = { "710210": "楠梓区", "710211": "小港区" }, - "710300": {"710301": "仁爱区", "710302": "信义区", "710303": "中正区", "710304": "暖暖区", "710305": "安乐区", "710307": "七堵区"}, + "710300": { + "710301": "仁爱区", + "710302": "信义区", + "710303": "中正区", + "710304": "暖暖区", + "710305": "安乐区", + "710307": "七堵区" + }, "710400": { "710301": "中区", "710302": "东区", @@ -4093,7 +4492,14 @@ const locations = { "710307": "西屯区", "710308": "南屯区" }, - "710500": {"710501": "中西区", "710502": "东区", "710503": "南区", "710504": "北区", "710505": "安平区", "710506": "安南区"}, + "710500": { + "710501": "中西区", + "710502": "东区", + "710503": "南区", + "710504": "北区", + "710505": "安平区", + "710506": "安南区" + }, "710600": {"710601": "东区", "710602": "北区", "710603": "香山区"}, "710700": {"710701": "东区", "710702": "西区"}, "810000": { @@ -4134,9 +4540,9 @@ class Address { // 是否在市、区级添加 全部选项 static bool addAllItem = true; - static getCities(String province) { - var emptyData = {'name': "全部", 'cityCode': ''}; - if (province == '全部' && addAllItem) return [emptyData]; + static getCities(String province, BuildContext context) { + var emptyData = {'name': getAllText(context), 'cityCode': ''}; + if (province == getAllText(context) && addAllItem) return [emptyData]; String code = ''; locations['86']!.forEach((key, value) { @@ -4156,15 +4562,17 @@ class Address { return data; } - static getTowns(cityCode) { - if (PicketUtil.strEmpty(cityCode)) return ['']; + static getTowns(String cityCode, BuildContext context) { + // 返回有意义的占位文本,避免空字符串导致 CupertinoPicker 语义问题 + final placeholder = getAllText(context); + if (PicketUtil.strEmpty(cityCode)) return [placeholder]; Map? areaList = locations[cityCode]; if (PicketUtil.mapEmpty(areaList)) { - return ['']; + return [placeholder]; } else { var data = areaList!.values.toList(); - if (addAllItem) data.insert(0, '全部'); + if (addAllItem) data.insert(0, placeholder); return data; } } @@ -4188,7 +4596,8 @@ class Address { // ignore: unintended_html_in_doc_comment /// List cityCode = Locations.getTownsCityCode("四川省","成都市","锦江区"); /// return [510000,510100,510104] or [510000,510000] or [510000] or [] - static List getCityCodeByName({String? provinceName, String? cityName, String? townName}) { + static List getCityCodeByName( + {String? provinceName, String? cityName, String? townName}) { List cityCode = []; if (PicketUtil.strEmpty(provinceName)) return cityCode; @@ -4256,7 +4665,8 @@ class Address { // ignore: unintended_html_in_doc_comment /// List cityName = Address.getCityNameByCode(provinceCode: "510000", cityCode: "510100", townCode: "510104"); /// return [四川省, 成都市, 锦江区] or [四川省, 成都市] or [四川省] or [] - static List getCityNameByCode({String? provinceCode, String? cityCode, String? townCode}) { + static List getCityNameByCode( + {String? provinceCode, String? cityCode, String? townCode}) { List cityName = []; if (PicketUtil.strEmpty(provinceCode)) return cityName; diff --git a/lib/address_picker/route/address_picker_route.dart b/lib/address_picker/route/address_picker_route.dart index 63cd75e..304e2d8 100644 --- a/lib/address_picker/route/address_picker_route.dart +++ b/lib/address_picker/route/address_picker_route.dart @@ -1,5 +1,8 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter_pickers/src/cascading_picker/address_adapter.dart'; +import 'package:flutter_pickers/src/cascading_picker/cascading_selection.dart'; +import 'package:flutter_pickers/src/route/picker_popup_route.dart'; +import 'package:flutter_pickers/src/route/picker_sheet.dart'; import 'package:flutter_pickers/style/picker_style.dart'; import '../locations_data.dart'; @@ -13,18 +16,18 @@ typedef AddressCallback = Function(String province, String city, String? town); /// [onChanged] 选择器发生变动 /// [onConfirm] 选择器提交 /// [addAllItem] 市、区是否添加 '全部' 选项 默认:true -class AddressPickerRoute extends PopupRoute { +class AddressPickerRoute extends PickerPopupRoute { AddressPickerRoute({ required this.addAllItem, - required this.pickerStyle, + required super.pickerStyle, required this.initProvince, required this.initCity, this.initTown, this.onChanged, this.onConfirm, - this.onCancel, - this.theme, - this.barrierLabel, + super.onCancel, + super.theme, + super.barrierLabel, super.settings, }); @@ -32,68 +35,23 @@ class AddressPickerRoute extends PopupRoute { final String? initTown; final AddressCallback? onChanged; final AddressCallback? onConfirm; - final Function(bool isCancel)? onCancel; - final ThemeData? theme; final bool addAllItem; - late final PickerStyle pickerStyle; - - @override - Duration get transitionDuration => const Duration(milliseconds: 200); - - @override - bool get barrierDismissible => true; - - @override - bool didPop(T? result) { - if (result == null) { - onCancel?.call(false); - } else if (!(result as bool)) { - onCancel?.call(true); - } - return super.didPop(result); - } - - @override - final String? barrierLabel; - @override - Color get barrierColor => Colors.black54; - - AnimationController? _animationController; - - @override - AnimationController createAnimationController() { - assert(_animationController == null); - _animationController = BottomSheet.createAnimationController( - navigator!.overlay!, - ); - return _animationController!; - } - - @override - Widget buildPage( + Widget buildPickerContent( BuildContext context, - Animation animation, - Animation secondaryAnimation, + PickerStyle resolvedStyle, + double safeAreaBottom, ) { - Widget bottomSheet = MediaQuery.removePadding( - context: context, - removeTop: true, - child: PickerContentView( - initProvince: initProvince, - initCity: initCity, - initTown: initTown, - addAllItem: addAllItem, - pickerStyle: pickerStyle, - route: this, - ), + return PickerContentView( + initProvince: initProvince, + initCity: initCity, + initTown: initTown, + addAllItem: addAllItem, + pickerStyle: resolvedStyle, + safeAreaBottom: safeAreaBottom, + route: this, ); - if (theme != null) { - bottomSheet = Theme(data: theme!, child: bottomSheet); - } - - return bottomSheet; } } @@ -105,6 +63,7 @@ class PickerContentView extends StatefulWidget { this.initTown, required this.pickerStyle, required this.addAllItem, + this.safeAreaBottom = 0.0, required this.route, }); @@ -113,6 +72,7 @@ class PickerContentView extends StatefulWidget { final AddressPickerRoute route; final bool addAllItem; final PickerStyle pickerStyle; + final double safeAreaBottom; @override State createState() => _PickerState(); @@ -120,360 +80,121 @@ class PickerContentView extends StatefulWidget { class _PickerState extends State { late final PickerStyle _pickerStyle; - late String _currentProvince, _currentCity; - String? _currentTown; - var cities = []; - var towns = []; - var provinces = []; - - // 是否显示县级 - bool hasTown = true; - - // 是否添加全部 - late final bool addAllItem; - - AnimationController? controller; - Animation? animation; - - late FixedExtentScrollController provinceScrollCtrl, - cityScrollCtrl, - townScrollCtrl; + late final CascadingSelectionModule _selection; + final List scrollCtrl = []; + bool _isInitialized = false; @override void initState() { super.initState(); - _currentProvince = widget.initProvince; - _currentCity = widget.initCity; - _currentTown = widget.initTown; - addAllItem = widget.addAllItem; _pickerStyle = widget.pickerStyle; + } - provinces = Address.provinces; - hasTown = _currentTown != null; - _init(); + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_isInitialized) return; + + _isInitialized = true; + final hasTown = widget.initTown != null; + _selection = CascadingSelectionModule( + adapter: AddressAdapter( + allText: getAllText(context), + addAllItem: widget.addAllItem, + hasTown: hasTown, + ), + initial: [ + widget.initProvince, + widget.initCity, + if (hasTown) widget.initTown, + ], + ); + for (final position in _selection.state.positions) { + scrollCtrl.add(FixedExtentScrollController(initialItem: position)); + } } @override void dispose() { - provinceScrollCtrl.dispose(); - cityScrollCtrl.dispose(); - townScrollCtrl.dispose(); - + for (final controller in scrollCtrl) { + controller.dispose(); + } super.dispose(); } @override Widget build(BuildContext context) { - return GestureDetector( - child: AnimatedBuilder( - animation: widget.route.animation!, - builder: (BuildContext context, Widget? child) { - return ClipRect( - child: CustomSingleChildLayout( - delegate: _BottomPickerLayout( - widget.route.animation!.value, - _pickerStyle, - ), - child: GestureDetector( - child: Material( - color: Colors.transparent, - child: _renderPickerView(), - ), - ), - ), - ); - }, - ), + return PickerSheet( + animation: widget.route.animation!, + style: _pickerStyle, + safeAreaBottom: widget.safeAreaBottom, + body: _renderItemView(), + onConfirm: () => _notify(widget.route.onConfirm), ); } - void _init() { - Address.addAllItem = addAllItem; - int pindex = 0; - int cindex = 0; - int tindex = 0; - pindex = provinces.indexWhere((p) => p == _currentProvince); - pindex = pindex >= 0 ? pindex : 0; - String? selectedProvince = provinces[pindex]; - if (selectedProvince != null) { - _currentProvince = selectedProvince; - - cities = Address.getCities(selectedProvince); - - cindex = cities.indexWhere((c) => c['name'] == _currentCity); - cindex = cindex >= 0 ? cindex : 0; - _currentCity = cities[cindex]['name']; - - // debugPrint('longer >>> 外面接到的$cities'); - - if (hasTown) { - towns = Address.getTowns(cities[cindex]['cityCode']); - tindex = towns.indexWhere((t) => t == _currentTown); - tindex = tindex >= 0 ? tindex : 0; - if (towns.isEmpty) { - _currentTown = ''; - } else { - _currentTown = towns[tindex]; - } + void _setPicker(int column, int index) { + final next = _selection.select(column, index); + setState(() { + for (var downstream = column + 1; + downstream < next.columnCount; + downstream++) { + scrollCtrl[downstream].jumpToItem(next.positions[downstream]); } - } - - provinceScrollCtrl = FixedExtentScrollController(initialItem: pindex); - cityScrollCtrl = FixedExtentScrollController(initialItem: cindex); - townScrollCtrl = FixedExtentScrollController(initialItem: tindex); - } - - void _setProvince(int index) { - String selectedProvince = provinces[index]; - // debugPrint('longer >>> index:$index _currentProvince:$_currentProvince selectedProvince:$selectedProvince '); - - if (_currentProvince != selectedProvince) { - setState(() { - _currentProvince = selectedProvince; - - cities = Address.getCities(selectedProvince); - // debugPrint('longer >>> 返回的城市数据:$cities'); - - _currentCity = cities[0]['name']; - cityScrollCtrl.jumpToItem(0); - if (hasTown) { - towns = Address.getTowns(cities[0]['cityCode']); - _currentTown = towns[0]; - townScrollCtrl.jumpToItem(0); - } - }); - - _notifyLocationChanged(); - } - } - - void _setCity(int index) { - index = cities.length > index ? index : 0; - String selectedCity = cities[index]['name']; - if (_currentCity != selectedCity) { - setState(() { - _currentCity = selectedCity; - if (hasTown) { - towns = Address.getTowns(cities[index]['cityCode']); - _currentTown = towns.isNotEmpty ? towns[0] : ''; - townScrollCtrl.jumpToItem(0); - } - }); - - _notifyLocationChanged(); - } - } - - void _setTown(int index) { - index = towns.length > index ? index : 0; - String selectedTown = towns[index]; - if (_currentTown != selectedTown) { - _currentTown = selectedTown; - _notifyLocationChanged(); - } - } - - void _notifyLocationChanged() { - widget.route.onChanged?.call(_currentProvince, _currentCity, _currentTown); + }); + _notify(widget.route.onChanged); } - double _pickerFontSize(String text) { - double ratio = hasTown ? 0.0 : 2.0; - if (text.length <= 6) { - return 18.0; - } else if (text.length < 9) { - return 16.0 + ratio; - } else if (text.length < 13) { - return 12.0 + ratio; - } else { - return 10.0 + ratio; - } - } - - Widget _renderPickerView() { - Widget itemView = _renderItemView(); - - if (!_pickerStyle.showTitleBar && _pickerStyle.menu == null) { - return itemView; - } - List viewList = []; - if (_pickerStyle.showTitleBar) { - viewList.add(_titleView()); - } - if (_pickerStyle.menu != null) { - viewList.add(_pickerStyle.menu!); - } - viewList.add(itemView); - - return Column(children: viewList); + void _notify(AddressCallback? callback) { + if (callback == null) return; + final values = _selection.state.selection; + callback( + values[0] as String, + values[1] as String, + values.length > 2 ? values[2] as String : null, + ); } Widget _renderItemView() { - return Container( - height: _pickerStyle.pickerHeight, - color: _pickerStyle.backgroundColor, - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(8.0), - child: CupertinoPicker.builder( - scrollController: provinceScrollCtrl, - selectionOverlay: _pickerStyle.itemOverlay, - itemExtent: _pickerStyle.pickerItemHeight, - onSelectedItemChanged: (int index) { - _setProvince(index); - }, - childCount: Address.provinces.length, - itemBuilder: (_, index) { - String text = Address.provinces[index]; - return Align( - alignment: Alignment.center, - child: Text( - text, - style: TextStyle( - color: _pickerStyle.textColor, - fontSize: - _pickerStyle.textSize ?? _pickerFontSize(text), - ), - textAlign: TextAlign.start, - ), - ); - }, - ), - ), - ), - Expanded( - child: Container( - padding: EdgeInsets.all(8.0), - child: CupertinoPicker.builder( - scrollController: cityScrollCtrl, - selectionOverlay: _pickerStyle.itemOverlay, - itemExtent: _pickerStyle.pickerItemHeight, - onSelectedItemChanged: (int index) { - _setCity(index); - }, - childCount: cities.length, - itemBuilder: (_, index) { - String text = cities[index]['name']; - return Align( - alignment: Alignment.center, - child: Text( - text, - style: TextStyle( - color: _pickerStyle.textColor, - fontSize: _pickerStyle.textSize ?? _pickerFontSize(text), - ), - textAlign: TextAlign.start, - ), - ); - }, - ), - ), - ), - hasTown - ? Expanded( - child: Container( - padding: EdgeInsets.all(8.0), - child: CupertinoPicker.builder( - scrollController: townScrollCtrl, - selectionOverlay: _pickerStyle.itemOverlay, - itemExtent: _pickerStyle.pickerItemHeight, - onSelectedItemChanged: (int index) { - _setTown(index); - }, - childCount: towns.length, - itemBuilder: (_, index) { - String text = towns[index]; - return Align( - alignment: Alignment.center, - child: Text( - text, - style: TextStyle( - color: _pickerStyle.textColor, - fontSize: _pickerStyle.textSize ?? _pickerFontSize(text), - ), - textAlign: TextAlign.start, - ), - ); - }, - ), - ), - ) - : SizedBox(), - ], + return Row( + children: List.generate( + _selection.state.columnCount, + pickerView, ), ); } - // 选择器上面的view - Widget _titleView() { - return Container( - height: _pickerStyle.pickerTitleHeight, - decoration: _pickerStyle.headDecoration, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 取消按钮 - InkWell( - onTap: () => Navigator.pop(context, false), - child: _pickerStyle.cancelButton, - ), - - /// 标题 - Expanded(child: _pickerStyle.title), - - /// 确认按钮 - InkWell( - onTap: () { - widget.route.onConfirm?.call( - _currentProvince, - _currentCity, - _currentTown, - ); - Navigator.pop(context, true); - }, - child: _pickerStyle.commitButton, - ), - ], + Widget pickerView(int column) { + final values = _selection.state.column(column); + return Expanded( + child: Container( + padding: const EdgeInsets.all(8), + child: CupertinoPicker.builder( + scrollController: scrollCtrl[column], + selectionOverlay: _pickerStyle.itemOverlay, + itemExtent: _pickerStyle.pickerItemHeight, + onSelectedItemChanged: (index) => _setPicker(column, index), + childCount: values.length, + itemBuilder: (_, index) { + final text = values[index] as String; + return Semantics( + label: text, + child: Align( + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + color: _pickerStyle.textColor, + fontSize: _pickerStyle.textSize ?? 18.0, + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + ); + }, + ), ), ); } } - -class _BottomPickerLayout extends SingleChildLayoutDelegate { - _BottomPickerLayout(this.progress, this.pickerStyle); - - final double progress; - final PickerStyle pickerStyle; - - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - double maxHeight = pickerStyle.pickerHeight; - if (pickerStyle.showTitleBar) { - maxHeight += pickerStyle.pickerTitleHeight; - } - if (pickerStyle.menu != null) { - maxHeight += pickerStyle.menuHeight; - } - - return BoxConstraints( - minWidth: constraints.maxWidth, - maxWidth: constraints.maxWidth, - minHeight: 0.0, - maxHeight: maxHeight, - ); - } - - @override - Offset getPositionForChild(Size size, Size childSize) { - double height = size.height - childSize.height * progress; - return Offset(0.0, height); - } - - @override - bool shouldRelayout(_BottomPickerLayout oldDelegate) { - return progress != oldDelegate.progress; - } -} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..47bd3b7 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,181 @@ +{ + "@@locale": "en", + + "cancel": "Cancel", + "@cancel": { + "description": "Cancel button text" + }, + "confirm": "Confirm", + "@confirm": { + "description": "Confirm button text" + }, + "year": "Year", + "@year": { + "description": "Year unit" + }, + "month": "Month", + "@month": { + "description": "Month unit" + }, + "day": "Day", + "@day": { + "description": "Day unit" + }, + "hour": "Hour", + "@hour": { + "description": "Hour unit" + }, + "minute": "Minute", + "@minute": { + "description": "Minute unit" + }, + "second": "Second", + "@second": { + "description": "Second unit" + }, + "all": "All", + "@all": { + "description": "All option in address picker" + }, + + "sexAny": "Any", + "@sexAny": { + "description": "Any gender option" + }, + "sexMale": "Male", + "@sexMale": { + "description": "Male gender option" + }, + "sexFemale": "Female", + "@sexFemale": { + "description": "Female gender option" + }, + + "educationBelowHighSchool": "Below High School", + "@educationBelowHighSchool": { + "description": "Education level below high school" + }, + "educationHighSchool": "High School", + "@educationHighSchool": { + "description": "High school education" + }, + "educationAssociate": "Associate", + "@educationAssociate": { + "description": "Associate degree" + }, + "educationBachelor": "Bachelor", + "@educationBachelor": { + "description": "Bachelor degree" + }, + "educationMaster": "Master", + "@educationMaster": { + "description": "Master degree" + }, + "educationPhd": "PhD", + "@educationPhd": { + "description": "Doctorate degree" + }, + "educationPostdoc": "Postdoc", + "@educationPostdoc": { + "description": "Postdoctoral" + }, + "educationOther": "Other", + "@educationOther": { + "description": "Other education" + }, + + "subjectChinese": "Chinese", + "subjectMath": "Math", + "subjectEnglish": "English", + "subjectPhysics": "Physics", + "subjectChemistry": "Chemistry", + "subjectBiology": "Biology", + "subjectPolitics": "Politics", + "subjectGeography": "Geography", + "subjectHistory": "History", + + "constellationAquarius": "Aquarius", + "constellationPisces": "Pisces", + "constellationAries": "Aries", + "constellationTaurus": "Taurus", + "constellationGemini": "Gemini", + "constellationCancer": "Cancer", + "constellationLeo": "Leo", + "constellationVirgo": "Virgo", + "constellationLibra": "Libra", + "constellationScorpio": "Scorpio", + "constellationSagittarius": "Sagittarius", + "constellationCapricorn": "Capricorn", + + "zodiacRat": "Rat", + "zodiacOx": "Ox", + "zodiacTiger": "Tiger", + "zodiacRabbit": "Rabbit", + "zodiacDragon": "Dragon", + "zodiacSnake": "Snake", + "zodiacHorse": "Horse", + "zodiacGoat": "Goat", + "zodiacMonkey": "Monkey", + "zodiacRooster": "Rooster", + "zodiacDog": "Dog", + "zodiacPig": "Pig", + + "ethnicityHan": "Han", + "ethnicityMongol": "Mongol", + "ethnicityHui": "Hui", + "ethnicityTibetan": "Tibetan", + "ethnicityUygur": "Uygur", + "ethnicityMiao": "Miao", + "ethnicityYi": "Yi", + "ethnicityZhuang": "Zhuang", + "ethnicityBouyei": "Bouyei", + "ethnicityKorean": "Korean", + "ethnicityManchu": "Manchu", + "ethnicityDong": "Dong", + "ethnicityYao": "Yao", + "ethnicityBai": "Bai", + "ethnicityTujia": "Tujia", + "ethnicityHani": "Hani", + "ethnicityKazak": "Kazak", + "ethnicityDai": "Dai", + "ethnicityLi": "Li", + "ethnicityLisu": "Lisu", + "ethnicityVa": "Va", + "ethnicityShe": "She", + "ethnicityGaoshan": "Gaoshan", + "ethnicityLahu": "Lahu", + "ethnicityShui": "Shui", + "ethnicityDongxiang": "Dongxiang", + "ethnicityNaxi": "Naxi", + "ethnicityJingpo": "Jingpo", + "ethnicityKirgiz": "Kirgiz", + "ethnicityTu": "Tu", + "ethnicityDaur": "Daur", + "ethnicityMulao": "Mulao", + "ethnicityQiang": "Qiang", + "ethnicityBlang": "Blang", + "ethnicitySalar": "Salar", + "ethnicityMaonan": "Maonan", + "ethnicityGelao": "Gelao", + "ethnicityXibe": "Xibe", + "ethnicityAchang": "Achang", + "ethnicityPumi": "Pumi", + "ethnicityTajik": "Tajik", + "ethnicityNu": "Nu", + "ethnicityUzbek": "Uzbek", + "ethnicityRussian": "Russian", + "ethnicityEwenki": "Ewenki", + "ethnicityDeang": "Deang", + "ethnicityBonan": "Bonan", + "ethnicityYugur": "Yugur", + "ethnicityGin": "Gin", + "ethnicityTatar": "Tatar", + "ethnicityDerung": "Derung", + "ethnicityOroqen": "Oroqen", + "ethnicityHezhen": "Hezhen", + "ethnicityMonba": "Monba", + "ethnicityLhoba": "Lhoba", + "ethnicityJino": "Jino", + "ethnicityOther": "Other", + "ethnicityForeignBornChinese": "Foreign-born Chinese" +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb new file mode 100644 index 0000000..21b573d --- /dev/null +++ b/lib/l10n/app_zh.arb @@ -0,0 +1,121 @@ +{ + "@@locale": "zh", + + "cancel": "取消", + "confirm": "确定", + "year": "年", + "month": "月", + "day": "日", + "hour": "时", + "minute": "分", + "second": "秒", + "all": "全部", + + "sexAny": "不限", + "sexMale": "男", + "sexFemale": "女", + + "educationBelowHighSchool": "高中以下", + "educationHighSchool": "高中", + "educationAssociate": "大专", + "educationBachelor": "本科", + "educationMaster": "硕士", + "educationPhd": "博士", + "educationPostdoc": "博士后", + "educationOther": "其它", + + "subjectChinese": "语文", + "subjectMath": "数学", + "subjectEnglish": "英语", + "subjectPhysics": "物理", + "subjectChemistry": "化学", + "subjectBiology": "生物", + "subjectPolitics": "政治", + "subjectGeography": "地理", + "subjectHistory": "历史", + + "constellationAquarius": "水瓶座", + "constellationPisces": "双鱼座", + "constellationAries": "白羊座", + "constellationTaurus": "金牛座", + "constellationGemini": "双子座", + "constellationCancer": "巨蟹座", + "constellationLeo": "狮子座", + "constellationVirgo": "处女座", + "constellationLibra": "天秤座", + "constellationScorpio": "天蝎座", + "constellationSagittarius": "射手座", + "constellationCapricorn": "摩羯座", + + "zodiacRat": "鼠", + "zodiacOx": "牛", + "zodiacTiger": "虎", + "zodiacRabbit": "兔", + "zodiacDragon": "龙", + "zodiacSnake": "蛇", + "zodiacHorse": "马", + "zodiacGoat": "羊", + "zodiacMonkey": "猴", + "zodiacRooster": "鸡", + "zodiacDog": "狗", + "zodiacPig": "猪", + + "ethnicityHan": "汉族", + "ethnicityMongol": "蒙古族", + "ethnicityHui": "回族", + "ethnicityTibetan": "藏族", + "ethnicityUygur": "维吾尔族", + "ethnicityMiao": "苗族", + "ethnicityYi": "彝族", + "ethnicityZhuang": "壮族", + "ethnicityBouyei": "布依族", + "ethnicityKorean": "朝鲜族", + "ethnicityManchu": "满族", + "ethnicityDong": "侗族", + "ethnicityYao": "瑶族", + "ethnicityBai": "白族", + "ethnicityTujia": "土家族", + "ethnicityHani": "哈尼族", + "ethnicityKazak": "哈萨克族", + "ethnicityDai": "傣族", + "ethnicityLi": "黎族", + "ethnicityLisu": "傈僳族", + "ethnicityVa": "佤族", + "ethnicityShe": "畲族", + "ethnicityGaoshan": "高山族", + "ethnicityLahu": "拉祜族", + "ethnicityShui": "水族", + "ethnicityDongxiang": "东乡族", + "ethnicityNaxi": "纳西族", + "ethnicityJingpo": "景颇族", + "ethnicityKirgiz": "柯尔克孜族", + "ethnicityTu": "土族", + "ethnicityDaur": "达斡尔族", + "ethnicityMulao": "仫佬族", + "ethnicityQiang": "羌族", + "ethnicityBlang": "布朗族", + "ethnicitySalar": "撒拉族", + "ethnicityMaonan": "毛难族", + "ethnicityGelao": "仡佬族", + "ethnicityXibe": "锡伯族", + "ethnicityAchang": "阿昌族", + "ethnicityPumi": "普米族", + "ethnicityTajik": "塔吉克族", + "ethnicityNu": "怒族", + "ethnicityUzbek": "乌孜别克族", + "ethnicityRussian": "俄罗斯族", + "ethnicityEwenki": "鄂温克族", + "ethnicityDeang": "崩龙族", + "ethnicityBonan": "保安族", + "ethnicityYugur": "裕固族", + "ethnicityGin": "京族", + "ethnicityTatar": "塔塔尔族", + "ethnicityDerung": "独龙族", + "ethnicityOroqen": "鄂伦春族", + "ethnicityHezhen": "赫哲族", + "ethnicityMonba": "门巴族", + "ethnicityLhoba": "珞巴族", + "ethnicityJino": "基诺族", + "ethnicityOther": "其他", + "ethnicityForeignBornChinese": "外国血统中国人士" +} \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart new file mode 100644 index 0000000..ac415d4 --- /dev/null +++ b/lib/l10n/generated/app_localizations.dart @@ -0,0 +1,795 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('zh') + ]; + + /// Cancel button text + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// Confirm button text + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirm; + + /// Year unit + /// + /// In en, this message translates to: + /// **'Year'** + String get year; + + /// Month unit + /// + /// In en, this message translates to: + /// **'Month'** + String get month; + + /// Day unit + /// + /// In en, this message translates to: + /// **'Day'** + String get day; + + /// Hour unit + /// + /// In en, this message translates to: + /// **'Hour'** + String get hour; + + /// Minute unit + /// + /// In en, this message translates to: + /// **'Minute'** + String get minute; + + /// Second unit + /// + /// In en, this message translates to: + /// **'Second'** + String get second; + + /// All option in address picker + /// + /// In en, this message translates to: + /// **'All'** + String get all; + + /// Any gender option + /// + /// In en, this message translates to: + /// **'Any'** + String get sexAny; + + /// Male gender option + /// + /// In en, this message translates to: + /// **'Male'** + String get sexMale; + + /// Female gender option + /// + /// In en, this message translates to: + /// **'Female'** + String get sexFemale; + + /// Education level below high school + /// + /// In en, this message translates to: + /// **'Below High School'** + String get educationBelowHighSchool; + + /// High school education + /// + /// In en, this message translates to: + /// **'High School'** + String get educationHighSchool; + + /// Associate degree + /// + /// In en, this message translates to: + /// **'Associate'** + String get educationAssociate; + + /// Bachelor degree + /// + /// In en, this message translates to: + /// **'Bachelor'** + String get educationBachelor; + + /// Master degree + /// + /// In en, this message translates to: + /// **'Master'** + String get educationMaster; + + /// Doctorate degree + /// + /// In en, this message translates to: + /// **'PhD'** + String get educationPhd; + + /// Postdoctoral + /// + /// In en, this message translates to: + /// **'Postdoc'** + String get educationPostdoc; + + /// Other education + /// + /// In en, this message translates to: + /// **'Other'** + String get educationOther; + + /// No description provided for @subjectChinese. + /// + /// In en, this message translates to: + /// **'Chinese'** + String get subjectChinese; + + /// No description provided for @subjectMath. + /// + /// In en, this message translates to: + /// **'Math'** + String get subjectMath; + + /// No description provided for @subjectEnglish. + /// + /// In en, this message translates to: + /// **'English'** + String get subjectEnglish; + + /// No description provided for @subjectPhysics. + /// + /// In en, this message translates to: + /// **'Physics'** + String get subjectPhysics; + + /// No description provided for @subjectChemistry. + /// + /// In en, this message translates to: + /// **'Chemistry'** + String get subjectChemistry; + + /// No description provided for @subjectBiology. + /// + /// In en, this message translates to: + /// **'Biology'** + String get subjectBiology; + + /// No description provided for @subjectPolitics. + /// + /// In en, this message translates to: + /// **'Politics'** + String get subjectPolitics; + + /// No description provided for @subjectGeography. + /// + /// In en, this message translates to: + /// **'Geography'** + String get subjectGeography; + + /// No description provided for @subjectHistory. + /// + /// In en, this message translates to: + /// **'History'** + String get subjectHistory; + + /// No description provided for @constellationAquarius. + /// + /// In en, this message translates to: + /// **'Aquarius'** + String get constellationAquarius; + + /// No description provided for @constellationPisces. + /// + /// In en, this message translates to: + /// **'Pisces'** + String get constellationPisces; + + /// No description provided for @constellationAries. + /// + /// In en, this message translates to: + /// **'Aries'** + String get constellationAries; + + /// No description provided for @constellationTaurus. + /// + /// In en, this message translates to: + /// **'Taurus'** + String get constellationTaurus; + + /// No description provided for @constellationGemini. + /// + /// In en, this message translates to: + /// **'Gemini'** + String get constellationGemini; + + /// No description provided for @constellationCancer. + /// + /// In en, this message translates to: + /// **'Cancer'** + String get constellationCancer; + + /// No description provided for @constellationLeo. + /// + /// In en, this message translates to: + /// **'Leo'** + String get constellationLeo; + + /// No description provided for @constellationVirgo. + /// + /// In en, this message translates to: + /// **'Virgo'** + String get constellationVirgo; + + /// No description provided for @constellationLibra. + /// + /// In en, this message translates to: + /// **'Libra'** + String get constellationLibra; + + /// No description provided for @constellationScorpio. + /// + /// In en, this message translates to: + /// **'Scorpio'** + String get constellationScorpio; + + /// No description provided for @constellationSagittarius. + /// + /// In en, this message translates to: + /// **'Sagittarius'** + String get constellationSagittarius; + + /// No description provided for @constellationCapricorn. + /// + /// In en, this message translates to: + /// **'Capricorn'** + String get constellationCapricorn; + + /// No description provided for @zodiacRat. + /// + /// In en, this message translates to: + /// **'Rat'** + String get zodiacRat; + + /// No description provided for @zodiacOx. + /// + /// In en, this message translates to: + /// **'Ox'** + String get zodiacOx; + + /// No description provided for @zodiacTiger. + /// + /// In en, this message translates to: + /// **'Tiger'** + String get zodiacTiger; + + /// No description provided for @zodiacRabbit. + /// + /// In en, this message translates to: + /// **'Rabbit'** + String get zodiacRabbit; + + /// No description provided for @zodiacDragon. + /// + /// In en, this message translates to: + /// **'Dragon'** + String get zodiacDragon; + + /// No description provided for @zodiacSnake. + /// + /// In en, this message translates to: + /// **'Snake'** + String get zodiacSnake; + + /// No description provided for @zodiacHorse. + /// + /// In en, this message translates to: + /// **'Horse'** + String get zodiacHorse; + + /// No description provided for @zodiacGoat. + /// + /// In en, this message translates to: + /// **'Goat'** + String get zodiacGoat; + + /// No description provided for @zodiacMonkey. + /// + /// In en, this message translates to: + /// **'Monkey'** + String get zodiacMonkey; + + /// No description provided for @zodiacRooster. + /// + /// In en, this message translates to: + /// **'Rooster'** + String get zodiacRooster; + + /// No description provided for @zodiacDog. + /// + /// In en, this message translates to: + /// **'Dog'** + String get zodiacDog; + + /// No description provided for @zodiacPig. + /// + /// In en, this message translates to: + /// **'Pig'** + String get zodiacPig; + + /// No description provided for @ethnicityHan. + /// + /// In en, this message translates to: + /// **'Han'** + String get ethnicityHan; + + /// No description provided for @ethnicityMongol. + /// + /// In en, this message translates to: + /// **'Mongol'** + String get ethnicityMongol; + + /// No description provided for @ethnicityHui. + /// + /// In en, this message translates to: + /// **'Hui'** + String get ethnicityHui; + + /// No description provided for @ethnicityTibetan. + /// + /// In en, this message translates to: + /// **'Tibetan'** + String get ethnicityTibetan; + + /// No description provided for @ethnicityUygur. + /// + /// In en, this message translates to: + /// **'Uygur'** + String get ethnicityUygur; + + /// No description provided for @ethnicityMiao. + /// + /// In en, this message translates to: + /// **'Miao'** + String get ethnicityMiao; + + /// No description provided for @ethnicityYi. + /// + /// In en, this message translates to: + /// **'Yi'** + String get ethnicityYi; + + /// No description provided for @ethnicityZhuang. + /// + /// In en, this message translates to: + /// **'Zhuang'** + String get ethnicityZhuang; + + /// No description provided for @ethnicityBouyei. + /// + /// In en, this message translates to: + /// **'Bouyei'** + String get ethnicityBouyei; + + /// No description provided for @ethnicityKorean. + /// + /// In en, this message translates to: + /// **'Korean'** + String get ethnicityKorean; + + /// No description provided for @ethnicityManchu. + /// + /// In en, this message translates to: + /// **'Manchu'** + String get ethnicityManchu; + + /// No description provided for @ethnicityDong. + /// + /// In en, this message translates to: + /// **'Dong'** + String get ethnicityDong; + + /// No description provided for @ethnicityYao. + /// + /// In en, this message translates to: + /// **'Yao'** + String get ethnicityYao; + + /// No description provided for @ethnicityBai. + /// + /// In en, this message translates to: + /// **'Bai'** + String get ethnicityBai; + + /// No description provided for @ethnicityTujia. + /// + /// In en, this message translates to: + /// **'Tujia'** + String get ethnicityTujia; + + /// No description provided for @ethnicityHani. + /// + /// In en, this message translates to: + /// **'Hani'** + String get ethnicityHani; + + /// No description provided for @ethnicityKazak. + /// + /// In en, this message translates to: + /// **'Kazak'** + String get ethnicityKazak; + + /// No description provided for @ethnicityDai. + /// + /// In en, this message translates to: + /// **'Dai'** + String get ethnicityDai; + + /// No description provided for @ethnicityLi. + /// + /// In en, this message translates to: + /// **'Li'** + String get ethnicityLi; + + /// No description provided for @ethnicityLisu. + /// + /// In en, this message translates to: + /// **'Lisu'** + String get ethnicityLisu; + + /// No description provided for @ethnicityVa. + /// + /// In en, this message translates to: + /// **'Va'** + String get ethnicityVa; + + /// No description provided for @ethnicityShe. + /// + /// In en, this message translates to: + /// **'She'** + String get ethnicityShe; + + /// No description provided for @ethnicityGaoshan. + /// + /// In en, this message translates to: + /// **'Gaoshan'** + String get ethnicityGaoshan; + + /// No description provided for @ethnicityLahu. + /// + /// In en, this message translates to: + /// **'Lahu'** + String get ethnicityLahu; + + /// No description provided for @ethnicityShui. + /// + /// In en, this message translates to: + /// **'Shui'** + String get ethnicityShui; + + /// No description provided for @ethnicityDongxiang. + /// + /// In en, this message translates to: + /// **'Dongxiang'** + String get ethnicityDongxiang; + + /// No description provided for @ethnicityNaxi. + /// + /// In en, this message translates to: + /// **'Naxi'** + String get ethnicityNaxi; + + /// No description provided for @ethnicityJingpo. + /// + /// In en, this message translates to: + /// **'Jingpo'** + String get ethnicityJingpo; + + /// No description provided for @ethnicityKirgiz. + /// + /// In en, this message translates to: + /// **'Kirgiz'** + String get ethnicityKirgiz; + + /// No description provided for @ethnicityTu. + /// + /// In en, this message translates to: + /// **'Tu'** + String get ethnicityTu; + + /// No description provided for @ethnicityDaur. + /// + /// In en, this message translates to: + /// **'Daur'** + String get ethnicityDaur; + + /// No description provided for @ethnicityMulao. + /// + /// In en, this message translates to: + /// **'Mulao'** + String get ethnicityMulao; + + /// No description provided for @ethnicityQiang. + /// + /// In en, this message translates to: + /// **'Qiang'** + String get ethnicityQiang; + + /// No description provided for @ethnicityBlang. + /// + /// In en, this message translates to: + /// **'Blang'** + String get ethnicityBlang; + + /// No description provided for @ethnicitySalar. + /// + /// In en, this message translates to: + /// **'Salar'** + String get ethnicitySalar; + + /// No description provided for @ethnicityMaonan. + /// + /// In en, this message translates to: + /// **'Maonan'** + String get ethnicityMaonan; + + /// No description provided for @ethnicityGelao. + /// + /// In en, this message translates to: + /// **'Gelao'** + String get ethnicityGelao; + + /// No description provided for @ethnicityXibe. + /// + /// In en, this message translates to: + /// **'Xibe'** + String get ethnicityXibe; + + /// No description provided for @ethnicityAchang. + /// + /// In en, this message translates to: + /// **'Achang'** + String get ethnicityAchang; + + /// No description provided for @ethnicityPumi. + /// + /// In en, this message translates to: + /// **'Pumi'** + String get ethnicityPumi; + + /// No description provided for @ethnicityTajik. + /// + /// In en, this message translates to: + /// **'Tajik'** + String get ethnicityTajik; + + /// No description provided for @ethnicityNu. + /// + /// In en, this message translates to: + /// **'Nu'** + String get ethnicityNu; + + /// No description provided for @ethnicityUzbek. + /// + /// In en, this message translates to: + /// **'Uzbek'** + String get ethnicityUzbek; + + /// No description provided for @ethnicityRussian. + /// + /// In en, this message translates to: + /// **'Russian'** + String get ethnicityRussian; + + /// No description provided for @ethnicityEwenki. + /// + /// In en, this message translates to: + /// **'Ewenki'** + String get ethnicityEwenki; + + /// No description provided for @ethnicityDeang. + /// + /// In en, this message translates to: + /// **'Deang'** + String get ethnicityDeang; + + /// No description provided for @ethnicityBonan. + /// + /// In en, this message translates to: + /// **'Bonan'** + String get ethnicityBonan; + + /// No description provided for @ethnicityYugur. + /// + /// In en, this message translates to: + /// **'Yugur'** + String get ethnicityYugur; + + /// No description provided for @ethnicityGin. + /// + /// In en, this message translates to: + /// **'Gin'** + String get ethnicityGin; + + /// No description provided for @ethnicityTatar. + /// + /// In en, this message translates to: + /// **'Tatar'** + String get ethnicityTatar; + + /// No description provided for @ethnicityDerung. + /// + /// In en, this message translates to: + /// **'Derung'** + String get ethnicityDerung; + + /// No description provided for @ethnicityOroqen. + /// + /// In en, this message translates to: + /// **'Oroqen'** + String get ethnicityOroqen; + + /// No description provided for @ethnicityHezhen. + /// + /// In en, this message translates to: + /// **'Hezhen'** + String get ethnicityHezhen; + + /// No description provided for @ethnicityMonba. + /// + /// In en, this message translates to: + /// **'Monba'** + String get ethnicityMonba; + + /// No description provided for @ethnicityLhoba. + /// + /// In en, this message translates to: + /// **'Lhoba'** + String get ethnicityLhoba; + + /// No description provided for @ethnicityJino. + /// + /// In en, this message translates to: + /// **'Jino'** + String get ethnicityJino; + + /// No description provided for @ethnicityOther. + /// + /// In en, this message translates to: + /// **'Other'** + String get ethnicityOther; + + /// No description provided for @ethnicityForeignBornChinese. + /// + /// In en, this message translates to: + /// **'Foreign-born Chinese'** + String get ethnicityForeignBornChinese; +} + +class _AppLocalizationsDelegate extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': return AppLocalizationsEn(); + case 'zh': return AppLocalizationsZh(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.' + ); +} diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart new file mode 100644 index 0000000..b4020a2 --- /dev/null +++ b/lib/l10n/generated/app_localizations_en.dart @@ -0,0 +1,343 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get cancel => 'Cancel'; + + @override + String get confirm => 'Confirm'; + + @override + String get year => 'Year'; + + @override + String get month => 'Month'; + + @override + String get day => 'Day'; + + @override + String get hour => 'Hour'; + + @override + String get minute => 'Minute'; + + @override + String get second => 'Second'; + + @override + String get all => 'All'; + + @override + String get sexAny => 'Any'; + + @override + String get sexMale => 'Male'; + + @override + String get sexFemale => 'Female'; + + @override + String get educationBelowHighSchool => 'Below High School'; + + @override + String get educationHighSchool => 'High School'; + + @override + String get educationAssociate => 'Associate'; + + @override + String get educationBachelor => 'Bachelor'; + + @override + String get educationMaster => 'Master'; + + @override + String get educationPhd => 'PhD'; + + @override + String get educationPostdoc => 'Postdoc'; + + @override + String get educationOther => 'Other'; + + @override + String get subjectChinese => 'Chinese'; + + @override + String get subjectMath => 'Math'; + + @override + String get subjectEnglish => 'English'; + + @override + String get subjectPhysics => 'Physics'; + + @override + String get subjectChemistry => 'Chemistry'; + + @override + String get subjectBiology => 'Biology'; + + @override + String get subjectPolitics => 'Politics'; + + @override + String get subjectGeography => 'Geography'; + + @override + String get subjectHistory => 'History'; + + @override + String get constellationAquarius => 'Aquarius'; + + @override + String get constellationPisces => 'Pisces'; + + @override + String get constellationAries => 'Aries'; + + @override + String get constellationTaurus => 'Taurus'; + + @override + String get constellationGemini => 'Gemini'; + + @override + String get constellationCancer => 'Cancer'; + + @override + String get constellationLeo => 'Leo'; + + @override + String get constellationVirgo => 'Virgo'; + + @override + String get constellationLibra => 'Libra'; + + @override + String get constellationScorpio => 'Scorpio'; + + @override + String get constellationSagittarius => 'Sagittarius'; + + @override + String get constellationCapricorn => 'Capricorn'; + + @override + String get zodiacRat => 'Rat'; + + @override + String get zodiacOx => 'Ox'; + + @override + String get zodiacTiger => 'Tiger'; + + @override + String get zodiacRabbit => 'Rabbit'; + + @override + String get zodiacDragon => 'Dragon'; + + @override + String get zodiacSnake => 'Snake'; + + @override + String get zodiacHorse => 'Horse'; + + @override + String get zodiacGoat => 'Goat'; + + @override + String get zodiacMonkey => 'Monkey'; + + @override + String get zodiacRooster => 'Rooster'; + + @override + String get zodiacDog => 'Dog'; + + @override + String get zodiacPig => 'Pig'; + + @override + String get ethnicityHan => 'Han'; + + @override + String get ethnicityMongol => 'Mongol'; + + @override + String get ethnicityHui => 'Hui'; + + @override + String get ethnicityTibetan => 'Tibetan'; + + @override + String get ethnicityUygur => 'Uygur'; + + @override + String get ethnicityMiao => 'Miao'; + + @override + String get ethnicityYi => 'Yi'; + + @override + String get ethnicityZhuang => 'Zhuang'; + + @override + String get ethnicityBouyei => 'Bouyei'; + + @override + String get ethnicityKorean => 'Korean'; + + @override + String get ethnicityManchu => 'Manchu'; + + @override + String get ethnicityDong => 'Dong'; + + @override + String get ethnicityYao => 'Yao'; + + @override + String get ethnicityBai => 'Bai'; + + @override + String get ethnicityTujia => 'Tujia'; + + @override + String get ethnicityHani => 'Hani'; + + @override + String get ethnicityKazak => 'Kazak'; + + @override + String get ethnicityDai => 'Dai'; + + @override + String get ethnicityLi => 'Li'; + + @override + String get ethnicityLisu => 'Lisu'; + + @override + String get ethnicityVa => 'Va'; + + @override + String get ethnicityShe => 'She'; + + @override + String get ethnicityGaoshan => 'Gaoshan'; + + @override + String get ethnicityLahu => 'Lahu'; + + @override + String get ethnicityShui => 'Shui'; + + @override + String get ethnicityDongxiang => 'Dongxiang'; + + @override + String get ethnicityNaxi => 'Naxi'; + + @override + String get ethnicityJingpo => 'Jingpo'; + + @override + String get ethnicityKirgiz => 'Kirgiz'; + + @override + String get ethnicityTu => 'Tu'; + + @override + String get ethnicityDaur => 'Daur'; + + @override + String get ethnicityMulao => 'Mulao'; + + @override + String get ethnicityQiang => 'Qiang'; + + @override + String get ethnicityBlang => 'Blang'; + + @override + String get ethnicitySalar => 'Salar'; + + @override + String get ethnicityMaonan => 'Maonan'; + + @override + String get ethnicityGelao => 'Gelao'; + + @override + String get ethnicityXibe => 'Xibe'; + + @override + String get ethnicityAchang => 'Achang'; + + @override + String get ethnicityPumi => 'Pumi'; + + @override + String get ethnicityTajik => 'Tajik'; + + @override + String get ethnicityNu => 'Nu'; + + @override + String get ethnicityUzbek => 'Uzbek'; + + @override + String get ethnicityRussian => 'Russian'; + + @override + String get ethnicityEwenki => 'Ewenki'; + + @override + String get ethnicityDeang => 'Deang'; + + @override + String get ethnicityBonan => 'Bonan'; + + @override + String get ethnicityYugur => 'Yugur'; + + @override + String get ethnicityGin => 'Gin'; + + @override + String get ethnicityTatar => 'Tatar'; + + @override + String get ethnicityDerung => 'Derung'; + + @override + String get ethnicityOroqen => 'Oroqen'; + + @override + String get ethnicityHezhen => 'Hezhen'; + + @override + String get ethnicityMonba => 'Monba'; + + @override + String get ethnicityLhoba => 'Lhoba'; + + @override + String get ethnicityJino => 'Jino'; + + @override + String get ethnicityOther => 'Other'; + + @override + String get ethnicityForeignBornChinese => 'Foreign-born Chinese'; +} diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart new file mode 100644 index 0000000..9884970 --- /dev/null +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -0,0 +1,343 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get cancel => '取消'; + + @override + String get confirm => '确定'; + + @override + String get year => '年'; + + @override + String get month => '月'; + + @override + String get day => '日'; + + @override + String get hour => '时'; + + @override + String get minute => '分'; + + @override + String get second => '秒'; + + @override + String get all => '全部'; + + @override + String get sexAny => '不限'; + + @override + String get sexMale => '男'; + + @override + String get sexFemale => '女'; + + @override + String get educationBelowHighSchool => '高中以下'; + + @override + String get educationHighSchool => '高中'; + + @override + String get educationAssociate => '大专'; + + @override + String get educationBachelor => '本科'; + + @override + String get educationMaster => '硕士'; + + @override + String get educationPhd => '博士'; + + @override + String get educationPostdoc => '博士后'; + + @override + String get educationOther => '其它'; + + @override + String get subjectChinese => '语文'; + + @override + String get subjectMath => '数学'; + + @override + String get subjectEnglish => '英语'; + + @override + String get subjectPhysics => '物理'; + + @override + String get subjectChemistry => '化学'; + + @override + String get subjectBiology => '生物'; + + @override + String get subjectPolitics => '政治'; + + @override + String get subjectGeography => '地理'; + + @override + String get subjectHistory => '历史'; + + @override + String get constellationAquarius => '水瓶座'; + + @override + String get constellationPisces => '双鱼座'; + + @override + String get constellationAries => '白羊座'; + + @override + String get constellationTaurus => '金牛座'; + + @override + String get constellationGemini => '双子座'; + + @override + String get constellationCancer => '巨蟹座'; + + @override + String get constellationLeo => '狮子座'; + + @override + String get constellationVirgo => '处女座'; + + @override + String get constellationLibra => '天秤座'; + + @override + String get constellationScorpio => '天蝎座'; + + @override + String get constellationSagittarius => '射手座'; + + @override + String get constellationCapricorn => '摩羯座'; + + @override + String get zodiacRat => '鼠'; + + @override + String get zodiacOx => '牛'; + + @override + String get zodiacTiger => '虎'; + + @override + String get zodiacRabbit => '兔'; + + @override + String get zodiacDragon => '龙'; + + @override + String get zodiacSnake => '蛇'; + + @override + String get zodiacHorse => '马'; + + @override + String get zodiacGoat => '羊'; + + @override + String get zodiacMonkey => '猴'; + + @override + String get zodiacRooster => '鸡'; + + @override + String get zodiacDog => '狗'; + + @override + String get zodiacPig => '猪'; + + @override + String get ethnicityHan => '汉族'; + + @override + String get ethnicityMongol => '蒙古族'; + + @override + String get ethnicityHui => '回族'; + + @override + String get ethnicityTibetan => '藏族'; + + @override + String get ethnicityUygur => '维吾尔族'; + + @override + String get ethnicityMiao => '苗族'; + + @override + String get ethnicityYi => '彝族'; + + @override + String get ethnicityZhuang => '壮族'; + + @override + String get ethnicityBouyei => '布依族'; + + @override + String get ethnicityKorean => '朝鲜族'; + + @override + String get ethnicityManchu => '满族'; + + @override + String get ethnicityDong => '侗族'; + + @override + String get ethnicityYao => '瑶族'; + + @override + String get ethnicityBai => '白族'; + + @override + String get ethnicityTujia => '土家族'; + + @override + String get ethnicityHani => '哈尼族'; + + @override + String get ethnicityKazak => '哈萨克族'; + + @override + String get ethnicityDai => '傣族'; + + @override + String get ethnicityLi => '黎族'; + + @override + String get ethnicityLisu => '傈僳族'; + + @override + String get ethnicityVa => '佤族'; + + @override + String get ethnicityShe => '畲族'; + + @override + String get ethnicityGaoshan => '高山族'; + + @override + String get ethnicityLahu => '拉祜族'; + + @override + String get ethnicityShui => '水族'; + + @override + String get ethnicityDongxiang => '东乡族'; + + @override + String get ethnicityNaxi => '纳西族'; + + @override + String get ethnicityJingpo => '景颇族'; + + @override + String get ethnicityKirgiz => '柯尔克孜族'; + + @override + String get ethnicityTu => '土族'; + + @override + String get ethnicityDaur => '达斡尔族'; + + @override + String get ethnicityMulao => '仫佬族'; + + @override + String get ethnicityQiang => '羌族'; + + @override + String get ethnicityBlang => '布朗族'; + + @override + String get ethnicitySalar => '撒拉族'; + + @override + String get ethnicityMaonan => '毛难族'; + + @override + String get ethnicityGelao => '仡佬族'; + + @override + String get ethnicityXibe => '锡伯族'; + + @override + String get ethnicityAchang => '阿昌族'; + + @override + String get ethnicityPumi => '普米族'; + + @override + String get ethnicityTajik => '塔吉克族'; + + @override + String get ethnicityNu => '怒族'; + + @override + String get ethnicityUzbek => '乌孜别克族'; + + @override + String get ethnicityRussian => '俄罗斯族'; + + @override + String get ethnicityEwenki => '鄂温克族'; + + @override + String get ethnicityDeang => '崩龙族'; + + @override + String get ethnicityBonan => '保安族'; + + @override + String get ethnicityYugur => '裕固族'; + + @override + String get ethnicityGin => '京族'; + + @override + String get ethnicityTatar => '塔塔尔族'; + + @override + String get ethnicityDerung => '独龙族'; + + @override + String get ethnicityOroqen => '鄂伦春族'; + + @override + String get ethnicityHezhen => '赫哲族'; + + @override + String get ethnicityMonba => '门巴族'; + + @override + String get ethnicityLhoba => '珞巴族'; + + @override + String get ethnicityJino => '基诺族'; + + @override + String get ethnicityOther => '其他'; + + @override + String get ethnicityForeignBornChinese => '外国血统中国人士'; +} diff --git a/lib/l10n/l10n.yaml b/lib/l10n/l10n.yaml new file mode 100644 index 0000000..2c66bf9 --- /dev/null +++ b/lib/l10n/l10n.yaml @@ -0,0 +1,6 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +output-dir: lib/l10n/generated +synthetic-package: false \ No newline at end of file diff --git a/lib/more_pickers/route/multiple_link_picker_route.dart b/lib/more_pickers/route/multiple_link_picker_route.dart index a38a813..538bb6b 100644 --- a/lib/more_pickers/route/multiple_link_picker_route.dart +++ b/lib/more_pickers/route/multiple_link_picker_route.dart @@ -1,24 +1,28 @@ import 'dart:math'; + import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter_pickers/src/cascading_picker/cascading_selection.dart'; +import 'package:flutter_pickers/src/cascading_picker/nested_map_adapter.dart'; +import 'package:flutter_pickers/src/route/picker_popup_route.dart'; +import 'package:flutter_pickers/src/route/picker_sheet.dart'; import 'package:flutter_pickers/style/picker_style.dart'; typedef MultipleLinkCallback = Function(List res, List position); /// 多项选择器 /// 有关联 -class MultipleLinkPickerRoute extends PopupRoute { +class MultipleLinkPickerRoute extends PickerPopupRoute { MultipleLinkPickerRoute({ - required this.pickerStyle, + required super.pickerStyle, required this.data, required this.selectData, required this.columnNum, this.suffix, this.onChanged, this.onConfirm, - this.onCancel, - this.theme, - this.barrierLabel, + super.onCancel, + super.theme, + super.barrierLabel, super.settings, }); @@ -28,65 +32,21 @@ class MultipleLinkPickerRoute extends PopupRoute { final List? suffix; final MultipleLinkCallback? onChanged; final MultipleLinkCallback? onConfirm; - final Function(bool isCancel)? onCancel; - final ThemeData? theme; - - final PickerStyle pickerStyle; - - @override - Duration get transitionDuration => const Duration(milliseconds: 200); - - @override - bool get barrierDismissible => true; @override - bool didPop(T? result) { - if (result == null) { - onCancel?.call(false); - } else if (!(result as bool)) { - onCancel?.call(true); - } - return super.didPop(result); - } - - @override - final String? barrierLabel; - - @override - Color get barrierColor => Colors.black54; - - late AnimationController _animationController; - - @override - AnimationController createAnimationController() { - _animationController = BottomSheet.createAnimationController( - navigator!.overlay!, - ); - return _animationController; - } - - @override - Widget buildPage( + Widget buildPickerContent( BuildContext context, - Animation animation, - Animation secondaryAnimation, + PickerStyle resolvedStyle, + double safeAreaBottom, ) { - Widget bottomSheet = MediaQuery.removePadding( - context: context, - removeTop: true, - child: PickerContentView( - data: data, - columnNum: columnNum, - selectData: selectData, - pickerStyle: pickerStyle, - route: this, - ), + return PickerContentView( + data: data, + columnNum: columnNum, + selectData: selectData, + pickerStyle: resolvedStyle, + safeAreaBottom: safeAreaBottom, + route: this, ); - if (theme != null) { - bottomSheet = Theme(data: theme!, child: bottomSheet); - } - - return bottomSheet; } } @@ -97,6 +57,7 @@ class PickerContentView extends StatefulWidget { required this.columnNum, required this.pickerStyle, required this.selectData, + this.safeAreaBottom = 0.0, required this.route, }); @@ -105,6 +66,7 @@ class PickerContentView extends StatefulWidget { final List selectData; final MultipleLinkPickerRoute route; final PickerStyle pickerStyle; + final double safeAreaBottom; @override State createState() => _PickerState(); @@ -112,315 +74,108 @@ class PickerContentView extends StatefulWidget { class _PickerState extends State { late final PickerStyle _pickerStyle; - - // 没有数据时占位字符 - static const placeData = ''; - - /// 选中数据 - late List _selectData; - - /// 选中数据下标 - late List _selectDataPosition; - - /// 原始数据 - late final Map _data; - - /// 有多少列 - late final int _columnNum; - - /// 所有item 对应的数据 - late final List _columnData = []; - - AnimationController? controller; - Animation? animation; - - List scrollCtrl = []; - - // 选择器 高度 单独提出来,用来解决修改数据 不及时更新的BUG + late final CascadingSelectionModule _selection; + final List scrollCtrl = []; late double pickerItemHeight; @override void initState() { super.initState(); - _data = widget.data; - List mSelectData = widget.selectData; _pickerStyle = widget.pickerStyle; - _columnNum = widget.columnNum; pickerItemHeight = _pickerStyle.pickerItemHeight; - // 已选择器数据为准,因为初始化数据有可能和选择器对不上 - _selectData = []; - _selectDataPosition = []; - for (int i = 0; i < _columnNum; ++i) { - if (i >= mSelectData.length) { - _selectData.add(''); - } else { - _selectData.add(mSelectData[i]); - } - _selectDataPosition.add(0); + _selection = CascadingSelectionModule( + adapter: NestedMapAdapter( + data: widget.data, + columnCount: widget.columnNum, + ), + initial: widget.selectData, + ); + for (final position in _selection.state.positions) { + scrollCtrl.add(FixedExtentScrollController(initialItem: position)); } - - _init(mSelectData); } @override void dispose() { - for (var element in scrollCtrl) { - element.dispose(); + for (final controller in scrollCtrl) { + controller.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { - return GestureDetector( - child: AnimatedBuilder( - animation: widget.route.animation!, - builder: (BuildContext context, Widget? child) { - return ClipRect( - child: CustomSingleChildLayout( - delegate: _BottomPickerLayout( - widget.route.animation!.value, - pickerStyle: _pickerStyle, - ), - child: GestureDetector( - child: Material( - color: Colors.transparent, - child: _renderPickerView(), - ), - ), - ), - ); - }, - ), + return PickerSheet( + animation: widget.route.animation!, + style: _pickerStyle, + safeAreaBottom: widget.safeAreaBottom, + body: _renderItemView(), + onConfirm: () { + widget.route.onConfirm?.call( + List.from(_selection.state.selection), + List.from(_selection.state.positions), + ); + }, ); } - void _init(List mSelectData) { - int pindex; - scrollCtrl.clear(); - _columnData.clear(); - - for (int i = 0; i < _columnNum; ++i) { - pindex = 0; - - if (i == 0) { - /// 第一列 - pindex = _data.keys.toList().indexOf(_selectData[i]); - if (pindex < 0) { - _selectData[i] = _data.keys.first; - pindex = 0; - } - _selectDataPosition[i] = pindex; - - _columnData.add(_data.keys.toList()); - } else { - /// 其他列 - dynamic date = findNextData(i); - // print('longer 第$i列 >>> $date'); - - if (date is Map) { - pindex = date.keys.toList().indexOf(_selectData[i]); - if (pindex < 0) { - _selectData[i] = date.keys.first; - pindex = 0; - } - - _columnData.add(date.keys.toList()); - } else if (date is List) { - pindex = date.indexOf(_selectData[i]); - if (pindex < 0) { - _selectData[i] = date.first; - pindex = 0; - } - _columnData.add(date); - } else { - _selectData[i] = date; - pindex = 0; - - _columnData.add([date]); - } - - _selectDataPosition[i] = pindex; - } - - scrollCtrl.add(FixedExtentScrollController(initialItem: pindex)); - } - } - - /// [position] 变动的列 - /// [selectIndex] 对应列选中的index - /// [jump] 是否需要jumpToItem - void _setPicker(int position, int selectIndex, bool jump) { - // 得到新的选中的数据 - var selectValue = _columnData[position][selectIndex]; - // 更新选中数据 - _selectData[position] = selectValue; - _selectDataPosition[position] = selectIndex; - if (jump) { - scrollCtrl[position].jumpToItem(selectIndex); - } - - /// 如果不是最后一列 - /// 数据的变动都会造成剩下列的更新 - if (position < _columnNum - 1) { - // 先更新下一列所有数据 - // 如果这一列的所有数据都为空,下列直接也设为空数据(优化) - if (_columnData[position].length == 1 && - _columnData[position].first == placeData) { - _columnData[position + 1] = [placeData]; - } else { - _columnData[position + 1] = findColumnData(position + 1); - } - - // 再次递归 - _setPicker(position + 1, 0, true); - } else { - _notifyLocationChanged(); - } - } - - /// 找到对应位置的 下一列数据 - /// return map list other - dynamic findNextData(int position) { - dynamic nextData; - - for (int i = 0; i < position; i++) { - if (i == 0) { - // 肯定是map - nextData = _data[_selectData[0]]; - } else { - // 肯定是map - dynamic data = nextData[_selectData[i]]; - - if (data is Map) { - nextData = data; - } else if (data is List) { - nextData = data; - } else { - // 遍历到最后会返回该值 - nextData = [data]; - } - } - // print('longer i:$i >>> $nextData'); - - /// 如果数据 还没有到最后 就 已经不是Map - if (nextData is! Map && (i < position - 1)) { - return [placeData]; - } - } - - return nextData; - } - - /// 找到对应位置的数据 - /// 比如 position = 2; - /// 就是找到第2列数据 - /// return list - List findColumnData(int position) { - dynamic nextData; - for (int i = 0; i < position; i++) { - if (i == 0) { - // 肯定是map - nextData = _data[_selectData[0]]; - } else { - // print( - // 'longer 选中 >>> ${_selectData.join('-')} 当前选中: ${_selectData[i]}'); - // 肯定是map - dynamic data = nextData[_selectData[i]]; - - if (data is Map) { - nextData = data; - } else if (data is List) { - nextData = data; - } else { - // 遍历到最后会返回该值 - nextData = [data]; - } - } - // print('longer i:$i >>> $nextData'); - - /// 如果是map 并且是最后一列 返回他的key - if ((nextData is Map) && (i == position - 1)) { - return nextData.keys.toList(); - } - - /// 如果数据 还没有到最后 就 已经不是Map - if (nextData is! Map && (i < position - 1)) { - // print('longer2 第:$position列之前返回数据 >>> $nextData'); - return [placeData]; - } - } - - // print('longer 第:$position列返回数据 >>> $nextData'); - return nextData; - } - - void _notifyLocationChanged() { + void _setPicker(int column, int index) { + final next = _selection.select(column, index); setState(() { - /// FIX:https://github.com/flutter/flutter/issues/22999 + for (var downstream = column + 1; + downstream < next.columnCount; + downstream++) { + scrollCtrl[downstream].jumpToItem(next.positions[downstream]); + } + // Work around https://github.com/flutter/flutter/issues/22999. pickerItemHeight = _pickerStyle.pickerItemHeight - Random().nextDouble() / 100000000; }); - widget.route.onChanged?.call(_selectData, _selectDataPosition); - } - - Widget _renderPickerView() { - Widget itemView = _renderItemView(); - - if (!_pickerStyle.showTitleBar && _pickerStyle.menu == null) { - return itemView; - } - List viewList = []; - if (_pickerStyle.showTitleBar) { - viewList.add(_titleView()); - } - if (_pickerStyle.menu != null) { - viewList.add(_pickerStyle.menu!); - } - viewList.add(itemView); - - return Column(children: viewList); + widget.route.onChanged?.call( + List.from(next.selection), + List.from(next.positions), + ); } Widget _renderItemView() { - // 选择器 - List pickerList = - List.generate(_columnNum, (index) => pickerView(index)).toList(); - - return Container( - height: _pickerStyle.pickerHeight, - color: _pickerStyle.backgroundColor, - child: Row(children: pickerList), + return Row( + children: List.generate( + _selection.state.columnCount, + pickerView, + ), ); } - Widget pickerView(int position) { + Widget pickerView(int column) { + final values = _selection.state.column(column); return Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 2), child: CupertinoPicker.builder( - scrollController: scrollCtrl[position], + scrollController: scrollCtrl[column], itemExtent: pickerItemHeight, selectionOverlay: _pickerStyle.itemOverlay, - onSelectedItemChanged: (int selectIndex) { - _setPicker(position, selectIndex, false); - }, - childCount: _columnData[position].length, + onSelectedItemChanged: (index) => _setPicker(column, index), + childCount: values.length, itemBuilder: (_, index) { - // String text = _data[position][index].toString(); - String suffix = ''; + var suffix = ''; if (widget.route.suffix != null && - position < widget.route.suffix!.length) { - suffix = widget.route.suffix![position]; + column < widget.route.suffix!.length) { + suffix = widget.route.suffix![column]; } - - String text = '${_columnData[position][index]}$suffix'; - return Align( - alignment: Alignment.center, - child: Text( - text, - style: TextStyle(color: _pickerStyle.textColor, fontSize: _pickerStyle.textSize ?? 18.0), - textAlign: TextAlign.start, + final text = '${values[index]}$suffix'; + return Semantics( + label: text, + child: Align( + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + color: _pickerStyle.textColor, + fontSize: _pickerStyle.textSize ?? 18.0, + ), + textAlign: TextAlign.start, + ), ), ); }, @@ -428,71 +183,4 @@ class _PickerState extends State { ), ); } - - // 选择器上面的view - Widget _titleView() { - return Container( - height: _pickerStyle.pickerTitleHeight, - decoration: _pickerStyle.headDecoration, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 取消按钮 - InkWell( - onTap: () => Navigator.pop(context, false), - child: _pickerStyle.cancelButton, - ), - - /// 标题 - Expanded(child: _pickerStyle.title), - - /// 确认按钮 - InkWell( - onTap: () { - widget.route.onConfirm?.call(_selectData, _selectDataPosition); - Navigator.pop(context, true); - }, - child: _pickerStyle.commitButton, - ), - ], - ), - ); - } -} - -class _BottomPickerLayout extends SingleChildLayoutDelegate { - _BottomPickerLayout(this.progress, {required this.pickerStyle}); - - final double progress; - final PickerStyle pickerStyle; - - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - double maxHeight = pickerStyle.pickerHeight; - if (pickerStyle.showTitleBar) { - maxHeight += pickerStyle.pickerTitleHeight; - } - if (pickerStyle.menu != null) { - maxHeight += pickerStyle.menuHeight; - } - - return BoxConstraints( - minWidth: constraints.maxWidth, - maxWidth: constraints.maxWidth, - minHeight: 0.0, - maxHeight: maxHeight, - ); - } - - @override - Offset getPositionForChild(Size size, Size childSize) { - double height = size.height - childSize.height * progress; - return Offset(0.0, height); - } - - @override - bool shouldRelayout(_BottomPickerLayout oldDelegate) { - return progress != oldDelegate.progress; - } } diff --git a/lib/more_pickers/route/multiple_picker_route.dart b/lib/more_pickers/route/multiple_picker_route.dart index 0f8d65e..d1b8203 100644 --- a/lib/more_pickers/route/multiple_picker_route.dart +++ b/lib/more_pickers/route/multiple_picker_route.dart @@ -1,22 +1,38 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter_pickers/src/route/picker_popup_route.dart'; +import 'package:flutter_pickers/src/route/picker_sheet.dart'; import 'package:flutter_pickers/style/picker_style.dart'; typedef MultipleCallback = Function(List res, List position); +/// 将编辑区解析出的值同步回每一列;全部值都能匹配数据源时返回 true。 +typedef MultiplePickerSelectionUpdater = bool Function(List selection); + +/// 构建选择器下方的自定义编辑区。 +/// +/// [selection] 是当前各列选中值的只读快照。编辑内容解析为完整选项后, +/// 调用 [updateSelection] 即可驱动各列滚轮同步更新。 +typedef MultiplePickerEditorBuilder = Widget Function( + BuildContext context, + List selection, + MultiplePickerSelectionUpdater updateSelection, +); + /// 多项选择器 /// 无关联 -class MultiplePickerRoute extends PopupRoute { +class MultiplePickerRoute extends PickerPopupRoute { MultiplePickerRoute({ - required this.pickerStyle, + required super.pickerStyle, required this.data, required this.selectData, this.suffix, this.onChanged, this.onConfirm, - this.onCancel, - this.theme, - this.barrierLabel, + super.onCancel, + this.editorBuilder, + this.editorHeight = 56.0, + super.theme, + super.barrierLabel, super.settings, }); @@ -25,64 +41,22 @@ class MultiplePickerRoute extends PopupRoute { final List? suffix; final MultipleCallback? onChanged; final MultipleCallback? onConfirm; - final Function(bool isCancel)? onCancel; - final ThemeData? theme; - - final PickerStyle pickerStyle; - - @override - Duration get transitionDuration => const Duration(milliseconds: 200); - - @override - bool get barrierDismissible => true; - - @override - bool didPop(T? result) { - if (result == null) { - onCancel?.call(false); - } else if (!(result as bool)) { - onCancel?.call(true); - } - return super.didPop(result); - } - - @override - final String? barrierLabel; - - @override - Color get barrierColor => Colors.black54; - - late AnimationController _animationController; - - @override - AnimationController createAnimationController() { - _animationController = BottomSheet.createAnimationController( - navigator!.overlay!, - ); - return _animationController; - } + final MultiplePickerEditorBuilder? editorBuilder; + final double editorHeight; @override - Widget buildPage( + Widget buildPickerContent( BuildContext context, - Animation animation, - Animation secondaryAnimation, + PickerStyle resolvedStyle, + double safeAreaBottom, ) { - Widget bottomSheet = MediaQuery.removePadding( - context: context, - removeTop: true, - child: PickerContentView( - data: data, - selectData: selectData, - pickerStyle: pickerStyle, - route: this, - ), + return PickerContentView( + data: data, + selectData: selectData, + pickerStyle: resolvedStyle, + safeAreaBottom: safeAreaBottom, + route: this, ); - if (theme != null) { - bottomSheet = Theme(data: theme!, child: bottomSheet); - } - - return bottomSheet; } } @@ -92,6 +66,7 @@ class PickerContentView extends StatefulWidget { required this.data, required this.pickerStyle, required this.selectData, + this.safeAreaBottom = 0.0, required this.route, }); @@ -99,6 +74,7 @@ class PickerContentView extends StatefulWidget { final List selectData; final MultiplePickerRoute route; final PickerStyle pickerStyle; + final double safeAreaBottom; @override State createState() => _PickerState(); @@ -110,9 +86,6 @@ class _PickerState extends State { late List _selectDataPosition; late List _data; - AnimationController? controller; - Animation? animation; - List scrollCtrl = []; @override @@ -146,24 +119,27 @@ class _PickerState extends State { @override Widget build(BuildContext context) { - return GestureDetector( - child: AnimatedBuilder( + final keyboardInset = MediaQuery.of(context).viewInsets.bottom; + final editor = widget.route.editorBuilder?.call( + context, + List.unmodifiable(_selectData), + _updateSelectionFromEditor, + ); + + return AnimatedPadding( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutCubic, + padding: EdgeInsets.only(bottom: keyboardInset), + child: PickerSheet( animation: widget.route.animation!, - builder: (BuildContext context, Widget? child) { - return ClipRect( - child: CustomSingleChildLayout( - delegate: _BottomPickerLayout( - widget.route.animation!.value, - pickerStyle: _pickerStyle, - ), - child: GestureDetector( - child: Material( - color: Colors.transparent, - child: _renderPickerView(), - ), - ), - ), - ); + style: _pickerStyle, + safeAreaBottom: widget.safeAreaBottom, + body: _renderItemView(), + footer: editor, + footerHeight: editor == null ? 0.0 : widget.route.editorHeight, + onConfirm: () { + FocusScope.of(context).unfocus(); + widget.route.onConfirm?.call(_selectData, _selectDataPosition); }, ), ); @@ -192,48 +168,72 @@ class _PickerState extends State { void _setPicker(int index, int selectIndex) { var selectedName = _data[index][selectIndex]; - // if (_selectData[index].toString() != selectedName.toString()) { - // setState(() { - // }); - // } - _selectData[index] = selectedName; - _selectDataPosition[index] = selectIndex; + if (_selectDataPosition[index] == selectIndex && + _selectData[index].toString() == selectedName.toString()) { + return; + } - _notifyLocationChanged(); - } + void updateSelection() { + _selectData[index] = selectedName; + _selectDataPosition[index] = selectIndex; + } - void _notifyLocationChanged() { - widget.route.onChanged?.call(_selectData, _selectDataPosition); - } + if (widget.route.editorBuilder == null) { + updateSelection(); + } else { + setState(updateSelection); + } - Widget _renderPickerView() { - Widget itemView = _renderItemView(); + _notifyLocationChanged(); + } - if (!_pickerStyle.showTitleBar && _pickerStyle.menu == null) { - return itemView; + bool _updateSelectionFromEditor(List selection) { + if (selection.length != _data.length) { + return false; } - List viewList = []; - if (_pickerStyle.showTitleBar) { - viewList.add(_titleView()); + + final positions = []; + for (var column = 0; column < _data.length; column++) { + final position = _data[column].indexWhere( + (item) => item.toString() == selection[column].toString(), + ); + if (position < 0) { + return false; + } + positions.add(position); } - if (_pickerStyle.menu != null) { - viewList.add(_pickerStyle.menu!); + + final changed = positions.asMap().entries.any( + (entry) => _selectDataPosition[entry.key] != entry.value, + ); + if (!changed) { + return true; } - viewList.add(itemView); - return Column(children: viewList); + setState(() { + for (var column = 0; column < _data.length; column++) { + final position = positions[column]; + _selectData[column] = _data[column][position]; + _selectDataPosition[column] = position; + scrollCtrl[column].jumpToItem(position); + } + }); + _notifyLocationChanged(); + return true; + } + + void _notifyLocationChanged() { + widget.route.onChanged?.call(_selectData, _selectDataPosition); } Widget _renderItemView() { // 选择器 - List pickerList = - List.generate(_data.length, (index) => pickerView(index)).toList(); + List pickerList = List.generate( + _data.length, + (index) => pickerView(index), + ).toList(); - return Container( - height: _pickerStyle.pickerHeight, - color: _pickerStyle.backgroundColor, - child: Row(children: pickerList), - ); + return Row(children: pickerList); } Widget pickerView(int position) { @@ -244,8 +244,8 @@ class _PickerState extends State { scrollController: scrollCtrl[position], selectionOverlay: _pickerStyle.itemOverlay, itemExtent: _pickerStyle.pickerItemHeight, - onSelectedItemChanged: - (int selectIndex) => _setPicker(position, selectIndex), + onSelectedItemChanged: (int selectIndex) => + _setPicker(position, selectIndex), childCount: _data[position].length, itemBuilder: (_, index) { // String text = _data[position][index].toString(); @@ -256,12 +256,18 @@ class _PickerState extends State { } String text = '${_data[position][index]}$suffix'; - return Align( - alignment: Alignment.center, - child: Text( - text, - style: TextStyle(color: _pickerStyle.textColor, fontSize: _pickerStyle.textSize ?? 18.0), - textAlign: TextAlign.start, + return Semantics( + label: text, + child: Align( + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + color: _pickerStyle.textColor, + fontSize: _pickerStyle.textSize ?? 18.0, + ), + textAlign: TextAlign.start, + ), ), ); }, @@ -269,71 +275,4 @@ class _PickerState extends State { ), ); } - - // 选择器上面的view - Widget _titleView() { - return Container( - height: _pickerStyle.pickerTitleHeight, - decoration: _pickerStyle.headDecoration, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 取消按钮 - InkWell( - onTap: () => Navigator.pop(context, false), - child: _pickerStyle.cancelButton, - ), - - /// 标题 - Expanded(child: _pickerStyle.title), - - /// 确认按钮 - InkWell( - onTap: () { - widget.route.onConfirm?.call(_selectData, _selectDataPosition); - Navigator.pop(context, true); - }, - child: _pickerStyle.commitButton, - ), - ], - ), - ); - } -} - -class _BottomPickerLayout extends SingleChildLayoutDelegate { - _BottomPickerLayout(this.progress, {required this.pickerStyle}); - - final double progress; - final PickerStyle pickerStyle; - - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - double maxHeight = pickerStyle.pickerHeight; - if (pickerStyle.showTitleBar) { - maxHeight += pickerStyle.pickerTitleHeight; - } - if (pickerStyle.menu != null) { - maxHeight += pickerStyle.menuHeight; - } - - return BoxConstraints( - minWidth: constraints.maxWidth, - maxWidth: constraints.maxWidth, - minHeight: 0.0, - maxHeight: maxHeight, - ); - } - - @override - Offset getPositionForChild(Size size, Size childSize) { - double height = size.height - childSize.height * progress; - return Offset(0.0, height); - } - - @override - bool shouldRelayout(_BottomPickerLayout oldDelegate) { - return progress != oldDelegate.progress; - } } diff --git a/lib/more_pickers/route/single_picker_route.dart b/lib/more_pickers/route/single_picker_route.dart index 8508fa6..171717c 100644 --- a/lib/more_pickers/route/single_picker_route.dart +++ b/lib/more_pickers/route/single_picker_route.dart @@ -1,21 +1,23 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' show ThemeData; import 'package:flutter_pickers/more_pickers/init_data.dart'; +import 'package:flutter_pickers/src/route/picker_popup_route.dart'; +import 'package:flutter_pickers/src/route/picker_sheet.dart'; import 'package:flutter_pickers/style/picker_style.dart'; typedef SingleCallback = Function(dynamic data, int position); -class SinglePickerRoute extends PopupRoute { +class SinglePickerRoute extends PickerPopupRoute { SinglePickerRoute({ required this.data, this.selectData, this.suffix, this.onChanged, this.onConfirm, - this.onCancel, - required this.theme, - this.barrierLabel, - required this.pickerStyle, + super.onCancel, + required super.theme, + super.barrierLabel, + required super.pickerStyle, super.settings, }); @@ -23,49 +25,17 @@ class SinglePickerRoute extends PopupRoute { final dynamic data; final SingleCallback? onChanged; final SingleCallback? onConfirm; - final Function(bool isCancel)? onCancel; - final ThemeData theme; final String? suffix; - final PickerStyle pickerStyle; - - @override - Duration get transitionDuration => const Duration(milliseconds: 200); - - @override - bool get barrierDismissible => true; - - @override - final String? barrierLabel; @override - bool didPop(T? result) { - if (result == null) { - onCancel?.call(false); - } else if (!(result as bool)) { - onCancel?.call(true); - } - return super.didPop(result); - } - - @override - Color get barrierColor => Colors.black54; - - late AnimationController _animationController; + ThemeData get theme => super.theme!; @override - AnimationController createAnimationController() { - _animationController = BottomSheet.createAnimationController( - navigator!.overlay!, - ); - return _animationController; - } - - @override - Widget buildPage( + Widget buildPickerContent( BuildContext context, - Animation animation, - Animation secondaryAnimation, + PickerStyle resolvedStyle, + double safeAreaBottom, ) { List mData = []; // 初始化数据 @@ -75,19 +45,13 @@ class SinglePickerRoute extends PopupRoute { mData.addAll(data); } - Widget bottomSheet = MediaQuery.removePadding( - context: context, - removeTop: true, - child: PickerContentView( - data: mData, - selectData: selectData, - pickerStyle: pickerStyle, - route: this, - ), + return PickerContentView( + data: mData, + selectData: selectData, + pickerStyle: resolvedStyle, + safeAreaBottom: safeAreaBottom, + route: this, ); - bottomSheet = Theme(data: theme, child: bottomSheet); - - return bottomSheet; } } @@ -97,6 +61,7 @@ class PickerContentView extends StatefulWidget { required this.data, this.selectData, required this.pickerStyle, + this.safeAreaBottom = 0.0, required this.route, }); @@ -104,6 +69,7 @@ class PickerContentView extends StatefulWidget { final dynamic selectData; final SinglePickerRoute route; final PickerStyle pickerStyle; + final double safeAreaBottom; @override State createState() => _PickerState(); @@ -143,26 +109,15 @@ class _PickerState extends State { @override Widget build(BuildContext context) { - return GestureDetector( - child: AnimatedBuilder( - animation: widget.route.animation!, - builder: (BuildContext context, Widget? child) { - return ClipRect( - child: CustomSingleChildLayout( - delegate: _BottomPickerLayout( - widget.route.animation!.value, - pickerStyle: _pickerStyle, - ), - child: GestureDetector( - child: Material( - color: Colors.transparent, - child: _renderPickerView(), - ), - ), - ), - ); - }, - ), + return PickerSheet( + animation: widget.route.animation!, + style: _pickerStyle, + safeAreaBottom: widget.safeAreaBottom, + bodyPadding: const EdgeInsets.symmetric(horizontal: 40), + body: _renderItemView(), + onConfirm: () { + widget.route.onConfirm?.call(_selectData, _selectPosition); + }, ); } @@ -208,37 +163,6 @@ class _PickerState extends State { return left; } - /// 动态计算itemTextSize - double _pickerFontSize(String text) { - if (text.length <= 6) { - return 18.0; - } else if (text.length < 9) { - return 16.0; - } else if (text.length < 13) { - return 12.0; - } else { - return 10.0; - } - } - - Widget _renderPickerView() { - Widget itemView = _renderItemView(); - - if (!_pickerStyle.showTitleBar && _pickerStyle.menu == null) { - return itemView; - } - List viewList = []; - if (_pickerStyle.showTitleBar) { - viewList.add(_titleView()); - } - if (_pickerStyle.menu != null) { - viewList.add(_pickerStyle.menu!); - } - viewList.add(itemView); - - return Column(children: viewList); - } - Widget _renderItemView() { // 选择器 Widget cPicker = CupertinoPicker.builder( @@ -260,15 +184,20 @@ class _PickerState extends State { childCount: _data.length, itemBuilder: (_, index) { String text = _data[index].toString(); - return Align( - alignment: Alignment.center, - child: Text( - text, - style: TextStyle( - color: _pickerStyle.textColor, - fontSize: _pickerStyle.textSize ?? _pickerFontSize(text), + return Semantics( + label: text, + child: Align( + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + color: _pickerStyle.textColor, + fontSize: _pickerStyle.textSize ?? 18.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, ), - textAlign: TextAlign.start, ), ); }, @@ -297,79 +226,6 @@ class _PickerState extends State { view = cPicker; } - return Container( - padding: const EdgeInsets.symmetric(horizontal: 40), - height: _pickerStyle.pickerHeight, - color: _pickerStyle.backgroundColor, - child: view, - ); - } - - // 选择器上面的view - Widget _titleView() { - return Container( - height: _pickerStyle.pickerTitleHeight, - decoration: _pickerStyle.headDecoration, - child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 取消按钮 - InkWell( - onTap: () => Navigator.pop(context, false), - child: _pickerStyle.cancelButton, - ), - - /// 标题 - Expanded(child: _pickerStyle.title), - - /// 确认按钮 - InkWell( - onTap: () { - debugPrint('longer _selectPosition >>> $_selectPosition'); - widget.route.onConfirm?.call(_selectData, _selectPosition); - Navigator.pop(context, true); - }, - child: _pickerStyle.commitButton, - ), - ], - ), - ); - } -} - -class _BottomPickerLayout extends SingleChildLayoutDelegate { - _BottomPickerLayout(this.progress, {required this.pickerStyle}); - - final double progress; - final PickerStyle pickerStyle; - - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - double maxHeight = pickerStyle.pickerHeight; - if (pickerStyle.showTitleBar) { - maxHeight += pickerStyle.pickerTitleHeight; - } - if (pickerStyle.menu != null) { - maxHeight += pickerStyle.menuHeight; - } - - return BoxConstraints( - minWidth: constraints.maxWidth, - maxWidth: constraints.maxWidth, - minHeight: 0.0, - maxHeight: maxHeight, - ); - } - - @override - Offset getPositionForChild(Size size, Size childSize) { - double height = size.height - childSize.height * progress; - return Offset(0.0, height); - } - - @override - bool shouldRelayout(_BottomPickerLayout oldDelegate) { - return progress != oldDelegate.progress; + return view; } } diff --git a/lib/pickers.dart b/lib/pickers.dart index 28c7735..dd0539f 100644 --- a/lib/pickers.dart +++ b/lib/pickers.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_pickers/address_picker/route/address_picker_route.dart'; +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; import 'package:flutter_pickers/more_pickers/init_data.dart'; import 'package:flutter_pickers/more_pickers/route/multiple_link_picker_route.dart'; import 'package:flutter_pickers/more_pickers/route/multiple_picker_route.dart'; import 'package:flutter_pickers/more_pickers/route/single_picker_route.dart'; +import 'package:flutter_pickers/src/style/resolved_picker_style.dart'; import 'package:flutter_pickers/style/default_style.dart'; import 'package:flutter_pickers/style/picker_style.dart'; import 'package:flutter_pickers/time_picker/model/date_mode.dart'; @@ -12,6 +14,9 @@ import 'package:flutter_pickers/time_picker/model/suffix.dart'; import 'package:flutter_pickers/time_picker/route/date_picker_route.dart'; import 'time_picker/model/date_item_model.dart'; +export 'pickers_locale.dart'; +export 'l10n/generated/app_localizations.dart'; + /// Flutter 选择器工具类 /// /// 提供多种选择器: @@ -23,6 +28,7 @@ import 'time_picker/model/date_item_model.dart'; class Pickers { // 私有构造函数,防止实例化 Pickers._(); + /// 单列通用选择器 /// /// [context] 上下文 @@ -78,6 +84,8 @@ class Pickers { /// [onChanged] 选择器发生变动时的回调 /// [onConfirm] 选择器确认时的回调 /// [onCancel] 选择器取消时的回调 + /// [editorBuilder] 选择器下方的自定义编辑区,可通过 updateSelection 与滚轮双向联动 + /// [editorHeight] 自定义编辑区高度 /// [overlapTabBar] 是否覆盖 TabBar static void showMultiPicker( BuildContext context, { @@ -88,8 +96,11 @@ class Pickers { MultipleCallback? onChanged, MultipleCallback? onConfirm, Function(bool isCancel)? onCancel, + MultiplePickerEditorBuilder? editorBuilder, + double editorHeight = 56.0, bool overlapTabBar = false, }) { + assert(editorHeight >= 0, 'editorHeight must not be negative'); final style = _initPickerStyle(pickerStyle, context); Navigator.of(context, rootNavigator: overlapTabBar).push( @@ -101,6 +112,8 @@ class Pickers { onChanged: onChanged, onConfirm: onConfirm, onCancel: onCancel, + editorBuilder: editorBuilder, + editorHeight: editorHeight, theme: Theme.of(context), barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, @@ -277,7 +290,146 @@ class Pickers { BuildContext context, ) { final style = pickerStyle ?? DefaultPickerStyle(); - style.context ??= context; - return style; + return resolvePickerStyle(style, context); + } + + /// 获取国际化后的内置数据 + /// + /// [type] 数据类型 + /// [context] BuildContext,用于获取本地化实例 + /// 返回 `List` 类型的数据列表 + /// 如果 context 无法获取 AppLocalizations,返回中文数据 + static List getData(PickerDataType type, BuildContext context) { + final l10n = AppLocalizations.of(context); + if (l10n == null) { + return _getDefaultData(type); + } + return _getLocalizedData(type, l10n); + } + + static List _getDefaultData(PickerDataType type) { + return pickerData[type] ?? []; + } + + static List _getLocalizedData( + PickerDataType type, AppLocalizations l10n) { + switch (type) { + case PickerDataType.sex: + return [l10n.sexAny, l10n.sexMale, l10n.sexFemale]; + case PickerDataType.education: + return [ + l10n.educationBelowHighSchool, + l10n.educationHighSchool, + l10n.educationAssociate, + l10n.educationBachelor, + l10n.educationMaster, + l10n.educationPhd, + l10n.educationPostdoc, + l10n.educationOther, + ]; + case PickerDataType.subject: + return [ + l10n.subjectChinese, + l10n.subjectMath, + l10n.subjectEnglish, + l10n.subjectPhysics, + l10n.subjectChemistry, + l10n.subjectBiology, + l10n.subjectPolitics, + l10n.subjectGeography, + l10n.subjectHistory, + ]; + case PickerDataType.constellation: + return [ + l10n.constellationAquarius, + l10n.constellationPisces, + l10n.constellationAries, + l10n.constellationTaurus, + l10n.constellationGemini, + l10n.constellationCancer, + l10n.constellationLeo, + l10n.constellationVirgo, + l10n.constellationLibra, + l10n.constellationScorpio, + l10n.constellationSagittarius, + l10n.constellationCapricorn, + ]; + case PickerDataType.zodiac: + return [ + l10n.zodiacRat, + l10n.zodiacOx, + l10n.zodiacTiger, + l10n.zodiacRabbit, + l10n.zodiacDragon, + l10n.zodiacSnake, + l10n.zodiacHorse, + l10n.zodiacGoat, + l10n.zodiacMonkey, + l10n.zodiacRooster, + l10n.zodiacDog, + l10n.zodiacPig, + ]; + case PickerDataType.ethnicity: + return [ + l10n.ethnicityHan, + l10n.ethnicityMongol, + l10n.ethnicityHui, + l10n.ethnicityTibetan, + l10n.ethnicityUygur, + l10n.ethnicityMiao, + l10n.ethnicityYi, + l10n.ethnicityZhuang, + l10n.ethnicityBouyei, + l10n.ethnicityKorean, + l10n.ethnicityManchu, + l10n.ethnicityDong, + l10n.ethnicityYao, + l10n.ethnicityBai, + l10n.ethnicityTujia, + l10n.ethnicityHani, + l10n.ethnicityKazak, + l10n.ethnicityDai, + l10n.ethnicityLi, + l10n.ethnicityLisu, + l10n.ethnicityVa, + l10n.ethnicityShe, + l10n.ethnicityGaoshan, + l10n.ethnicityLahu, + l10n.ethnicityShui, + l10n.ethnicityDongxiang, + l10n.ethnicityNaxi, + l10n.ethnicityJingpo, + l10n.ethnicityKirgiz, + l10n.ethnicityTu, + l10n.ethnicityDaur, + l10n.ethnicityMulao, + l10n.ethnicityQiang, + l10n.ethnicityBlang, + l10n.ethnicitySalar, + l10n.ethnicityMaonan, + l10n.ethnicityGelao, + l10n.ethnicityXibe, + l10n.ethnicityAchang, + l10n.ethnicityPumi, + l10n.ethnicityTajik, + l10n.ethnicityNu, + l10n.ethnicityUzbek, + l10n.ethnicityRussian, + l10n.ethnicityEwenki, + l10n.ethnicityDeang, + l10n.ethnicityBonan, + l10n.ethnicityYugur, + l10n.ethnicityGin, + l10n.ethnicityTatar, + l10n.ethnicityDerung, + l10n.ethnicityOroqen, + l10n.ethnicityHezhen, + l10n.ethnicityMonba, + l10n.ethnicityLhoba, + l10n.ethnicityJino, + l10n.ethnicityOther, + l10n.ethnicityForeignBornChinese, + ]; + } } } diff --git a/lib/pickers_locale.dart b/lib/pickers_locale.dart new file mode 100644 index 0000000..d15631f --- /dev/null +++ b/lib/pickers_locale.dart @@ -0,0 +1,32 @@ +import 'dart:ui'; + +/// 选择器国际化语言管理类 +/// +/// 支持中英文切换,优先级:API 设置 > 系统语言 > 默认中文 +class PickersLocale { + static Locale? _overrideLocale; + + PickersLocale._(); + + /// 设置语言(优先级高于系统设置) + /// 仅支持 'zh' 和 'en',传入其他值将被忽略 + static void setLocale(Locale? locale) { + if (locale != null && !_isSupported(locale)) { + return; + } + _overrideLocale = locale; + } + + /// 获取当前语言 + static Locale? get currentLocale => _overrideLocale; + + /// 支持的语言列表 + static List get supportedLocales => [ + const Locale('zh'), + const Locale('en'), + ]; + + static bool _isSupported(Locale locale) { + return locale.languageCode == 'zh' || locale.languageCode == 'en'; + } +} diff --git a/lib/src/cascading_picker/address_adapter.dart b/lib/src/cascading_picker/address_adapter.dart new file mode 100644 index 0000000..2e854b8 --- /dev/null +++ b/lib/src/cascading_picker/address_adapter.dart @@ -0,0 +1,65 @@ +import 'package:flutter_pickers/address_picker/locations_data.dart'; +import 'package:flutter_pickers/src/cascading_picker/cascading_selection.dart'; + +class AddressAdapter implements CascadingDataAdapter { + AddressAdapter({ + required this.allText, + required this.addAllItem, + required this.hasTown, + }); + + final String allText; + final bool addAllItem; + final bool hasTown; + + @override + int get columnCount => hasTown ? 3 : 2; + + @override + List valuesFor(int column, List selection) { + switch (column) { + case 0: + return List.from(Address.provinces); + case 1: + return _cities(selection[0] as String); + case 2: + return _towns( + selection[0] as String, + selection[1] as String, + ); + default: + throw RangeError.range(column, 0, columnCount - 1, 'column'); + } + } + + List _cities(String province) { + final provinceCode = _provinceCode(province); + final names = locations[provinceCode]?.values.toList() ?? []; + if (addAllItem) names.insert(0, allText); + return names.isEmpty ? [allText] : names; + } + + List _towns(String province, String city) { + if (addAllItem && city == allText) return [allText]; + + final provinceCode = _provinceCode(province); + var cityCode = ''; + locations[provinceCode]?.forEach((code, name) { + if (name == city) cityCode = code; + }); + if (cityCode.isEmpty) return [allText]; + + final names = locations[cityCode]?.values.toList() ?? []; + if (names.isEmpty) return [allText]; + if (addAllItem) names.insert(0, allText); + return names; + } + + String _provinceCode(String province) { + var code = ''; + locations['86']?.forEach((candidate, name) { + if (name == province) code = candidate; + }); + return code; + } +} diff --git a/lib/src/cascading_picker/cascading_selection.dart b/lib/src/cascading_picker/cascading_selection.dart new file mode 100644 index 0000000..8499f9c --- /dev/null +++ b/lib/src/cascading_picker/cascading_selection.dart @@ -0,0 +1,82 @@ +import 'dart:collection'; + +/// Interprets one data shape at the cascading Selection seam. +abstract class CascadingDataAdapter { + int get columnCount; + + List valuesFor(int column, List selection); +} + +/// Pure Linked Picker state shared by every cascading data adapter. +class CascadingSelectionModule { + CascadingSelectionModule({ + required CascadingDataAdapter adapter, + List initial = const [], + }) : _adapter = adapter { + _state = _derive(initial); + } + + final CascadingDataAdapter _adapter; + late CascadingSelectionState _state; + + CascadingSelectionState get state => _state; + + CascadingSelectionState select(int column, int index) { + if (column < 0 || column >= _adapter.columnCount) { + throw RangeError.range(column, 0, _adapter.columnCount - 1, 'column'); + } + final values = _state.column(column); + if (index < 0 || index >= values.length) { + throw RangeError.index(index, values, 'index'); + } + + final desired = _state.selection.take(column).toList()..add(values[index]); + _state = _derive(desired); + return _state; + } + + CascadingSelectionState _derive(List desired) { + final columns = []; + final selection = []; + final positions = []; + + for (var column = 0; column < _adapter.columnCount; column++) { + final values = List.unmodifiable( + List.from(_adapter.valuesFor(column, selection)), + ); + if (values.isEmpty) { + throw StateError('Linked Picker column $column has no values'); + } + + final desiredValue = column < desired.length ? desired[column] : null; + var position = values.indexOf(desiredValue); + if (position < 0) position = 0; + + columns.add(values); + positions.add(position); + selection.add(values[position]); + } + + return CascadingSelectionState._(columns, selection, positions); + } +} + +class CascadingSelectionState { + CascadingSelectionState._( + List columns, + List selection, + List positions, + ) : _columns = UnmodifiableListView( + columns.map((column) => UnmodifiableListView(List.from(column))), + ), + selection = UnmodifiableListView(List.from(selection)), + positions = UnmodifiableListView(List.from(positions)); + + final List _columns; + final List selection; + final List positions; + + int get columnCount => _columns.length; + + List column(int index) => _columns[index]; +} diff --git a/lib/src/cascading_picker/nested_map_adapter.dart b/lib/src/cascading_picker/nested_map_adapter.dart new file mode 100644 index 0000000..2382779 --- /dev/null +++ b/lib/src/cascading_picker/nested_map_adapter.dart @@ -0,0 +1,32 @@ +import 'package:flutter_pickers/src/cascading_picker/cascading_selection.dart'; + +class NestedMapAdapter implements CascadingDataAdapter { + NestedMapAdapter({ + required Map data, + required this.columnCount, + this.placeholder = '', + }) : _data = data; + + final Map _data; + + @override + final int columnCount; + + final dynamic placeholder; + + @override + List valuesFor(int column, List selection) { + dynamic node = _data; + for (var index = 0; index < column; index++) { + if (node is! Map || index >= selection.length) { + return [placeholder]; + } + node = node[selection[index]]; + } + + if (node is Map) return node.keys.toList(); + if (node is List) return List.from(node); + if (node == null) return [placeholder]; + return [node]; + } +} diff --git a/lib/src/date_picker/date_selection.dart b/lib/src/date_picker/date_selection.dart new file mode 100644 index 0000000..cfabc77 --- /dev/null +++ b/lib/src/date_picker/date_selection.dart @@ -0,0 +1,281 @@ +import 'dart:collection'; + +import 'package:flutter_pickers/time_picker/model/date_item_model.dart'; +import 'package:flutter_pickers/time_picker/model/date_mode.dart'; +import 'package:flutter_pickers/time_picker/model/date_type.dart'; +import 'package:flutter_pickers/time_picker/model/pduration.dart'; +import 'package:flutter_pickers/time_picker/time_utils.dart'; + +/// Pure Date Picker state. It owns range propagation and selected positions, +/// while the Flutter route adapter owns wheel controllers and rendering. +class DateSelectionModule { + DateSelectionModule({ + required DateMode mode, + required PDuration initial, + required PDuration min, + required PDuration max, + }) : _visibleTypes = _typesFor(mode), + _initial = _DateValues.fromDuration(initial), + _min = _DateValues.fromDuration(min), + _max = _DateValues.fromDuration(max) { + _state = _derive(_initial, clampInvalidValues: false); + } + + final List _visibleTypes; + final _DateValues _initial; + final _DateValues _min; + final _DateValues _max; + late DateSelectionState _state; + + DateSelectionState get state => _state; + + DateSelectionState select(DateType type, int index) { + final values = _state.values(type); + if (index < 0 || index >= values.length) { + throw RangeError.index(index, values, 'index'); + } + + final desired = _DateValues.fromState(_state).withValue( + type, + values[index], + ); + _state = _derive(desired, clampInvalidValues: true); + return _state; + } + + DateSelectionState _derive( + _DateValues desired, { + required bool clampInvalidValues, + }) { + final columns = >{}; + var selected = const _DateValues(); + + for (final type in _visibleTypes) { + final values = _valuesFor(type, selected); + if (values.isEmpty) { + throw StateError('Date Picker has no values for $type'); + } + + final desiredValue = desired.value(type); + final value = values.contains(desiredValue) + ? desiredValue! + : _fallbackValue(values, desiredValue, clampInvalidValues); + columns[type] = values; + selected = selected.withValue(type, value); + } + + return DateSelectionState._(columns, selected); + } + + int _fallbackValue(List values, int? desired, bool clamp) { + if (clamp && desired != null && desired > values.last) { + return values.last; + } + return values.first; + } + + List _valuesFor(DateType type, _DateValues selected) { + switch (type) { + case DateType.year: + return _ints( + TimeUtils.calcYears( + begin: _min.year.nonZeroOr(1900), + end: _max.year.nonZeroOr(2100), + ), + ); + case DateType.month: + var begin = 1; + var end = 12; + if (_visibleTypes.contains(DateType.year)) { + if (_min.month.isNotEmpty && selected.year == _min.year) { + begin = _min.month!; + } + if (_max.month.isNotEmpty && selected.year == _max.year) { + end = _max.month!; + } + } + return _ints(TimeUtils.calcMonth(begin: begin, end: end)); + case DateType.day: + var begin = 1; + var end = 31; + if (_visibleTypes.contains(DateType.year) && + selected.year == _min.year && + selected.month == _min.month && + _min.day.isNotEmpty) { + begin = _min.day!; + } + if (_visibleTypes.contains(DateType.year) && + selected.year == _max.year && + selected.month == _max.month && + _max.day.isNotEmpty) { + end = _max.day!; + } + return _ints( + TimeUtils.calcDay( + selected.year ?? _initial.year.nonZeroOr(DateTime.now().year), + selected.month ?? _initial.month.nonZeroOr(1), + begin: begin, + end: end, + ), + ); + case DateType.hour: + return _ints( + TimeUtils.calcHour( + begin: _min.hour.nonZeroOr(0), + end: _max.hour.nonZeroOr(23), + ), + ); + case DateType.minute: + var begin = 0; + var end = 59; + if (_min.minute.isNotEmpty || _max.minute.isNotEmpty) { + if (_visibleTypes.contains(DateType.hour)) { + if (selected.hour == _min.hour) begin = _min.minute ?? 0; + if (selected.hour == _max.hour) end = _max.minute ?? 59; + } else { + begin = _min.minute.nonZeroOr(0); + end = _max.minute.nonZeroOr(59); + } + } + return _ints(TimeUtils.calcMinAndSecond(begin: begin, end: end)); + case DateType.second: + var begin = 0; + var end = 59; + if (_min.second.isNotEmpty || _max.second.isNotEmpty) { + final hasHour = _visibleTypes.contains(DateType.hour); + final hasMinute = _visibleTypes.contains(DateType.minute); + if (hasHour && hasMinute) { + if (selected.hour == _min.hour && selected.minute == _min.minute) { + begin = _min.second ?? 0; + } + if (selected.hour == _max.hour && selected.minute == _max.minute) { + end = _max.second ?? 59; + } + } else if (hasMinute) { + if (selected.minute == _min.minute) begin = _min.second ?? 0; + if (selected.minute == _max.minute) end = _max.second ?? 59; + } else { + begin = _min.second.nonZeroOr(0); + end = _max.second.nonZeroOr(59); + } + } + return _ints(TimeUtils.calcMinAndSecond(begin: begin, end: end)); + } + } + + static List _typesFor(DateMode mode) { + final items = DateItemModel.parse(mode); + return [ + if (items.year) DateType.year, + if (items.month) DateType.month, + if (items.day) DateType.day, + if (items.hour) DateType.hour, + if (items.minute) DateType.minute, + if (items.second) DateType.second, + ]; + } + + static List _ints(List values) => values.cast(); +} + +class DateSelectionState { + DateSelectionState._( + Map> columns, + _DateValues selected, + ) : _columns = Map.unmodifiable( + columns.map( + (type, values) => MapEntry( + type, + UnmodifiableListView(List.from(values)), + ), + ), + ), + _selected = selected; + + final Map> _columns; + final _DateValues _selected; + + Iterable get types => _columns.keys; + + List values(DateType type) => _columns[type] ?? const []; + + int? selected(DateType type) => _selected.value(type); + + int position(DateType type) => values(type).indexOf(selected(type)!); + + PDuration toDuration() { + return PDuration( + year: _selected.year ?? 0, + month: _selected.month ?? 0, + day: _selected.day ?? 0, + hour: _selected.hour ?? 0, + minute: _selected.minute ?? 0, + second: _selected.second ?? 0, + ); + } +} + +class _DateValues { + const _DateValues({ + this.year, + this.month, + this.day, + this.hour, + this.minute, + this.second, + }); + + factory _DateValues.fromDuration(PDuration duration) { + return _DateValues( + year: duration.year, + month: duration.month, + day: duration.day, + hour: duration.hour, + minute: duration.minute, + second: duration.second, + ); + } + + factory _DateValues.fromState(DateSelectionState state) => state._selected; + + final int? year; + final int? month; + final int? day; + final int? hour; + final int? minute; + final int? second; + + int? value(DateType type) { + switch (type) { + case DateType.year: + return year; + case DateType.month: + return month; + case DateType.day: + return day; + case DateType.hour: + return hour; + case DateType.minute: + return minute; + case DateType.second: + return second; + } + } + + _DateValues withValue(DateType type, int value) { + return _DateValues( + year: type == DateType.year ? value : year, + month: type == DateType.month ? value : month, + day: type == DateType.day ? value : day, + hour: type == DateType.hour ? value : hour, + minute: type == DateType.minute ? value : minute, + second: type == DateType.second ? value : second, + ); + } +} + +extension on int? { + bool get isNotEmpty => this != null && this != 0; + + int nonZeroOr(int fallback) => isNotEmpty ? this! : fallback; +} diff --git a/lib/src/route/picker_popup_route.dart b/lib/src/route/picker_popup_route.dart new file mode 100644 index 0000000..8cfafa7 --- /dev/null +++ b/lib/src/route/picker_popup_route.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/src/style/resolved_picker_style.dart'; +import 'package:flutter_pickers/style/picker_style.dart'; + +/// Shared popup lifecycle and page assembly for every picker route. +abstract class PickerPopupRoute extends PopupRoute { + PickerPopupRoute({ + PickerStyle? pickerStyle, + this.theme, + this.onCancel, + this.barrierLabel, + super.settings, + }) : pickerStyle = pickerStyle ?? PickerStyle(); + + final PickerStyle pickerStyle; + final ThemeData? theme; + final Function(bool isCancel)? onCancel; + + @override + final String? barrierLabel; + + @override + Duration get transitionDuration => const Duration(milliseconds: 200); + + @override + bool get barrierDismissible => true; + + @override + Color get barrierColor => Colors.black54; + + @override + bool didPop(T? result) { + if (result == null) { + onCancel?.call(false); + } else if (!(result as bool)) { + onCancel?.call(true); + } + return super.didPop(result); + } + + @override + AnimationController createAnimationController() { + return BottomSheet.createAnimationController(navigator!.overlay!); + } + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + final resolvedStyle = resolvePickerStyle(pickerStyle, context); + // viewPadding remains stable while the keyboard is visible, preserving + // Android gesture navigation and the iOS home indicator on full screens. + final safeAreaBottom = resolvedStyle.safeArea + ? MediaQuery.of(context).viewPadding.bottom + : 0.0; + + Widget sheet = MediaQuery.removePadding( + context: context, + removeTop: true, + child: buildPickerContent(context, resolvedStyle, safeAreaBottom), + ); + if (theme != null) { + sheet = Theme(data: theme!, child: sheet); + } + return sheet; + } + + Widget buildPickerContent( + BuildContext context, + PickerStyle resolvedStyle, + double safeAreaBottom, + ); +} diff --git a/lib/src/route/picker_sheet.dart b/lib/src/route/picker_sheet.dart new file mode 100644 index 0000000..73604b4 --- /dev/null +++ b/lib/src/route/picker_sheet.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/style/picker_style.dart'; + +/// Shared animated shell for picker wheels, title actions, menu, and footer. +class PickerSheet extends StatelessWidget { + const PickerSheet({ + super.key, + required this.animation, + required this.style, + required this.safeAreaBottom, + required this.body, + required this.onConfirm, + this.bodyPadding = EdgeInsets.zero, + this.footer, + this.footerHeight = 0.0, + }); + + final Animation animation; + final PickerStyle style; + final double safeAreaBottom; + final Widget body; + final VoidCallback onConfirm; + final EdgeInsetsGeometry bodyPadding; + final Widget? footer; + final double footerHeight; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + builder: (context, child) { + return ClipRect( + child: CustomSingleChildLayout( + delegate: PickerSheetLayout( + animation.value, + style: style, + safeAreaBottom: safeAreaBottom, + footerHeight: footer == null ? 0.0 : footerHeight, + ), + child: Material( + color: Colors.transparent, + child: _buildContent(context), + ), + ), + ); + }, + ); + } + + Widget _buildContent(BuildContext context) { + final bodySafeArea = footer == null ? safeAreaBottom : 0.0; + final pickerBody = Container( + height: style.pickerHeight + bodySafeArea, + padding: bodyPadding.add(EdgeInsets.only(bottom: bodySafeArea)), + color: style.backgroundColor, + child: body, + ); + + final children = []; + if (style.showTitleBar) { + children.add(_buildTitle(context)); + } + if (style.menu != null) { + children.add(style.menu!); + } + children.add(pickerBody); + if (footer != null) { + children.add( + Container( + height: footerHeight + safeAreaBottom, + padding: EdgeInsets.only(bottom: safeAreaBottom), + color: style.backgroundColor, + child: SizedBox(height: footerHeight, child: footer), + ), + ); + } + + return children.length == 1 ? children.single : Column(children: children); + } + + Widget _buildTitle(BuildContext context) { + return Container( + height: style.pickerTitleHeight, + decoration: style.headDecoration, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onTap: () => Navigator.pop(context, false), + child: style.cancelButton, + ), + Expanded(child: style.title), + InkWell( + onTap: () { + onConfirm(); + Navigator.pop(context, true); + }, + child: style.commitButton, + ), + ], + ), + ); + } +} + +class PickerSheetLayout extends SingleChildLayoutDelegate { + const PickerSheetLayout( + this.progress, { + required this.style, + required this.safeAreaBottom, + this.footerHeight = 0.0, + }); + + final double progress; + final PickerStyle style; + final double safeAreaBottom; + final double footerHeight; + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + var maxHeight = style.pickerHeight + safeAreaBottom + footerHeight; + if (style.showTitleBar) { + maxHeight += style.pickerTitleHeight; + } + if (style.menu != null) { + maxHeight += style.menuHeight; + } + + return BoxConstraints( + minWidth: constraints.maxWidth, + maxWidth: constraints.maxWidth, + minHeight: 0.0, + maxHeight: maxHeight, + ); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + return Offset(0.0, size.height - childSize.height * progress); + } + + @override + bool shouldRelayout(PickerSheetLayout oldDelegate) { + return progress != oldDelegate.progress || + safeAreaBottom != oldDelegate.safeAreaBottom || + footerHeight != oldDelegate.footerHeight || + style != oldDelegate.style; + } +} diff --git a/lib/src/style/resolved_picker_style.dart b/lib/src/style/resolved_picker_style.dart new file mode 100644 index 0000000..ab53fc7 --- /dev/null +++ b/lib/src/style/resolved_picker_style.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/style/picker_style.dart'; + +/// Resolves a mutable public [PickerStyle] into the immutable snapshot used by +/// one open picker. +PickerStyle resolvePickerStyle(PickerStyle source, BuildContext context) { + // Keep the historical public side effect while ensuring every invocation + // resolves inherited values from its own context. + source.context = context; + + return PickerStyle( + context: context, + showTitleBar: source.showTitleBar, + safeArea: source.safeArea, + menu: source.menu, + pickerHeight: source.pickerHeight, + pickerTitleHeight: source.pickerTitleHeight, + pickerItemHeight: source.pickerItemHeight, + menuHeight: source.menuHeight, + cancelButton: source.cancelButton, + commitButton: source.commitButton, + title: source.title, + headDecoration: source.headDecoration, + backgroundColor: source.backgroundColor, + textColor: source.textColor, + textSize: source.textSize, + itemOverlay: source.itemOverlay, + ); +} diff --git a/lib/style/default_style.dart b/lib/style/default_style.dart index 4d495d9..e25906f 100644 --- a/lib/style/default_style.dart +++ b/lib/style/default_style.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_pickers/style/picker_style.dart'; +// 注意:预设样式(如 DefaultPickerStyle.dark、RaisedPickerStyle 等) +// 中的按钮文本为固定中文,不支持国际化。 +// 如需国际化,请使用默认样式或自定义 PickerStyle。 + // 日间圆角 final headDecorationLight = BoxDecoration( color: Colors.white, diff --git a/lib/style/picker_style.dart b/lib/style/picker_style.dart index 41235de..7e4bacb 100644 --- a/lib/style/picker_style.dart +++ b/lib/style/picker_style.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; /// 基础样式 /// [showTitleBar] 是否显示头部(选择器以上的控件) 默认:true +/// [safeArea] 是否启用安全区(通过 padding 适配底部安全区域) 默认:true /// [menu] 头部和选择器之间的菜单widget,默认null 不显示 /// [title] 头部 中间的标题 默认SizedBox() 不显示 /// [pickerHeight] 选择器下面 picker 的整体高度 固定高度:220.0 @@ -19,6 +21,7 @@ class PickerStyle { BuildContext? context; bool? _showTitleBar; + bool? _safeArea; Widget? menu; double? _pickerHeight; double? _pickerTitleHeight; @@ -37,6 +40,7 @@ class PickerStyle { PickerStyle({ this.context, bool? showTitleBar, + bool? safeArea, this.menu, double? pickerHeight, double? pickerTitleHeight, @@ -51,6 +55,7 @@ class PickerStyle { this.textSize, this.itemOverlay, }) : _showTitleBar = showTitleBar, + _safeArea = safeArea, _pickerHeight = pickerHeight, _pickerTitleHeight = pickerTitleHeight, _pickerItemHeight = pickerItemHeight, @@ -106,6 +111,13 @@ class PickerStyle { _showTitleBar = value; } + set safeArea(bool value) { + _safeArea = value; + } + + /// 是否启用安全区 默认 true + bool get safeArea => _safeArea ?? true; + /// 选择器背景色 默认白色 Color get backgroundColor => _backgroundColor ?? Colors.white; @@ -132,42 +144,38 @@ class PickerStyle { Widget getCommitButton() { if (_commitButton != null) return _commitButton!; - + + final l10n = context != null ? AppLocalizations.of(context!) : null; + final text = l10n?.confirm ?? '确定'; + // 提供安全的默认值,避免 context 为 null 时崩溃 - final primaryColor = context != null - ? Theme.of(context!).primaryColor - : Colors.blue; - + final primaryColor = + context != null ? Theme.of(context!).primaryColor : Colors.blue; + return Container( alignment: Alignment.center, padding: const EdgeInsets.only(left: 12, right: 22), - child: Text( - '确定', - style: TextStyle( - color: primaryColor, - fontSize: 16.0, - ), - ), + child: Text(text, style: TextStyle(color: primaryColor, fontSize: 16.0)), ); } Widget getCancelButton() { if (_cancelButton != null) return _cancelButton!; - + + final l10n = context != null ? AppLocalizations.of(context!) : null; + final text = l10n?.cancel ?? '取消'; + // 提供安全的默认值,避免 context 为 null 时崩溃 final unselectedColor = context != null ? Theme.of(context!).unselectedWidgetColor : Colors.grey; - + return Container( alignment: Alignment.center, padding: const EdgeInsets.only(left: 22, right: 12), child: Text( - '取消', - style: TextStyle( - color: unselectedColor, - fontSize: 16.0, - ), + text, + style: TextStyle(color: unselectedColor, fontSize: 16.0), ), ); } diff --git a/lib/time_picker/model/suffix.dart b/lib/time_picker/model/suffix.dart index 3cf08d2..1d1a90a 100644 --- a/lib/time_picker/model/suffix.dart +++ b/lib/time_picker/model/suffix.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/l10n/generated/app_localizations.dart'; import 'package:flutter_pickers/time_picker/model/date_type.dart'; /// 后缀标签 @@ -27,6 +29,22 @@ class Suffix { this.seconds = '', }); + /// 从 BuildContext 获取国际化的 Suffix + factory Suffix.fromContext(BuildContext context) { + final l10n = AppLocalizations.of(context); + if (l10n == null) { + return Suffix.normal(); + } + return Suffix( + years: l10n.year, + month: l10n.month, + days: l10n.day, + hours: l10n.hour, + minutes: l10n.minute, + seconds: l10n.second, + ); + } + String getSingle(DateType dateType) { switch (dateType) { case DateType.year: diff --git a/lib/time_picker/route/date_picker_route.dart b/lib/time_picker/route/date_picker_route.dart index 5cd58ca..e2f8f04 100644 --- a/lib/time_picker/route/date_picker_route.dart +++ b/lib/time_picker/route/date_picker_route.dart @@ -1,32 +1,31 @@ import 'dart:math'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter_pickers/src/date_picker/date_selection.dart'; +import 'package:flutter_pickers/src/route/picker_popup_route.dart'; +import 'package:flutter_pickers/src/route/picker_sheet.dart'; import 'package:flutter_pickers/style/picker_style.dart'; -import 'package:flutter_pickers/time_picker/model/date_item_model.dart'; import 'package:flutter_pickers/time_picker/model/date_mode.dart'; -import 'package:flutter_pickers/time_picker/model/date_time_data.dart'; import 'package:flutter_pickers/time_picker/model/date_type.dart'; import 'package:flutter_pickers/time_picker/model/pduration.dart'; import 'package:flutter_pickers/time_picker/model/suffix.dart'; -import '../time_utils.dart'; - typedef DateCallback = Function(PDuration res); -class DatePickerRoute extends PopupRoute { +class DatePickerRoute extends PickerPopupRoute { DatePickerRoute({ required this.mode, required this.initDate, - this.pickerStyle, + super.pickerStyle, required this.maxDate, required this.minDate, this.suffix, this.onChanged, this.onConfirm, - this.onCancel, - this.theme, - this.barrierLabel, + super.onCancel, + super.theme, + super.barrierLabel, super.settings, }); @@ -35,68 +34,24 @@ class DatePickerRoute extends PopupRoute { late final PDuration maxDate; late final PDuration minDate; final Suffix? suffix; - final ThemeData? theme; final DateCallback? onChanged; final DateCallback? onConfirm; - final Function(bool isCancel)? onCancel; - final PickerStyle? pickerStyle; - - @override - Duration get transitionDuration => const Duration(milliseconds: 200); - - @override - bool get barrierDismissible => true; - - @override - bool didPop(T? result) { - if (result == null) { - onCancel?.call(false); - } else if (!(result as bool)) { - onCancel?.call(true); - } - return super.didPop(result); - } - - @override - final String? barrierLabel; @override - Color get barrierColor => Colors.black54; - - AnimationController? _animationController; - - @override - AnimationController createAnimationController() { - assert(_animationController == null); - _animationController = BottomSheet.createAnimationController( - navigator!.overlay!, - ); - return _animationController!; - } - - @override - Widget buildPage( + Widget buildPickerContent( BuildContext context, - Animation animation, - Animation secondaryAnimation, + PickerStyle resolvedStyle, + double safeAreaBottom, ) { - Widget bottomSheet = MediaQuery.removePadding( - context: context, - removeTop: true, - child: PickerContentView( - mode: mode, - initData: initDate, - maxDate: maxDate, - minDate: minDate, - pickerStyle: pickerStyle!, - route: this, - ), + return PickerContentView( + mode: mode, + initData: initDate, + maxDate: maxDate, + minDate: minDate, + pickerStyle: resolvedStyle, + safeAreaBottom: safeAreaBottom, + route: this, ); - if (theme != null) { - bottomSheet = Theme(data: theme!, child: bottomSheet); - } - - return bottomSheet; } } @@ -108,6 +63,7 @@ class PickerContentView extends StatefulWidget { required this.pickerStyle, required this.maxDate, required this.minDate, + this.safeAreaBottom = 0.0, required this.route, }); @@ -115,8 +71,7 @@ class PickerContentView extends StatefulWidget { final PDuration initData; final DatePickerRoute route; final PickerStyle pickerStyle; - - // 限制时间 + final double safeAreaBottom; final PDuration maxDate; final PDuration minDate; @@ -126,692 +81,129 @@ class PickerContentView extends StatefulWidget { class _PickerState extends State { late final PickerStyle _pickerStyle; + late final DateSelectionModule _selection; + late final PDuration _selectData; + late Suffix _suffix; + bool _isInitialized = false; - // 是否显示 [年月日时分秒] - late DateItemModel _dateItemModel; - - // 初始 设置选中的数据 - late final PDuration _initSelectData; - - // 选中的数据 用于回传 - late PDuration _selectData; - - // 所有item 对应的数据 - late DateTimeData _dateTimeData; - - // 限制时间 - late final PDuration maxDate; - late final PDuration minDate; - - Animation? animation; - Map scrollCtrl = {}; - - // 选择器 高度 单独提出来,用来解决修改数据 不及时更新的BUG + final Map scrollCtrl = {}; late double pickerItemHeight; @override void initState() { super.initState(); - _initSelectData = widget.initData; - maxDate = widget.maxDate; - minDate = widget.minDate; _pickerStyle = widget.pickerStyle; - _dateItemModel = DateItemModel.parse(widget.mode); pickerItemHeight = _pickerStyle.pickerItemHeight; - _init(); } - void _init() { - scrollCtrl.clear(); - - _dateTimeData = DateTimeData(); - int index = 0; // 初始选中值 Index - _selectData = PDuration(); - - /// minDate 和 maxDate 都初始化了,可以省略很多空判断,直接比较选中值 是否相等 _selectData.month == minDate.month - /// -------年 - if (_dateItemModel.year) { - index = 0; - _dateTimeData.year = TimeUtils.calcYears( - begin: minDate.year!, - end: maxDate.year!, - ); - - if (_initSelectData.year != null) { - index = _dateTimeData.year.indexOf(_initSelectData.year); - index = index < 0 ? 0 : index; - } - _selectData.year = _dateTimeData.year[index]; - scrollCtrl[DateType.year] = FixedExtentScrollController( - initialItem: index, - ); - } - - /// ------月 - // 选中的月 用于之后 day 的计算 - int selectMonth = 1; - if (_dateItemModel.month) { - index = 0; - int begin = 1; - int end = 12; - // 限制区域 - if (intNotEmpty(minDate.month) && _selectData.year == minDate.year) { - begin = minDate.month!; - } - if (intNotEmpty(maxDate.month) && _selectData.year == maxDate.year) { - end = maxDate.month!; - } - - _dateTimeData.month = TimeUtils.calcMonth(begin: begin, end: end); - - if (_initSelectData.month != null) { - index = _dateTimeData.month.indexOf(_initSelectData.month); - index = index < 0 ? 0 : index; - } - selectMonth = _dateTimeData.month[index]; - _selectData.month = selectMonth; - scrollCtrl[DateType.month] = FixedExtentScrollController( - initialItem: index, - ); - } - - /// -------日 (有日肯定有 年月数据) - if (_dateItemModel.day) { - index = 0; - int begin = 1; - int end = 31; - // 限制区域 - if (intNotEmpty(minDate.day) || intNotEmpty(maxDate.day)) { - if (_selectData.year == minDate.year && - _selectData.month == minDate.month) { - begin = minDate.day!; - } - if (_selectData.year == maxDate.year && - _selectData.month == maxDate.month) { - end = maxDate.day!; - } - } - - _dateTimeData.day = TimeUtils.calcDay( - _initSelectData.year!, - selectMonth, - begin: begin, - end: end, - ); - - if (_initSelectData.day != null) { - index = _dateTimeData.day.indexOf(_initSelectData.day); - index = index < 0 ? 0 : index; - } - _selectData.day = _dateTimeData.day[index]; - scrollCtrl[DateType.day] = FixedExtentScrollController( - initialItem: index, - ); - } - - /// ---------时 - if (_dateItemModel.hour) { - index = 0; - int begin = 0; - int end = 23; - // 限制区域 - if (intNotEmpty(minDate.hour)) { - begin = minDate.hour!; - } - if (intNotEmpty(maxDate.hour)) { - end = maxDate.hour!; - } - - _dateTimeData.hour = TimeUtils.calcHour(begin: begin, end: end); - - if (_initSelectData.hour != null) { - index = _dateTimeData.hour.indexOf(_initSelectData.hour); - index = index < 0 ? 0 : index; - } - _selectData.hour = _dateTimeData.hour[index]; - scrollCtrl[DateType.hour] = FixedExtentScrollController( - initialItem: index, - ); - } - - /// ---------分 - if (_dateItemModel.minute) { - index = 0; - int begin = 0; - int end = 59; - // 限制区域 - if (intNotEmpty(minDate.minute) || intNotEmpty(maxDate.minute)) { - if (_dateItemModel.hour) { - // 如果有上级 还有时,要根据时再判断 - if (_selectData.hour == minDate.hour) { - begin = minDate.minute!; - } - if (_selectData.hour == maxDate.hour) { - end = maxDate.minute!; - } - } else { - // 上级没有时间限制 直接取 - if (intNotEmpty(minDate.minute)) { - begin = minDate.minute!; - } - if (intNotEmpty(maxDate.minute)) { - end = maxDate.minute!; - } - } - } - - _dateTimeData.minute = TimeUtils.calcMinAndSecond(begin: begin, end: end); - - if (_initSelectData.minute != null) { - index = _dateTimeData.minute.indexOf(_initSelectData.minute); - index = index < 0 ? 0 : index; - } - _selectData.minute = _dateTimeData.minute[index]; - scrollCtrl[DateType.minute] = FixedExtentScrollController( - initialItem: index, - ); - } - - /// --------秒 - if (_dateItemModel.second) { - index = 0; - int begin = 0; - int end = 59; - // 限制区域 - if (intNotEmpty(minDate.second) || intNotEmpty(maxDate.second)) { - if (_dateItemModel.hour && _dateItemModel.minute) { - // 如果有上级 还有时 分,要根据时分再判断 - if (_selectData.hour == minDate.hour && - _selectData.minute == minDate.minute) { - begin = minDate.second!; - } - if (_selectData.hour == maxDate.hour && - _selectData.minute == maxDate.minute) { - end = maxDate.second!; - } - } else if (_dateItemModel.minute) { - /// 上级没有时,只有分限制 - if (_selectData.minute == minDate.minute) { - begin = minDate.second!; - } - if (_selectData.minute == maxDate.minute) { - end = maxDate.second!; - } - } else { - /// 上级没有时间限制 直接取 - if (intNotEmpty(minDate.second)) { - begin = minDate.second!; - } - if (intNotEmpty(maxDate.second)) { - end = maxDate.second!; - } - } - } - - _dateTimeData.second = TimeUtils.calcMinAndSecond(begin: begin, end: end); - - if (_initSelectData.second != null) { - index = _dateTimeData.second.indexOf(_initSelectData.second); - index = index < 0 ? 0 : index; - } - _selectData.second = _dateTimeData.second[index]; - scrollCtrl[DateType.second] = FixedExtentScrollController( - initialItem: index, + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_isInitialized) return; + + _isInitialized = true; + _suffix = widget.route.suffix ?? Suffix.fromContext(context); + _selection = DateSelectionModule( + mode: widget.mode, + initial: widget.initData, + min: widget.minDate, + max: widget.maxDate, + ); + _selectData = _selection.state.toDuration(); + for (final type in _selection.state.types) { + scrollCtrl[type] = FixedExtentScrollController( + initialItem: _selection.state.position(type), ); } } @override void dispose() { - scrollCtrl.forEach((key, value) { - value.dispose(); - }); + for (final controller in scrollCtrl.values) { + controller.dispose(); + } super.dispose(); } @override Widget build(BuildContext context) { - return GestureDetector( - child: AnimatedBuilder( - animation: widget.route.animation!, - builder: (BuildContext context, Widget? child) { - return ClipRect( - child: CustomSingleChildLayout( - delegate: _BottomPickerLayout( - widget.route.animation!.value, - _pickerStyle, - ), - child: GestureDetector( - child: Material( - color: Colors.transparent, - child: _renderPickerView(), - ), - ), - ), - ); - }, - ), + return PickerSheet( + animation: widget.route.animation!, + style: _pickerStyle, + safeAreaBottom: widget.safeAreaBottom, + body: _renderItemView(), + onConfirm: () => widget.route.onConfirm?.call(_selectData), ); } - void _setPicker(DateType dateType, int selectIndex) { - // 得到新的选中的数据 - var selectValue = _dateTimeData.getListByName(dateType)[selectIndex]; - // 更新选中数据 - _selectData.setSingle(dateType, selectValue); - - switch (dateType) { - case DateType.year: - _setYear(); - break; - case DateType.month: - _setMonth(); - break; - case DateType.day: - break; - case DateType.hour: - _setHour(); - break; - case DateType.minute: - _setMinute(); - break; - case DateType.second: - break; - } - _notifyLocationChanged(); - } - - // -------------------- set begin ------------ - void _setYear() { - // 可能造成 月 日 list的改变 - if (_dateItemModel.month) { - // 月的数据是否需要更新 - bool updateMonth = false; - bool updateDay = false; - - /// 如果只有月 - int beginMonth = 1; - int endMonth = 12; - // 限制区域 - if (intNotEmpty(minDate.month) && _selectData.year == minDate.year) { - beginMonth = minDate.month!; - } - if (intNotEmpty(maxDate.month) && _selectData.year == maxDate.year) { - endMonth = maxDate.month!; - } - - var resultMonth = TimeUtils.calcMonth(begin: beginMonth, end: endMonth); - - int jumpToIndexMonth = 0; - - if (!listEquals(_dateTimeData.month, resultMonth)) { - //可能 选中的月份 由于设置了新数据后没有了 - // 小于不用考虑 会进else - if (_selectData.month! > resultMonth.last) { - jumpToIndexMonth = resultMonth.length - 1; - } else { - jumpToIndexMonth = resultMonth.indexOf(_selectData.month); - } - jumpToIndexMonth = jumpToIndexMonth < 0 ? 0 : jumpToIndexMonth; - _selectData.month = resultMonth[jumpToIndexMonth]; - updateMonth = true; - } - - /// 还有 日 - int jumpToIndexDay = 0; - // 新的day 数据 - List resultDay = []; - if (_dateItemModel.day) { - int beginDay = 1; - int endDay = 31; - // 限制区域 - if (intNotEmpty(minDate.day) || intNotEmpty(maxDate.day)) { - if (_selectData.year == minDate.year && - _selectData.month == minDate.month) { - beginDay = minDate.day!; - } - if (_selectData.year == maxDate.year && - _selectData.month == maxDate.month) { - endDay = maxDate.day!; - } - } - resultDay = TimeUtils.calcDay( - _selectData.year!, - _selectData.month!, - begin: beginDay, - end: endDay, - ); - - if (!listEquals(_dateTimeData.day, resultDay)) { - //可能 选中的年 月份 由于设置了新数据后没有了 - // 小于不用考虑 会进else - if (_selectData.day! > resultDay.last) { - jumpToIndexDay = resultDay.length - 1; - } else { - jumpToIndexDay = resultDay.indexOf(_selectData.day); - } - jumpToIndexDay = jumpToIndexDay < 0 ? 0 : jumpToIndexDay; - _selectData.day = resultDay[jumpToIndexDay]; - updateDay = true; - } - } - - if (updateMonth || updateDay) { - setState(() { - if (updateMonth) { - _dateTimeData.month = resultMonth; - scrollCtrl[DateType.month]?.jumpToItem(jumpToIndexMonth); - } - if (updateDay) { - _dateTimeData.day = resultDay; - scrollCtrl[DateType.day]?.jumpToItem(jumpToIndexDay); - } - - /// FIX:https://github.com/flutter/flutter/issues/22999 - pickerItemHeight = - _pickerStyle.pickerItemHeight - Random().nextDouble() / 100000000; - }); - } - } - } - - void _setMonth() { - // 可能造成 日 list的改变 - bool updateDay = false; - int jumpToIndexDay = 0; - // 新的day 数据 - List resultDay = []; - if (_dateItemModel.day) { - int beginDay = 1; - int endDay = 31; - // 限制区域 - if (intNotEmpty(minDate.day) || intNotEmpty(maxDate.day)) { - if (_selectData.year == minDate.year && - _selectData.month == minDate.month) { - beginDay = minDate.day!; - } - if (_selectData.year == maxDate.year && - _selectData.month == maxDate.month) { - endDay = maxDate.day!; - } - } - resultDay = TimeUtils.calcDay( - _selectData.year!, - _selectData.month!, - begin: beginDay, - end: endDay, - ); + void _setPicker(DateType type, int index) { + final previous = _selection.state; + final next = _selection.select(type, index); + final columnsChanged = next.types.any( + (column) => !listEquals(previous.values(column), next.values(column)), + ); - if (!listEquals(_dateTimeData.day, resultDay)) { - //可能 选中的年 月份 由于设置了新数据后没有了 - // 小于不用考虑 会进else - if (_selectData.day! > resultDay.last) { - jumpToIndexDay = resultDay.length - 1; - } else { - jumpToIndexDay = resultDay.indexOf(_selectData.day); + setState(() { + _copySelection(next.toDuration()); + for (final column in next.types) { + if (column != type && + previous.position(column) != next.position(column)) { + scrollCtrl[column]?.jumpToItem(next.position(column)); } - jumpToIndexDay = jumpToIndexDay < 0 ? 0 : jumpToIndexDay; - _selectData.day = resultDay[jumpToIndexDay]; - updateDay = true; } - } - if (updateDay) { - setState(() { - _dateTimeData.day = resultDay; - scrollCtrl[DateType.day]?.jumpToItem(jumpToIndexDay); - - /// FIX:https://github.com/flutter/flutter/issues/22999 + if (columnsChanged) { + // Work around https://github.com/flutter/flutter/issues/22999. pickerItemHeight = _pickerStyle.pickerItemHeight - Random().nextDouble() / 100000000; - }); - } - } - - void _setHour() { - // 可能造成 分 秒 list的改变 - if (_dateItemModel.minute) { - // 月的数据是否需要更新 - bool updateMinute = false; - bool updateSecond = false; - - /// 如果只有分 - int beginMinute = 0; - int endMinute = 59; - // 限制区域 - if (intNotEmpty(minDate.minute) && _selectData.hour == minDate.hour) { - beginMinute = minDate.minute!; - } - if (intNotEmpty(maxDate.minute) && _selectData.hour == maxDate.hour) { - endMinute = maxDate.minute!; - } - - var resultMinute = TimeUtils.calcMinAndSecond( - begin: beginMinute, - end: endMinute, - ); - - int jumpToIndexMinute = 0; - - if (!listEquals(_dateTimeData.month, resultMinute)) { - //可能 选中的时间 由于设置了新数据后没有了 - // 小于不用考虑 会进else - if (_selectData.minute! > resultMinute.last) { - jumpToIndexMinute = resultMinute.length - 1; - } else { - jumpToIndexMinute = resultMinute.indexOf(_selectData.minute); - } - jumpToIndexMinute = jumpToIndexMinute < 0 ? 0 : jumpToIndexMinute; - _selectData.minute = resultMinute[jumpToIndexMinute]; - updateMinute = true; - } - - /// 还有 秒 - int jumpToIndexSecond = 0; - // 新的day 数据 - List resultSecond = []; - if (_dateItemModel.second) { - int beginSecond = 0; - int endSecond = 59; - // 限制区域 - if (intNotEmpty(minDate.second) || intNotEmpty(maxDate.second)) { - if (_selectData.hour == minDate.hour && - _selectData.minute == minDate.minute) { - beginSecond = minDate.second!; - } - if (_selectData.hour == maxDate.hour && - _selectData.minute == maxDate.minute) { - endSecond = maxDate.second!; - } - } - resultSecond = TimeUtils.calcMinAndSecond( - begin: beginSecond, - end: endSecond, - ); - - if (!listEquals(_dateTimeData.second, resultSecond)) { - //可能 选中的时 分 由于设置了新数据后没有了 - // 小于不用考虑 会进else - if (_selectData.second! > resultSecond.last) { - jumpToIndexSecond = resultSecond.length - 1; - } else { - jumpToIndexSecond = resultSecond.indexOf(_selectData.second); - } - jumpToIndexSecond = jumpToIndexSecond < 0 ? 0 : jumpToIndexSecond; - _selectData.second = resultSecond[jumpToIndexSecond]; - updateSecond = true; - } - } - - if (updateMinute || updateSecond) { - setState(() { - if (updateMinute) { - _dateTimeData.minute = resultMinute; - scrollCtrl[DateType.minute]?.jumpToItem(jumpToIndexMinute); - } - if (updateSecond) { - _dateTimeData.second = resultSecond; - scrollCtrl[DateType.second]?.jumpToItem(jumpToIndexSecond); - } - - /// FIX:https://github.com/flutter/flutter/issues/22999 - pickerItemHeight = - _pickerStyle.pickerItemHeight - Random().nextDouble() / 100000000; - }); - } - } - } - - void _setMinute() { - // 可能造成 秒 list的改变 - bool updateSecond = false; - int jumpToIndexSecond = 0; - // 新的day 数据 - List resultSecond = []; - if (_dateItemModel.second) { - int beginSecond = 0; - int endSecond = 59; - // 限制区域 - if (intNotEmpty(minDate.second) || intNotEmpty(maxDate.second)) { - if (_dateItemModel.hour) { - // 如果上面还有 时 - if (_selectData.hour == minDate.hour && - _selectData.minute == minDate.minute) { - beginSecond = minDate.second!; - } - if (_selectData.hour == maxDate.hour && - _selectData.minute == maxDate.minute) { - endSecond = maxDate.second!; - } - } else { - // 没有时,分秒 - if (_selectData.minute == minDate.minute) { - beginSecond = minDate.second!; - } - if (_selectData.minute == maxDate.minute) { - endSecond = maxDate.second!; - } - } - } - resultSecond = TimeUtils.calcMinAndSecond( - begin: beginSecond, - end: endSecond, - ); - - if (!listEquals(_dateTimeData.second, resultSecond)) { - //可能 选中的分 由于设置了新数据后没有了 - // 小于不用考虑 会进else - if (_selectData.second! > resultSecond.last) { - jumpToIndexSecond = resultSecond.length - 1; - } else { - jumpToIndexSecond = resultSecond.indexOf(_selectData.second); - } - jumpToIndexSecond = jumpToIndexSecond < 0 ? 0 : jumpToIndexSecond; - _selectData.second = resultSecond[jumpToIndexSecond]; - updateSecond = true; } - } - if (updateSecond) { - setState(() { - _dateTimeData.second = resultSecond; - scrollCtrl[DateType.second]?.jumpToItem(jumpToIndexSecond); - - /// FIX:https://github.com/flutter/flutter/issues/22999 - pickerItemHeight = - _pickerStyle.pickerItemHeight - Random().nextDouble() / 100000000; - }); - } - } - - // -------------------- set end ------------ + }); - void _notifyLocationChanged() { widget.route.onChanged?.call(_selectData); } - double _pickerFontSize(String text) { - if (text == '') return 18.0; - - if (_dateItemModel.length == 6 && (text.length > 4 && text.length <= 6)) { - return 16.0; - } - - if (text.length <= 6) { - return 18.0; - } else if (text.length < 9) { - return 16.0; - } else if (text.length < 13) { - return 12.0; - } else { - return 10.0; - } - } - - Widget _renderPickerView() { - Widget itemView = _renderItemView(); - - if (!_pickerStyle.showTitleBar && _pickerStyle.menu == null) { - return itemView; - } - List viewList = []; - if (_pickerStyle.showTitleBar) { - viewList.add(_titleView()); - } - if (_pickerStyle.menu != null) { - viewList.add(_pickerStyle.menu!); - } - viewList.add(itemView); - - return Column(children: viewList); + void _copySelection(PDuration value) { + _selectData + ..year = value.year + ..month = value.month + ..day = value.day + ..hour = value.hour + ..minute = value.minute + ..second = value.second; } Widget _renderItemView() { - // 选择器 - List pickerList = []; - if (_dateItemModel.year) pickerList.add(pickerView(DateType.year)); - if (_dateItemModel.month) pickerList.add(pickerView(DateType.month)); - if (_dateItemModel.day) pickerList.add(pickerView(DateType.day)); - if (_dateItemModel.hour) pickerList.add(pickerView(DateType.hour)); - if (_dateItemModel.minute) pickerList.add(pickerView(DateType.minute)); - if (_dateItemModel.second) pickerList.add(pickerView(DateType.second)); - - return Container( - height: _pickerStyle.pickerHeight, - color: _pickerStyle.backgroundColor, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: pickerList, - ), + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _selection.state.types.map(pickerView).toList(), ); } - /// CupertinoPicker.builder - Widget pickerView(DateType dateType) { + Widget pickerView(DateType type) { + final values = _selection.state.values(type); return Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 2), child: CupertinoPicker.builder( - /// key :年月拼接 就不会重复了 fixme - /// 最好别使用key 会生成新的widget - /// 官方的bug : https://github.com/flutter/flutter/issues/22999 - /// 临时方法 通过修改height - scrollController: scrollCtrl[dateType], + scrollController: scrollCtrl[type], itemExtent: pickerItemHeight, selectionOverlay: _pickerStyle.itemOverlay, - onSelectedItemChanged: - (int selectIndex) => _setPicker(dateType, selectIndex), - childCount: _dateTimeData.getListByName(dateType).length, + onSelectedItemChanged: (index) => _setPicker(type, index), + childCount: values.length, itemBuilder: (_, index) { - String text = - '${_dateTimeData.getListByName(dateType)[index]}${widget.route.suffix?.getSingle(dateType)}'; - return Align( - alignment: Alignment.center, - child: Text( - text, - style: TextStyle( - color: _pickerStyle.textColor, - fontSize: _pickerStyle.textSize ?? _pickerFontSize(text), + final text = '${values[index]}${_suffix.getSingle(type)}'; + return Semantics( + label: text, + child: Align( + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + color: _pickerStyle.textColor, + fontSize: _pickerStyle.textSize ?? 18.0, + ), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, ), - textAlign: TextAlign.start, ), ); }, @@ -819,71 +211,4 @@ class _PickerState extends State { ), ); } - - // 选择器上面的view - Widget _titleView() { - return Container( - height: _pickerStyle.pickerTitleHeight, - decoration: _pickerStyle.headDecoration, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - /// 取消按钮 - InkWell( - onTap: () => Navigator.pop(context, false), - child: _pickerStyle.cancelButton, - ), - - /// 标题 - Expanded(child: _pickerStyle.title), - - /// 确认按钮 - InkWell( - onTap: () { - widget.route.onConfirm?.call(_selectData); - Navigator.pop(context, true); - }, - child: _pickerStyle.commitButton, - ), - ], - ), - ); - } -} - -class _BottomPickerLayout extends SingleChildLayoutDelegate { - _BottomPickerLayout(this.progress, this.pickerStyle); - - final double progress; - final PickerStyle pickerStyle; - - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - double maxHeight = pickerStyle.pickerHeight; - if (pickerStyle.showTitleBar) { - maxHeight += pickerStyle.pickerTitleHeight; - } - if (pickerStyle.menu != null) { - maxHeight += pickerStyle.menuHeight; - } - - return BoxConstraints( - minWidth: constraints.maxWidth, - maxWidth: constraints.maxWidth, - minHeight: 0.0, - maxHeight: maxHeight, - ); - } - - @override - Offset getPositionForChild(Size size, Size childSize) { - double height = size.height - childSize.height * progress; - return Offset(0.0, height); - } - - @override - bool shouldRelayout(_BottomPickerLayout oldDelegate) { - return progress != oldDelegate.progress; - } } diff --git a/pubspec.lock b/pubspec.lock index cb4513a..de1bce4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,50 +5,50 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.flutter-io.cn" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.flutter-io.cn" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf - url: "https://pub.flutter-io.cn" + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -59,78 +59,91 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" - url: "https://pub.flutter-io.cn" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" - url: "https://pub.flutter-io.cn" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.flutter-io.cn" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.flutter-io.cn" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.flutter-io.cn" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.flutter-io.cn" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -140,66 +153,66 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.flutter-io.cn" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" - url: "https://pub.flutter-io.cn" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.flutter-io.cn" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" - url: "https://pub.flutter-io.cn" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" - url: "https://pub.flutter-io.cn" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.11" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b - url: "https://pub.flutter-io.cn" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "15.2.0" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index b68573d..c48555e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_pickers description: Flutter selector library, including date and time selector, single item selector (gender, occupation, education, constellation), city address selector, etc. #description2: flutter 选择器类库,包括日期及时间选择器(可设置范围)、单项选择器(可用于性别、职业、学历、星座等)、城市地址选择器(分省级、地级及县级)、数字选择器(可用于年龄、身高、体重、温度等)等…… 欢迎Fork & pr贡献您的代码,大家共同学习 -version: 2.2.0 +version: 2.2.3 homepage: https://github.com/longer96/flutter_pickers repository: https://github.com/longer96/flutter_pickers issue_tracker: https://github.com/longer96/flutter_pickers/issues @@ -13,6 +13,9 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter + intl: ^0.20.2 dev_dependencies: flutter_test: @@ -20,4 +23,5 @@ dev_dependencies: flutter_lints: ^5.0.0 flutter: + generate: true # This package is a pure Dart package that provides Flutter widgets \ No newline at end of file diff --git a/test/address_picker_state_test.dart b/test/address_picker_state_test.dart new file mode 100644 index 0000000..f7f585d --- /dev/null +++ b/test/address_picker_state_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/address_picker/locations_data.dart'; +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Address Picker keeps addAllItem local to the open picker', ( + tester, + ) async { + final original = Address.addAllItem; + Address.addAllItem = true; + addTearDown(() => Address.addAllItem = original); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => TextButton( + onPressed: () { + Pickers.showAddressPicker(context, addAllItem: false); + }, + child: const Text('Show address picker'), + ), + ), + ), + ); + + await tester.tap(find.text('Show address picker')); + await tester.pumpAndSettle(); + + expect(Address.addAllItem, isTrue); + expect(find.text('全部'), findsNothing); + + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + }); +} diff --git a/test/cascading_selection_test.dart b/test/cascading_selection_test.dart new file mode 100644 index 0000000..74ed39f --- /dev/null +++ b/test/cascading_selection_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_pickers/src/cascading_picker/cascading_selection.dart'; +import 'package:flutter_pickers/src/cascading_picker/nested_map_adapter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Linked Picker resets downstream columns through its data adapter', () { + final selection = CascadingSelectionModule( + adapter: NestedMapAdapter( + data: const { + 'A': { + 'A1': ['x', 'y'], + 'A2': ['z'], + }, + 'B': { + 'B1': ['q'], + }, + }, + columnCount: 3, + ), + initial: const ['A', 'A1', 'y'], + ); + + final state = selection.select(0, 1); + + expect(state.selection, ['B', 'B1', 'q']); + expect(state.positions, [1, 0, 0]); + expect(state.column(1), ['B1']); + expect(state.column(2), ['q']); + }); + + test('Linked Picker fills columns after an early terminal branch', () { + final selection = CascadingSelectionModule( + adapter: NestedMapAdapter( + data: const { + 'short': 'value', + 'deep': { + 'child': ['leaf'], + }, + }, + columnCount: 3, + ), + initial: const ['short'], + ); + + expect(selection.state.selection, ['short', 'value', '']); + expect(selection.state.positions, [0, 0, 0]); + expect(selection.state.column(2), ['']); + }); + + test('Linked Picker state exposes immutable selection snapshots', () { + final selection = CascadingSelectionModule( + adapter: NestedMapAdapter( + data: const { + 'A': ['A1'], + }, + columnCount: 2, + ), + ); + + expect(() => selection.state.selection.add('A2'), throwsUnsupportedError); + expect(() => selection.state.positions[0] = 1, throwsUnsupportedError); + expect(() => selection.state.column(0).add('B'), throwsUnsupportedError); + }); +} diff --git a/test/date_selection_test.dart b/test/date_selection_test.dart new file mode 100644 index 0000000..e1bf112 --- /dev/null +++ b/test/date_selection_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter_pickers/src/date_picker/date_selection.dart'; +import 'package:flutter_pickers/time_picker/model/date_mode.dart'; +import 'package:flutter_pickers/time_picker/model/date_type.dart'; +import 'package:flutter_pickers/time_picker/model/pduration.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Date Picker clamps leap day when the selected year changes', () { + final selection = DateSelectionModule( + mode: DateMode.YMD, + initial: PDuration(year: 2024, month: 2, day: 29), + min: PDuration(year: 2023, month: 1, day: 1), + max: PDuration(year: 2024, month: 12, day: 31), + ); + + final state = selection.select(DateType.year, 0); + + expect(state.selected(DateType.year), 2023); + expect(state.selected(DateType.month), 2); + expect(state.selected(DateType.day), 28); + expect(state.values(DateType.day).last, 28); + }); + + test('Date Picker propagates an hour change into the minute range', () { + final selection = DateSelectionModule( + mode: DateMode.HM, + initial: PDuration(hour: 10, minute: 45), + min: PDuration(hour: 10, minute: 30), + max: PDuration(hour: 11, minute: 15), + ); + + final state = selection.select(DateType.hour, 1); + + expect(state.selected(DateType.hour), 11); + expect(state.values(DateType.minute), List.generate(16, (index) => index)); + expect(state.selected(DateType.minute), 15); + }); + + test('Date Picker propagates a minute change into the second range', () { + final selection = DateSelectionModule( + mode: DateMode.HMS, + initial: PDuration(hour: 10, minute: 30, second: 45), + min: PDuration(hour: 10, minute: 30, second: 20), + max: PDuration(hour: 10, minute: 31, second: 10), + ); + + final state = selection.select(DateType.minute, 1); + + expect(state.selected(DateType.minute), 31); + expect(state.values(DateType.second), List.generate(11, (index) => index)); + expect(state.selected(DateType.second), 10); + }); + + test('Date Picker applies month and day bounds at the selected year', () { + final selection = DateSelectionModule( + mode: DateMode.YMD, + initial: PDuration(year: 2024, month: 6, day: 15), + min: PDuration(year: 2023, month: 3, day: 10), + max: PDuration(year: 2024, month: 8, day: 20), + ); + + final state = selection.select(DateType.year, 0); + + expect( + state.values(DateType.month), List.generate(10, (index) => index + 3)); + expect(state.selected(DateType.month), 6); + expect(state.values(DateType.day).first, 1); + + final march = selection.select(DateType.month, 0); + expect(march.selected(DateType.month), 3); + expect(march.values(DateType.day).first, 10); + expect(march.selected(DateType.day), 15); + }); + + test('Date Picker does not constrain a day that has no explicit bound', () { + final selection = DateSelectionModule( + mode: DateMode.YMD, + initial: PDuration(year: 2024, month: 1, day: 15), + min: PDuration(year: 2023), + max: PDuration(year: 2024, month: 1), + ); + + expect(selection.state.values(DateType.day).last, 31); + expect(selection.state.selected(DateType.day), 15); + }); + + test('Date Picker state exposes immutable column snapshots', () { + final selection = DateSelectionModule( + mode: DateMode.YM, + initial: PDuration(year: 2024, month: 6), + min: PDuration(year: 2020), + max: PDuration(year: 2030), + ); + + expect( + () => selection.state.values(DateType.month).add(13), + throwsUnsupportedError, + ); + }); + + const expectedColumnCounts = { + DateMode.YMDHMS: 6, + DateMode.YMDHM: 5, + DateMode.YMDH: 4, + DateMode.YMD: 3, + DateMode.YM: 2, + DateMode.Y: 1, + DateMode.MDHMS: 5, + DateMode.MDHM: 4, + DateMode.MDH: 3, + DateMode.MD: 2, + DateMode.HMS: 3, + DateMode.HM: 2, + DateMode.MS: 2, + DateMode.S: 1, + DateMode.M: 1, + DateMode.H: 1, + }; + + for (final entry in expectedColumnCounts.entries) { + test('Date Picker exposes ${entry.value} columns for ${entry.key}', () { + final selection = DateSelectionModule( + mode: entry.key, + initial: PDuration( + year: 2024, + month: 6, + day: 15, + hour: 12, + minute: 30, + second: 30, + ), + min: PDuration(year: 2020), + max: PDuration(year: 2030), + ); + + expect(selection.state.types.length, entry.value); + for (final type in selection.state.types) { + expect(selection.state.values(type), isNotEmpty); + } + }); + } +} diff --git a/test/flutter_picker_test.dart b/test/flutter_picker_test.dart deleted file mode 100644 index 7c519e9..0000000 --- a/test/flutter_picker_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/foundation.dart'; - -void main() { - // List fruits = ['apples', 'bananas', 1]; - // List foods = ['apples', 'bananas', 1]; - - // var a = listEquals(fruits, foods); - // print('longer >>> ${a}'); - - // test('通过城市code 返回城市名', () { - // Address.getCityNameByCode( - // provinceCode: "510000", cityCode: "510100", townCode: "510104"); - // Address.getCityNameByCode( - // provinceCode: "510000", cityCode: "510100", townCode: "5104"); - // Address.getCityNameByCode(provinceCode: "510000", cityCode: "510102"); - // Address.getCityNameByCode(provinceCode: "510000", cityCode: "510100"); - // Address.getCityNameByCode(provinceCode: "510000", cityCode: "5101020"); - // Address.getCityNameByCode(provinceCode: "5100", cityCode: "5101020"); - // Address.getCityNameByCode(); - // }); - // - // test('根据城市名 找到城市编码', () { - // // Address.getCityCodeByName(provinceName: '四川省', cityName: '成都市', townName: '武侯区'); - // Address.getCityCodeByName( - // townName: '四川省', cityName: '成都市', provinceName: '武侯区'); - // // Address.getCityCodeByName(provinceName: '四川省'); - // // Address.getCityCodeByName(provinceName: '四川省', cityName: '成都市'); - // // Address.getCityCodeByName(cityName: '成都市'); - // // Address.getCityCodeByName(); - // }); - - // test('adds one to input values', () { - // print('longer >>> ${DateMode.MDHMS.toString()}'); - // print('longer >>> ${DateMode.MDHMS.toString().length - 9}'); - // }); - - /// 计算 - // var h = TimeUtils.calcHour(); - // var m = TimeUtils.calcMonth(); - // var d = TimeUtils.calcDay(2021,1); - // - // print('longer >>> $h'); - // print('longer >>> $m'); - // print('longer >>> $d'); - - // var picker = DefaultPickerStyle.dark(); - // print(picker.menuHeight); - - // var m = PDuration(year: 2011); - // print('longer2 >>> ${m.toString()}'); - // {year: 2011, month: null, day: null, hour: null, minute: null, second: null} - - dynamic a = 111; - debugPrint('longer >>> $a'); - a = 'aaaaa'; - debugPrint('longer >>> $a'); - a = [11, 22, 33]; - debugPrint('longer >>> $a'); -} diff --git a/test/multiple_picker_editor_test.dart b/test/multiple_picker_editor_test.dart new file mode 100644 index 0000000..cb54896 --- /dev/null +++ b/test/multiple_picker_editor_test.dart @@ -0,0 +1,137 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_pickers/style/picker_style.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('multi picker editor and wheels stay synchronized', ( + tester, + ) async { + final editorController = TextEditingController(); + final editorFocusNode = FocusNode(); + addTearDown(editorController.dispose); + addTearDown(editorFocusNode.dispose); + addTearDown(tester.view.reset); + + List? changedSelection; + List? confirmedSelection; + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return Scaffold( + body: TextButton( + onPressed: () { + Pickers.showMultiPicker( + context, + data: [ + [158, 159, 160, 161, 162, 163], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + ], + selectData: const [160, 1], + pickerStyle: PickerStyle( + commitButton: const Text( + 'Confirm', + key: Key('confirm-picker'), + ), + ), + editorHeight: 64, + editorBuilder: (context, selection, updateSelection) { + final value = '${selection[0]}.${selection[1]}'; + if (editorController.text != value) { + editorController.value = TextEditingValue( + text: value, + selection: TextSelection.collapsed( + offset: value.length, + ), + ); + } + + return TextField( + key: const Key('picker-editor'), + controller: editorController, + focusNode: editorFocusNode, + onChanged: (value) { + final parts = value.split('.'); + if (parts.length != 2) return; + updateSelection([ + int.tryParse(parts[0]), + int.tryParse(parts[1]), + ]); + }, + ); + }, + onChanged: (selection, positions) { + changedSelection = List.of(selection); + }, + onConfirm: (selection, positions) { + confirmedSelection = List.of(selection); + }, + ); + }, + child: const Text('Show picker'), + ), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show picker')); + await tester.pumpAndSettle(); + + expect(editorController.text, '160.1'); + + await tester.drag( + find.byType(CupertinoPicker).first, + const Offset(0, -40), + ); + await tester.pumpAndSettle(); + + expect(editorController.text, '161.1'); + expect(changedSelection, [161, 1]); + + await tester.enterText(find.byKey(const Key('picker-editor')), '162.2'); + await tester.pumpAndSettle(); + + expect(changedSelection, [162, 2]); + expect(editorFocusNode.hasFocus, isTrue); + + final editorTopBeforeKeyboard = + tester.getTopLeft(find.byKey(const Key('picker-editor'))).dy; + const keyboardInset = 200.0; + tester.view.viewInsets = FakeViewPadding( + bottom: keyboardInset * tester.view.devicePixelRatio, + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 125)); + + final editorTopDuringAnimation = + tester.getTopLeft(find.byKey(const Key('picker-editor'))).dy; + expect(editorTopDuringAnimation, lessThan(editorTopBeforeKeyboard)); + expect( + editorTopDuringAnimation, + greaterThan(editorTopBeforeKeyboard - keyboardInset), + ); + + await tester.pumpAndSettle(); + final editorTopAboveKeyboard = + tester.getTopLeft(find.byKey(const Key('picker-editor'))).dy; + expect( + editorTopAboveKeyboard, + closeTo(editorTopBeforeKeyboard - keyboardInset, 0.01), + ); + + await tester.tap(find.byKey(const Key('confirm-picker'))); + await tester.pump(); + + expect(confirmedSelection, [162, 2]); + expect(editorFocusNode.hasFocus, isFalse); + + tester.view.resetViewInsets(); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + }); +} diff --git a/test/picker_route_shell_test.dart b/test/picker_route_shell_test.dart new file mode 100644 index 0000000..a1b8faa --- /dev/null +++ b/test/picker_route_shell_test.dart @@ -0,0 +1,200 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_pickers/style/picker_style.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + for (final fullScreenCase in const [ + _FullScreenCase('iOS', TargetPlatform.iOS, 34), + _FullScreenCase('Android', TargetPlatform.android, 24), + ]) { + testWidgets( + 'all picker wheels respect the ${fullScreenCase.name} full-screen safe area', + (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(390, 844); + tester.view.viewPadding = FakeViewPadding( + bottom: fullScreenCase.bottomInset, + ); + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(platform: fullScreenCase.platform), + home: Builder( + builder: (context) => Column( + children: [ + _showButton( + key: 'single', + onPressed: () => Pickers.showSinglePicker( + context, + data: const ['A', 'B'], + ), + ), + _showButton( + key: 'multiple', + onPressed: () => Pickers.showMultiPicker( + context, + data: const [ + ['A', 'B'], + ['1', '2'], + ], + ), + ), + _showButton( + key: 'linked', + onPressed: () => Pickers.showMultiLinkPicker( + context, + data: const { + 'A': ['A1', 'A2'], + 'B': ['B1', 'B2'], + }, + columnNum: 2, + ), + ), + _showButton( + key: 'address', + onPressed: () => Pickers.showAddressPicker(context), + ), + _showButton( + key: 'date', + onPressed: () => Pickers.showDatePicker(context), + ), + ], + ), + ), + ), + ); + + for (final key in ['single', 'multiple', 'linked', 'address', 'date']) { + await tester.tap(find.byKey(Key(key))); + await tester.pumpAndSettle(); + + final wheelBottom = + tester.getBottomRight(find.byType(CupertinoPicker).first).dy; + expect( + wheelBottom, + lessThanOrEqualTo(844 - fullScreenCase.bottomInset + 0.01), + reason: '${fullScreenCase.name}: $key', + ); + + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + expect( + tester.takeException(), + isNull, + reason: '${fullScreenCase.name}: $key', + ); + } + }, + ); + } + + testWidgets('safeArea false allows a picker wheel to reach the screen edge', ( + tester, + ) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(390, 844); + tester.view.viewPadding = const FakeViewPadding(bottom: 34); + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => _showButton( + key: 'show', + onPressed: () => Pickers.showSinglePicker( + context, + data: const ['A', 'B'], + pickerStyle: PickerStyle( + showTitleBar: false, + safeArea: false, + ), + ), + ), + ), + ), + ); + + await tester.tap(find.byKey(const Key('show'))); + await tester.pumpAndSettle(); + + final wheelBottom = + tester.getBottomRight(find.byType(CupertinoPicker).first).dy; + expect(wheelBottom, closeTo(844, 0.01)); + + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + }); + + testWidgets('editor stays above the keyboard and full-screen safe area', ( + tester, + ) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(390, 844); + tester.view.viewPadding = const FakeViewPadding(bottom: 34); + addTearDown(tester.view.reset); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => _showButton( + key: 'editor', + onPressed: () => Pickers.showMultiPicker( + context, + data: const [ + ['A', 'B'], + ], + editorHeight: 64, + editorBuilder: (_, selection, updateSelection) { + return const SizedBox( + key: Key('picker-editor-safe-area'), + height: 64, + ); + }, + ), + ), + ), + ), + ); + + await tester.tap(find.byKey(const Key('editor'))); + await tester.pumpAndSettle(); + expect( + tester + .getBottomRight(find.byKey(const Key('picker-editor-safe-area'))) + .dy, + closeTo(810, 0.01), + ); + + tester.view.viewInsets = const FakeViewPadding(bottom: 200); + await tester.pumpAndSettle(); + expect( + tester + .getBottomRight(find.byKey(const Key('picker-editor-safe-area'))) + .dy, + closeTo(610, 0.01), + ); + + await tester.tapAt(const Offset(10, 10)); + tester.view.resetViewInsets(); + await tester.pumpAndSettle(); + }); +} + +Widget _showButton({required String key, required VoidCallback onPressed}) { + return TextButton( + key: Key(key), + onPressed: onPressed, + child: Text('Show $key'), + ); +} + +class _FullScreenCase { + const _FullScreenCase(this.name, this.platform, this.bottomInset); + + final String name; + final TargetPlatform platform; + final double bottomInset; +} diff --git a/test/picker_style_context_test.dart b/test/picker_style_context_test.dart new file mode 100644 index 0000000..1c32b23 --- /dev/null +++ b/test/picker_style_context_test.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_pickers/style/picker_style.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('PickerStyle resolves against each picker context', + (tester) async { + final style = PickerStyle(); + await tester.pumpWidget( + MaterialApp( + home: Column( + children: [ + Theme( + data: ThemeData(primaryColor: Colors.red), + child: Builder( + builder: (context) => TextButton( + key: const Key('show-red-picker'), + onPressed: () { + Pickers.showSinglePicker( + context, + data: const ['Dart', 'Flutter'], + pickerStyle: style, + ); + }, + child: const Text('Show red picker'), + ), + ), + ), + Theme( + data: ThemeData(primaryColor: Colors.green), + child: Builder( + builder: (context) => TextButton( + key: const Key('show-green-picker'), + onPressed: () { + Pickers.showSinglePicker( + context, + data: const ['Dart', 'Flutter'], + pickerStyle: style, + ); + }, + child: const Text('Show green picker'), + ), + ), + ), + ], + ), + ), + ); + + await tester.tap(find.byKey(const Key('show-red-picker'))); + await tester.pumpAndSettle(); + expect(tester.widget(find.text('确定')).style?.color, Colors.red); + + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('show-green-picker'))); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + expect(tester.widget(find.text('确定')).style?.color, Colors.green); + }); + + testWidgets('PickerStyle is frozen when a picker opens', (tester) async { + final style = PickerStyle( + commitButton: const Text('Original', key: Key('original-commit')), + ); + + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => TextButton( + onPressed: () { + Pickers.showSinglePicker( + context, + data: const ['Dart', 'Flutter'], + pickerStyle: style, + ); + }, + child: const Text('Show picker'), + ), + ), + ), + ); + + await tester.tap(find.text('Show picker')); + await tester.pump(const Duration(milliseconds: 50)); + + style.commitButton = const Text('Changed', key: Key('changed-commit')); + await tester.pump(const Duration(milliseconds: 50)); + + expect(find.byKey(const Key('original-commit')), findsOneWidget); + expect(find.byKey(const Key('changed-commit')), findsNothing); + + await tester.tap(find.byKey(const Key('original-commit'))); + await tester.pumpAndSettle(); + }); +} diff --git a/test/public_route_compatibility_test.dart b/test/public_route_compatibility_test.dart new file mode 100644 index 0000000..67d699b --- /dev/null +++ b/test/public_route_compatibility_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/address_picker/route/address_picker_route.dart'; +import 'package:flutter_pickers/more_pickers/route/multiple_link_picker_route.dart'; +import 'package:flutter_pickers/more_pickers/route/multiple_picker_route.dart'; +import 'package:flutter_pickers/more_pickers/route/single_picker_route.dart'; +import 'package:flutter_pickers/style/picker_style.dart'; +import 'package:flutter_pickers/time_picker/model/date_mode.dart'; +import 'package:flutter_pickers/time_picker/model/pduration.dart'; +import 'package:flutter_pickers/time_picker/route/date_picker_route.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('legacy public picker route constructors remain source compatible', () { + final style = PickerStyle(); + final theme = ThemeData(); + void onCancel(bool isCancel) {} + + final single = SinglePickerRoute( + data: const ['A'], + selectData: 'A', + suffix: 'unit', + pickerStyle: style, + theme: theme, + onCancel: onCancel, + barrierLabel: 'dismiss', + settings: const RouteSettings(name: 'single'), + ); + final multiple = MultiplePickerRoute( + data: const [ + ['A'], + ], + selectData: const ['A'], + pickerStyle: style, + theme: theme, + onCancel: onCancel, + barrierLabel: 'dismiss', + settings: const RouteSettings(name: 'multiple'), + ); + final linked = MultipleLinkPickerRoute( + data: const { + 'A': ['A1'], + }, + selectData: const ['A', 'A1'], + columnNum: 2, + pickerStyle: style, + theme: theme, + onCancel: onCancel, + barrierLabel: 'dismiss', + settings: const RouteSettings(name: 'linked'), + ); + final address = AddressPickerRoute( + addAllItem: true, + pickerStyle: style, + initProvince: '', + initCity: '', + theme: theme, + onCancel: onCancel, + barrierLabel: 'dismiss', + settings: const RouteSettings(name: 'address'), + ); + final date = DatePickerRoute( + mode: DateMode.YMD, + initDate: PDuration(year: 2024, month: 1, day: 1), + minDate: PDuration(year: 2020), + maxDate: PDuration(year: 2030), + pickerStyle: style, + theme: theme, + onCancel: onCancel, + barrierLabel: 'dismiss', + settings: const RouteSettings(name: 'date'), + ); + + final routes = >[ + single, + multiple, + linked, + address, + date, + ]; + expect(routes, hasLength(5)); + expect(single.theme, same(theme)); + expect(multiple.theme, same(theme)); + expect(linked.pickerStyle, same(style)); + expect(address.onCancel, same(onCancel)); + expect(date.barrierLabel, 'dismiss'); + }); +} diff --git a/test/single_picker_route_test.dart b/test/single_picker_route_test.dart new file mode 100644 index 0000000..8f51e2c --- /dev/null +++ b/test/single_picker_route_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_pickers/pickers.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('single picker route can be dismissed without dispose errors', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + return Scaffold( + body: TextButton( + onPressed: () { + Pickers.showSinglePicker( + context, + data: const ['Dart', 'Flutter'], + ); + }, + child: const Text('Show picker'), + ), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Show picker')); + await tester.pumpAndSettle(); + + await tester.tapAt(const Offset(10, 10)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + }); +}