├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── NOTICE ├── README.md ├── lib ├── sorted_chips_row.dart └── src │ ├── chip_spec.dart │ ├── sorted_chip.dart │ └── sorted_chips_row.dart ├── pubspec.yaml ├── sorted_chips_row.iml └── test └── sorted_chips_row_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter repo-specific 23 | /bin/cache/ 24 | /bin/mingit/ 25 | /dev/benchmarks/mega_gallery/ 26 | /dev/bots/.recipe_deps 27 | /dev/bots/android_tools/ 28 | /dev/docs/doc/ 29 | /dev/docs/flutter.docs.zip 30 | /dev/docs/lib/ 31 | /dev/docs/pubspec.yaml 32 | /packages/flutter/coverage/ 33 | version 34 | 35 | # packages file containing multi-root paths 36 | .packages.generated 37 | 38 | # Flutter/Dart/Pub related 39 | **/doc/api/ 40 | .dart_tool/ 41 | .flutter-plugins 42 | .packages 43 | .pub-cache/ 44 | .pub/ 45 | .pubspec.lock 46 | .metadata 47 | build/ 48 | flutter_*.png 49 | linked_*.ds 50 | unlinked.ds 51 | unlinked_spec.ds 52 | 53 | # Android related 54 | **/android/**/gradle-wrapper.jar 55 | **/android/.gradle 56 | **/android/captures/ 57 | **/android/gradlew 58 | **/android/gradlew.bat 59 | **/android/local.properties 60 | **/android/**/GeneratedPluginRegistrant.java 61 | **/android/key.properties 62 | *.jks 63 | 64 | # iOS/XCode related 65 | **/ios/**/*.mode1v3 66 | **/ios/**/*.mode2v3 67 | **/ios/**/*.moved-aside 68 | **/ios/**/*.pbxuser 69 | **/ios/**/*.perspectivev3 70 | **/ios/**/*sync/ 71 | **/ios/**/.sconsign.dblite 72 | **/ios/**/.tags* 73 | **/ios/**/.vagrant/ 74 | **/ios/**/DerivedData/ 75 | **/ios/**/Icon? 76 | **/ios/**/Pods/ 77 | **/ios/**/.symlinks/ 78 | **/ios/**/profile 79 | **/ios/**/xcuserdata 80 | **/ios/.generated/ 81 | **/ios/Flutter/App.framework 82 | **/ios/Flutter/Flutter.framework 83 | **/ios/Flutter/Generated.xcconfig 84 | **/ios/Flutter/app.flx 85 | **/ios/Flutter/app.zip 86 | **/ios/Flutter/flutter_assets/ 87 | **/ios/ServiceDefinitions.json 88 | **/ios/Runner/GeneratedPluginRegistrant.* 89 | 90 | # Exceptions to above rules. 91 | !**/ios/**/default.mode1v3 92 | !**/ios/**/default.mode2v3 93 | !**/ios/**/default.pbxuser 94 | !**/ios/**/default.perspectivev3 95 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 96 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.1.0] - 28 April 2019 2 | 3 | * Initial public release. 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Callstack.io sp. z o.o. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sorted_chips_row 2 | 3 | A Flutter Widget displaying a row of [Material Chips](https://material.io/design/components/chips.html), sorted according to the provided comparison function. 4 | 5 | ![sorted chips row](https://static.callstack.com/assets/sorted_chips_row.gif) 6 | 7 | ## How to use 8 | 9 | ### Adding dependency 10 | 11 | #### Regular 12 | 13 | Add the following dependency to your `pubspec.yaml` file: 14 | 15 | ``` 16 | dependencies: 17 | sorted_chips_row: ^0.1.0 18 | ``` 19 | 20 | You can read more about adding pub dependencies in [Dart documentation](https://www.dartlang.org/tools/pub/dependencies). 21 | 22 | #### Bleeding edge 23 | 24 | You can also depend on the code from the GitHub repository. To add this package as a dependency from git, add the following under `dependencies` section in your `pubspec.yaml`: 25 | 26 | ``` 27 | sorted_chips_row: 28 | git: 29 | url: https://github.com/callstackincubator/flutter-sorted-chips-row.git 30 | ``` 31 | 32 | By default this dependency will get upgraded whenever a new version is being pushed to the `master` branch. To avoid that, we recommend that you also specify a ref pointing to a commit you verified: 33 | ``` 34 | ref: COMMIT_ID 35 | ``` 36 | 37 | For details see the [Dart documentation on Git dependencies](https://www.dartlang.org/tools/pub/dependencies#git-packages) 38 | 39 | ### Using in code 40 | 41 | The main widget class in this package is [`SortedChipsRow`](https://github.com/callstackincubator/flutter-sorted-chips-row/blob/master/lib/src/sorted_chips_row.dart). See the [library's main file](https://github.com/callstackincubator/flutter-sorted-chips-row/blob/master/lib/sorted_chips_row.dart) for usage example. 42 | 43 | ## Getting Started with Flutter 44 | 45 | This project is a starting point for a Dart [package](https://flutter.dev/developing-packages/), a library module containing code that can be shared easily across multiple Flutter or Dart projects. 46 | 47 | For help getting started with Flutter, view the [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. 48 | -------------------------------------------------------------------------------- /lib/sorted_chips_row.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Callstack.io sp zo.o. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Widget rendering Material Chip widgets in a horizontal, scrollable 16 | /// container, animating the widgets' positions in container according to the 17 | /// provided comparator function. 18 | /// 19 | /// Example usage: 20 | /// 21 | /// ``` 22 | /// import 'package:sorted_chips_row/sorted_chips_row.dart' 23 | /// 24 | /// … 25 | /// 26 | /// ChipSpec chip1 = ChipSpec(label: Text('Label One'), initiallyEnabled: true) 27 | /// ChipSpec chip2 = ChipSpec(label: Text('Label Two')) 28 | /// 29 | /// Comparator chipComparator = (chipState1, chipState2) => … 30 | /// 31 | /// return SortedChipsRow(chips: [chip1, chip2], comparator: chipComparator); 32 | /// ``` 33 | /// 34 | /// Note that the SortedChipsRow needs to have an ancestor providing 35 | /// [Directionality](https://docs.flutter.io/flutter/widgets/Directionality-class.html). 36 | library sorted_chips_row; 37 | 38 | export './src/chip_spec.dart' show ChipSpec; 39 | export './src/sorted_chips_row.dart' show ChipState, SortedChipsRow; 40 | -------------------------------------------------------------------------------- /lib/src/chip_spec.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Callstack.io sp zo.o. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/foundation.dart'; 16 | import 'package:flutter/widgets.dart'; 17 | 18 | /// Defines attributes of a rendered [Chip] widget. 19 | class ChipSpec { 20 | /// [Key] to pass to the underlying [Chip] widget. 21 | final Key key; 22 | 23 | /// Label widget passed to the [Chip] widget. This is usually an instance of 24 | /// [Text]. 25 | final Widget label; 26 | 27 | /// Whether the widget should be initially enabled. 28 | final bool initiallyEnabled; 29 | 30 | /// Style to use for the [label] widget. Only makes sense if the label 31 | /// respects the [DefaultTextStyle]. 32 | final TextStyle labelStyle; 33 | 34 | /// The widget used as the chips avatar. If not specified, a default one 35 | /// showing the 'tick' icon will be used. 36 | final Widget avatar; 37 | 38 | /// Background color to use if the widget is the enabled state. 39 | final Color enabledColor; 40 | 41 | /// Background color to use if the widget is the disabled state. 42 | final Color disabledColor; 43 | 44 | /// Clip behaviour to pass to the [Chip] widget. Defaults to [Clip.none]. 45 | final Clip clipBehaviour; 46 | 47 | /// Elevation to pass to the [Chip] widget. 48 | final double elevation; 49 | 50 | ChipSpec( 51 | {this.key, 52 | @required this.label, 53 | this.initiallyEnabled = false, 54 | this.labelStyle, 55 | this.avatar, 56 | this.enabledColor, 57 | this.disabledColor, 58 | this.clipBehaviour = Clip.none, 59 | this.elevation}); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/sorted_chip.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Callstack.io sp zo.o. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/foundation.dart'; 16 | import 'package:flutter/material.dart'; 17 | import 'package:after_layout/after_layout.dart'; 18 | import './chip_spec.dart'; 19 | 20 | /// Renders [Chip] according to the provided [ChipSpec]. 21 | class SortedChip extends StatefulWidget { 22 | final ChipSpec chipSpec; 23 | final void Function(double) widthCallback; 24 | final bool isEnabled; 25 | 26 | /// Creates the SortedChip widget. 27 | /// 28 | /// This widget primarily draws [Chip] according to the passed `chipSpec`, 29 | /// while choosing the color scheme based on `isEnabled` argument. 30 | /// 31 | /// On first render, the widget will execute the passed `widthCallback` with 32 | /// the value of `context.size.width`. 33 | /// 34 | /// Note that the SortedChip widget is Stateful only to satisfy requirements 35 | /// of [AfterLayoutMixin]. 36 | const SortedChip( 37 | {Key key, 38 | @required this.chipSpec, 39 | @required this.widthCallback, 40 | @required this.isEnabled}) 41 | : super(key: key); 42 | 43 | @override 44 | _SortedChipState createState() => _SortedChipState(); 45 | } 46 | 47 | class _SortedChipState extends State 48 | with AfterLayoutMixin { 49 | static const CHIP_ICON_SIZE = 15.0; 50 | 51 | @override 52 | void afterFirstLayout(BuildContext context) { 53 | this.widget.widthCallback(context.size.width); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | final theme = Theme.of(context); 59 | final Color color = this.widget.isEnabled 60 | ? this.widget.chipSpec.enabledColor ?? theme.buttonColor 61 | : this.widget.chipSpec.disabledColor ?? theme.disabledColor; 62 | 63 | return Material( 64 | type: MaterialType.card, 65 | color: Colors.transparent, 66 | child: Chip( 67 | avatar: this.widget.chipSpec.avatar ?? 68 | DecoratedBox( 69 | position: DecorationPosition.foreground, 70 | decoration: BoxDecoration( 71 | borderRadius: BorderRadius.all(Radius.circular(100.0)), 72 | border: Border.all(color: color)), 73 | child: CircleAvatar( 74 | backgroundColor: this.widget.isEnabled 75 | ? Colors.white 76 | : theme.buttonColor, 77 | child: 78 | Icon(Icons.check, size: CHIP_ICON_SIZE, color: color)), 79 | ), 80 | key: this.widget.key, 81 | backgroundColor: color, 82 | label: this.widget.chipSpec.label, 83 | labelStyle: this.widget.chipSpec.labelStyle, 84 | clipBehavior: this.widget.chipSpec.clipBehaviour, 85 | elevation: this.widget.chipSpec.elevation), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/sorted_chips_row.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Callstack.io sp zo.o. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:flutter/foundation.dart'; 16 | import 'package:flutter/material.dart'; 17 | import './chip_spec.dart'; 18 | import './sorted_chip.dart'; 19 | 20 | /// Encapsulates the current state of the chip being rendered. 21 | /// 22 | /// The primary purpose of this class is to provide all data necessary for the 23 | /// [SortedChipsRow]'s `comparator`. 24 | class ChipState { 25 | /// Spec used in creation of the chip. 26 | final ChipSpec spec; 27 | 28 | /// Initial index of the associated chip's spec in the [SortedChipsRow]'s 29 | /// `chips` parameter. 30 | final int initialIndex; 31 | 32 | /// Actual width of the associated chip, calculated after first layout. 33 | double _width; 34 | 35 | int _currentIndex; 36 | 37 | /// Current position of the associated chip. 38 | int get currentIndex => _currentIndex; 39 | 40 | bool _isEnabled; 41 | 42 | /// Whether the associated chip is in enabled state. 43 | bool get isEnabled => _isEnabled; 44 | 45 | ChipState({@required this.spec, @required this.initialIndex}) 46 | : this._isEnabled = spec.initiallyEnabled, 47 | this._currentIndex = initialIndex, 48 | this._width = 0.0; 49 | } 50 | 51 | /// Renders a row of [Chip]s that get sorted according to the given comparator. 52 | /// 53 | /// The widget renders a horizontal, scrollable row of 54 | /// [Material Chip](https://material.io/design/components/chips.html) widgets 55 | /// that get sorted by the function provided to the constructor. 56 | /// 57 | /// Upon a tap landing on a Chip, the provided `onPress` function is called 58 | /// with [ChipState] associated with that Chip. The call's return value is 59 | /// treated as the new value of a Chip's "enabled" state. If the `onPress` 60 | /// function is not provided by the user, a default one is used, which 61 | /// unconditionally flips the chip's state. 62 | /// 63 | /// If `onPress` returns a different value than stored in [ChipState.isEnabled] 64 | /// then the `comparator` function is executed to sort the Chip widgets. After 65 | /// determining the new positions of the chips, they are being animated to the 66 | /// appropriate places in the row. If no `comparator` parameter is given to the 67 | /// [SortedChipsRow]'s constructor, a default one is used that puts enabled 68 | /// chips first, and maintains the current order of the chips that haven't 69 | /// changed their state. If a chip has become enabled, it is placed last in the 70 | /// group of enabled chips. 71 | /// 72 | /// Currently the chip animation cannot be configured. PRs are welcome to 73 | /// introduce such options. 74 | class SortedChipsRow extends StatefulWidget { 75 | static int _defaultComparator(ChipState a, ChipState b) { 76 | if (a.isEnabled != b.isEnabled) { 77 | return (a.isEnabled ? -1 : 1); 78 | } else { 79 | return (a.currentIndex - b.currentIndex); 80 | } 81 | } 82 | 83 | static bool _defaultOnPress(ChipState state) => 84 | !state._isEnabled; 85 | 86 | final List chips; 87 | final bool Function(ChipState) onPress; 88 | final Comparator comparator; 89 | 90 | /// Creates a SortedChipsRow widget. 91 | SortedChipsRow( 92 | {this.chips = const [], 93 | this.onPress = _defaultOnPress, 94 | this.comparator = _defaultComparator}); 95 | 96 | @override 97 | _SortedChipsRowState createState() { 98 | return new _SortedChipsRowState(); 99 | } 100 | } 101 | 102 | class _SortedChipsRowState extends State 103 | with SingleTickerProviderStateMixin { 104 | static const FIXED_HEIGHT = 60.0; 105 | static const FIXED_HORIZONTAL_PADDING = 8.0; 106 | 107 | final List _chipStates = []; 108 | final List> _chipsAnimations = []; 109 | double _totalWidth = 0.0; 110 | AnimationController _animationController; 111 | 112 | @override 113 | void initState() { 114 | super.initState(); 115 | 116 | this.widget.chips.asMap().forEach((index, spec) { 117 | _chipStates.add(ChipState(spec: spec, initialIndex: index)); 118 | }); 119 | 120 | _chipsAnimations.addAll(List.filled( 121 | this.widget.chips.length, AlwaysStoppedAnimation(RelativeRect.fill))); 122 | 123 | _animationController = AnimationController( 124 | vsync: this, 125 | duration: Duration(milliseconds: 250), 126 | animationBehavior: AnimationBehavior.normal); 127 | _animationController.addListener(() { 128 | setState(() {}); 129 | }); 130 | } 131 | 132 | @override 133 | void dispose() { 134 | _animationController.dispose(); 135 | super.dispose(); 136 | } 137 | 138 | _toggleChip(int chipIndex) { 139 | final ChipState chipState = _chipStates[chipIndex]; 140 | assert(chipState.initialIndex == chipIndex); 141 | 142 | bool nextEnabled = this.widget.onPress(chipState); 143 | if (nextEnabled != chipState._isEnabled) { 144 | chipState._isEnabled = nextEnabled; 145 | var sortedStates = List.of(_chipStates)..sort(this.widget.comparator); 146 | Iterable.generate(sortedStates.length) 147 | .forEach((index) => sortedStates[index]._currentIndex = index); 148 | 149 | double totalOffset = 0.0; 150 | sortedStates.forEach((chipState) { 151 | final chipIndex = chipState.initialIndex; 152 | final currentRect = _chipsAnimations[chipIndex].value; 153 | final targetRect = RelativeRect.fromLTRB(totalOffset, 0.0, 154 | context.size.width - totalOffset - chipState._width, 0.0); 155 | _chipsAnimations[chipIndex] = 156 | RelativeRectTween(begin: currentRect, end: targetRect) 157 | .animate(_animationController); 158 | totalOffset += chipState._width + FIXED_HORIZONTAL_PADDING; 159 | }); 160 | 161 | _animationController 162 | ..reset() 163 | ..animateTo(1.0); 164 | } 165 | } 166 | 167 | void perhapsLayout() { 168 | if (_chipStates.any((chipState) => chipState._width == 0.0)) { 169 | return; 170 | } 171 | 172 | double totalOffset = 0.0; 173 | for (var chipIndex = 0; chipIndex < this.widget.chips.length; chipIndex++) { 174 | final chipRelativeRect = RelativeRect.fromLTRB(totalOffset, 0.0, 175 | context.size.width - totalOffset - _chipStates[chipIndex]._width, 0.0); 176 | _chipsAnimations[chipIndex] = AlwaysStoppedAnimation(chipRelativeRect); 177 | totalOffset += _chipStates[chipIndex]._width + FIXED_HORIZONTAL_PADDING; 178 | } 179 | 180 | this.setState(() { 181 | this._totalWidth = totalOffset - FIXED_HORIZONTAL_PADDING; 182 | }); 183 | } 184 | 185 | @override 186 | Widget build(BuildContext context) { 187 | return SingleChildScrollView( 188 | scrollDirection: Axis.horizontal, 189 | padding: EdgeInsets.symmetric(horizontal: 20.0), 190 | child: LimitedBox( 191 | maxWidth: _totalWidth, 192 | maxHeight: FIXED_HEIGHT, 193 | child: Container( 194 | alignment: Alignment.centerLeft, 195 | height: FIXED_HEIGHT, 196 | child: Stack( 197 | overflow: Overflow.visible, 198 | fit: StackFit.expand, 199 | children: List.of( 200 | Iterable.generate(this.widget.chips.length) 201 | .map((index) { 202 | bool isEnabled = _chipStates[index]._isEnabled; 203 | return PositionedTransition( 204 | key: Key(index.toString()), 205 | child: GestureDetector( 206 | onTap: () { 207 | _toggleChip(index); 208 | }, 209 | child: FittedBox( 210 | alignment: Alignment.centerLeft, 211 | fit: BoxFit.scaleDown, 212 | child: SortedChip( 213 | chipSpec: this.widget.chips[index], 214 | isEnabled: isEnabled, 215 | widthCallback: (width) { 216 | _chipStates[index]._width = width; 217 | perhapsLayout(); 218 | }), 219 | ), 220 | ), 221 | rect: _chipsAnimations[index]); 222 | })))), 223 | ), 224 | ); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sorted_chips_row 2 | description: Material widget displaying a row of Chip buttons that get sorted, 3 | and their position gets animated, according to the given comparison function. 4 | version: 0.1.0+1 5 | author: Krzysztof Sroka 6 | repository: https://github.com/callstackincubator/flutter-sorted-chips-row 7 | homepage: https://github.com/callstackincubator/flutter-sorted-chips-row 8 | 9 | environment: 10 | sdk: ">=2.1.0 <3.0.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | after_layout: ^1.0.7 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | 21 | # For information on the generic Dart part of this file, see the 22 | # following page: https://www.dartlang.org/tools/pub/pubspec 23 | 24 | # The following section is specific to Flutter. 25 | flutter: 26 | 27 | # To add assets to your package, add an assets section, like this: 28 | # assets: 29 | # - images/a_dot_burr.jpeg 30 | # - images/a_dot_ham.jpeg 31 | # 32 | # For details regarding assets in packages, see 33 | # https://flutter.io/assets-and-images/#from-packages 34 | # 35 | # An image asset can refer to one or more resolution-specific "variants", see 36 | # https://flutter.io/assets-and-images/#resolution-aware. 37 | 38 | # To add custom fonts to your package, add a fonts section here, 39 | # in this "flutter" section. Each entry in this list should have a 40 | # "family" key with the font family name, and a "fonts" key with a 41 | # list giving the asset and other descriptors for the font. For 42 | # example: 43 | # fonts: 44 | # - family: Schyler 45 | # fonts: 46 | # - asset: fonts/Schyler-Regular.ttf 47 | # - asset: fonts/Schyler-Italic.ttf 48 | # style: italic 49 | # - family: Trajan Pro 50 | # fonts: 51 | # - asset: fonts/TrajanPro.ttf 52 | # - asset: fonts/TrajanPro_Bold.ttf 53 | # weight: 700 54 | # 55 | # For details regarding fonts in packages, see 56 | # https://flutter.io/custom-fonts/#from-packages 57 | -------------------------------------------------------------------------------- /sorted_chips_row.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/sorted_chips_row_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:sorted_chips_row/sorted_chips_row.dart'; 5 | 6 | void main() { 7 | var findChipByLabelAndColor = (Text label, Color color) => find.byWidgetPredicate( 8 | (widget) => 9 | widget is Chip && 10 | widget.label == label && 11 | widget.backgroundColor == color); 12 | 13 | group('SortedChipsRow', () { 14 | testWidgets("doesn\'t render Chips if passed empty list", 15 | (WidgetTester wt) async { 16 | await wt.pumpWidget(MaterialApp(home: SortedChipsRow(chips: []))); 17 | expect(find.byType(Chip), findsNothing); 18 | }); 19 | 20 | testWidgets("renders single Chip with given label", 21 | (WidgetTester wt) async { 22 | final Text label = Text('Test Label'); 23 | await wt.pumpWidget( 24 | MaterialApp(home: SortedChipsRow(chips: [ChipSpec(label: label)]))); 25 | 26 | expect( 27 | find.byWidgetPredicate( 28 | (widget) => widget is Chip && widget.label == label), 29 | findsOneWidget); 30 | }); 31 | 32 | testWidgets("renders single Chip with other attributes", 33 | (WidgetTester wt) async { 34 | final ChipSpec spec = ChipSpec( 35 | label: Text('Foo bar'), 36 | labelStyle: TextStyle(color: Colors.amber), 37 | avatar: CircleAvatar(child: Text('F')), 38 | clipBehaviour: Clip.hardEdge, 39 | enabledColor: Colors.white, 40 | disabledColor: Colors.black, 41 | elevation: 100.0, 42 | initiallyEnabled: true, 43 | key: Key("Chip Key")); 44 | 45 | await wt.pumpWidget(MaterialApp(home: SortedChipsRow(chips: [spec]))); 46 | 47 | expect( 48 | find.byWidgetPredicate((widget) => 49 | widget is Chip && 50 | widget.label == spec.label && 51 | widget.labelStyle == spec.labelStyle && 52 | widget.avatar == spec.avatar && 53 | widget.clipBehavior == spec.clipBehaviour && 54 | widget.backgroundColor == spec.enabledColor && 55 | widget.elevation == spec.elevation), 56 | findsOneWidget); 57 | }); 58 | 59 | testWidgets("Rerenders chips according to default onPress function", 60 | (WidgetTester wt) async { 61 | final ChipSpec spec1 = ChipSpec( 62 | label: Text('Foo Bar'), 63 | initiallyEnabled: false, 64 | enabledColor: Colors.white, 65 | disabledColor: Colors.black); 66 | final ChipSpec spec2 = ChipSpec( 67 | label: Text('Bar Baz'), 68 | initiallyEnabled: true, 69 | enabledColor: Colors.white, 70 | disabledColor: Colors.black); 71 | 72 | await wt.pumpWidget(MaterialApp( 73 | home: Center(child: SortedChipsRow(chips: [spec1, spec2])))); 74 | await wt.pumpAndSettle(); 75 | 76 | expect(find.byType(Chip), findsNWidgets(2)); 77 | expect(findChipByLabelAndColor(spec1.label, Colors.black), findsOneWidget); 78 | expect(findChipByLabelAndColor(spec2.label, Colors.white), findsOneWidget); 79 | 80 | await wt.tap(find.byType(Chip).first); 81 | await wt.tap(find.byType(Chip).last); 82 | await wt.pumpAndSettle(); 83 | 84 | expect(findChipByLabelAndColor(spec1.label, Colors.white), findsOneWidget); 85 | expect(findChipByLabelAndColor(spec2.label, Colors.black), findsOneWidget); 86 | }); 87 | }); 88 | } 89 | --------------------------------------------------------------------------------