├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── need-help.md └── workflows │ └── pr.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── example │ │ │ │ └── MainActivity.java │ │ │ └── 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 │ ├── 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 │ │ └── xcshareddata │ │ │ └── WorkspaceSettings.xcsettings │ └── 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 │ │ └── zh-Hans.lproj │ │ ├── LaunchScreen.strings │ │ └── Main.strings ├── lib │ ├── asset_image.dart │ ├── icon_text_button.dart │ ├── main.dart │ ├── picked_example.dart │ └── preview.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── lib ├── photo.dart └── src │ ├── delegate │ ├── badge_delegate.dart │ ├── checkbox_builder_delegate.dart │ ├── loading_delegate.dart │ ├── sort_asset_delegate.dart │ └── sort_delegate.dart │ ├── engine │ ├── lru_cache.dart │ └── throttle.dart │ ├── entity │ └── options.dart │ ├── error │ └── permission_error.dart │ ├── provider │ ├── asset_provider.dart │ ├── config_provider.dart │ ├── gallery_list_provider.dart │ ├── i18n_provider.dart │ └── selected_provider.dart │ └── ui │ ├── dialog │ ├── change_gallery_dialog.dart │ └── not_permission_dialog.dart │ ├── page │ ├── main │ │ ├── bottom_widget.dart │ │ └── image_item.dart │ ├── photo_main_page.dart │ └── photo_preview_page.dart │ ├── photo_app.dart │ └── widget │ ├── check_box_copy.dart │ └── check_tile_copy.dart ├── pubspec.lock ├── pubspec.yaml └── test └── photo_test.dart /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: need confirm 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | How to reproduce 13 | 14 | ## ScreenShot 15 | 16 | if have, post to there 17 | 18 | 19 | ## Log 20 | 21 |
22 | 23 | ```log 24 | /// paste your log to there 25 | ``` 26 | 27 |
28 | 29 | ## Plugin version 30 | 31 | such as 0.4.7 32 | 33 | ## Flutter version 34 | 35 | flutter doctor -v 36 | 37 |
38 | 39 | ```bash 40 | # post info to there 41 | ``` 42 | 43 |
44 | 45 | ## Project native version 46 | 47 | ### Android 48 | 49 | such as: 50 | 51 | compile version: 29 52 | target version: 29 53 | 54 | ## Your phone info 55 | 56 | platform: such as Android 57 | os version: such as Android 10.0 58 | model: such as Huawei 59 | other info: such as huawei EMUI 6.1 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: need confirm 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/need-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Need help 3 | about: Describe this issue template's purpose here. 4 | title: "[Need Help] " 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | Describe your problem as clearly as possible. 13 | 14 | ## More info 15 | 16 | Additional information such as prototypes or screenshots can help developers understand what you mean. 17 | 18 | ## Gist code 19 | 20 | ```dart 21 | ``` 22 | 23 | ## Plugin version 24 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR_CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Test on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: actions/setup-java@v1 15 | with: 16 | java-version: '12.x' 17 | - uses: subosito/flutter-action@v1 18 | with: 19 | flutter-version: '1.9.1+hotfix.6' 20 | - run: flutter pub get 21 | - run: flutter analyze lib example/lib 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | ios/.generated/ 9 | ios/Flutter/Generated.xcconfig 10 | ios/Runner/GeneratedPluginRegistrant.* 11 | 12 | .idea/ 13 | *.iml 14 | example/.flutter-plugins-dependencies 15 | fvm 16 | .vscode/settings.json 17 | example/ios/Flutter/flutter_export_environment.sh 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "example/lib/main.dart" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.5.0 4 | 5 | Rollback to 0.5.1 photo_manager 6 | 7 | ## 0.5.0-dev.3 8 | 9 | Fix: 10 | 11 | video type error. 12 | 13 | Feature: 14 | 15 | Clear memory cache. 16 | 17 | ## 0.5.0-dev.2 18 | 19 | - Rollback photo_manager 20 | 21 | Fix: 22 | 23 | - no selected text 24 | 25 | ## 0.4.8 26 | 27 | - Rollback photo_manager 28 | 29 | Fix: 30 | 31 | - pickedAssetList error 32 | 33 | ## 0.4.7 34 | 35 | - Rollback photo_manager. 36 | - Add `pickedAssetList` for `pickAsset`. 37 | 38 | ## 0.4.5+1 39 | 40 | Fix: 41 | 42 | - `photoPathList` of `PhotoPicker.pickAsset`. 43 | 44 | ## 0.4.5 45 | 46 | Rollback `photo_manager` to `0.4.5`. 47 | 48 | ## 0.4.4 49 | 50 | Rollback `photo_manager` to `0.4.4`. 51 | 52 | To fix some error. 53 | 54 | ## 0.4.3+1 55 | 56 | Fix a error for build. 57 | 58 | ## 0.4.3 59 | 60 | Rollback `photo_manager` to `0.4.3`. 61 | 62 | ## 0.4.2+1 63 | 64 | Rollback `photo_manager` to `0.4.2`. 65 | 66 | ## 0.4.2 67 | 68 | ## 0.4.1+1 69 | 70 | Rollback `photo_manager` to `0.4.1`. 71 | 72 | ## 0.4.1 73 | 74 | **The version will build fail.** 75 | 76 | Rollback `photo_manager` to `0.4.1`. 77 | 78 | ## 0.4.0 79 | 80 | **The version will build fail.** 81 | 82 | Rollback `photo_manager` to `0.4.0`. 83 | 84 | - support androidQ. 85 | 86 | ## 0.3.4+1 87 | 88 | fix: 89 | 90 | - title and left icon color. 91 | - checkColor in preview can be edit. 92 | 93 | ## 0.3.4 94 | 95 | Rollback `photo_manager` to `0.3.4` 96 | 97 | ## 0.3.3 98 | 99 | Rollback `photo_manager` to `0.3.3` 100 | 101 | ## 0.3.2 102 | 103 | Rollback `photo_manager` to `0.3.2` 104 | 105 | ## 0.3.1 106 | 107 | `photo_manager` to `0.3.1` 108 | 109 | If the selected image is deleted by other applications, the selected status in the selector will be updated correctly. 110 | 111 | ## 0.3.0 add params photoList 112 | 113 | **Breaking change**. Migrate from the deprecated original Android Support Library to AndroidX. This shouldn't result in any functional changes, but it requires any Android apps using this plugin to also migrate if they're using the original support library. 114 | 115 | add: 116 | 117 | - When the album changes, it refreshes in real time. 118 | 119 | fix: 120 | 121 | - Video duration badge time problem. 122 | - Images sort by create time. 123 | 124 | ## 0.2.0 125 | 126 | **break change** 127 | support pick only image or video 128 | 129 | ## 0.1.11 130 | 131 | fix pubspec version 132 | 133 | ## 0.1.10 fix bug 134 | 135 | fix a error widget bug. 136 | 137 | ## 0.1.9 138 | 139 | fix all assets i18n provider 140 | 141 | ## 0.1.8 142 | 143 | Fixed crash bug when the number of photos or videos was zero. 144 | 145 | ## 0.1.7 146 | 147 | add a badge delegate for asset 148 | 149 | ## 0.1.6 Rollback photo_manager version 150 | 151 | sort asset by date 152 | 153 | ## 0.1.5 Rollback photo_manager version 154 | 155 | ## 0.1.4 fix thumb is null bug 156 | 157 | fix thumb bug. 158 | 159 | ## 0.1.3 support ios icloud asset 160 | 161 | ## 0.1.2 fix bug 162 | 163 | fix all path hasVideo property bug 164 | 165 | ## 0.1.1 fix bug and add params 166 | 167 | add loadingDelegate 168 | 169 | ## 0.1.0 support video 170 | 171 | API incompatibility 172 | 173 | ImageXXX rename AssetXXX 174 | 175 | ## 0.0.8 fix bug 176 | 177 | DefaultCheckBoxBuilderDelegate params checkColor not valid bug 178 | 179 | ## 0.0.7 fix bug 180 | 181 | fix dividerColor not valid bug 182 | 183 | ## 0.0.6 add checkbox delegate 184 | 185 | users can use CheckBoxDelegate to custom preview right bottom widget 186 | 187 | ## 0.0.5 add a params 188 | 189 | add the sort delegate to help user sort gallery 190 | 191 | Optimized LruCache 192 | 193 | add a loading refresh indicator in the gallery 194 | 195 | ## 0.0.4 fix #1 196 | 197 | fix request other permission will crash bug 198 | 199 | depo photo_manager 0.0.3 200 | 201 | ## 0.0.3 add the thumb size to option 202 | 203 | add a params for pick image , thumb size 204 | 205 | ## 0.0.2 fix bug 206 | 207 | preview sure button bug 208 | preview bottom safeArea 209 | 210 | ## 0.0.1 first version 211 | 212 | image picker 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2018] [caijinglong] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # archived 2 | The package is archived. 3 | Please use [flutter_wechat_assets_picker](https://github.com/fluttercandies/flutter_wechat_assets_picker). 4 | 5 | # photo 6 | 7 | [![pub package](https://img.shields.io/pub/v/photo.svg)](https://pub.dartlang.org/packages/photo) [![pub package](https://img.shields.io/pub/v/photo?include_prereleases)](https://pub.dartlang.org/packages/photo) 8 | [![GitHub](https://img.shields.io/github/license/CaiJingLong/flutter_photo.svg?style=flat-square)](https://github.com/CaiJingLong/flutter_photo) 9 | [![GitHub stars](https://img.shields.io/github/stars/CaiJingLong/flutter_photo.svg?style=social&label=Stars)](https://github.com/CaiJingLong/flutter_photo) 10 | 11 | image picker, multi picker 12 | 13 | support ios icloud 14 | 15 | support video 16 | 17 | use flutter as ui 18 | 19 | if you want to build custom ui, you just need api to make custom ui. to use [photo_manager](https://github.com/CaiJingLong/flutter_photo_manager) or fork the library to custom ui. 20 | 21 | ## Screenshot 22 | 23 | ![image](https://github.com/CaiJingLong/some_asset/blob/master/image_picker1.gif) 24 | 25 | ## install 26 | 27 | latest version : [![pub package](https://img.shields.io/pub/v/photo.svg)](https://pub.dartlang.org/packages/photo) [![pub package](https://img.shields.io/pub/v/photo?include_prereleases)](https://pub.dartlang.org/packages/photo) 28 | 29 | ```yaml 30 | dependencies: 31 | photo: $latest_version 32 | ``` 33 | 34 | ## Import 35 | 36 | ```dart 37 | import 'package:photo/photo.dart'; 38 | import 'package:photo_manager/photo_manager.dart'; 39 | ``` 40 | 41 | ## Usage 42 | 43 | ### Simple use 44 | 45 | ```dart 46 | void pickAssets() async { 47 | List assetList = await PhotoPicker.pickAsset(context: context); 48 | /// Use assetList to do something. 49 | } 50 | ``` 51 | 52 | ### More option 53 | 54 | The context is required, other params is optional. 55 | 56 | ```dart 57 | void pickAsset() async { 58 | List imgList = await PhotoPicker.pickAsset( 59 | context: context, 60 | // BuildContext requied 61 | 62 | /// The following are optional parameters. 63 | themeColor: Colors.green, 64 | // the title color and bottom color 65 | padding: 1.0, 66 | // item padding 67 | dividerColor: Colors.grey, 68 | // divider color 69 | disableColor: Colors.grey.shade300, 70 | // the check box disable color 71 | itemRadio: 0.88, 72 | // the content item radio 73 | maxSelected: 8, 74 | // max picker image count 75 | provider: I18nProvider.chinese, 76 | // i18n provider ,default is chinese. , you can custom I18nProvider or use ENProvider() 77 | rowCount: 5, 78 | // item row count 79 | textColor: Colors.white, 80 | // text color 81 | thumbSize: 150, 82 | // preview thumb size , default is 64 83 | sortDelegate: SortDelegate.common, 84 | // default is common ,or you make custom delegate to sort your gallery 85 | checkBoxBuilderDelegate: DefaultCheckBoxBuilderDelegate( 86 | activeColor: Colors.white, 87 | unselectedColor: Colors.white, 88 | checkColor: Colors.blue, 89 | ), // default is DefaultCheckBoxBuilderDelegate ,or you make custom delegate to create checkbox 90 | 91 | loadingDelegate: 92 | this, // if you want to build custom loading widget,extends LoadingDelegate [see example/lib/main.dart] 93 | 94 | badgeDelegate: const DefaultBadgeDelegate(), /// or custom class extends [BadgeDelegate] 95 | 96 | pickType: type, // all/image/video 97 | 98 | List photoPathList, /// when [photoPathList] is not null , [pickType] invalid . 99 | ); 100 | ``` 101 | 102 | ### About photoPathList params 103 | 104 | You can use [photo_manager] package to get `List` and handle or cache. 105 | 106 | This parameter is then passed into the `pickAsset` method, where the incoming photoList is rendered instead of the data in the album. 107 | 108 | ## Whole example 109 | 110 | You can see [github](https://github.com/caijinglong/flutter_photo/blob/master/example/) [main.dart](https://github.com/caijinglong/flutter_photo/blob/master/example/lib/main.dart) 111 | 112 | ## About android 113 | 114 | ### Migrate to androidX 115 | 116 | See the [gitbook](https://caijinglong.gitbooks.io/migrate-flutter-to-androidx/content/) 117 | 118 | ### Glide 119 | 120 | Android native use glide to create image thumb bytes, version is 4.8.0. 121 | 122 | If your other android library use the library, and version is not same, then you need edit your android project's build.gradle. 123 | 124 | ```groovy 125 | rootProject.allprojects { 126 | 127 | subprojects { 128 | project.configurations.all { 129 | resolutionStrategy.eachDependency { details -> 130 | if (details.requested.group == 'com.github.bumptech.glide' 131 | && details.requested.name.contains('glide')) { 132 | details.useVersion "4.8.0" 133 | } 134 | } 135 | } 136 | } 137 | 138 | } 139 | ``` 140 | 141 | if you use the proguard 142 | 143 | see the [github](https://github.com/bumptech/glide#proguard) 144 | 145 | ## About ios 146 | 147 | Because the album is a privacy privilege, you need user permission to access it. You must to modify the `Info.plist` file in Runner project. 148 | 149 | like next 150 | 151 | ```plist 152 | NSPhotoLibraryUsageDescription 153 | App need your agree, can visit your album 154 | ``` 155 | 156 | xcode like image 157 | ![in xcode](https://github.com/CaiJingLong/some_asset/blob/master/flutter_photo2.png) 158 | 159 | ### Build error 160 | 161 | if you build error like include of non-modular header inside framework module, see [#10](https://github.com/CaiJingLong/flutter_photo/issues/10) or [so](https://stackoverflow.com/questions/27776497/include-of-non-modular-header-inside-framework-module) 162 | 163 | ## Thanks 164 | 165 | Part of the Android code comes from [debuggerx01](https://github.com/debuggerx01). 166 | 167 | ## Donate 168 | 169 | If my code helps you, and you're willing to buy me a cup of coffee. 170 | 171 | you can use [paypal](https://paypal.me/caijinglong) 172 | 173 | or scan my alipay 174 | 175 | 176 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | **/ios/Runner.xcworkspace/ 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /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: bf7c27095cc4dc07c03d8a6ad2e9e0e28e26227e 8 | channel: dev 9 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | photo library example 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](https://flutter.io/). 9 | -------------------------------------------------------------------------------- /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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.example" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | // androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | // androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.example; 2 | 3 | import android.os.Bundle; 4 | 5 | import io.flutter.app.FlutterActivity; 6 | import io.flutter.plugins.GeneratedPluginRegistrant; 7 | 8 | public class MainActivity extends FlutterActivity { 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | GeneratedPluginRegistrant.registerWith(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.3.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableJetifier=true 3 | android.useAndroidX=true 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /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.2.1-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 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 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.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 15 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 16 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 17 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 18 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 19 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 20 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 21 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 22 | F1F41F4DCF67837579E0EB42 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B01AC60EFE8E04B0E4D3E1C1 /* libPods-Runner.a */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXCopyFilesBuildPhase section */ 26 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 27 | isa = PBXCopyFilesBuildPhase; 28 | buildActionMask = 2147483647; 29 | dstPath = ""; 30 | dstSubfolderSpec = 10; 31 | files = ( 32 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 33 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 34 | ); 35 | name = "Embed Frameworks"; 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXCopyFilesBuildPhase section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 42 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 43 | 223FFDA7243467D600A654DC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 44 | 223FFDA8243467D600A654DC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; 45 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 46 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 47 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 48 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 49 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 50 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 51 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 52 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 53 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 55 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 58 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | 9DAC079CBB3999833A0EAF83 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 60 | B01AC60EFE8E04B0E4D3E1C1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | CCE4CADAA52D72A2FD3450C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 62 | /* End PBXFileReference section */ 63 | 64 | /* Begin PBXFrameworksBuildPhase section */ 65 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 70 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 71 | F1F41F4DCF67837579E0EB42 /* libPods-Runner.a in Frameworks */, 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 5CA87AA23185EF26B0BDF0EC /* Pods */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | CCE4CADAA52D72A2FD3450C5 /* Pods-Runner.debug.xcconfig */, 82 | 9DAC079CBB3999833A0EAF83 /* Pods-Runner.release.xcconfig */, 83 | ); 84 | name = Pods; 85 | sourceTree = ""; 86 | }; 87 | 9740EEB11CF90186004384FC /* Flutter */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 3B80C3931E831B6300D905FE /* App.framework */, 91 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 92 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 93 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 94 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 95 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 96 | ); 97 | name = Flutter; 98 | sourceTree = ""; 99 | }; 100 | 97C146E51CF9000F007C117D = { 101 | isa = PBXGroup; 102 | children = ( 103 | 9740EEB11CF90186004384FC /* Flutter */, 104 | 97C146F01CF9000F007C117D /* Runner */, 105 | 97C146EF1CF9000F007C117D /* Products */, 106 | 5CA87AA23185EF26B0BDF0EC /* Pods */, 107 | C74FBCC187FD75D483219296 /* Frameworks */, 108 | ); 109 | sourceTree = ""; 110 | }; 111 | 97C146EF1CF9000F007C117D /* Products */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 97C146EE1CF9000F007C117D /* Runner.app */, 115 | ); 116 | name = Products; 117 | sourceTree = ""; 118 | }; 119 | 97C146F01CF9000F007C117D /* Runner */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 123 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 124 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 125 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 126 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 127 | 97C147021CF9000F007C117D /* Info.plist */, 128 | 97C146F11CF9000F007C117D /* Supporting Files */, 129 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 130 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 131 | ); 132 | path = Runner; 133 | sourceTree = ""; 134 | }; 135 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 97C146F21CF9000F007C117D /* main.m */, 139 | ); 140 | name = "Supporting Files"; 141 | sourceTree = ""; 142 | }; 143 | C74FBCC187FD75D483219296 /* Frameworks */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | B01AC60EFE8E04B0E4D3E1C1 /* libPods-Runner.a */, 147 | ); 148 | name = Frameworks; 149 | sourceTree = ""; 150 | }; 151 | /* End PBXGroup section */ 152 | 153 | /* Begin PBXNativeTarget section */ 154 | 97C146ED1CF9000F007C117D /* Runner */ = { 155 | isa = PBXNativeTarget; 156 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 157 | buildPhases = ( 158 | A5808EB70AE398D55DAAB482 /* [CP] Check Pods Manifest.lock */, 159 | 9740EEB61CF901F6004384FC /* Run Script */, 160 | 97C146EA1CF9000F007C117D /* Sources */, 161 | 97C146EB1CF9000F007C117D /* Frameworks */, 162 | 97C146EC1CF9000F007C117D /* Resources */, 163 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 164 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 165 | 0844198878D85C0576400BDE /* [CP] Embed Pods Frameworks */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | ); 171 | name = Runner; 172 | productName = Runner; 173 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 174 | productType = "com.apple.product-type.application"; 175 | }; 176 | /* End PBXNativeTarget section */ 177 | 178 | /* Begin PBXProject section */ 179 | 97C146E61CF9000F007C117D /* Project object */ = { 180 | isa = PBXProject; 181 | attributes = { 182 | LastUpgradeCheck = 0910; 183 | ORGANIZATIONNAME = "The Chromium Authors"; 184 | TargetAttributes = { 185 | 97C146ED1CF9000F007C117D = { 186 | CreatedOnToolsVersion = 7.3.1; 187 | DevelopmentTeam = S5GU4EMC47; 188 | }; 189 | }; 190 | }; 191 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 192 | compatibilityVersion = "Xcode 3.2"; 193 | developmentRegion = English; 194 | hasScannedForEncodings = 0; 195 | knownRegions = ( 196 | English, 197 | en, 198 | Base, 199 | "zh-Hans", 200 | ); 201 | mainGroup = 97C146E51CF9000F007C117D; 202 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 203 | projectDirPath = ""; 204 | projectRoot = ""; 205 | targets = ( 206 | 97C146ED1CF9000F007C117D /* Runner */, 207 | ); 208 | }; 209 | /* End PBXProject section */ 210 | 211 | /* Begin PBXResourcesBuildPhase section */ 212 | 97C146EC1CF9000F007C117D /* Resources */ = { 213 | isa = PBXResourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 217 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 218 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 219 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 220 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 221 | ); 222 | runOnlyForDeploymentPostprocessing = 0; 223 | }; 224 | /* End PBXResourcesBuildPhase section */ 225 | 226 | /* Begin PBXShellScriptBuildPhase section */ 227 | 0844198878D85C0576400BDE /* [CP] Embed Pods Frameworks */ = { 228 | isa = PBXShellScriptBuildPhase; 229 | buildActionMask = 2147483647; 230 | files = ( 231 | ); 232 | inputPaths = ( 233 | ); 234 | name = "[CP] Embed Pods Frameworks"; 235 | outputPaths = ( 236 | ); 237 | runOnlyForDeploymentPostprocessing = 0; 238 | shellPath = /bin/sh; 239 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 240 | showEnvVarsInLog = 0; 241 | }; 242 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 243 | isa = PBXShellScriptBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | ); 247 | inputPaths = ( 248 | ); 249 | name = "Thin Binary"; 250 | outputPaths = ( 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | shellPath = /bin/sh; 254 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 255 | }; 256 | 9740EEB61CF901F6004384FC /* Run Script */ = { 257 | isa = PBXShellScriptBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | ); 261 | inputPaths = ( 262 | ); 263 | name = "Run Script"; 264 | outputPaths = ( 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | shellPath = /bin/sh; 268 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 269 | }; 270 | A5808EB70AE398D55DAAB482 /* [CP] Check Pods Manifest.lock */ = { 271 | isa = PBXShellScriptBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | ); 275 | inputFileListPaths = ( 276 | ); 277 | inputPaths = ( 278 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 279 | "${PODS_ROOT}/Manifest.lock", 280 | ); 281 | name = "[CP] Check Pods Manifest.lock"; 282 | outputFileListPaths = ( 283 | ); 284 | outputPaths = ( 285 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | shellPath = /bin/sh; 289 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 290 | showEnvVarsInLog = 0; 291 | }; 292 | /* End PBXShellScriptBuildPhase section */ 293 | 294 | /* Begin PBXSourcesBuildPhase section */ 295 | 97C146EA1CF9000F007C117D /* Sources */ = { 296 | isa = PBXSourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 300 | 97C146F31CF9000F007C117D /* main.m in Sources */, 301 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | /* End PBXSourcesBuildPhase section */ 306 | 307 | /* Begin PBXVariantGroup section */ 308 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 309 | isa = PBXVariantGroup; 310 | children = ( 311 | 97C146FB1CF9000F007C117D /* Base */, 312 | 223FFDA7243467D600A654DC /* zh-Hans */, 313 | ); 314 | name = Main.storyboard; 315 | sourceTree = ""; 316 | }; 317 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 318 | isa = PBXVariantGroup; 319 | children = ( 320 | 97C147001CF9000F007C117D /* Base */, 321 | 223FFDA8243467D600A654DC /* zh-Hans */, 322 | ); 323 | name = LaunchScreen.storyboard; 324 | sourceTree = ""; 325 | }; 326 | /* End PBXVariantGroup section */ 327 | 328 | /* Begin XCBuildConfiguration section */ 329 | 97C147031CF9000F007C117D /* Debug */ = { 330 | isa = XCBuildConfiguration; 331 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 332 | buildSettings = { 333 | ALWAYS_SEARCH_USER_PATHS = NO; 334 | CLANG_ANALYZER_NONNULL = YES; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 336 | CLANG_CXX_LIBRARY = "libc++"; 337 | CLANG_ENABLE_MODULES = YES; 338 | CLANG_ENABLE_OBJC_ARC = YES; 339 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 340 | CLANG_WARN_BOOL_CONVERSION = YES; 341 | CLANG_WARN_COMMA = YES; 342 | CLANG_WARN_CONSTANT_CONVERSION = YES; 343 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 344 | CLANG_WARN_EMPTY_BODY = YES; 345 | CLANG_WARN_ENUM_CONVERSION = YES; 346 | CLANG_WARN_INFINITE_RECURSION = YES; 347 | CLANG_WARN_INT_CONVERSION = YES; 348 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 349 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 350 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 351 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 352 | CLANG_WARN_STRICT_PROTOTYPES = YES; 353 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 354 | CLANG_WARN_UNREACHABLE_CODE = YES; 355 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 356 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 357 | COPY_PHASE_STRIP = NO; 358 | DEBUG_INFORMATION_FORMAT = dwarf; 359 | ENABLE_STRICT_OBJC_MSGSEND = YES; 360 | ENABLE_TESTABILITY = YES; 361 | GCC_C_LANGUAGE_STANDARD = gnu99; 362 | GCC_DYNAMIC_NO_PIC = NO; 363 | GCC_NO_COMMON_BLOCKS = YES; 364 | GCC_OPTIMIZATION_LEVEL = 0; 365 | GCC_PREPROCESSOR_DEFINITIONS = ( 366 | "DEBUG=1", 367 | "$(inherited)", 368 | ); 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 376 | MTL_ENABLE_DEBUG_INFO = YES; 377 | ONLY_ACTIVE_ARCH = YES; 378 | SDKROOT = iphoneos; 379 | TARGETED_DEVICE_FAMILY = "1,2"; 380 | }; 381 | name = Debug; 382 | }; 383 | 97C147041CF9000F007C117D /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 386 | buildSettings = { 387 | ALWAYS_SEARCH_USER_PATHS = NO; 388 | CLANG_ANALYZER_NONNULL = YES; 389 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 390 | CLANG_CXX_LIBRARY = "libc++"; 391 | CLANG_ENABLE_MODULES = YES; 392 | CLANG_ENABLE_OBJC_ARC = YES; 393 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 394 | CLANG_WARN_BOOL_CONVERSION = YES; 395 | CLANG_WARN_COMMA = YES; 396 | CLANG_WARN_CONSTANT_CONVERSION = YES; 397 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 398 | CLANG_WARN_EMPTY_BODY = YES; 399 | CLANG_WARN_ENUM_CONVERSION = YES; 400 | CLANG_WARN_INFINITE_RECURSION = YES; 401 | CLANG_WARN_INT_CONVERSION = YES; 402 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 403 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 404 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 405 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 406 | CLANG_WARN_STRICT_PROTOTYPES = YES; 407 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 408 | CLANG_WARN_UNREACHABLE_CODE = YES; 409 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 410 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 411 | COPY_PHASE_STRIP = NO; 412 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 413 | ENABLE_NS_ASSERTIONS = NO; 414 | ENABLE_STRICT_OBJC_MSGSEND = YES; 415 | GCC_C_LANGUAGE_STANDARD = gnu99; 416 | GCC_NO_COMMON_BLOCKS = YES; 417 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 418 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 419 | GCC_WARN_UNDECLARED_SELECTOR = YES; 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 421 | GCC_WARN_UNUSED_FUNCTION = YES; 422 | GCC_WARN_UNUSED_VARIABLE = YES; 423 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 424 | MTL_ENABLE_DEBUG_INFO = NO; 425 | SDKROOT = iphoneos; 426 | TARGETED_DEVICE_FAMILY = "1,2"; 427 | VALIDATE_PRODUCT = YES; 428 | }; 429 | name = Release; 430 | }; 431 | 97C147061CF9000F007C117D /* Debug */ = { 432 | isa = XCBuildConfiguration; 433 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 434 | buildSettings = { 435 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 436 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 437 | DEVELOPMENT_TEAM = S5GU4EMC47; 438 | ENABLE_BITCODE = NO; 439 | FRAMEWORK_SEARCH_PATHS = ( 440 | "$(inherited)", 441 | "$(PROJECT_DIR)/Flutter", 442 | ); 443 | INFOPLIST_FILE = Runner/Info.plist; 444 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 445 | LIBRARY_SEARCH_PATHS = ( 446 | "$(inherited)", 447 | "$(PROJECT_DIR)/Flutter", 448 | ); 449 | PRODUCT_BUNDLE_IDENTIFIER = top.kikt.photo.manager; 450 | PRODUCT_NAME = Runner; 451 | VERSIONING_SYSTEM = "apple-generic"; 452 | }; 453 | name = Debug; 454 | }; 455 | 97C147071CF9000F007C117D /* Release */ = { 456 | isa = XCBuildConfiguration; 457 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 458 | buildSettings = { 459 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 460 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 461 | DEVELOPMENT_TEAM = S5GU4EMC47; 462 | ENABLE_BITCODE = NO; 463 | FRAMEWORK_SEARCH_PATHS = ( 464 | "$(inherited)", 465 | "$(PROJECT_DIR)/Flutter", 466 | ); 467 | INFOPLIST_FILE = Runner/Info.plist; 468 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 469 | LIBRARY_SEARCH_PATHS = ( 470 | "$(inherited)", 471 | "$(PROJECT_DIR)/Flutter", 472 | ); 473 | PRODUCT_BUNDLE_IDENTIFIER = top.kikt.photo.manager; 474 | PRODUCT_NAME = Runner; 475 | VERSIONING_SYSTEM = "apple-generic"; 476 | }; 477 | name = Release; 478 | }; 479 | /* End XCBuildConfiguration section */ 480 | 481 | /* Begin XCConfigurationList section */ 482 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 483 | isa = XCConfigurationList; 484 | buildConfigurations = ( 485 | 97C147031CF9000F007C117D /* Debug */, 486 | 97C147041CF9000F007C117D /* Release */, 487 | ); 488 | defaultConfigurationIsVisible = 0; 489 | defaultConfigurationName = Release; 490 | }; 491 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 492 | isa = XCConfigurationList; 493 | buildConfigurations = ( 494 | 97C147061CF9000F007C117D /* Debug */, 495 | 97C147071CF9000F007C117D /* Release */, 496 | ); 497 | defaultConfigurationIsVisible = 0; 498 | defaultConfigurationName = Release; 499 | }; 500 | /* End XCConfigurationList section */ 501 | }; 502 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 503 | } 504 | -------------------------------------------------------------------------------- /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 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CaiJingLong/flutter_photo/2e0ab7531a4d0a2cf01d2eb7bd3a6ba45821d2dc/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 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | PhotoExample 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | NSPhotoLibraryUsageDescription 26 | App需要您的同意,才能访问相册 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /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/ios/Runner/zh-Hans.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/ios/Runner/zh-Hans.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/lib/asset_image.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:photo_manager/photo_manager.dart'; 5 | 6 | class AssetImageWidget extends StatelessWidget { 7 | final AssetEntity assetEntity; 8 | final double width; 9 | final double height; 10 | final BoxFit boxFit; 11 | 12 | const AssetImageWidget({ 13 | Key key, 14 | @required this.assetEntity, 15 | this.width, 16 | this.height, 17 | this.boxFit, 18 | }) : super(key: key); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | if (assetEntity == null) { 23 | return _buildContainer(); 24 | } 25 | 26 | print( 27 | "assetEntity.width = ${assetEntity.width} , assetEntity.height = ${assetEntity.height}"); 28 | 29 | return FutureBuilder( 30 | builder: (BuildContext context, snapshot) { 31 | if (snapshot.hasData) { 32 | return _buildContainer( 33 | child: Image.memory( 34 | snapshot.data, 35 | width: width, 36 | height: height, 37 | fit: boxFit, 38 | ), 39 | ); 40 | } else { 41 | return _buildContainer(); 42 | } 43 | }, 44 | future: assetEntity.thumbDataWithSize( 45 | width.toInt(), 46 | height.toInt(), 47 | ), 48 | ); 49 | } 50 | 51 | Widget _buildContainer({Widget child}) { 52 | child ??= Container(); 53 | return Container( 54 | width: width, 55 | height: height, 56 | child: child, 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/lib/icon_text_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class IconTextButton extends StatelessWidget { 4 | final IconData icon; 5 | final String text; 6 | final Function onTap; 7 | 8 | const IconTextButton({Key key, this.icon, this.text, this.onTap}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return InkWell( 14 | onTap: onTap, 15 | child: Container( 16 | child: ListTile( 17 | leading: Icon(icon ?? Icons.device_unknown), 18 | title: Text(text ?? ""), 19 | ), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:oktoast/oktoast.dart'; 4 | import 'package:photo/photo.dart'; 5 | import 'package:photo_manager/photo_manager.dart'; 6 | 7 | import './preview.dart'; 8 | import 'icon_text_button.dart'; 9 | import 'picked_example.dart'; 10 | 11 | void main() => runApp(MyApp()); 12 | 13 | class MyApp extends StatelessWidget { 14 | // This widget is the root of your application. 15 | @override 16 | Widget build(BuildContext context) { 17 | return OKToast( 18 | child: MaterialApp( 19 | title: 'Pick Image Demo', 20 | theme: ThemeData( 21 | primarySwatch: Colors.lime, 22 | ), 23 | home: MyHomePage(title: 'Pick Image Demo'), 24 | ), 25 | ); 26 | } 27 | } 28 | 29 | class MyHomePage extends StatefulWidget { 30 | MyHomePage({Key key, this.title}) : super(key: key); 31 | final String title; 32 | 33 | @override 34 | _MyHomePageState createState() => _MyHomePageState(); 35 | } 36 | 37 | class _MyHomePageState extends State with LoadingDelegate { 38 | String currentSelected = ""; 39 | 40 | @override 41 | Widget buildBigImageLoading( 42 | BuildContext context, AssetEntity entity, Color themeColor) { 43 | return Center( 44 | child: Container( 45 | width: 50.0, 46 | height: 50.0, 47 | child: CupertinoActivityIndicator( 48 | radius: 25.0, 49 | ), 50 | ), 51 | ); 52 | } 53 | 54 | @override 55 | Widget buildPreviewLoading( 56 | BuildContext context, AssetEntity entity, Color themeColor) { 57 | return Center( 58 | child: Container( 59 | width: 50.0, 60 | height: 50.0, 61 | child: CupertinoActivityIndicator( 62 | radius: 25.0, 63 | ), 64 | ), 65 | ); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return Scaffold( 71 | appBar: AppBar( 72 | title: Text(widget.title), 73 | actions: [ 74 | FlatButton( 75 | child: Icon(Icons.image), 76 | onPressed: _testPhotoListParams, 77 | ), 78 | ], 79 | ), 80 | body: Container( 81 | child: SingleChildScrollView( 82 | child: Column( 83 | children: [ 84 | IconTextButton( 85 | icon: Icons.photo, 86 | text: "photo", 87 | onTap: () => _pickAsset(PickType.onlyImage), 88 | ), 89 | IconTextButton( 90 | icon: Icons.videocam, 91 | text: "video", 92 | onTap: () => _pickAsset(PickType.onlyVideo), 93 | ), 94 | IconTextButton( 95 | icon: Icons.album, 96 | text: "all", 97 | onTap: () => _pickAsset(PickType.all), 98 | ), 99 | IconTextButton( 100 | icon: CupertinoIcons.reply_all, 101 | text: "Picked asset example.", 102 | onTap: () => routePage(PickedExample()), 103 | ), 104 | ], 105 | ), 106 | ), 107 | ), 108 | floatingActionButton: FloatingActionButton( 109 | onPressed: () => _pickAsset(PickType.all), 110 | tooltip: 'pickImage', 111 | child: Icon(Icons.add), 112 | ), 113 | ); 114 | } 115 | 116 | void _testPhotoListParams() async { 117 | var assetPathList = 118 | await PhotoManager.getAssetPathList(type: RequestType.image); 119 | _pickAsset(PickType.all, pathList: assetPathList); 120 | } 121 | 122 | void _pickAsset(PickType type, {List pathList}) async { 123 | /// context is required, other params is optional. 124 | /// context is required, other params is optional. 125 | /// context is required, other params is optional. 126 | 127 | PhotoPicker.clearThumbMemoryCache(); 128 | 129 | List imgList = await PhotoPicker.pickAsset( 130 | // BuildContext required 131 | context: context, 132 | 133 | /// The following are optional parameters. 134 | themeColor: Colors.green, 135 | // the title color and bottom color 136 | 137 | textColor: Colors.white, 138 | // text color 139 | padding: 1.0, 140 | // item padding 141 | dividerColor: Colors.grey, 142 | // divider color 143 | disableColor: Colors.grey.shade300, 144 | // the check box disable color 145 | itemRadio: 0.88, 146 | // the content item radio 147 | maxSelected: 8, 148 | // max picker image count 149 | // provider: I18nProvider.english, 150 | provider: I18nProvider.chinese, 151 | // i18n provider ,default is chinese. , you can custom I18nProvider or use ENProvider() 152 | rowCount: 3, 153 | // item row count 154 | 155 | thumbSize: 150, 156 | // preview thumb size , default is 64 157 | sortDelegate: SortDelegate.common, 158 | // default is common ,or you make custom delegate to sort your gallery 159 | checkBoxBuilderDelegate: DefaultCheckBoxBuilderDelegate( 160 | activeColor: Colors.white, 161 | unselectedColor: Colors.white, 162 | checkColor: Colors.green, 163 | ), 164 | // default is DefaultCheckBoxBuilderDelegate ,or you make custom delegate to create checkbox 165 | 166 | loadingDelegate: this, 167 | // if you want to build custom loading widget,extends LoadingDelegate, [see example/lib/main.dart] 168 | 169 | badgeDelegate: const DurationBadgeDelegate(), 170 | // badgeDelegate to show badge widget 171 | 172 | pickType: type, 173 | 174 | photoPathList: pathList, 175 | ); 176 | 177 | if (imgList == null || imgList.isEmpty) { 178 | showToast("No pick item."); 179 | return; 180 | } else { 181 | List r = []; 182 | for (var e in imgList) { 183 | var file = await e.file; 184 | r.add(file.absolute.path); 185 | } 186 | currentSelected = r.join("\n\n"); 187 | 188 | List preview = []; 189 | preview.addAll(imgList); 190 | Navigator.push(context, 191 | MaterialPageRoute(builder: (_) => PreviewPage(list: preview))); 192 | } 193 | setState(() {}); 194 | } 195 | 196 | void routePage(Widget widget) { 197 | Navigator.push( 198 | context, 199 | MaterialPageRoute( 200 | builder: (BuildContext context) { 201 | return widget; 202 | }, 203 | ), 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /example/lib/picked_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:photo/photo.dart'; 4 | 5 | import 'icon_text_button.dart'; 6 | import 'package:photo_manager/photo_manager.dart'; 7 | 8 | class PickedExample extends StatefulWidget { 9 | @override 10 | _PickedExampleState createState() => _PickedExampleState(); 11 | } 12 | 13 | class _PickedExampleState extends State { 14 | List picked = []; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: Text("Cross Picked asset"), 21 | ), 22 | body: Column( 23 | children: [ 24 | IconTextButton( 25 | icon: Icons.assignment, 26 | text: "Pick asset", 27 | onTap: _pickAsset, 28 | ), 29 | ListTile( 30 | title: Text("picked asset count = ${picked.length}"), 31 | ), 32 | ], 33 | ), 34 | ); 35 | } 36 | 37 | void _pickAsset() async { 38 | final result = await PhotoPicker.pickAsset( 39 | context: context, 40 | pickedAssetList: picked, 41 | ); 42 | if (result != null && result.isNotEmpty) { 43 | picked.clear(); 44 | picked.addAll(result); 45 | setState(() {}); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/lib/preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | import './asset_image.dart'; 5 | 6 | class PreviewPage extends StatelessWidget { 7 | final List list; 8 | 9 | const PreviewPage({Key key, this.list = const []}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: Text("Preview"), 16 | ), 17 | body: ListView( 18 | children: list 19 | .map((item) => AssetImageWidget( 20 | assetEntity: item, 21 | width: 300, 22 | height: 200, 23 | boxFit: BoxFit.cover, 24 | )) 25 | .toList(), 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.0.0+1 11 | 12 | environment: 13 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | 19 | # The following adds the Cupertino Icons font to your application. 20 | # Use with the CupertinoIcons class for iOS style icons. 21 | cupertino_icons: ^0.1.2 22 | 23 | photo: 24 | path: ../ 25 | 26 | oktoast: ^2.0.0 27 | 28 | dev_dependencies: 29 | flutter_test: 30 | sdk: flutter 31 | 32 | # For information on the generic Dart part of this file, see the 33 | # following page: https://www.dartlang.org/tools/pub/pubspec 34 | 35 | # The following section is specific to Flutter. 36 | flutter: 37 | # The following line ensures that the Material Icons font is 38 | # included with your application, so that you can use the icons in 39 | # the material Icons class. 40 | uses-material-design: true 41 | # To add assets to your application, add an assets section, like this: 42 | # assets: 43 | # - images/a_dot_burr.jpeg 44 | # - images/a_dot_ham.jpeg 45 | # An image asset can refer to one or more resolution-specific "variants", see 46 | # https://flutter.io/assets-and-images/#resolution-aware. 47 | # For details regarding adding assets from package dependencies, see 48 | # https://flutter.io/assets-and-images/#from-packages 49 | # To add custom fonts to your application, add a fonts section here, 50 | # in this "flutter" section. Each entry in this list should have a 51 | # "family" key with the font family name, and a "fonts" key with a 52 | # list giving the asset and other descriptors for the font. For 53 | # example: 54 | # fonts: 55 | # - family: Schyler 56 | # fonts: 57 | # - asset: fonts/Schyler-Regular.ttf 58 | # - asset: fonts/Schyler-Italic.ttf 59 | # style: italic 60 | # - family: Trajan Pro 61 | # fonts: 62 | # - asset: fonts/TrajanPro.ttf 63 | # - asset: fonts/TrajanPro_Bold.ttf 64 | # weight: 700 65 | # 66 | # For details regarding fonts from package dependencies, 67 | # see https://flutter.io/custom-fonts/#from-packages 68 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import '../lib/main.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/photo.dart: -------------------------------------------------------------------------------- 1 | library photo; 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:photo/src/engine/lru_cache.dart'; 7 | 8 | import 'package:photo_manager/photo_manager.dart'; 9 | 10 | import 'package:photo/src/delegate/badge_delegate.dart'; 11 | import 'package:photo/src/delegate/checkbox_builder_delegate.dart'; 12 | import 'package:photo/src/delegate/loading_delegate.dart'; 13 | import 'package:photo/src/delegate/sort_delegate.dart'; 14 | import 'package:photo/src/entity/options.dart'; 15 | import 'package:photo/src/provider/i18n_provider.dart'; 16 | import 'package:photo/src/ui/dialog/not_permission_dialog.dart'; 17 | import 'package:photo/src/ui/photo_app.dart'; 18 | export 'package:photo/src/delegate/checkbox_builder_delegate.dart'; 19 | export 'package:photo/src/delegate/loading_delegate.dart'; 20 | export 'package:photo/src/delegate/sort_delegate.dart'; 21 | export 'package:photo/src/provider/i18n_provider.dart' 22 | show I18NCustomProvider, I18nProvider, CNProvider, ENProvider; 23 | export 'package:photo/src/entity/options.dart' show PickType; 24 | export 'package:photo/src/delegate/badge_delegate.dart'; 25 | 26 | class PhotoPicker { 27 | static PhotoPicker _instance; 28 | 29 | PhotoPicker._(); 30 | 31 | factory PhotoPicker() { 32 | _instance ??= PhotoPicker._(); 33 | return _instance; 34 | } 35 | 36 | /// Clear memory Lru cache. 37 | /// 38 | /// Suitable for the following scenarios: 39 | /// 40 | /// 1. The app's memory is tight. 41 | /// 2. When the resources was modified, all the thumbnails are no longer as expected. 42 | static void clearThumbMemoryCache() { 43 | ImageLruCache.clearCache(); 44 | } 45 | 46 | static const String rootRouteName = "photo_picker_image"; 47 | 48 | /// 没有授予权限的时候,会开启一个dialog去帮助用户去应用设置页面开启权限 49 | /// 确定开启设置页面,取消关闭弹窗,无论选择什么,返回值都是null 50 | /// 51 | /// 52 | /// 当用户给予权限后 53 | /// 54 | /// 当用户确定时,返回一个图片[AssetEntity]列表 55 | /// 56 | /// 当用户取消时返回一个空数组 57 | /// 58 | /// [photoPathList] 一旦设置 则 [pickType]参数无效 59 | /// 60 | /// [pickedAssetList] 已选择的asset 61 | /// 62 | /// 关于参数可以查看readme文档介绍 63 | /// 64 | /// if user not grand permission, then return null and show a dialog to help user open setting. 65 | /// sure is open setting cancel ,cancel to dismiss dialog, return null 66 | /// 67 | /// when user give permission. 68 | /// 69 | /// when user sure , return a [AssetEntity] of [List] 70 | /// 71 | /// when user cancel selected,result is empty list 72 | /// 73 | /// when [photoPathList] is not null , [pickType] invalid 74 | /// 75 | /// [pickedAssetList]: The results of the last selection can be passed in for easy secondary selection. 76 | /// 77 | /// params see readme.md 78 | static Future> pickAsset({ 79 | @required BuildContext context, 80 | int rowCount = 4, 81 | int maxSelected = 9, 82 | double padding = 0.5, 83 | double itemRadio = 1.0, 84 | Color themeColor, 85 | Color dividerColor, 86 | Color textColor, 87 | Color disableColor, 88 | int thumbSize = 64, 89 | I18nProvider provider = I18nProvider.chinese, 90 | SortDelegate sortDelegate, 91 | CheckBoxBuilderDelegate checkBoxBuilderDelegate, 92 | LoadingDelegate loadingDelegate, 93 | PickType pickType = PickType.all, 94 | BadgeDelegate badgeDelegate = const DefaultBadgeDelegate(), 95 | List photoPathList, 96 | List pickedAssetList, 97 | }) { 98 | assert(provider != null, "provider must be not null"); 99 | assert(context != null, "context must be not null"); 100 | assert(pickType != null, "pickType must be not null"); 101 | 102 | themeColor ??= Theme.of(context)?.primaryColor ?? Colors.black; 103 | dividerColor ??= Theme.of(context)?.dividerColor ?? Colors.grey; 104 | disableColor ??= Theme.of(context)?.disabledColor ?? Colors.grey; 105 | textColor ??= Colors.white; 106 | 107 | sortDelegate ??= SortDelegate.common; 108 | checkBoxBuilderDelegate ??= DefaultCheckBoxBuilderDelegate(); 109 | 110 | loadingDelegate ??= DefaultLoadingDelegate(); 111 | 112 | var options = Options( 113 | rowCount: rowCount, 114 | dividerColor: dividerColor, 115 | maxSelected: maxSelected, 116 | itemRadio: itemRadio, 117 | padding: padding, 118 | disableColor: disableColor, 119 | textColor: textColor, 120 | themeColor: themeColor, 121 | thumbSize: thumbSize, 122 | sortDelegate: sortDelegate, 123 | checkBoxBuilderDelegate: checkBoxBuilderDelegate, 124 | loadingDelegate: loadingDelegate, 125 | badgeDelegate: badgeDelegate, 126 | pickType: pickType, 127 | ); 128 | 129 | return PhotoPicker()._pickAsset( 130 | context, 131 | options, 132 | provider, 133 | photoPathList, 134 | pickedAssetList, 135 | ); 136 | } 137 | 138 | Future> _pickAsset( 139 | BuildContext context, 140 | Options options, 141 | I18nProvider provider, 142 | List photoList, 143 | List pickedAssetList, 144 | ) async { 145 | var requestPermission = await PhotoManager.requestPermission(); 146 | if (requestPermission != true) { 147 | var result = await showDialog( 148 | context: context, 149 | builder: (ctx) => NotPermissionDialog( 150 | provider.getNotPermissionText(options), 151 | ), 152 | ); 153 | if (result == true) { 154 | PhotoManager.openSetting(); 155 | } 156 | return null; 157 | } 158 | 159 | return _openGalleryContentPage( 160 | context, 161 | options, 162 | provider, 163 | photoList, 164 | pickedAssetList, 165 | ); 166 | } 167 | 168 | Future> _openGalleryContentPage( 169 | BuildContext context, 170 | Options options, 171 | I18nProvider provider, 172 | List photoList, 173 | List pickedAssetList, 174 | ) async { 175 | return Navigator.of(context, rootNavigator: true).push( 176 | MaterialPageRoute( 177 | builder: (ctx) => PhotoApp( 178 | options: options, 179 | provider: provider, 180 | photoList: photoList, 181 | pickedAssetList: pickedAssetList, 182 | ), 183 | ), 184 | ); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /lib/src/delegate/badge_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | abstract class BadgeDelegate { 5 | const BadgeDelegate(); 6 | 7 | Widget buildBadge(BuildContext context, AssetType type, Duration duration); 8 | } 9 | 10 | class DefaultBadgeDelegate extends BadgeDelegate { 11 | final AlignmentGeometry alignment; 12 | 13 | const DefaultBadgeDelegate({ 14 | this.alignment = Alignment.topLeft, 15 | }); 16 | 17 | @override 18 | Widget buildBadge(BuildContext context, AssetType type, Duration duration) { 19 | if (type == AssetType.video) { 20 | return Padding( 21 | padding: const EdgeInsets.all(2.0), 22 | child: Align( 23 | alignment: alignment, 24 | child: Container( 25 | decoration: BoxDecoration( 26 | color: Theme.of(context).primaryColor, 27 | borderRadius: BorderRadius.circular(3.0), 28 | ), 29 | child: Text( 30 | "video", 31 | style: const TextStyle( 32 | fontSize: 12.0, 33 | color: Colors.white, 34 | ), 35 | ), 36 | padding: const EdgeInsets.all(4.0), 37 | ), 38 | ), 39 | ); 40 | } 41 | 42 | return Container(); 43 | } 44 | } 45 | 46 | class DurationBadgeDelegate extends BadgeDelegate { 47 | final AlignmentGeometry alignment; 48 | const DurationBadgeDelegate({this.alignment = Alignment.bottomRight}); 49 | 50 | @override 51 | Widget buildBadge(BuildContext context, AssetType type, Duration duration) { 52 | if (type == AssetType.video) { 53 | var s = duration.inSeconds % 60; 54 | var m = duration.inMinutes % 60; 55 | var h = duration.inHours; 56 | 57 | String text = 58 | "$h:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}"; 59 | 60 | return Padding( 61 | padding: const EdgeInsets.all(2.0), 62 | child: Align( 63 | alignment: alignment, 64 | child: Container( 65 | decoration: BoxDecoration( 66 | color: Theme.of(context).primaryColor.withOpacity(0.65), 67 | ), 68 | child: Text( 69 | text, 70 | style: const TextStyle( 71 | fontSize: 12.0, 72 | color: Colors.white, 73 | ), 74 | ), 75 | padding: const EdgeInsets.all(4.0), 76 | ), 77 | ), 78 | ); 79 | } 80 | 81 | return Container(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/delegate/checkbox_builder_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide CheckboxListTile; 2 | import 'package:photo/src/entity/options.dart'; 3 | import 'package:photo/src/provider/i18n_provider.dart'; 4 | import 'package:photo/src/ui/widget/check_tile_copy.dart'; 5 | 6 | abstract class CheckBoxBuilderDelegate { 7 | Widget buildCheckBox( 8 | BuildContext context, 9 | bool checked, 10 | int index, 11 | Options options, 12 | I18nProvider i18nProvider, 13 | ); 14 | } 15 | 16 | class DefaultCheckBoxBuilderDelegate extends CheckBoxBuilderDelegate { 17 | Color activeColor; 18 | Color unselectedColor; 19 | Color checkColor; 20 | 21 | DefaultCheckBoxBuilderDelegate({ 22 | this.activeColor = Colors.white, 23 | this.unselectedColor = Colors.white, 24 | this.checkColor = Colors.black, 25 | }); 26 | 27 | @override 28 | Widget buildCheckBox( 29 | BuildContext context, 30 | bool checked, 31 | int index, 32 | Options options, 33 | I18nProvider i18nProvider, 34 | ) { 35 | return Theme( 36 | data: Theme.of(context).copyWith(unselectedWidgetColor: unselectedColor), 37 | child: CheckboxListTile( 38 | value: checked, 39 | onChanged: (bool check) {}, 40 | activeColor: activeColor, 41 | checkColor: checkColor, 42 | title: Text( 43 | i18nProvider.getSelectedOptionsText(options), 44 | textAlign: TextAlign.end, 45 | style: TextStyle(color: options.textColor), 46 | ), 47 | ), 48 | ); 49 | } 50 | } 51 | 52 | class RadioCheckBoxBuilderDelegate extends CheckBoxBuilderDelegate { 53 | Color activeColor; 54 | Color unselectedColor; 55 | 56 | RadioCheckBoxBuilderDelegate({ 57 | this.activeColor = Colors.white, 58 | this.unselectedColor = Colors.white, 59 | }); 60 | 61 | @override 62 | Widget buildCheckBox( 63 | BuildContext context, 64 | bool checked, 65 | int index, 66 | Options options, 67 | I18nProvider i18nProvider, 68 | ) { 69 | return Theme( 70 | data: Theme.of(context).copyWith(unselectedWidgetColor: unselectedColor), 71 | child: RadioListTile( 72 | value: true, 73 | onChanged: (bool check) {}, 74 | activeColor: activeColor, 75 | title: Text( 76 | i18nProvider.getSelectedOptionsText(options), 77 | textAlign: TextAlign.end, 78 | style: TextStyle(color: options.textColor, fontSize: 14.0), 79 | ), 80 | groupValue: checked, 81 | controlAffinity: ListTileControlAffinity.trailing, 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/delegate/loading_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo_manager/photo_manager.dart'; 3 | 4 | abstract class LoadingDelegate { 5 | Widget buildBigImageLoading( 6 | BuildContext context, AssetEntity entity, Color themeColor); 7 | 8 | Widget buildPreviewLoading( 9 | BuildContext context, AssetEntity entity, Color themeColor); 10 | } 11 | 12 | class DefaultLoadingDelegate extends LoadingDelegate { 13 | @override 14 | Widget buildBigImageLoading( 15 | BuildContext context, AssetEntity entity, Color themeColor) { 16 | return Center( 17 | child: Container( 18 | width: 30.0, 19 | height: 30.0, 20 | child: CircularProgressIndicator( 21 | valueColor: AlwaysStoppedAnimation(themeColor), 22 | ), 23 | ), 24 | ); 25 | } 26 | 27 | @override 28 | Widget buildPreviewLoading( 29 | BuildContext context, AssetEntity entity, Color themeColor) { 30 | return Center( 31 | child: Container( 32 | width: 30.0, 33 | height: 30.0, 34 | child: CircularProgressIndicator( 35 | valueColor: AlwaysStoppedAnimation(themeColor), 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/delegate/sort_asset_delegate.dart: -------------------------------------------------------------------------------- 1 | part of './sort_delegate.dart'; 2 | 3 | abstract class SortAssetDelegate { 4 | const SortAssetDelegate(); 5 | 6 | void sort(List list); 7 | } 8 | 9 | class DefaultAssetDelegate extends SortAssetDelegate { 10 | const DefaultAssetDelegate(); 11 | 12 | @override 13 | void sort(List list) { 14 | list.sort((entity1, entity2) { 15 | return entity2.createDateTime.compareTo(entity1.createDateTime); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/delegate/sort_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:photo_manager/photo_manager.dart'; 2 | 3 | part './sort_asset_delegate.dart'; 4 | 5 | /// SortPathDelegate 6 | abstract class SortDelegate { 7 | final SortAssetDelegate assetDelegate; 8 | 9 | const SortDelegate({ 10 | this.assetDelegate = const DefaultAssetDelegate(), 11 | }); 12 | 13 | void sort(List list); 14 | 15 | static const none = DefaultSortDelegate(); 16 | 17 | static const common = CommonSortDelegate(); 18 | } 19 | 20 | class DefaultSortDelegate extends SortDelegate { 21 | const DefaultSortDelegate({ 22 | SortAssetDelegate assetDelegate = const DefaultAssetDelegate(), 23 | }) : super(assetDelegate: assetDelegate); 24 | 25 | @override 26 | void sort(List list) {} 27 | } 28 | 29 | class CommonSortDelegate extends SortDelegate { 30 | const CommonSortDelegate({ 31 | SortAssetDelegate assetDelegate = const DefaultAssetDelegate(), 32 | }) : super(assetDelegate: assetDelegate); 33 | 34 | @override 35 | void sort(List list) { 36 | list.sort((path1, path2) { 37 | if (path1.isAll) { 38 | return -1; 39 | } 40 | 41 | if (path2.isAll) { 42 | return 1; 43 | } 44 | 45 | if (_isCamera(path1)) { 46 | return -1; 47 | } 48 | 49 | if (_isCamera(path2)) { 50 | return 1; 51 | } 52 | 53 | if (_isScreenShot(path1)) { 54 | return -1; 55 | } 56 | 57 | if (_isScreenShot(path2)) { 58 | return 1; 59 | } 60 | 61 | return otherSort(path1, path2); 62 | }); 63 | } 64 | 65 | int otherSort(AssetPathEntity path1, AssetPathEntity path2) { 66 | return path1.name.compareTo(path2.name); 67 | } 68 | 69 | bool _isCamera(AssetPathEntity entity) { 70 | return entity.name.toUpperCase() == "camera".toUpperCase(); 71 | } 72 | 73 | bool _isScreenShot(AssetPathEntity entity) { 74 | return entity.name.toUpperCase() == "screenshots".toUpperCase() || 75 | entity.name.toUpperCase() == "screenshot".toUpperCase(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/engine/lru_cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:photo_manager/photo_manager.dart'; 5 | 6 | class ImageLruCache { 7 | static LRUMap<_ImageCacheEntity, Uint8List> _map = LRUMap(500); 8 | 9 | static Uint8List getData(AssetEntity entity, [int size = 64]) { 10 | return _map.get(_ImageCacheEntity(entity, size)); 11 | } 12 | 13 | static void setData(AssetEntity entity, int size, Uint8List list) { 14 | _map.put(_ImageCacheEntity(entity, size), list); 15 | } 16 | 17 | static void clearCache() { 18 | _map.clear(); 19 | } 20 | } 21 | 22 | class _ImageCacheEntity { 23 | AssetEntity entity; 24 | int size; 25 | 26 | _ImageCacheEntity(this.entity, this.size); 27 | 28 | @override 29 | bool operator ==(Object other) => 30 | identical(this, other) || 31 | other is _ImageCacheEntity && 32 | runtimeType == other.runtimeType && 33 | entity == other.entity && 34 | size == other.size; 35 | 36 | @override 37 | int get hashCode => entity.hashCode ^ size.hashCode; 38 | } 39 | 40 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 41 | // for details. All rights reserved. Use of this source code is governed by a 42 | // BSD-style license that can be found in the LICENSE file. 43 | 44 | typedef EvictionHandler(K key, V value); 45 | 46 | class LRUMap { 47 | final LinkedHashMap _map = LinkedHashMap(); 48 | final int _maxSize; 49 | final EvictionHandler _handler; 50 | 51 | LRUMap(this._maxSize, [this._handler]); 52 | 53 | V get(K key) { 54 | V value = _map.remove(key); 55 | if (value != null) { 56 | _map[key] = value; 57 | } 58 | return value; 59 | } 60 | 61 | void put(K key, V value) { 62 | _map.remove(key); 63 | _map[key] = value; 64 | if (_map.length > _maxSize) { 65 | K evictedKey = _map.keys.first; 66 | V evictedValue = _map.remove(evictedKey); 67 | if (_handler != null) { 68 | _handler(evictedKey, evictedValue); 69 | } 70 | } 71 | } 72 | 73 | void remove(K key) { 74 | _map.remove(key); 75 | } 76 | 77 | void clear() { 78 | _map.clear(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/src/engine/throttle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | typedef VoidCallback(); 6 | 7 | /// When multiple calls are repeated, only the first time is valid. 8 | /// 9 | /// Like rxdart `throttle` method 10 | class Throttle { 11 | Duration duration; 12 | 13 | VoidCallback onCall; 14 | 15 | bool _isRunning = false; 16 | 17 | Timer _timer; 18 | 19 | Throttle({ 20 | @required this.onCall, 21 | this.duration = const Duration(seconds: 2), 22 | }); 23 | 24 | void call(call) { 25 | if (!_isRunning) { 26 | _startTimer(); 27 | onCall?.call(); 28 | } 29 | } 30 | 31 | void _startTimer() { 32 | if (_timer != null) { 33 | _stopTimer(); 34 | } 35 | _isRunning = true; 36 | _timer = Timer(duration, () { 37 | _isRunning = false; 38 | _timer = null; 39 | }); 40 | } 41 | 42 | void _stopTimer() { 43 | _timer?.cancel(); 44 | _isRunning = false; 45 | _timer = null; 46 | } 47 | 48 | void dispose() { 49 | this.onCall = null; 50 | _stopTimer(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/entity/options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo/src/delegate/badge_delegate.dart'; 3 | import 'package:photo/src/delegate/checkbox_builder_delegate.dart'; 4 | import 'package:photo/src/delegate/loading_delegate.dart'; 5 | import 'package:photo/src/delegate/sort_delegate.dart'; 6 | 7 | class Options { 8 | final int rowCount; 9 | 10 | final int maxSelected; 11 | 12 | final double padding; 13 | 14 | final double itemRadio; 15 | 16 | final Color themeColor; 17 | 18 | final Color dividerColor; 19 | 20 | final Color textColor; 21 | 22 | final Color disableColor; 23 | 24 | final int thumbSize; 25 | 26 | final SortDelegate sortDelegate; 27 | 28 | final CheckBoxBuilderDelegate checkBoxBuilderDelegate; 29 | 30 | final LoadingDelegate loadingDelegate; 31 | 32 | final BadgeDelegate badgeDelegate; 33 | 34 | final PickType pickType; 35 | 36 | const Options({ 37 | this.rowCount, 38 | this.maxSelected, 39 | this.padding, 40 | this.itemRadio, 41 | this.themeColor, 42 | this.dividerColor, 43 | this.textColor, 44 | this.disableColor, 45 | this.thumbSize, 46 | this.sortDelegate, 47 | this.checkBoxBuilderDelegate, 48 | this.loadingDelegate, 49 | this.badgeDelegate, 50 | this.pickType, 51 | }); 52 | } 53 | 54 | enum PickType { 55 | all, 56 | onlyImage, 57 | onlyVideo, 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/error/permission_error.dart: -------------------------------------------------------------------------------- 1 | class NoPermissionError implements Exception { 2 | const NoPermissionError(); 3 | } 4 | -------------------------------------------------------------------------------- /lib/src/provider/asset_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:photo_manager/photo_manager.dart'; 4 | 5 | class AssetProvider { 6 | Map _dataMap = {}; 7 | 8 | AssetPathEntity _current; 9 | 10 | AssetPathEntity get current => _current; 11 | 12 | set current(AssetPathEntity current) { 13 | _current = current; 14 | if (_dataMap[current] == null) { 15 | final paging = AssetPaging(current); 16 | _dataMap[current] = paging; 17 | } 18 | } 19 | 20 | List get data => _dataMap[current]?.data ?? []; 21 | 22 | Future loadMore() async { 23 | final paging = getPaging(); 24 | if (paging != null) { 25 | await paging.loadMore(); 26 | } 27 | } 28 | 29 | AssetPaging getPaging() => _dataMap[current]; 30 | 31 | bool get noMore => getPaging()?.noMore ?? false; 32 | 33 | int get count => data?.length ?? 0; 34 | } 35 | 36 | class AssetPaging { 37 | int page = 0; 38 | 39 | List data = []; 40 | 41 | final AssetPathEntity path; 42 | 43 | final int pageCount; 44 | 45 | bool noMore = false; 46 | 47 | AssetPaging(this.path, {this.pageCount = 50}); 48 | 49 | Future loadMore() async { 50 | if (noMore == true) { 51 | return; 52 | } 53 | var data = await path.getAssetListPaged(page, pageCount); 54 | if (data.length == 0) { 55 | noMore = true; 56 | } 57 | page++; 58 | this.data.addAll(data); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/provider/config_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo/src/entity/options.dart'; 3 | import 'package:photo/src/provider/asset_provider.dart'; 4 | import 'package:photo/src/provider/i18n_provider.dart'; 5 | import 'package:photo_manager/photo_manager.dart'; 6 | 7 | // ignore_for_file: deprecated_member_use 8 | class PhotoPickerProvider extends InheritedWidget { 9 | final Options options; 10 | final I18nProvider provider; 11 | final AssetProvider assetProvider = AssetProvider(); 12 | final List pickedAssetList; 13 | PhotoPickerProvider({ 14 | @required this.options, 15 | @required this.provider, 16 | @required Widget child, 17 | this.pickedAssetList, 18 | Key key, 19 | }) : super(key: key, child: child); 20 | 21 | @override 22 | bool updateShouldNotify(InheritedWidget oldWidget) { 23 | return true; 24 | } 25 | 26 | static PhotoPickerProvider of(BuildContext context) => 27 | context.inheritFromWidgetOfExactType(PhotoPickerProvider); 28 | 29 | static AssetProvider assetProviderOf(BuildContext context) => 30 | of(context).assetProvider; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/provider/gallery_list_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:photo_manager/photo_manager.dart'; 2 | 3 | abstract class GalleryListProvider { 4 | List galleryPathList = []; 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/provider/i18n_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:photo/src/entity/options.dart'; 2 | import 'package:photo/src/provider/selected_provider.dart'; 3 | 4 | abstract class I18nProvider { 5 | const I18nProvider._(); 6 | 7 | String getTitleText(Options options); 8 | 9 | String getSureText(Options options, int currentCount); 10 | 11 | String getPreviewText(Options options, SelectedProvider selectedProvider); 12 | 13 | String getSelectedOptionsText(Options options); 14 | 15 | String getMaxTipText(Options options); 16 | 17 | String getAllGalleryText(Options options); 18 | 19 | String loadingText() { 20 | return "Loading..."; 21 | } 22 | 23 | I18NPermissionProvider getNotPermissionText(Options options); 24 | 25 | static const I18nProvider chinese = CNProvider(); 26 | 27 | static const I18nProvider english = ENProvider(); 28 | 29 | static const I18nProvider german = DEProvider(); 30 | 31 | String getNoSelectedText(Options options) { 32 | return 'Select Folder'; 33 | } 34 | } 35 | 36 | class CNProvider extends I18nProvider { 37 | const CNProvider() : super._(); 38 | 39 | @override 40 | String getTitleText(Options options) { 41 | return "图片选择"; 42 | } 43 | 44 | @override 45 | String getPreviewText(Options options, SelectedProvider selectedProvider) { 46 | return "预览(${selectedProvider.selectedCount})"; 47 | } 48 | 49 | @override 50 | String getSureText(Options options, int currentCount) { 51 | return "确定($currentCount/${options.maxSelected})"; 52 | } 53 | 54 | @override 55 | String getSelectedOptionsText(Options options) { 56 | return "选择"; 57 | } 58 | 59 | @override 60 | String getMaxTipText(Options options) { 61 | return "您已经选择了${options.maxSelected}张图片"; 62 | } 63 | 64 | @override 65 | String getAllGalleryText(Options options) { 66 | return "全部"; 67 | } 68 | 69 | @override 70 | String loadingText() { 71 | return "加载中..."; 72 | } 73 | 74 | @override 75 | String getNoSelectedText(Options options) { 76 | return getAllGalleryText(options); 77 | } 78 | 79 | @override 80 | I18NPermissionProvider getNotPermissionText(Options options) { 81 | return I18NPermissionProvider( 82 | cancelText: "取消", sureText: "去开启", titleText: "没有访问相册的权限"); 83 | } 84 | } 85 | 86 | class ENProvider extends I18nProvider { 87 | const ENProvider() : super._(); 88 | 89 | @override 90 | String getTitleText(Options options) { 91 | return "Image Picker"; 92 | } 93 | 94 | @override 95 | String getPreviewText(Options options, SelectedProvider selectedProvider) { 96 | return "Preview (${selectedProvider.selectedCount})"; 97 | } 98 | 99 | @override 100 | String getSureText(Options options, int currentCount) { 101 | return "Save ($currentCount/${options.maxSelected})"; 102 | } 103 | 104 | @override 105 | String getSelectedOptionsText(Options options) { 106 | return "Selected"; 107 | } 108 | 109 | @override 110 | String getMaxTipText(Options options) { 111 | return "Select ${options.maxSelected} pictures at most"; 112 | } 113 | 114 | @override 115 | String getAllGalleryText(Options options) { 116 | return "Recent"; 117 | } 118 | 119 | @override 120 | I18NPermissionProvider getNotPermissionText(Options options) { 121 | return I18NPermissionProvider( 122 | cancelText: "Cancel", 123 | sureText: "Allow", 124 | titleText: "No permission to access gallery"); 125 | } 126 | } 127 | 128 | class DEProvider extends I18nProvider { 129 | const DEProvider() : super._(); 130 | 131 | @override 132 | String getTitleText(Options options) { 133 | return "Medienauswahl"; 134 | } 135 | 136 | @override 137 | String getPreviewText(Options options, SelectedProvider selectedProvider) { 138 | return "Vorschau (${selectedProvider.selectedCount})"; 139 | } 140 | 141 | @override 142 | String getSureText(Options options, int currentCount) { 143 | return "Speichern ($currentCount/${options.maxSelected})"; 144 | } 145 | 146 | @override 147 | String getSelectedOptionsText(Options options) { 148 | return "Ausgewählt"; 149 | } 150 | 151 | @override 152 | String getMaxTipText(Options options) { 153 | return "Wählen Sie höchstens ${options.maxSelected} Medien aus"; 154 | } 155 | 156 | @override 157 | String getAllGalleryText(Options options) { 158 | return "Alle"; 159 | } 160 | 161 | String loadingText() { 162 | return "Lädt..."; 163 | } 164 | 165 | String getNoSelectedText(Options options) { 166 | return 'Ordner auswählen'; 167 | } 168 | 169 | @override 170 | I18NPermissionProvider getNotPermissionText(Options options) { 171 | return I18NPermissionProvider( 172 | cancelText: "Abbrechen", 173 | sureText: "Erlauben", 174 | titleText: "Kein Zugriff auf den Ordner"); 175 | } 176 | } 177 | 178 | abstract class I18NCustomProvider implements I18nProvider { 179 | final String maxTipText; 180 | final String previewText; 181 | final String selectedOptionsText; 182 | final String sureText; 183 | final String titleText; 184 | final I18NPermissionProvider notPermissionText; 185 | 186 | I18NCustomProvider( 187 | this.maxTipText, 188 | this.previewText, 189 | this.selectedOptionsText, 190 | this.sureText, 191 | this.titleText, 192 | this.notPermissionText); 193 | 194 | @override 195 | String getMaxTipText(Options options) { 196 | return maxTipText; 197 | } 198 | 199 | @override 200 | String getSelectedOptionsText(Options options) { 201 | return selectedOptionsText; 202 | } 203 | 204 | @override 205 | String getTitleText(Options options) { 206 | return titleText; 207 | } 208 | 209 | @override 210 | I18NPermissionProvider getNotPermissionText(Options options) { 211 | return notPermissionText; 212 | } 213 | } 214 | 215 | class I18NPermissionProvider { 216 | final String titleText; 217 | final String sureText; 218 | final String cancelText; 219 | 220 | const I18NPermissionProvider( 221 | {this.titleText, this.sureText, this.cancelText}); 222 | } 223 | -------------------------------------------------------------------------------- /lib/src/provider/selected_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:photo_manager/photo_manager.dart'; 4 | 5 | abstract class SelectedProvider { 6 | List selectedList = []; 7 | 8 | int get selectedCount => selectedList.length; 9 | 10 | bool containsEntity(AssetEntity entity) { 11 | return selectedList.contains(entity); 12 | } 13 | 14 | int indexOfSelected(AssetEntity entity) { 15 | return selectedList.indexOf(entity); 16 | } 17 | 18 | bool isUpperLimit(); 19 | 20 | bool addSelectEntity(AssetEntity entity) { 21 | if (containsEntity(entity)) { 22 | return false; 23 | } 24 | if (isUpperLimit() == true) { 25 | return false; 26 | } 27 | selectedList.add(entity); 28 | return true; 29 | } 30 | 31 | bool removeSelectEntity(AssetEntity entity) { 32 | return selectedList.remove(entity); 33 | } 34 | 35 | void compareAndRemoveEntities(List previewSelectedList) { 36 | var srcList = List.of(selectedList); 37 | selectedList.clear(); 38 | srcList.forEach((entity) { 39 | if (previewSelectedList.contains(entity)) { 40 | selectedList.add(entity); 41 | } 42 | }); 43 | } 44 | 45 | void sure(); 46 | 47 | Future checkPickImageEntity() async { 48 | List notExistsList = []; 49 | for (var entity in selectedList) { 50 | var exists = await entity.exists; 51 | if (!exists) { 52 | notExistsList.add(entity); 53 | } 54 | } 55 | 56 | selectedList.removeWhere((e) { 57 | return notExistsList.contains(e); 58 | }); 59 | } 60 | 61 | addPickedAsset(List list) { 62 | for (final entity in list) { 63 | addSelectEntity(entity); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/ui/dialog/change_gallery_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo/src/entity/options.dart'; 3 | import 'package:photo/src/provider/i18n_provider.dart'; 4 | import 'package:photo_manager/photo_manager.dart'; 5 | 6 | class ChangeGalleryDialog extends StatefulWidget { 7 | final List galleryList; 8 | final I18nProvider i18n; 9 | final Options options; 10 | 11 | const ChangeGalleryDialog({ 12 | Key key, 13 | this.galleryList, 14 | this.i18n, 15 | this.options, 16 | }) : super(key: key); 17 | 18 | @override 19 | _ChangeGalleryDialogState createState() => _ChangeGalleryDialogState(); 20 | } 21 | 22 | class _ChangeGalleryDialogState extends State { 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | child: ListView.builder( 27 | itemBuilder: _buildItem, 28 | itemCount: widget.galleryList.length, 29 | ), 30 | ); 31 | } 32 | 33 | Widget _buildItem(BuildContext context, int index) { 34 | var entity = widget.galleryList[index]; 35 | String text; 36 | 37 | if (entity.isAll) { 38 | text = widget.i18n?.getAllGalleryText(widget.options); 39 | } 40 | 41 | text = text ?? entity.name; 42 | 43 | return FlatButton( 44 | child: ListTile( 45 | title: Text("$text (${entity.assetCount})"), 46 | ), 47 | onPressed: () { 48 | Navigator.pop(context, entity); 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/ui/dialog/not_permission_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:photo/src/provider/i18n_provider.dart'; 3 | 4 | class NotPermissionDialog extends StatefulWidget { 5 | final I18NPermissionProvider provider; 6 | 7 | const NotPermissionDialog(this.provider); 8 | 9 | @override 10 | _NotPermissionDialogState createState() => _NotPermissionDialogState(); 11 | } 12 | 13 | class _NotPermissionDialogState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | var provider = widget.provider; 17 | return AlertDialog( 18 | title: Text(provider.titleText), 19 | actions: [ 20 | FlatButton( 21 | onPressed: _onCancel, 22 | child: Text(provider.cancelText), 23 | ), 24 | FlatButton( 25 | onPressed: _onSure, 26 | child: Text(provider.sureText), 27 | ), 28 | ], 29 | ); 30 | } 31 | 32 | void _onCancel() { 33 | Navigator.pop(context); 34 | } 35 | 36 | void _onSure() { 37 | Navigator.pop(context, true); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/ui/page/main/bottom_widget.dart: -------------------------------------------------------------------------------- 1 | part of '../photo_main_page.dart'; 2 | 3 | class _BottomWidget extends StatefulWidget { 4 | final ValueChanged onGalleryChange; 5 | 6 | final Options options; 7 | 8 | final I18nProvider provider; 9 | 10 | final SelectedProvider selectedProvider; 11 | 12 | final String galleryName; 13 | 14 | final GalleryListProvider galleryListProvider; 15 | final VoidCallback onTapPreview; 16 | 17 | const _BottomWidget({ 18 | Key key, 19 | this.onGalleryChange, 20 | this.options, 21 | this.provider, 22 | this.selectedProvider, 23 | this.galleryName = "", 24 | this.galleryListProvider, 25 | this.onTapPreview, 26 | }) : super(key: key); 27 | 28 | @override 29 | __BottomWidgetState createState() => __BottomWidgetState(); 30 | } 31 | 32 | class __BottomWidgetState extends State<_BottomWidget> { 33 | Options get options => widget.options; 34 | 35 | I18nProvider get i18nProvider => widget.provider; 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | var textStyle = TextStyle(fontSize: 14.0); 40 | const textPadding = const EdgeInsets.symmetric(horizontal: 16.0); 41 | return Container( 42 | color: options.themeColor, 43 | child: SafeArea( 44 | bottom: true, 45 | top: false, 46 | child: Container( 47 | height: 52.0, 48 | child: Row( 49 | children: [ 50 | FlatButton( 51 | onPressed: _showGallerySelectDialog, 52 | splashColor: Colors.transparent, 53 | child: Container( 54 | alignment: Alignment.center, 55 | height: 44.0, 56 | padding: textPadding, 57 | child: Text( 58 | widget.galleryName, 59 | style: textStyle.copyWith(color: options.textColor), 60 | ), 61 | ), 62 | ), 63 | Expanded( 64 | child: Container(), 65 | ), 66 | FlatButton( 67 | onPressed: widget.onTapPreview, 68 | textColor: options.textColor, 69 | splashColor: Colors.transparent, 70 | disabledTextColor: options.disableColor, 71 | child: Container( 72 | height: 44.0, 73 | alignment: Alignment.center, 74 | child: Text( 75 | i18nProvider.getPreviewText( 76 | options, widget.selectedProvider), 77 | style: textStyle, 78 | ), 79 | padding: textPadding, 80 | ), 81 | ), 82 | ], 83 | ), 84 | ), 85 | ), 86 | ); 87 | } 88 | 89 | void _showGallerySelectDialog() async { 90 | var result = await showModalBottomSheet( 91 | context: context, 92 | builder: (ctx) => ChangeGalleryDialog( 93 | galleryList: widget.galleryListProvider.galleryPathList, 94 | i18n: i18nProvider, 95 | options: options, 96 | ), 97 | ); 98 | 99 | if (result != null) widget.onGalleryChange?.call(result); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/ui/page/main/image_item.dart: -------------------------------------------------------------------------------- 1 | part of '../photo_main_page.dart'; 2 | 3 | class ImageItem extends StatelessWidget { 4 | final AssetEntity entity; 5 | 6 | final Color themeColor; 7 | 8 | final int size; 9 | 10 | final LoadingDelegate loadingDelegate; 11 | 12 | final BadgeDelegate badgeDelegate; 13 | 14 | const ImageItem({ 15 | Key key, 16 | this.entity, 17 | this.themeColor, 18 | this.size = 64, 19 | this.loadingDelegate, 20 | this.badgeDelegate, 21 | }) : super(key: key); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var thumb = ImageLruCache.getData(entity, size); 26 | if (thumb != null) { 27 | return _buildImageItem(context, thumb); 28 | } 29 | 30 | return FutureBuilder( 31 | future: entity.thumbDataWithSize(size, size), 32 | builder: (BuildContext context, AsyncSnapshot snapshot) { 33 | var futureData = snapshot.data; 34 | if (snapshot.connectionState == ConnectionState.done && 35 | futureData != null) { 36 | ImageLruCache.setData(entity, size, futureData); 37 | return _buildImageItem(context, futureData); 38 | } 39 | return Center( 40 | child: loadingDelegate.buildPreviewLoading( 41 | context, 42 | entity, 43 | themeColor, 44 | ), 45 | ); 46 | }, 47 | ); 48 | } 49 | 50 | Widget _buildImageItem(BuildContext context, Uint8List data) { 51 | var image = Image.memory( 52 | data, 53 | width: double.infinity, 54 | height: double.infinity, 55 | fit: BoxFit.cover, 56 | ); 57 | var badge; 58 | final badgeBuilder = 59 | badgeDelegate?.buildBadge(context, entity.type, entity.videoDuration); 60 | if (badgeBuilder == null) { 61 | badge = Container(); 62 | } else { 63 | badge = badgeBuilder; 64 | } 65 | 66 | return Stack( 67 | children: [ 68 | image, 69 | IgnorePointer( 70 | child: badge, 71 | ), 72 | ], 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/ui/page/photo_main_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:photo/src/delegate/badge_delegate.dart'; 6 | import 'package:photo/src/delegate/loading_delegate.dart'; 7 | import 'package:photo/src/engine/lru_cache.dart'; 8 | import 'package:photo/src/engine/throttle.dart'; 9 | import 'package:photo/src/entity/options.dart'; 10 | import 'package:photo/src/provider/asset_provider.dart'; 11 | import 'package:photo/src/provider/config_provider.dart'; 12 | import 'package:photo/src/provider/gallery_list_provider.dart'; 13 | import 'package:photo/src/provider/i18n_provider.dart'; 14 | import 'package:photo/src/provider/selected_provider.dart'; 15 | import 'package:photo/src/ui/dialog/change_gallery_dialog.dart'; 16 | import 'package:photo/src/ui/page/photo_preview_page.dart'; 17 | import 'package:photo_manager/photo_manager.dart'; 18 | 19 | part './main/bottom_widget.dart'; 20 | part './main/image_item.dart'; 21 | 22 | class PhotoMainPage extends StatefulWidget { 23 | final ValueChanged> onClose; 24 | final Options options; 25 | final List photoList; 26 | 27 | const PhotoMainPage({ 28 | Key key, 29 | this.onClose, 30 | this.options, 31 | this.photoList, 32 | }) : super(key: key); 33 | 34 | @override 35 | _PhotoMainPageState createState() => _PhotoMainPageState(); 36 | } 37 | 38 | class _PhotoMainPageState extends State 39 | with SelectedProvider, GalleryListProvider { 40 | Options get options => widget.options; 41 | 42 | I18nProvider get i18nProvider => PhotoPickerProvider.of(context).provider; 43 | AssetProvider get assetProvider => 44 | PhotoPickerProvider.of(context).assetProvider; 45 | 46 | List get list => assetProvider.data; 47 | 48 | Color get themeColor => options.themeColor; 49 | 50 | AssetPathEntity _currentPath; 51 | 52 | bool _isInit = false; 53 | 54 | AssetPathEntity get currentPath { 55 | if (_currentPath == null) { 56 | return null; 57 | } 58 | return _currentPath; 59 | } 60 | 61 | set currentPath(AssetPathEntity value) { 62 | _currentPath = value; 63 | } 64 | 65 | String get currentGalleryName { 66 | if (currentPath?.isAll == true) { 67 | return i18nProvider.getAllGalleryText(options); 68 | } else if (currentPath == null) { 69 | return i18nProvider.getNoSelectedText(options); 70 | } 71 | return currentPath?.name ?? "Select Folder"; 72 | } 73 | 74 | GlobalKey scaffoldKey; 75 | ScrollController scrollController; 76 | 77 | bool isPushed = false; 78 | 79 | bool get useAlbum => widget.photoList == null || widget.photoList.isEmpty; 80 | 81 | Throttle _changeThrottle; 82 | 83 | @override 84 | void initState() { 85 | super.initState(); 86 | scaffoldKey = GlobalKey(); 87 | scrollController = ScrollController(); 88 | _changeThrottle = Throttle(onCall: _onAssetChange); 89 | PhotoManager.addChangeCallback(_changeThrottle.call); 90 | PhotoManager.startChangeNotify(); 91 | } 92 | 93 | @override 94 | void didChangeDependencies() { 95 | super.didChangeDependencies(); 96 | if (!_isInit) { 97 | final pickedList = PhotoPickerProvider.of(context).pickedAssetList ?? []; 98 | addPickedAsset(pickedList.toList()); 99 | _refreshList(); 100 | } 101 | } 102 | 103 | @override 104 | void dispose() { 105 | PhotoManager.removeChangeCallback(_changeThrottle.call); 106 | PhotoManager.stopChangeNotify(); 107 | _changeThrottle.dispose(); 108 | scaffoldKey = null; 109 | super.dispose(); 110 | } 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | var textStyle = TextStyle( 115 | color: options.textColor, 116 | fontSize: 14.0, 117 | ); 118 | return Theme( 119 | data: Theme.of(context).copyWith(primaryColor: options.themeColor), 120 | child: DefaultTextStyle( 121 | style: textStyle, 122 | child: Scaffold( 123 | appBar: AppBar( 124 | leading: IconButton( 125 | icon: Icon( 126 | Icons.close, 127 | color: options.textColor, 128 | ), 129 | onPressed: _cancel, 130 | ), 131 | title: Text( 132 | i18nProvider.getTitleText(options), 133 | style: TextStyle( 134 | color: options.textColor, 135 | ), 136 | ), 137 | actions: [ 138 | FlatButton( 139 | splashColor: Colors.transparent, 140 | child: Text( 141 | i18nProvider.getSureText(options, selectedCount), 142 | style: selectedCount == 0 143 | ? textStyle.copyWith(color: options.disableColor) 144 | : textStyle, 145 | ), 146 | onPressed: selectedCount == 0 ? null : sure, 147 | ), 148 | ], 149 | ), 150 | body: _buildBody(), 151 | bottomNavigationBar: _BottomWidget( 152 | key: scaffoldKey, 153 | provider: i18nProvider, 154 | options: options, 155 | galleryName: currentGalleryName, 156 | onGalleryChange: _onGalleryChange, 157 | onTapPreview: selectedList.isEmpty ? null : _onTapPreview, 158 | selectedProvider: this, 159 | galleryListProvider: this, 160 | ), 161 | ), 162 | ), 163 | ); 164 | } 165 | 166 | void _cancel() { 167 | selectedList.clear(); 168 | widget.onClose(selectedList); 169 | } 170 | 171 | @override 172 | bool isUpperLimit() { 173 | var result = selectedCount == options.maxSelected; 174 | if (result) _showTip(i18nProvider.getMaxTipText(options)); 175 | return result; 176 | } 177 | 178 | void sure() { 179 | widget.onClose?.call(selectedList); 180 | } 181 | 182 | void _showTip(String msg) { 183 | if (isPushed) { 184 | return; 185 | } 186 | Scaffold.of(scaffoldKey.currentContext).showSnackBar( 187 | SnackBar( 188 | content: Text( 189 | msg, 190 | style: TextStyle( 191 | color: options.textColor, 192 | fontSize: 14.0, 193 | ), 194 | ), 195 | duration: Duration(milliseconds: 1500), 196 | backgroundColor: themeColor.withOpacity(0.7), 197 | ), 198 | ); 199 | } 200 | 201 | void _refreshList() async { 202 | await Future.delayed(Duration.zero); 203 | if (!useAlbum) { 204 | _refreshListFromWidget(); 205 | return; 206 | } 207 | 208 | _refreshListFromGallery(); 209 | } 210 | 211 | Future _refreshListFromWidget() async { 212 | _onRefreshAssetPathList(widget.photoList); 213 | } 214 | 215 | Future _refreshListFromGallery() async { 216 | List pathList; 217 | switch (options.pickType) { 218 | case PickType.onlyImage: 219 | pathList = await PhotoManager.getAssetPathList(type: RequestType.image); 220 | break; 221 | case PickType.onlyVideo: 222 | pathList = await PhotoManager.getAssetPathList(type: RequestType.video); 223 | break; 224 | default: 225 | pathList = await PhotoManager.getAssetPathList( 226 | type: RequestType.image | RequestType.video); 227 | } 228 | 229 | _onRefreshAssetPathList(pathList); 230 | } 231 | 232 | Future _onRefreshAssetPathList(List pathList) async { 233 | if (pathList == null) { 234 | return; 235 | } 236 | 237 | options.sortDelegate.sort(pathList); 238 | 239 | galleryPathList.clear(); 240 | galleryPathList.addAll(pathList); 241 | 242 | if (pathList.isNotEmpty) { 243 | assetProvider.current = pathList[0]; 244 | await assetProvider.loadMore(); 245 | } 246 | 247 | for (var path in pathList) { 248 | if (path.isAll) { 249 | path.name = i18nProvider.getAllGalleryText(options); 250 | } 251 | } 252 | 253 | setState(() { 254 | _isInit = true; 255 | }); 256 | } 257 | 258 | Widget _buildBody() { 259 | if (!_isInit) { 260 | return _buildLoading(); 261 | } 262 | 263 | final noMore = assetProvider.noMore; 264 | 265 | final count = assetProvider.count + (noMore ? 0 : 1); 266 | 267 | return Container( 268 | color: options.dividerColor, 269 | child: GridView.builder( 270 | controller: scrollController, 271 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 272 | crossAxisCount: options.rowCount, 273 | childAspectRatio: options.itemRadio, 274 | crossAxisSpacing: options.padding, 275 | mainAxisSpacing: options.padding, 276 | ), 277 | itemBuilder: _buildItem, 278 | itemCount: count, 279 | ), 280 | ); 281 | } 282 | 283 | Widget _buildItem(BuildContext context, int index) { 284 | final noMore = assetProvider.noMore; 285 | if (!noMore && index == assetProvider.count) { 286 | _loadMore(); 287 | return _buildLoading(); 288 | } 289 | 290 | var data = list[index]; 291 | return RepaintBoundary( 292 | child: GestureDetector( 293 | onTap: () => _onItemClick(data, index), 294 | child: Stack( 295 | children: [ 296 | ImageItem( 297 | entity: data, 298 | themeColor: themeColor, 299 | size: options.thumbSize, 300 | loadingDelegate: options.loadingDelegate, 301 | badgeDelegate: options.badgeDelegate, 302 | ), 303 | _buildMask(containsEntity(data)), 304 | _buildSelected(data), 305 | ], 306 | ), 307 | ), 308 | ); 309 | } 310 | 311 | _loadMore() async { 312 | await assetProvider.loadMore(); 313 | setState(() {}); 314 | } 315 | 316 | _buildMask(bool showMask) { 317 | return IgnorePointer( 318 | child: AnimatedContainer( 319 | color: showMask ? Colors.black.withOpacity(0.5) : Colors.transparent, 320 | duration: Duration(milliseconds: 300), 321 | ), 322 | ); 323 | } 324 | 325 | Widget _buildSelected(AssetEntity entity) { 326 | var currentSelected = containsEntity(entity); 327 | return Positioned( 328 | right: 0.0, 329 | width: 36.0, 330 | height: 36.0, 331 | child: GestureDetector( 332 | onTap: () { 333 | changeCheck(!currentSelected, entity); 334 | }, 335 | behavior: HitTestBehavior.translucent, 336 | child: _buildText(entity), 337 | ), 338 | ); 339 | } 340 | 341 | Widget _buildText(AssetEntity entity) { 342 | var isSelected = containsEntity(entity); 343 | Widget child; 344 | BoxDecoration decoration; 345 | if (isSelected) { 346 | child = Text( 347 | (indexOfSelected(entity) + 1).toString(), 348 | textAlign: TextAlign.center, 349 | style: TextStyle( 350 | fontSize: 12.0, 351 | color: options.textColor, 352 | ), 353 | ); 354 | decoration = BoxDecoration(color: themeColor); 355 | } else { 356 | decoration = BoxDecoration( 357 | borderRadius: BorderRadius.circular(1.0), 358 | border: Border.all( 359 | color: themeColor, 360 | ), 361 | ); 362 | } 363 | return Padding( 364 | padding: const EdgeInsets.all(8.0), 365 | child: AnimatedContainer( 366 | duration: Duration(milliseconds: 300), 367 | decoration: decoration, 368 | alignment: Alignment.center, 369 | child: child, 370 | ), 371 | ); 372 | } 373 | 374 | void changeCheck(bool value, AssetEntity entity) { 375 | if (value) { 376 | addSelectEntity(entity); 377 | } else { 378 | removeSelectEntity(entity); 379 | } 380 | setState(() {}); 381 | } 382 | 383 | void _onGalleryChange(AssetPathEntity assetPathEntity) async { 384 | // _currentPath = assetPathEntity; 385 | 386 | // _currentPath.assetList.then((v) async { 387 | // _sortAssetList(v); 388 | // list.clear(); 389 | // list.addAll(v); 390 | // scrollController.jumpTo(0.0); 391 | // await checkPickImageEntity(); 392 | // setState(() {}); 393 | // }); 394 | if (assetPathEntity != assetProvider.current) { 395 | assetProvider.current = assetPathEntity; 396 | await assetProvider.loadMore(); 397 | setState(() {}); 398 | } 399 | } 400 | 401 | void _onItemClick(AssetEntity data, int index) { 402 | var result = PhotoPreviewResult(); 403 | isPushed = true; 404 | Navigator.of(context).push( 405 | MaterialPageRoute( 406 | builder: (ctx) { 407 | return PhotoPickerProvider( 408 | provider: PhotoPickerProvider.of(context).provider, 409 | options: options, 410 | child: PhotoPreviewPage( 411 | selectedProvider: this, 412 | list: List.of(list), 413 | initIndex: index, 414 | changeProviderOnCheckChange: true, 415 | result: result, 416 | isPreview: false, 417 | assetProvider: assetProvider, 418 | ), 419 | ); 420 | }, 421 | ), 422 | ).then((v) { 423 | if (handlePreviewResult(v)) { 424 | Navigator.pop(context, v); 425 | return; 426 | } 427 | isPushed = false; 428 | setState(() {}); 429 | }); 430 | } 431 | 432 | void _onTapPreview() async { 433 | var result = PhotoPreviewResult(); 434 | isPushed = true; 435 | var v = await Navigator.of(context).push( 436 | MaterialPageRoute( 437 | builder: (ctx) => PhotoPickerProvider( 438 | provider: PhotoPickerProvider.of(context).provider, 439 | options: options, 440 | child: PhotoPreviewPage( 441 | selectedProvider: this, 442 | list: List.of(selectedList), 443 | changeProviderOnCheckChange: false, 444 | result: result, 445 | isPreview: true, 446 | assetProvider: assetProvider, 447 | ), 448 | ), 449 | ), 450 | ); 451 | if (handlePreviewResult(v)) { 452 | // print(v); 453 | Navigator.pop(context, v); 454 | return; 455 | } 456 | isPushed = false; 457 | compareAndRemoveEntities(result.previewSelectedList); 458 | } 459 | 460 | bool handlePreviewResult(List v) { 461 | if (v == null) { 462 | return false; 463 | } 464 | if (v is List) { 465 | return true; 466 | } 467 | return false; 468 | } 469 | 470 | Widget _buildLoading() { 471 | return Center( 472 | child: Column( 473 | children: [ 474 | Container( 475 | width: 40.0, 476 | height: 40.0, 477 | padding: const EdgeInsets.all(5.0), 478 | child: CircularProgressIndicator( 479 | valueColor: AlwaysStoppedAnimation(themeColor), 480 | ), 481 | ), 482 | Padding( 483 | padding: const EdgeInsets.all(8.0), 484 | child: Text( 485 | i18nProvider.loadingText(), 486 | style: const TextStyle( 487 | fontSize: 12.0, 488 | ), 489 | ), 490 | ), 491 | ], 492 | crossAxisAlignment: CrossAxisAlignment.center, 493 | mainAxisAlignment: MainAxisAlignment.center, 494 | ), 495 | ); 496 | } 497 | 498 | void _onAssetChange() { 499 | if (useAlbum) { 500 | _onPhotoRefresh(); 501 | } 502 | } 503 | 504 | void _onPhotoRefresh() async { 505 | List pathList; 506 | switch (options.pickType) { 507 | case PickType.onlyImage: 508 | pathList = await PhotoManager.getAssetPathList(type: RequestType.image); 509 | break; 510 | case PickType.onlyVideo: 511 | pathList = await PhotoManager.getAssetPathList(type: RequestType.image); 512 | break; 513 | default: 514 | pathList = await PhotoManager.getAssetPathList(); 515 | } 516 | 517 | if (pathList == null) { 518 | return; 519 | } 520 | 521 | this.galleryPathList.clear(); 522 | this.galleryPathList.addAll(pathList); 523 | 524 | if (!this.galleryPathList.contains(this.currentPath)) { 525 | // current path is deleted , 当前的相册被删除, 应该提示刷新 526 | if (this.galleryPathList.length > 0) { 527 | _onGalleryChange(this.galleryPathList[0]); 528 | } 529 | return; 530 | } 531 | // Not deleted 532 | _onGalleryChange(this.currentPath); 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /lib/src/ui/page/photo_preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:photo/src/entity/options.dart'; 6 | import 'package:photo/src/provider/asset_provider.dart'; 7 | import 'package:photo/src/provider/config_provider.dart'; 8 | import 'package:photo/src/provider/selected_provider.dart'; 9 | import 'package:photo/src/ui/page/photo_main_page.dart'; 10 | import 'package:photo_manager/photo_manager.dart'; 11 | 12 | class PhotoPreviewPage extends StatefulWidget { 13 | final SelectedProvider selectedProvider; 14 | 15 | final List list; 16 | 17 | final int initIndex; 18 | 19 | /// 这个参数是控制在内部点击check后是否实时修改provider状态 20 | final bool changeProviderOnCheckChange; 21 | 22 | /// 是否通过预览进来的 23 | final bool isPreview; 24 | 25 | /// 这里封装了结果 26 | final PhotoPreviewResult result; 27 | 28 | final AssetProvider assetProvider; 29 | 30 | const PhotoPreviewPage({ 31 | Key key, 32 | @required this.selectedProvider, 33 | @required this.list, 34 | @required this.changeProviderOnCheckChange, 35 | @required this.result, 36 | @required this.assetProvider, 37 | this.initIndex = 0, 38 | this.isPreview = false, 39 | }) : super(key: key); 40 | 41 | @override 42 | _PhotoPreviewPageState createState() => _PhotoPreviewPageState(); 43 | } 44 | 45 | class _PhotoPreviewPageState extends State { 46 | PhotoPickerProvider get config => PhotoPickerProvider.of(context); 47 | AssetProvider get assetProvider => widget.assetProvider; 48 | 49 | Options get options => config.options; 50 | 51 | Color get themeColor => options.themeColor; 52 | 53 | Color get textColor => options.textColor; 54 | 55 | SelectedProvider get selectedProvider => widget.selectedProvider; 56 | 57 | List get list { 58 | if (!widget.isPreview) { 59 | return assetProvider.data; 60 | } 61 | return widget.list; 62 | } 63 | 64 | StreamController pageChangeController = StreamController.broadcast(); 65 | 66 | Stream get pageStream => pageChangeController.stream; 67 | 68 | bool get changeProviderOnCheckChange => widget.changeProviderOnCheckChange; 69 | 70 | PhotoPreviewResult get result => widget.result; 71 | 72 | /// 缩略图用的数据 73 | /// 74 | /// 用于与provider数据联动 75 | List get previewList { 76 | return selectedProvider.selectedList; 77 | } 78 | 79 | /// 选中的数据 80 | List _selectedList = []; 81 | 82 | List get selectedList { 83 | if (changeProviderOnCheckChange) { 84 | return previewList; 85 | } 86 | return _selectedList; 87 | } 88 | 89 | PageController pageController; 90 | 91 | @override 92 | void initState() { 93 | super.initState(); 94 | pageChangeController.add(0); 95 | pageController = PageController( 96 | initialPage: widget.initIndex, 97 | ); 98 | 99 | _selectedList.clear(); 100 | _selectedList.addAll(selectedProvider.selectedList); 101 | 102 | result.previewSelectedList = _selectedList; 103 | } 104 | 105 | @override 106 | void dispose() { 107 | pageChangeController.close(); 108 | super.dispose(); 109 | } 110 | 111 | @override 112 | Widget build(BuildContext context) { 113 | int totalCount = assetProvider.current.assetCount ?? 0; 114 | if (!widget.isPreview) { 115 | totalCount = assetProvider.current.assetCount; 116 | } else { 117 | totalCount = list.length; 118 | } 119 | 120 | var data = Theme.of(context); 121 | var textStyle = TextStyle( 122 | color: options.textColor, 123 | fontSize: 14.0, 124 | ); 125 | return Theme( 126 | data: data.copyWith( 127 | primaryColor: options.themeColor, 128 | ), 129 | child: Scaffold( 130 | appBar: AppBar( 131 | backgroundColor: config.options.themeColor, 132 | leading: BackButton( 133 | color: options.textColor, 134 | ), 135 | title: StreamBuilder( 136 | stream: pageStream, 137 | initialData: widget.initIndex, 138 | builder: (ctx, snap) { 139 | return Text( 140 | "${snap.data + 1}/$totalCount", 141 | style: TextStyle( 142 | color: options.textColor, 143 | ), 144 | ); 145 | }, 146 | ), 147 | actions: [ 148 | StreamBuilder( 149 | stream: pageStream, 150 | builder: (ctx, s) => FlatButton( 151 | splashColor: Colors.transparent, 152 | onPressed: selectedList.length == 0 ? null : sure, 153 | child: Text( 154 | config.provider.getSureText(options, selectedList.length), 155 | style: selectedList.length == 0 156 | ? textStyle.copyWith(color: options.disableColor) 157 | : textStyle, 158 | ), 159 | ), 160 | ), 161 | ], 162 | ), 163 | body: PageView.builder( 164 | controller: pageController, 165 | itemBuilder: _buildItem, 166 | itemCount: totalCount, 167 | onPageChanged: _onPageChanged, 168 | ), 169 | bottomSheet: _buildThumb(), 170 | bottomNavigationBar: _buildBottom(), 171 | ), 172 | ); 173 | } 174 | 175 | Widget _buildBottom() { 176 | return Container( 177 | color: themeColor, 178 | child: SafeArea( 179 | child: Container( 180 | height: 52.0, 181 | child: Row( 182 | children: [ 183 | Expanded( 184 | child: Container(), 185 | ), 186 | _buildCheckbox(), 187 | ], 188 | ), 189 | ), 190 | ), 191 | ); 192 | } 193 | 194 | Container _buildCheckbox() { 195 | return Container( 196 | constraints: BoxConstraints( 197 | maxWidth: 150.0, 198 | ), 199 | child: StreamBuilder( 200 | builder: (ctx, snapshot) { 201 | var index = snapshot.data; 202 | var data = list[index]; 203 | var checked = selectedList.contains(data); 204 | return Stack( 205 | children: [ 206 | IgnorePointer( 207 | child: _buildCheckboxContent(checked, index), 208 | ), 209 | Positioned( 210 | top: 0.0, 211 | bottom: 0.0, 212 | left: 0.0, 213 | right: 0.0, 214 | child: GestureDetector( 215 | onTap: () => _changeSelected(!checked, index), 216 | behavior: HitTestBehavior.translucent, 217 | child: Container(), 218 | ), 219 | ), 220 | ], 221 | ); 222 | }, 223 | initialData: widget.initIndex, 224 | stream: pageStream, 225 | ), 226 | ); 227 | } 228 | 229 | Widget _buildCheckboxContent(bool checked, int index) { 230 | return options.checkBoxBuilderDelegate.buildCheckBox( 231 | context, 232 | checked, 233 | index, 234 | options, 235 | config.provider, 236 | ); 237 | } 238 | 239 | void _changeSelected(bool isChecked, int index) { 240 | if (changeProviderOnCheckChange) { 241 | _onChangeProvider(isChecked, index); 242 | } else { 243 | _onCheckInOnlyPreview(isChecked, index); 244 | } 245 | } 246 | 247 | /// 仅仅修改预览时的状态,在退出时,再更新provider的顺序,这里无论添加与否不修改顺序 248 | void _onCheckInOnlyPreview(bool check, int index) { 249 | var item = list[index]; 250 | if (check) { 251 | selectedList.add(item); 252 | } else { 253 | selectedList.remove(item); 254 | } 255 | pageChangeController.add(index); 256 | } 257 | 258 | /// 直接修改预览状态,会直接移除item 259 | void _onChangeProvider(bool check, int index) { 260 | var item = list[index]; 261 | if (check) { 262 | selectedProvider.addSelectEntity(item); 263 | } else { 264 | selectedProvider.removeSelectEntity(item); 265 | } 266 | pageChangeController.add(index); 267 | } 268 | 269 | Widget _buildItem(BuildContext context, int index) { 270 | if (!widget.isPreview && index >= list.length - 5) { 271 | _loadMore(); 272 | } 273 | 274 | var data = list[index]; 275 | return BigPhotoImage( 276 | assetEntity: data, 277 | loadingWidget: _buildLoadingWidget(data), 278 | ); 279 | } 280 | 281 | Future _loadMore() async { 282 | assetProvider.loadMore(); 283 | } 284 | 285 | Widget _buildLoadingWidget(AssetEntity entity) { 286 | return options.loadingDelegate 287 | .buildBigImageLoading(context, entity, themeColor); 288 | } 289 | 290 | void _onPageChanged(int value) { 291 | pageChangeController.add(value); 292 | } 293 | 294 | Widget _buildThumb() { 295 | return StreamBuilder( 296 | builder: (ctx, snapshot) => Container( 297 | height: 80.0, 298 | child: ListView.builder( 299 | itemBuilder: _buildThumbItem, 300 | itemCount: previewList.length, 301 | scrollDirection: Axis.horizontal, 302 | ), 303 | ), 304 | stream: pageStream, 305 | ); 306 | } 307 | 308 | Widget _buildThumbItem(BuildContext context, int index) { 309 | var item = previewList[index]; 310 | return RepaintBoundary( 311 | child: GestureDetector( 312 | onTap: () => changeSelected(item, index), 313 | child: Container( 314 | width: 80.0, 315 | child: Stack( 316 | children: [ 317 | ImageItem( 318 | themeColor: themeColor, 319 | entity: item, 320 | size: options.thumbSize, 321 | loadingDelegate: options.loadingDelegate, 322 | ), 323 | IgnorePointer( 324 | child: StreamBuilder( 325 | stream: pageStream, 326 | builder: (BuildContext context, AsyncSnapshot snapshot) { 327 | if (selectedList.contains(item)) { 328 | return Container(); 329 | } 330 | return Container( 331 | color: Colors.white.withOpacity(0.5), 332 | ); 333 | }, 334 | ), 335 | ), 336 | ], 337 | ), 338 | ), 339 | ), 340 | ); 341 | } 342 | 343 | void changeSelected(AssetEntity entity, int index) { 344 | var itemIndex = list.indexOf(entity); 345 | if (itemIndex != -1) pageController.jumpToPage(itemIndex); 346 | } 347 | 348 | void sure() { 349 | Navigator.pop(context, selectedList); 350 | } 351 | } 352 | 353 | class BigPhotoImage extends StatefulWidget { 354 | final AssetEntity assetEntity; 355 | final Widget loadingWidget; 356 | 357 | const BigPhotoImage({ 358 | Key key, 359 | this.assetEntity, 360 | this.loadingWidget, 361 | }) : super(key: key); 362 | 363 | @override 364 | _BigPhotoImageState createState() => _BigPhotoImageState(); 365 | } 366 | 367 | class _BigPhotoImageState extends State 368 | with AutomaticKeepAliveClientMixin { 369 | Widget get loadingWidget { 370 | return widget.loadingWidget ?? Container(); 371 | } 372 | 373 | @override 374 | Widget build(BuildContext context) { 375 | super.build(context); 376 | var width = MediaQuery.of(context).size.width; 377 | var height = MediaQuery.of(context).size.height; 378 | return FutureBuilder( 379 | future: 380 | widget.assetEntity.thumbDataWithSize(width.floor(), height.floor()), 381 | builder: (BuildContext context, AsyncSnapshot snapshot) { 382 | var file = snapshot.data; 383 | if (snapshot.connectionState == ConnectionState.done && file != null) { 384 | print(file.length); 385 | return Image.memory( 386 | file, 387 | fit: BoxFit.contain, 388 | width: double.infinity, 389 | height: double.infinity, 390 | ); 391 | } 392 | return loadingWidget; 393 | }, 394 | ); 395 | } 396 | 397 | @override 398 | bool get wantKeepAlive => true; 399 | } 400 | 401 | class PhotoPreviewResult { 402 | List previewSelectedList = []; 403 | } 404 | -------------------------------------------------------------------------------- /lib/src/ui/photo_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:photo_manager/photo_manager.dart'; 4 | 5 | import 'package:photo/src/entity/options.dart'; 6 | import 'package:photo/src/provider/config_provider.dart'; 7 | import 'package:photo/src/provider/i18n_provider.dart'; 8 | import 'package:photo/src/ui/page/photo_main_page.dart'; 9 | 10 | class PhotoApp extends StatelessWidget { 11 | final Options options; 12 | final I18nProvider provider; 13 | final List photoList; 14 | final List pickedAssetList; 15 | const PhotoApp({ 16 | Key key, 17 | this.options, 18 | this.provider, 19 | this.photoList, 20 | this.pickedAssetList, 21 | }) : super(key: key); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final pickerProvider = PhotoPickerProvider( 26 | provider: provider, 27 | options: options, 28 | pickedAssetList: pickedAssetList, 29 | child: PhotoMainPage( 30 | onClose: (List value) { 31 | Navigator.pop(context, value); 32 | }, 33 | options: options, 34 | photoList: photoList, 35 | ), 36 | ); 37 | 38 | return pickerProvider; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/ui/widget/check_box_copy.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart' hide Checkbox; 4 | import 'package:flutter/rendering.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// A material design checkbox. 8 | /// 9 | /// The checkbox itself does not maintain any state. Instead, when the state of 10 | /// the checkbox changes, the widget calls the [onChanged] callback. Most 11 | /// widgets that use a checkbox will listen for the [onChanged] callback and 12 | /// rebuild the checkbox with a new [value] to update the visual appearance of 13 | /// the checkbox. 14 | /// 15 | /// The checkbox can optionally display three values - true, false, and null - 16 | /// if [tristate] is true. When [value] is null a dash is displayed. By default 17 | /// [tristate] is false and the checkbox's [value] must be true or false. 18 | /// 19 | /// Requires one of its ancestors to be a [Material] widget. 20 | /// 21 | /// See also: 22 | /// 23 | /// * [CheckboxListTile], which combines this widget with a [ListTile] so that 24 | /// you can give the checkbox a label. 25 | /// * [Switch], a widget with semantics similar to [Checkbox]. 26 | /// * [Radio], for selecting among a set of explicit values. 27 | /// * [Slider], for selecting a value in a range. 28 | /// * 29 | /// * 30 | class Checkbox extends StatefulWidget { 31 | /// Creates a material design checkbox. 32 | /// 33 | /// The checkbox itself does not maintain any state. Instead, when the state of 34 | /// the checkbox changes, the widget calls the [onChanged] callback. Most 35 | /// widgets that use a checkbox will listen for the [onChanged] callback and 36 | /// rebuild the checkbox with a new [value] to update the visual appearance of 37 | /// the checkbox. 38 | /// 39 | /// The following arguments are required: 40 | /// 41 | /// * [value], which determines whether the checkbox is checked. The [value] 42 | /// can only be null if [tristate] is true. 43 | /// * [onChanged], which is called when the value of the checkbox should 44 | /// change. It can be set to null to disable the checkbox. 45 | /// 46 | /// The value of [tristate] must not be null. 47 | const Checkbox({ 48 | Key key, 49 | @required this.value, 50 | this.tristate = false, 51 | @required this.onChanged, 52 | this.activeColor, 53 | this.checkColor, 54 | this.materialTapTargetSize, 55 | }) : assert(tristate != null), 56 | assert(tristate || value != null), 57 | super(key: key); 58 | 59 | /// Whether this checkbox is checked. 60 | /// 61 | /// This property must not be null. 62 | final bool value; 63 | 64 | /// Called when the value of the checkbox should change. 65 | /// 66 | /// The checkbox passes the new value to the callback but does not actually 67 | /// change state until the parent widget rebuilds the checkbox with the new 68 | /// value. 69 | /// 70 | /// If this callback is null, the checkbox will be displayed as disabled 71 | /// and will not respond to input gestures. 72 | /// 73 | /// When the checkbox is tapped, if [tristate] is false (the default) then 74 | /// the [onChanged] callback will be applied to `!value`. If [tristate] is 75 | /// true this callback cycle from false to true to null. 76 | /// 77 | /// The callback provided to [onChanged] should update the state of the parent 78 | /// [StatefulWidget] using the [State.setState] method, so that the parent 79 | /// gets rebuilt; for example: 80 | /// 81 | /// ```dart 82 | /// Checkbox( 83 | /// value: _throwShotAway, 84 | /// onChanged: (bool newValue) { 85 | /// setState(() { 86 | /// _throwShotAway = newValue; 87 | /// }); 88 | /// }, 89 | /// ) 90 | /// ``` 91 | final ValueChanged onChanged; 92 | 93 | /// The color to use when this checkbox is checked. 94 | /// 95 | /// Defaults to [ThemeData.toggleableActiveColor]. 96 | final Color activeColor; 97 | 98 | final Color checkColor; 99 | 100 | /// If true the checkbox's [value] can be true, false, or null. 101 | /// 102 | /// Checkbox displays a dash when its value is null. 103 | /// 104 | /// When a tri-state checkbox is tapped its [onChanged] callback will be 105 | /// applied to true if the current value is null or false, false otherwise. 106 | /// Typically tri-state checkboxes are disabled (the onChanged callback is 107 | /// null) so they don't respond to taps. 108 | /// 109 | /// If tristate is false (the default), [value] must not be null. 110 | final bool tristate; 111 | 112 | /// Configures the minimum size of the tap target. 113 | /// 114 | /// Defaults to [ThemeData.materialTapTargetSize]. 115 | /// 116 | /// See also: 117 | /// 118 | /// * [MaterialTapTargetSize], for a description of how this affects tap targets. 119 | final MaterialTapTargetSize materialTapTargetSize; 120 | 121 | /// The width of a checkbox widget. 122 | static const double width = 18.0; 123 | 124 | @override 125 | _CheckboxState createState() => _CheckboxState(); 126 | } 127 | 128 | class _CheckboxState extends State with TickerProviderStateMixin { 129 | @override 130 | Widget build(BuildContext context) { 131 | assert(debugCheckHasMaterial(context)); 132 | final ThemeData themeData = Theme.of(context); 133 | Size size; 134 | switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { 135 | case MaterialTapTargetSize.padded: 136 | size = const Size( 137 | 2 * kRadialReactionRadius + 8.0, 2 * kRadialReactionRadius + 8.0); 138 | break; 139 | case MaterialTapTargetSize.shrinkWrap: 140 | size = const Size(2 * kRadialReactionRadius, 2 * kRadialReactionRadius); 141 | break; 142 | } 143 | final BoxConstraints additionalConstraints = BoxConstraints.tight(size); 144 | return _CheckboxRenderObjectWidget( 145 | value: widget.value, 146 | tristate: widget.tristate, 147 | activeColor: widget.activeColor ?? themeData.toggleableActiveColor, 148 | checkColor: widget.checkColor ?? themeData.primaryColor, 149 | inactiveColor: widget.onChanged != null 150 | ? themeData.unselectedWidgetColor 151 | : themeData.disabledColor, 152 | onChanged: widget.onChanged, 153 | additionalConstraints: additionalConstraints, 154 | vsync: this, 155 | ); 156 | } 157 | } 158 | 159 | class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget { 160 | const _CheckboxRenderObjectWidget({ 161 | Key key, 162 | @required this.value, 163 | @required this.tristate, 164 | @required this.activeColor, 165 | @required this.inactiveColor, 166 | @required this.checkColor, 167 | @required this.onChanged, 168 | @required this.vsync, 169 | @required this.additionalConstraints, 170 | }) : assert(tristate != null), 171 | assert(tristate || value != null), 172 | assert(activeColor != null), 173 | assert(inactiveColor != null), 174 | assert(vsync != null), 175 | super(key: key); 176 | 177 | final bool value; 178 | final bool tristate; 179 | final Color activeColor; 180 | final Color checkColor; 181 | final Color inactiveColor; 182 | final ValueChanged onChanged; 183 | final TickerProvider vsync; 184 | final BoxConstraints additionalConstraints; 185 | 186 | @override 187 | _RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox( 188 | value: value, 189 | tristate: tristate, 190 | activeColor: activeColor, 191 | checkColor: checkColor, 192 | inactiveColor: inactiveColor, 193 | onChanged: onChanged, 194 | vsync: vsync, 195 | additionalConstraints: additionalConstraints, 196 | ); 197 | 198 | @override 199 | void updateRenderObject(BuildContext context, _RenderCheckbox renderObject) { 200 | renderObject 201 | ..value = value 202 | ..tristate = tristate 203 | ..activeColor = activeColor 204 | ..inactiveColor = inactiveColor 205 | ..checkColor = checkColor 206 | ..onChanged = onChanged 207 | ..additionalConstraints = additionalConstraints 208 | ..vsync = vsync; 209 | } 210 | } 211 | 212 | const double _kEdgeSize = Checkbox.width; 213 | const Radius _kEdgeRadius = Radius.circular(1.0); 214 | const double _kStrokeWidth = 2.0; 215 | 216 | class _RenderCheckbox extends RenderToggleable { 217 | _RenderCheckbox({ 218 | bool value, 219 | bool tristate, 220 | Color activeColor, 221 | Color checkColor, 222 | Color inactiveColor, 223 | BoxConstraints additionalConstraints, 224 | ValueChanged onChanged, 225 | @required TickerProvider vsync, 226 | }) : _oldValue = value, 227 | checkColor = checkColor, 228 | super( 229 | value: value, 230 | tristate: tristate, 231 | activeColor: activeColor, 232 | inactiveColor: inactiveColor, 233 | onChanged: onChanged, 234 | additionalConstraints: additionalConstraints, 235 | vsync: vsync, 236 | ); 237 | 238 | bool _oldValue; 239 | 240 | Color checkColor; 241 | 242 | @override 243 | set value(bool newValue) { 244 | if (newValue == value) return; 245 | _oldValue = value; 246 | super.value = newValue; 247 | } 248 | 249 | @override 250 | void describeSemanticsConfiguration(SemanticsConfiguration config) { 251 | super.describeSemanticsConfiguration(config); 252 | config.isChecked = value == true; 253 | } 254 | 255 | // The square outer bounds of the checkbox at t, with the specified origin. 256 | // At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width) 257 | // At t == 0.5, .. is _kEdgeSize - _kStrokeWidth 258 | // At t == 1.0, .. is _kEdgeSize 259 | RRect _outerRectAt(Offset origin, double t) { 260 | final double inset = 1.0 - (t - 0.5).abs() * 2.0; 261 | final double size = _kEdgeSize - inset * _kStrokeWidth; 262 | final Rect rect = 263 | Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size); 264 | return RRect.fromRectAndRadius(rect, _kEdgeRadius); 265 | } 266 | 267 | // The checkbox's border color if value == false, or its fill color when 268 | // value == true or null. 269 | Color _colorAt(double t) { 270 | // As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor. 271 | return onChanged == null 272 | ? inactiveColor 273 | : (t >= 0.25 274 | ? activeColor 275 | : Color.lerp(inactiveColor, activeColor, t * 4.0)); 276 | } 277 | 278 | // White stroke used to paint the check and dash. 279 | void _initStrokePaint(Paint paint) { 280 | paint 281 | ..color = checkColor 282 | ..style = PaintingStyle.stroke 283 | ..strokeWidth = _kStrokeWidth; 284 | } 285 | 286 | void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) { 287 | assert(t >= 0.0 && t <= 0.5); 288 | final double size = outer.width; 289 | // As t goes from 0.0 to 1.0, gradually fill the outer RRect. 290 | final RRect inner = 291 | outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t)); 292 | canvas.drawDRRect(outer, inner, paint); 293 | } 294 | 295 | void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) { 296 | assert(t >= 0.0 && t <= 1.0); 297 | // As t goes from 0.0 to 1.0, animate the two check mark strokes from the 298 | // short side to the long side. 299 | final Path path = Path(); 300 | const Offset start = Offset(_kEdgeSize * 0.15, _kEdgeSize * 0.45); 301 | const Offset mid = Offset(_kEdgeSize * 0.4, _kEdgeSize * 0.7); 302 | const Offset end = Offset(_kEdgeSize * 0.85, _kEdgeSize * 0.25); 303 | if (t < 0.5) { 304 | final double strokeT = t * 2.0; 305 | final Offset drawMid = Offset.lerp(start, mid, strokeT); 306 | path.moveTo(origin.dx + start.dx, origin.dy + start.dy); 307 | path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy); 308 | } else { 309 | final double strokeT = (t - 0.5) * 2.0; 310 | final Offset drawEnd = Offset.lerp(mid, end, strokeT); 311 | path.moveTo(origin.dx + start.dx, origin.dy + start.dy); 312 | path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy); 313 | path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy); 314 | } 315 | canvas.drawPath(path, paint); 316 | } 317 | 318 | void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) { 319 | assert(t >= 0.0 && t <= 1.0); 320 | // As t goes from 0.0 to 1.0, animate the horizontal line from the 321 | // mid point outwards. 322 | const Offset start = Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5); 323 | const Offset mid = Offset(_kEdgeSize * 0.5, _kEdgeSize * 0.5); 324 | const Offset end = Offset(_kEdgeSize * 0.8, _kEdgeSize * 0.5); 325 | final Offset drawStart = Offset.lerp(start, mid, 1.0 - t); 326 | final Offset drawEnd = Offset.lerp(mid, end, t); 327 | canvas.drawLine(origin + drawStart, origin + drawEnd, paint); 328 | } 329 | 330 | @override 331 | void paint(PaintingContext context, Offset offset) { 332 | final Canvas canvas = context.canvas; 333 | paintRadialReaction(canvas, offset, size.center(Offset.zero)); 334 | 335 | final Offset origin = 336 | offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0); 337 | final AnimationStatus status = position.status; 338 | final double tNormalized = 339 | status == AnimationStatus.forward || status == AnimationStatus.completed 340 | ? position.value 341 | : 1.0 - position.value; 342 | 343 | // Four cases: false to null, false to true, null to false, true to false 344 | if (_oldValue == false || value == false) { 345 | final double t = value == false ? 1.0 - tNormalized : tNormalized; 346 | final RRect outer = _outerRectAt(origin, t); 347 | final Paint paint = Paint()..color = _colorAt(t); 348 | 349 | if (t <= 0.5) { 350 | _drawBorder(canvas, outer, t, paint); 351 | } else { 352 | canvas.drawRRect(outer, paint); 353 | 354 | _initStrokePaint(paint); 355 | final double tShrink = (t - 0.5) * 2.0; 356 | if (_oldValue == null) 357 | _drawDash(canvas, origin, tShrink, paint); 358 | else 359 | _drawCheck(canvas, origin, tShrink, paint); 360 | } 361 | } else { 362 | // Two cases: null to true, true to null 363 | final RRect outer = _outerRectAt(origin, 1.0); 364 | final Paint paint = Paint()..color = _colorAt(1.0); 365 | canvas.drawRRect(outer, paint); 366 | 367 | _initStrokePaint(paint); 368 | if (tNormalized <= 0.5) { 369 | final double tShrink = 1.0 - tNormalized * 2.0; 370 | if (_oldValue == true) 371 | _drawCheck(canvas, origin, tShrink, paint); 372 | else 373 | _drawDash(canvas, origin, tShrink, paint); 374 | } else { 375 | final double tExpand = (tNormalized - 0.5) * 2.0; 376 | if (value == true) 377 | _drawCheck(canvas, origin, tExpand, paint); 378 | else 379 | _drawDash(canvas, origin, tExpand, paint); 380 | } 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /lib/src/ui/widget/check_tile_copy.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 'package:flutter/material.dart' hide Checkbox, CheckboxListTile; 6 | 7 | import 'package:photo/src/ui/widget/check_box_copy.dart'; 8 | 9 | /// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label. 10 | /// 11 | /// The entire list tile is interactive: tapping anywhere in the tile toggles 12 | /// the checkbox. 13 | /// 14 | /// The [value], [onChanged], and [activeColor] properties of this widget are 15 | /// identical to the similarly-named properties on the [Checkbox] widget. 16 | /// 17 | /// The [title], [subtitle], [isThreeLine], and [dense] properties are like 18 | /// those of the same name on [ListTile]. 19 | /// 20 | /// The [selected] property on this widget is similar to the [ListTile.selected] 21 | /// property, but the color used is that described by [activeColor], if any, 22 | /// defaulting to the accent color of the current [Theme]. No effort is made to 23 | /// coordinate the [selected] state and the [value] state; to have the list tile 24 | /// appear selected when the checkbox is checked, pass the same value to both. 25 | /// 26 | /// The checkbox is shown on the right by default in left-to-right languages 27 | /// (i.e. the trailing edge). This can be changed using [controlAffinity]. The 28 | /// [secondary] widget is placed on the opposite side. This maps to the 29 | /// [ListTile.leading] and [ListTile.trailing] properties of [ListTile]. 30 | /// 31 | /// To show the [CheckboxListTile] as disabled, pass null as the [onChanged] 32 | /// callback. 33 | /// 34 | /// ## Sample code 35 | /// 36 | /// This widget shows a checkbox that, when checked, slows down all animations 37 | /// (including the animation of the checkbox itself getting checked!). 38 | /// 39 | /// ```dart 40 | /// CheckboxListTile( 41 | /// title: const Text('Animate Slowly'), 42 | /// value: timeDilation != 1.0, 43 | /// onChanged: (bool value) { 44 | /// setState(() { timeDilation = value ? 20.0 : 1.0; }); 45 | /// }, 46 | /// secondary: const Icon(Icons.hourglass_empty), 47 | /// ) 48 | /// ``` 49 | /// 50 | /// This sample requires that you also import 'package:flutter/scheduler.dart', 51 | /// so that you can reference [timeDilation]. 52 | /// 53 | /// See also: 54 | /// 55 | /// * [ListTileTheme], which can be used to affect the style of list tiles, 56 | /// including checkbox list tiles. 57 | /// * [RadioListTile], a similar widget for radio buttons. 58 | /// * [SwitchListTile], a similar widget for switches. 59 | /// * [ListTile] and [Checkbox], the widgets from which this widget is made. 60 | class CheckboxListTile extends StatelessWidget { 61 | /// Creates a combination of a list tile and a checkbox. 62 | /// 63 | /// The checkbox tile itself does not maintain any state. Instead, when the 64 | /// state of the checkbox changes, the widget calls the [onChanged] callback. 65 | /// Most widgets that use a checkbox will listen for the [onChanged] callback 66 | /// and rebuild the checkbox tile with a new [value] to update the visual 67 | /// appearance of the checkbox. 68 | /// 69 | /// The following arguments are required: 70 | /// 71 | /// * [value], which determines whether the checkbox is checked, and must not 72 | /// be null. 73 | /// 74 | /// * [onChanged], which is called when the value of the checkbox should 75 | /// change. It can be set to null to disable the checkbox. 76 | const CheckboxListTile({ 77 | Key key, 78 | @required this.value, 79 | @required this.onChanged, 80 | this.activeColor, 81 | this.checkColor, 82 | this.title, 83 | this.subtitle, 84 | this.isThreeLine = false, 85 | this.dense, 86 | this.secondary, 87 | this.selected = false, 88 | this.controlAffinity = ListTileControlAffinity.platform, 89 | }) : assert(value != null), 90 | assert(isThreeLine != null), 91 | assert(!isThreeLine || subtitle != null), 92 | assert(selected != null), 93 | assert(controlAffinity != null), 94 | super(key: key); 95 | 96 | /// Whether this checkbox is checked. 97 | /// 98 | /// This property must not be null. 99 | final bool value; 100 | 101 | /// Called when the value of the checkbox should change. 102 | /// 103 | /// The checkbox passes the new value to the callback but does not actually 104 | /// change state until the parent widget rebuilds the checkbox tile with the 105 | /// new value. 106 | /// 107 | /// If null, the checkbox will be displayed as disabled. 108 | /// 109 | /// The callback provided to [onChanged] should update the state of the parent 110 | /// [StatefulWidget] using the [State.setState] method, so that the parent 111 | /// gets rebuilt; for example: 112 | /// 113 | /// ```dart 114 | /// CheckboxListTile( 115 | /// value: _throwShotAway, 116 | /// onChanged: (bool newValue) { 117 | /// setState(() { 118 | /// _throwShotAway = newValue; 119 | /// }); 120 | /// }, 121 | /// title: Text('Throw away your shot'), 122 | /// ) 123 | /// ``` 124 | final ValueChanged onChanged; 125 | 126 | /// The color to use when this checkbox is checked. 127 | /// 128 | /// Defaults to accent color of the current [Theme]. 129 | final Color activeColor; 130 | 131 | final Color checkColor; 132 | 133 | /// The primary content of the list tile. 134 | /// 135 | /// Typically a [Text] widget. 136 | final Widget title; 137 | 138 | /// Additional content displayed below the title. 139 | /// 140 | /// Typically a [Text] widget. 141 | final Widget subtitle; 142 | 143 | /// A widget to display on the opposite side of the tile from the checkbox. 144 | /// 145 | /// Typically an [Icon] widget. 146 | final Widget secondary; 147 | 148 | /// Whether this list tile is intended to display three lines of text. 149 | /// 150 | /// If false, the list tile is treated as having one line if the subtitle is 151 | /// null and treated as having two lines if the subtitle is non-null. 152 | final bool isThreeLine; 153 | 154 | /// Whether this list tile is part of a vertically dense list. 155 | /// 156 | /// If this property is null then its value is based on [ListTileTheme.dense]. 157 | final bool dense; 158 | 159 | /// Whether to render icons and text in the [activeColor]. 160 | /// 161 | /// No effort is made to automatically coordinate the [selected] state and the 162 | /// [value] state. To have the list tile appear selected when the checkbox is 163 | /// checked, pass the same value to both. 164 | /// 165 | /// Normally, this property is left to its default value, false. 166 | final bool selected; 167 | 168 | /// Where to place the control relative to the text. 169 | final ListTileControlAffinity controlAffinity; 170 | 171 | @override 172 | Widget build(BuildContext context) { 173 | final Widget control = Checkbox( 174 | value: value, 175 | onChanged: onChanged, 176 | checkColor: checkColor, 177 | activeColor: activeColor, 178 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 179 | ); 180 | Widget leading, trailing; 181 | switch (controlAffinity) { 182 | case ListTileControlAffinity.leading: 183 | leading = control; 184 | trailing = secondary; 185 | break; 186 | case ListTileControlAffinity.trailing: 187 | case ListTileControlAffinity.platform: 188 | leading = secondary; 189 | trailing = control; 190 | break; 191 | } 192 | return MergeSemantics( 193 | child: ListTileTheme.merge( 194 | selectedColor: activeColor ?? Theme.of(context).accentColor, 195 | child: ListTile( 196 | leading: leading, 197 | title: title, 198 | subtitle: subtitle, 199 | trailing: trailing, 200 | isThreeLine: isThreeLine, 201 | dense: dense, 202 | enabled: onChanged != null, 203 | onTap: onChanged != null 204 | ? () { 205 | onChanged(!value); 206 | } 207 | : null, 208 | selected: selected, 209 | ), 210 | ), 211 | ); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.3" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | image: 71 | dependency: transitive 72 | description: 73 | name: image 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "2.1.4" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "0.12.6" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "1.1.8" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "1.6.4" 98 | pedantic: 99 | dependency: transitive 100 | description: 101 | name: pedantic 102 | url: "https://pub.flutter-io.cn" 103 | source: hosted 104 | version: "1.8.0+1" 105 | petitparser: 106 | dependency: transitive 107 | description: 108 | name: petitparser 109 | url: "https://pub.flutter-io.cn" 110 | source: hosted 111 | version: "2.4.0" 112 | photo_manager: 113 | dependency: "direct main" 114 | description: 115 | name: photo_manager 116 | url: "https://pub.flutter-io.cn" 117 | source: hosted 118 | version: "0.5.1-dev.8" 119 | quiver: 120 | dependency: transitive 121 | description: 122 | name: quiver 123 | url: "https://pub.flutter-io.cn" 124 | source: hosted 125 | version: "2.0.5" 126 | sky_engine: 127 | dependency: transitive 128 | description: flutter 129 | source: sdk 130 | version: "0.0.99" 131 | source_span: 132 | dependency: transitive 133 | description: 134 | name: source_span 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.5.5" 138 | stack_trace: 139 | dependency: transitive 140 | description: 141 | name: stack_trace 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "1.9.3" 145 | stream_channel: 146 | dependency: transitive 147 | description: 148 | name: stream_channel 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "2.0.0" 152 | string_scanner: 153 | dependency: transitive 154 | description: 155 | name: string_scanner 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "1.0.5" 159 | term_glyph: 160 | dependency: transitive 161 | description: 162 | name: term_glyph 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "1.1.0" 166 | test_api: 167 | dependency: transitive 168 | description: 169 | name: test_api 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "0.2.11" 173 | typed_data: 174 | dependency: transitive 175 | description: 176 | name: typed_data 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "1.1.6" 180 | vector_math: 181 | dependency: transitive 182 | description: 183 | name: vector_math 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "2.0.8" 187 | xml: 188 | dependency: transitive 189 | description: 190 | name: xml 191 | url: "https://pub.flutter-io.cn" 192 | source: hosted 193 | version: "3.5.0" 194 | sdks: 195 | dart: ">=2.4.0 <3.0.0" 196 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: photo 2 | description: image picker, multi picker support video/icloud asset, use flutter as ui, if you want to build custom ui, you just use photo_manager. 3 | version: 0.5.0 4 | author: caijinglong 5 | homepage: https://github.com/CaiJingLong/flutter_photo 6 | 7 | environment: 8 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | photo_manager: ^0.5.1 15 | # photo_manager: 16 | # git: 17 | # url: https://github.com/CaiJingLong/flutter_photo_manager.git 18 | # ref: ffaa6e8470708e63c3456837c8bd6eef7b0d42df 19 | # photo_manager: 20 | # path: ../flutter_photo_manager 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | 26 | # For information on the generic Dart part of this file, see the 27 | # following page: https://www.dartlang.org/tools/pub/pubspec 28 | 29 | # The following section is specific to Flutter. 30 | flutter: 31 | # To add assets to your package, add an assets section, like this: 32 | # assets: 33 | # - images/a_dot_burr.jpeg 34 | # - images/a_dot_ham.jpeg 35 | # 36 | # For details regarding assets in packages, see 37 | # https://flutter.io/assets-and-images/#from-packages 38 | # 39 | # An image asset can refer to one or more resolution-specific "variants", see 40 | # https://flutter.io/assets-and-images/#resolution-aware. 41 | # To add custom fonts to your package, add a fonts section here, 42 | # in this "flutter" section. Each entry in this list should have a 43 | # "family" key with the font family name, and a "fonts" key with a 44 | # list giving the asset and other descriptors for the font. For 45 | # example: 46 | # fonts: 47 | # - family: Schyler 48 | # fonts: 49 | # - asset: fonts/Schyler-Regular.ttf 50 | # - asset: fonts/Schyler-Italic.ttf 51 | # style: italic 52 | # - family: Trajan Pro 53 | # fonts: 54 | # - asset: fonts/TrajanPro.ttf 55 | # - asset: fonts/TrajanPro_Bold.ttf 56 | # weight: 700 57 | # 58 | # For details regarding fonts in packages, see 59 | # https://flutter.io/custom-fonts/#from-packages 60 | -------------------------------------------------------------------------------- /test/photo_test.dart: -------------------------------------------------------------------------------- 1 | //import 'package:test/test.dart'; 2 | 3 | void main() { 4 | // test('adds one to input values', () {}); 5 | } 6 | --------------------------------------------------------------------------------