├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README-CN.md ├── README.md ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ └── main.dart ├── pubspec.lock └── pubspec.yaml ├── lib ├── core │ ├── bus.dart │ ├── cell.dart │ ├── controller.dart │ ├── events.dart │ ├── store.dart │ ├── swipe_action_cell_tap_close_area.dart │ ├── swipe_action_navigator_observer.dart │ ├── swipe_data.dart │ ├── swipe_pull_align_button.dart │ └── swipe_pull_button.dart └── flutter_swipe_action_cell.dart ├── pubspec.yaml └── test └── tests.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | 77 | /pubspec.lock 78 | -------------------------------------------------------------------------------- /.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: 8af6b2f038c1172e61d418869363a28dffec3cb4 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.1.5] Fix #71 (Assertion error during selectCellAt on second list) 2 | ## [3.1.4] Add dispose to SwipeActionController #70 3 | ## [3.1.3] Fix action width when screen rotating. 适配横竖屏切换 4 | ## [3.1.2] 5 | - [Breaking change] Change normalAnimationDuration to openAnimationDuration and closeAnimationDuration. 6 | - [New] You can control animation curve by passing openAnimationCurve or closeAnimationCurve to cell. 7 | 8 | ## [3.1.1] - Fix #59 9 | ## [3.1.0] - Adjust flutter 3.7 10 | ## [3.0.7] - Fix backgroundColor. 11 | ## [3.0.6] - Fix lints. 12 | ## [3.0.5] - Fix lints. 13 | ## [3.0.4] - Add gesture settings for gestures. 14 | ## [3.0.3] - Remove bad code 15 | ## [3.0.2] - Fix issue : size not correct when change widget size and action button showing 16 | ## [3.0.1] - Add SwipeActionNavigatorObserver to close opening cell when navigator changes routes 17 | ## [3.0.0] - Support flutter 3 18 | ## [2.2.3] - Fix performance issue #44 (Avoid unnecessary rebuild all cells when scrolling). 19 | ## [2.2.2] - Ignore the null-aware operator for flutter 3 20 | ## [2.2.1] - Fix performance issue #44 (Avoid unnecessary rebuild all cells when scrolling). 21 | ## [2.2.0] - Bug fix 22 | ## [2.1.8] - Bug fix 23 | ## [2.1.7] - [Breaking change] Change SwipeActionController isEditing from bool to ValueNotifier 24 | ## [2.1.6] - Fix edit mode controller event identifier and add customized selectedForegroundColor 25 | ## [2.1.5] - Fix #40 issue 26 | ## [2.1.4] - Repair: open other controllers with the same index under multiple controllers 27 | ## [2.1.3] - Breaking change! Make performsFirstActionWithFullSwipe can control leading or trailing actions separately 28 | ## [2.1.3-beta] - Make performsFirstActionWithFullSwipe can control leading or trailing actions separately 29 | ## [2.1.2] - Simply format code 30 | ## [2.1.1] - Ignore drag gestures if no actions provided 31 | ## [2.1.0] - simple update 32 | ## [2.0.8] - let controller can open cell programmatically 33 | ## [2.0.7] - modify doc,reduce package size 34 | ## [2.0.6] - fix bug 35 | ## [2.0.5] - fix bug about using handler and make some animation's duration customizable. 36 | ## [2.0.4] - fix bug when delete cell using controller,and add tap close area widget 37 | ## [2.0.3] - cancel action的paddingToBoundary,content center automatically 38 | ## [2.0.2] - bugfix 39 | ## [2.0.1] - customize fullDrag 40 | ## [2.0.0] - bugfix 41 | ## [1.3.4] - bugfix 42 | ## [1.3.3] - bugfix 43 | ## [1.3.2] - bugfix 44 | ## [1.3.1] - bugfix 45 | ## [1.3.0] - bugfix 46 | ## [1.2.7] - bugfix 47 | ## [1.2.6] - actions nullable 48 | ## [1.2.5] - bugfix 49 | ## [1.2.4] - customizable editModeOffset 50 | ## [1.2.3] - cell can customize bg color 51 | ## [1.2.2] - add select cell call back 52 | ## [1.2.1] - modify foc 53 | ## [1.2.0] - modify doc 54 | ## [1.1.2] - modify doc 55 | ## [1.1.1] - bugfix 56 | ## [1.1.0] - bugfix 57 | ## [1.0.9] - cancel overflow 58 | ## [1.0.8] - bugfix 59 | ## [1.0.7] - controller can selectAll can unselectAll 60 | ## [1.0.6+1] - bugfix 61 | ## [1.0.6] - bugfix 62 | ## [1.0.5+9] - bugfix 63 | ## [1.0.5+8] - bugfix 64 | ## [1.0.5+7] - bugfix 65 | ## [1.0.5+6] - bugfix 66 | ## [1.0.5+5] - bugfix 67 | ## [1.0.5+4] - bugfix 68 | ## [1.0.5+4] - bugfix 69 | ## [1.0.5+2] - customizable content 70 | ## [1.0.5+1] - controller can close cell 71 | ## [1.0.5] - bugfix 72 | ## [1.0.4+7] - doc fixed 73 | ## [1.0.4+6] - doc fixed 74 | ## [1.0.4+5] - Add edit mode 75 | ## [1.0.4+5] - doc fixed 76 | ## [1.0.4+3] - doc fixed 77 | ## [1.0.4+2] - refactor code 78 | ## [1.0.4+1] - bug fixed 79 | ## [1.0.4] - add nestedAction and better performance 80 | ## [1.0.3+3] - doc fixed 81 | ## [1.0.3+2] - doc fixed 82 | ## [1.0.3+1] - bug fixed 83 | ## [1.0.3] - better performance and bug fix 84 | ## [1.0.2] - better performance 85 | ## [1.0.1+4] - modify readme 86 | ## [1.0.1+3] - add example 87 | ## [1.0.1+1] - fix 88 | ## [1.0.1] - add customizable action width 89 | ## [1.0.0] - first release -------------------------------------------------------------------------------- /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 {yyyy} {name of copyright owner} 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-CN.md: -------------------------------------------------------------------------------- 1 | # flutter_swipe_action_cell 2 | 一个强大的列表项侧滑库 3 | 4 | ### 如果你喜欢这个库,不要吝啬你的star😀,这个项目的star越多,说明这个库越受欢迎,使用的人越多,我也就会花更多的时间在这个库上。 5 | 6 | ### Language: 7 | [English](https://github.com/luckysmg/flutter_swipe_action_cell/blob/master/README.md) 8 | | [中文简体](https://github.com/luckysmg/flutter_swipe_action_cell/blob/master/README-CN.md) 9 | 10 |
11 | 12 | #### QQ交流群(估计还没什么人):892912160 13 | 14 | #### 从 3.0.0 版本开始支持flutter 3 15 | 16 | 和框架有关问题,无论是新功能探索,开发,bug提出,还是有关建议都可以发群里交流,只要我有时间,我很乐意帮助大家(^▽^) 17 | 18 | ### 捐赠: 19 | 20 | Alipay | Wechat | 21 | -------- | ----- 22 | | 23 | 24 | ## 直接进入正题: 25 | 26 | #### pub 仓库点这里: [pub](https://pub.dev/packages/flutter_swipe_action_cell) 27 | #### 安装: 28 | ```yaml 29 | flutter_swipe_action_cell: ^3.1.5 30 | ``` 31 | 32 |
33 | 34 | 35 | ### 效果预览(gif可能比较大,稍微等一下): 36 | 37 | 简单删除 | 拉满执行第一个action | 38 | -------- | ----- 39 | | 40 | 41 | 42 | 伴随动画删除 | 多于一个action的样式 | 43 | -------- | ----- 44 | | 45 | 46 | 仿微信确认删除交互 | 仿微信确认删除自动调整按钮大小 47 | -------- | -------- 48 | || 49 | 50 | 仿微信收藏页 自定义按钮形状交互 | 51 | -------- | 52 | 53 | 54 | 支持左侧按钮和右侧的按钮 | 55 | -------- | 56 | | 57 | 58 | 59 | 60 | 编辑模式 (GIF 较大) | 61 | -------- | 62 | | 63 | 64 | 65 | ### 目前已经实现的内容 : 66 | - [x] 支持左右两边拉出的按钮 67 | - [x] 拉出按钮,顺滑的拉出动画和回弹动画 68 | - [x] 支持iOS原生的弹出按钮触发第一个动作交互 69 | - [x] 支持拉出另外一个按钮时关闭已经打开的按钮 70 | - [x] 支持滚动时关闭已经打开的按钮 71 | - [x] 支持删除时的折叠动画 72 | - [x] 支持微信iOS端消息列表确认删除功能 73 | - [x] 支持自定义按钮内容,形状,颜色等。 74 | - [x] 支持仿iOS原生列表编辑模式交互,多选,全选,取消全选,获取选中项等多种操作 75 | - [x] 支持只使用编辑模式,通过设置取消侧滑参数,防止和TabView或PageView手势冲突 76 | - [x] 支持全局配置页面路由切换时关闭此时正打开的cell菜单 77 | 78 | 79 | ## 最完整的例子(几乎涵盖所有常用api): 80 | [完整效果预览 (西瓜视频)](https://v.ixigua.com/JAqWvNM/) 81 | 你可以点击 [example page](https://pub.dev/packages/flutter_swipe_action_cell/example) 来看实现full example的完整代码 82 | 83 |
84 | 85 | ### 下面分隔出来的小例子 86 | 87 | - ## Example 1:最简单的例子---删除 88 | 89 | 90 | 91 | - ##### Tip:你把下面的放在你ListView的itemBuilder里面返回就行 92 | ```dart 93 | SwipeActionCell( 94 | /// 这个key是必要的 95 | key: ValueKey(list[index]), 96 | trailingActions: [ 97 | SwipeAction( 98 | title: "delete", 99 | onTap: (CompletionHandler handler) async { 100 | list.removeAt(index); 101 | setState(() {}); 102 | }, 103 | color: Colors.red), 104 | ], 105 | child: Padding( 106 | padding: const EdgeInsets.all(8.0), 107 | child: Text("this is index of ${list[index]}", 108 | style: TextStyle(fontSize: 40)), 109 | ), 110 | ); 111 | ``` 112 | 113 | 114 | - ## Example 2:拉满将会执行第一个action 115 | 116 | 117 | 118 | ```dart 119 | SwipeActionCell( 120 | key: ValueKey(list[index]), 121 | trailingActions: [ 122 | SwipeAction( 123 | /// 参数名和iOS原生相同 124 | performsFirstActionWithFullSwipe: true, 125 | title: "delete", 126 | onTap: (CompletionHandler handler) async { 127 | list.removeAt(index); 128 | setState(() {}); 129 | }, 130 | color: Colors.red), 131 | ], 132 | child: Padding( 133 | padding: const EdgeInsets.all(8.0), 134 | child: Text("this is index of ${list[index]}", 135 | style: TextStyle(fontSize: 40)), 136 | ), 137 | ); 138 | ``` 139 | 140 | - ## Example 3:伴随动画的删除(按照iOS原生动画做的) 141 | 142 | 143 | 144 | ```dart 145 | SwipeActionCell( 146 | key: ValueKey(list[index]), 147 | trailingActions: [ 148 | SwipeAction( 149 | title: "delete", 150 | onTap: (CompletionHandler handler) async { 151 | 152 | /// await handler(true) : 代表将会删除这一行 153 | /// 在删除动画结束后,setState函数才应该被调用来同步你的数据和UI 154 | 155 | await handler(true); 156 | list.removeAt(index); 157 | setState(() {}); 158 | }, 159 | color: Colors.red), 160 | ], 161 | child: Padding( 162 | padding: const EdgeInsets.all(8.0), 163 | child: Text("this is index of ${list[index]}", 164 | style: TextStyle(fontSize: 40)), 165 | ), 166 | ); 167 | ``` 168 | 169 | - ## Example 4:多于一个action 170 | 171 | 172 | 173 | 174 | ```dart 175 | SwipeActionCell( 176 | key: ValueKey(list[index]), 177 | trailingActions: [ 178 | SwipeAction( 179 | title: "delete", 180 | onTap: (CompletionHandler handler) async { 181 | await handler(true); 182 | list.removeAt(index); 183 | setState(() {}); 184 | }, 185 | color: Colors.red), 186 | 187 | SwipeAction( 188 | widthSpace: 120, 189 | title: "popAlert", 190 | onTap: (CompletionHandler handler) async { 191 | /// false 代表他不会删除这一行,默认情况下会关闭这个action button 192 | handler(false); 193 | showCupertinoDialog( 194 | context: context, 195 | builder: (c) { 196 | return CupertinoAlertDialog( 197 | title: Text('ok'), 198 | actions: [ 199 | CupertinoDialogAction( 200 | child: Text('confirm'), 201 | isDestructiveAction: true, 202 | onPressed: () { 203 | Navigator.pop(context); 204 | }, 205 | ), 206 | ], 207 | ); 208 | }); 209 | }, 210 | color: Colors.orange), 211 | ], 212 | child: Padding( 213 | padding: const EdgeInsets.all(8.0), 214 | child: Text( 215 | "this is index of ${list[index]}", 216 | style: TextStyle(fontSize: 40)), 217 | ), 218 | ); 219 | ``` 220 | 221 | - ## Example 5:仿微信iOS端消息删除效果 222 | 223 | 224 | ```dart 225 | return SwipeActionCell( 226 | key: vValueKey(list[index]), 227 | trailingActions: [ 228 | SwipeAction( 229 | 230 | /// 这个参数只能给的第一个action设置哦 231 | nestedAction: SwipeNestedAction(title: "确认删除"), 232 | title: "删除", 233 | onTap: (CompletionHandler handler) async { 234 | await handler(true); 235 | list.removeAt(index); 236 | setState(() {}); 237 | }, 238 | color: Colors.red, 239 | ), 240 | SwipeAction( 241 | title: "置顶", 242 | onTap: (CompletionHandler handler) async { 243 | handler(false); 244 | }, 245 | color: Colors.grey), 246 | ], 247 | child: Padding( 248 | padding: const EdgeInsets.all(8.0), 249 | child: Text("this is index of ${list[index]}", 250 | style: TextStyle(fontSize: 40)), 251 | ), 252 | ); 253 | ``` 254 | 255 | 256 | 257 | - ## Example 6:编辑模式(类似iOS原生效果) 258 | 259 | 260 | ```dart 261 | /// 控制器(目前就是控制编辑的) 262 | SwipeActionEditController controller; 263 | 264 | /// 在init里面初始化 265 | @override 266 | void initState() { 267 | super.initState(); 268 | controller = SwipeActionController(); 269 | } 270 | /// 如果你想获取你选中的行,那么请调用以下API 271 | List selectedIndexes = controller.getSelectedIndexes(); 272 | 273 | /// 打开cell 274 | controller.openCellAt(index: 2, trailing: true, animated: true); 275 | 276 | /// 关闭 cell 277 | controller.closeAllOpenCell(); 278 | 279 | /// 切换编辑模式 280 | controller.toggleEditingMode() 281 | 282 | /// 开始编辑模式 283 | controller.startEditingMode() 284 | 285 | /// 停止编辑模式 286 | controller.stopEditingMode() 287 | 288 | 289 | /// 在build中传入你的列表组件,这里用常用的ListView: 290 | ListView.builder( 291 | itemBuilder: (c, index) { 292 | return _item(index); 293 | }, 294 | itemCount: list.length, 295 | ); 296 | 297 | 298 | Widget _item(int index) { 299 | return SwipeActionCell( 300 | /// 在这传入controller 301 | controller: controller, 302 | /// 这个index需要你传入,否则会报错 303 | index: index, 304 | key: ValueKey(list[index]), 305 | trailingActions: [ 306 | SwipeAction( 307 | performsFirstActionWithFullSwipe:true 308 | onTap: (handler) async { 309 | await handler(true); 310 | list.removeAt(index); 311 | setState(() {}); 312 | }, 313 | title: "删除"), 314 | ], 315 | child: Padding( 316 | padding: const EdgeInsets.all(15.0), 317 | child: Text("This is index of ${list[index]}", 318 | style: TextStyle(fontSize: 35)), 319 | ), 320 | ); 321 | } 322 | 323 | ``` 324 | 325 | 326 | - ## Example 7:仿美团iOS端订单页删除效果 327 | 328 | 329 | 330 | #### 根据gif图可以判断,删除逻辑应该是这样的: 331 | - 1.点击或者拉动到最后触发删除动作 332 | - 2.请求服务器删除,服务器返回删除成功 333 | - 3.触发删除动画,更新UI 334 | 335 | 那么对应的例子如下: 336 | 337 | ```dart 338 | Widget _item(int index) { 339 | return SwipeActionCell( 340 | key: ValueKey(list[index]), 341 | trailingActions: [ 342 | SwipeAction( 343 | icon: Icon(Icons.add), 344 | title: "delete", 345 | onTap: (CompletionHandler handler) async { 346 | 347 | /// 利用延时模拟请求网络的过程 348 | await Future.delayed(Duration(seconds: 1)); 349 | 350 | /// 准备执行删除动画,更新UI 351 | /// 可以把handler当做参数传到其他地方去调用 352 | _remove(index, handler); 353 | }, 354 | color: Colors.red), 355 | ], 356 | child: Padding( 357 | padding: const EdgeInsets.all(8.0), 358 | child: Text("this the index of ${list[index]}", 359 | style: TextStyle(fontSize: 40)), 360 | ), 361 | ); 362 | } 363 | 364 | void _remove(int index, CompletionHandler handler) async { 365 | /// 在这里删除,删除后更新UI 366 | await handler(true); 367 | list.removeAt(index); 368 | setState(() {}); 369 | } 370 | ``` 371 | 372 | - ## Example 8:仿微信ios端收藏列表效果(自定义形状按钮) 373 | 374 | 375 | 376 | ```dart 377 | 378 | Widget _item(int index) { 379 | return SwipeActionCell( 380 | key: ValueKey(list[index]), 381 | trailingActions: [ 382 | SwipeAction( 383 | nestedAction: SwipeNestedAction( 384 | 385 | /// 自定义你nestedAction 的内容 386 | content: Container( 387 | decoration: BoxDecoration( 388 | borderRadius: BorderRadius.circular(30), 389 | color: Colors.red, 390 | ), 391 | width: 130, 392 | height: 60, 393 | child: OverflowBox( 394 | maxWidth: double.infinity, 395 | child: Row( 396 | mainAxisAlignment: MainAxisAlignment.center, 397 | children: [ 398 | Icon( 399 | Icons.delete, 400 | color: Colors.white, 401 | ), 402 | Text('确认删除', 403 | style: TextStyle(color: Colors.white, fontSize: 20)), 404 | ], 405 | ), 406 | ), 407 | ), 408 | ), 409 | /// 将原本的背景设置为透明,因为要用你自己的背景 410 | color: Colors.transparent, 411 | 412 | /// 设置了content就不要设置title和icon了 413 | content: _getIconButton(Colors.red, Icons.delete), 414 | onTap: (handler) async { 415 | list.removeAt(index); 416 | setState(() {}); 417 | }), 418 | SwipeAction( 419 | content: _getIconButton(Colors.grey, Icons.vertical_align_top), 420 | color: Colors.transparent, 421 | onTap: (handler) {}), 422 | ], 423 | child: Padding( 424 | padding: const EdgeInsets.all(15.0), 425 | child: Text( 426 | "This is index of ${list[index]},Awesome Swipe Action Cell!! I like it very much!", 427 | style: TextStyle(fontSize: 25)), 428 | ), 429 | ); 430 | } 431 | 432 | Widget _getIconButton(color, icon) { 433 | return Container( 434 | width: 50, 435 | height: 50, 436 | decoration: BoxDecoration( 437 | borderRadius: BorderRadius.circular(25), 438 | 439 | /// 设置你自己的背景 440 | color: color, 441 | ), 442 | child: Icon( 443 | icon, 444 | color: Colors.white, 445 | ), 446 | ); 447 | } 448 | 449 | 450 | ``` 451 | 452 | - ## Example 9:页面切换时统一关闭打开的cell 453 | 只需要在App的路由观察者中加上一个`SwipeActionNavigatorObserver`即可 454 | ```dart 455 | return MaterialApp( 456 | navigatorObservers: [SwipeActionNavigatorObserver()], 457 | .... 458 | ); 459 | 460 | ``` 461 | 462 | 463 | ## 关于 CompletionHandler 464 | 它代表你在点击action之后如何操纵这个cell,如果你不想要任何动画,那么就不执行handler,而是直接更新你的数据,然后setState就行 465 | 466 | 如果你想要动画: 467 | - handler(true) :代表这一行将会被删除(虽然UI上看不到那一行了,但是你仍然应该更新你的数据并且setState) 468 | 469 | - await handler(true) :代表你将会等待删除动画执行完毕,你应该在这一行之后去执行setState,否则看不到动画(适合同步删除,也就是删除这个cell在业务上不需要服务器的参与) 470 | 471 | - handler(false) : 点击后内部不会有删除这一行的动作,默认地,他只会关闭这个action button 472 | 473 | - await handler(false) : 相比上面来说,他只会等待关闭动画结束 474 | 475 | # 其他参数如下: 476 | #### SwipeActionCell: 477 | 参数名 | 含义 | 是否必填 478 | -------- | --- |----- 479 | trailingActions | 这个cell下的所有右侧action|否 480 | leadingActions | 这个cell下的所有左侧action|否 481 | child| cell内容 | 是 482 | closeWhenScrolling | 滚动时关闭打开的cell|否(def=true) 483 | firstActionWillCoverAllSpaceOnDeleting|执行动画删除时是否让第一个覆盖cell|否(def=true) 484 | editModeOffset|进入编辑模式的cell偏移|否(def=60) 485 | backgroundColor|cell的背景颜色|否(def=Theme.of(context).scaffoldBackgroundColor) 486 | 487 | 488 | #### SwipeAction: 489 | 参数名 | 含义 | 是否必填 490 | -------- | --- |----- 491 | onTap | 点击此action执行的动作|是 492 | title | action的文字 |否(不填就不显示文字) 493 | style | title的TextStyle|否(有一个默认样式) 494 | color | action拉出的背景颜色|否(def=Color.red) 495 | performsFirstActionWithFullSwipe|拉满时执行第一个action|否(def=false) 496 | icon | action的图标|否(不填就不显示) 497 | closeOnTap | 点击此action是否关闭cell|否(def=true) 498 | backgroundRadius|拉出的button的左上和左下圆角大小|否(def=0.0) 499 | forceAlignmentLeft|当只有一个按钮的时候,让内容持续贴在左边|否(def=false) 500 | widthSpace|这个button在正常展开状态下的宽度大小|否(def=80) 501 | content| 自定义的内容视图|否(如果你需要这个参数,请保持title和icon都为null 502 | 503 | 504 | #### SwipeNestedAction: 505 | 参数名 | 含义 | 是否必填 506 | -------- | --- |----- 507 | icon | 弹出的action的图标|否 508 | title | 弹出的action的标题 |否 509 | nestedWidth | 弹出的action的宽度|否(一般不需要设置,此宽度可以调整弹出的宽度) 510 | curve| 动画曲线|否 511 | content| 自定义的内容视图|否(如果你需要这个参数,请保持title和icon都为null 512 | impactWhenShowing|弹出的时候的震动(知乎app消息页面的删除效果)|否(def=false) 513 | 514 | 515 | #### SwipeActionController: 516 | 参数名(方法名) | 含义 | 517 | -------- | --- | 518 | isEditing | 是否处于编辑模式 519 | selectedIndexPathsChangeCallback|获取选择/取消选择cell的回调 520 | openCellAt|打开特定位置的cell 521 | closeAllOpenCell|关闭所有打开的cell 522 | getSelectedIndexPaths() | 获取选中的行的索引集合 523 | toggleEditingMode() | 切换编辑模式 524 | stopEditingMode()|暂停编辑模式 525 | startEditingMode()| 开始编辑 526 | selectCellAt (indexPaths)|选中一些行 527 | deselectCellAt (indexPaths)|取消选择一些行 528 | selectAll (length)|全选(需要你提供你数据集合的长度 529 | deselectAll()|取消全选 530 | deleteCellAt(indexPaths)|删除所在index的cell(只是同步内部数据,并不会刷新UI) 531 | 532 | 533 | 534 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Language: 2 | 3 | [English](https://github.com/luckysmg/flutter_swipe_action_cell/blob/master/README.md) 4 | | [中文简体](https://github.com/luckysmg/flutter_swipe_action_cell/blob/master/README-CN.md) 5 | 6 | # flutter_swipe_action_cell 7 | 8 | A package that can give you a cell that can be swiped, effect is like iOS native 9 | 10 | ### If you like this package, you can give me a star😀. The more stars this project has, the more time I will speant in the project😀 11 | 12 | 13 | ### Donate: 14 | 15 | Alipay | Wechat | 16 | -------- | ----- 17 | | 18 | 19 | 20 | ## Get started 21 | 22 | ### 3.0.0 and later version is for flutter 3 23 | 24 | ##### pub home page click here: [pub](https://pub.dev/packages/flutter_swipe_action_cell) 25 | 26 | ##### install: 27 | 28 | ```yaml 29 | flutter_swipe_action_cell: ^3.1.5 30 | ``` 31 | 32 | ## 1.Preview: 33 | 34 | Simple delete | Perform first action when full swipe | 35 | -------- | ----- 36 | | 37 | 38 | Delete with animation | More than one action | 39 | -------- | ----- 40 | | 41 | 42 | Effect like WeChat(confirm delete) | Automatically adjust the button width 43 | -------- | ----- 44 | | 45 | 46 | Effect like WeChat collection Page: Customize your button shape | 47 | -------- | 48 | 49 | 50 | 51 | With leading Action and trailing action | 52 | -------- | 53 | | 54 | 55 | Edit mode | 56 | -------- | 57 | | 58 | 59 | ## Full example: 60 | 61 | [Preview (YouTobe video)](https://youtu.be/LWuHas8Zspw) 62 | 63 | And you can find full example code in [example page](https://pub.dev/packages/flutter_swipe_action_cell/example) 64 | 65 | ## Examples 66 | 67 | - ## Example 1: Simple delete the item in ListView 68 | 69 | 70 | 71 | - #### Tip: put the code in the itemBuilder of your ListView 72 | 73 | ```dart 74 | SwipeActionCell( 75 | key: ObjectKey(list[index]), /// this key is necessary 76 | trailingActions: [ 77 | SwipeAction( 78 | title: "delete", 79 | onTap: (CompletionHandler handler) async { 80 | list.removeAt(index); 81 | setState(() {}); 82 | }, 83 | color: Colors.red), 84 | ], 85 | child: Padding( 86 | padding: const EdgeInsets.all(8.0), 87 | child: Text("this is index of ${list[index]}", 88 | style: TextStyle(fontSize: 40)), 89 | ), 90 | ); 91 | ``` 92 | 93 | - ## Example 2: Perform first action when full swipe 94 | 95 | 96 | 97 | ```dart 98 | SwipeActionCell( 99 | /// this key is necessary 100 | key: ObjectKey(list[index]), 101 | trailingActions: [ 102 | SwipeAction( 103 | /// this is the same as iOS native 104 | performsFirstActionWithFullSwipe: true, 105 | title: "delete", 106 | onTap: (CompletionHandler handler) async { 107 | list.removeAt(index); 108 | setState(() {}); 109 | }, 110 | color: Colors.red), 111 | ], 112 | child: Padding( 113 | padding: const EdgeInsets.all(8.0), 114 | child: Text("this is index of ${list[index]}", 115 | style: TextStyle(fontSize: 40)), 116 | ), 117 | ); 118 | ``` 119 | 120 | - ## Example 3: Delete with animation 121 | 122 | 123 | 124 | ```dart 125 | SwipeActionCell( 126 | key: ObjectKey(list[index]), 127 | trailingActions: [ 128 | SwipeAction( 129 | title: "delete", 130 | onTap: (CompletionHandler handler) async { 131 | 132 | /// await handler(true) : will delete this row 133 | /// And after delete animation,setState will called to 134 | /// sync your data source with your UI 135 | 136 | await handler(true); 137 | list.removeAt(index); 138 | setState(() {}); 139 | }, 140 | color: Colors.red), 141 | ], 142 | child: Padding( 143 | padding: const EdgeInsets.all(8.0), 144 | child: Text("this is index of ${list[index]}", 145 | style: TextStyle(fontSize: 40)), 146 | ), 147 | ); 148 | ``` 149 | 150 | - ## Example 4: More than one action: 151 | 152 | 153 | 154 | 155 | ```dart 156 | SwipeActionCell( 157 | key: ObjectKey(list[index]), 158 | trailingActions: [ 159 | SwipeAction( 160 | title: "delete", 161 | onTap: (CompletionHandler handler) async { 162 | await handler(true); 163 | list.removeAt(index); 164 | setState(() {}); 165 | }, 166 | color: Colors.red), 167 | 168 | SwipeAction( 169 | widthSpace: 120, 170 | title: "popAlert", 171 | onTap: (CompletionHandler handler) async { 172 | /// false means that you just do nothing,it will close 173 | /// action buttons by default 174 | handler(false); 175 | showCupertinoDialog( 176 | context: context, 177 | builder: (c) { 178 | return CupertinoAlertDialog( 179 | title: Text('ok'), 180 | actions: [ 181 | CupertinoDialogAction( 182 | child: Text('confirm'), 183 | isDestructiveAction: true, 184 | onPressed: () { 185 | Navigator.pop(context); 186 | }, 187 | ), 188 | ], 189 | ); 190 | }); 191 | }, 192 | color: Colors.orange), 193 | ], 194 | child: Padding( 195 | padding: const EdgeInsets.all(8.0), 196 | child: Text( 197 | "this is index of ${list[index]}", 198 | style: TextStyle(fontSize: 40)), 199 | ), 200 | ); 201 | ``` 202 | 203 | - ## Example 5:Delete like WeChat message page(need to confirm it: 204 | 205 | 206 | 207 | ```dart 208 | return SwipeActionCell( 209 | key: ValueKey(list[index]), 210 | trailingActions: [ 211 | SwipeAction( 212 | /// 213 | /// This attr should be passed to first action 214 | /// 215 | nestedAction: SwipeNestedAction(title: "确认删除"), 216 | title: "删除", 217 | onTap: (CompletionHandler handler) async { 218 | await handler(true); 219 | list.removeAt(index); 220 | setState(() {}); 221 | }, 222 | color: Colors.red, 223 | ), 224 | SwipeAction( 225 | title: "置顶", 226 | onTap: (CompletionHandler handler) async { 227 | /// false means that you just do nothing,it will close 228 | /// action buttons by default 229 | handler(false); 230 | }, 231 | color: Colors.grey), 232 | ], 233 | child: Padding( 234 | padding: const EdgeInsets.all(8.0), 235 | child: Text("this is index of ${list[index]}", 236 | style: TextStyle(fontSize: 40)), 237 | ), 238 | ); 239 | ``` 240 | 241 | - ## Example 6:Edit mode(just like iOS native effect) 242 | 243 | 244 | 245 | ```dart 246 | /// To controller edit mode 247 | SwipeActionEditController controller; 248 | 249 | /// 在initState 250 | @override 251 | void initState() { 252 | super.initState(); 253 | controller = SwipeActionController(); 254 | } 255 | /// To get the selected rows index 256 | List selectedIndexes = controller.getSelectedIndexes(); 257 | 258 | 259 | /// open cell 260 | controller.openCellAt(index: 2, trailing: true, animated: true); 261 | 262 | /// close cell 263 | controller.closeAllOpenCell(); 264 | 265 | /// toggleEditingMode 266 | controller.toggleEditingMode() 267 | 268 | /// startEditMode 269 | controller.startEditingMode() 270 | 271 | /// stopEditMode 272 | controller.stopEditingMode() 273 | 274 | /// select cell 275 | controller.selectCellAt(indexPaths:[1,2,3]) 276 | 277 | controller.deselectCellAt(indexPaths:[1,2,3]) 278 | 279 | /// pass your data length to selectedAll 280 | controller.selectAll(30 281 | ) 282 | 283 | /// deselect all cell 284 | controller deselectAll() 285 | 286 | ListView.builder( 287 | itemBuilder: (c, index) { 288 | return _item(index); 289 | }, 290 | itemCount: list.length, 291 | ); 292 | 293 | 294 | Widget _item(int index) { 295 | return SwipeActionCell( 296 | /// controller 297 | controller: controller, 298 | /// index is required if you want to enter edit mode 299 | index: index, 300 | key: ValueKey(list[index]), 301 | trailingActions: [ 302 | SwipeAction( 303 | /// this is the same as iOS native 304 | performsFirstActionWithFullSwipe: true, 305 | onTap: (handler) async { 306 | await handler(true); 307 | list.removeAt(index); 308 | setState(() {}); 309 | }, 310 | title: "delete"), 311 | ], 312 | child: Padding( 313 | padding: const EdgeInsets.all(15.0), 314 | child: Text("This is index of ${list[index]}", 315 | style: TextStyle(fontSize: 35)), 316 | ), 317 | ); 318 | } 319 | 320 | ``` 321 | 322 | - ## Example 7:customize shape 323 | 324 | 325 | 326 | ```dart 327 | 328 | Widget _item(int index) { 329 | return SwipeActionCell( 330 | key: ValueKey(list[index]), 331 | trailingActions: [ 332 | SwipeAction( 333 | nestedAction: SwipeNestedAction( 334 | /// customize your nested action content 335 | content: Container( 336 | decoration: BoxDecoration( 337 | borderRadius: BorderRadius.circular(30), 338 | color: Colors.red, 339 | ), 340 | width: 130, 341 | height: 60, 342 | child: OverflowBox( 343 | maxWidth: double.infinity, 344 | child: Row( 345 | mainAxisAlignment: MainAxisAlignment.center, 346 | children: [ 347 | Icon( 348 | Icons.delete, 349 | color: Colors.white, 350 | ), 351 | Text('确认删除', 352 | style: TextStyle(color: Colors.white, fontSize: 20)), 353 | ], 354 | ), 355 | ), 356 | ), 357 | ), 358 | 359 | /// you should set the default bg color to transparent 360 | color: Colors.transparent, 361 | 362 | /// set content instead of title of icon 363 | content: _getIconButton(Colors.red, Icons.delete), 364 | onTap: (handler) async { 365 | list.removeAt(index); 366 | setState(() {}); 367 | }), 368 | SwipeAction( 369 | content: _getIconButton(Colors.grey, Icons.vertical_align_top), 370 | color: Colors.transparent, 371 | onTap: (handler) {}), 372 | ], 373 | child: Padding( 374 | padding: const EdgeInsets.all(15.0), 375 | child: Text( 376 | "This is index of ${list[index]},Awesome Swipe Action Cell!! I like it very much!", 377 | style: TextStyle(fontSize: 25)), 378 | ), 379 | ); 380 | } 381 | 382 | Widget _getIconButton(color, icon) { 383 | return Container( 384 | width: 50, 385 | height: 50, 386 | decoration: BoxDecoration( 387 | borderRadius: BorderRadius.circular(25), 388 | 389 | /// set you real bg color in your content 390 | color: color, 391 | ), 392 | child: Icon( 393 | icon, 394 | color: Colors.white, 395 | ), 396 | ); 397 | } 398 | 399 | ``` 400 | 401 | 402 | - ## Example 8:Close opening cell when navigator change its routes. 403 | Add a `SwipeActionNavigatorObserver` in `MaterialApp`'s `navigatorObservers` 404 | ```dart 405 | return MaterialApp( 406 | navigatorObservers: [SwipeActionNavigatorObserver()], 407 | .... 408 | ); 409 | 410 | ``` 411 | 412 | # About CompletionHandler in onTap function of SwipeAction 413 | 414 | it means how you want control this cell after you tap it. If you don't want any animation, just don't call it and update 415 | your data and UI with setState() 416 | 417 | If you want some animation: 418 | 419 | - handler(true) : Means this row will be deleted(You should call setState after it) 420 | 421 | - await handler(true) : Means that you will await the animation to complete(you should call setState after it so that 422 | you will get an animation) 423 | 424 | - handler(false) : means it will not delete this row.By default, it just close this cell's action buttons. 425 | 426 | - await handler(false) : means it will wait the close animation to complete. 427 | 428 | # About all parameter: 429 | 430 | I wrote them in my code with dart doc comments. You can read them in source code. 431 | 432 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /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. 5 | 6 | version: 7 | revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 17 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 18 | - platform: android 19 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 20 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 21 | - platform: ios 22 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 23 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion flutter.compileSdkVersion 29 | ndkVersion flutter.ndkVersion 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | defaultConfig { 37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 38 | applicationId "com.example.example" 39 | // You can update the following values to match your application needs. 40 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 41 | minSdkVersion flutter.minSdkVersion 42 | targetSdkVersion flutter.targetSdkVersion 43 | versionCode flutterVersionCode.toInteger() 44 | versionName flutterVersionName 45 | } 46 | 47 | buildTypes { 48 | release { 49 | // TODO: Add your own signing config for the release build. 50 | // Signing with the debug keys for now, so `flutter run --release` works. 51 | signingConfig signingConfigs.debug 52 | } 53 | } 54 | } 55 | 56 | flutter { 57 | source '../..' 58 | } 59 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/example/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.1.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | mavenCentral() 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 | tasks.register("clean", Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 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 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1510; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | alwaysOutOfDate = 1; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | ); 178 | inputPaths = ( 179 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 180 | ); 181 | name = "Thin Binary"; 182 | outputPaths = ( 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | shellPath = /bin/sh; 186 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 187 | }; 188 | 9740EEB61CF901F6004384FC /* Run Script */ = { 189 | isa = PBXShellScriptBuildPhase; 190 | alwaysOutOfDate = 1; 191 | buildActionMask = 2147483647; 192 | files = ( 193 | ); 194 | inputPaths = ( 195 | ); 196 | name = "Run Script"; 197 | outputPaths = ( 198 | ); 199 | runOnlyForDeploymentPostprocessing = 0; 200 | shellPath = /bin/sh; 201 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 202 | }; 203 | /* End PBXShellScriptBuildPhase section */ 204 | 205 | /* Begin PBXSourcesBuildPhase section */ 206 | 97C146EA1CF9000F007C117D /* Sources */ = { 207 | isa = PBXSourcesBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 211 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | }; 215 | /* End PBXSourcesBuildPhase section */ 216 | 217 | /* Begin PBXVariantGroup section */ 218 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 219 | isa = PBXVariantGroup; 220 | children = ( 221 | 97C146FB1CF9000F007C117D /* Base */, 222 | ); 223 | name = Main.storyboard; 224 | sourceTree = ""; 225 | }; 226 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 227 | isa = PBXVariantGroup; 228 | children = ( 229 | 97C147001CF9000F007C117D /* Base */, 230 | ); 231 | name = LaunchScreen.storyboard; 232 | sourceTree = ""; 233 | }; 234 | /* End PBXVariantGroup section */ 235 | 236 | /* Begin XCBuildConfiguration section */ 237 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 247 | CLANG_WARN_BOOL_CONVERSION = YES; 248 | CLANG_WARN_COMMA = YES; 249 | CLANG_WARN_CONSTANT_CONVERSION = YES; 250 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 251 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 252 | CLANG_WARN_EMPTY_BODY = YES; 253 | CLANG_WARN_ENUM_CONVERSION = YES; 254 | CLANG_WARN_INFINITE_RECURSION = YES; 255 | CLANG_WARN_INT_CONVERSION = YES; 256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 260 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 261 | CLANG_WARN_STRICT_PROTOTYPES = YES; 262 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 263 | CLANG_WARN_UNREACHABLE_CODE = YES; 264 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 265 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 266 | COPY_PHASE_STRIP = NO; 267 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 268 | ENABLE_NS_ASSERTIONS = NO; 269 | ENABLE_STRICT_OBJC_MSGSEND = YES; 270 | GCC_C_LANGUAGE_STANDARD = gnu99; 271 | GCC_NO_COMMON_BLOCKS = YES; 272 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 273 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 274 | GCC_WARN_UNDECLARED_SELECTOR = YES; 275 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 276 | GCC_WARN_UNUSED_FUNCTION = YES; 277 | GCC_WARN_UNUSED_VARIABLE = YES; 278 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 279 | MTL_ENABLE_DEBUG_INFO = NO; 280 | SDKROOT = iphoneos; 281 | SUPPORTED_PLATFORMS = iphoneos; 282 | TARGETED_DEVICE_FAMILY = "1,2"; 283 | VALIDATE_PRODUCT = YES; 284 | }; 285 | name = Profile; 286 | }; 287 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 288 | isa = XCBuildConfiguration; 289 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 290 | buildSettings = { 291 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 292 | CLANG_ENABLE_MODULES = YES; 293 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 294 | DEVELOPMENT_TEAM = 38A979M322; 295 | ENABLE_BITCODE = NO; 296 | INFOPLIST_FILE = Runner/Info.plist; 297 | LD_RUNPATH_SEARCH_PATHS = ( 298 | "$(inherited)", 299 | "@executable_path/Frameworks", 300 | ); 301 | PRODUCT_BUNDLE_IDENTIFIER = "-539699336"; 302 | PRODUCT_NAME = "$(TARGET_NAME)"; 303 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 304 | SWIFT_VERSION = 5.0; 305 | VERSIONING_SYSTEM = "apple-generic"; 306 | }; 307 | name = Profile; 308 | }; 309 | 97C147031CF9000F007C117D /* Debug */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ALWAYS_SEARCH_USER_PATHS = NO; 313 | CLANG_ANALYZER_NONNULL = YES; 314 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 315 | CLANG_CXX_LIBRARY = "libc++"; 316 | CLANG_ENABLE_MODULES = YES; 317 | CLANG_ENABLE_OBJC_ARC = YES; 318 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 319 | CLANG_WARN_BOOL_CONVERSION = YES; 320 | CLANG_WARN_COMMA = YES; 321 | CLANG_WARN_CONSTANT_CONVERSION = YES; 322 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 323 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 324 | CLANG_WARN_EMPTY_BODY = YES; 325 | CLANG_WARN_ENUM_CONVERSION = YES; 326 | CLANG_WARN_INFINITE_RECURSION = YES; 327 | CLANG_WARN_INT_CONVERSION = YES; 328 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 329 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 330 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 331 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 332 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 333 | CLANG_WARN_STRICT_PROTOTYPES = YES; 334 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 335 | CLANG_WARN_UNREACHABLE_CODE = YES; 336 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 337 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 338 | COPY_PHASE_STRIP = NO; 339 | DEBUG_INFORMATION_FORMAT = dwarf; 340 | ENABLE_STRICT_OBJC_MSGSEND = YES; 341 | ENABLE_TESTABILITY = YES; 342 | GCC_C_LANGUAGE_STANDARD = gnu99; 343 | GCC_DYNAMIC_NO_PIC = NO; 344 | GCC_NO_COMMON_BLOCKS = YES; 345 | GCC_OPTIMIZATION_LEVEL = 0; 346 | GCC_PREPROCESSOR_DEFINITIONS = ( 347 | "DEBUG=1", 348 | "$(inherited)", 349 | ); 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 357 | MTL_ENABLE_DEBUG_INFO = YES; 358 | ONLY_ACTIVE_ARCH = YES; 359 | SDKROOT = iphoneos; 360 | TARGETED_DEVICE_FAMILY = "1,2"; 361 | }; 362 | name = Debug; 363 | }; 364 | 97C147041CF9000F007C117D /* Release */ = { 365 | isa = XCBuildConfiguration; 366 | buildSettings = { 367 | ALWAYS_SEARCH_USER_PATHS = NO; 368 | CLANG_ANALYZER_NONNULL = YES; 369 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 370 | CLANG_CXX_LIBRARY = "libc++"; 371 | CLANG_ENABLE_MODULES = YES; 372 | CLANG_ENABLE_OBJC_ARC = YES; 373 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 374 | CLANG_WARN_BOOL_CONVERSION = YES; 375 | CLANG_WARN_COMMA = YES; 376 | CLANG_WARN_CONSTANT_CONVERSION = YES; 377 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 378 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 379 | CLANG_WARN_EMPTY_BODY = YES; 380 | CLANG_WARN_ENUM_CONVERSION = YES; 381 | CLANG_WARN_INFINITE_RECURSION = YES; 382 | CLANG_WARN_INT_CONVERSION = YES; 383 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 385 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 386 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 387 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 388 | CLANG_WARN_STRICT_PROTOTYPES = YES; 389 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu99; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 406 | MTL_ENABLE_DEBUG_INFO = NO; 407 | SDKROOT = iphoneos; 408 | SUPPORTED_PLATFORMS = iphoneos; 409 | SWIFT_COMPILATION_MODE = wholemodule; 410 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 411 | TARGETED_DEVICE_FAMILY = "1,2"; 412 | VALIDATE_PRODUCT = YES; 413 | }; 414 | name = Release; 415 | }; 416 | 97C147061CF9000F007C117D /* Debug */ = { 417 | isa = XCBuildConfiguration; 418 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 419 | buildSettings = { 420 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 421 | CLANG_ENABLE_MODULES = YES; 422 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 423 | DEVELOPMENT_TEAM = 38A979M322; 424 | ENABLE_BITCODE = NO; 425 | INFOPLIST_FILE = Runner/Info.plist; 426 | LD_RUNPATH_SEARCH_PATHS = ( 427 | "$(inherited)", 428 | "@executable_path/Frameworks", 429 | ); 430 | PRODUCT_BUNDLE_IDENTIFIER = "-539699336"; 431 | PRODUCT_NAME = "$(TARGET_NAME)"; 432 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 433 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 434 | SWIFT_VERSION = 5.0; 435 | VERSIONING_SYSTEM = "apple-generic"; 436 | }; 437 | name = Debug; 438 | }; 439 | 97C147071CF9000F007C117D /* Release */ = { 440 | isa = XCBuildConfiguration; 441 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 442 | buildSettings = { 443 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 444 | CLANG_ENABLE_MODULES = YES; 445 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 446 | DEVELOPMENT_TEAM = 38A979M322; 447 | ENABLE_BITCODE = NO; 448 | INFOPLIST_FILE = Runner/Info.plist; 449 | LD_RUNPATH_SEARCH_PATHS = ( 450 | "$(inherited)", 451 | "@executable_path/Frameworks", 452 | ); 453 | PRODUCT_BUNDLE_IDENTIFIER = "-539699336"; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 456 | SWIFT_VERSION = 5.0; 457 | VERSIONING_SYSTEM = "apple-generic"; 458 | }; 459 | name = Release; 460 | }; 461 | /* End XCBuildConfiguration section */ 462 | 463 | /* Begin XCConfigurationList section */ 464 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 465 | isa = XCConfigurationList; 466 | buildConfigurations = ( 467 | 97C147031CF9000F007C117D /* Debug */, 468 | 97C147041CF9000F007C117D /* Release */, 469 | 249021D3217E4FDB00AE95B9 /* Profile */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 475 | isa = XCConfigurationList; 476 | buildConfigurations = ( 477 | 97C147061CF9000F007C117D /* Debug */, 478 | 97C147071CF9000F007C117D /* Release */, 479 | 249021D4217E4FDB00AE95B9 /* Profile */, 480 | ); 481 | defaultConfigurationIsVisible = 0; 482 | defaultConfigurationName = Release; 483 | }; 484 | /* End XCConfigurationList section */ 485 | }; 486 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 487 | } 488 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/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/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luckysmg/flutter_swipe_action_cell/71ad20b7db489acc92dac2d979aac7f552afa03f/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_swipe_action_cell/flutter_swipe_action_cell.dart'; 4 | 5 | void main() { 6 | runApp(const MyApp()); 7 | } 8 | 9 | class MyApp extends StatelessWidget { 10 | const MyApp({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | /// Add this SwipeActionNavigatorObserver to close opening cell when navigator changes its routes 16 | /// 添加这个可以在路由切换的时候统一关闭打开的cell,全局有效 17 | navigatorObservers: [SwipeActionNavigatorObserver()], 18 | debugShowCheckedModeBanner: false, 19 | title: 'Flutter Demo', 20 | theme: ThemeData( 21 | primarySwatch: Colors.blue, 22 | visualDensity: VisualDensity.adaptivePlatformDensity, 23 | ), 24 | home: const HomePage(), 25 | ); 26 | } 27 | } 28 | 29 | class HomePage extends StatefulWidget { 30 | const HomePage({Key? key}) : super(key: key); 31 | 32 | @override 33 | _HomePageState createState() => _HomePageState(); 34 | } 35 | 36 | class _HomePageState extends State { 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scaffold( 40 | body: Center( 41 | child: CupertinoButton.filled( 42 | child: const Text('Enter new page'), 43 | onPressed: () { 44 | Navigator.push(context, 45 | CupertinoPageRoute(builder: (c) => const SwipeActionPage())); 46 | }), 47 | ), 48 | ); 49 | } 50 | } 51 | 52 | class Model { 53 | String id = UniqueKey().toString(); 54 | int index = 0; 55 | 56 | @override 57 | String toString() { 58 | return index.toString(); 59 | } 60 | } 61 | 62 | class SwipeActionPage extends StatefulWidget { 63 | const SwipeActionPage({Key? key}) : super(key: key); 64 | 65 | @override 66 | _SwipeActionPageState createState() => _SwipeActionPageState(); 67 | } 68 | 69 | class _SwipeActionPageState extends State { 70 | List list = List.generate(30, (index) { 71 | return Model()..index = index; 72 | }); 73 | 74 | late SwipeActionController controller; 75 | 76 | @override 77 | void initState() { 78 | super.initState(); 79 | controller = SwipeActionController(selectedIndexPathsChangeCallback: 80 | (changedIndexPaths, selected, currentCount) { 81 | print( 82 | 'cell at ${changedIndexPaths.toString()} is/are ${selected ? 'selected' : 'unselected'} ,current selected count is $currentCount'); 83 | 84 | /// I just call setState() to update simply in this example. 85 | /// But the whole page will be rebuilt. 86 | /// So when you are developing,you'd better update a little piece 87 | /// of UI sub tree for best performance.... 88 | 89 | setState(() {}); 90 | }); 91 | } 92 | 93 | Widget bottomBar() { 94 | return Container( 95 | color: Colors.grey[200], 96 | padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), 97 | child: Padding( 98 | padding: const EdgeInsets.all(8.0), 99 | child: Row( 100 | children: [ 101 | Expanded( 102 | child: CupertinoButton.filled( 103 | padding: const EdgeInsets.only(), 104 | child: const Text('open cell at 2'), 105 | onPressed: () { 106 | controller.openCellAt( 107 | index: 2, trailing: true, animated: true); 108 | }), 109 | ), 110 | const SizedBox( 111 | width: 10, 112 | ), 113 | Expanded( 114 | child: CupertinoButton.filled( 115 | padding: const EdgeInsets.only(), 116 | child: const Text('switch edit mode'), 117 | onPressed: () { 118 | controller.toggleEditingMode(); 119 | }), 120 | ), 121 | ], 122 | ), 123 | ), 124 | ); 125 | } 126 | 127 | @override 128 | Widget build(BuildContext context) { 129 | return Scaffold( 130 | bottomNavigationBar: bottomBar(), 131 | appBar: CupertinoNavigationBar( 132 | middle: CupertinoButton.filled( 133 | padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), 134 | minSize: 0, 135 | child: const Text('deselect all', style: TextStyle(fontSize: 22)), 136 | onPressed: () { 137 | controller.deselectAll(); 138 | }), 139 | leading: CupertinoButton.filled( 140 | padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10), 141 | minSize: 0, 142 | child: Text( 143 | 'delete cells (${controller.getSelectedIndexPaths().length})', 144 | style: const TextStyle(color: Colors.white)), 145 | onPressed: () { 146 | /// 获取选取的索引集合 147 | List selectedIndexes = controller.getSelectedIndexPaths(); 148 | 149 | List idList = []; 150 | for (var element in selectedIndexes) { 151 | idList.add(list[element].id); 152 | } 153 | 154 | /// 遍历id集合,并且在原来的list中删除这些id所对应的数据 155 | for (var itemId in idList) { 156 | list.removeWhere((element) { 157 | return element.id == itemId; 158 | }); 159 | } 160 | 161 | /// 更新内部数据,这句话一定要写哦 162 | controller.deleteCellAt(indexPaths: selectedIndexes); 163 | setState(() {}); 164 | }), 165 | trailing: CupertinoButton.filled( 166 | minSize: 0, 167 | padding: const EdgeInsets.all(10), 168 | child: const Text('select all'), 169 | onPressed: () { 170 | controller.selectAll(dataLength: list.length); 171 | }), 172 | ), 173 | body: ListView.builder( 174 | physics: const BouncingScrollPhysics(), 175 | itemCount: list.length, 176 | itemBuilder: (context, index) { 177 | return _item(context, index); 178 | }, 179 | ), 180 | ); 181 | } 182 | 183 | Widget _item(BuildContext ctx, int index) { 184 | return SwipeActionCell( 185 | controller: controller, 186 | index: index, 187 | 188 | // Required! 189 | key: ValueKey(list[index]), 190 | 191 | // Animation default value below 192 | // deleteAnimationDuration: 400, 193 | selectedForegroundColor: Colors.black.withAlpha(30), 194 | trailingActions: [ 195 | SwipeAction( 196 | title: "delete", 197 | performsFirstActionWithFullSwipe: true, 198 | nestedAction: SwipeNestedAction(title: "confirm"), 199 | onTap: (handler) async { 200 | await handler(true); 201 | 202 | list.removeAt(index); 203 | setState(() {}); 204 | }), 205 | SwipeAction(title: "action2", color: Colors.grey, onTap: (handler) {}), 206 | ], 207 | leadingActions: [ 208 | SwipeAction( 209 | title: "delete", 210 | onTap: (handler) async { 211 | await handler(true); 212 | list.removeAt(index); 213 | setState(() {}); 214 | }), 215 | SwipeAction( 216 | title: "action3", color: Colors.orange, onTap: (handler) {}), 217 | ], 218 | child: GestureDetector( 219 | onTap: () { 220 | Navigator.push( 221 | context, CupertinoPageRoute(builder: (ctx) => const HomePage())); 222 | }, 223 | child: Padding( 224 | padding: const EdgeInsets.all(20.0), 225 | child: Text("This is index of ${list[index]}", 226 | style: const TextStyle(fontSize: 30)), 227 | ), 228 | ), 229 | ); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.18.0" 44 | cupertino_icons: 45 | dependency: "direct main" 46 | description: 47 | name: cupertino_icons 48 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.0.8" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_lints: 66 | dependency: "direct dev" 67 | description: 68 | name: flutter_lints 69 | sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 70 | url: "https://pub.dev" 71 | source: hosted 72 | version: "1.0.4" 73 | flutter_swipe_action_cell: 74 | dependency: "direct main" 75 | description: 76 | path: ".." 77 | relative: true 78 | source: path 79 | version: "3.1.4" 80 | flutter_test: 81 | dependency: "direct dev" 82 | description: flutter 83 | source: sdk 84 | version: "0.0.0" 85 | leak_tracker: 86 | dependency: transitive 87 | description: 88 | name: leak_tracker 89 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 90 | url: "https://pub.dev" 91 | source: hosted 92 | version: "10.0.5" 93 | leak_tracker_flutter_testing: 94 | dependency: transitive 95 | description: 96 | name: leak_tracker_flutter_testing 97 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 98 | url: "https://pub.dev" 99 | source: hosted 100 | version: "3.0.5" 101 | leak_tracker_testing: 102 | dependency: transitive 103 | description: 104 | name: leak_tracker_testing 105 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 106 | url: "https://pub.dev" 107 | source: hosted 108 | version: "3.0.1" 109 | lints: 110 | dependency: transitive 111 | description: 112 | name: lints 113 | sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c 114 | url: "https://pub.dev" 115 | source: hosted 116 | version: "1.0.1" 117 | matcher: 118 | dependency: transitive 119 | description: 120 | name: matcher 121 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 122 | url: "https://pub.dev" 123 | source: hosted 124 | version: "0.12.16+1" 125 | material_color_utilities: 126 | dependency: transitive 127 | description: 128 | name: material_color_utilities 129 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 130 | url: "https://pub.dev" 131 | source: hosted 132 | version: "0.11.1" 133 | meta: 134 | dependency: transitive 135 | description: 136 | name: meta 137 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 138 | url: "https://pub.dev" 139 | source: hosted 140 | version: "1.15.0" 141 | path: 142 | dependency: transitive 143 | description: 144 | name: path 145 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 146 | url: "https://pub.dev" 147 | source: hosted 148 | version: "1.9.0" 149 | sky_engine: 150 | dependency: transitive 151 | description: flutter 152 | source: sdk 153 | version: "0.0.99" 154 | source_span: 155 | dependency: transitive 156 | description: 157 | name: source_span 158 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "1.10.0" 162 | stack_trace: 163 | dependency: transitive 164 | description: 165 | name: stack_trace 166 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "1.11.1" 170 | stream_channel: 171 | dependency: transitive 172 | description: 173 | name: stream_channel 174 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "2.1.2" 178 | string_scanner: 179 | dependency: transitive 180 | description: 181 | name: string_scanner 182 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "1.2.0" 186 | term_glyph: 187 | dependency: transitive 188 | description: 189 | name: term_glyph 190 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "1.2.1" 194 | test_api: 195 | dependency: transitive 196 | description: 197 | name: test_api 198 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "0.7.2" 202 | vector_math: 203 | dependency: transitive 204 | description: 205 | name: vector_math 206 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 207 | url: "https://pub.dev" 208 | source: hosted 209 | version: "2.1.4" 210 | vm_service: 211 | dependency: transitive 212 | description: 213 | name: vm_service 214 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 215 | url: "https://pub.dev" 216 | source: hosted 217 | version: "14.2.5" 218 | sdks: 219 | dart: ">=3.3.0 <4.0.0" 220 | flutter: ">=3.18.0-18.0.pre.54" 221 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.2 37 | flutter_swipe_action_cell: 38 | path: ../ 39 | 40 | dev_dependencies: 41 | flutter_test: 42 | sdk: flutter 43 | 44 | # The "flutter_lints" package below contains a set of recommended lints to 45 | # encourage good coding practices. The lint set provided by the package is 46 | # activated in the `analysis_options.yaml` file located at the root of your 47 | # package. See that file for information about deactivating specific lint 48 | # rules and activating additional ones. 49 | flutter_lints: ^1.0.0 50 | 51 | # For information on the generic Dart part of this file, see the 52 | # following page: https://dart.dev/tools/pub/pubspec 53 | 54 | # The following section is specific to Flutter. 55 | flutter: 56 | 57 | # The following line ensures that the Material Icons font is 58 | # included with your application, so that you can use the icons in 59 | # the material Icons class. 60 | uses-material-design: true 61 | 62 | # To add assets to your application, add an assets section, like this: 63 | # assets: 64 | # - images/a_dot_burr.jpeg 65 | # - images/a_dot_ham.jpeg 66 | 67 | # An image asset can refer to one or more resolution-specific "variants", see 68 | # https://flutter.dev/assets-and-images/#resolution-aware. 69 | 70 | # For details regarding adding assets from package dependencies, see 71 | # https://flutter.dev/assets-and-images/#from-packages 72 | 73 | # To add custom fonts to your application, add a fonts section here, 74 | # in this "flutter" section. Each entry in this list should have a 75 | # "family" key with the font family name, and a "fonts" key with a 76 | # list giving the asset and other descriptors for the font. For 77 | # example: 78 | # fonts: 79 | # - family: Schyler 80 | # fonts: 81 | # - asset: fonts/Schyler-Regular.ttf 82 | # - asset: fonts/Schyler-Italic.ttf 83 | # style: italic 84 | # - family: Trajan Pro 85 | # fonts: 86 | # - asset: fonts/TrajanPro.ttf 87 | # - asset: fonts/TrajanPro_Bold.ttf 88 | # weight: 700 89 | # 90 | # For details regarding fonts from package dependencies, 91 | # see https://flutter.dev/custom-fonts/#from-packages 92 | -------------------------------------------------------------------------------- /lib/core/bus.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class SwipeActionBus { 4 | StreamController _streamController; 5 | 6 | StreamController get streamController => _streamController; 7 | 8 | SwipeActionBus({bool sync = false}) 9 | : _streamController = StreamController.broadcast(sync: sync); 10 | 11 | Stream on() { 12 | if (T == dynamic) { 13 | return streamController.stream as Stream; 14 | } else { 15 | return streamController.stream.where((event) => event is T).cast(); 16 | } 17 | } 18 | 19 | void fire(event) { 20 | streamController.add(event); 21 | } 22 | 23 | void destroy() { 24 | _streamController.close(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/core/cell.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | import 'controller.dart'; 8 | import 'events.dart'; 9 | import 'store.dart'; 10 | import 'swipe_data.dart'; 11 | import 'swipe_pull_align_button.dart'; 12 | import 'swipe_pull_button.dart'; 13 | 14 | /// 15 | /// @created by 文景睿 16 | /// 2020 年 7月13日 17 | /// 18 | 19 | class SwipeActionCell extends StatefulWidget { 20 | /// Actions on trailing 21 | /// 22 | /// 右边的action 23 | final List? trailingActions; 24 | 25 | /// Actions on leading 26 | /// 27 | /// 左边的action 28 | final List? leadingActions; 29 | 30 | /// Your content view 31 | /// 32 | /// 无需多言 33 | final Widget child; 34 | 35 | /// Close actions When you scroll the ListView . default value = true 36 | /// 37 | /// 当你滚动(比如ListView之类的时候,这个item将会关闭拉出的actions,默认为true 38 | final bool closeWhenScrolling; 39 | 40 | /// When deleting the cell 41 | /// the first action will cover all content size with animation.(emm.. just like iOS native effect) 42 | /// def value = true 43 | /// 44 | /// 当删除的时候,第一个按钮会在删除动画执行的时候覆盖整个cell( 和iOS原生动画相似 ) 45 | /// 默认为true 46 | final bool firstActionWillCoverAllSpaceOnDeleting; 47 | 48 | /// The controller to control edit mode 49 | /// 控制器 50 | final SwipeActionController? controller; 51 | 52 | /// The identifier of edit mode 53 | /// 54 | /// 如果你想用编辑模式,这个参数必传!!! 它的值就是你列表的itemBuilder中的index,直接传进来即可 55 | final int? index; 56 | 57 | /// When use edit mode,if you select this row,you will see this indicator on the left of the cell. 58 | /// 59 | /// (可以不传,有默认组件)当你进入编辑模式的时候,如果你选择了这一行,那么你将会在cell左边看到这个组件 60 | final Widget selectedIndicator; 61 | 62 | /// It is contrary to [selectedIndicator] 63 | /// (可以不传,有默认组件)和上面的相反,不说了 64 | final Widget unselectedIndicator; 65 | 66 | /// Indicates that you can swipe the cell or not 67 | /// 68 | /// 代表是否能够侧滑交互(如果你只想用编辑模式而不需要侧滑) 69 | final bool isDraggable; 70 | 71 | /// Background color for cell and def value = Theme.of(context).scaffoldBackgroundColor) 72 | /// 73 | /// 整个cell控件的背景色 默认是Theme.of(context).scaffoldBackgroundColor 74 | final Color? backgroundColor; 75 | 76 | /// The offset that cell will move when entering the edit mode 77 | /// 78 | /// 当你进入编辑模式的时候,cell的content向右边移动的距离 79 | /// def value = 60 80 | final double editModeOffset; 81 | 82 | /// The factor describing how far the cell need to be swiped, for swipe to be considered "full" 83 | /// only valid when [performsFirstActionWithFullSwipe] = true 84 | /// 85 | /// 当拖动到cell宽度 * fullSwipeFactor 的这个距离时,将会触发第一个按钮的事件 86 | /// 注意:[performsFirstActionWithFullSwipe] 为true的时候此参数才有效 87 | /// def value = 0.75 88 | final double fullSwipeFactor; 89 | 90 | /// The open animation duration,such as open animation and close animation duration. The unit is ms 91 | /// 92 | /// 开启动画的执行时间,单位是毫秒 93 | final int openAnimationDuration; 94 | 95 | /// The close animation duration 96 | /// 97 | /// 关闭动画的执行时间,单位是毫秒 98 | final int closeAnimationDuration; 99 | 100 | /// The animation duration of the delete animation.The unit is ms. 101 | /// 102 | /// 删除动画的执行时间。单位是毫秒 103 | final int deleteAnimationDuration; 104 | 105 | /// The foreground color showing when the cell is selected in edit mode,def value is Colors.black.withAlpha(30) 106 | /// 107 | /// 当选中cell的时候的一个前景蒙版颜色,默认为Colors.black.withAlpha(30) 108 | final Color? selectedForegroundColor; 109 | 110 | /// The curve of open animation 111 | /// 112 | /// 开启动画的曲线 113 | final Curve openAnimationCurve; 114 | 115 | /// The curve of close animation 116 | /// 117 | /// 关闭动画的曲线 118 | final Curve closeAnimationCurve; 119 | 120 | /// ## About [key] / 关于[key] 121 | /// You should put a key,like [ValueKey] or [ObjectKey] 122 | /// don't use [GlobalKey] or [UniqueKey] 123 | /// because that will make your app slow. 124 | /// 125 | /// 你应该在构造的时候放入key,推荐使用[ValueKey] 或者 [ObjectKey] 。 126 | /// 最好 不要 使用[GlobalKey]和[UniqueKey]。 127 | /// 我之前在内部也想使用[GlobalKey] 和 [UniqueKey]。 128 | /// 但是想到有性能问题,所以需要您从外部提供轻量级的key用于我框架内部判断,同时用于 129 | /// flutter框架内部刷新。 130 | const SwipeActionCell({ 131 | required Key key, 132 | required this.child, 133 | this.trailingActions, 134 | this.leadingActions, 135 | this.isDraggable = true, 136 | this.closeWhenScrolling = true, 137 | this.firstActionWillCoverAllSpaceOnDeleting = true, 138 | this.controller, 139 | this.index, 140 | this.selectedIndicator = const Icon( 141 | Icons.add_circle, 142 | color: Colors.blue, 143 | ), 144 | this.unselectedIndicator = const Icon( 145 | Icons.do_not_disturb_on, 146 | color: Colors.red, 147 | ), 148 | this.backgroundColor, 149 | this.editModeOffset = 60, 150 | this.fullSwipeFactor = 0.75, 151 | this.deleteAnimationDuration = 400, 152 | this.openAnimationDuration = 400, 153 | this.closeAnimationDuration = 400, 154 | this.openAnimationCurve = Curves.easeOutQuart, 155 | this.closeAnimationCurve = Curves.easeOutQuart, 156 | this.selectedForegroundColor, 157 | }) : super(key: key); 158 | 159 | @override 160 | SwipeActionCellState createState() => SwipeActionCellState(); 161 | } 162 | 163 | class SwipeActionCellState extends State 164 | with TickerProviderStateMixin { 165 | double width = 0; 166 | 167 | late Offset currentOffset; 168 | late double maxTrailingPullWidth; 169 | late double maxLeadingPullWidth; 170 | 171 | bool lockAnim = false; 172 | bool lastItemOut = false; 173 | 174 | late AnimationController controller; 175 | 176 | late AnimationController deleteController; 177 | late AnimationController editController; 178 | 179 | late Animation animation; 180 | late Animation openCurvedAnim; 181 | late Animation closeCurvedAnim; 182 | late Animation deleteCurvedAnim; 183 | late Animation editCurvedAnim; 184 | 185 | ScrollPosition? scrollPosition; 186 | 187 | StreamSubscription? otherCellOpenEventSubscription; 188 | StreamSubscription? programOpenCellEventSubscription; 189 | StreamSubscription? ignorePointerSubscription; 190 | StreamSubscription? changeEditingModeSubscription; 191 | StreamSubscription? selectedSubscription; 192 | 193 | bool ignorePointer = false; 194 | 195 | /// bool field to avoid action button be tapped open when cell is closing 196 | bool ignoreActionButtonHit = false; 197 | 198 | late bool editing; 199 | late bool selected; 200 | 201 | late bool whenTrailingActionShowing; 202 | late bool whenLeadingActionShowing; 203 | 204 | int get trailingActionsCount => widget.trailingActions?.length ?? 0; 205 | 206 | int get leadingActionsCount => widget.leadingActions?.length ?? 0; 207 | 208 | bool get hasTrailingAction => trailingActionsCount > 0; 209 | 210 | bool get hasLeadingAction => leadingActionsCount > 0; 211 | 212 | @override 213 | void initState() { 214 | super.initState(); 215 | lastItemOut = false; 216 | lockAnim = false; 217 | 218 | ignorePointer = false; 219 | maxTrailingPullWidth = _getTrailingMaxPullWidth(); 220 | maxLeadingPullWidth = _getLeadingMaxPullWidth(); 221 | currentOffset = Offset.zero; 222 | 223 | controller = AnimationController( 224 | vsync: this, 225 | duration: Duration(milliseconds: widget.openAnimationDuration), 226 | value: 0.0, 227 | ); 228 | 229 | deleteController = AnimationController( 230 | vsync: this, 231 | value: 1.0, 232 | duration: Duration(milliseconds: widget.deleteAnimationDuration), 233 | ); 234 | editController = AnimationController( 235 | vsync: this, 236 | duration: const Duration(milliseconds: 200), 237 | ); 238 | openCurvedAnim = 239 | CurvedAnimation(parent: controller, curve: widget.openAnimationCurve); 240 | closeCurvedAnim = 241 | CurvedAnimation(parent: controller, curve: widget.closeAnimationCurve); 242 | deleteCurvedAnim = 243 | CurvedAnimation(parent: deleteController, curve: Curves.easeInToLinear); 244 | editCurvedAnim = 245 | CurvedAnimation(parent: editController, curve: Curves.linear); 246 | _listenEvent(); 247 | } 248 | 249 | void _startEditingWithAnim() { 250 | lockAnim = true; 251 | editController.value = 0.0; 252 | lockAnim = false; 253 | animation = 254 | Tween(begin: currentOffset.dx, end: widget.editModeOffset) 255 | .animate(editCurvedAnim) 256 | ..addListener(() { 257 | if (lockAnim) return; 258 | currentOffset = Offset(animation.value, 0); 259 | setState(() {}); 260 | }); 261 | editController.forward(); 262 | } 263 | 264 | void _stopEditingWithAnim() { 265 | lockAnim = true; 266 | editController.value = 0.0; 267 | lockAnim = false; 268 | animation = Tween(begin: widget.editModeOffset, end: 0) 269 | .animate(editCurvedAnim) 270 | ..addListener(() { 271 | if (lockAnim) return; 272 | currentOffset = Offset(animation.value, 0); 273 | setState(() {}); 274 | }); 275 | editController.forward(); 276 | } 277 | 278 | double _getTrailingMaxPullWidth() { 279 | if (widget.trailingActions == null) { 280 | return 0.0; 281 | } 282 | double sum = 0.0; 283 | 284 | for (final action in widget.trailingActions!) { 285 | sum += action.widthSpace; 286 | } 287 | return sum; 288 | } 289 | 290 | double _getLeadingMaxPullWidth() { 291 | if (widget.leadingActions == null) { 292 | return 0.0; 293 | } 294 | double sum = 0.0; 295 | for (final action in widget.leadingActions!) { 296 | sum += action.widthSpace; 297 | } 298 | return sum; 299 | } 300 | 301 | void _listenEvent() { 302 | selectedSubscription = SwipeActionStore.getInstance() 303 | .bus 304 | .on() 305 | .listen((event) { 306 | if (event.controller != widget.controller) { 307 | return; 308 | } 309 | assert(widget.controller != null && widget.index != null); 310 | 311 | if (event.selected && 312 | widget.controller!.selectedSet.contains(widget.index)) { 313 | setState(() {}); 314 | } else if (!event.selected) { 315 | if (selected) { 316 | setState(() {}); 317 | } 318 | } 319 | }); 320 | 321 | otherCellOpenEventSubscription = SwipeActionStore.getInstance() 322 | .bus 323 | .on() 324 | .listen((event) { 325 | if (event.key != widget.key && 326 | currentOffset.dx != 0.0 && 327 | !editing && 328 | !editController.isAnimating) { 329 | closeWithAnim(); 330 | _closeNestedAction(); 331 | } 332 | }); 333 | 334 | programOpenCellEventSubscription = SwipeActionStore.getInstance() 335 | .bus 336 | .on() 337 | .listen((event) { 338 | assert(widget.index != null); 339 | 340 | //If cell is opening or animating,just return 341 | if (currentOffset.dx != 0.0) { 342 | return; 343 | } 344 | 345 | if (event.controller != widget.controller) { 346 | return; 347 | } 348 | 349 | if (event.trailing && !hasTrailingAction || 350 | !event.trailing && !hasLeadingAction) { 351 | return; 352 | } 353 | if (event.index != this.widget.index) { 354 | return; 355 | } 356 | 357 | //fire a CellFingerOpenEvent to tell other cell this cell is opening,and close itself 358 | SwipeActionStore.getInstance() 359 | .bus 360 | .fire(CellFingerOpenEvent(key: widget.key!)); 361 | _open(trailing: event.trailing, animated: event.animated); 362 | }); 363 | 364 | ignorePointerSubscription = SwipeActionStore.getInstance() 365 | .bus 366 | .on() 367 | .listen((event) { 368 | this.ignorePointer = event.ignore; 369 | if (mounted) setState(() {}); 370 | }); 371 | 372 | if (widget.controller == null) return; 373 | changeEditingModeSubscription = SwipeActionStore.getInstance() 374 | .bus 375 | .on() 376 | .listen((event) { 377 | assert( 378 | widget.controller != null, 379 | "If you want to use edit mode,you must pass the " 380 | "SwipeActionController to cell.\n" 381 | "如果你要使用编辑模式必须给cell传入SwipeActionController"); 382 | if (event.controller != widget.controller) { 383 | return; 384 | } 385 | event.editing ? _startEditingWithAnim() : _stopEditingWithAnim(); 386 | }); 387 | } 388 | 389 | void _updateControllerSelectedIndexChangedCallback({required bool selected}) { 390 | widget.controller?.selectedIndexPathsChangeCallback?.call( 391 | [widget.index!], selected, widget.controller!.selectedSet.length); 392 | } 393 | 394 | @override 395 | void dispose() { 396 | _removeScrollListener(); 397 | controller.dispose(); 398 | deleteController.dispose(); 399 | editController.dispose(); 400 | selectedSubscription?.cancel(); 401 | otherCellOpenEventSubscription?.cancel(); 402 | ignorePointerSubscription?.cancel(); 403 | changeEditingModeSubscription?.cancel(); 404 | programOpenCellEventSubscription?.cancel(); 405 | super.dispose(); 406 | } 407 | 408 | @override 409 | void didChangeDependencies() { 410 | super.didChangeDependencies(); 411 | _removeScrollListener(); 412 | _addScrollListener(); 413 | } 414 | 415 | @override 416 | void didUpdateWidget(SwipeActionCell oldWidget) { 417 | super.didUpdateWidget(oldWidget); 418 | maxTrailingPullWidth = _getTrailingMaxPullWidth(); 419 | maxLeadingPullWidth = _getLeadingMaxPullWidth(); 420 | if (widget.closeWhenScrolling != oldWidget.closeWhenScrolling) { 421 | _removeScrollListener(); 422 | _addScrollListener(); 423 | } 424 | _resetControllerWhenDidUpdate(oldWidget); 425 | } 426 | 427 | /// It mainly deal with hot reload 428 | void _resetControllerWhenDidUpdate(SwipeActionCell oldWidget) { 429 | if (oldWidget.controller != widget.controller) { 430 | editing = false; 431 | selected = false; 432 | if (widget.controller == null) { 433 | currentOffset = Offset.zero; 434 | 435 | /// cancel event 436 | changeEditingModeSubscription?.cancel(); 437 | setState(() {}); 438 | } else { 439 | changeEditingModeSubscription = SwipeActionStore.getInstance() 440 | .bus 441 | .on() 442 | .listen((event) { 443 | assert( 444 | widget.controller != null, 445 | "If you want to use edit mode,you must pass the " 446 | "SwipeActionController to cell.\n" 447 | "如果你要使用编辑模式必须给cell传入SwipeActionController"); 448 | if (event.controller != widget.controller) { 449 | return; 450 | } 451 | event.editing ? _startEditingWithAnim() : _stopEditingWithAnim(); 452 | }); 453 | } 454 | } 455 | } 456 | 457 | void _addScrollListener() { 458 | if (widget.closeWhenScrolling) { 459 | scrollPosition = Scrollable.maybeOf(context)?.position; 460 | scrollPosition?.isScrollingNotifier.addListener(_scrollListener); 461 | } 462 | } 463 | 464 | void _removeScrollListener() { 465 | scrollPosition?.isScrollingNotifier.removeListener(_scrollListener); 466 | } 467 | 468 | void _scrollListener() { 469 | final bool isScrolling = scrollPosition?.isScrollingNotifier.value ?? false; 470 | final bool isCellOpening = currentOffset.dx != 0.0; 471 | if (isCellOpening && isScrolling && !editing) { 472 | closeWithAnim(); 473 | _closeNestedAction(); 474 | } 475 | } 476 | 477 | void _onHorizontalDragStart(DragStartDetails details) { 478 | if (editing) return; 479 | //indicates this cell is opening 480 | SwipeActionStore.getInstance() 481 | .bus 482 | .fire(CellFingerOpenEvent(key: widget.key!)); 483 | _closeNestedAction(); 484 | } 485 | 486 | void _onHorizontalDragUpdate(DragUpdateDetails details) { 487 | if (editing) return; 488 | if (!hasLeadingAction && details.delta.dx >= 0 && currentOffset.dx >= 0.0) { 489 | return; 490 | } 491 | if (!hasTrailingAction && 492 | details.delta.dx <= 0 && 493 | currentOffset.dx <= 0.0) { 494 | return; 495 | } 496 | 497 | final bool leadingActionCanFullSwipe = whenLeadingActionShowing && 498 | leadingActionsCount > 0 && 499 | widget.leadingActions![0].performsFirstActionWithFullSwipe; 500 | 501 | final bool trailingActionCanFullSwipe = whenTrailingActionShowing && 502 | trailingActionsCount > 0 && 503 | widget.trailingActions![0].performsFirstActionWithFullSwipe; 504 | 505 | if (leadingActionCanFullSwipe || trailingActionCanFullSwipe) { 506 | _updateWithFullDraggableEffect(details); 507 | } else { 508 | _updateWithNormalEffect(details); 509 | } 510 | } 511 | 512 | void _updateWithFullDraggableEffect(DragUpdateDetails details) { 513 | currentOffset += Offset(details.delta.dx, 0); 514 | 515 | /// set performsFirstActionWithFullSwipe 516 | if (currentOffset.dx.abs() > widget.fullSwipeFactor * width) { 517 | if (!lastItemOut) { 518 | SwipeActionStore.getInstance() 519 | .bus 520 | .fire(PullLastButtonEvent(key: widget.key!, isPullingOut: true)); 521 | lastItemOut = true; 522 | HapticFeedback.heavyImpact(); 523 | } 524 | } else { 525 | if (lastItemOut) { 526 | SwipeActionStore.getInstance() 527 | .bus 528 | .fire(PullLastButtonEvent(key: widget.key!, isPullingOut: false)); 529 | lastItemOut = false; 530 | HapticFeedback.heavyImpact(); 531 | } 532 | } 533 | 534 | if (currentOffset.dx.abs() > width) { 535 | if (currentOffset.dx < 0) { 536 | currentOffset = Offset(-width, 0); 537 | } else { 538 | currentOffset = Offset(width, 0); 539 | } 540 | } 541 | 542 | modifyOffsetIfOverScrolled(); 543 | setState(() {}); 544 | } 545 | 546 | void _updateWithNormalEffect(DragUpdateDetails details) { 547 | /// When currentOffset.dx == 0,need to exec this code to judge which direction 548 | if (currentOffset.dx == 0.0) { 549 | if (details.delta.dx < 0) { 550 | whenTrailingActionShowing = true; 551 | } else if (details.delta.dx > 0) { 552 | whenLeadingActionShowing = true; 553 | } 554 | } 555 | 556 | if (whenTrailingActionShowing) { 557 | if (-currentOffset.dx > maxTrailingPullWidth && details.delta.dx < 0) { 558 | currentOffset += Offset(details.delta.dx / 9, 0); 559 | } else { 560 | currentOffset += Offset(details.delta.dx, 0); 561 | } 562 | 563 | if (currentOffset.dx < -maxTrailingPullWidth - 100) { 564 | currentOffset = Offset(-maxTrailingPullWidth - 100, 0); 565 | } 566 | } else if (whenLeadingActionShowing) { 567 | if (currentOffset.dx > maxLeadingPullWidth && details.delta.dx > 0) { 568 | currentOffset += Offset(details.delta.dx / 9, 0); 569 | } else { 570 | currentOffset += Offset(details.delta.dx, 0); 571 | } 572 | 573 | if (currentOffset.dx > maxLeadingPullWidth + 100) { 574 | currentOffset = Offset(maxLeadingPullWidth + 100, 0); 575 | } 576 | } 577 | 578 | modifyOffsetIfOverScrolled(); 579 | setState(() {}); 580 | } 581 | 582 | /// modify the offset if over scrolled 583 | void modifyOffsetIfOverScrolled() { 584 | if ((!hasLeadingAction && currentOffset.dx > 0.0) || 585 | (!hasTrailingAction && currentOffset.dx < 0.0)) { 586 | currentOffset = Offset.zero; 587 | } 588 | } 589 | 590 | void _onHorizontalDragEnd(DragEndDetails details) async { 591 | if (editing) return; 592 | 593 | final bool canFullSwipe = leadingActionsCount > 0 && 594 | widget.leadingActions![0].performsFirstActionWithFullSwipe || 595 | trailingActionsCount > 0 && 596 | widget.trailingActions![0].performsFirstActionWithFullSwipe; 597 | 598 | if (lastItemOut && canFullSwipe) { 599 | CompletionHandler completionHandler = (delete) async { 600 | if (delete) { 601 | SwipeActionStore.getInstance() 602 | .bus 603 | .fire(IgnorePointerEvent(ignore: true)); 604 | if (widget.firstActionWillCoverAllSpaceOnDeleting) { 605 | SwipeActionStore.getInstance() 606 | .bus 607 | .fire(PullLastButtonToCoverCellEvent(key: widget.key!)); 608 | } 609 | 610 | /// wait animation to complete 611 | await deleteWithAnim(); 612 | } else { 613 | lastItemOut = false; 614 | _closeNestedAction(); 615 | 616 | /// wait animation to complete 617 | await closeWithAnim(); 618 | } 619 | }; 620 | 621 | if (whenTrailingActionShowing && widget.trailingActions != null) { 622 | widget.trailingActions?[0].onTap(completionHandler); 623 | } else if (whenLeadingActionShowing && widget.leadingActions != null) { 624 | widget.leadingActions?[0].onTap(completionHandler); 625 | } 626 | } else { 627 | /// normal dragging update 628 | if (details.velocity.pixelsPerSecond.dx < 0.0) { 629 | if (!whenLeadingActionShowing && hasTrailingAction) { 630 | _open(trailing: true); 631 | } else { 632 | closeWithAnim(); 633 | } 634 | return; 635 | } else if (details.velocity.pixelsPerSecond.dx > 0.0) { 636 | if (!whenTrailingActionShowing && hasLeadingAction) { 637 | _open(trailing: false); 638 | } else { 639 | closeWithAnim(); 640 | } 641 | return; 642 | } 643 | 644 | if (whenTrailingActionShowing) { 645 | if (-currentOffset.dx < maxTrailingPullWidth / 4) { 646 | closeWithAnim(); 647 | } else { 648 | _open(trailing: true); 649 | } 650 | } else if (whenLeadingActionShowing) { 651 | if (currentOffset.dx < maxLeadingPullWidth / 4) { 652 | closeWithAnim(); 653 | } else { 654 | _open(trailing: false); 655 | } 656 | } 657 | 658 | if (trailingActionsCount == 1 || leadingActionsCount == 1) { 659 | SwipeActionStore.getInstance() 660 | .bus 661 | .fire(PullLastButtonEvent(isPullingOut: false)); 662 | } 663 | } 664 | } 665 | 666 | /// When nestedAction is open ,adjust currentOffset if nestedWidth > currentOffset 667 | void adjustOffset( 668 | {required double offsetX, required Curve curve, required bool trailing}) { 669 | controller.stop(); 670 | final adjustOffsetAnimController = AnimationController( 671 | vsync: this, duration: const Duration(milliseconds: 150)); 672 | final curveAnim = 673 | CurvedAnimation(parent: adjustOffsetAnimController, curve: curve); 674 | 675 | final endOffset = trailing ? -offsetX : offsetX; 676 | animation = Tween(begin: currentOffset.dx, end: endOffset) 677 | .animate(curveAnim) 678 | ..addListener(() { 679 | if (lockAnim) return; 680 | this.currentOffset = Offset(animation.value, 0); 681 | setState(() {}); 682 | }); 683 | adjustOffsetAnimController.forward().whenCompleteOrCancel(() { 684 | adjustOffsetAnimController.dispose(); 685 | }); 686 | } 687 | 688 | void _open({required bool trailing, bool animated = true}) { 689 | if (animated) { 690 | _resetAnimValue(); 691 | animation = Tween( 692 | begin: currentOffset.dx, 693 | end: trailing ? -maxTrailingPullWidth : maxLeadingPullWidth) 694 | .animate(openCurvedAnim) 695 | ..addListener(() { 696 | if (lockAnim) return; 697 | this.currentOffset = Offset(animation.value, 0); 698 | setState(() {}); 699 | }); 700 | controller.duration = 701 | Duration(milliseconds: widget.openAnimationDuration); 702 | controller.forward(); 703 | } else { 704 | this.currentOffset = 705 | Offset(trailing ? -maxTrailingPullWidth : maxLeadingPullWidth, 0); 706 | setState(() {}); 707 | } 708 | } 709 | 710 | /// close this cell and return the [Future] of the animation 711 | Future closeWithAnim() async { 712 | //when close animation is running,ignore action button hit test 713 | ignoreActionButtonHit = true; 714 | _resetAnimValue(); 715 | if (mounted) { 716 | animation = Tween(begin: currentOffset.dx, end: 0.0) 717 | .animate(closeCurvedAnim) 718 | ..addListener(() { 719 | if (lockAnim) return; 720 | this.currentOffset = Offset(animation.value, 0); 721 | setState(() {}); 722 | }); 723 | 724 | controller.duration = 725 | Duration(milliseconds: widget.closeAnimationDuration); 726 | return controller.forward() 727 | ..whenCompleteOrCancel(() { 728 | ignoreActionButtonHit = false; 729 | }); 730 | } 731 | } 732 | 733 | void _closeNestedAction() { 734 | if (trailingActionsCount > 0 && 735 | widget.trailingActions?.first.nestedAction != null || 736 | leadingActionsCount > 0 && 737 | widget.leadingActions?.first.nestedAction != null) { 738 | SwipeActionStore.getInstance() 739 | .bus 740 | .fire(CloseNestedActionEvent(key: widget.key!)); 741 | } 742 | } 743 | 744 | void _resetAnimValue() { 745 | lockAnim = true; 746 | controller.value = 0.0; 747 | lockAnim = false; 748 | } 749 | 750 | /// delete this cell and return the [Future] of the animation 751 | Future deleteWithAnim() async { 752 | animation = Tween(begin: 1.0, end: 0.01).animate(deleteCurvedAnim) 753 | ..addListener(() { 754 | /// When quickly click the delete button,the animation will not be seen 755 | /// so the code below is to solve this problem.... 756 | if (whenTrailingActionShowing) { 757 | currentOffset = Offset(-maxTrailingPullWidth, 0); 758 | } else if (whenLeadingActionShowing) { 759 | currentOffset = Offset(maxLeadingPullWidth, 0); 760 | } 761 | }); 762 | 763 | return deleteController.reverse() 764 | ..whenCompleteOrCancel(() { 765 | SwipeActionStore.getInstance() 766 | .bus 767 | .fire(IgnorePointerEvent(ignore: false)); 768 | }); 769 | } 770 | 771 | Map get gestures { 772 | final DeviceGestureSettings? gestureSettings = 773 | MediaQuery.maybeOf(context)?.gestureSettings; 774 | return { 775 | TapGestureRecognizer: 776 | GestureRecognizerFactoryWithHandlers( 777 | () => TapGestureRecognizer(), (instance) { 778 | instance 779 | ..onTap = editing && !editController.isAnimating || 780 | currentOffset.dx != 0.0 781 | ? () { 782 | if (editing && !editController.isAnimating) { 783 | assert( 784 | widget.index != null, 785 | "From SwipeActionCell:\nIf you want to enter edit mode,please pass the 'index' parameter in SwipeActionCell\n" 786 | "=====================================================================================\n" 787 | "如果你要进入编辑模式,请在SwipeActionCell中传入index 参数,他的值就是你列表组件的itemBuilder中返回的index即可"); 788 | 789 | if (selected) { 790 | widget.controller?.selectedSet.remove(widget.index); 791 | _updateControllerSelectedIndexChangedCallback( 792 | selected: false); 793 | } else { 794 | widget.controller?.selectedSet.add(widget.index!); 795 | _updateControllerSelectedIndexChangedCallback( 796 | selected: true); 797 | } 798 | setState(() {}); 799 | } else if (currentOffset.dx != 0 && !controller.isAnimating) { 800 | closeWithAnim(); 801 | _closeNestedAction(); 802 | } 803 | } 804 | : null 805 | ..gestureSettings = gestureSettings; 806 | }), 807 | if (widget.isDraggable) 808 | _DirectionDependentDragGestureRecognizer: 809 | GestureRecognizerFactoryWithHandlers< 810 | _DirectionDependentDragGestureRecognizer>( 811 | () => _DirectionDependentDragGestureRecognizer(), (instance) { 812 | instance 813 | ..onStart = _onHorizontalDragStart 814 | ..onUpdate = _onHorizontalDragUpdate 815 | ..onEnd = _onHorizontalDragEnd 816 | ..gestureSettings = gestureSettings 817 | ..isActionShowing = 818 | whenTrailingActionShowing || whenLeadingActionShowing 819 | ..canDragToLeft = hasTrailingAction 820 | ..canDragToRight = hasLeadingAction; 821 | }), 822 | }; 823 | } 824 | 825 | @override 826 | Widget build(BuildContext context) { 827 | editing = widget.controller?.isEditing.value ?? false; 828 | 829 | if (widget.controller != null) { 830 | selected = widget.controller!.selectedSet.contains(widget.index); 831 | } else { 832 | selected = false; 833 | } 834 | 835 | whenTrailingActionShowing = currentOffset.dx < 0; 836 | whenLeadingActionShowing = currentOffset.dx > 0; 837 | 838 | final Widget selectedButton = widget.controller != null && 839 | (widget.controller!.isEditing.value || editController.isAnimating) 840 | ? _buildSelectedButton(selected) 841 | : const SizedBox(); 842 | 843 | final Widget content = Transform.translate( 844 | offset: editing && !editController.isAnimating 845 | ? Offset(widget.editModeOffset, 0) 846 | : currentOffset, 847 | transformHitTests: false, 848 | child: SizedBox( 849 | width: double.infinity, 850 | child: IgnorePointer( 851 | ignoring: editController.isAnimating || 852 | editing || 853 | currentOffset.dx.abs() > 20, 854 | child: widget.child), 855 | ), 856 | ); 857 | 858 | return IgnorePointer( 859 | ignoring: ignorePointer, 860 | child: SizeTransition( 861 | sizeFactor: deleteCurvedAnim, 862 | child: RawGestureDetector( 863 | behavior: HitTestBehavior.opaque, 864 | gestures: gestures, 865 | child: ColoredBox( 866 | color: widget.backgroundColor ?? 867 | Theme.of(context).scaffoldBackgroundColor, 868 | child: DecoratedBox( 869 | position: DecorationPosition.foreground, 870 | decoration: BoxDecoration( 871 | color: selected 872 | ? (widget.selectedForegroundColor ?? 873 | Colors.black.withAlpha(30)) 874 | : Colors.transparent, 875 | ), 876 | child: LayoutBuilder( 877 | builder: (BuildContext context, BoxConstraints constraints) { 878 | width = constraints.maxWidth; 879 | // Action buttons 880 | final bool shouldHideActionButtons = 881 | currentOffset.dx == 0.0 || editController.isAnimating || editing; 882 | final Widget trailing = shouldHideActionButtons 883 | ? const SizedBox() 884 | : _buildTrailingActionButtons(); 885 | 886 | final Widget leading = shouldHideActionButtons 887 | ? const SizedBox() 888 | : _buildLeadingActionButtons(); 889 | return Stack( 890 | alignment: Alignment.centerLeft, 891 | children: [ 892 | selectedButton, 893 | content, 894 | trailing, 895 | leading, 896 | ], 897 | ); 898 | }, 899 | ), 900 | ), 901 | ), 902 | ), 903 | ), 904 | ); 905 | } 906 | 907 | Widget _buildSelectedButton(bool selected) { 908 | return Container( 909 | alignment: Alignment.center, 910 | width: widget.editModeOffset, 911 | child: selected ? widget.selectedIndicator : widget.unselectedIndicator, 912 | ); 913 | } 914 | 915 | Widget _buildLeadingActionButtons() { 916 | if (currentOffset.dx < 0 || !hasLeadingAction) { 917 | return const SizedBox(); 918 | } 919 | final List actionButtons = 920 | List.generate(leadingActionsCount, (index) { 921 | final actualIndex = leadingActionsCount - 1 - index; 922 | if (widget.leadingActions!.length == 1 && 923 | !widget.leadingActions![0].forceAlignmentToBoundary && 924 | widget.leadingActions![0].performsFirstActionWithFullSwipe) { 925 | return SwipePullAlignButton(actionIndex: actualIndex, trailing: false); 926 | } else { 927 | return SwipePullButton(actionIndex: actualIndex, trailing: false); 928 | } 929 | }); 930 | 931 | return SwipeData( 932 | willPull: lastItemOut && 933 | widget.leadingActions![0].performsFirstActionWithFullSwipe, 934 | firstActionWillCoverAllSpaceOnDeleting: 935 | widget.firstActionWillCoverAllSpaceOnDeleting, 936 | parentKey: widget.key!, 937 | totalActionWidth: maxLeadingPullWidth, 938 | actions: widget.leadingActions!, 939 | contentWidth: width, 940 | currentOffset: currentOffset.dx, 941 | fullDraggable: widget.leadingActions![0].performsFirstActionWithFullSwipe, 942 | parentState: this, 943 | child: Positioned.fill( 944 | child: Stack( 945 | children: actionButtons, 946 | ), 947 | ), 948 | ); 949 | } 950 | 951 | Widget _buildTrailingActionButtons() { 952 | if (currentOffset.dx > 0 || !hasTrailingAction) { 953 | return const SizedBox(); 954 | } 955 | final List actionButtons = 956 | List.generate(trailingActionsCount, (index) { 957 | final actualIndex = trailingActionsCount - 1 - index; 958 | if (trailingActionsCount == 1 && 959 | !widget.trailingActions![0].forceAlignmentToBoundary && 960 | widget.trailingActions![0].performsFirstActionWithFullSwipe) { 961 | return SwipePullAlignButton(actionIndex: actualIndex, trailing: true); 962 | } else { 963 | return SwipePullButton(actionIndex: actualIndex, trailing: true); 964 | } 965 | }); 966 | 967 | return SwipeData( 968 | willPull: lastItemOut && 969 | widget.trailingActions![0].performsFirstActionWithFullSwipe, 970 | firstActionWillCoverAllSpaceOnDeleting: 971 | widget.firstActionWillCoverAllSpaceOnDeleting, 972 | parentKey: widget.key!, 973 | totalActionWidth: maxTrailingPullWidth, 974 | actions: widget.trailingActions!, 975 | contentWidth: width, 976 | currentOffset: currentOffset.dx, 977 | fullDraggable: 978 | widget.trailingActions![0].performsFirstActionWithFullSwipe, 979 | parentState: this, 980 | child: Positioned.fill( 981 | child: Stack( 982 | children: actionButtons, 983 | ), 984 | ), 985 | ); 986 | } 987 | } 988 | 989 | /// If you want the animation I support 990 | /// you should modify your data source first,then wait handler to execute,after that, 991 | /// you can call setState to update your UI. 992 | /// 993 | /// Code Example: 994 | /// ``` 995 | /// initState() { 996 | /// List list = [1,2,3,5]; 997 | /// } 998 | /// 999 | /// onTap(handler) async { 1000 | /// list.removeAt(2); 1001 | /// 1002 | /// // true: will delete this row in ListView 1003 | /// // false: will not delete it 1004 | /// // Q: When to use "await"? 1005 | /// // A: The time when you want animation 1006 | /// await handler(true or false); 1007 | /// 1008 | /// setState((){}); 1009 | /// } 1010 | /// ``` 1011 | /// 1012 | typedef CompletionHandler = Future Function(bool delete); 1013 | 1014 | typedef SwipeActionOnTapCallback = void Function(CompletionHandler handler); 1015 | 1016 | class SwipeAction { 1017 | /// title's text Style 1018 | /// default value is :TextStyle(fontSize: 18,color: Colors.white) 1019 | /// 1020 | /// 标题的字体样式,默认值在上面 1021 | final TextStyle style; 1022 | 1023 | /// close the actions button after you tap it,default value is true 1024 | /// 1025 | /// 点击这个按钮的时候,是否关闭actions 默认为true 1026 | final bool closeOnTap; 1027 | 1028 | /// When you have just one button,if it is on leading/trailing,set this param to true will 1029 | /// make the content inside button [Alignment.centerRight] / [Alignment.centerLeft] 1030 | final bool forceAlignmentToBoundary; 1031 | 1032 | /// The width space this action button will take when opening. 1033 | /// 1034 | /// 当处于打开状态下这个按钮所占的宽度 1035 | final double widthSpace; 1036 | 1037 | /// bg color 1038 | /// 1039 | /// 背景颜色 1040 | final Color color; 1041 | 1042 | /// onTap callback 1043 | /// 1044 | /// 点击事件回调 1045 | final SwipeActionOnTapCallback onTap; 1046 | 1047 | /// 图标 1048 | final Widget? icon; 1049 | 1050 | /// 标题 1051 | final String? title; 1052 | 1053 | /// 背景左上(右上)和左下(左上)的圆角 1054 | final double backgroundRadius; 1055 | 1056 | /// 嵌套的action 1057 | final SwipeNestedAction? nestedAction; 1058 | 1059 | /// If you want to customize your content,you can use this attr. 1060 | /// And don't set [title] and [icon] attrs 1061 | /// 1062 | /// 如果你想自定义你的按钮内容,那么就设置这个content参数 1063 | /// 注意如果你设置了content,那么就不要设置title和icon,两个都必须为null 1064 | final Widget? content; 1065 | 1066 | /// Tip:It is ok to set this property only in first action. 1067 | /// When drag cell a long distance,it will be dismissed, 1068 | /// and it will execute the onTap of the first [SwipeAction] 1069 | /// def value = false 1070 | /// 1071 | /// 这个属性设置给第一个action就好 1072 | /// 就像iOS一样,往左拉满会直接删除一样,拉满后会执行第一个 [SwipeAction] 的onTap方法 1073 | /// 默认为false 1074 | final bool performsFirstActionWithFullSwipe; 1075 | 1076 | const SwipeAction({ 1077 | required this.onTap, 1078 | this.title, 1079 | this.style = const TextStyle(fontSize: 18, color: Colors.white), 1080 | this.color = Colors.red, 1081 | this.icon, 1082 | this.closeOnTap = true, 1083 | this.backgroundRadius = 0.0, 1084 | this.forceAlignmentToBoundary = false, 1085 | this.widthSpace = 80, 1086 | this.nestedAction, 1087 | this.content, 1088 | this.performsFirstActionWithFullSwipe = false, 1089 | }); 1090 | } 1091 | 1092 | /// 点击后弹出的action 1093 | class SwipeNestedAction { 1094 | /// 图标 1095 | final Widget? icon; 1096 | 1097 | /// 标题 1098 | final String? title; 1099 | 1100 | /// normally,you dont need to set this value.When your [SwipeNestedAction] take more width than 1101 | /// original [SwipeAction] ,you can set this value. 1102 | /// !!!!! this value must be smaller than the sum of all buttons 1103 | /// 1104 | /// 一般不建议设置此项,此项一般在只有一个action的时候,可能NestedAction的title比较长装不下,才需要设置这个值来调整宽度 1105 | /// 注意,如果你要设置这个值,那么这个值必须比所有按钮宽度值的总和要小,不然你可能会看到下面的按钮露出来 1106 | /// 1107 | /// (这个参数的作用也就是微信ios端消息列表里面,你侧滑"订阅号消息"那个cell所呈现的效果。 1108 | /// 因为弹出的"确认删除"四个字需要调整原本宽度 1109 | /// 1110 | final double? nestedWidth; 1111 | 1112 | /// The Animation Curve when pull the nestedAction 1113 | /// 弹出动画的曲线 1114 | final Curve curve; 1115 | 1116 | /// 是否在弹出的时候有震动(知乎app 消息页面点击删除的效果) 1117 | final bool impactWhenShowing; 1118 | 1119 | /// You can customize your content using this attr 1120 | /// If you want to use this attr,please don't set title and icon 1121 | /// 1122 | /// 你可以通过这个参数来自定义你的nestAction的内容 1123 | /// 如果你要使用这个参数,请不要设置title和icon 1124 | final Widget? content; 1125 | 1126 | SwipeNestedAction({ 1127 | this.icon, 1128 | this.title, 1129 | this.content, 1130 | this.nestedWidth, 1131 | this.curve = Curves.easeOutQuart, 1132 | this.impactWhenShowing = false, 1133 | }); 1134 | } 1135 | 1136 | class _DirectionDependentDragGestureRecognizer 1137 | extends HorizontalDragGestureRecognizer { 1138 | late bool canDragToLeft; 1139 | late bool canDragToRight; 1140 | late bool isActionShowing; 1141 | 1142 | @override 1143 | void handleEvent(PointerEvent event) { 1144 | final double delta = event.delta.dx; 1145 | if (isActionShowing || 1146 | canDragToLeft && delta < 0 || 1147 | canDragToRight && delta > 0 || 1148 | delta == 0) { 1149 | super.handleEvent(event); 1150 | } 1151 | } 1152 | } 1153 | -------------------------------------------------------------------------------- /lib/core/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'events.dart'; 5 | import 'store.dart'; 6 | 7 | /// When you tap cell under edit mode, or call the method below,this callback func will be called once: 8 | /// [SwipeActionController.selectCellAt] 9 | /// [SwipeActionController.deselectCellAt] 10 | /// [SwipeActionController.selectAll] 11 | /// [SwipeActionController.deselectAll] 12 | /// [SwipeActionController.stopEditingMode] 13 | /// 14 | typedef SelectedIndexPathsChangeCallback = Function( 15 | List changedIndexPaths, bool selected, int currentSelectedCount); 16 | 17 | /// An controller to control the cell's behavior 18 | /// 19 | /// 一个可以控制cell行为的控制器 20 | class SwipeActionController { 21 | SwipeActionController({this.selectedIndexPathsChangeCallback}); 22 | 23 | Set selectedSet = Set(); 24 | 25 | SelectedIndexPathsChangeCallback? selectedIndexPathsChangeCallback; 26 | 27 | /// edit mode or not 28 | /// 29 | /// 获取是否正处于编辑模式 30 | final ValueNotifier isEditing = ValueNotifier(false); 31 | 32 | /// start editing 33 | void startEditingMode() { 34 | if (isEditing.value) { 35 | return; 36 | } 37 | isEditing.value = true; 38 | _fireEditEvent(controller: this, editing: true); 39 | } 40 | 41 | /// stop editing 42 | void stopEditingMode() { 43 | if (!isEditing.value) { 44 | return; 45 | } 46 | selectedIndexPathsChangeCallback?.call(List.of(selectedSet), false, 0); 47 | selectedSet.clear(); 48 | isEditing.value = false; 49 | _fireEditEvent(controller: this, editing: false); 50 | } 51 | 52 | /// If it is editing,stop it. 53 | /// If it is not editing, start it 54 | void toggleEditingMode() { 55 | if (isEditing.value) { 56 | stopEditingMode(); 57 | } else { 58 | startEditingMode(); 59 | } 60 | } 61 | 62 | /// Get the list of selected cell 's index 63 | /// 64 | /// 拿到选择的cell的索引集合 65 | List getSelectedIndexPaths({bool sorted = false}) { 66 | final List res = List.from(selectedSet); 67 | if (sorted) { 68 | res.sort((d1, d2) { 69 | return d1.compareTo(d2); 70 | }); 71 | } 72 | return res; 73 | } 74 | 75 | /// This method is called of sync internal data model. 76 | /// You still need to call [setState] after calling this method 77 | /// 78 | /// 这个方法只是为了更新内部数据源,你仍然需要在调用这个方法之后 79 | /// 去调用 [setState] 来更新你自己的数据源 80 | void deleteCellAt({required List indexPaths}) { 81 | indexPaths.forEach((element) { 82 | selectedSet.remove(element); 83 | }); 84 | } 85 | 86 | /// Open a cell programmatically 87 | /// 1.If cell has already opening,nothing will happen ! 88 | /// 2.You can only open one cell,when you open cell use this method,other opening cell will close. 89 | /// 3.If cell is not on screen,nothing will happen ! 90 | /// 91 | /// 利用编程的方式打开一个cell 92 | /// 1. 如果cell已经打开,那么什么都不会发生!! 93 | /// 2.你只能在同一时刻打开一个cell,当你调用此方法进行打开cell的时候,如果那个cell已经打开,则不会做任何事情 94 | /// 3.如果cell不在屏幕上,什么也不会发生!! 95 | void openCellAt({ 96 | required int index, 97 | required bool trailing, 98 | bool animated = true, 99 | }) { 100 | SwipeActionStore.getInstance().bus.fire(CellProgramOpenEvent( 101 | index: index, 102 | trailing: trailing, 103 | animated: animated, 104 | controller: this, 105 | )); 106 | } 107 | 108 | /// You can call this method to close all opening cell without passing controller into cell 109 | /// 110 | /// 你可以不把controller传入cell就可以直接调用这个方法 111 | /// 用于关闭所有打开的cell 112 | void closeAllOpenCell() { 113 | //Send a CellFingerOpenEvent with UniqueKey,so all opening cell don't have this key 114 | //so all of opening cell will close 115 | SwipeActionStore.getInstance() 116 | .bus 117 | .fire(CellFingerOpenEvent(key: UniqueKey())); 118 | } 119 | 120 | /// Select a cell (You must pass [SwipeActionCell.index] attr to your [SwipeActionCell] 121 | /// 122 | /// 选中cell (注意!!!你必须把 index 参数传入cell 123 | void selectCellAt({required List indexPaths}) { 124 | assert( 125 | isEditing.value, 126 | "Please call method :selectCellAt(index) when you are in edit mode\n" 127 | "请在编辑模式打开的情况下调用 selectCellAt(index)"); 128 | indexPaths.forEach((element) { 129 | selectedSet.add(element); 130 | }); 131 | selectedIndexPathsChangeCallback?.call( 132 | indexPaths, true, selectedSet.length); 133 | SwipeActionStore.getInstance().bus.fire(CellSelectedEvent(selected: true, controller: this)); 134 | } 135 | 136 | /// Deselect cells (You must pass [SwipeActionCell.index] attr to your [SwipeActionCell] 137 | /// 138 | /// 选中一个cell (注意!!!你必须把 index 参数传入cell 139 | void deselectCellAt({required List indexPaths}) { 140 | assert( 141 | isEditing.value, 142 | "Please call method :selectCellAt(index) when you are in edit mode\n" 143 | "请在编辑模式打开的情况下调用 selectCellAt(index)"); 144 | 145 | indexPaths.forEach((element) { 146 | selectedSet.remove(element); 147 | }); 148 | selectedIndexPathsChangeCallback?.call( 149 | indexPaths, false, selectedSet.length); 150 | SwipeActionStore.getInstance().bus.fire(CellSelectedEvent(selected: false, controller: this)); 151 | } 152 | 153 | /// select all cell 154 | /// 155 | /// 选择所有的cell 156 | void selectAll({required int dataLength}) { 157 | assert( 158 | isEditing.value, 159 | "Please call method :selectCellAt(index) when you are in edit mode\n" 160 | "请在编辑模式打开的情况下调用 selectCellAt(index)"); 161 | 162 | List selectedList = List.generate(dataLength, (index) => index); 163 | selectedSet.addAll(selectedList); 164 | selectedIndexPathsChangeCallback?.call(selectedList, true, dataLength); 165 | SwipeActionStore.getInstance().bus.fire(CellSelectedEvent(selected: true, controller: this)); 166 | } 167 | 168 | /// deselect all cell 169 | /// 170 | /// 取消选择所有的cell 171 | void deselectAll() { 172 | assert( 173 | isEditing.value, 174 | "Please call method :selectCellAt(index) when you are in edit mode\n" 175 | "请在编辑模式打开的情况下调用 selectCellAt(index)"); 176 | 177 | final List deselectedList = selectedSet.toList(); 178 | selectedSet.clear(); 179 | selectedIndexPathsChangeCallback?.call( 180 | deselectedList, false, selectedSet.length); 181 | SwipeActionStore.getInstance().bus.fire(CellSelectedEvent(selected: false, controller: this)); 182 | } 183 | 184 | void _fireEditEvent( 185 | {required SwipeActionController controller, required bool editing}) { 186 | SwipeActionStore.getInstance() 187 | .bus 188 | .fire(EditingModeEvent(controller: controller, editing: editing)); 189 | } 190 | 191 | void dispose() { 192 | isEditing.dispose(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /lib/core/events.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | import 'controller.dart'; 4 | 5 | class CellFingerOpenEvent { 6 | CellFingerOpenEvent({required this.key}); 7 | 8 | final Key key; 9 | } 10 | 11 | class CellProgramOpenEvent { 12 | const CellProgramOpenEvent({ 13 | required this.controller, 14 | required this.index, 15 | required this.animated, 16 | required this.trailing, 17 | }); 18 | 19 | final SwipeActionController controller; 20 | final int index; 21 | final bool animated; 22 | final bool trailing; 23 | } 24 | 25 | class PullLastButtonEvent { 26 | const PullLastButtonEvent({this.key, required this.isPullingOut}); 27 | 28 | final Key? key; 29 | final bool isPullingOut; 30 | } 31 | 32 | class PullLastButtonToCoverCellEvent { 33 | const PullLastButtonToCoverCellEvent({required this.key}); 34 | 35 | final Key key; 36 | } 37 | 38 | class IgnorePointerEvent { 39 | const IgnorePointerEvent({required this.ignore}); 40 | 41 | final bool ignore; 42 | } 43 | 44 | class CloseNestedActionEvent { 45 | const CloseNestedActionEvent({required this.key}); 46 | 47 | final Key key; 48 | } 49 | 50 | class EditingModeEvent { 51 | const EditingModeEvent({required this.controller, required this.editing}); 52 | 53 | final SwipeActionController controller; 54 | final bool editing; 55 | } 56 | 57 | class CellSelectedEvent { 58 | const CellSelectedEvent({required this.selected, required this.controller}); 59 | 60 | final SwipeActionController controller; 61 | final bool selected; 62 | } 63 | -------------------------------------------------------------------------------- /lib/core/store.dart: -------------------------------------------------------------------------------- 1 | import 'bus.dart'; 2 | 3 | class SwipeActionStore { 4 | static SwipeActionStore? _instance; 5 | late SwipeActionBus bus; 6 | 7 | static SwipeActionStore getInstance() { 8 | if (_instance == null) { 9 | _instance = SwipeActionStore(); 10 | _instance?.bus = SwipeActionBus(); 11 | } 12 | return _instance!; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/core/swipe_action_cell_tap_close_area.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_swipe_action_cell/flutter_swipe_action_cell.dart'; 3 | 4 | /// This widget maybe not compatible with nested action... 5 | class SwipeActionCellTapCloseArea extends StatelessWidget { 6 | final Widget child; 7 | 8 | final SwipeActionController _controller = SwipeActionController(); 9 | 10 | SwipeActionCellTapCloseArea({Key? key, required this.child}) 11 | : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Listener( 16 | onPointerDown: (PointerDownEvent event) { 17 | _controller.closeAllOpenCell(); 18 | }, 19 | child: child, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/core/swipe_action_navigator_observer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_swipe_action_cell/flutter_swipe_action_cell.dart'; 3 | 4 | /// This class is used to close opening cell when navigator change its routes. 5 | /// 这个类是用来在路由改变的时候对打开的cell进行关闭 6 | class SwipeActionNavigatorObserver extends NavigatorObserver { 7 | final SwipeActionController _controller = SwipeActionController(); 8 | 9 | @override 10 | void didPush(Route route, Route? previousRoute) { 11 | _controller.closeAllOpenCell(); 12 | } 13 | 14 | @override 15 | void didPop(Route route, Route? previousRoute) { 16 | _controller.closeAllOpenCell(); 17 | } 18 | 19 | @override 20 | void didRemove(Route route, Route? previousRoute) { 21 | _controller.closeAllOpenCell(); 22 | } 23 | 24 | @override 25 | void didReplace({Route? newRoute, Route? oldRoute}) { 26 | _controller.closeAllOpenCell(); 27 | } 28 | 29 | @override 30 | void didStartUserGesture( 31 | Route route, Route? previousRoute) { 32 | _controller.closeAllOpenCell(); 33 | } 34 | 35 | @override 36 | void didStopUserGesture() { 37 | _controller.closeAllOpenCell(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/core/swipe_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import 'cell.dart'; 4 | 5 | class SwipeData extends InheritedWidget { 6 | final List actions; 7 | final Widget child; 8 | final double currentOffset; 9 | final bool fullDraggable; 10 | final Key parentKey; 11 | final bool firstActionWillCoverAllSpaceOnDeleting; 12 | final double contentWidth; 13 | final double totalActionWidth; 14 | final bool willPull; 15 | final SwipeActionCellState parentState; 16 | 17 | SwipeData({ 18 | required this.child, 19 | required this.actions, 20 | required this.willPull, 21 | required this.currentOffset, 22 | required this.fullDraggable, 23 | required this.parentKey, 24 | required this.firstActionWillCoverAllSpaceOnDeleting, 25 | required this.contentWidth, 26 | required this.totalActionWidth, 27 | required this.parentState, 28 | }) : super(child: child); 29 | 30 | static SwipeData of(BuildContext context) { 31 | return context.dependOnInheritedWidgetOfExactType(aspect: SwipeData)!; 32 | } 33 | 34 | @override 35 | bool updateShouldNotify(SwipeData oldWidget) { 36 | return oldWidget.actions != actions || 37 | oldWidget.currentOffset != currentOffset || 38 | oldWidget.fullDraggable != fullDraggable || 39 | oldWidget.parentKey != parentKey || 40 | oldWidget.firstActionWillCoverAllSpaceOnDeleting != 41 | firstActionWillCoverAllSpaceOnDeleting || 42 | oldWidget.contentWidth != contentWidth || 43 | oldWidget.totalActionWidth != totalActionWidth || 44 | oldWidget.willPull != willPull; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/core/swipe_pull_align_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | import 'cell.dart'; 8 | import 'events.dart'; 9 | import 'store.dart'; 10 | import 'swipe_data.dart'; 11 | 12 | class SwipePullAlignButton extends StatefulWidget { 13 | final int actionIndex; 14 | final bool trailing; 15 | 16 | const SwipePullAlignButton( 17 | {Key? key, required this.actionIndex, required this.trailing}) 18 | : super(key: key); 19 | 20 | @override 21 | _SwipePullAlignButtonState createState() => _SwipePullAlignButtonState(); 22 | } 23 | 24 | class _SwipePullAlignButtonState extends State 25 | with TickerProviderStateMixin { 26 | bool get trailing => widget.trailing; 27 | 28 | late double offsetX; 29 | late Alignment alignment; 30 | late CompletionHandler handler; 31 | 32 | StreamSubscription? pullLastButtonSubscription; 33 | StreamSubscription? pullLastButtonToCoverCellEventSubscription; 34 | StreamSubscription? closeNestedActionEventSubscription; 35 | 36 | bool whenNestedActionShowing = false; 37 | bool whenFirstAction = false; 38 | bool whenDeleting = false; 39 | 40 | late SwipeData data; 41 | late SwipeAction action; 42 | 43 | AnimationController? offsetController; 44 | AnimationController? widthFillActionContentController; 45 | AnimationController? alignController; 46 | late Animation alignCurve; 47 | late Animation offsetCurve; 48 | late Animation widthFillActionContentCurve; 49 | 50 | late Animation animation; 51 | 52 | bool lockAnim = false; 53 | 54 | @override 55 | void initState() { 56 | super.initState(); 57 | whenFirstAction = widget.actionIndex == 0; 58 | alignment = trailing ? Alignment.centerRight : Alignment.centerLeft; 59 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 60 | _initAnim(); 61 | _initCompletionHandler(); 62 | }); 63 | 64 | _listenEvent(); 65 | } 66 | 67 | void _pullActionButton(bool isPullingOut) { 68 | _resetAnimationController(alignController); 69 | if (isPullingOut) { 70 | var tween = AlignmentTween( 71 | begin: alignment, 72 | end: trailing ? Alignment.centerLeft : Alignment.centerRight) 73 | .animate(alignCurve); 74 | tween.addListener(() { 75 | if (lockAnim) return; 76 | alignment = tween.value; 77 | setState(() {}); 78 | }); 79 | 80 | alignController?.forward(); 81 | } else { 82 | var tween = AlignmentTween( 83 | begin: alignment, 84 | end: trailing ? Alignment.centerRight : Alignment.centerLeft) 85 | .animate(alignCurve); 86 | tween.addListener(() { 87 | if (lockAnim) return; 88 | alignment = tween.value; 89 | setState(() {}); 90 | }); 91 | alignController?.forward(); 92 | } 93 | } 94 | 95 | void _listenEvent() { 96 | /// Cell layer has judged the value of performsFirstActionWithFullSwipe 97 | pullLastButtonSubscription = SwipeActionStore.getInstance() 98 | .bus 99 | .on() 100 | .listen((event) async { 101 | if (event.key == data.parentKey && whenFirstAction) { 102 | _pullActionButton(event.isPullingOut); 103 | } 104 | }); 105 | 106 | pullLastButtonToCoverCellEventSubscription = SwipeActionStore.getInstance() 107 | .bus 108 | .on() 109 | .listen((event) { 110 | if (event.key == data.parentKey) { 111 | _animToCoverCell(); 112 | } 113 | }); 114 | 115 | closeNestedActionEventSubscription = SwipeActionStore.getInstance() 116 | .bus 117 | .on() 118 | .listen((event) { 119 | if (event.key == data.parentKey && 120 | action.nestedAction != null && 121 | whenNestedActionShowing) { 122 | _resetNestedAction(); 123 | } 124 | if (event.key != data.parentKey && whenNestedActionShowing) { 125 | _resetNestedAction(); 126 | } 127 | }); 128 | } 129 | 130 | void _resetNestedAction() { 131 | whenNestedActionShowing = false; 132 | alignment = trailing ? Alignment.centerRight : Alignment.centerLeft; 133 | setState(() {}); 134 | } 135 | 136 | void _initCompletionHandler() { 137 | handler = (delete) async { 138 | if (delete) { 139 | SwipeActionStore.getInstance() 140 | .bus 141 | .fire(IgnorePointerEvent(ignore: true)); 142 | 143 | if (data.firstActionWillCoverAllSpaceOnDeleting) { 144 | await _animToCoverCell(); 145 | } 146 | await data.parentState.deleteWithAnim(); 147 | } else { 148 | if (action.closeOnTap) { 149 | data.parentState.closeWithAnim(); 150 | } 151 | } 152 | }; 153 | } 154 | 155 | Future _animToCoverCell() async { 156 | whenDeleting = true; 157 | _resetAnimationController(offsetController); 158 | animation = Tween( 159 | begin: offsetX, 160 | end: trailing ? -data.contentWidth : data.contentWidth) 161 | .animate(offsetCurve) 162 | ..addListener(() { 163 | if (lockAnim) return; 164 | offsetX = animation.value; 165 | setState(() {}); 166 | }); 167 | await offsetController?.forward(); 168 | } 169 | 170 | void _animToCoverPullActionContent() async { 171 | if (action.nestedAction?.nestedWidth != null) { 172 | try { 173 | assert( 174 | (action.nestedAction?.nestedWidth ?? 0) >= data.totalActionWidth, 175 | "Your nested width must be larger than the width of all action buttons" 176 | "\n 你的nestedWidth必须要大于或者等于所有按钮的总长度,否则下面的按钮会显现出来"); 177 | } catch (e) { 178 | print(e.toString()); 179 | } 180 | } 181 | 182 | _resetAnimationController(widthFillActionContentController); 183 | whenNestedActionShowing = true; 184 | alignment = Alignment.center; 185 | 186 | if (action.nestedAction?.nestedWidth != null && 187 | action.nestedAction!.nestedWidth! > data.totalActionWidth) { 188 | data.parentState.adjustOffset( 189 | offsetX: action.nestedAction!.nestedWidth!, 190 | curve: action.nestedAction!.curve, 191 | trailing: trailing); 192 | } 193 | 194 | double endOffset; 195 | if (action.nestedAction?.nestedWidth != null) { 196 | endOffset = trailing 197 | ? -action.nestedAction!.nestedWidth! 198 | : action.nestedAction!.nestedWidth!; 199 | } else { 200 | endOffset = trailing ? -data.totalActionWidth : data.totalActionWidth; 201 | } 202 | 203 | animation = Tween(begin: offsetX, end: endOffset) 204 | .animate(widthFillActionContentCurve) 205 | ..addListener(() { 206 | if (lockAnim) return; 207 | offsetX = animation.value; 208 | alignment = Alignment.lerp(alignment, Alignment.center, 209 | widthFillActionContentController!.value)!; 210 | setState(() {}); 211 | }); 212 | widthFillActionContentController?.forward(); 213 | } 214 | 215 | @override 216 | Widget build(BuildContext context) { 217 | data = SwipeData.of(context); 218 | action = data.actions[widget.actionIndex]; 219 | 220 | final bool shouldShowNestedActionInfo = widget.actionIndex == 0 && 221 | action.nestedAction != null && 222 | whenNestedActionShowing; 223 | 224 | if (!whenNestedActionShowing && !whenDeleting) { 225 | offsetX = data.currentOffset; 226 | } 227 | 228 | return GestureDetector( 229 | onTap: () { 230 | if (whenFirstAction && 231 | action.nestedAction != null && 232 | !whenNestedActionShowing) { 233 | if (action.nestedAction!.impactWhenShowing) { 234 | HapticFeedback.mediumImpact(); 235 | } 236 | _animToCoverPullActionContent(); 237 | return; 238 | } 239 | action.onTap.call(handler); 240 | }, 241 | child: Transform.translate( 242 | offset: Offset((trailing ? 1 : -1) * data.contentWidth + offsetX, 0), 243 | child: DecoratedBox( 244 | decoration: BoxDecoration( 245 | borderRadius: BorderRadius.circular(action.backgroundRadius), 246 | color: action.color, 247 | ), 248 | child: Align( 249 | alignment: trailing ? Alignment.centerLeft : Alignment.centerRight, 250 | child: Container( 251 | padding: const EdgeInsets.only(left: 16, right: 16), 252 | alignment: alignment, 253 | width: offsetX.abs(), 254 | child: _buildButtonContent(shouldShowNestedActionInfo), 255 | ), 256 | ), 257 | ), 258 | ), 259 | ); 260 | } 261 | 262 | Widget _buildButtonContent(bool shouldShowNestedActionInfo) { 263 | if (whenDeleting) return const SizedBox(); 264 | if (shouldShowNestedActionInfo && action.nestedAction?.content != null) { 265 | return action.nestedAction!.content!; 266 | } 267 | 268 | return action.title != null || action.icon != null 269 | ? Column( 270 | mainAxisAlignment: MainAxisAlignment.center, 271 | children: [ 272 | _buildIcon(action, shouldShowNestedActionInfo), 273 | _buildTitle(action, shouldShowNestedActionInfo), 274 | ], 275 | ) 276 | : action.content ?? const SizedBox(); 277 | } 278 | 279 | Widget _buildIcon(SwipeAction action, bool shouldShowNestedActionInfo) { 280 | return shouldShowNestedActionInfo 281 | ? action.nestedAction?.icon ?? const SizedBox() 282 | : action.icon ?? const SizedBox(); 283 | } 284 | 285 | Widget _buildTitle(SwipeAction action, bool shouldShowNestedActionInfo) { 286 | if (shouldShowNestedActionInfo) { 287 | if (action.nestedAction?.title == null) return const SizedBox(); 288 | return Text( 289 | action.nestedAction!.title!, 290 | overflow: TextOverflow.clip, 291 | maxLines: 1, 292 | style: action.style, 293 | ); 294 | } else { 295 | if (action.title == null) return const SizedBox(); 296 | return Text( 297 | action.title!, 298 | overflow: TextOverflow.clip, 299 | maxLines: 1, 300 | style: action.style, 301 | ); 302 | } 303 | } 304 | 305 | @override 306 | void dispose() { 307 | offsetController?.dispose(); 308 | alignController?.dispose(); 309 | widthFillActionContentController?.dispose(); 310 | pullLastButtonSubscription?.cancel(); 311 | pullLastButtonToCoverCellEventSubscription?.cancel(); 312 | closeNestedActionEventSubscription?.cancel(); 313 | super.dispose(); 314 | } 315 | 316 | void _initAnim() { 317 | offsetController = AnimationController( 318 | vsync: this, duration: const Duration(milliseconds: 60)); 319 | alignController = AnimationController( 320 | vsync: this, duration: const Duration(milliseconds: 500)); 321 | 322 | alignCurve = 323 | CurvedAnimation(parent: alignController!, curve: Curves.easeOutCirc); 324 | 325 | offsetCurve = CurvedAnimation( 326 | parent: offsetController!, curve: Curves.easeInToLinear); 327 | 328 | if (widget.actionIndex == 0 && action.nestedAction != null) { 329 | widthFillActionContentController = AnimationController( 330 | vsync: this, duration: const Duration(milliseconds: 350)); 331 | widthFillActionContentCurve = CurvedAnimation( 332 | parent: widthFillActionContentController!, 333 | curve: action.nestedAction!.curve); 334 | } 335 | } 336 | 337 | void _resetAnimationController(AnimationController? controller) { 338 | lockAnim = true; 339 | controller?.value = 0; 340 | lockAnim = false; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /lib/core/swipe_pull_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | 7 | import 'cell.dart'; 8 | import 'events.dart'; 9 | import 'store.dart'; 10 | import 'swipe_data.dart'; 11 | 12 | /// The normal swipe action button 13 | class SwipePullButton extends StatefulWidget { 14 | final int actionIndex; 15 | final bool trailing; 16 | 17 | const SwipePullButton({ 18 | Key? key, 19 | required this.actionIndex, 20 | required this.trailing, 21 | }) : super(key: key); 22 | 23 | @override 24 | _SwipePullButtonState createState() { 25 | return _SwipePullButtonState(); 26 | } 27 | } 28 | 29 | class _SwipePullButtonState extends State 30 | with TickerProviderStateMixin { 31 | /// The cell's total offset,not button's 32 | late double offsetX; 33 | 34 | late Alignment alignment; 35 | late CompletionHandler handler; 36 | 37 | StreamSubscription? pullLastButtonSubscription; 38 | StreamSubscription? pullLastButtonToCoverCellEventSubscription; 39 | StreamSubscription? closeNestedActionEventSubscription; 40 | 41 | bool whenActiveToOffset = true; 42 | bool whenNestedActionShowing = false; 43 | bool whenFirstAction = false; 44 | bool whenPullingOut = false; 45 | bool whenDeleting = false; 46 | 47 | late SwipeData data; 48 | late SwipeAction action; 49 | 50 | AnimationController? offsetController; 51 | AnimationController? offsetFillActionContentController; 52 | late Animation widthPullCurve; 53 | late Animation offsetFillActionContentCurve; 54 | 55 | late Animation animation; 56 | 57 | bool lockAnim = false; 58 | 59 | bool get trailing => widget.trailing; 60 | 61 | @override 62 | void initState() { 63 | super.initState(); 64 | lockAnim = false; 65 | whenNestedActionShowing = false; 66 | whenFirstAction = widget.actionIndex == 0; 67 | alignment = trailing ? Alignment.centerLeft : Alignment.centerRight; 68 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) { 69 | _initAnim(); 70 | _initCompletionHandler(); 71 | }); 72 | 73 | _listenEvent(); 74 | } 75 | 76 | void _pullActionButton(bool isPullingOut) { 77 | _resetAnimationController(offsetController); 78 | whenActiveToOffset = false; 79 | if (isPullingOut) { 80 | animation = Tween(begin: offsetX, end: data.currentOffset) 81 | .animate(widthPullCurve) 82 | ..addListener(() { 83 | if (lockAnim) return; 84 | offsetX = animation.value; 85 | setState(() {}); 86 | }); 87 | offsetController?.forward().whenComplete(() { 88 | whenActiveToOffset = true; 89 | whenPullingOut = true; 90 | }); 91 | } else { 92 | final factor = data.currentOffset / data.totalActionWidth; 93 | double sumWidth = 0.0; 94 | for (int i = 0; i <= widget.actionIndex; i++) { 95 | sumWidth += data.actions[i].widthSpace; 96 | } 97 | final currentOffset = sumWidth * factor; 98 | animation = Tween(begin: data.currentOffset, end: currentOffset) 99 | .animate(widthPullCurve) 100 | ..addListener(() { 101 | if (lockAnim) return; 102 | offsetX = animation.value; 103 | setState(() {}); 104 | }); 105 | offsetController?.forward().whenComplete(() { 106 | whenActiveToOffset = true; 107 | whenPullingOut = false; 108 | }); 109 | } 110 | } 111 | 112 | void _listenEvent() { 113 | /// Cell layer has judged the value of performsFirstActionWithFullSwipe 114 | pullLastButtonSubscription = SwipeActionStore.getInstance() 115 | .bus 116 | .on() 117 | .listen((event) async { 118 | if (event.key == data.parentKey && whenFirstAction) { 119 | _pullActionButton(event.isPullingOut); 120 | } 121 | }); 122 | 123 | pullLastButtonToCoverCellEventSubscription = SwipeActionStore.getInstance() 124 | .bus 125 | .on() 126 | .listen((event) { 127 | if (event.key == data.parentKey) { 128 | _animToCoverCell(); 129 | } 130 | }); 131 | 132 | closeNestedActionEventSubscription = SwipeActionStore.getInstance() 133 | .bus 134 | .on() 135 | .listen((event) { 136 | if (event.key == data.parentKey && 137 | action.nestedAction != null && 138 | whenNestedActionShowing) { 139 | _resetNestedAction(); 140 | } 141 | if (event.key != data.parentKey && whenNestedActionShowing) { 142 | _resetNestedAction(); 143 | } 144 | }); 145 | } 146 | 147 | void _resetNestedAction() { 148 | whenActiveToOffset = true; 149 | whenNestedActionShowing = false; 150 | alignment = trailing ? Alignment.centerLeft : Alignment.centerRight; 151 | setState(() {}); 152 | } 153 | 154 | void _initCompletionHandler() { 155 | handler = (delete) async { 156 | if (delete) { 157 | SwipeActionStore.getInstance() 158 | .bus 159 | .fire(IgnorePointerEvent(ignore: true)); 160 | 161 | if (data.firstActionWillCoverAllSpaceOnDeleting) { 162 | await _animToCoverCell(); 163 | } 164 | await data.parentState.deleteWithAnim(); 165 | } else { 166 | if (action.closeOnTap) { 167 | _resetNestedAction(); 168 | await data.parentState.closeWithAnim(); 169 | } 170 | } 171 | }; 172 | } 173 | 174 | Future _animToCoverCell() async { 175 | whenDeleting = true; 176 | _resetAnimationController(offsetController); 177 | whenActiveToOffset = false; 178 | animation = Tween( 179 | begin: offsetX, 180 | end: widget.trailing ? -data.contentWidth : data.contentWidth) 181 | .animate(widthPullCurve) 182 | ..addListener(() { 183 | if (lockAnim) return; 184 | offsetX = animation.value; 185 | setState(() {}); 186 | }); 187 | await offsetController?.forward(); 188 | } 189 | 190 | void _animToCoverPullActionContent() async { 191 | if (action.nestedAction?.nestedWidth != null) { 192 | try { 193 | assert( 194 | action.nestedAction!.nestedWidth! >= data.totalActionWidth, 195 | "Your nested width must be larger than the width of all action buttons" 196 | "\n 你的nestedWidth必须要大于或者等于所有按钮的总长度,否则下面的按钮会显现出来"); 197 | } catch (e) { 198 | print(e.toString()); 199 | } 200 | } 201 | 202 | _resetAnimationController(offsetFillActionContentController); 203 | whenNestedActionShowing = true; 204 | alignment = Alignment.center; 205 | 206 | if (action.nestedAction?.nestedWidth != null && 207 | action.nestedAction!.nestedWidth! > data.totalActionWidth) { 208 | data.parentState.adjustOffset( 209 | offsetX: action.nestedAction!.nestedWidth!, 210 | curve: action.nestedAction!.curve, 211 | trailing: widget.trailing); 212 | } 213 | 214 | double endOffset; 215 | if (action.nestedAction?.nestedWidth != null) { 216 | endOffset = trailing 217 | ? -action.nestedAction!.nestedWidth! 218 | : action.nestedAction!.nestedWidth!; 219 | } else { 220 | endOffset = trailing ? -data.totalActionWidth : data.totalActionWidth; 221 | } 222 | 223 | animation = Tween(begin: offsetX, end: endOffset) 224 | .animate(offsetFillActionContentCurve) 225 | ..addListener(() { 226 | if (lockAnim) return; 227 | offsetX = animation.value; 228 | setState(() {}); 229 | }); 230 | offsetFillActionContentController?.forward(); 231 | } 232 | 233 | @override 234 | Widget build(BuildContext context) { 235 | data = SwipeData.of(context); 236 | action = data.actions[widget.actionIndex]; 237 | final bool willPull = data.willPull && whenFirstAction; 238 | 239 | final bool shouldShowNestedActionInfo = widget.actionIndex == 0 && 240 | action.nestedAction != null && 241 | whenNestedActionShowing; 242 | 243 | if (whenActiveToOffset && !whenNestedActionShowing) { 244 | /// compute offset 245 | final currentPullOffset = data.currentOffset; 246 | if (willPull) { 247 | offsetX = data.currentOffset; 248 | } else { 249 | final factor = currentPullOffset / data.totalActionWidth; 250 | double sumWidth = 0.0; 251 | for (int i = 0; i <= widget.actionIndex; i++) { 252 | sumWidth += data.actions[i].widthSpace; 253 | } 254 | offsetX = sumWidth * factor; 255 | } 256 | } 257 | 258 | return GestureDetector( 259 | onTap: () { 260 | if (data.parentState.ignoreActionButtonHit) { 261 | return; 262 | } 263 | 264 | if (whenFirstAction && 265 | action.nestedAction != null && 266 | !whenNestedActionShowing) { 267 | if (action.nestedAction!.impactWhenShowing) { 268 | HapticFeedback.mediumImpact(); 269 | } 270 | _animToCoverPullActionContent(); 271 | return; 272 | } 273 | action.onTap.call(handler); 274 | }, 275 | child: Transform.translate( 276 | offset: Offset((trailing ? 1 : -1) * data.contentWidth + offsetX, 0), 277 | child: DecoratedBox( 278 | decoration: BoxDecoration( 279 | borderRadius: BorderRadius.circular(action.backgroundRadius), 280 | color: action.color, 281 | ), 282 | child: Align( 283 | alignment: trailing ? Alignment.centerLeft : Alignment.centerRight, 284 | child: Container( 285 | alignment: Alignment.center, 286 | width: shouldShowNestedActionInfo 287 | ? offsetX.abs() 288 | : action.widthSpace, 289 | child: _buildButtonContent(shouldShowNestedActionInfo)), 290 | ), 291 | ), 292 | ), 293 | ); 294 | } 295 | 296 | Widget _buildButtonContent(bool shouldShowNestedActionInfo) { 297 | if (whenDeleting) return const SizedBox(); 298 | if (shouldShowNestedActionInfo && action.nestedAction?.content != null) { 299 | return action.nestedAction!.content!; 300 | } 301 | 302 | return action.title != null || action.icon != null 303 | ? Column( 304 | mainAxisAlignment: MainAxisAlignment.center, 305 | children: [ 306 | _buildIcon(action, shouldShowNestedActionInfo), 307 | _buildTitle(action, shouldShowNestedActionInfo), 308 | ], 309 | ) 310 | : action.content ?? const SizedBox(); 311 | } 312 | 313 | Widget _buildIcon(SwipeAction action, bool shouldShowNestedActionInfo) { 314 | return shouldShowNestedActionInfo 315 | ? action.nestedAction?.icon ?? const SizedBox() 316 | : action.icon ?? const SizedBox(); 317 | } 318 | 319 | Widget _buildTitle(SwipeAction action, bool shouldShowNestedActionInfo) { 320 | if (shouldShowNestedActionInfo) { 321 | if (action.nestedAction?.title == null) return const SizedBox(); 322 | return Text( 323 | action.nestedAction!.title!, 324 | overflow: TextOverflow.clip, 325 | maxLines: 1, 326 | style: action.style, 327 | ); 328 | } else { 329 | if (action.title == null) return const SizedBox(); 330 | return Text( 331 | action.title!, 332 | overflow: TextOverflow.clip, 333 | maxLines: 1, 334 | style: action.style, 335 | ); 336 | } 337 | } 338 | 339 | @override 340 | void dispose() { 341 | offsetController?.dispose(); 342 | offsetFillActionContentController?.dispose(); 343 | pullLastButtonSubscription?.cancel(); 344 | pullLastButtonToCoverCellEventSubscription?.cancel(); 345 | closeNestedActionEventSubscription?.cancel(); 346 | super.dispose(); 347 | } 348 | 349 | void _initAnim() { 350 | offsetController = AnimationController( 351 | vsync: this, duration: const Duration(milliseconds: 60)); 352 | 353 | widthPullCurve = CurvedAnimation( 354 | parent: offsetController!, curve: Curves.easeInToLinear); 355 | 356 | if (widget.actionIndex == 0) { 357 | offsetFillActionContentController = AnimationController( 358 | vsync: this, duration: const Duration(milliseconds: 350)); 359 | offsetFillActionContentCurve = CurvedAnimation( 360 | parent: offsetFillActionContentController!, 361 | curve: action.nestedAction?.curve ?? Curves.easeOutQuart); 362 | } 363 | } 364 | 365 | void _resetAnimationController(AnimationController? controller) { 366 | lockAnim = true; 367 | controller?.value = 0; 368 | lockAnim = false; 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /lib/flutter_swipe_action_cell.dart: -------------------------------------------------------------------------------- 1 | library flutter_swipe_action_cell; 2 | 3 | export 'package:flutter_swipe_action_cell/core/cell.dart'; 4 | export 'package:flutter_swipe_action_cell/core/controller.dart'; 5 | export 'package:flutter_swipe_action_cell/core/swipe_action_cell_tap_close_area.dart'; 6 | export 'package:flutter_swipe_action_cell/core/swipe_action_navigator_observer.dart'; 7 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_swipe_action_cell 2 | description: An awesome UI package incluing iOS style cell swipe action effect.You can use this package to implement iOS style tableView cell swipe action 3 | version: 3.1.5 4 | homepage: https://github.com/luckysmg/flutter_swipe_action_cell 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | 24 | # To add assets to your package, add an assets section, like this: 25 | # assets: 26 | # - images/a_dot_burr.jpeg 27 | # - images/a_dot_ham.jpeg 28 | # 29 | # For details regarding assets in packages, see 30 | # https://flutter.dev/assets-and-images/#from-packages 31 | # 32 | # An image asset can refer to one or more resolution-specific "variants", see 33 | # https://flutter.dev/assets-and-images/#resolution-aware. 34 | 35 | # To add custom fonts to your package, add a fonts section here, 36 | # in this "flutter" section. Each entry in this list should have a 37 | # "family" key with the font family name, and a "fonts" key with a 38 | # list giving the asset and other descriptors for the font. For 39 | # example: 40 | # fonts: 41 | # - family: Schyler 42 | # fonts: 43 | # - asset: fonts/Schyler-Regular.ttf 44 | # - asset: fonts/Schyler-Italic.ttf 45 | # style: italic 46 | # - family: Trajan Pro 47 | # fonts: 48 | # - asset: fonts/TrajanPro.ttf 49 | # - asset: fonts/TrajanPro_Bold.ttf 50 | # weight: 700 51 | # 52 | # For details regarding fonts in packages, see 53 | # https://flutter.dev/custom-fonts/#from-packages 54 | -------------------------------------------------------------------------------- /test/tests.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_swipe_action_cell/flutter_swipe_action_cell.dart'; 4 | 5 | void main() { 6 | testWidgets('Actions buttons can show and hide correctly when actions update.', (tester) async { 7 | final GlobalKey key = GlobalKey(); 8 | final List trailingActions = [ 9 | SwipeAction(onTap: (handler) {}, title: "trailingAction 1"), 10 | SwipeAction(onTap: (handler) {}, title: "trailingAction 2"), 11 | ]; 12 | final List leadingActions = [ 13 | SwipeAction(onTap: (handler) {}, title: "leadingActions 1"), 14 | ]; 15 | 16 | final SwipeActionController controller = SwipeActionController(); 17 | 18 | // No actions. we expect to find nothing. 19 | await tester.pumpWidget(MaterialApp( 20 | home: Scaffold( 21 | body: ListView( 22 | children: [ 23 | SwipeActionCell( 24 | controller: controller, 25 | key: key, 26 | child: Container( 27 | color: Colors.red, 28 | height: 200, 29 | ), 30 | ), 31 | ], 32 | ), 33 | ), 34 | )); 35 | 36 | await tester.timedDrag( 37 | find.byKey(key), 38 | const Offset(-100, 0), 39 | const Duration(milliseconds: 100), 40 | ); 41 | expect(find.text('trailingAction 1'), findsNothing); 42 | expect(find.text('trailingAction 2'), findsNothing); 43 | 44 | controller.closeAllOpenCell(); 45 | await tester.pumpAndSettle(); 46 | await tester.timedDrag( 47 | find.byKey(key), 48 | const Offset(100, 0), 49 | const Duration(milliseconds: 100), 50 | ); 51 | expect(find.text('leadingActions 1'), findsNothing); 52 | 53 | controller.closeAllOpenCell(); 54 | await tester.pumpAndSettle(); 55 | 56 | // Now update the trailing and leading actions, we expect to see buttons when dragging. 57 | await tester.pumpWidget(MaterialApp( 58 | home: Scaffold( 59 | body: ListView( 60 | children: [ 61 | SwipeActionCell( 62 | trailingActions: trailingActions, 63 | leadingActions: leadingActions, 64 | key: key, 65 | child: Container( 66 | color: Colors.red, 67 | height: 200, 68 | ), 69 | ), 70 | ], 71 | ), 72 | ), 73 | )); 74 | await tester.timedDrag( 75 | find.byKey(key), const Offset(-100, 0), const Duration(milliseconds: 100)); 76 | expect(find.text('trailingAction 1'), findsOneWidget); 77 | expect(find.text('trailingAction 2'), findsOneWidget); 78 | 79 | controller.closeAllOpenCell(); 80 | await tester.pumpAndSettle(); 81 | await tester.timedDrag( 82 | find.byKey(key), const Offset(100, 0), const Duration(milliseconds: 100)); 83 | expect(find.text('leadingActions 1'), findsOneWidget); 84 | 85 | // No update the actions to null again, and we expect to see buttons. 86 | await tester.pumpWidget(MaterialApp( 87 | home: Scaffold( 88 | body: ListView( 89 | children: [ 90 | SwipeActionCell( 91 | key: key, 92 | child: Container( 93 | color: Colors.red, 94 | height: 200, 95 | ), 96 | ), 97 | ], 98 | ), 99 | ), 100 | )); 101 | await tester.timedDrag( 102 | find.byKey(key), const Offset(-100, 0), const Duration(milliseconds: 100)); 103 | expect(find.text('trailingAction 1'), findsNothing); 104 | expect(find.text('trailingAction 2'), findsNothing); 105 | 106 | controller.closeAllOpenCell(); 107 | await tester.pumpAndSettle(); 108 | await tester.timedDrag( 109 | find.byKey(key), const Offset(100, 0), const Duration(milliseconds: 100)); 110 | expect(find.text('leadingActions 1'), findsNothing); 111 | 112 | controller.dispose(); 113 | }); 114 | 115 | testWidgets('Select event should not conflict with each other.', (tester) async { 116 | final SwipeActionController controller = SwipeActionController(); 117 | 118 | final List words = [ 119 | 'Apple', 120 | 'Banana', 121 | 'Cherry', 122 | 'Date', 123 | ]; 124 | 125 | await tester.pumpWidget( 126 | MaterialApp( 127 | home: Scaffold( 128 | body: Column( 129 | children: [ 130 | Expanded( 131 | child: ListView.builder( 132 | itemCount: 3, 133 | itemBuilder: (context, index) { 134 | return SwipeActionCell( 135 | key: ObjectKey(words[index]), 136 | trailingActions: [ 137 | SwipeAction( 138 | onTap: (handler) async { 139 | await handler(false); 140 | }, 141 | color: Colors.grey, 142 | icon: const Icon(Icons.edit, color: Colors.white), 143 | ) 144 | ], 145 | child: ListTile( 146 | title: Text("Test"), 147 | ), 148 | ); 149 | }, 150 | ), 151 | ), 152 | Expanded( 153 | child: ListView.builder( 154 | itemCount: words.length, 155 | itemBuilder: (context, index) { 156 | final word = words[index]; 157 | return SwipeActionCell( 158 | key: ObjectKey(word), 159 | index: index, 160 | controller: controller, 161 | child: ListTile( 162 | title: Text(word), 163 | onLongPress: () { 164 | controller.startEditingMode(); 165 | controller.selectCellAt(indexPaths: [index]); 166 | }, 167 | ), 168 | ); 169 | }, 170 | ), 171 | ), 172 | ], 173 | ), 174 | ), 175 | ), 176 | ); 177 | await tester.longPress(find.text(words[0])); 178 | }); 179 | } 180 | --------------------------------------------------------------------------------