├── .github ├── ISSUE_TEMPLATE │ ├── ---.md │ └── bug-report.md └── workflows │ └── CI.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── me │ └── yohom │ └── location_picker_fluttify │ └── LocationPickerFluttifyPlugin.kt ├── example ├── .flutter-plugins-dependencies ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── me │ │ │ │ │ └── yohom │ │ │ │ │ └── location_picker_fluttify_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Flutter.podspec │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ └── contents.xcworkspacedata │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m ├── lib │ └── main.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── images ├── 1.5x │ ├── wechat_locate.png │ └── wechat_locator.png ├── 2.0x │ ├── wechat_locate.png │ └── wechat_locator.png ├── 3.0x │ ├── wechat_locate.png │ └── wechat_locator.png ├── wechat_locate.png └── wechat_locator.png ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── LocationPickerFluttifyPlugin.h │ └── LocationPickerFluttifyPlugin.m └── location_picker_fluttify.podspec ├── lib ├── location_picker_fluttify.dart └── src │ ├── bloc │ └── location_picker.bloc.dart │ ├── ui │ ├── route │ │ └── decorated_route.route.dart │ ├── screen │ │ ├── candidates.widget.dart │ │ ├── location_picker.screen.dart │ │ ├── locator.widget.dart │ │ ├── map_view.widget.dart │ │ └── pointer.widget.dart │ └── widget │ │ ├── candidate_poi.widget.dart │ │ ├── nonvisual │ │ └── auto_close_keyboard.widget.dart │ │ └── visual │ │ ├── decorated │ │ ├── decorated_flex.widget.dart │ │ ├── decorated_text.widget.dart │ │ ├── decorated_widget.widget.dart │ │ └── fractional_screen.widget.dart │ │ ├── placeholder │ │ ├── empty_placeholder.widget.dart │ │ ├── error_placeholder.widget.dart │ │ └── loading.widget.dart │ │ ├── preferred │ │ ├── async_list_view.widget.dart │ │ ├── debounce_text_field.widget.dart │ │ ├── flat_text.widget.dart │ │ ├── free │ │ │ ├── free_bottom_sheet.widget.dart │ │ │ ├── free_check_box.widget.dart │ │ │ ├── free_dialog.widget.dart │ │ │ └── tap_tooltip.widget.dart │ │ ├── preferred_async_builder.widget.dart │ │ └── primary_icon.widget.dart │ │ └── special_affect │ │ ├── blur.widget.dart │ │ ├── carousel.widget.dart │ │ ├── dot_indicator.widget.dart │ │ ├── form_sheet.widget.dart │ │ ├── gradient_button.widget.dart │ │ ├── notification_badge.widget.dart │ │ ├── rating_bar.widget.dart │ │ ├── snap_list.widget.dart │ │ └── unknown_route.screen.dart │ └── utils │ ├── bloc │ ├── bloc.dart │ ├── bloc_io.dart │ └── bloc_provider.widget.dart │ ├── empty_util.dart │ ├── log.dart │ ├── objects.dart │ ├── permissions.dart │ └── value.dart ├── pubspec.lock ├── pubspec.yaml └── test └── location_picker_fluttify_test.dart /.github/ISSUE_TEMPLATE/---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 新功能 3 | about: 添加功能 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **功能描述** 11 | 12 | **Native端对应字段/方法** 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 遇到了bug. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **插件版本** 11 | 12 | **遇到bug的平台(Android/iOS)** 13 | 14 | **描述bug** 15 | 16 | **复现步骤** 17 | 18 | **期望行为** 19 | 20 | **截图** 21 | 22 | **flutter doctor** 23 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: macos-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - uses: actions/setup-java@v1 9 | with: 10 | java-version: '12.x' 11 | - uses: subosito/flutter-action@v1 12 | with: 13 | channel: 'stable' 14 | - run: flutter pub get 15 | - run: flutter test 16 | - run: cd example; flutter build apk; flutter build ios --no-codesign; 17 | 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 68587a0916366e9512a78df22c44163d041dd5f3 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.3 2 | - enhance: 地图移动结束动画在获取到poi之前执行 3 | - enhance: 更新依赖 4 | 5 | ## 0.0.2 6 | - 移动地图后中心跳动 7 | - 拆分widget, 引入BLoC架构 8 | 9 | ## 0.0.1 10 | - mvp 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 位置选择器 2 | 3 | 一个位置选择器, 基于高德[地图插件](https://github.com/fluttify-project/amap_map_fluttify)和[搜索插件](https://github.com/fluttify-project/amap_search_fluttify), 目标是和微信的位置选择器一模一样的同时, 提供尽可能高的可定制化. -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'me.yohom.location_picker_fluttify' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.3.50' 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.5.0' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | sourceSets { 31 | main.java.srcDirs += 'src/main/kotlin' 32 | } 33 | defaultConfig { 34 | minSdkVersion 16 35 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 36 | } 37 | lintOptions { 38 | disable 'InvalidPackage' 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 44 | } 45 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'location_picker_fluttify' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/kotlin/me/yohom/location_picker_fluttify/LocationPickerFluttifyPlugin.kt: -------------------------------------------------------------------------------- 1 | package me.yohom.location_picker_fluttify 2 | 3 | import io.flutter.plugin.common.MethodCall 4 | import io.flutter.plugin.common.MethodChannel 5 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 6 | import io.flutter.plugin.common.MethodChannel.Result 7 | import io.flutter.plugin.common.PluginRegistry.Registrar 8 | 9 | class LocationPickerFluttifyPlugin: MethodCallHandler { 10 | companion object { 11 | @JvmStatic 12 | fun registerWith(registrar: Registrar) { 13 | val channel = MethodChannel(registrar.messenger(), "location_picker_fluttify") 14 | channel.setMethodCallHandler(LocationPickerFluttifyPlugin()) 15 | } 16 | } 17 | 18 | override fun onMethodCall(call: MethodCall, result: Result) { 19 | if (call.method == "getPlatformVersion") { 20 | result.success("Android ${android.os.Build.VERSION.RELEASE}") 21 | } else { 22 | result.notImplemented() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/.flutter-plugins-dependencies: -------------------------------------------------------------------------------- 1 | {"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"amap_core_fluttify","dependencies":["foundation_fluttify"]},{"name":"amap_map_fluttify","dependencies":["amap_core_fluttify","url_launcher"]},{"name":"amap_search_fluttify","dependencies":["amap_core_fluttify"]},{"name":"foundation_fluttify","dependencies":[]},{"name":"location_picker_fluttify","dependencies":["amap_map_fluttify","amap_search_fluttify","permission_handler"]},{"name":"permission_handler","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_web","url_launcher_macos"]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]}]} -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/Flutter/flutter_export_environment.sh 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 68587a0916366e9512a78df22c44163d041dd5f3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # location_picker_fluttify_example 2 | 3 | Demonstrates how to use the location_picker_fluttify plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "me.yohom.location_picker_fluttify_example" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 14 | 17 | 18 | 25 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/me/yohom/location_picker_fluttify_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package me.yohom.location_picker_fluttify_example 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.enableR8=true 4 | android.useAndroidX=true 5 | android.enableJetifier=true 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # 4 | 5 | Pod::Spec.new do |s| 6 | s.name = 'Flutter' 7 | s.version = '1.0.0' 8 | s.summary = 'High-performance, high-fidelity mobile apps.' 9 | s.description = <<-DESC 10 | Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS. 11 | DESC 12 | s.homepage = 'https://flutter.io' 13 | s.license = { :type => 'MIT' } 14 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 15 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 16 | s.ios.deployment_target = '8.0' 17 | s.vendored_frameworks = 'Flutter.framework' 18 | end 19 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | generated_key_values = {} 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) do |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | generated_key_values[podname] = podpath 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | end 32 | generated_key_values 33 | end 34 | 35 | target 'Runner' do 36 | # Flutter Pod 37 | 38 | copied_flutter_dir = File.join(__dir__, 'Flutter') 39 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 40 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 41 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 42 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 43 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 44 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 45 | 46 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 47 | unless File.exist?(generated_xcode_build_settings_path) 48 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 49 | end 50 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 51 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 52 | 53 | unless File.exist?(copied_framework_path) 54 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 55 | end 56 | unless File.exist?(copied_podspec_path) 57 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 58 | end 59 | end 60 | 61 | # Keep pod path relative so it can be checked into Podfile.lock. 62 | pod 'Flutter', :path => 'Flutter' 63 | 64 | # Plugin Pods 65 | 66 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 67 | # referring to absolute paths on developers' machines. 68 | system('rm -rf .symlinks') 69 | system('mkdir -p .symlinks/plugins') 70 | plugin_pods = parse_KV_file('../.flutter-plugins') 71 | plugin_pods.each do |name, path| 72 | symlink = File.join('.symlinks', 'plugins', name) 73 | File.symlink(path, symlink) 74 | pod name, :path => File.join(symlink, 'ios') 75 | end 76 | end 77 | 78 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 79 | install! 'cocoapods', :disable_input_output_paths => true 80 | 81 | post_install do |installer| 82 | installer.pods_project.targets.each do |target| 83 | target.build_configurations.each do |config| 84 | config.build_settings['ENABLE_BITCODE'] = 'NO' 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | location_picker_fluttify_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | io.flutter.embedded_views_preview 45 | YES 46 | NSLocationWhenInUseUsageDescription 47 | 需要定位权限 48 | LSApplicationQueriesSchemes 49 | 50 | iosamap 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:location_picker_fluttify/location_picker_fluttify.dart'; 4 | 5 | Future main() async { 6 | await enableFluttifyLog(false); 7 | await AmapCore.init('0a536ec318043a4b61e7b2a8796fba41'); 8 | runApp(MyApp()); 9 | } 10 | 11 | class MyApp extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp(home: HomeScreen()); 15 | } 16 | } 17 | 18 | class HomeScreen extends StatefulWidget { 19 | @override 20 | _HomeScreenState createState() => _HomeScreenState(); 21 | } 22 | 23 | class _HomeScreenState extends State { 24 | String _title; 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | body: Column( 30 | crossAxisAlignment: CrossAxisAlignment.stretch, 31 | mainAxisAlignment: MainAxisAlignment.center, 32 | children: [ 33 | RaisedButton( 34 | onPressed: () => _handleShowPicker(context), 35 | child: Text('地址选择器'), 36 | ), 37 | if (_title != null) Text(_title, textAlign: TextAlign.center), 38 | ], 39 | ), 40 | ); 41 | } 42 | 43 | Future _handleShowPicker(BuildContext context) async { 44 | final poi = await showLocationPicker( 45 | context, 46 | poiBuilder: (context, poi) async { 47 | return CandidatePoi( 48 | onTap: () => Navigator.pop(context, poi), 49 | title: Text( 50 | await poi.title, 51 | style: TextStyle(color: Colors.black, fontSize: 18), 52 | ), 53 | subtitle: Text( 54 | await poi.address, 55 | overflow: TextOverflow.ellipsis, 56 | ), 57 | ); 58 | }, 59 | ); 60 | poi.title.then((title) => setState(() => _title = title)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | after_layout: 5 | dependency: transitive 6 | description: 7 | name: after_layout 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "1.0.7+2" 11 | amap_core_fluttify: 12 | dependency: transitive 13 | description: 14 | name: amap_core_fluttify 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "0.3.0+e26c0fa" 18 | amap_map_fluttify: 19 | dependency: transitive 20 | description: 21 | name: amap_map_fluttify 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "0.14.2+e26c0fa" 25 | amap_search_fluttify: 26 | dependency: transitive 27 | description: 28 | name: amap_search_fluttify 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "0.5.0+acdcd89" 32 | archive: 33 | dependency: transitive 34 | description: 35 | name: archive 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.0.11" 39 | args: 40 | dependency: transitive 41 | description: 42 | name: args 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.5.2" 46 | async: 47 | dependency: transitive 48 | description: 49 | name: async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.4.0" 53 | boolean_selector: 54 | dependency: transitive 55 | description: 56 | name: boolean_selector 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.0.5" 60 | charcode: 61 | dependency: transitive 62 | description: 63 | name: charcode 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.1.2" 67 | collection: 68 | dependency: transitive 69 | description: 70 | name: collection 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.14.11" 74 | convert: 75 | dependency: transitive 76 | description: 77 | name: convert 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "2.1.1" 81 | crypto: 82 | dependency: transitive 83 | description: 84 | name: crypto 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "2.1.3" 88 | cupertino_icons: 89 | dependency: "direct main" 90 | description: 91 | name: cupertino_icons 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "0.1.2" 95 | flutter: 96 | dependency: "direct main" 97 | description: flutter 98 | source: sdk 99 | version: "0.0.0" 100 | flutter_test: 101 | dependency: "direct dev" 102 | description: flutter 103 | source: sdk 104 | version: "0.0.0" 105 | flutter_web_plugins: 106 | dependency: transitive 107 | description: flutter 108 | source: sdk 109 | version: "0.0.0" 110 | foundation_fluttify: 111 | dependency: transitive 112 | description: 113 | name: foundation_fluttify 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "0.3.0" 117 | image: 118 | dependency: transitive 119 | description: 120 | name: image 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.4" 124 | location_picker_fluttify: 125 | dependency: "direct dev" 126 | description: 127 | path: ".." 128 | relative: true 129 | source: path 130 | version: "0.0.3" 131 | matcher: 132 | dependency: transitive 133 | description: 134 | name: matcher 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "0.12.6" 138 | meta: 139 | dependency: transitive 140 | description: 141 | name: meta 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.1.8" 145 | path: 146 | dependency: transitive 147 | description: 148 | name: path 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.6.4" 152 | pedantic: 153 | dependency: transitive 154 | description: 155 | name: pedantic 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.8.0+1" 159 | permission_handler: 160 | dependency: transitive 161 | description: 162 | name: permission_handler 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "4.0.0" 166 | petitparser: 167 | dependency: transitive 168 | description: 169 | name: petitparser 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "2.4.0" 173 | plugin_platform_interface: 174 | dependency: transitive 175 | description: 176 | name: plugin_platform_interface 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "1.0.1" 180 | quiver: 181 | dependency: transitive 182 | description: 183 | name: quiver 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "2.0.5" 187 | rxdart: 188 | dependency: transitive 189 | description: 190 | name: rxdart 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "0.23.1" 194 | sky_engine: 195 | dependency: transitive 196 | description: flutter 197 | source: sdk 198 | version: "0.0.99" 199 | source_span: 200 | dependency: transitive 201 | description: 202 | name: source_span 203 | url: "https://pub.dartlang.org" 204 | source: hosted 205 | version: "1.5.5" 206 | stack_trace: 207 | dependency: transitive 208 | description: 209 | name: stack_trace 210 | url: "https://pub.dartlang.org" 211 | source: hosted 212 | version: "1.9.3" 213 | stream_channel: 214 | dependency: transitive 215 | description: 216 | name: stream_channel 217 | url: "https://pub.dartlang.org" 218 | source: hosted 219 | version: "2.0.0" 220 | string_scanner: 221 | dependency: transitive 222 | description: 223 | name: string_scanner 224 | url: "https://pub.dartlang.org" 225 | source: hosted 226 | version: "1.0.5" 227 | term_glyph: 228 | dependency: transitive 229 | description: 230 | name: term_glyph 231 | url: "https://pub.dartlang.org" 232 | source: hosted 233 | version: "1.1.0" 234 | test_api: 235 | dependency: transitive 236 | description: 237 | name: test_api 238 | url: "https://pub.dartlang.org" 239 | source: hosted 240 | version: "0.2.11" 241 | typed_data: 242 | dependency: transitive 243 | description: 244 | name: typed_data 245 | url: "https://pub.dartlang.org" 246 | source: hosted 247 | version: "1.1.6" 248 | url_launcher: 249 | dependency: transitive 250 | description: 251 | name: url_launcher 252 | url: "https://pub.dartlang.org" 253 | source: hosted 254 | version: "5.4.1" 255 | url_launcher_macos: 256 | dependency: transitive 257 | description: 258 | name: url_launcher_macos 259 | url: "https://pub.dartlang.org" 260 | source: hosted 261 | version: "0.0.1+2" 262 | url_launcher_platform_interface: 263 | dependency: transitive 264 | description: 265 | name: url_launcher_platform_interface 266 | url: "https://pub.dartlang.org" 267 | source: hosted 268 | version: "1.0.4" 269 | url_launcher_web: 270 | dependency: transitive 271 | description: 272 | name: url_launcher_web 273 | url: "https://pub.dartlang.org" 274 | source: hosted 275 | version: "0.1.0+2" 276 | vector_math: 277 | dependency: transitive 278 | description: 279 | name: vector_math 280 | url: "https://pub.dartlang.org" 281 | source: hosted 282 | version: "2.0.8" 283 | xml: 284 | dependency: transitive 285 | description: 286 | name: xml 287 | url: "https://pub.dartlang.org" 288 | source: hosted 289 | version: "3.5.0" 290 | sdks: 291 | dart: ">=2.6.0 <3.0.0" 292 | flutter: ">=1.12.13 <2.0.0" 293 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: location_picker_fluttify_example 2 | description: Demonstrates how to use the location_picker_fluttify plugin. 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: ">=2.2.2 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | cupertino_icons: ^0.1.2 12 | 13 | dev_dependencies: 14 | flutter_test: 15 | sdk: flutter 16 | location_picker_fluttify: 17 | path: ../ 18 | 19 | flutter: 20 | uses-material-design: true -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:location_picker_fluttify_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /images/1.5x/wechat_locate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/1.5x/wechat_locate.png -------------------------------------------------------------------------------- /images/1.5x/wechat_locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/1.5x/wechat_locator.png -------------------------------------------------------------------------------- /images/2.0x/wechat_locate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/2.0x/wechat_locate.png -------------------------------------------------------------------------------- /images/2.0x/wechat_locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/2.0x/wechat_locator.png -------------------------------------------------------------------------------- /images/3.0x/wechat_locate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/3.0x/wechat_locate.png -------------------------------------------------------------------------------- /images/3.0x/wechat_locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/3.0x/wechat_locator.png -------------------------------------------------------------------------------- /images/wechat_locate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/wechat_locate.png -------------------------------------------------------------------------------- /images/wechat_locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/images/wechat_locator.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttify-project/location_picker_fluttify/f8fa890353ab4e7df090427337453c836cb54cb1/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/LocationPickerFluttifyPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface LocationPickerFluttifyPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/LocationPickerFluttifyPlugin.m: -------------------------------------------------------------------------------- 1 | #import "LocationPickerFluttifyPlugin.h" 2 | 3 | @implementation LocationPickerFluttifyPlugin 4 | + (void)registerWithRegistrar:(NSObject*)registrar { 5 | FlutterMethodChannel* channel = [FlutterMethodChannel 6 | methodChannelWithName:@"location_picker_fluttify" 7 | binaryMessenger:[registrar messenger]]; 8 | LocationPickerFluttifyPlugin* instance = [[LocationPickerFluttifyPlugin alloc] init]; 9 | [registrar addMethodCallDelegate:instance channel:channel]; 10 | } 11 | 12 | - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 13 | if ([@"getPlatformVersion" isEqualToString:call.method]) { 14 | result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); 15 | } else { 16 | result(FlutterMethodNotImplemented); 17 | } 18 | } 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /ios/location_picker_fluttify.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'location_picker_fluttify' 6 | s.version = '0.0.1' 7 | s.summary = 'A Location Picker. Based on `amap_x_fluttify` plugins.' 8 | s.description = <<-DESC 9 | A Location Picker. Based on `amap_x_fluttify` plugins. 10 | DESC 11 | s.homepage = 'http://example.com' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Your Company' => 'email@example.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.ios.deployment_target = '8.0' 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/location_picker_fluttify.dart: -------------------------------------------------------------------------------- 1 | library location_picker_fluttify; 2 | 3 | import 'package:amap_search_fluttify/amap_search_fluttify.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:location_picker_fluttify/src/bloc/location_picker.bloc.dart'; 6 | import 'package:location_picker_fluttify/src/ui/route/decorated_route.route.dart'; 7 | 8 | import 'src/ui/screen/location_picker.screen.dart'; 9 | 10 | export 'package:amap_map_fluttify/amap_map_fluttify.dart'; 11 | export 'package:amap_search_fluttify/amap_search_fluttify.dart'; 12 | 13 | export 'src/ui/widget/candidate_poi.widget.dart'; 14 | 15 | Future showLocationPicker( 16 | BuildContext context, { 17 | @required PoiBuilder poiBuilder, 18 | Widget center, 19 | Widget locate, 20 | AlignmentGeometry locateAlignment = AlignmentDirectional.bottomEnd, 21 | Duration maskDelay = const Duration(milliseconds: 800), 22 | }) { 23 | return Navigator.push( 24 | context, 25 | DecoratedRoute( 26 | bloc: LocationPickerBLoC(), 27 | screen: LocationPickerScreen( 28 | poiBuilder: poiBuilder, 29 | pointer: center, 30 | locator: locate, 31 | locateAlignment: locateAlignment, 32 | maskDelay: maskDelay, 33 | ), 34 | ), 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/bloc/location_picker.bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:location_picker_fluttify/location_picker_fluttify.dart'; 2 | import 'package:location_picker_fluttify/src/utils/bloc/bloc.dart'; 3 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_io.dart'; 4 | 5 | class LocationPickerBLoC extends LocalBLoC with _ComponentMixin { 6 | LocationPickerBLoC() : super('位置选择 BLoC'); 7 | } 8 | 9 | mixin _ComponentMixin on LocalBLoC { 10 | @override 11 | List get disposeBag => [poiList]; 12 | 13 | final poiList = ListIO(semantics: 'poi列表'); 14 | 15 | final moveEnd = IO(semantics: '地图移动结束'); 16 | 17 | final locate = IO(semantics: '定位'); 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/ui/route/decorated_route.route.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:location_picker_fluttify/src/ui/widget/nonvisual/auto_close_keyboard.widget.dart'; 5 | import 'package:location_picker_fluttify/src/utils/bloc/bloc.dart'; 6 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_provider.widget.dart'; 7 | import 'package:location_picker_fluttify/src/utils/empty_util.dart'; 8 | 9 | typedef void _InitAction(T bloc); 10 | 11 | /// [B]是指定的BLoC, [T]是Route的返回类型 12 | class DecoratedRoute 13 | extends MaterialPageRoute { 14 | DecoratedRoute({ 15 | Key key, 16 | @required this.screen, 17 | this.bloc, 18 | this.autoCloseKeyboard = true, 19 | this.init, 20 | this.animate = true, 21 | this.lateinit = false, 22 | this.withForm = false, 23 | this.withDefaultTabController = false, 24 | this.tabLength, 25 | this.onDispose, 26 | String routeName, 27 | bool isInitialRoute = false, 28 | bool fullscreenDialog = false, 29 | bool maintainState = true, 30 | }) : // 要么同时设置泛型B和bloc参数, 要么就都不设置 31 | assert((B != BLoC && bloc != null) || (B == BLoC && bloc == null)), 32 | // 如果withDefaultTabController为true, 那么必须设置tabLength 33 | assert((withDefaultTabController && tabLength != null) || 34 | !withDefaultTabController), 35 | super( 36 | fullscreenDialog: fullscreenDialog, 37 | maintainState: maintainState, 38 | builder: (context) => screen, 39 | settings: RouteSettings( 40 | name: routeName, 41 | isInitialRoute: isInitialRoute, 42 | ), 43 | ); 44 | 45 | /// 直接传递的BLoC 46 | final B bloc; 47 | 48 | /// child 49 | final Widget screen; 50 | 51 | /// 是否自动关闭输入法 52 | final bool autoCloseKeyboard; 53 | 54 | /// 初始化方法 55 | final _InitAction init; 56 | 57 | /// 是否执行动画 58 | final bool animate; 59 | 60 | /// 是否等待入场动画结束之后再进行初始化动作 61 | final bool lateinit; 62 | 63 | /// 是否带有表单 64 | final bool withForm; 65 | 66 | /// 是否含有TabBar 67 | final bool withDefaultTabController; 68 | 69 | /// tab bar长度, 必须和[withDefaultTabController]一起设置 70 | final int tabLength; 71 | 72 | final VoidCallback onDispose; 73 | 74 | /// 是否已经初始化 75 | bool _inited = false; 76 | 77 | /// 网络状态监听的订阅 78 | StreamSubscription _subscription; 79 | 80 | @override 81 | Widget buildPage( 82 | BuildContext context, 83 | Animation animation, 84 | Animation secondaryAnimation, 85 | ) { 86 | Widget result; 87 | if (isNotEmpty(bloc)) { 88 | result = BLoCProvider( 89 | bloc: bloc, 90 | init: lateinit ? null : init, // 可以设置为null, BLoCProvider会处理的 91 | child: builder(context), 92 | onDispose: onDispose, 93 | ); 94 | } else { 95 | result = builder(context); 96 | } 97 | 98 | // 是否自动收起键盘 99 | if (autoCloseKeyboard) { 100 | result = AutoCloseKeyboard(child: result); 101 | } 102 | 103 | // 是否带有表单 104 | if (withForm) { 105 | result = Form(child: result); 106 | } 107 | 108 | if (withDefaultTabController) { 109 | result = DefaultTabController(length: tabLength, child: result); 110 | } 111 | 112 | return result; 113 | } 114 | 115 | @override 116 | Widget buildTransitions(BuildContext context, Animation animation, 117 | Animation secondaryAnimation, Widget child) { 118 | animation.addStatusListener((status) { 119 | // 如果是懒加载, 那么动画结束时开始初始化 120 | if (status == AnimationStatus.completed && 121 | lateinit && 122 | init != null && 123 | bloc != null && 124 | !_inited) { 125 | init(bloc); 126 | _inited = true; 127 | } 128 | }); 129 | return animate 130 | ? super.buildTransitions(context, animation, secondaryAnimation, child) 131 | : child; 132 | } 133 | 134 | @override 135 | void dispose() { 136 | _subscription?.cancel(); 137 | super.dispose(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/ui/screen/candidates.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:amap_search_fluttify/amap_search_fluttify.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:location_picker_fluttify/src/bloc/location_picker.bloc.dart'; 5 | import 'package:location_picker_fluttify/src/ui/widget/visual/preferred/preferred_async_builder.widget.dart'; 6 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_provider.widget.dart'; 7 | 8 | import 'location_picker.screen.dart'; 9 | 10 | class Candidates extends StatelessWidget { 11 | const Candidates({ 12 | Key key, 13 | @required this.poiBuilder, 14 | }) : super(key: key); 15 | 16 | final PoiBuilder poiBuilder; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | final bloc = BLoCProvider.of(context); 21 | return PreferredStreamBuilder>( 22 | stream: bloc.poiList.stream, 23 | builder: (data) { 24 | return ListView.separated( 25 | padding: EdgeInsets.zero, 26 | shrinkWrap: true, 27 | itemCount: data.length, 28 | itemBuilder: (context, index) { 29 | return FutureBuilder( 30 | future: poiBuilder(context, data[index]), 31 | builder: (context, snapshot) { 32 | if (snapshot.hasData) { 33 | return snapshot.data; 34 | } else { 35 | return SizedBox.shrink(); 36 | } 37 | }, 38 | ); 39 | }, 40 | separatorBuilder: (_, __) => Divider(height: 0, indent: 8), 41 | ); 42 | }, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/ui/screen/location_picker.screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:amap_search_fluttify/amap_search_fluttify.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:location_picker_fluttify/src/ui/screen/locator.widget.dart'; 5 | 6 | import 'candidates.widget.dart'; 7 | import 'map_view.widget.dart'; 8 | import 'pointer.widget.dart'; 9 | 10 | typedef Future PoiBuilder(BuildContext context, Poi poi); 11 | 12 | class LocationPickerScreen extends StatelessWidget { 13 | const LocationPickerScreen({ 14 | Key key, 15 | this.pointer, 16 | this.locator, 17 | this.locateAlignment = AlignmentDirectional.bottomEnd, 18 | this.maskDelay = const Duration(milliseconds: 800), 19 | @required this.poiBuilder, 20 | }) : super(key: key); 21 | 22 | final Widget pointer; 23 | final Widget locator; 24 | final AlignmentGeometry locateAlignment; 25 | final Duration maskDelay; 26 | final PoiBuilder poiBuilder; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Scaffold( 31 | body: Column( 32 | children: [ 33 | Flexible( 34 | flex: 1, 35 | child: Stack( 36 | children: [ 37 | MapView(maskDelay: maskDelay), 38 | Pointer(pointer: pointer), 39 | Align( 40 | alignment: locateAlignment, 41 | child: Locator(child: locator), 42 | ), 43 | ], 44 | ), 45 | ), 46 | Flexible( 47 | child: Candidates(poiBuilder: poiBuilder), 48 | ), 49 | ], 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/ui/screen/locator.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:location_picker_fluttify/src/bloc/location_picker.bloc.dart'; 3 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_provider.widget.dart'; 4 | import 'package:location_picker_fluttify/src/utils/objects.dart'; 5 | 6 | class Locator extends StatelessWidget { 7 | const Locator({ 8 | Key key, 9 | @required this.child, 10 | }) : super(key: key); 11 | 12 | final Widget child; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return GestureDetector( 17 | child: child ?? gDefaultShowMyLocation, 18 | onTap: () => _handleLocate(context), 19 | ); 20 | } 21 | 22 | Future _handleLocate(BuildContext context) async { 23 | final bloc = BLoCProvider.of(context); 24 | bloc.locate.add(Object()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/ui/screen/map_view.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:after_layout/after_layout.dart'; 2 | import 'package:amap_map_fluttify/amap_map_fluttify.dart'; 3 | import 'package:amap_search_fluttify/amap_search_fluttify.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:location_picker_fluttify/src/bloc/location_picker.bloc.dart'; 6 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_provider.widget.dart'; 7 | import 'package:location_picker_fluttify/src/utils/permissions.dart'; 8 | 9 | class MapView extends StatefulWidget { 10 | const MapView({Key key, this.maskDelay}) : super(key: key); 11 | 12 | final Duration maskDelay; 13 | 14 | @override 15 | _MapViewState createState() => _MapViewState(); 16 | } 17 | 18 | class _MapViewState extends State with AfterLayoutMixin, _Private { 19 | @override 20 | Widget build(BuildContext context) { 21 | return AmapView( 22 | maskDelay: widget.maskDelay, 23 | showZoomControl: false, 24 | onMapMoveEnd: _handleMapMoveEnd, 25 | onMapCreated: _handleCreate, 26 | ); 27 | } 28 | } 29 | 30 | mixin _Private on AfterLayoutMixin { 31 | AmapController _controller; 32 | 33 | @override 34 | void afterFirstLayout(BuildContext context) { 35 | final bloc = BLoCProvider.of(context); 36 | bloc.locate.listen((_) => _controller?.showMyLocation(true)); 37 | } 38 | 39 | Future _handleCreate(AmapController controller) async { 40 | _controller = controller; 41 | if (await requestPermission()) { 42 | await _controller.showMyLocation(true); 43 | await _controller.setZoomLevel(15, animated: false); 44 | await _controller.showLocateControl(false); 45 | await _handleMapMoveEnd(null); 46 | } 47 | } 48 | 49 | Future _handleMapMoveEnd(_) async { 50 | final bloc = BLoCProvider.of(context); 51 | 52 | bloc.moveEnd.add(Object()); 53 | 54 | final center = await _controller.getCenterCoordinate(); 55 | final around = await AmapSearch.searchAround(center); 56 | 57 | Stream.fromIterable(around) 58 | .asyncMap((poi) => poi) 59 | .toList() 60 | .then(bloc.poiList.add); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/ui/screen/pointer.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:after_layout/after_layout.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:location_picker_fluttify/src/bloc/location_picker.bloc.dart'; 4 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_provider.widget.dart'; 5 | import 'package:location_picker_fluttify/src/utils/objects.dart'; 6 | 7 | const _iconSize = 50.0; 8 | 9 | class Pointer extends StatefulWidget { 10 | const Pointer({ 11 | Key key, 12 | @required this.pointer, 13 | }) : super(key: key); 14 | 15 | final Widget pointer; 16 | 17 | @override 18 | _PointerState createState() => _PointerState(); 19 | } 20 | 21 | class _PointerState extends State 22 | with SingleTickerProviderStateMixin, AfterLayoutMixin { 23 | AnimationController _jumpController; 24 | Animation _tween; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _jumpController = 30 | AnimationController(vsync: this, duration: Duration(milliseconds: 300)); 31 | _tween = Tween(begin: Offset(0, 0), end: Offset(0, -15)).animate( 32 | CurvedAnimation(parent: _jumpController, curve: Curves.easeInOut)); 33 | } 34 | 35 | @override 36 | void afterFirstLayout(BuildContext context) { 37 | final bloc = BLoCProvider.of(context); 38 | bloc.moveEnd.listen((_) { 39 | // 执行跳动动画 40 | _jumpController.forward().then((it) => _jumpController.reverse()); 41 | }); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return Center( 47 | child: AnimatedBuilder( 48 | animation: _tween, 49 | builder: (context, child) { 50 | return Transform.translate( 51 | offset: Offset( 52 | _tween.value.dx, 53 | _tween.value.dy - _iconSize / 2, 54 | ), 55 | child: child, 56 | ); 57 | }, 58 | child: widget.pointer ?? gDefaultPointer, 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/ui/widget/candidate_poi.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CandidatePoi extends StatelessWidget { 4 | const CandidatePoi({ 5 | Key key, 6 | @required this.title, 7 | @required this.subtitle, 8 | this.trailing, 9 | this.padding = const EdgeInsets.all(8.0), 10 | this.onTap, 11 | }) : super(key: key); 12 | 13 | final Widget title; 14 | final Widget subtitle; 15 | final Widget trailing; 16 | final EdgeInsets padding; 17 | final VoidCallback onTap; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return GestureDetector( 22 | onTap: onTap, 23 | child: Padding( 24 | padding: padding, 25 | child: Row( 26 | crossAxisAlignment: CrossAxisAlignment.center, 27 | children: [ 28 | Expanded( 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [title, subtitle], 32 | ), 33 | ), 34 | if (trailing != null) trailing, 35 | ], 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/ui/widget/nonvisual/auto_close_keyboard.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AutoCloseKeyboard extends StatelessWidget { 4 | final Widget child; 5 | 6 | const AutoCloseKeyboard({ 7 | Key key, 8 | this.child, 9 | }) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return GestureDetector( 14 | behavior: HitTestBehavior.opaque, 15 | onTap: () => FocusScope.of(context).unfocus(), 16 | child: child, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/decorated/decorated_flex.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef void PressedCallback(BuildContext context); 4 | 5 | class DecoratedRow extends StatelessWidget { 6 | const DecoratedRow({ 7 | Key key, 8 | this.padding, 9 | this.margin, 10 | this.color, 11 | this.decoration, 12 | this.foregroundDecoration, 13 | this.constraints, 14 | this.transform, 15 | this.width, 16 | this.height, 17 | this.alignment, 18 | this.mainAxisAlignment = MainAxisAlignment.start, 19 | this.mainAxisSize = MainAxisSize.max, 20 | this.crossAxisAlignment = CrossAxisAlignment.center, 21 | this.textBaseline, 22 | this.onPressed, 23 | this.onLongPressed, 24 | this.behavior = HitTestBehavior.opaque, 25 | this.itemSpacing = 0, 26 | this.divider, 27 | this.visible = true, 28 | this.crossExpanded = false, 29 | this.forceItemSameExtent = false, 30 | this.children, 31 | this.safeArea, 32 | }) : super(key: key); 33 | 34 | //region Container 35 | final EdgeInsets padding; 36 | final EdgeInsets margin; 37 | final Color color; 38 | final Decoration decoration; 39 | final Decoration foregroundDecoration; 40 | final BoxConstraints constraints; 41 | final Matrix4 transform; 42 | final double width; 43 | final double height; 44 | 45 | //endregion 46 | //region Row 47 | final AlignmentGeometry alignment; 48 | final MainAxisAlignment mainAxisAlignment; 49 | final MainAxisSize mainAxisSize; 50 | final CrossAxisAlignment crossAxisAlignment; 51 | final TextBaseline textBaseline; 52 | 53 | //endregion 54 | //region GestureDetector 55 | final PressedCallback onPressed; 56 | final PressedCallback onLongPressed; 57 | final HitTestBehavior behavior; 58 | 59 | //endregion 60 | /// item间距 61 | final double itemSpacing; 62 | 63 | /// 分隔控件 与[itemSpacing]功能类似, 但是优先使用[divider] 64 | final Widget divider; 65 | 66 | /// 是否可见 67 | final bool visible; 68 | 69 | /// 垂直方向上Expand 70 | final bool crossExpanded; 71 | 72 | /// 强制子widget拥有相同的宽度, 会获取到屏幕宽度然后除以item个数来计算 73 | final bool forceItemSameExtent; 74 | 75 | /// 是否安全区域 76 | final bool safeArea; 77 | final List children; 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return DecoratedFlex( 82 | direction: Axis.horizontal, 83 | padding: padding, 84 | margin: margin, 85 | color: color, 86 | decoration: decoration, 87 | foregroundDecoration: foregroundDecoration, 88 | constraints: constraints, 89 | transform: transform, 90 | width: width, 91 | height: height, 92 | alignment: alignment, 93 | mainAxisAlignment: mainAxisAlignment, 94 | mainAxisSize: mainAxisSize, 95 | crossAxisAlignment: crossAxisAlignment, 96 | textBaseline: textBaseline, 97 | onPressed: onPressed, 98 | onLongPressed: onLongPressed, 99 | behavior: behavior, 100 | itemSpacing: itemSpacing, 101 | visible: visible, 102 | crossExpanded: crossExpanded, 103 | forceItemSameExtent: forceItemSameExtent, 104 | safeArea: safeArea, 105 | divider: divider, 106 | children: children, 107 | ); 108 | } 109 | } 110 | 111 | class DecoratedColumn extends StatelessWidget { 112 | const DecoratedColumn({ 113 | Key key, 114 | this.padding, 115 | this.margin, 116 | this.color, 117 | this.decoration, 118 | this.foregroundDecoration, 119 | this.constraints, 120 | this.transform, 121 | this.width, 122 | this.height, 123 | this.alignment, 124 | this.mainAxisAlignment = MainAxisAlignment.start, 125 | this.mainAxisSize = MainAxisSize.max, 126 | this.crossAxisAlignment = CrossAxisAlignment.center, 127 | this.textBaseline, 128 | this.onPressed, 129 | this.onLongPressed, 130 | this.behavior = HitTestBehavior.opaque, 131 | this.itemSpacing = 0, 132 | this.divider, 133 | this.visible = true, 134 | this.crossExpanded = false, 135 | this.scrollable = false, 136 | this.forceItemSameExtent = false, 137 | this.safeArea, 138 | this.children, 139 | }) : super(key: key); 140 | 141 | //region Container 142 | final EdgeInsets padding; 143 | final EdgeInsets margin; 144 | final Color color; 145 | final Decoration decoration; 146 | final Decoration foregroundDecoration; 147 | final BoxConstraints constraints; 148 | final Matrix4 transform; 149 | final double width; 150 | final double height; 151 | 152 | //endregion 153 | //region Row 154 | final AlignmentGeometry alignment; 155 | final MainAxisAlignment mainAxisAlignment; 156 | final MainAxisSize mainAxisSize; 157 | final CrossAxisAlignment crossAxisAlignment; 158 | final TextBaseline textBaseline; 159 | 160 | //endregion 161 | //region GestureDetector 162 | final PressedCallback onPressed; 163 | final PressedCallback onLongPressed; 164 | final HitTestBehavior behavior; 165 | 166 | //endregion 167 | final double itemSpacing; 168 | 169 | /// 分隔控件 与[itemSpacing]功能类似, 但是优先使用[divider] 170 | final Widget divider; 171 | final bool visible; 172 | final bool crossExpanded; 173 | final bool scrollable; 174 | 175 | /// 强制子widget拥有相同的高度, 会获取到屏幕高度然后除以item个数来计算 176 | final bool forceItemSameExtent; 177 | 178 | /// 是否安全区域 179 | final bool safeArea; 180 | final List children; 181 | 182 | @override 183 | Widget build(BuildContext context) { 184 | Widget result = DecoratedFlex( 185 | direction: Axis.vertical, 186 | padding: padding, 187 | margin: margin, 188 | color: color, 189 | decoration: decoration, 190 | foregroundDecoration: foregroundDecoration, 191 | constraints: constraints, 192 | transform: transform, 193 | width: width, 194 | height: height, 195 | alignment: alignment, 196 | mainAxisAlignment: mainAxisAlignment, 197 | mainAxisSize: mainAxisSize, 198 | crossAxisAlignment: crossAxisAlignment, 199 | textBaseline: textBaseline, 200 | onPressed: onPressed, 201 | onLongPressed: onLongPressed, 202 | behavior: behavior, 203 | itemSpacing: itemSpacing, 204 | visible: visible, 205 | crossExpanded: crossExpanded, 206 | forceItemSameExtent: forceItemSameExtent, 207 | safeArea: safeArea, 208 | divider: divider, 209 | children: children, 210 | ); 211 | 212 | if (scrollable) { 213 | result = ListView(children: [result], shrinkWrap: true); 214 | } 215 | 216 | return result; 217 | } 218 | } 219 | 220 | class DecoratedFlex extends StatelessWidget { 221 | DecoratedFlex({ 222 | Key key, 223 | this.padding, 224 | this.margin, 225 | this.color, 226 | this.decoration, 227 | this.foregroundDecoration, 228 | this.constraints, 229 | this.transform, 230 | this.width, 231 | this.height, 232 | @required this.direction, 233 | this.alignment, 234 | this.mainAxisAlignment = MainAxisAlignment.start, 235 | this.mainAxisSize = MainAxisSize.max, 236 | this.crossAxisAlignment = CrossAxisAlignment.center, 237 | this.textBaseline, 238 | this.onPressed, 239 | this.onLongPressed, 240 | this.behavior = HitTestBehavior.opaque, 241 | this.itemSpacing = 0, 242 | this.divider, 243 | this.visible = true, 244 | this.crossExpanded = false, 245 | this.forceItemSameExtent = false, 246 | this.safeArea, 247 | this.children, 248 | }) : super(key: key); 249 | 250 | //region Container 251 | final EdgeInsets padding; 252 | final EdgeInsets margin; 253 | final Color color; 254 | final Decoration decoration; 255 | final Decoration foregroundDecoration; 256 | final BoxConstraints constraints; 257 | final Matrix4 transform; 258 | final double width; 259 | final double height; 260 | 261 | //endregion 262 | //region Flex 263 | final Axis direction; 264 | final AlignmentGeometry alignment; 265 | final MainAxisAlignment mainAxisAlignment; 266 | final MainAxisSize mainAxisSize; 267 | final CrossAxisAlignment crossAxisAlignment; 268 | final TextBaseline textBaseline; 269 | 270 | //endregion 271 | //region GestureDetector 272 | final PressedCallback onPressed; 273 | final PressedCallback onLongPressed; 274 | final HitTestBehavior behavior; 275 | 276 | //endregion 277 | /// 元素间距 278 | final double itemSpacing; 279 | 280 | /// 分隔控件 与[itemSpacing]功能类似, 但是优先使用[divider] 281 | final Widget divider; 282 | 283 | /// 是否可见 284 | final bool visible; 285 | 286 | /// cross方向上是否展开 287 | final bool crossExpanded; 288 | 289 | /// 是否强制子控件等长 290 | final bool forceItemSameExtent; 291 | 292 | /// 是否安全区域 293 | final bool safeArea; 294 | 295 | /// 子元素 296 | final List children; 297 | 298 | @override 299 | Widget build(BuildContext context) { 300 | List _children = children; 301 | 302 | if (forceItemSameExtent) { 303 | _children = children.map((it) { 304 | switch (direction) { 305 | case Axis.horizontal: 306 | return SizedBox( 307 | width: MediaQuery.of(context).size.width / children.length, 308 | child: it, 309 | ); 310 | case Axis.vertical: 311 | return SizedBox( 312 | height: MediaQuery.of(context).size.height / children.length, 313 | child: it, 314 | ); 315 | } 316 | }).toList(); 317 | } 318 | 319 | Widget result = Flex( 320 | direction: direction, 321 | mainAxisAlignment: mainAxisAlignment, 322 | mainAxisSize: mainAxisSize, 323 | crossAxisAlignment: crossAxisAlignment, 324 | textBaseline: textBaseline, 325 | children: itemSpacing != 0 || divider != null 326 | ? addItemDivider(_children, itemSpacing, divider) 327 | : _children, 328 | ); 329 | 330 | if (padding != null || 331 | margin != null || 332 | width != null || 333 | height != null || 334 | color != null || 335 | decoration != null || 336 | foregroundDecoration != null || 337 | constraints != null || 338 | transform != null || 339 | alignment != null) { 340 | result = Container( 341 | padding: padding, 342 | margin: margin, 343 | width: width, 344 | height: height, 345 | color: color, 346 | decoration: decoration, 347 | foregroundDecoration: foregroundDecoration, 348 | constraints: constraints, 349 | transform: transform, 350 | alignment: alignment, 351 | child: result, 352 | ); 353 | } 354 | 355 | if (behavior != null || onPressed != null || onLongPressed != null) { 356 | result = GestureDetector( 357 | behavior: behavior == null ?? result != null 358 | ? HitTestBehavior.deferToChild 359 | : HitTestBehavior.translucent, 360 | onTap: onPressed != null ? () => onPressed(context) : null, 361 | onLongPress: 362 | onLongPressed != null ? () => onLongPressed(context) : null, 363 | child: result, 364 | ); 365 | } 366 | 367 | if (crossExpanded) { 368 | result = Expanded(child: result); 369 | } 370 | 371 | if (safeArea != null) { 372 | result = SafeArea(child: result); 373 | } 374 | return Visibility(visible: visible, child: result); 375 | } 376 | 377 | List addItemDivider( 378 | List children, 379 | double itemSpacing, 380 | Widget divider, 381 | ) { 382 | assert(children != null); 383 | 384 | // 确认要往哪几个index(以最终的插入后的List为参考系)插空间 385 | int currentLength = children.length; 386 | if (currentLength > 1) { 387 | final indexes = []; 388 | // `currentLength + (currentLength - 1)`是插入后的长度 389 | // 这里的循环是在纸上画过得出的结论 390 | for (int i = 1; i < currentLength + (currentLength - 1); i += 2) { 391 | indexes.add(i); 392 | } 393 | 394 | if (direction == Axis.horizontal) { 395 | indexes.forEach((index) { 396 | children.insert(index, divider ?? SizedBox(width: itemSpacing)); 397 | }); 398 | } else if (direction == Axis.vertical) { 399 | indexes.forEach((index) { 400 | children.insert(index, divider ?? SizedBox(height: itemSpacing)); 401 | }); 402 | } 403 | } 404 | 405 | return children; 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/decorated/decorated_text.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DecoratedText extends StatelessWidget { 4 | const DecoratedText( 5 | this.data, { 6 | Key key, 7 | this.border, 8 | this.borderRadius, 9 | this.padding, 10 | this.margin, 11 | this.style = const TextStyle(), 12 | this.safeArea, 13 | this.onPressed, 14 | this.backgroundColor, 15 | this.maxLines, 16 | this.textAlign, 17 | }) : super(key: key); 18 | 19 | final BoxBorder border; 20 | final BorderRadiusGeometry borderRadius; 21 | final EdgeInsetsGeometry padding; 22 | final EdgeInsetsGeometry margin; 23 | final TextStyle style; 24 | final String data; 25 | final bool safeArea; 26 | final ValueChanged onPressed; 27 | final Color backgroundColor; 28 | final int maxLines; 29 | final TextAlign textAlign; 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | Widget result = Text( 34 | data, 35 | maxLines: maxLines, 36 | style: style, 37 | textAlign: textAlign, 38 | ); 39 | 40 | if (border != null || 41 | borderRadius != null || 42 | padding != null || 43 | margin != null || 44 | backgroundColor != null) { 45 | result = Container( 46 | margin: margin, 47 | padding: padding, 48 | decoration: BoxDecoration( 49 | color: backgroundColor, 50 | border: border, 51 | borderRadius: borderRadius, 52 | ), 53 | child: result, 54 | ); 55 | } 56 | 57 | if (safeArea != null) { 58 | result = SafeArea(child: result); 59 | } 60 | 61 | if (onPressed != null) { 62 | result = GestureDetector( 63 | onTap: () => onPressed(data), 64 | child: result, 65 | ); 66 | } 67 | 68 | return result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/decorated/decorated_widget.widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:location_picker_fluttify/src/ui/widget/nonvisual/auto_close_keyboard.widget.dart'; 5 | import 'package:location_picker_fluttify/src/utils/bloc/bloc.dart'; 6 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_provider.widget.dart'; 7 | import 'package:location_picker_fluttify/src/utils/empty_util.dart'; 8 | 9 | typedef void _InitAction(T bloc); 10 | 11 | /// [B]是指定的BLoC 12 | class DecoratedWidget extends StatefulWidget { 13 | DecoratedWidget({ 14 | Key key, 15 | @required this.widget, 16 | this.bloc, 17 | this.autoCloseKeyboard = true, 18 | this.init, 19 | this.withForm = false, 20 | this.withDefaultTabController = false, 21 | this.tabLength, 22 | }) : assert((B != BLoC && bloc != null) || (B == BLoC && bloc == null)), 23 | assert((withDefaultTabController && tabLength != null) || 24 | !withDefaultTabController), 25 | super(); 26 | 27 | /// 直接传递的BLoC 28 | final B bloc; 29 | 30 | /// child 31 | final Widget widget; 32 | 33 | /// 是否自动关闭输入法 34 | final bool autoCloseKeyboard; 35 | 36 | /// 初始化方法 37 | final _InitAction init; 38 | 39 | /// 是否带有表单 40 | final bool withForm; 41 | 42 | /// 是否含有TabBar 43 | final bool withDefaultTabController; 44 | 45 | /// tab bar长度, 必须和[withDefaultTabController]一起设置 46 | final int tabLength; 47 | 48 | @override 49 | _DecoratedWidgetState createState() => _DecoratedWidgetState(); 50 | } 51 | 52 | class _DecoratedWidgetState extends State> { 53 | /// 网络状态监听的订阅 54 | StreamSubscription _subscription; 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | Widget result; 59 | if (isNotEmpty(widget.bloc)) { 60 | result = BLoCProvider( 61 | bloc: widget.bloc, 62 | init: widget.init, 63 | child: widget.widget, 64 | ); 65 | } else { 66 | result = widget.widget; 67 | } 68 | 69 | // 是否自动收起键盘 70 | if (widget.autoCloseKeyboard) { 71 | result = AutoCloseKeyboard(child: result); 72 | } 73 | 74 | // 是否带有表单 75 | if (widget.withForm) { 76 | result = Form(child: result); 77 | } 78 | 79 | if (widget.withDefaultTabController) { 80 | result = DefaultTabController(length: widget.tabLength, child: result); 81 | } 82 | 83 | return result; 84 | } 85 | 86 | @override 87 | void dispose() { 88 | _subscription?.cancel(); 89 | super.dispose(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/decorated/fractional_screen.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FractionalScreen extends StatelessWidget { 4 | const FractionalScreen({ 5 | Key key, 6 | this.widthFactor = 1.0, 7 | this.heightFactor = 1.0, 8 | this.child, 9 | this.columnChildren, 10 | this.rowChildren, 11 | this.direction = Axis.vertical, 12 | this.scrollable = true, 13 | this.mainAxisAlignment = MainAxisAlignment.start, 14 | this.mainAxisSize = MainAxisSize.max, 15 | this.crossAxisAlignment = CrossAxisAlignment.center, 16 | this.textDirection, 17 | this.verticalDirection = VerticalDirection.down, 18 | this.textBaseline, 19 | this.padding = EdgeInsets.zero, 20 | this.safeArea, 21 | @required this.children, 22 | }) : super(key: key); 23 | 24 | /// 占屏幕宽度百分比 25 | final double widthFactor; 26 | 27 | /// 占屏幕高度百分比 28 | final double heightFactor; 29 | 30 | /// [child], [columnChildren], [rowChildren]三个只能设置一个 31 | /// 如果都设置了, 那就按[child]->[columnChildren]->[rowChildren]的优先级作为child使用 32 | /// child 33 | @Deprecated('使用children') 34 | final Widget child; 35 | 36 | /// 子控件为Column的children 37 | @Deprecated('使用children') 38 | final List columnChildren; 39 | 40 | /// 子控件为Row的children 41 | @Deprecated('使用children') 42 | final List rowChildren; 43 | 44 | /// 不管有一个还是多个child, 都用这个, 然后用[direction]来区分方向 45 | final List children; 46 | 47 | /// 排列方向 48 | final Axis direction; 49 | 50 | /// 内间距 51 | final EdgeInsets padding; 52 | 53 | /// 是否可以滚动, 可以配合键盘使用 54 | final bool scrollable; 55 | 56 | /// 是否Safe Area 57 | final bool safeArea; 58 | 59 | //region Flex属性 60 | final MainAxisAlignment mainAxisAlignment; 61 | final MainAxisSize mainAxisSize; 62 | final CrossAxisAlignment crossAxisAlignment; 63 | final TextDirection textDirection; 64 | final VerticalDirection verticalDirection; 65 | final TextBaseline textBaseline; 66 | 67 | //endregion 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | Widget content = SizedBox( 72 | width: MediaQuery.of(context).size.width * widthFactor, 73 | height: MediaQuery.of(context).size.height * heightFactor, 74 | child: Padding( 75 | padding: padding, 76 | child: Flex( 77 | direction: direction, 78 | mainAxisAlignment: mainAxisAlignment, 79 | mainAxisSize: mainAxisSize, 80 | crossAxisAlignment: crossAxisAlignment, 81 | textDirection: textDirection, 82 | verticalDirection: verticalDirection, 83 | textBaseline: textBaseline, 84 | children: children, 85 | ), 86 | ), 87 | ); 88 | 89 | if (safeArea != null) { 90 | content = SafeArea(child: content); 91 | } 92 | 93 | return scrollable ? SingleChildScrollView(child: content) : content; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/placeholder/empty_placeholder.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyPlaceholder extends StatelessWidget { 4 | const EmptyPlaceholder({Key key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Center( 9 | child: Text('没有数据'), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/placeholder/error_placeholder.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ErrorPlaceholder extends StatelessWidget { 4 | const ErrorPlaceholder({Key key}) : super(key: key); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Center( 9 | child: Text('出现错误'), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/placeholder/loading.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class LoadingWidget extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Center( 8 | child: CircularProgressIndicator(), 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/async_list_view.widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:location_picker_fluttify/src/utils/log.dart'; 5 | import 'package:location_picker_fluttify/src/utils/value.dart'; 6 | 7 | import 'preferred_async_builder.widget.dart'; 8 | 9 | typedef Widget _ItemBuilder(BuildContext context, int index, T data); 10 | typedef Widget _ErrorPlaceholderBuilder(BuildContext context, Object error); 11 | typedef bool _Filter(T element); 12 | 13 | class FutureListView extends StatelessWidget { 14 | const FutureListView({ 15 | Key key, 16 | @required this.future, 17 | @required this.itemBuilder, 18 | this.shrinkWrap = true, 19 | this.showLoading = true, 20 | this.initialData, 21 | this.emptyPlaceholder, 22 | this.loadingPlaceholder, 23 | this.errorPlaceholderBuilder, 24 | this.padding, 25 | this.physics = const ClampingScrollPhysics(), 26 | this.reverse = false, 27 | this.divider, 28 | this.startWithDivider = false, 29 | this.endWithDivider = false, 30 | this.where, 31 | }) : super(key: key); 32 | 33 | //region FutureWidget 34 | final Future> future; 35 | final bool showLoading; 36 | final List initialData; 37 | final Widget emptyPlaceholder; 38 | final Widget loadingPlaceholder; 39 | final _ErrorPlaceholderBuilder errorPlaceholderBuilder; 40 | 41 | //endregion 42 | //region ListView.builder 43 | final _ItemBuilder itemBuilder; 44 | final bool shrinkWrap; 45 | final EdgeInsets padding; 46 | final ScrollPhysics physics; 47 | final bool reverse; 48 | 49 | //endregion 50 | final Widget divider; 51 | final bool startWithDivider; 52 | final bool endWithDivider; 53 | final _Filter where; 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | return PreferredFutureBuilder>( 58 | future: future, 59 | showLoading: showLoading, 60 | initialData: initialData, 61 | emptyPlaceholder: emptyPlaceholder, 62 | loadingPlaceholder: loadingPlaceholder, 63 | errorPlaceholderBuilder: errorPlaceholderBuilder, 64 | builder: (data) { 65 | List filteredData = data; 66 | if (where != null) { 67 | filteredData = data.where(where).toList(); 68 | } 69 | return ListView.builder( 70 | padding: padding, 71 | shrinkWrap: shrinkWrap, 72 | physics: physics, 73 | reverse: reverse, 74 | itemCount: filteredData.length ?? 0, 75 | itemBuilder: (context, index) { 76 | return _buildItem( 77 | context, 78 | filteredData, 79 | index, 80 | reverse, 81 | startWithDivider, 82 | endWithDivider, 83 | divider, 84 | itemBuilder, 85 | ); 86 | }, 87 | ); 88 | }, 89 | ); 90 | } 91 | } 92 | 93 | class StreamListView extends StatelessWidget { 94 | StreamListView({ 95 | Key key, 96 | this.stream, 97 | @required this.itemBuilder, 98 | this.shrinkWrap = true, 99 | this.showLoading = true, 100 | this.initialData, 101 | this.emptyPlaceholder, 102 | this.loadingPlaceholder, 103 | this.errorPlaceholderBuilder, 104 | this.padding, 105 | this.physics = const ClampingScrollPhysics(), 106 | this.reverse = false, 107 | this.divider, 108 | this.onRefresh, 109 | this.onLoadMore, 110 | ScrollController controller, 111 | this.refreshDisplacement = 40.0, 112 | this.refreshColor, 113 | this.refreshBackgroundColor, 114 | this.notificationPredicate = defaultScrollNotificationPredicate, 115 | this.where, 116 | this.distinct = false, 117 | this.startWithDivider = false, 118 | this.endWithDivider = false, 119 | this.header, 120 | }) : _controller = controller ?? ScrollController(), 121 | super(key: key) { 122 | _controller?.addListener(() { 123 | if (_controller.position.maxScrollExtent == _controller.offset) { 124 | if (onLoadMore != null && !_inLoading.value) { 125 | _inLoading.value = true; 126 | onLoadMore().whenComplete(() { 127 | L.p('加载更多完成'); 128 | _inLoading.value = false; 129 | }); 130 | } 131 | } 132 | }); 133 | } 134 | 135 | //region FutureWidget 136 | final Stream> stream; 137 | final bool showLoading; 138 | final List initialData; 139 | final Widget emptyPlaceholder; 140 | final Widget loadingPlaceholder; 141 | final _ErrorPlaceholderBuilder errorPlaceholderBuilder; 142 | 143 | //endregion 144 | //region ListView.builder 145 | final _ItemBuilder itemBuilder; 146 | final bool shrinkWrap; 147 | final EdgeInsets padding; 148 | final ScrollPhysics physics; 149 | final bool reverse; 150 | 151 | //endregion 152 | //region RefreshIndicator 153 | final RefreshCallback onRefresh; 154 | final RefreshCallback onLoadMore; 155 | final ScrollController _controller; 156 | final double refreshDisplacement; 157 | final Color refreshColor; 158 | final Color refreshBackgroundColor; 159 | final ScrollNotificationPredicate notificationPredicate; 160 | 161 | //endregion 162 | final Widget divider; 163 | 164 | /// 过滤器 165 | final _Filter where; 166 | 167 | /// 元素是否唯一 168 | final bool distinct; 169 | 170 | /// 头部插入divider 171 | final bool startWithDivider; 172 | 173 | /// 尾部插入divider 174 | final bool endWithDivider; 175 | 176 | /// 头部控件 177 | final Widget header; 178 | 179 | final _inLoading = Value(false); 180 | 181 | @override 182 | Widget build(BuildContext context) { 183 | Widget result = PreferredStreamBuilder>( 184 | stream: stream, 185 | showLoading: showLoading, 186 | initialData: initialData, 187 | emptyPlaceholder: emptyPlaceholder, 188 | loadingPlaceholder: loadingPlaceholder, 189 | errorPlaceholderBuilder: errorPlaceholderBuilder, 190 | builder: (data) { 191 | List filteredData = data; 192 | 193 | if (where != null) { 194 | filteredData = filteredData.where(where).toList(); 195 | } 196 | 197 | if (distinct) { 198 | filteredData = filteredData.toSet().toList(); 199 | } 200 | 201 | return ListView.builder( 202 | padding: padding, 203 | shrinkWrap: shrinkWrap, 204 | physics: physics, 205 | reverse: reverse, 206 | controller: _controller, 207 | itemCount: filteredData.length ?? 0, 208 | itemBuilder: (context, index) { 209 | if (header != null) { 210 | if (index == 0) { 211 | return header; 212 | } else { 213 | return _buildItem( 214 | context, 215 | filteredData, 216 | index - 1, 217 | reverse, 218 | startWithDivider, 219 | endWithDivider, 220 | divider, 221 | itemBuilder, 222 | ); 223 | } 224 | } else { 225 | return _buildItem( 226 | context, 227 | filteredData, 228 | index, 229 | reverse, 230 | startWithDivider, 231 | endWithDivider, 232 | divider, 233 | itemBuilder, 234 | ); 235 | } 236 | }, 237 | ); 238 | }, 239 | ); 240 | 241 | if (onRefresh != null) { 242 | result = RefreshIndicator( 243 | child: result, 244 | onRefresh: onRefresh, 245 | displacement: refreshDisplacement, 246 | color: refreshColor, 247 | backgroundColor: refreshBackgroundColor, 248 | notificationPredicate: notificationPredicate, 249 | ); 250 | } 251 | return result; 252 | } 253 | } 254 | 255 | Widget _buildItem( 256 | BuildContext context, 257 | List filteredData, 258 | int index, 259 | bool reverse, 260 | bool startWithDivider, 261 | bool endWithDivider, 262 | Widget divider, 263 | _ItemBuilder itemBuilder, 264 | ) { 265 | final data = filteredData[index]; 266 | if (divider == null) { 267 | return itemBuilder(context, index, data); 268 | } else if (startWithDivider && index == 0) { 269 | return Column( 270 | children: [ 271 | divider, 272 | itemBuilder(context, index, data), 273 | divider, 274 | ], 275 | ); 276 | } else if (endWithDivider) { 277 | if (reverse) { 278 | return Column( 279 | children: [ 280 | divider, 281 | itemBuilder(context, index, data), 282 | ], 283 | ); 284 | } else { 285 | return Column( 286 | children: [ 287 | itemBuilder(context, index, data), 288 | divider, 289 | ], 290 | ); 291 | } 292 | } else { 293 | if (index < filteredData.length - 1) { 294 | if (reverse) { 295 | return Column( 296 | children: [ 297 | divider, 298 | itemBuilder(context, index, data), 299 | ], 300 | ); 301 | } else { 302 | return Column( 303 | children: [ 304 | itemBuilder(context, index, data), 305 | divider, 306 | ], 307 | ); 308 | } 309 | } else { 310 | return itemBuilder(context, index, data); 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/debounce_text_field.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:rxdart/rxdart.dart'; 4 | 5 | /// 可以设置在停止接收到[TextField]的[onChanged]方法多久后再触发[DebounceTextField]的 6 | /// [onChanged]方法 7 | /// 适用场景: [TextField]一边输入内容, 一边根据内容进行网络请求(或其他耗资源的操作), 这时需要 8 | /// 根据输入的节奏来调整网络请求的次数, [duration]参数可以设置在用户停止输入多久之后, 开始网络 9 | /// 请求(或其他耗资源操作). 10 | class DebounceTextField extends StatefulWidget { 11 | /// change触发方法 12 | final ValueChanged onChanged; 13 | 14 | /// 延迟多少时间触发[onChanged]方法 15 | final Duration duration; 16 | 17 | // 以下属性为TextField的属性 18 | final TextEditingController controller; 19 | final FocusNode focusNode; 20 | final InputDecoration decoration; 21 | final TextInputType keyboardType; 22 | final TextStyle style; 23 | final TextAlign textAlign; 24 | final bool autofocus; 25 | final bool obscureText; 26 | final bool autocorrect; 27 | final int maxLines; 28 | final int maxLength; 29 | final bool maxLengthEnforced; 30 | final ValueChanged onSubmitted; 31 | final List inputFormatters; 32 | final bool enabled; 33 | final TextInputAction textInputAction; 34 | 35 | DebounceTextField({ 36 | Key key, 37 | this.onChanged, 38 | this.duration = const Duration(milliseconds: 500), 39 | this.controller, 40 | this.focusNode, 41 | this.decoration = const InputDecoration(), 42 | TextInputType keyboardType = TextInputType.text, 43 | this.style, 44 | this.textAlign = TextAlign.start, 45 | this.autofocus = false, 46 | this.obscureText = false, 47 | this.autocorrect = true, 48 | this.maxLines = 1, 49 | this.maxLength, 50 | this.maxLengthEnforced = true, 51 | this.onSubmitted, 52 | this.inputFormatters, 53 | this.enabled, 54 | this.textInputAction, 55 | }) : assert(keyboardType != null), 56 | assert(textAlign != null), 57 | assert(autofocus != null), 58 | assert(obscureText != null), 59 | assert(autocorrect != null), 60 | assert(maxLengthEnforced != null), 61 | assert(maxLines == null || maxLines > 0), 62 | assert(maxLength == null || maxLength > 0), 63 | keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline, 64 | super(key: key); 65 | 66 | @override 67 | _DebounceTextFieldState createState() => _DebounceTextFieldState(); 68 | } 69 | 70 | class _DebounceTextFieldState extends State { 71 | final _subject = PublishSubject(); 72 | 73 | @override 74 | void initState() { 75 | super.initState(); 76 | _subject.debounceTime(widget.duration).listen(widget.onChanged); 77 | } 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return TextField( 82 | onChanged: (text) => _subject.add(text), 83 | controller: widget.controller, 84 | focusNode: widget.focusNode, 85 | decoration: widget.decoration, 86 | keyboardType: widget.keyboardType, 87 | style: widget.style, 88 | textAlign: widget.textAlign, 89 | autofocus: widget.autofocus, 90 | obscureText: widget.obscureText, 91 | autocorrect: widget.autocorrect, 92 | maxLines: widget.maxLines, 93 | maxLength: widget.maxLength, 94 | maxLengthEnforced: widget.maxLengthEnforced, 95 | onSubmitted: widget.onSubmitted, 96 | inputFormatters: widget.inputFormatters, 97 | enabled: widget.enabled, 98 | textInputAction: widget.textInputAction, 99 | ); 100 | } 101 | 102 | @override 103 | void dispose() { 104 | _subject.close(); 105 | super.dispose(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/flat_text.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 这里的*Flat*并不是视觉上的*Flat*, 只是把[TextStyle]里的参数提取到[Text]的构造器里, 4 | /// 减少层级 5 | class FlatText extends StatelessWidget { 6 | //region Text 7 | final String data; 8 | 9 | final TextSpan textSpan; 10 | 11 | final TextAlign textAlign; 12 | 13 | final TextDirection textDirection; 14 | 15 | final Locale locale; 16 | 17 | final bool softWrap; 18 | 19 | final TextOverflow overflow; 20 | 21 | final double textScaleFactor; 22 | 23 | final int maxLines; 24 | 25 | final String semanticsLabel; 26 | //endregion 27 | 28 | //region TextStyle 29 | final bool inherit; 30 | 31 | final Color color; 32 | 33 | final String fontFamily; 34 | 35 | final double fontSize; 36 | 37 | final FontWeight fontWeight; 38 | 39 | final FontStyle fontStyle; 40 | 41 | final double letterSpacing; 42 | 43 | final double wordSpacing; 44 | 45 | final TextBaseline textBaseline; 46 | 47 | final double height; 48 | 49 | final Paint foreground; 50 | 51 | final Paint background; 52 | 53 | final TextDecoration decoration; 54 | 55 | final Color decorationColor; 56 | 57 | final TextDecorationStyle decorationStyle; 58 | 59 | final String debugLabel; 60 | //endregion 61 | 62 | const FlatText( 63 | this.data, { 64 | Key key, 65 | this.textSpan, 66 | this.textAlign, 67 | this.textDirection, 68 | this.locale, 69 | this.softWrap, 70 | this.overflow, 71 | this.textScaleFactor, 72 | this.maxLines, 73 | this.semanticsLabel, 74 | this.inherit = true, 75 | this.color, 76 | this.fontFamily, 77 | this.fontSize, 78 | this.fontWeight, 79 | this.fontStyle, 80 | this.letterSpacing, 81 | this.wordSpacing, 82 | this.textBaseline, 83 | this.height, 84 | this.foreground, 85 | this.background, 86 | this.decoration, 87 | this.decorationColor, 88 | this.decorationStyle, 89 | this.debugLabel, 90 | }) : super(key: key); 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return Text( 95 | data, 96 | key: key, 97 | style: TextStyle( 98 | inherit: inherit, 99 | color: color, 100 | fontSize: fontSize, 101 | fontWeight: fontWeight, 102 | fontStyle: fontStyle, 103 | letterSpacing: letterSpacing, 104 | wordSpacing: wordSpacing, 105 | textBaseline: textBaseline, 106 | height: height, 107 | locale: locale, 108 | foreground: foreground, 109 | background: background, 110 | decoration: decoration, 111 | decorationColor: decorationColor, 112 | decorationStyle: decorationStyle, 113 | debugLabel: debugLabel, 114 | fontFamily: fontFamily, 115 | ), 116 | textAlign: textAlign, 117 | textDirection: textDirection, 118 | locale: locale, 119 | softWrap: softWrap, 120 | overflow: overflow, 121 | textScaleFactor: textScaleFactor, 122 | maxLines: maxLines, 123 | semanticsLabel: semanticsLabel, 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/free/free_bottom_sheet.widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | const Duration _kBottomSheetDuration = Duration(milliseconds: 200); 8 | const double _kMinFlingVelocity = 700.0; 9 | const double _kCloseProgressThreshold = 0.5; 10 | 11 | /// 可以自由设置高度的bottom sheet, 就是解除了系统自带sheet的高度限制 12 | class FreeBottomSheet extends StatefulWidget { 13 | const FreeBottomSheet({ 14 | Key key, 15 | this.animationController, 16 | this.enableDrag = true, 17 | this.elevation = 0.0, 18 | @required this.onClosing, 19 | @required this.builder, 20 | }) : assert(enableDrag != null), 21 | assert(onClosing != null), 22 | assert(builder != null), 23 | assert(elevation != null), 24 | super(key: key); 25 | 26 | final AnimationController animationController; 27 | 28 | final VoidCallback onClosing; 29 | 30 | final WidgetBuilder builder; 31 | 32 | final bool enableDrag; 33 | 34 | final double elevation; 35 | 36 | @override 37 | _FreeBottomSheetState createState() => _FreeBottomSheetState(); 38 | 39 | static AnimationController createAnimationController(TickerProvider vsync) { 40 | return AnimationController( 41 | duration: _kBottomSheetDuration, 42 | debugLabel: 'BottomSheet', 43 | vsync: vsync, 44 | ); 45 | } 46 | } 47 | 48 | class _FreeBottomSheetState extends State { 49 | final GlobalKey _childKey = GlobalKey(debugLabel: 'BottomSheet child'); 50 | 51 | double get _childHeight { 52 | final RenderBox renderBox = _childKey.currentContext.findRenderObject(); 53 | return renderBox.size.height; 54 | } 55 | 56 | bool get _dismissUnderway => 57 | widget.animationController.status == AnimationStatus.reverse; 58 | 59 | void _handleDragUpdate(DragUpdateDetails details) { 60 | if (_dismissUnderway) return; 61 | widget.animationController.value -= 62 | details.primaryDelta / (_childHeight ?? details.primaryDelta); 63 | } 64 | 65 | void _handleDragEnd(DragEndDetails details) { 66 | if (_dismissUnderway) return; 67 | if (details.velocity.pixelsPerSecond.dy > _kMinFlingVelocity) { 68 | final double flingVelocity = 69 | -details.velocity.pixelsPerSecond.dy / _childHeight; 70 | if (widget.animationController.value > 0.0) 71 | widget.animationController.fling(velocity: flingVelocity); 72 | if (flingVelocity < 0.0) widget.onClosing(); 73 | } else if (widget.animationController.value < _kCloseProgressThreshold) { 74 | if (widget.animationController.value > 0.0) 75 | widget.animationController.fling(velocity: -1.0); 76 | widget.onClosing(); 77 | } else { 78 | widget.animationController.forward(); 79 | } 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | final Widget bottomSheet = Material( 85 | key: _childKey, 86 | elevation: widget.elevation, 87 | child: widget.builder(context), 88 | ); 89 | return !widget.enableDrag 90 | ? bottomSheet 91 | : GestureDetector( 92 | onVerticalDragUpdate: _handleDragUpdate, 93 | onVerticalDragEnd: _handleDragEnd, 94 | child: bottomSheet, 95 | excludeFromSemantics: true, 96 | ); 97 | } 98 | } 99 | 100 | // PERSISTENT BOTTOM SHEETS 101 | 102 | // See scaffold.dart 103 | 104 | // MODAL BOTTOM SHEETS 105 | 106 | class _ModalBottomSheetLayout extends SingleChildLayoutDelegate { 107 | _ModalBottomSheetLayout(this.progress); 108 | 109 | final double progress; 110 | 111 | @override 112 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 113 | return BoxConstraints( 114 | minWidth: constraints.maxWidth, 115 | maxWidth: constraints.maxWidth, 116 | minHeight: 0.0, 117 | maxHeight: constraints.maxHeight, 118 | ); 119 | } 120 | 121 | @override 122 | Offset getPositionForChild(Size size, Size childSize) { 123 | return Offset(0.0, size.height - childSize.height * progress); 124 | } 125 | 126 | @override 127 | bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) { 128 | return progress != oldDelegate.progress; 129 | } 130 | } 131 | 132 | class _ModalBottomSheet extends StatefulWidget { 133 | const _ModalBottomSheet({Key key, this.route}) : super(key: key); 134 | 135 | final _ModalBottomSheetRoute route; 136 | 137 | @override 138 | _ModalBottomSheetState createState() => _ModalBottomSheetState(); 139 | } 140 | 141 | class _ModalBottomSheetState extends State<_ModalBottomSheet> { 142 | @override 143 | Widget build(BuildContext context) { 144 | final MediaQueryData mediaQuery = MediaQuery.of(context); 145 | final MaterialLocalizations localizations = 146 | MaterialLocalizations.of(context); 147 | String routeLabel; 148 | switch (defaultTargetPlatform) { 149 | case TargetPlatform.iOS: 150 | routeLabel = ''; 151 | break; 152 | case TargetPlatform.android: 153 | case TargetPlatform.fuchsia: 154 | routeLabel = localizations.dialogLabel; 155 | break; 156 | } 157 | 158 | return GestureDetector( 159 | excludeFromSemantics: true, 160 | onTap: () => Navigator.pop(context), 161 | child: AnimatedBuilder( 162 | animation: widget.route.animation, 163 | builder: (BuildContext context, Widget child) { 164 | // Disable the initial animation when accessible navigation is on so 165 | // that the semantics are added to the tree at the correct time. 166 | final double animationValue = mediaQuery.accessibleNavigation 167 | ? 1.0 168 | : widget.route.animation.value; 169 | return Semantics( 170 | scopesRoute: true, 171 | namesRoute: true, 172 | label: routeLabel, 173 | explicitChildNodes: true, 174 | child: ClipRect( 175 | child: CustomSingleChildLayout( 176 | delegate: _ModalBottomSheetLayout(animationValue), 177 | child: FreeBottomSheet( 178 | animationController: widget.route._animationController, 179 | onClosing: () => Navigator.pop(context), 180 | builder: widget.route.builder, 181 | ), 182 | ), 183 | ), 184 | ); 185 | }, 186 | ), 187 | ); 188 | } 189 | } 190 | 191 | class _ModalBottomSheetRoute extends PopupRoute { 192 | _ModalBottomSheetRoute({ 193 | this.builder, 194 | this.theme, 195 | this.barrierLabel, 196 | RouteSettings settings, 197 | }) : super(settings: settings); 198 | 199 | final WidgetBuilder builder; 200 | final ThemeData theme; 201 | 202 | @override 203 | Duration get transitionDuration => _kBottomSheetDuration; 204 | 205 | @override 206 | bool get barrierDismissible => true; 207 | 208 | @override 209 | final String barrierLabel; 210 | 211 | @override 212 | Color get barrierColor => Colors.black54; 213 | 214 | AnimationController _animationController; 215 | 216 | @override 217 | AnimationController createAnimationController() { 218 | assert(_animationController == null); 219 | _animationController = 220 | FreeBottomSheet.createAnimationController(navigator.overlay); 221 | return _animationController; 222 | } 223 | 224 | @override 225 | Widget buildPage(BuildContext context, Animation animation, 226 | Animation secondaryAnimation) { 227 | // By definition, the bottom sheet is aligned to the bottom of the page 228 | // and isn't exposed to the top padding of the MediaQuery. 229 | Widget bottomSheet = MediaQuery.removePadding( 230 | context: context, 231 | removeTop: true, 232 | child: _ModalBottomSheet(route: this), 233 | ); 234 | if (theme != null) bottomSheet = Theme(data: theme, child: bottomSheet); 235 | return bottomSheet; 236 | } 237 | } 238 | 239 | Future showFreeModalBottomSheet({ 240 | @required BuildContext context, 241 | @required WidgetBuilder builder, 242 | }) { 243 | assert(context != null); 244 | assert(builder != null); 245 | assert(debugCheckHasMaterialLocalizations(context)); 246 | return Navigator.push( 247 | context, 248 | _ModalBottomSheetRoute( 249 | builder: builder, 250 | theme: Theme.of(context, shadowThemeOnly: true), 251 | barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, 252 | ), 253 | ); 254 | } 255 | 256 | PersistentBottomSheetController showFreeBottomSheet({ 257 | @required BuildContext context, 258 | @required WidgetBuilder builder, 259 | }) { 260 | assert(context != null); 261 | assert(builder != null); 262 | return Scaffold.of(context).showBottomSheet(builder); 263 | } 264 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/free/free_check_box.widget.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter/rendering.dart'; 9 | import 'package:flutter/widgets.dart'; 10 | 11 | /// 去除了官方checkbox中默认padding的限制 12 | class FreeCheckbox extends StatefulWidget { 13 | const FreeCheckbox({ 14 | Key key, 15 | @required this.value, 16 | this.tristate = false, 17 | @required this.onChanged, 18 | this.activeColor, 19 | this.checkColor, 20 | this.materialTapTargetSize, 21 | }) : assert(tristate != null), 22 | assert(tristate || value != null), 23 | super(key: key); 24 | 25 | final bool value; 26 | 27 | final ValueChanged onChanged; 28 | 29 | final Color activeColor; 30 | 31 | final Color checkColor; 32 | 33 | final bool tristate; 34 | 35 | final MaterialTapTargetSize materialTapTargetSize; 36 | 37 | static const double width = 18.0; 38 | 39 | @override 40 | _FreeCheckboxState createState() => _FreeCheckboxState(); 41 | } 42 | 43 | class _FreeCheckboxState extends State 44 | with TickerProviderStateMixin { 45 | @override 46 | Widget build(BuildContext context) { 47 | assert(debugCheckHasMaterial(context)); 48 | final ThemeData themeData = Theme.of(context); 49 | Size size; 50 | switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { 51 | case MaterialTapTargetSize.padded: 52 | size = const Size( 53 | 2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0); 54 | break; 55 | case MaterialTapTargetSize.shrinkWrap: 56 | size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius); 57 | break; 58 | } 59 | // final BoxConstraints additionalConstraints = BoxConstraints.tight(size); 60 | Size noTapTargetSize = Size(FreeCheckbox.width, FreeCheckbox.width); 61 | final additionalConstraints = BoxConstraints.tight(noTapTargetSize); 62 | return _CheckboxRenderObjectWidget( 63 | value: widget.value, 64 | tristate: widget.tristate, 65 | activeColor: widget.activeColor ?? themeData.toggleableActiveColor, 66 | checkColor: widget.checkColor ?? const Color(0xFFFFFFFF), 67 | inactiveColor: widget.onChanged != null 68 | ? themeData.unselectedWidgetColor 69 | : themeData.disabledColor, 70 | onChanged: widget.onChanged, 71 | additionalConstraints: additionalConstraints, 72 | vsync: this, 73 | ); 74 | } 75 | } 76 | 77 | class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { 78 | const _CheckboxRenderObjectWidget({ 79 | Key key, 80 | @required this.value, 81 | @required this.tristate, 82 | @required this.activeColor, 83 | @required this.checkColor, 84 | @required this.inactiveColor, 85 | @required this.onChanged, 86 | @required this.vsync, 87 | @required this.additionalConstraints, 88 | }) : assert(tristate != null), 89 | assert(tristate || value != null), 90 | assert(activeColor != null), 91 | assert(inactiveColor != null), 92 | assert(vsync != null), 93 | super(key: key); 94 | 95 | final bool value; 96 | final bool tristate; 97 | final Color activeColor; 98 | final Color checkColor; 99 | final Color inactiveColor; 100 | final ValueChanged onChanged; 101 | final TickerProvider vsync; 102 | final BoxConstraints additionalConstraints; 103 | 104 | @override 105 | _RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox( 106 | value: value, 107 | tristate: tristate, 108 | activeColor: activeColor, 109 | checkColor: checkColor, 110 | inactiveColor: inactiveColor, 111 | onChanged: onChanged, 112 | vsync: vsync, 113 | additionalConstraints: additionalConstraints, 114 | ); 115 | 116 | @override 117 | void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) { 118 | renderObject 119 | ..value = value 120 | ..tristate = tristate 121 | ..activeColor = activeColor 122 | ..checkColor = checkColor 123 | ..inactiveColor = inactiveColor 124 | ..onChanged = onChanged 125 | ..additionalConstraints = additionalConstraints 126 | ..vsync = vsync; 127 | } 128 | } 129 | 130 | const double _kEdgeSize = FreeCheckbox.width; 131 | const Radius _kEdgeRadius = Radius.circular(1.0); 132 | const double _kStrokeWidth = 2.0; 133 | 134 | class _RenderCheckbox extends RenderToggleable { 135 | _RenderCheckbox({ 136 | bool value, 137 | bool tristate, 138 | Color activeColor, 139 | this.checkColor, 140 | Color inactiveColor, 141 | BoxConstraints additionalConstraints, 142 | ValueChanged onChanged, 143 | @required TickerProvider vsync, 144 | }) : _oldValue = value, 145 | super( 146 | value: value, 147 | tristate: tristate, 148 | activeColor: activeColor, 149 | inactiveColor: inactiveColor, 150 | onChanged: onChanged, 151 | additionalConstraints: additionalConstraints, 152 | vsync: vsync, 153 | ); 154 | 155 | bool _oldValue; 156 | Color checkColor; 157 | 158 | @override 159 | set value(bool newValue) { 160 | if (newValue == value) return; 161 | _oldValue = value; 162 | super.value = newValue; 163 | } 164 | 165 | @override 166 | void describeSemanticsConfiguration(SemanticsConfiguration config) { 167 | super.describeSemanticsConfiguration(config); 168 | config.isChecked = value == true; 169 | } 170 | 171 | // The square outer bounds of the checkbox at t, with the specified origin. 172 | // At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width) 173 | // At t == 0.5, .. is _kEdgeSize - _kStrokeWidth 174 | // At t == 1.0, .. is _kEdgeSize 175 | RRect _outerRectAt(Offset origin, double t) { 176 | final double inset = 1.0 - (t - 0.5).abs() * 2.0; 177 | final double size = _kEdgeSize - inset * _kStrokeWidth; 178 | final Rect rect = 179 | Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size); 180 | return RRect.fromRectAndRadius(rect, _kEdgeRadius); 181 | } 182 | 183 | // The checkbox's border color if value == false, or its fill color when 184 | // value == true or null. 185 | Color _colorAt(double t) { 186 | // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor. 187 | return onChanged == null 188 | ? inactiveColor 189 | : (t >= 0.25 190 | ? activeColor 191 | : Color.lerp(inactiveColor, activeColor, t * 4.0)); 192 | } 193 | 194 | // White stroke used to paint the check and dash. 195 | void _initStrokePaint(Paint paint) { 196 | paint 197 | ..color = checkColor 198 | ..style = PaintingStyle.stroke 199 | ..strokeWidth = _kStrokeWidth; 200 | } 201 | 202 | void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) { 203 | assert(t >= 0.0 && t <= 0.5); 204 | final double size = outer.width; 205 | // As t goes from 0.0 to 1.0, gradually fill the outer RRect. 206 | final RRect inner = 207 | outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t)); 208 | canvas.drawDRRect(outer, inner, paint); 209 | } 210 | 211 | void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) { 212 | assert(t >= 0.0 && t <= 1.0); 213 | // As t goes from 0.0 to 1.0, animate the two check mark strokes from the 214 | // short side to the long side. 215 | final Path path = Path(); 216 | const Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45); 217 | const Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7); 218 | const Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25); 219 | if (t < 0.5) { 220 | final double strokeT = t * 2.0; 221 | final Offset drawMid = Offset.lerp(start, mid, strokeT); 222 | path.moveTo(origin.dx + start.dx, origin.dy + start.dy); 223 | path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy); 224 | } else { 225 | final double strokeT = (t - 0.5) * 2.0; 226 | final Offset drawEnd = Offset.lerp(mid, end, strokeT); 227 | path.moveTo(origin.dx + start.dx, origin.dy + start.dy); 228 | path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy); 229 | path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy); 230 | } 231 | canvas.drawPath(path, paint); 232 | } 233 | 234 | void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) { 235 | assert(t >= 0.0 && t <= 1.0); 236 | // As t goes from 0.0 to 1.0, animate the horizontal line from the 237 | // mid point outwards. 238 | const Offset start = Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5); 239 | const Offset mid = Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5); 240 | const Offset end = Offset(_kEdgeSize * 0.8, _kEdgeSize * 0.5); 241 | final Offset drawStart = Offset.lerp(start, mid, 1.0 - t); 242 | final Offset drawEnd = Offset.lerp(mid, end, t); 243 | canvas.drawLine(origin + drawStart, origin + drawEnd, paint); 244 | } 245 | 246 | @override 247 | void paint(PaintingContext context, Offset offset) { 248 | final Canvas canvas = context.canvas; 249 | paintRadialReaction(canvas, offset, size.center(Offset.zero)); 250 | 251 | final Offset origin = 252 | offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0); 253 | final AnimationStatus status = position.status; 254 | final double tNormalized = 255 | status == AnimationStatus.forward || status == AnimationStatus.completed 256 | ? position.value 257 | : 1.0 - position.value; 258 | 259 | // Four cases: false to null, false to true, null to false, true to false 260 | if (_oldValue == false || value == false) { 261 | final double t = value == false ? 1.0 - tNormalized : tNormalized; 262 | final RRect outer = _outerRectAt(origin, t); 263 | final Paint paint = Paint()..color = _colorAt(t); 264 | 265 | if (t <= 0.5) { 266 | _drawBorder(canvas, outer, t, paint); 267 | } else { 268 | canvas.drawRRect(outer, paint); 269 | 270 | _initStrokePaint(paint); 271 | final double tShrink = (t - 0.5) * 2.0; 272 | if (_oldValue == null || value == null) 273 | _drawDash(canvas, origin, tShrink, paint); 274 | else 275 | _drawCheck(canvas, origin, tShrink, paint); 276 | } 277 | } else { 278 | // Two cases: null to true, true to null 279 | final RRect outer = _outerRectAt(origin, 1.0); 280 | final Paint paint = Paint()..color = _colorAt(1.0); 281 | canvas.drawRRect(outer, paint); 282 | 283 | _initStrokePaint(paint); 284 | if (tNormalized <= 0.5) { 285 | final double tShrink = 1.0 - tNormalized * 2.0; 286 | if (_oldValue == true) 287 | _drawCheck(canvas, origin, tShrink, paint); 288 | else 289 | _drawDash(canvas, origin, tShrink, paint); 290 | } else { 291 | final double tExpand = (tNormalized - 0.5) * 2.0; 292 | if (value == true) 293 | _drawCheck(canvas, origin, tExpand, paint); 294 | else 295 | _drawDash(canvas, origin, tExpand, paint); 296 | } 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/free/free_dialog.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 解除了Material Design对于Dialog的边距限制的Dialog 4 | /// 大体部分之间拷贝了原始的[Dialog]类, 就修改了下[padding] 5 | class FreeDialog extends StatelessWidget { 6 | const FreeDialog({ 7 | Key key, 8 | this.child, 9 | this.insetAnimationDuration = const Duration(milliseconds: 100), 10 | this.insetAnimationCurve = Curves.decelerate, 11 | }) : super(key: key); 12 | 13 | final Widget child; 14 | 15 | final Duration insetAnimationDuration; 16 | 17 | final Curve insetAnimationCurve; 18 | 19 | Color _getColor(BuildContext context) { 20 | return Theme.of(context).dialogBackgroundColor; 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return AnimatedPadding( 26 | padding: MediaQuery.of(context).viewInsets, 27 | duration: insetAnimationDuration, 28 | curve: insetAnimationCurve, 29 | child: MediaQuery.removeViewInsets( 30 | removeLeft: true, 31 | removeTop: true, 32 | removeRight: true, 33 | removeBottom: true, 34 | context: context, 35 | child: Center( 36 | child: ConstrainedBox( 37 | constraints: const BoxConstraints(minWidth: 280.0), 38 | child: Material( 39 | elevation: 24.0, 40 | color: _getColor(context), 41 | type: MaterialType.card, 42 | child: child, 43 | ), 44 | ), 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/free/tap_tooltip.widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/rendering.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | 8 | const Duration _kFadeDuration = Duration(milliseconds: 200); 9 | const Duration _kShowDuration = Duration(milliseconds: 1500); 10 | 11 | /// 点击即出现帮助信息的Tooltip, 基本上复用了官方的[Tooltip], 就是长按触发换成了点击触发 12 | class TapTooltip extends StatefulWidget { 13 | const TapTooltip({ 14 | Key key, 15 | @required this.message, 16 | this.height = 32.0, 17 | this.padding = const EdgeInsets.symmetric(horizontal: 16.0), 18 | this.verticalOffset = 24.0, 19 | this.preferBelow = true, 20 | this.excludeFromSemantics = false, 21 | this.child, 22 | }) : assert(message != null), 23 | assert(height != null), 24 | assert(padding != null), 25 | assert(verticalOffset != null), 26 | assert(preferBelow != null), 27 | assert(excludeFromSemantics != null), 28 | super(key: key); 29 | 30 | /// The text to display in the tooltip. 31 | final String message; 32 | 33 | /// The amount of vertical space the tooltip should occupy (inside its padding). 34 | final double height; 35 | 36 | /// The amount of space by which to inset the child. 37 | /// 38 | /// Defaults to 16.0 logical pixels in each direction. 39 | final EdgeInsetsGeometry padding; 40 | 41 | /// The amount of vertical distance between the widget and the displayed tooltip. 42 | final double verticalOffset; 43 | 44 | /// Whether the tooltip defaults to being displayed below the widget. 45 | /// 46 | /// Defaults to true. If there is insufficient space to display the tooltip in 47 | /// the preferred direction, the tooltip will be displayed in the opposite 48 | /// direction. 49 | final bool preferBelow; 50 | 51 | /// Whether the tooltip's [message] should be excluded from the semantics 52 | /// tree. 53 | final bool excludeFromSemantics; 54 | 55 | /// The widget below this widget in the tree. 56 | /// 57 | /// {@macro flutter.widgets.child} 58 | final Widget child; 59 | 60 | @override 61 | _TooltipState createState() => _TooltipState(); 62 | 63 | @override 64 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 65 | super.debugFillProperties(properties); 66 | properties.add(StringProperty('message', message, showName: false)); 67 | properties.add(DoubleProperty('vertical offset', verticalOffset)); 68 | properties.add(FlagProperty('position', 69 | value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true)); 70 | } 71 | } 72 | 73 | class _TooltipState extends State 74 | with SingleTickerProviderStateMixin { 75 | AnimationController _controller; 76 | OverlayEntry _entry; 77 | Timer _timer; 78 | 79 | @override 80 | void initState() { 81 | super.initState(); 82 | _controller = AnimationController(duration: _kFadeDuration, vsync: this) 83 | ..addStatusListener(_handleStatusChanged); 84 | } 85 | 86 | void _handleStatusChanged(AnimationStatus status) { 87 | if (status == AnimationStatus.dismissed) _removeEntry(); 88 | } 89 | 90 | /// Shows the tooltip if it is not already visible. 91 | /// 92 | /// Returns `false` when the tooltip was already visible. 93 | bool ensureTooltipVisible() { 94 | if (_entry != null) { 95 | _timer?.cancel(); 96 | _timer = null; 97 | _controller.forward(); 98 | return false; // Already visible. 99 | } 100 | final RenderBox box = context.findRenderObject(); 101 | final Offset target = box.localToGlobal(box.size.center(Offset.zero)); 102 | // We create this widget outside of the overlay entry's builder to prevent 103 | // updated values from happening to leak into the overlay when the overlay 104 | // rebuilds. 105 | final Widget overlay = _TooltipOverlay( 106 | message: widget.message, 107 | height: widget.height, 108 | padding: widget.padding, 109 | animation: 110 | CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn), 111 | target: target, 112 | verticalOffset: widget.verticalOffset, 113 | preferBelow: widget.preferBelow); 114 | _entry = OverlayEntry(builder: (BuildContext context) => overlay); 115 | Overlay.of(context, debugRequiredFor: widget).insert(_entry); 116 | GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent); 117 | SemanticsService.tooltip(widget.message); 118 | _controller.forward(); 119 | return true; 120 | } 121 | 122 | void _removeEntry() { 123 | assert(_entry != null); 124 | _timer?.cancel(); 125 | _timer = null; 126 | _entry.remove(); 127 | _entry = null; 128 | GestureBinding.instance.pointerRouter 129 | .removeGlobalRoute(_handlePointerEvent); 130 | } 131 | 132 | void _handlePointerEvent(PointerEvent event) { 133 | assert(_entry != null); 134 | if (event is PointerUpEvent || event is PointerCancelEvent) 135 | _timer ??= Timer(_kShowDuration, _controller.reverse); 136 | else if (event is PointerDownEvent) _controller.reverse(); 137 | } 138 | 139 | @override 140 | void deactivate() { 141 | if (_entry != null) _controller.reverse(); 142 | super.deactivate(); 143 | } 144 | 145 | @override 146 | void dispose() { 147 | if (_entry != null) _removeEntry(); 148 | _controller.dispose(); 149 | super.dispose(); 150 | } 151 | 152 | void _handleTap() { 153 | final bool tooltipCreated = ensureTooltipVisible(); 154 | if (tooltipCreated) Feedback.forLongPress(context); 155 | } 156 | 157 | @override 158 | Widget build(BuildContext context) { 159 | assert(Overlay.of(context, debugRequiredFor: widget) != null); 160 | return GestureDetector( 161 | behavior: HitTestBehavior.opaque, 162 | onTap: _handleTap, 163 | excludeFromSemantics: true, 164 | child: Semantics( 165 | label: widget.excludeFromSemantics ? null : widget.message, 166 | child: widget.child, 167 | ), 168 | ); 169 | } 170 | } 171 | 172 | /// A delegate for computing the layout of a tooltip to be displayed above or 173 | /// bellow a target specified in the global coordinate system. 174 | class _TooltipPositionDelegate extends SingleChildLayoutDelegate { 175 | /// Creates a delegate for computing the layout of a tooltip. 176 | /// 177 | /// The arguments must not be null. 178 | _TooltipPositionDelegate({ 179 | @required this.target, 180 | @required this.verticalOffset, 181 | @required this.preferBelow, 182 | }) : assert(target != null), 183 | assert(verticalOffset != null), 184 | assert(preferBelow != null); 185 | 186 | /// The offset of the target the tooltip is positioned near in the global 187 | /// coordinate system. 188 | final Offset target; 189 | 190 | /// The amount of vertical distance between the target and the displayed 191 | /// tooltip. 192 | final double verticalOffset; 193 | 194 | /// Whether the tooltip defaults to being displayed below the widget. 195 | /// 196 | /// If there is insufficient space to display the tooltip in the preferred 197 | /// direction, the tooltip will be displayed in the opposite direction. 198 | final bool preferBelow; 199 | 200 | @override 201 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) => 202 | constraints.loosen(); 203 | 204 | @override 205 | Offset getPositionForChild(Size size, Size childSize) { 206 | return positionDependentBox( 207 | size: size, 208 | childSize: childSize, 209 | target: target, 210 | verticalOffset: verticalOffset, 211 | preferBelow: preferBelow, 212 | ); 213 | } 214 | 215 | @override 216 | bool shouldRelayout(_TooltipPositionDelegate oldDelegate) { 217 | return target != oldDelegate.target || 218 | verticalOffset != oldDelegate.verticalOffset || 219 | preferBelow != oldDelegate.preferBelow; 220 | } 221 | } 222 | 223 | class _TooltipOverlay extends StatelessWidget { 224 | const _TooltipOverlay({ 225 | Key key, 226 | this.message, 227 | this.height, 228 | this.padding, 229 | this.animation, 230 | this.target, 231 | this.verticalOffset, 232 | this.preferBelow, 233 | }) : super(key: key); 234 | 235 | final String message; 236 | final double height; 237 | final EdgeInsetsGeometry padding; 238 | final Animation animation; 239 | final Offset target; 240 | final double verticalOffset; 241 | final bool preferBelow; 242 | 243 | @override 244 | Widget build(BuildContext context) { 245 | final ThemeData theme = Theme.of(context); 246 | final ThemeData darkTheme = ThemeData( 247 | brightness: Brightness.dark, 248 | textTheme: theme.brightness == Brightness.dark 249 | ? theme.textTheme 250 | : theme.primaryTextTheme, 251 | platform: theme.platform, 252 | ); 253 | return Positioned.fill( 254 | child: IgnorePointer( 255 | child: CustomSingleChildLayout( 256 | delegate: _TooltipPositionDelegate( 257 | target: target, 258 | verticalOffset: verticalOffset, 259 | preferBelow: preferBelow, 260 | ), 261 | child: FadeTransition( 262 | opacity: animation, 263 | child: Opacity( 264 | opacity: 0.9, 265 | child: ConstrainedBox( 266 | constraints: BoxConstraints(minHeight: height), 267 | child: Container( 268 | decoration: BoxDecoration( 269 | color: darkTheme.backgroundColor, 270 | borderRadius: BorderRadius.circular(2.0), 271 | ), 272 | padding: padding, 273 | child: Center( 274 | widthFactor: 1.0, 275 | heightFactor: 1.0, 276 | child: Text(message, style: darkTheme.textTheme.body1), 277 | ), 278 | ), 279 | ), 280 | ), 281 | ), 282 | ), 283 | ), 284 | ); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/preferred_async_builder.widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:location_picker_fluttify/src/ui/widget/visual/placeholder/empty_placeholder.widget.dart'; 5 | import 'package:location_picker_fluttify/src/ui/widget/visual/placeholder/loading.widget.dart'; 6 | import 'package:location_picker_fluttify/src/utils/empty_util.dart'; 7 | import 'package:location_picker_fluttify/src/utils/log.dart'; 8 | 9 | import '../placeholder/error_placeholder.widget.dart'; 10 | 11 | typedef Widget _Builder(DATA data); 12 | typedef Widget _ErrorPlaceholderBuilder(BuildContext context, Object error); 13 | 14 | /// **Preferred**表达的语义是对目标widget预定义了一些参数 15 | /// **Decorated**表达的语义是对目标widget进行了一些外围的包装 16 | class PreferredFutureBuilder extends StatelessWidget { 17 | static Widget defaultEmptyPlaceholder; 18 | static Widget defaultErrorPlaceholder; 19 | 20 | static void setDefaultPlaceholder({ 21 | Widget emptyPlaceholder, 22 | Widget errorPlaceholder, 23 | }) { 24 | defaultEmptyPlaceholder = emptyPlaceholder; 25 | defaultErrorPlaceholder = errorPlaceholder; 26 | } 27 | 28 | const PreferredFutureBuilder({ 29 | Key key, 30 | @required this.future, 31 | @required this.builder, 32 | this.showLoading = true, 33 | this.initialData, 34 | this.emptyPlaceholder, 35 | this.errorPlaceholderBuilder, 36 | this.loadingPlaceholder, 37 | }) : super(key: key); 38 | 39 | final Future future; 40 | final _Builder builder; 41 | final bool showLoading; 42 | final T initialData; 43 | final Widget emptyPlaceholder; 44 | final _ErrorPlaceholderBuilder errorPlaceholderBuilder; 45 | final Widget loadingPlaceholder; 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | return FutureBuilder( 50 | initialData: initialData, 51 | future: future, 52 | builder: (ctx, snapshot) { 53 | if (snapshot.hasError) { 54 | L.p('PreferredFutureBuilder出现错误: ${snapshot.error}'); 55 | if (snapshot.error is Error) { 56 | L.p((snapshot.error as Error).stackTrace); 57 | } 58 | if (errorPlaceholderBuilder != null) { 59 | return errorPlaceholderBuilder(context, snapshot.error); 60 | } else { 61 | return defaultErrorPlaceholder ?? const ErrorPlaceholder(); 62 | } 63 | } 64 | 65 | if (snapshot.hasData) { 66 | if (isEmpty(snapshot.data)) { 67 | return emptyPlaceholder ?? 68 | defaultEmptyPlaceholder ?? 69 | const EmptyPlaceholder(); 70 | } else { 71 | return builder(snapshot.data); 72 | } 73 | } else if (showLoading) { 74 | return loadingPlaceholder ?? LoadingWidget(); 75 | } else { 76 | return SizedBox.shrink(); 77 | } 78 | }, 79 | ); 80 | } 81 | } 82 | 83 | class PreferredStreamBuilder extends StatelessWidget { 84 | static Widget defaultEmptyPlaceholder; 85 | static Widget defaultErrorPlaceholder; 86 | 87 | static void setDefaultPlaceholder({ 88 | Widget emptyPlaceholder, 89 | Widget errorPlaceholder, 90 | }) { 91 | defaultEmptyPlaceholder = emptyPlaceholder; 92 | defaultErrorPlaceholder = errorPlaceholder; 93 | } 94 | 95 | const PreferredStreamBuilder({ 96 | Key key, 97 | @required this.stream, 98 | @required this.builder, 99 | this.showLoading = true, 100 | this.initialData, 101 | this.emptyPlaceholder, 102 | this.errorPlaceholderBuilder, 103 | this.loadingPlaceholder, 104 | }) : super(key: key); 105 | 106 | final Stream stream; 107 | final _Builder builder; 108 | final bool showLoading; 109 | final T initialData; 110 | final Widget emptyPlaceholder; 111 | final _ErrorPlaceholderBuilder errorPlaceholderBuilder; 112 | final Widget loadingPlaceholder; 113 | 114 | @override 115 | Widget build(BuildContext context) { 116 | return StreamBuilder( 117 | initialData: initialData, 118 | stream: stream, 119 | builder: (ctx, snapshot) { 120 | if (snapshot.hasError) { 121 | L.p('PreferredStreamBuilder出现错误: ${snapshot.error}'); 122 | if (snapshot.error is Error) { 123 | L.p((snapshot.error as Error).stackTrace); 124 | } 125 | if (errorPlaceholderBuilder != null) { 126 | return errorPlaceholderBuilder(context, snapshot.error); 127 | } else { 128 | return defaultErrorPlaceholder ?? const ErrorPlaceholder(); 129 | } 130 | } 131 | 132 | if (snapshot.hasData) { 133 | if (isEmpty(snapshot.data)) { 134 | return emptyPlaceholder ?? 135 | defaultEmptyPlaceholder ?? 136 | const EmptyPlaceholder(); 137 | } else { 138 | return builder(snapshot.data); 139 | } 140 | } else if (showLoading) { 141 | return loadingPlaceholder ?? LoadingWidget(); 142 | } else { 143 | return SizedBox.shrink(); 144 | } 145 | }, 146 | ); 147 | } 148 | } 149 | 150 | @Deprecated('用DecoratedFutureBuilder代替, 只是简单的名称代替, 这个类不再维护') 151 | class FutureWidget extends StatelessWidget { 152 | static Widget defaultEmptyPlaceholder; 153 | static Widget defaultErrorPlaceholder; 154 | 155 | final Future future; 156 | final _Builder builder; 157 | final bool showLoading; 158 | final T initialData; 159 | final bool isExpanded; 160 | final Widget emptyPlaceholder; 161 | final Widget errorPlaceholder; 162 | 163 | static void setDefaultPlaceholder({ 164 | Widget emptyPlaceholder, 165 | Widget errorPlaceholder, 166 | }) { 167 | defaultEmptyPlaceholder = emptyPlaceholder; 168 | defaultErrorPlaceholder = errorPlaceholder; 169 | } 170 | 171 | const FutureWidget({ 172 | Key key, 173 | @required this.future, 174 | @required this.builder, 175 | this.showLoading = true, 176 | this.isExpanded = false, 177 | this.initialData, 178 | this.emptyPlaceholder, 179 | this.errorPlaceholder, 180 | }) : super(key: key); 181 | 182 | @override 183 | Widget build(BuildContext context) { 184 | return FutureBuilder( 185 | initialData: initialData, 186 | future: future, 187 | builder: (ctx, snapshot) { 188 | if (snapshot.hasError) { 189 | return errorPlaceholder ?? 190 | defaultErrorPlaceholder ?? 191 | const ErrorPlaceholder(); 192 | } 193 | 194 | if (snapshot.hasData) { 195 | if (isEmpty(snapshot.data)) { 196 | return emptyPlaceholder ?? 197 | defaultEmptyPlaceholder ?? 198 | const EmptyPlaceholder(); 199 | } else { 200 | return builder(snapshot.data); 201 | } 202 | } else if (showLoading) { 203 | return LoadingWidget(); 204 | } else if (isExpanded) { 205 | return Expanded(child: SizedBox.shrink()); 206 | } else { 207 | return SizedBox.shrink(); 208 | } 209 | }, 210 | ); 211 | } 212 | } 213 | 214 | @Deprecated('用DecoratedStreamBuilder代替, 只是简单的名称代替, 这个类不再维护') 215 | class StreamWidget extends StatelessWidget { 216 | static Widget defaultEmptyPlaceholder; 217 | static Widget defaultErrorPlaceholder; 218 | 219 | final Stream stream; 220 | final _Builder builder; 221 | 222 | /// 是否显示loading 223 | final bool showLoading; 224 | 225 | final T initData; 226 | final Widget emptyPlaceholder; 227 | final Widget errorPlaceholder; 228 | 229 | static void setDefaultPlaceholder({ 230 | Widget emptyPlaceholder, 231 | Widget errorPlaceholder, 232 | }) { 233 | defaultEmptyPlaceholder = emptyPlaceholder; 234 | defaultErrorPlaceholder = errorPlaceholder; 235 | } 236 | 237 | const StreamWidget({ 238 | Key key, 239 | @required this.stream, 240 | @required this.builder, 241 | this.showLoading = true, 242 | this.initData, 243 | this.emptyPlaceholder, 244 | this.errorPlaceholder, 245 | }) : super(key: key); 246 | 247 | @override 248 | Widget build(BuildContext context) { 249 | return StreamBuilder( 250 | initialData: initData, 251 | stream: stream, 252 | builder: (ctx, snapshot) { 253 | if (snapshot.hasError) { 254 | return errorPlaceholder ?? 255 | defaultErrorPlaceholder ?? 256 | const ErrorPlaceholder(); 257 | } 258 | 259 | if (snapshot.hasData) { 260 | if (isEmpty(snapshot.data)) { 261 | return emptyPlaceholder ?? 262 | defaultEmptyPlaceholder ?? 263 | const EmptyPlaceholder(); 264 | } else { 265 | return builder(snapshot.data); 266 | } 267 | } else if (showLoading) { 268 | return LoadingWidget(); 269 | } else { 270 | return SizedBox.shrink(); 271 | } 272 | }, 273 | ); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/preferred/primary_icon.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 使用primaryColor的Icon 4 | class PrimaryIcon extends StatelessWidget { 5 | final IconData icon; 6 | final double size; 7 | final String semanticLabel; 8 | final TextDirection textDirection; 9 | 10 | const PrimaryIcon( 11 | this.icon, { 12 | Key key, 13 | this.size, 14 | this.semanticLabel, 15 | this.textDirection, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Icon( 21 | icon, 22 | size: size, 23 | color: Theme.of(context).primaryColor, 24 | semanticLabel: semanticLabel, 25 | textDirection: textDirection, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/blur.widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class Blur extends StatelessWidget { 6 | const Blur({ 7 | Key key, 8 | @required this.background, 9 | @required this.foreground, 10 | this.alignment = AlignmentDirectional.center, 11 | this.sigmaX = 0, 12 | this.sigmaY = 0, 13 | }) : super(key: key); 14 | 15 | final Widget background; 16 | final Widget foreground; 17 | final AlignmentGeometry alignment; 18 | final double sigmaX; 19 | final double sigmaY; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Stack( 24 | alignment: alignment, 25 | children: [ 26 | background, 27 | ClipRect( 28 | child: BackdropFilter( 29 | filter: ui.ImageFilter.blur( 30 | sigmaX: sigmaX, 31 | sigmaY: sigmaY, 32 | ), 33 | child: foreground, 34 | ), 35 | ) 36 | ], 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/carousel.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Carousel extends StatefulWidget { 4 | final IndexedWidgetBuilder itemBuilder; 5 | final int itemCount; 6 | final double viewportFraction; 7 | 8 | const Carousel.builder({ 9 | Key key, 10 | @required this.itemCount, 11 | @required this.itemBuilder, 12 | this.viewportFraction = 1.0, 13 | }) : super(key: key); 14 | 15 | @override 16 | _CarouselState createState() => _CarouselState(); 17 | } 18 | 19 | class _CarouselState extends State { 20 | PageController _controller; 21 | 22 | @override 23 | initState() { 24 | super.initState(); 25 | _controller = PageController( 26 | initialPage: 0, 27 | viewportFraction: widget.viewportFraction, 28 | ); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return PageView.builder( 34 | controller: _controller, 35 | itemCount: widget.itemCount, 36 | itemBuilder: (context, index) { 37 | return AnimatedBuilder( 38 | animation: _controller, 39 | builder: (context, child) { 40 | double value = 1.0; 41 | if (_controller.position.haveDimensions) { 42 | value = _controller.page - index; 43 | value = (1 - (value.abs() * .5)).clamp(0.5, 1.0) as double; 44 | } else if (index == 1) { 45 | // 如果是首次加载, 由于haveDimensions为false, 会导致index为1处的item 46 | // 的value也会是1, 这里手动调整一下value. 47 | value = 0.5; 48 | } 49 | 50 | return Transform.scale( 51 | scale: Curves.easeOut.transform(value), 52 | child: child, 53 | ); 54 | }, 55 | child: widget.itemBuilder(context, index), 56 | ); 57 | }, 58 | ); 59 | } 60 | 61 | @override 62 | dispose() { 63 | _controller.dispose(); 64 | super.dispose(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/dot_indicator.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | class DotTabIndicator extends Decoration { 5 | final double offset; 6 | final double dotRadius; 7 | final Color color; 8 | 9 | const DotTabIndicator({ 10 | this.offset = 5 / 6, 11 | this.dotRadius = 2.0, 12 | this.color = Colors.black, 13 | }); 14 | 15 | @override 16 | Decoration lerpFrom(Decoration a, double t) { 17 | if (a is DotTabIndicator) { 18 | return DotTabIndicator(); 19 | } 20 | return super.lerpFrom(a, t); 21 | } 22 | 23 | @override 24 | Decoration lerpTo(Decoration b, double t) { 25 | if (b is DotTabIndicator) { 26 | return DotTabIndicator(); 27 | } 28 | return super.lerpTo(b, t); 29 | } 30 | 31 | @override 32 | _DotPainter createBoxPainter([VoidCallback onChanged]) { 33 | return _DotPainter(this, onChanged); 34 | } 35 | } 36 | 37 | class _DotPainter extends BoxPainter { 38 | _DotPainter(this.decoration, VoidCallback onChanged) 39 | : assert(decoration != null), 40 | super(onChanged); 41 | 42 | final DotTabIndicator decoration; 43 | 44 | @override 45 | void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { 46 | assert(configuration != null); 47 | assert(configuration.size != null); 48 | 49 | final _rawOffset = (offset & configuration.size).bottomCenter; 50 | final _offset = Offset(_rawOffset.dx, _rawOffset.dy * decoration.offset); 51 | canvas.drawCircle( 52 | _offset, 53 | decoration.dotRadius, 54 | Paint()..color = decoration.color, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/form_sheet.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 表单Widget 4 | /// 1. 自带提交按钮, 可以自定义样式 5 | /// 2. 可以区分滚动和不能滚动 6 | class FormSheet extends StatelessWidget { 7 | const FormSheet({ 8 | Key key, 9 | this.scrollable = false, 10 | this.submit, 11 | this.autovalidate = false, 12 | this.onWillPop, 13 | this.onChanged, 14 | @required this.children, 15 | }) : super(key: key); 16 | 17 | /// 是否可以滚动, 如果是, 那么内部会使用[ListView], 如果不是, 内部会使用[Column] 18 | final bool scrollable; 19 | 20 | /// 提交回调 21 | final Widget submit; 22 | 23 | /// 是否自动检查, 传递给内部[Form] 24 | final bool autovalidate; 25 | 26 | /// 传递给内部[Form] 27 | final WillPopCallback onWillPop; 28 | 29 | /// 传递给内部[Form] 30 | final VoidCallback onChanged; 31 | 32 | /// children列表 33 | final List children; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | final children = []; 38 | children.addAll(this.children); 39 | children.add(submit); 40 | return Form( 41 | autovalidate: autovalidate, 42 | onWillPop: onWillPop, 43 | onChanged: onChanged, 44 | child: scrollable 45 | ? ListView(children: children) 46 | : Column(children: children), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/gradient_button.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GradientButton extends StatelessWidget { 4 | final Widget child; 5 | final Gradient gradient; 6 | final double width; 7 | final double height; 8 | final Function onPressed; 9 | final BorderRadius borderRadius; 10 | final BoxShadow boxShadow; 11 | 12 | const GradientButton({ 13 | Key key, 14 | @required this.child, 15 | this.gradient, 16 | this.width = double.infinity, 17 | this.height = 48.0, 18 | this.onPressed, 19 | this.borderRadius, 20 | this.boxShadow = const BoxShadow( 21 | color: Colors.grey, 22 | offset: Offset(0.0, 0.0), 23 | blurRadius: 8, 24 | ), 25 | }) : super(key: key); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Container( 30 | width: width, 31 | height: height, 32 | decoration: BoxDecoration( 33 | borderRadius: borderRadius, 34 | gradient: gradient, 35 | boxShadow: [boxShadow], 36 | ), 37 | child: Material( 38 | color: Colors.transparent, 39 | child: InkWell( 40 | onTap: onPressed, 41 | child: Center( 42 | child: child, 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/notification_badge.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NotificationBadge extends StatelessWidget { 4 | const NotificationBadge({ 5 | Key key, 6 | this.radius = 12, 7 | this.color = const Color(0xffE05252), 8 | }) : super(key: key); 9 | 10 | final double radius; 11 | final Color color; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | width: 8, 17 | height: 8, 18 | decoration: BoxDecoration( 19 | shape: BoxShape.circle, 20 | color: color, 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/rating_bar.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef void RatingChangeCallback(double rating); 4 | 5 | class StarRating extends StatelessWidget { 6 | StarRating({ 7 | this.starCount = 5, 8 | this.rating = .0, 9 | this.onRatingChanged, 10 | this.color, 11 | this.borderColor, 12 | this.size, 13 | }); 14 | 15 | final int starCount; 16 | final double rating; 17 | final RatingChangeCallback onRatingChanged; 18 | final Color color; 19 | final Color borderColor; 20 | final double size; 21 | 22 | Widget buildStar(BuildContext context, int index) { 23 | Icon icon; 24 | if (index >= rating) { 25 | icon = Icon( 26 | Icons.star_border, 27 | color: borderColor ?? Theme.of(context).buttonColor, 28 | size: size ?? 25.0, 29 | ); 30 | } else if (index > rating - 1 && index < rating) { 31 | icon = Icon( 32 | Icons.star_half, 33 | color: color ?? Theme.of(context).primaryColor, 34 | size: size ?? 25.0, 35 | ); 36 | } else { 37 | icon = Icon( 38 | Icons.star, 39 | color: color ?? Theme.of(context).primaryColor, 40 | size: size ?? 25.0, 41 | ); 42 | } 43 | return InkResponse( 44 | onTap: 45 | onRatingChanged == null ? null : () => onRatingChanged(index + 1.0), 46 | child: icon, 47 | ); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return Center( 53 | child: Row( 54 | children: List.generate( 55 | starCount, 56 | (index) => buildStar(context, index), 57 | ), 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/snap_list.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SnapList extends StatelessWidget { 4 | SnapList({ 5 | Key key, 6 | this.initialPage = 0, 7 | this.aspectRatio = 1.0, 8 | this.viewportFraction = 1.0, 9 | this.padding = EdgeInsets.zero, 10 | @required IndexedWidgetBuilder itemBuilder, 11 | int itemCount, 12 | bool addAutomaticKeepAlive: true, 13 | bool addRepaintBoundaries: true, 14 | }) : childrenDelegate = SliverChildBuilderDelegate( 15 | itemBuilder, 16 | childCount: itemCount, 17 | addAutomaticKeepAlives: addAutomaticKeepAlive, 18 | addRepaintBoundaries: addRepaintBoundaries, 19 | ), 20 | super(key: key); 21 | 22 | final int initialPage; 23 | final double aspectRatio; 24 | final double viewportFraction; 25 | final EdgeInsetsGeometry padding; 26 | final SliverChildDelegate childrenDelegate; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return LayoutBuilder( 31 | builder: (BuildContext context, BoxConstraints constraints) { 32 | final itemWidth = 33 | (constraints.maxWidth - padding.horizontal) * viewportFraction; 34 | final itemHeight = itemWidth * aspectRatio; 35 | return Container( 36 | height: itemHeight, 37 | child: ListView.custom( 38 | scrollDirection: Axis.horizontal, 39 | controller: PageController( 40 | initialPage: initialPage, 41 | viewportFraction: viewportFraction, 42 | ), 43 | physics: const PageScrollPhysics(), 44 | padding: padding, 45 | itemExtent: itemWidth + (padding.horizontal * viewportFraction), 46 | childrenDelegate: childrenDelegate, 47 | ), 48 | ); 49 | }, 50 | ); 51 | } 52 | } 53 | 54 | class FeaturedItem extends StatelessWidget { 55 | final String title; 56 | final String price; 57 | final VoidCallback onTap; 58 | final Widget child; 59 | 60 | FeaturedItem({ 61 | @required this.title, 62 | @required this.price, 63 | this.onTap, 64 | this.child, 65 | }); 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Padding( 70 | padding: const EdgeInsets.all(8.0), 71 | child: Column( 72 | crossAxisAlignment: CrossAxisAlignment.stretch, 73 | children: [ 74 | Expanded( 75 | child: Material( 76 | elevation: 12.0, 77 | borderRadius: BorderRadius.circular(10.0), 78 | child: Stack( 79 | fit: StackFit.expand, 80 | children: [ 81 | child, 82 | Material( 83 | type: MaterialType.transparency, 84 | child: InkWell(onTap: onTap), 85 | ), 86 | ], 87 | ), 88 | ), 89 | ), 90 | Padding( 91 | padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), 92 | child: Text(title), 93 | ), 94 | Padding( 95 | padding: const EdgeInsets.only(bottom: 12.0), 96 | child: Text(price, style: TextStyle(fontWeight: FontWeight.bold)), 97 | ), 98 | ], 99 | ), 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/src/ui/widget/visual/special_affect/unknown_route.screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class UnknownScreen extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar(title: Text('Unknown')), 8 | body: Center(child: Text('Unknown')), 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/utils/bloc/bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:location_picker_fluttify/src/utils/bloc/bloc_io.dart'; 3 | 4 | @immutable 5 | abstract class BLoC { 6 | /// BLoC代表的语义 7 | final String semantics; 8 | 9 | BLoC([this.semantics]); 10 | 11 | /// 对应[State]的[reassemble]方法 12 | void reassemble() {} 13 | 14 | @mustCallSuper 15 | void close() { 16 | debugPrint('=============================================\n' 17 | '${semantics ?? runtimeType.toString()} closed ' 18 | '\n============================================='); 19 | } 20 | } 21 | 22 | /// 顶层[BLoC], 这个[BLoC]只有子[BLoC], 没有[Event], 并且子[BLoC]都是[GlobalBLoC] 23 | abstract class RootBLoC extends BLoC { 24 | RootBLoC([String semantics]) : super(semantics); 25 | 26 | @protected 27 | List get disposeBag => []; 28 | 29 | @override 30 | void close() { 31 | disposeBag?.forEach((bloc) => bloc.close()); 32 | 33 | super.close(); 34 | } 35 | } 36 | 37 | /// 局部[BLoC], 局部[BLoC]通常包在Screen的外面, 作用范围就是这个Screen内部 38 | abstract class LocalBLoC extends BLoC { 39 | LocalBLoC([String semantics]) : super(semantics); 40 | 41 | @protected 42 | List get disposeBag => []; 43 | 44 | @override 45 | void close() { 46 | disposeBag?.forEach((event) => event.dispose()); 47 | 48 | super.close(); 49 | } 50 | } 51 | 52 | /// 全局BLoC, 全局BLoC和局部BLoC目前没用什么功能上的区别, 只是做一下区分 53 | abstract class GlobalBLoC extends BLoC { 54 | GlobalBLoC([String semantics]) : super(semantics); 55 | 56 | @protected 57 | List get disposeBag => []; 58 | 59 | @override 60 | void close() { 61 | disposeBag?.forEach((event) => event.dispose()); 62 | 63 | super.close(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/utils/bloc/bloc_io.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:rxdart/rxdart.dart'; 5 | 6 | import '../empty_util.dart'; 7 | import '../log.dart'; 8 | 9 | typedef bool _Equal(T data1, T data2); 10 | typedef Future _Fetch(ARG_TYPE arg); 11 | 12 | /// 业务单元基类 13 | abstract class BaseIO { 14 | BaseIO({ 15 | /// 初始值, 传递给内部的[_subject] 16 | T seedValue, 17 | 18 | /// Event代表的语义 19 | String semantics, 20 | 21 | /// 是否同步发射数据, 传递给内部的[_subject] 22 | bool sync = true, 23 | 24 | /// 是否使用BehaviorSubject, 如果使用, 那么Event内部的[_subject]会保存最近一次的值 25 | /// 默认为false 26 | bool isBehavior = false, 27 | }) : _semantics = semantics, 28 | _seedValue = seedValue, 29 | latest = seedValue, 30 | _subject = isBehavior 31 | ? BehaviorSubject.seeded(seedValue, sync: sync) 32 | : PublishSubject(sync: sync) { 33 | _subject.listen((data) { 34 | latest = data; 35 | L.p('当前${semantics ??= data.runtimeType.toString()} latest: $latest' 36 | '\n+++++++++++++++++++++++++++END+++++++++++++++++++++++++++++'); 37 | }); 38 | } 39 | 40 | /// 最新的值 41 | T latest; 42 | 43 | /// 初始值 44 | @protected 45 | T _seedValue; 46 | 47 | /// 语义 48 | @protected 49 | String _semantics; 50 | 51 | /// 内部中转对象 52 | @protected 53 | Subject _subject; 54 | 55 | void addError(Object error, [StackTrace stackTrace]) { 56 | if (!_subject.isClosed) _subject.addError(error, stackTrace); 57 | } 58 | 59 | Stream map(S convert(T event)) { 60 | return _subject.map(convert); 61 | } 62 | 63 | Stream where(bool test(T event)) { 64 | return _subject.where(test); 65 | } 66 | 67 | /// 清理保存的值, 恢复成初始状态 68 | void clear() { 69 | L.p('-----------------------------BEGIN---------------------------------\n' 70 | '${_semantics ??= runtimeType.toString()}事件 cleared ' 71 | '\n------------------------------END----------------------------------'); 72 | if (!_subject.isClosed) _subject.add(_seedValue); 73 | } 74 | 75 | /// 关闭流 76 | void dispose() { 77 | L.p('=============================BEGIN===============================\n' 78 | '${_semantics ??= runtimeType.toString()}事件 disposed ' 79 | '\n==============================END================================'); 80 | if (!_subject.isClosed) _subject.close(); 81 | } 82 | 83 | /// 运行时概要 84 | String runtimeSummary() { 85 | return '$_semantics:\n\t\tseedValue: $_seedValue,\n\t\tlatest: $latest'; 86 | } 87 | 88 | @override 89 | String toString() { 90 | return 'Output{latest: $latest, seedValue: $_seedValue, semantics: $_semantics, subject: $_subject}'; 91 | } 92 | } 93 | 94 | /// BLoC内的静态值, 也就是供初始化时的值, 之前都是直接写成字段, 这里提供一个类, 保持与IO的一致性 95 | class Static { 96 | T _content; 97 | 98 | void set(T value) { 99 | assert(_content == null); 100 | if (_content != null) { 101 | throw ''; 102 | } 103 | _content = value; 104 | } 105 | 106 | T get() => _content; 107 | } 108 | 109 | /// 只输入数据的业务单元 110 | class Input extends BaseIO with InputMixin { 111 | Input({ 112 | T seedValue, 113 | String semantics, 114 | bool sync = true, 115 | bool isBehavior = false, 116 | bool acceptEmpty = true, 117 | bool isDistinct = false, 118 | _Equal test, 119 | }) : super( 120 | seedValue: seedValue, 121 | semantics: semantics, 122 | sync: sync, 123 | isBehavior: isBehavior, 124 | ) { 125 | _acceptEmpty = acceptEmpty; 126 | _isDistinct = isDistinct; 127 | _test = test; 128 | } 129 | } 130 | 131 | /// 只输出数据的业务单元 132 | class Output extends BaseIO with OutputMixin { 133 | Output({ 134 | T seedValue, 135 | String semantics, 136 | bool sync = true, 137 | bool isBehavior = false, 138 | @required _Fetch fetch, 139 | }) : super( 140 | seedValue: seedValue, 141 | semantics: semantics, 142 | sync: sync, 143 | isBehavior: isBehavior, 144 | ) { 145 | stream = _subject.stream; 146 | _fetch = fetch; 147 | } 148 | } 149 | 150 | /// 既可以输入又可以输出的事件 151 | class IO extends BaseIO with InputMixin, OutputMixin { 152 | IO({ 153 | T seedValue, 154 | String semantics, 155 | bool sync = true, 156 | bool isBehavior = false, 157 | bool acceptEmpty = true, 158 | bool isDistinct = false, 159 | _Equal test, 160 | _Fetch fetch, 161 | }) : super( 162 | seedValue: seedValue, 163 | semantics: semantics, 164 | sync: sync, 165 | isBehavior: isBehavior, 166 | ) { 167 | stream = _subject.stream; 168 | 169 | _acceptEmpty = acceptEmpty; 170 | _isDistinct = isDistinct; 171 | _test = test; 172 | _fetch = fetch; 173 | } 174 | } 175 | 176 | //region 衍生IO 177 | /// 内部数据类型是[List]的输入业务单元 178 | class ListInput extends Input> with ListMixin { 179 | ListInput({ 180 | List seedValue, 181 | String semantics, 182 | bool sync = true, 183 | bool isBehavior = false, 184 | bool acceptEmpty = true, 185 | bool isDistinct = false, 186 | _Equal test, 187 | }) : super( 188 | seedValue: seedValue, 189 | semantics: semantics, 190 | sync: sync, 191 | isBehavior: isBehavior, 192 | acceptEmpty: acceptEmpty, 193 | isDistinct: isDistinct, 194 | test: test, 195 | ); 196 | } 197 | 198 | /// 内部数据类型是[List]的输出业务单元 199 | class ListOutput extends Output, ARG_TYPE> with ListMixin { 200 | ListOutput({ 201 | List seedValue, 202 | String semantics, 203 | bool sync = true, 204 | bool isBehavior = false, 205 | @required _Fetch, ARG_TYPE> fetch, 206 | }) : super( 207 | seedValue: seedValue, 208 | semantics: semantics, 209 | sync: sync, 210 | isBehavior: isBehavior, 211 | fetch: fetch, 212 | ); 213 | } 214 | 215 | /// 内部数据类型是[List]的输入输出业务单元 216 | class ListIO extends IO> with ListMixin { 217 | ListIO({ 218 | List seedValue, 219 | String semantics, 220 | bool sync = true, 221 | bool isBehavior = false, 222 | bool acceptEmpty = true, 223 | bool isDistinct = false, 224 | _Equal test, 225 | _Fetch, dynamic> fetch, 226 | }) : super( 227 | seedValue: seedValue, 228 | semantics: semantics, 229 | sync: sync, 230 | isBehavior: isBehavior, 231 | acceptEmpty: acceptEmpty, 232 | isDistinct: isDistinct, 233 | test: test, 234 | fetch: fetch, 235 | ); 236 | } 237 | 238 | /// 只接收bool类型数据的IO 239 | class BoolIO extends IO with BoolMixin { 240 | BoolIO({ 241 | bool seedValue, 242 | String semantics, 243 | bool sync = true, 244 | bool isBehavior = false, 245 | bool acceptEmpty = true, 246 | bool isDistinct = false, 247 | _Equal test, 248 | _Fetch fetch, 249 | }) : super( 250 | seedValue: seedValue, 251 | semantics: semantics, 252 | sync: sync, 253 | isBehavior: isBehavior, 254 | acceptEmpty: acceptEmpty, 255 | isDistinct: isDistinct, 256 | test: test, 257 | fetch: fetch, 258 | ); 259 | } 260 | 261 | /// 只接收bool类型数据的Output 262 | class BoolOutput extends Output with BoolMixin { 263 | BoolOutput({ 264 | bool seedValue, 265 | String semantics, 266 | bool sync = true, 267 | bool isBehavior = false, 268 | bool acceptEmpty = true, 269 | bool isDistinct = false, 270 | _Equal test, 271 | _Fetch fetch, 272 | }) : super( 273 | seedValue: seedValue, 274 | semantics: semantics, 275 | sync: sync, 276 | isBehavior: isBehavior, 277 | fetch: fetch, 278 | ); 279 | } 280 | //endregion 281 | 282 | /// 输入单元特有的成员 283 | mixin InputMixin on BaseIO { 284 | bool _acceptEmpty; 285 | bool _isDistinct; 286 | _Equal _test; 287 | 288 | T add(T data) { 289 | L.p('+++++++++++++++++++++++++++BEGIN+++++++++++++++++++++++++++++\n' 290 | 'IO接收到**${_semantics ??= data.runtimeType.toString()}**数据: $data'); 291 | 292 | if (isEmpty(data) && !_acceptEmpty) { 293 | L.p('转发被拒绝! 原因: 需要非Empty值, 但是接收到Empty值' 294 | '\n+++++++++++++++++++++++++++END+++++++++++++++++++++++++++++++'); 295 | return data; 296 | } 297 | 298 | // 如果需要distinct的话, 就判断是否相同; 如果不需要distinct, 直接发射数据 299 | if (_isDistinct) { 300 | // 如果是不一样的数据, 才发射新的通知,防止TabBar的addListener那种 301 | // 不停地发送通知(但是值又是一样)的情况 302 | if (_test != null) { 303 | if (!_test(latest, data)) { 304 | L.p('IO转发出**${_semantics ??= data.runtimeType.toString()}**数据: $data'); 305 | if (!_subject.isClosed) _subject.add(data); 306 | } else { 307 | L.p('转发被拒绝! 原因: 需要唯一, 但是没有通过唯一性测试' 308 | '\n+++++++++++++++++++++++++++END+++++++++++++++++++++++++++++'); 309 | } 310 | } else { 311 | if (data != latest) { 312 | L.p('IO转发出**${_semantics ??= data.runtimeType.toString()}**数据: $data'); 313 | if (!_subject.isClosed) _subject.add(data); 314 | } else { 315 | L.p('转发被拒绝! 原因: 需要唯一, 但是新数据与最新值相同' 316 | '\n+++++++++++++++++++++++++++END+++++++++++++++++++++++++++++'); 317 | } 318 | } 319 | } else { 320 | L.p('IO转发出**${_semantics ??= data.runtimeType.toString()}**数据: $data'); 321 | if (!_subject.isClosed) _subject.add(data); 322 | } 323 | 324 | return data; 325 | } 326 | 327 | T addIfAbsent(T data) { 328 | // 如果最新值是_seedValue或者是空, 那么才add新数据, 换句话说, 就是如果event已经被add过 329 | // 了的话那就不add了, 用于第一次add 330 | if (_seedValue == latest || isEmpty(latest)) { 331 | add(data); 332 | } 333 | return data; 334 | } 335 | 336 | Future addStream(Stream source, {bool cancelOnError: true}) { 337 | return _subject.addStream(source, cancelOnError: cancelOnError); 338 | } 339 | } 340 | 341 | /// 输出单元特有的成员 342 | mixin OutputMixin on BaseIO { 343 | /// 输出Future 344 | Future get future => stream.first; 345 | 346 | /// 输出Stream 347 | Stream stream; 348 | 349 | StreamSubscription listen( 350 | ValueChanged listener, { 351 | Function onError, 352 | VoidCallback onDone, 353 | bool cancelOnError, 354 | }) { 355 | return stream.listen( 356 | listener, 357 | onError: onError, 358 | onDone: onDone, 359 | cancelOnError: cancelOnError, 360 | ); 361 | } 362 | 363 | /// 输出Stream 364 | _Fetch _fetch; 365 | 366 | /// 使用内部的trigger获取数据 367 | Future update([ARG_TYPE arg]) { 368 | return _fetch(arg) 369 | ..then((data) { 370 | if (!_subject.isClosed) _subject.add(data); 371 | }) 372 | ..catchError((error) { 373 | if (!_subject.isClosed) _subject.addError(error); 374 | }); 375 | } 376 | } 377 | 378 | /// 内部数据是[List]特有的成员 379 | mixin ListMixin on BaseIO> { 380 | /// 按条件过滤, 并发射过滤后的数据 381 | List filterItem(bool test(T element)) { 382 | final filtered = latest.where(test).toList(); 383 | if (!_subject.isClosed) _subject.add(filtered); 384 | return filtered; 385 | } 386 | 387 | /// 追加, 并发射 388 | T append(T element, {bool fromHead = false}) { 389 | if (!_subject.isClosed) { 390 | if (fromHead) { 391 | _subject.add(latest..insert(0, element)); 392 | } else { 393 | _subject.add(latest..add(element)); 394 | } 395 | } 396 | return element; 397 | } 398 | 399 | /// 追加一个list, 并发射 400 | List appendAll(List elements, {bool fromHead = false}) { 401 | if (!_subject.isClosed) { 402 | if (fromHead) { 403 | _subject.add(latest..insertAll(0, elements)); 404 | } else { 405 | _subject.add(latest..addAll(elements)); 406 | } 407 | } 408 | return elements; 409 | } 410 | 411 | /// 对list的item做变换之后重新组成list 412 | Stream> flatMap(S mapper(T value)) { 413 | return _subject.map((list) => list.map(mapper).toList()); 414 | } 415 | 416 | /// 替换指定index的元素, 并发射 417 | T replace(int index, T element) { 418 | if (!_subject.isClosed) { 419 | _subject.add(latest..replaceRange(index, index + 1, [element])); 420 | } 421 | return element; 422 | } 423 | 424 | /// 替换最后一个的元素, 并发射 425 | T replaceLast(T element) { 426 | if (!_subject.isClosed) { 427 | _subject.add(latest 428 | ..replaceRange( 429 | latest.length - 1, 430 | latest.length, 431 | [element], 432 | )); 433 | } 434 | return element; 435 | } 436 | 437 | /// 替换第一个的元素, 并发射 438 | T replaceFirst(T element) { 439 | if (!_subject.isClosed) { 440 | _subject.add(latest..replaceRange(0, 1, [element])); 441 | } 442 | return element; 443 | } 444 | 445 | /// 删除最后一个的元素, 并发射 446 | T removeLast() { 447 | final lastElement = latest.last; 448 | if (!_subject.isClosed) { 449 | _subject.add(latest..removeLast()); 450 | } 451 | return lastElement; 452 | } 453 | 454 | /// 删除一个的元素, 并发射 455 | T remove(T element) { 456 | if (!_subject.isClosed) { 457 | _subject.add(latest..remove(element)); 458 | } 459 | return element; 460 | } 461 | 462 | /// 删除第一个的元素, 并发射 463 | T removeFirst() { 464 | final firstElement = latest.first; 465 | if (!_subject.isClosed) { 466 | _subject.add(latest..removeAt(0)); 467 | } 468 | return firstElement; 469 | } 470 | 471 | /// 删除指定索引的元素, 并发射 472 | T removeAt(int index) { 473 | final element = latest.elementAt(index); 474 | if (!_subject.isClosed) { 475 | _subject.add(latest..removeAt(index)); 476 | } 477 | return element; 478 | } 479 | } 480 | 481 | mixin BoolMixin on BaseIO { 482 | bool toggle() { 483 | final toggled = !latest; 484 | if (!_subject.isClosed) { 485 | _subject.add(toggled); 486 | } 487 | return toggled; 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /lib/src/utils/bloc/bloc_provider.widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:location_picker_fluttify/src/utils/bloc/bloc.dart'; 3 | 4 | import '../empty_util.dart'; 5 | 6 | typedef void _Init(T bloc); 7 | 8 | class BLoCProvider extends StatefulWidget { 9 | BLoCProvider({ 10 | Key key, 11 | @required this.child, 12 | @required this.bloc, 13 | this.init, 14 | this.onDispose, 15 | }) : super(key: key); 16 | 17 | final T bloc; 18 | final _Init init; 19 | final Widget child; 20 | final VoidCallback onDispose; 21 | 22 | @override 23 | _BLoCProviderState createState() => _BLoCProviderState(); 24 | 25 | static T of(BuildContext context) { 26 | final type = _typeOf<_BLoCProviderInherited>(); 27 | _BLoCProviderInherited provider = 28 | context.ancestorInheritedElementForWidgetOfExactType(type)?.widget; 29 | return provider?.bloc; 30 | } 31 | 32 | static Type _typeOf() => T; 33 | } 34 | 35 | class _BLoCProviderState extends State> { 36 | @override 37 | void initState() { 38 | super.initState(); 39 | 40 | if (isNotEmpty(widget.init)) widget.init(widget.bloc); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return _BLoCProviderInherited( 46 | child: widget.child, 47 | bloc: widget.bloc, 48 | ); 49 | } 50 | 51 | @override 52 | void reassemble() { 53 | widget.bloc.reassemble(); 54 | super.reassemble(); 55 | } 56 | 57 | @override 58 | void dispose() { 59 | widget.bloc.close(); 60 | 61 | if (widget.onDispose != null) { 62 | widget.onDispose(); 63 | } 64 | super.dispose(); 65 | } 66 | } 67 | 68 | class _BLoCProviderInherited extends InheritedWidget { 69 | _BLoCProviderInherited({ 70 | Key key, 71 | @required Widget child, 72 | @required this.bloc, 73 | }) : super(key: key, child: child); 74 | 75 | final T bloc; 76 | 77 | @override 78 | bool updateShouldNotify(_BLoCProviderInherited oldWidget) => false; 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/utils/empty_util.dart: -------------------------------------------------------------------------------- 1 | bool isEmpty(Object object) { 2 | if (object == null) { 3 | return true; 4 | } 5 | 6 | if (object is String) { 7 | return object.isEmpty; 8 | } 9 | 10 | if (object is Iterable) { 11 | return object.isEmpty; 12 | } 13 | 14 | if (object is Map) { 15 | return object.isEmpty; 16 | } 17 | 18 | return false; 19 | } 20 | 21 | bool isNotEmpty(Object object) { 22 | return !isEmpty(object); 23 | } 24 | 25 | bool isAllEmpty(List list) { 26 | if (isEmpty(list)) { 27 | return true; 28 | } else { 29 | return list.every(isEmpty); 30 | } 31 | } 32 | 33 | bool isAllNotEmpty(List list) { 34 | if (isEmpty(list)) { 35 | return false; 36 | } 37 | return !list.any(isEmpty); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/utils/log.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// If you're in release mode, const bool.fromEnvironment("dart.vm.product") will be true. 4 | /// If you're in debug mode, assert(() { ...; return true; }); will execute .... 5 | /// If you're in profile mode, neither of the above will happen. 6 | class L { 7 | /// debug模式打印 8 | static void d(Object content) async { 9 | assert(() { 10 | debugPrint(content.toString()); 11 | return true; 12 | }()); 13 | } 14 | 15 | /// profile模式打印 16 | static void p(Object content) async { 17 | if (!bool.fromEnvironment('dart.vm.product')) { 18 | debugPrint(content.toString()); 19 | } 20 | } 21 | 22 | /// release模式打印 23 | static void r(Object content) async { 24 | debugPrint(content.toString()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/utils/objects.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | final gDefaultPointer = Image.asset( 4 | 'images/wechat_locate.png', 5 | package: 'location_picker_fluttify', 6 | ); 7 | 8 | final gDefaultShowMyLocation = Image.asset( 9 | 'images/wechat_locator.png', 10 | package: 'location_picker_fluttify', 11 | ); 12 | -------------------------------------------------------------------------------- /lib/src/utils/permissions.dart: -------------------------------------------------------------------------------- 1 | import 'package:permission_handler/permission_handler.dart'; 2 | 3 | Future requestPermission() async { 4 | final permissions = 5 | await PermissionHandler().requestPermissions([PermissionGroup.location]); 6 | 7 | if (permissions[PermissionGroup.location] == PermissionStatus.granted) { 8 | return true; 9 | } else { 10 | return false; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/utils/value.dart: -------------------------------------------------------------------------------- 1 | class Value { 2 | T value; 3 | 4 | Value(this.value); 5 | } 6 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | after_layout: 5 | dependency: "direct main" 6 | description: 7 | name: after_layout 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "1.0.7+2" 11 | amap_core_fluttify: 12 | dependency: transitive 13 | description: 14 | name: amap_core_fluttify 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "0.3.0+e26c0fa" 18 | amap_map_fluttify: 19 | dependency: "direct main" 20 | description: 21 | name: amap_map_fluttify 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "0.14.2+e26c0fa" 25 | amap_search_fluttify: 26 | dependency: "direct main" 27 | description: 28 | name: amap_search_fluttify 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "0.5.0+acdcd89" 32 | archive: 33 | dependency: transitive 34 | description: 35 | name: archive 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.0.11" 39 | args: 40 | dependency: transitive 41 | description: 42 | name: args 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.5.2" 46 | async: 47 | dependency: transitive 48 | description: 49 | name: async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.4.0" 53 | boolean_selector: 54 | dependency: transitive 55 | description: 56 | name: boolean_selector 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.0.5" 60 | charcode: 61 | dependency: transitive 62 | description: 63 | name: charcode 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.1.2" 67 | collection: 68 | dependency: transitive 69 | description: 70 | name: collection 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.14.11" 74 | convert: 75 | dependency: transitive 76 | description: 77 | name: convert 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "2.1.1" 81 | crypto: 82 | dependency: transitive 83 | description: 84 | name: crypto 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "2.1.3" 88 | flutter: 89 | dependency: "direct main" 90 | description: flutter 91 | source: sdk 92 | version: "0.0.0" 93 | flutter_test: 94 | dependency: "direct dev" 95 | description: flutter 96 | source: sdk 97 | version: "0.0.0" 98 | flutter_web_plugins: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.0" 103 | foundation_fluttify: 104 | dependency: transitive 105 | description: 106 | name: foundation_fluttify 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "0.3.0" 110 | image: 111 | dependency: transitive 112 | description: 113 | name: image 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "2.1.4" 117 | matcher: 118 | dependency: transitive 119 | description: 120 | name: matcher 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "0.12.6" 124 | meta: 125 | dependency: transitive 126 | description: 127 | name: meta 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.8" 131 | path: 132 | dependency: transitive 133 | description: 134 | name: path 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.6.4" 138 | pedantic: 139 | dependency: transitive 140 | description: 141 | name: pedantic 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.8.0+1" 145 | permission_handler: 146 | dependency: "direct main" 147 | description: 148 | name: permission_handler 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "4.0.0" 152 | petitparser: 153 | dependency: transitive 154 | description: 155 | name: petitparser 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "2.4.0" 159 | plugin_platform_interface: 160 | dependency: transitive 161 | description: 162 | name: plugin_platform_interface 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "1.0.1" 166 | quiver: 167 | dependency: transitive 168 | description: 169 | name: quiver 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "2.0.5" 173 | rxdart: 174 | dependency: "direct main" 175 | description: 176 | name: rxdart 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "0.23.1" 180 | sky_engine: 181 | dependency: transitive 182 | description: flutter 183 | source: sdk 184 | version: "0.0.99" 185 | source_span: 186 | dependency: transitive 187 | description: 188 | name: source_span 189 | url: "https://pub.dartlang.org" 190 | source: hosted 191 | version: "1.5.5" 192 | stack_trace: 193 | dependency: transitive 194 | description: 195 | name: stack_trace 196 | url: "https://pub.dartlang.org" 197 | source: hosted 198 | version: "1.9.3" 199 | stream_channel: 200 | dependency: transitive 201 | description: 202 | name: stream_channel 203 | url: "https://pub.dartlang.org" 204 | source: hosted 205 | version: "2.0.0" 206 | string_scanner: 207 | dependency: transitive 208 | description: 209 | name: string_scanner 210 | url: "https://pub.dartlang.org" 211 | source: hosted 212 | version: "1.0.5" 213 | term_glyph: 214 | dependency: transitive 215 | description: 216 | name: term_glyph 217 | url: "https://pub.dartlang.org" 218 | source: hosted 219 | version: "1.1.0" 220 | test_api: 221 | dependency: transitive 222 | description: 223 | name: test_api 224 | url: "https://pub.dartlang.org" 225 | source: hosted 226 | version: "0.2.11" 227 | typed_data: 228 | dependency: transitive 229 | description: 230 | name: typed_data 231 | url: "https://pub.dartlang.org" 232 | source: hosted 233 | version: "1.1.6" 234 | url_launcher: 235 | dependency: transitive 236 | description: 237 | name: url_launcher 238 | url: "https://pub.dartlang.org" 239 | source: hosted 240 | version: "5.4.1" 241 | url_launcher_macos: 242 | dependency: transitive 243 | description: 244 | name: url_launcher_macos 245 | url: "https://pub.dartlang.org" 246 | source: hosted 247 | version: "0.0.1+2" 248 | url_launcher_platform_interface: 249 | dependency: transitive 250 | description: 251 | name: url_launcher_platform_interface 252 | url: "https://pub.dartlang.org" 253 | source: hosted 254 | version: "1.0.4" 255 | url_launcher_web: 256 | dependency: transitive 257 | description: 258 | name: url_launcher_web 259 | url: "https://pub.dartlang.org" 260 | source: hosted 261 | version: "0.1.0+2" 262 | vector_math: 263 | dependency: transitive 264 | description: 265 | name: vector_math 266 | url: "https://pub.dartlang.org" 267 | source: hosted 268 | version: "2.0.8" 269 | xml: 270 | dependency: transitive 271 | description: 272 | name: xml 273 | url: "https://pub.dartlang.org" 274 | source: hosted 275 | version: "3.5.0" 276 | sdks: 277 | dart: ">=2.6.0 <3.0.0" 278 | flutter: ">=1.12.13 <2.0.0" 279 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: location_picker_fluttify 2 | description: A Location Picker. Based on `amap_x_fluttify` Plugins. 3 | version: 0.0.3 4 | author: yohom <382146139@qq.com> 5 | homepage: https://github.com/fluttify-project/location_picker_fluttify 6 | 7 | environment: 8 | sdk: ">=2.6.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | amap_map_fluttify: ^0.14.2+e26c0fa 14 | amap_search_fluttify: ^0.5.0+acdcd89 15 | permission_handler: ^4.0.0 16 | rxdart: ^0.23.1 17 | after_layout: ^1.0.7+2 18 | 19 | dev_dependencies: 20 | flutter_test: 21 | sdk: flutter 22 | 23 | flutter: 24 | uses-material-design: true 25 | plugin: 26 | androidPackage: me.yohom.location_picker_fluttify 27 | pluginClass: LocationPickerFluttifyPlugin 28 | assets: 29 | - images/ -------------------------------------------------------------------------------- /test/location_picker_fluttify_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | --------------------------------------------------------------------------------