Repository: Dn-a/flutter_tags Branch: master Commit: cf6d3f861354 Files: 35 Total size: 158.7 KB Directory structure: gitextract_ehrk8p54/ ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── ios/ │ │ ├── Flutter/ │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── flutter_export_environment.sh │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── README.md │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ └── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ └── Runner.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── lib/ │ │ └── main.dart │ ├── pubspec.yaml │ └── test/ │ └── widget_test.dart ├── flutter_tags.iml ├── lib/ │ ├── flutter_tags.dart │ └── src/ │ ├── item_tags.dart │ ├── suggestions_textfield.dart │ ├── tags.dart │ └── util/ │ └── custom_wrap.dart ├── pubspec.yaml └── test/ └── flutter_tags_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store .dart_tool/ .packages .pub/ build/ ios/.generated/ ios/Flutter/Generated.xcconfig ios/Runner/GeneratedPluginRegistrant.* lib/generated/ res/ .idea/ android/ lib/selectable_tags_back.dart example/lib/main_back.dart ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: d48e6e433cc5ca67b24b19f70aaa197e84ba63c1 channel: beta project_type: package ================================================ FILE: CHANGELOG.md ================================================ ## [0.4.9+1] - 2020-11-11. * `[Fixed]` - Issue #66. * `[Fixed]` - `DataList` null id. ## [0.4.9] - 2020-09-15. * `[Added]` - Possibility to to define the padding of the `textField`. ## [0.4.8+2] - 2020-06-02. * `[Updated]` - Documentation. ## [0.4.8+1] - 2020-05-13. * `[Updated]` - Documentation. ## [0.4.8] - 2020-03-07. * `[Updated]` - Documentation. ## [0.4.7] - 2020-02-26. * `[Updated]` - Documentation. * General improvement of the code. ## [0.4.6] - 2020-02-26. * `[Fixed]` - some problem. * `[Added]` - Possibility to disabled/enabled `textField` field - Issue #36. * `[Added]` - Possibility to insert tags not present in the list of suggestions with the `constraintSuggestion` field - Issue #33. * `onRemoved` field has been moved inside `ItemTagsRemoveButton()` to maintain consistency. * `onRemoved` now has a bool return type (ex: when you want to add a control before removing a tag). ## [0.4.5] - 2019-10-30. * Renamed `TagstextField` to `TagsTextField`. * Added `textCapitalization` field in `TagsTextField`. ## [0.4.4] - 2019-10-16. * New Features and Documentation Updates. * Added `child` field in `ItemTagsImage()`, allows more control over the images. * Fixed alignment `textField` and `suggestions` - Issue #31. * Added the possibility to get a list of all the `ItemTags` via GlobalKey. ## [0.4.3] - 2019-07-28. * Minor Update Documentation. * Fixed some problem. * removed the 'position' parameter in TagsTexField () because it is not essential. the same result can be obtained by setting the verticalDirection and TextDirection parameters in Tags (). ## [0.4.2] - 2019-07-27. * Update Documentation. * General improvement of the code. ## [0.4.1] - 2019-07-27. * Minor Update Documentation. ## [0.4.0] - 2019-07-27. * **Improvements, new Structure, new Features and Documentation Updates** * * `SelectableTags` and `InputTags`have been removed, now there is only one widget. **Tags()** * Now it is possible to personalize every single tag, with the possibility of adding icons, images and a removal button. * Possibility to display the list with horizontal scroll. ## [0.3.2] - 2019-06-20. * Issue #14 and #13 fixed. * Added `customData` field in Tag Class. ## [0.3.1] - 2019-05-13. * General improvement of the code. ## [0.3.0] - 2019-04-09. * **New features** * Possibility to create a customizable popupMenu. * **SelectableTags**. Possibility to set a color and an activeColor for each tag. * **InputTags**. Possibility to hide textField. ## [0.2.4] - 2019-04-08. * fixed some problem. ## [0.2.3] - 2019-04-05. * General improvement of the code. * OnInsert, onDelete and onPressed are now optional. ## [0.2.2] - 2019-03-02. * Added property `textStyle` in InputTags. NOTE: `textColor` has been removed. now it can be set with `textStyle`. * added property `textStyle` in SelectableTags. NOTE: if you set `color` in it will be ignored, you must use `textColor` `textActiveColor`. * Created InputSuggestions. Return suggestions in the TextField. Is not complete, soon the list of suggestions will be implemented. * General improvement of the code. ## [0.2.1] - 2019-03-01. * The code has been largely rewritten. * Now the Tag width calculation is very accurate. ## [0.2.0] - 2019-02-24. * Improved tag width calculation; * Possibility to change the margin and padding of the close icon ( InputTags ). ## [0.1.9] - 2019-02-23. * Width calculated based on the byte length of the title; * When the orientation changes, a recalculation of the screen width is performed. ## [0.1.8] - 2019-02-16. * Improvement of library documentation. ## [0.1.7] - 2019-02-07. * Added new feature SingleItem on SelectableTags; * Possibility to change color/background-color icon on InputTags; - General improvement of the code. ## [0.1.6] - 2019-01-07. * Fixed error "Infinity or NaN toInt" on InputTags; - general improvement of the code. ## [0.1.5] - 2018-12-24. * General improvement of the code. ## [0.1.4] - 2018-12-18. * Added new features. ## [0.1.3] - 2018-12-16. * Added new highlight feature (InputTags) - general improvement of the code. ## [0.1.2] - 2018-12-15. * Add InputTags Widget - Improved documentation. ## [0.1.1] - 2018-12-08. * Improved documentation. ## [0.1.0] - 2018-12-08. * Did some changing readme. ## [0.0.1] - 2018-12-08. * Created Selectable Tags. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Antonino Di Natale Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE ================================================ FILE: README.md ================================================ # flutter_tags [![pub package](https://img.shields.io/badge/pub-0.4.9+1-orange.svg)](https://pub.dartlang.org/packages/flutter_tags) [![Awesome Flutter](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter#ui) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/dnag88) Create beautiful tags quickly and easily. ## Installing Add this to your package's pubspec.yaml file: ```dart dependencies: flutter_tags: "^0.4.9+1" ``` ## DEMO
## Simple usage ```dart import 'package:flutter_tags/flutter_tags.dart'; . . . List _items; double _fontSize = 14; @override void initState(){ super.initState(); // if you store data on a local database (sqflite), then you could do something like this Model().getItems().then((items){ _items = items; }); } @override Widget build(BuildContext context) { return Tags( key:_tagStateKey, textField: TagsTextField( textStyle: TextStyle(fontSize: _fontSize), constraintSuggestion: true, suggestions: [], //width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 10), onSubmitted: (String str) { // Add item to the data source. setState(() { // required _items.add(str); }); }, ), itemCount: _items.length, // required itemBuilder: (int index){ final item = _items[index]; return ItemTags( // Each ItemTags must contain a Key. Keys allow Flutter to // uniquely identify widgets. key: Key(index.toString()), index: index, // required title: item.title, active: item.active, customData: item.customData, textStyle: TextStyle( fontSize: _fontSize, ), combine: ItemTagsCombine.withTextBefore, image: ItemTagsImage( image: AssetImage("img.jpg") // OR NetworkImage("https://...image.png") ), // OR null, icon: ItemTagsIcon( icon: Icons.add, ), // OR null, removeButton: ItemTagsRemoveButton( onRemoved: (){ // Remove the item from the data source. setState(() { // required _items.removeAt(index); }); //required return true; }, ), // OR null, onPressed: (item) => print(item), onLongPressed: (item) => print(item), ); }, ); } final GlobalKey _tagStateKey = GlobalKey(); // Allows you to get a list of all the ItemTags _getAllItem(){ List lst = _tagStateKey.currentState?.getAllItem; if(lst!=null) lst.where((a) => a.active==true).forEach( ( a) => print(a.title)); } ``` ## Wrapped widget example You are free to wrap ItemTags () inside another widget ```dart Tags( itemCount: items.length, itemBuilder: (int index){ return Tooltip( message: item.title, child:ItemTags( title:item.title, ) ); }, ); ``` ### Tags() parameters |PropName|Description|default value| |:-------|:----------|:------------| |`columns`|*Possibility to set number of columns when necessary*|null| |`itemCount`|*Tag number to display*|required| |`symmetry`|*Ability to view and scroll tags horizontally*|false| |`horizontalScroll`|*Offset drawer width*|0.4| |`heightHorizontalScroll`|*height for HorizontalScroll to set to display tags correctly*|60| |`spacing`|*Horizontal space between the tags*|6| |`runSpacing`|*Vertical space between the tags*|14| |`alignment`|*Horizontal WrapAlignment*|WrapAlignment.center| |`runAlignment`|*Vertical WrapAlignment*|WrapAlignment.center| |`direction`|*Direction of the ItemTags*|Axis.horizontal| |`verticalDirection`|*Iterate Item from the lower to the upper direction or vice versa*|VerticalDirection.down| |`textDirection`|*Text direction of the ItemTags*|TextDirection.ltr| |`itemBuilder`|*tag generator*|| |`textField`|*add textField*|TagsTextFiled()| ### ItemTags() parameters * `index` - *required* * `title` - *required* * `textScaleFactor` - *custom textScaleFactor* * `active` - *bool value (default true)* * `pressEnabled` - *active onPress tag ( default true)* * `customData` - *Possibility to add any custom value in customData field, you can retrieve this later. A good example: store an id from Firestore document.* * `textStyle` - *textStyle()* * `alignment` - *MainAxisAlignment ( default MainAxisAlignment.center)* * `combine` - * ability to combine text, icons, images in different ways ( default ItemTagsCombine.imageOrIconOrText)* * `icon` - *ItemTagsIcon()* * `image` - *ItemTagsImage()* * `removeButton` - *ItemTagsRemoveButton()* * `borderRadius` - *BorderRadius* * `border` - *custom border-side* * `padding` - *default EdgeInsets.symmetric(horizontal: 7, vertical: 5)* * `elevation` - *default 5* * `singleItem` - *default false* * `textOverflow` - *default TextOverflow.fade* * `textColor` - *default Colors.black* * `textActiveColor` - *default Colors.white* * `color` - *default Colors.white* * `activeColor` - *default Colors.blueGrey* * `highlightColor` - * `splashColor` - * `colorShowDuplicate` - *default Colors.red* * `onPressed` - *callback* * `onLongPressed` - *callback* * `onRemoved` - *callback* ## Donate It takes time to carry on this project. If you found it useful or learned something from the source code please consider the idea of donating 5, 20, 50 € or whatever you can to support the project. - [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/dnag88) ## Issues If you encounter problems, open an issue. Pull request are also welcome. ================================================ FILE: example/.gitignore ================================================ # Miscellaneous *.class *.lock *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # Visual Studio Code related .vscode/ # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .packages .pub-cache/ .pub/ build/ # Android related **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages ================================================ FILE: example/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b channel: beta project_type: app ================================================ FILE: example/README.md ================================================ # Example Flutter Tags An example of how you could implement it. ## Getting Started - Selectable Tags ```dart import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_tags/flutter_tags.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Tags Demo', theme: ThemeData( primarySwatch: Colors.blueGrey, ), home: MyHomePage(title: 'Flutter Tags'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State with SingleTickerProviderStateMixin { TabController _tabController; ScrollController _scrollViewController; final List _list = [ '0', 'SDK', 'plugin updates', 'Facebook', '哔了狗了QP又不够了', 'Kirchhoff', 'Italy', 'France', 'Spain', '美', 'Dart', 'SDK', 'Foo', 'Select', 'lorem ip', '9', 'Star', 'Flutter Selectable Tags', '1', 'Hubble', '2', 'Input flutter tags', 'A B C', '8', 'Android Studio developer', 'welcome to the jungle', 'Gauss', '美术', '互联网', '炫舞时代', '篝火营地', ]; bool _symmetry = false; bool _removeButton = true; bool _singleItem = false; bool _startDirection = false; bool _horizontalScroll = false; bool _withSuggesttions = false; int _count = 0; int _column = 0; double _fontSize = 14; String _itemCombine = 'withTextBefore'; String _onPressed = ''; List _icon = [Icons.home, Icons.language, Icons.headset]; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _scrollViewController = ScrollController(); _items = _list.toList(); } List _items; final GlobalKey _tagStateKey = GlobalKey(); @override Widget build(BuildContext context) { //List lst = _tagStateKey.currentState?.getAllItem; lst.forEach((f) => print(f.title)); return Scaffold( body: NestedScrollView( controller: _scrollViewController, headerSliverBuilder: (BuildContext context, bool boxIsScrolled) { return [ SliverAppBar( title: Text("flutter tags"), centerTitle: true, pinned: true, expandedHeight: 0, floating: true, forceElevated: boxIsScrolled, bottom: TabBar( isScrollable: false, indicatorSize: TabBarIndicatorSize.label, labelStyle: TextStyle(fontSize: 18.0), tabs: [ Tab(text: "Demo 1"), Tab(text: "Demo 2"), ], controller: _tabController, ), ) ]; }, body: TabBarView( controller: _tabController, children: [ CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ Container( decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey[300], width: 0.5))), margin: EdgeInsets.symmetric(horizontal: 10, vertical: 10), child: ExpansionTile( title: Text("Settings"), children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _removeButton, onChanged: (a) { setState(() { _removeButton = !_removeButton; }); }), Text('Remove Button') ], ), onTap: () { setState(() { _removeButton = !_removeButton; }); }, ), Padding( padding: EdgeInsets.all(5), ), GestureDetector( child: Row( children: [ Checkbox( value: _symmetry, onChanged: (a) { setState(() { _symmetry = !_symmetry; }); }), Text('Symmetry') ], ), onTap: () { setState(() { _symmetry = !_symmetry; }); }, ), Padding( padding: EdgeInsets.all(5), ), DropdownButton( hint: _column == 0 ? Text("Not set") : Text(_column.toString()), items: _buildItems(), onChanged: (a) { setState(() { _column = a; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _horizontalScroll, onChanged: (a) { setState(() { _horizontalScroll = !_horizontalScroll; }); }), Text('Horizontal scroll') ], ), onTap: () { setState(() { _horizontalScroll = !_horizontalScroll; }); }, ), GestureDetector( child: Row( children: [ Checkbox( value: _singleItem, onChanged: (a) { setState(() { _singleItem = !_singleItem; }); }), Text('Single Item') ], ), onTap: () { setState(() { _singleItem = !_singleItem; }); }, ), ], ), Column( children: [ Text('Font Size'), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Slider( value: _fontSize, min: 6, max: 30, onChanged: (a) { setState(() { _fontSize = (a.round()).toDouble(); }); }, ), Text(_fontSize.toString()), Padding( padding: EdgeInsets.symmetric(horizontal: 20), ), Container( height: 30, width: 30, //color: Colors.blueGrey, child: IconButton( padding: EdgeInsets.all(0), //color: Colors.white, icon: Icon(Icons.add), onPressed: () { setState(() { _count++; _items.add(_count.toString()); //_items.removeAt(3); _items.removeAt(10); }); }, ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 5), ), Container( height: 30, width: 30, //color: Colors.grey, child: IconButton( padding: EdgeInsets.all(0), //color: Colors.white, icon: Icon(Icons.refresh), onPressed: () { setState(() { _items = _list.toList(); }); }, ), ), ], ), ], ), ], ), ), Padding( padding: EdgeInsets.all(20), ), _tags1, Container( padding: EdgeInsets.all(20), child: Column( children: [ Divider( color: Colors.blueGrey, ), Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Text(_onPressed), ), ], )), ])), ], ), CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ Container( decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey[300], width: 0.5))), margin: EdgeInsets.symmetric(horizontal: 10, vertical: 10), child: ExpansionTile( title: Text("Settings"), children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _withSuggesttions, onChanged: (a) { setState(() { _withSuggesttions = !_withSuggesttions; }); }), Text('Suggestions') ], ), onTap: () { setState(() { _withSuggesttions = !_withSuggesttions; }); }, ), Padding( padding: EdgeInsets.all(20), ), DropdownButton( hint: Text(_itemCombine), items: _buildItems2(), onChanged: (val) { setState(() { _itemCombine = val; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _horizontalScroll, onChanged: (a) { setState(() { _horizontalScroll = !_horizontalScroll; }); }), Text('Horizontal scroll') ], ), onTap: () { setState(() { _horizontalScroll = !_horizontalScroll; }); }, ), GestureDetector( child: Row( children: [ Checkbox( value: _startDirection, onChanged: (a) { setState(() { _startDirection = !_startDirection; }); }), Text('Start Direction') ], ), onTap: () { setState(() { _startDirection = !_startDirection; }); }, ), ], ), Column( children: [ Text('Font Size'), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Slider( value: _fontSize, min: 6, max: 30, onChanged: (a) { setState(() { _fontSize = (a.round()).toDouble(); }); }, ), Text(_fontSize.toString()), ], ), ], ), ], ), ), Padding( padding: EdgeInsets.all(20), ), _tags2, Container( padding: EdgeInsets.all(20), child: Column( children: [ Divider( color: Colors.blueGrey, ), Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Text(_onPressed), ), ], )), ])), ], ), ], )), ); } Widget get _tags1 { return Tags( key: _tagStateKey, symmetry: _symmetry, columns: _column, horizontalScroll: _horizontalScroll, //verticalDirection: VerticalDirection.up, textDirection: TextDirection.rtl, heightHorizontalScroll: 60 * (_fontSize / 14), itemCount: _items.length, itemBuilder: (index) { final item = _items[index]; return ItemTags( key: Key(index.toString()), index: index, title: item, pressEnabled: true, activeColor: Colors.blueGrey[600], singleItem: _singleItem, splashColor: Colors.green, combine: ItemTagsCombine.withTextBefore, image: index > 0 && index < 5 ? ItemTagsImage( //image: AssetImage("img/p$index.jpg"), child: Image.network( "http://www.clipartpanda.com/clipart_images/user-66327738/download", width: 16 * _fontSize / 14, height: 16 * _fontSize / 14, )) : (1 == 1 ? ItemTagsImage( image: NetworkImage( "https://d32ogoqmya1dw8.cloudfront.net/images/serc/empty_user_icon_256.v2.png"), ) : null), icon: (item == '0' || item == '1' || item == '2') ? ItemTagsIcon( icon: _icon[int.parse(item)], ) : null, removeButton: _removeButton ? ItemTagsRemoveButton( onRemoved: () { setState(() { _items.removeAt(index); }); return true; }, ) : null, textScaleFactor: utf8.encode(item.substring(0, 1)).length > 2 ? 0.8 : 1, textStyle: TextStyle( fontSize: _fontSize, ), onPressed: (item) => print(item), ); }, ); } // Position for popup menu Offset _tapPosition; Widget get _tags2 { //popup Menu final RenderBox overlay = Overlay.of(context).context?.findRenderObject(); ItemTagsCombine combine = ItemTagsCombine.onlyText; switch (_itemCombine) { case 'onlyText': combine = ItemTagsCombine.onlyText; break; case 'onlyIcon': combine = ItemTagsCombine.onlyIcon; break; case 'onlyIcon': combine = ItemTagsCombine.onlyIcon; break; case 'onlyImage': combine = ItemTagsCombine.onlyImage; break; case 'imageOrIconOrText': combine = ItemTagsCombine.imageOrIconOrText; break; case 'withTextAfter': combine = ItemTagsCombine.withTextAfter; break; case 'withTextBefore': combine = ItemTagsCombine.withTextBefore; break; } return Tags( key: Key("2"), symmetry: _symmetry, columns: _column, horizontalScroll: _horizontalScroll, verticalDirection: _startDirection ? VerticalDirection.up : VerticalDirection.down, textDirection: _startDirection ? TextDirection.rtl : TextDirection.ltr, heightHorizontalScroll: 60 * (_fontSize / 14), textField: _textField, itemCount: _items.length, itemBuilder: (index) { final item = _items[index]; return GestureDetector( child: ItemTags( key: Key(index.toString()), index: index, title: item, pressEnabled: false, activeColor: Colors.green[400], combine: combine, image: index > 0 && index < 5 ? ItemTagsImage(image: AssetImage("img/p$index.jpg")) : (1 == 1 ? ItemTagsImage( image: NetworkImage( "https://image.flaticon.com/icons/png/512/44/44948.png")) : null), icon: (item == '0' || item == '1' || item == '2') ? ItemTagsIcon( icon: _icon[int.parse(item)], ) : null, removeButton: ItemTagsRemoveButton( backgroundColor: Colors.green[900], onRemoved: () { setState(() { _items.removeAt(index); }); return true; }, ), textScaleFactor: utf8.encode(item.substring(0, 1)).length > 2 ? 0.8 : 1, textStyle: TextStyle( fontSize: _fontSize, ), ), onTapDown: (details) => _tapPosition = details.globalPosition, onLongPress: () { showMenu( //semanticLabel: item, items: [ PopupMenuItem( child: Text(item, style: TextStyle(color: Colors.blueGrey)), enabled: false, ), PopupMenuDivider(), PopupMenuItem( value: 1, child: Row( children: [ Icon(Icons.content_copy), Text("Copy text"), ], ), ), ], context: context, position: RelativeRect.fromRect( _tapPosition & Size(40, 40), Offset.zero & overlay .size) // & RelativeRect.fromLTRB(65.0, 40.0, 0.0, 0.0), ) .then((value) { if (value == 1) Clipboard.setData(ClipboardData(text: item)); }); }, ); }, ); } TagsTextField get _textField { return TagsTextField( autofocus: false, //width: double.infinity, //padding: EdgeInsets.symmetric(horizontal: 10), textStyle: TextStyle( fontSize: _fontSize, //height: 1 ), enabled: true, constraintSuggestion: true, suggestions: _withSuggesttions ? [ "One", "two", "android", "Dart", "flutter", "test", "tests", "androids", "androidsaaa", "Test", "suggest", "suggestions", "互联网", "last", "lest", "炫舞时代" ] : null, onSubmitted: (String str) { setState(() { _items.add(str); }); }, ); } List _buildItems() { List list = []; int count = 19; list.add( DropdownMenuItem( child: Text("Not set"), value: 0, ), ); for (int i = 1; i < count; i++) list.add( DropdownMenuItem( child: Text(i.toString()), value: i, ), ); return list; } List _buildItems2() { List list = []; list.add(DropdownMenuItem( child: Text("onlyText"), value: 'onlyText', )); list.add(DropdownMenuItem( child: Text("onlyIcon"), value: 'onlyIcon', )); list.add(DropdownMenuItem( child: Text("onlyImage"), value: 'onlyImage', )); list.add(DropdownMenuItem( child: Text("imageOrIconOrText"), value: 'imageOrIconOrText', )); list.add(DropdownMenuItem( child: Text("withTextBefore"), value: 'withTextBefore', )); list.add(DropdownMenuItem( child: Text("withTextAfter"), value: 'withTextAfter', )); return list; } } ``` ## DEMO ![Demo 1](https://github.com/Dn-a/flutter_tags/raw/master/repo-file/img/example0.4.0_1.gif) ![Demo 1](https://github.com/Dn-a/flutter_tags/raw/master/repo-file/img/example0.4.0_2.gif) ## Other This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.io/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: example/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 8.0 ================================================ FILE: example/ios/Flutter/Debug.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: example/ios/Flutter/Release.xcconfig ================================================ #include "Generated.xcconfig" ================================================ FILE: example/ios/Flutter/flutter_export_environment.sh ================================================ #!/bin/sh # This is a generated file; do not edit or check into version control. export "FLUTTER_ROOT=C:\flutter" export "FLUTTER_APPLICATION_PATH=D:\Android\Flutter\package\flutter_tags\example" export "FLUTTER_TARGET=lib\main.dart" export "FLUTTER_BUILD_DIR=build" export "SYMROOT=${SOURCE_ROOT}/../build\ios" export "OTHER_LDFLAGS=$(inherited) -framework Flutter" export "FLUTTER_FRAMEWORK_DIR=C:\flutter\bin\cache\artifacts\engine\ios" export "FLUTTER_BUILD_NAME=1.0.0" export "FLUTTER_BUILD_NUMBER=1" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=false" export "TREE_SHAKE_ICONS=false" export "PACKAGE_CONFIG=.packages" ================================================ FILE: example/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. 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. ================================================ FILE: example/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: example/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: example/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName example_flutter_tags CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance ================================================ FILE: example/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: example/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; 97C146F11CF9000F007C117D /* Supporting Files */ = { isa = PBXGroup; children = ( ); name = "Supporting Files"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 0910; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 0910; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = S8QB4VV633; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.dna.exampleFlutterTags; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.dna.exampleFlutterTags; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_VERSION = 4.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.dna.exampleFlutterTags; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_VERSION = 4.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: example/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/lib/main.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_tags/flutter_tags.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Tags Demo', theme: ThemeData( primarySwatch: Colors.blueGrey, ), home: MyHomePage(title: 'Flutter Tags'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State with SingleTickerProviderStateMixin { TabController _tabController; ScrollController _scrollViewController; final List _list = [ '0', 'SDK', 'plugin updates', 'Facebook', '哔了狗了QP又不够了', 'Kirchhoff', 'Italy', 'France', 'Spain', '美', 'Dart', 'SDK', 'Foo', 'Select', 'lorem ip', '9', 'Star', 'Flutter Selectable Tags', '1', 'Hubble', '2', 'Input flutter tags', 'A B C', '8', 'Android Studio developer', 'welcome to the jungle', 'Gauss', '美术', '互联网', '炫舞时代', '篝火营地', ]; bool _symmetry = false; bool _removeButton = true; bool _singleItem = true; bool _startDirection = false; bool _horizontalScroll = true; bool _withSuggesttions = false; int _count = 0; int _column = 0; double _fontSize = 14; String _itemCombine = 'withTextBefore'; String _onPressed = ''; List _icon = [Icons.home, Icons.language, Icons.headset]; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _scrollViewController = ScrollController(); _items = _list.toList(); } List _items; final GlobalKey _tagStateKey = GlobalKey(); @override Widget build(BuildContext context) { //List lst = _tagStateKey.currentState?.getAllItem; lst.forEach((f) => print(f.title)); return Scaffold( body: NestedScrollView( controller: _scrollViewController, headerSliverBuilder: (BuildContext context, bool boxIsScrolled) { return [ SliverAppBar( title: Text("flutter tags"), centerTitle: true, pinned: true, expandedHeight: 0, floating: true, forceElevated: boxIsScrolled, bottom: TabBar( isScrollable: false, indicatorSize: TabBarIndicatorSize.label, labelStyle: TextStyle(fontSize: 18.0), tabs: [ Tab(text: "Demo 1"), Tab(text: "Demo 2"), ], controller: _tabController, ), ) ]; }, body: TabBarView( controller: _tabController, children: [ CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ Container( decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey[300], width: 0.5))), margin: EdgeInsets.symmetric(horizontal: 10, vertical: 10), child: ExpansionTile( title: Text("Settings"), children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _removeButton, onChanged: (a) { setState(() { _removeButton = !_removeButton; }); }), Text('Remove Button') ], ), onTap: () { setState(() { _removeButton = !_removeButton; }); }, ), Padding( padding: EdgeInsets.all(5), ), GestureDetector( child: Row( children: [ Checkbox( value: _symmetry, onChanged: (a) { setState(() { _symmetry = !_symmetry; }); }), Text('Symmetry') ], ), onTap: () { setState(() { _symmetry = !_symmetry; }); }, ), Padding( padding: EdgeInsets.all(5), ), DropdownButton( hint: _column == 0 ? Text("Not set") : Text(_column.toString()), items: _buildItems(), onChanged: (a) { setState(() { _column = a; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _horizontalScroll, onChanged: (a) { setState(() { _horizontalScroll = !_horizontalScroll; }); }), Text('Horizontal scroll') ], ), onTap: () { setState(() { _horizontalScroll = !_horizontalScroll; }); }, ), GestureDetector( child: Row( children: [ Checkbox( value: _singleItem, onChanged: (a) { setState(() { _singleItem = !_singleItem; }); }), Text('Single Item') ], ), onTap: () { setState(() { _singleItem = !_singleItem; }); }, ), ], ), Column( children: [ Text('Font Size'), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Slider( value: _fontSize, min: 6, max: 30, onChanged: (a) { setState(() { _fontSize = (a.round()).toDouble(); }); }, ), Text(_fontSize.toString()), Padding( padding: EdgeInsets.symmetric(horizontal: 20), ), Container( height: 30, width: 30, //color: Colors.blueGrey, child: IconButton( padding: EdgeInsets.all(0), //color: Colors.white, icon: Icon(Icons.add), onPressed: () { setState(() { _count++; _items.add(_count.toString()); //_items.removeAt(3); _items.removeAt(10); }); }, ), ), Padding( padding: EdgeInsets.symmetric(horizontal: 5), ), Container( height: 30, width: 30, //color: Colors.grey, child: IconButton( padding: EdgeInsets.all(0), //color: Colors.white, icon: Icon(Icons.refresh), onPressed: () { setState(() { _items = _list.toList(); }); }, ), ), ], ), ], ), ], ), ), Padding( padding: EdgeInsets.all(20), ), _tags1, Container( padding: EdgeInsets.all(20), child: Column( children: [ Divider( color: Colors.blueGrey, ), Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Text(_onPressed), ), ], )), ])), ], ), CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ Container( decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.grey[300], width: 0.5))), margin: EdgeInsets.symmetric(horizontal: 10, vertical: 10), child: ExpansionTile( title: Text("Settings"), children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _withSuggesttions, onChanged: (a) { setState(() { _withSuggesttions = !_withSuggesttions; }); }), Text('Suggestions') ], ), onTap: () { setState(() { _withSuggesttions = !_withSuggesttions; }); }, ), Padding( padding: EdgeInsets.all(20), ), DropdownButton( hint: Text(_itemCombine), items: _buildItems2(), onChanged: (val) { setState(() { _itemCombine = val; }); }, ), ], ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Row( children: [ Checkbox( value: _horizontalScroll, onChanged: (a) { setState(() { _horizontalScroll = !_horizontalScroll; }); }), Text('Horizontal scroll') ], ), onTap: () { setState(() { _horizontalScroll = !_horizontalScroll; }); }, ), GestureDetector( child: Row( children: [ Checkbox( value: _startDirection, onChanged: (a) { setState(() { _startDirection = !_startDirection; }); }), Text('Start Direction') ], ), onTap: () { setState(() { _startDirection = !_startDirection; }); }, ), ], ), Column( children: [ Text('Font Size'), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Slider( value: _fontSize, min: 6, max: 30, onChanged: (a) { setState(() { _fontSize = (a.round()).toDouble(); }); }, ), Text(_fontSize.toString()), ], ), ], ), ], ), ), Padding( padding: EdgeInsets.all(20), ), _tags2, Container( padding: EdgeInsets.all(20), child: Column( children: [ Divider( color: Colors.blueGrey, ), Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Text(_onPressed), ), ], )), ])), ], ), ], )), ); } Widget get _tags1 { return Tags( key: _tagStateKey, symmetry: _symmetry, columns: _column, horizontalScroll: _horizontalScroll, //verticalDirection: VerticalDirection.up, textDirection: TextDirection.rtl, heightHorizontalScroll: 60 * (_fontSize / 14), itemCount: _items.length, itemBuilder: (index) { final item = _items[index]; return ItemTags( key: Key(index.toString()), index: index, title: item, pressEnabled: true, activeColor: Colors.blueGrey[600], singleItem: _singleItem, splashColor: Colors.green, combine: ItemTagsCombine.withTextBefore, image: index > 0 && index < 5 ? ItemTagsImage( //image: AssetImage("img/p$index.jpg"), child: Image.network( "http://www.clipartpanda.com/clipart_images/user-66327738/download", width: 16 * _fontSize / 14, height: 16 * _fontSize / 14, )) : (1 == 1 ? ItemTagsImage( image: NetworkImage( "https://d32ogoqmya1dw8.cloudfront.net/images/serc/empty_user_icon_256.v2.png"), ) : null), icon: (item == '0' || item == '1' || item == '2') ? ItemTagsIcon( icon: _icon[int.parse(item)], ) : null, removeButton: _removeButton ? ItemTagsRemoveButton( onRemoved: () { setState(() { _items.removeAt(index); }); return true; }, ) : null, textScaleFactor: utf8.encode(item.substring(0, 1)).length > 2 ? 0.8 : 1, textStyle: TextStyle( fontSize: _fontSize, ), onPressed: (item) => print(item), ); }, ); } // Position for popup menu Offset _tapPosition; Widget get _tags2 { //popup Menu final RenderBox overlay = Overlay.of(context).context?.findRenderObject(); ItemTagsCombine combine = ItemTagsCombine.onlyText; switch (_itemCombine) { case 'onlyText': combine = ItemTagsCombine.onlyText; break; case 'onlyIcon': combine = ItemTagsCombine.onlyIcon; break; case 'onlyIcon': combine = ItemTagsCombine.onlyIcon; break; case 'onlyImage': combine = ItemTagsCombine.onlyImage; break; case 'imageOrIconOrText': combine = ItemTagsCombine.imageOrIconOrText; break; case 'withTextAfter': combine = ItemTagsCombine.withTextAfter; break; case 'withTextBefore': combine = ItemTagsCombine.withTextBefore; break; } return Tags( key: Key("2"), symmetry: _symmetry, columns: _column, horizontalScroll: _horizontalScroll, verticalDirection: _startDirection ? VerticalDirection.up : VerticalDirection.down, textDirection: _startDirection ? TextDirection.rtl : TextDirection.ltr, heightHorizontalScroll: 60 * (_fontSize / 14), textField: _textField, itemCount: _items.length, itemBuilder: (index) { final item = _items[index]; return GestureDetector( child: ItemTags( key: Key(index.toString()), index: index, title: item, pressEnabled: false, activeColor: Colors.green[400], combine: combine, image: index > 0 && index < 5 ? ItemTagsImage(image: AssetImage("img/p$index.jpg")) : (1 == 1 ? ItemTagsImage( image: NetworkImage( "https://image.flaticon.com/icons/png/512/44/44948.png")) : null), icon: (item == '0' || item == '1' || item == '2') ? ItemTagsIcon( icon: _icon[int.parse(item)], ) : null, removeButton: ItemTagsRemoveButton( backgroundColor: Colors.green[900], onRemoved: () { setState(() { _items.removeAt(index); }); return true; }, ), textScaleFactor: utf8.encode(item.substring(0, 1)).length > 2 ? 0.8 : 1, textStyle: TextStyle( fontSize: _fontSize, ), ), onTapDown: (details) => _tapPosition = details.globalPosition, onLongPress: () { showMenu( //semanticLabel: item, items: [ PopupMenuItem( child: Text(item, style: TextStyle(color: Colors.blueGrey)), enabled: false, ), PopupMenuDivider(), PopupMenuItem( value: 1, child: Row( children: [ Icon(Icons.content_copy), Text("Copy text"), ], ), ), ], context: context, position: RelativeRect.fromRect( _tapPosition & Size(40, 40), Offset.zero & overlay .size) // & RelativeRect.fromLTRB(65.0, 40.0, 0.0, 0.0), ) .then((value) { if (value == 1) Clipboard.setData(ClipboardData(text: item)); }); }, ); }, ); } TagsTextField get _textField { return TagsTextField( autofocus: false, width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 10), textStyle: TextStyle( fontSize: _fontSize, //height: 1 ), enabled: true, constraintSuggestion: true, suggestions: _withSuggesttions ? [ "One", "two", "android", "Dart", "flutter", "test", "tests", "androids", "androidsaaa", "Test", "suggest", "suggestions", "互联网", "last", "lest", "炫舞时代" ] : null, onSubmitted: (String str) { setState(() { _items.add(str); }); }, ); } List _buildItems() { List list = []; int count = 19; list.add( DropdownMenuItem( child: Text("Not set"), value: 0, ), ); for (int i = 1; i < count; i++) list.add( DropdownMenuItem( child: Text(i.toString()), value: i, ), ); return list; } List _buildItems2() { List list = []; list.add(DropdownMenuItem( child: Text("onlyText"), value: 'onlyText', )); list.add(DropdownMenuItem( child: Text("onlyIcon"), value: 'onlyIcon', )); list.add(DropdownMenuItem( child: Text("onlyImage"), value: 'onlyImage', )); list.add(DropdownMenuItem( child: Text("imageOrIconOrText"), value: 'imageOrIconOrText', )); list.add(DropdownMenuItem( child: Text("withTextBefore"), value: 'withTextBefore', )); list.add(DropdownMenuItem( child: Text("withTextAfter"), value: 'withTextAfter', )); return list; } } ================================================ FILE: example/pubspec.yaml ================================================ name: example_flutter_tags description: A new Flutter application. # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # Read more about versioning at semver.org. version: 1.0.0+1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: sdk: flutter flutter_tags: path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 dev_dependencies: flutter_test: sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - img/p1.jpg - img/p2.jpg - img/p3.jpg - img/p4.jpg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.io/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.io/custom-fonts/#from-packages ================================================ FILE: example/test/widget_test.dart ================================================ import 'package:flutter/material.dart'; void main() {} ================================================ FILE: flutter_tags.iml ================================================ ================================================ FILE: lib/flutter_tags.dart ================================================ library flutter_tags; export 'src/suggestions_textfield.dart'; export 'src/item_tags.dart'; export 'src/tags.dart'; ================================================ FILE: lib/src/item_tags.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_tags/src/tags.dart'; /// Used by [ItemTags.onPressed]. typedef OnPressedCallback = void Function(Item i); /// Used by [ItemTags.OnLongPressed]. typedef OnLongPressedCallback = void Function(Item i); /// Used by [ItemTags.removeButton.onRemoved]. typedef OnRemovedCallback = bool Function(); /// combines icon text or image enum ItemTagsCombine { onlyText, onlyIcon, onlyImage, imageOrIconOrText, withTextBefore, withTextAfter } class ItemTags extends StatefulWidget { ItemTags( {@required this.index, @required this.title, this.textScaleFactor, this.active = true, this.pressEnabled = true, this.customData, this.textStyle = const TextStyle(fontSize: 14), this.alignment = MainAxisAlignment.center, this.combine = ItemTagsCombine.imageOrIconOrText, this.icon, this.image, this.removeButton, this.borderRadius, this.border, this.padding = const EdgeInsets.symmetric(horizontal: 7, vertical: 5), this.elevation = 5, this.singleItem = false, this.textOverflow = TextOverflow.fade, this.textColor = Colors.black, this.textActiveColor = Colors.white, this.color = Colors.white, this.activeColor = Colors.blueGrey, this.highlightColor, this.splashColor, this.colorShowDuplicate = Colors.red, this.onPressed, this.onLongPressed, Key key}) : assert(index != null), assert(title != null), super(key: key); /// Id of [ItemTags] - required final int index; /// Title of [ItemTags] - required final String title; /// Scale Factor of [ItemTags] - double final double textScaleFactor; /// Initial bool value final bool active; /// Initial bool value final bool pressEnabled; /// Possibility to add any custom value in customData field, you can retrieve this later. A good example: store an id from Firestore document. final dynamic customData; /// ItemTagsCombine (text,icon,textIcon,textImage) of [ItemTags] final ItemTagsCombine combine; /// Icon of [ItemTags] final ItemTagsIcon icon; /// Image of [ItemTags] final ItemTagsImage image; /// Custom Remove Button of [ItemTags] final ItemTagsRemoveButton removeButton; /// TextStyle of the [ItemTags] final TextStyle textStyle; /// TextStyle of the [ItemTags] final MainAxisAlignment alignment; /// border-radius of [ItemTags] final BorderRadius borderRadius; /// custom border-side of [ItemTags] final BoxBorder border; /// padding of the [ItemTags] final EdgeInsets padding; /// BoxShadow of the [ItemTags] final double elevation; /// when you want only one tag selected. same radio-button final bool singleItem; /// type of text overflow within the [ItemTags] final TextOverflow textOverflow; /// text color of the [ItemTags] final Color textColor; /// color of the [ItemTags] text activated final Color textActiveColor; /// background color [ItemTags] final Color color; /// background color [ItemTags] activated final Color activeColor; /// highlight Color [ItemTags] final Color highlightColor; /// Splash color [ItemTags] final Color splashColor; /// Color show duplicate [ItemTags] final Color colorShowDuplicate; /// callback final OnPressedCallback onPressed; /// callback final OnLongPressedCallback onLongPressed; @override _ItemTagsState createState() => _ItemTagsState(); } class _ItemTagsState extends State { final double _initBorderRadius = 50; DataListInherited _dataListInherited; DataList _dataList; void _setDataList() { // Get List from Tags widget _dataListInherited = DataListInherited.of(context); // set List length if (_dataListInherited.list.length < _dataListInherited.itemCount) _dataListInherited.list.length = _dataListInherited.itemCount; if (_dataListInherited.list.length > (widget.index + 1) && _dataListInherited.list.elementAt(widget.index) != null && _dataListInherited.list.elementAt(widget.index).title != widget.title) { // when an element is removed from the data source _dataListInherited.list.removeAt(widget.index); // when all item list changed in data source if (_dataListInherited.list.elementAt(widget.index) != null && _dataListInherited.list.elementAt(widget.index).title != widget.title) _dataListInherited.list .removeRange(widget.index, _dataListInherited.list.length); } // add new Item in the List if (_dataListInherited.list.length < (widget.index + 1)) { //print("add"); _dataListInherited.list.insert( widget.index, DataList( title: widget.title, index: widget.index, active: widget.singleItem ? false : widget.active, customData: widget.customData)); } else if (_dataListInherited.list.elementAt(widget.index) == null) { //print("replace"); _dataListInherited.list[widget.index] = DataList( title: widget.title, index: widget.index, active: widget.singleItem ? false : widget.active, customData: widget.customData); } // removes items that have been orphaned if (_dataListInherited.itemCount == widget.index + 1 && _dataListInherited.list.length > _dataListInherited.itemCount) _dataListInherited.list .removeRange(widget.index + 1, _dataListInherited.list.length); //print(_dataListInherited.list.length); // update Listener if (_dataList != null) _dataList.removeListener(_didValueChange); _dataList = _dataListInherited.list.elementAt(widget.index); _dataList.addListener(_didValueChange); } _didValueChange() => setState(() {}); @override void dispose() { _dataList.removeListener(_didValueChange); super.dispose(); } @override Widget build(BuildContext context) { _setDataList(); final double fontSize = widget.textStyle.fontSize; Color color = _dataList.active ? widget.activeColor : widget.color; if (_dataList.showDuplicate) color = widget.colorShowDuplicate; return Material( color: color, borderRadius: widget.borderRadius ?? BorderRadius.circular(_initBorderRadius), elevation: widget.elevation, //shadowColor: _dataList.highlights? Colors.red : Colors.blue, child: InkWell( borderRadius: widget.borderRadius ?? BorderRadius.circular(_initBorderRadius), highlightColor: widget.pressEnabled ? widget.highlightColor : Colors.transparent, splashColor: widget.pressEnabled ? widget.splashColor : Colors.transparent, child: Container( decoration: BoxDecoration( border: widget.border ?? Border.all(color: widget.activeColor, width: 0.5), borderRadius: widget.borderRadius ?? BorderRadius.circular(_initBorderRadius)), padding: widget.padding * (fontSize / 14), child: _combine), onTap: widget.pressEnabled ? () { if (widget.singleItem) { _singleItem(_dataListInherited, _dataList); _dataList.active = true; } else _dataList.active = !_dataList.active; if (widget.onPressed != null) widget.onPressed(Item( index: widget.index, title: _dataList.title, active: _dataList.active, customData: widget.customData)); } : null, onLongPress: widget.onLongPressed != null ? () => widget.onLongPressed(Item( index: widget.index, title: _dataList.title, active: _dataList.active, customData: widget.customData)) : null, ), ); } Widget get _combine { if (widget.image != null) assert((widget.image.image != null && widget.image.child == null) || (widget.image.child != null && widget.image.image == null)); final Widget text = Text( widget.title, softWrap: false, textAlign: _textAlignment, overflow: widget.textOverflow, textScaleFactor: widget.textScaleFactor, style: _textStyle, ); final Widget icon = widget.icon != null ? Container( padding: widget.icon.padding ?? (widget.combine == ItemTagsCombine.onlyIcon || widget.combine == ItemTagsCombine.imageOrIconOrText ? null : widget.combine == ItemTagsCombine.withTextAfter ? EdgeInsets.only(right: 5) : EdgeInsets.only(left: 5)), child: Icon( widget.icon.icon, color: _textStyle.color, size: _textStyle.fontSize * 1.2, ), ) : text; final Widget image = widget.image != null ? Container( padding: widget.image.padding ?? (widget.combine == ItemTagsCombine.onlyImage || widget.combine == ItemTagsCombine.imageOrIconOrText ? null : widget.combine == ItemTagsCombine.withTextAfter ? EdgeInsets.only(right: 5) : EdgeInsets.only(left: 5)), child: widget.image.child ?? CircleAvatar( radius: widget.image.radius * (widget.textStyle.fontSize / 14), backgroundColor: Colors.transparent, backgroundImage: widget.image.image, ), ) : text; final List list = List(); switch (widget.combine) { case ItemTagsCombine.onlyText: list.add(text); break; case ItemTagsCombine.onlyIcon: list.add(icon); break; case ItemTagsCombine.onlyImage: list.add(image); break; case ItemTagsCombine.imageOrIconOrText: list.add((image != text ? image : icon)); break; case ItemTagsCombine.withTextBefore: list.add(text); if (image != text) list.add(image); else if (icon != text) list.add(icon); break; case ItemTagsCombine.withTextAfter: if (image != text) list.add(image); else if (icon != text) list.add(icon); list.add(text); } final Widget row = Row( mainAxisAlignment: widget.alignment, mainAxisSize: MainAxisSize.min, children: List.generate(list.length, (i) { if (i == 0 && list.length > 1) return Flexible( flex: widget.combine == ItemTagsCombine.withTextAfter ? 0 : 1, child: list[i], ); return Flexible( flex: widget.combine == ItemTagsCombine.withTextAfter || list.length == 1 ? 1 : 0, child: list[i], ); })); if (widget.removeButton != null) return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min, children: [ Flexible( fit: _dataListInherited.symmetry ? FlexFit.tight : FlexFit.loose, flex: 2, child: row), Flexible( flex: 0, child: FittedBox( alignment: Alignment.centerRight, fit: BoxFit.fill, child: GestureDetector( child: Container( margin: widget.removeButton.margin ?? EdgeInsets.only(left: 5), padding: (widget.removeButton.padding ?? EdgeInsets.all(2)) * (widget.textStyle.fontSize / 14), decoration: BoxDecoration( color: widget.removeButton.backgroundColor ?? Colors.black, borderRadius: widget.removeButton.borderRadius ?? BorderRadius.circular(_initBorderRadius), ), child: widget.removeButton.padding ?? Icon( Icons.clear, color: widget.removeButton.color ?? Colors.white, size: (widget.removeButton.size ?? 12) * (widget.textStyle.fontSize / 14), ), ), onTap: () { if (widget.removeButton.onRemoved != null) { if (widget.removeButton.onRemoved()) _dataListInherited.list.removeAt(widget.index); } }, ))) ]); return row; } ///Text Alignment TextAlign get _textAlignment { switch (widget.alignment) { case MainAxisAlignment.spaceBetween: case MainAxisAlignment.start: return TextAlign.start; break; case MainAxisAlignment.end: return TextAlign.end; break; case MainAxisAlignment.spaceAround: case MainAxisAlignment.spaceEvenly: case MainAxisAlignment.center: return TextAlign.center; } return null; } ///TextStyle TextStyle get _textStyle { return widget.textStyle.apply( color: _dataList.active ? widget.textActiveColor : widget.textColor, ); } /// Single item selection void _singleItem(DataListInherited dataSetIn, DataList dataSet) { dataSetIn.list .where((tg) => tg != null) .where((tg) => tg.active) .where((tg2) => tg2 != dataSet) .forEach((tg) => tg.active = false); } } ///callback class Item { Item({this.index, this.title, this.active, this.customData}); final int index; final String title; final bool active; final dynamic customData; @override String toString() { return "id:$index, title: $title, active: $active, customData: $customData"; } } /// ItemTag Image class ItemTagsImage { ItemTagsImage({this.radius = 8, this.padding, this.image, this.child}); final double radius; final EdgeInsets padding; final ImageProvider image; final Widget child; } /// ItemTag Icon class ItemTagsIcon { ItemTagsIcon({this.padding, @required this.icon}); final EdgeInsets padding; final IconData icon; } /// ItemTag RemoveButton class ItemTagsRemoveButton { ItemTagsRemoveButton( {this.icon, this.size, this.backgroundColor, this.color, this.borderRadius, this.padding, this.margin, this.onRemoved}); final IconData icon; final double size; final Color backgroundColor; final Color color; final BorderRadius borderRadius; final EdgeInsets padding; final EdgeInsets margin; /// callback final OnRemovedCallback onRemoved; } ================================================ FILE: lib/src/suggestions_textfield.dart ================================================ import 'package:flutter/material.dart'; // InputSuggestions version 0.0.1 // currently yield inline suggestions // I will soon implement a list with suggestions // Credit Dn-a -> https://github.com/Dn-a /// Used by [SuggestionsTextField.onChanged]. typedef OnChangedCallback = void Function(String string); /// Used by [SuggestionsTextField.onSubmitted]. typedef OnSubmittedCallback = void Function(String string); class SuggestionsTextField extends StatefulWidget { SuggestionsTextField( {@required this.tagsTextField, this.onSubmitted, Key key}) : assert(tagsTextField != null), super(key: key); final TagsTextField tagsTextField; final OnSubmittedCallback onSubmitted; @override _SuggestionsTextFieldState createState() => _SuggestionsTextFieldState(); } class _SuggestionsTextFieldState extends State { final _controller = TextEditingController(); List _matches = List(); String _helperText; bool _helperCheck = true; List _suggestions; bool _constraintSuggestion; double _fontSize; InputDecoration _inputDecoration; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { _helperText = widget.tagsTextField.helperText ?? "no matches"; _suggestions = widget.tagsTextField.suggestions; _constraintSuggestion = widget.tagsTextField.constraintSuggestion; _inputDecoration = widget.tagsTextField.inputDecoration; _fontSize = widget.tagsTextField.textStyle.fontSize; return Stack( alignment: Alignment.centerLeft, children: [ Visibility( visible: _suggestions != null, child: Container( //width: double.infinity, padding: _inputDecoration != null ? _inputDecoration.contentPadding : EdgeInsets.symmetric( vertical: 6 * (_fontSize / 14), horizontal: 6 * (_fontSize / 14)), child: Text( _matches.isNotEmpty ? (_matches.first) : "", softWrap: false, overflow: TextOverflow.fade, style: TextStyle( height: widget.tagsTextField.textStyle.height == null ? 1 : widget.tagsTextField.textStyle.height, fontSize: _fontSize ?? null, color: widget.tagsTextField.suggestionTextColor ?? Colors.red, ), ), ), ), TextField( controller: _controller, enabled: widget.tagsTextField.enabled, autofocus: widget.tagsTextField.autofocus ?? true, keyboardType: widget.tagsTextField.keyboardType ?? null, textCapitalization: widget.tagsTextField.textCapitalization ?? TextCapitalization.none, maxLength: widget.tagsTextField.maxLength ?? null, maxLines: 1, autocorrect: widget.tagsTextField.autocorrect ?? false, style: widget.tagsTextField.textStyle.copyWith( height: widget.tagsTextField.textStyle.height == null ? 1 : null), decoration: _initialInputDecoration, onChanged: (str) => _checkOnChanged(str), onSubmitted: (str) => _onSubmitted(str), ) ], ); } InputDecoration get _initialInputDecoration { var input = _inputDecoration ?? InputDecoration( disabledBorder: InputBorder.none, errorBorder: InputBorder.none, contentPadding: EdgeInsets.symmetric( vertical: 6 * (_fontSize / 14), horizontal: 6 * (_fontSize / 14)), focusedBorder: UnderlineInputBorder( borderSide: BorderSide( color: Colors.blueGrey[300], ), ), enabledBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey[400].withOpacity(0.3)), ), border: UnderlineInputBorder( borderSide: BorderSide(color: Colors.blueGrey[400].withOpacity(0.3)), )); return input.copyWith( helperText: _helperCheck || _suggestions == null ? null : _helperText, helperStyle: widget.tagsTextField.helperTextStyle, hintText: widget.tagsTextField.hintText ?? 'Add a tag', hintStyle: TextStyle(color: widget.tagsTextField.hintTextColor)); } ///OnSubmitted void _onSubmitted(String str) { var onSubmitted = widget.onSubmitted; if (_suggestions != null && _matches.isNotEmpty) str = _matches.first; if (widget.tagsTextField.lowerCase) str = str.toLowerCase(); str = str.trim(); if (_suggestions != null) { if (_matches.isNotEmpty || !_constraintSuggestion) { if (onSubmitted != null) onSubmitted(str); setState(() { _matches = []; }); _controller.clear(); } } else if (str.isNotEmpty) { if (onSubmitted != null) onSubmitted(str); _controller.clear(); } } ///Check onChanged void _checkOnChanged(String str) { if (_suggestions != null) { _matches = _suggestions.where((String sgt) => sgt.startsWith(str)).toList(); if (str.isEmpty) _matches = []; if (_matches.length > 1) _matches.removeWhere((String mtc) => mtc == str); setState(() { _helperCheck = _matches.isNotEmpty || str.isEmpty || !_constraintSuggestion ? true : false; _matches.sort((a, b) => a.compareTo(b)); }); } if (widget.tagsTextField.onChanged != null) widget.tagsTextField.onChanged(str); } } /// Tags TextField class TagsTextField { TagsTextField( {this.lowerCase = false, this.textStyle = const TextStyle(fontSize: 14), this.width = 200, this.padding, this.enabled = true, this.duplicates = false, this.suggestions, this.constraintSuggestion = true, this.autocorrect, this.autofocus, this.hintText, this.hintTextColor, this.suggestionTextColor, this.helperText, this.helperTextStyle, this.keyboardType, this.textCapitalization, this.maxLength, this.inputDecoration, this.onSubmitted, this.onChanged}); final double width; final EdgeInsets padding; final bool enabled; final bool duplicates; final TextStyle textStyle; final InputDecoration inputDecoration; final bool autocorrect; final List suggestions; /// Allows you to insert tags not present in the list of suggestions final bool constraintSuggestion; final bool lowerCase; final bool autofocus; final String hintText; final Color hintTextColor; final Color suggestionTextColor; final String helperText; final TextStyle helperTextStyle; final TextInputType keyboardType; final TextCapitalization textCapitalization; final int maxLength; final OnSubmittedCallback onSubmitted; final OnChangedCallback onChanged; } ================================================ FILE: lib/src/tags.dart ================================================ import 'package:flutter/material.dart'; import '../flutter_tags.dart'; import 'util/custom_wrap.dart'; import 'package:flutter_tags/src/suggestions_textfield.dart'; ///ItemBuilder typedef Widget ItemBuilder(int index); class Tags extends StatefulWidget { Tags( {this.columns, this.itemCount = 0, this.symmetry = false, this.horizontalScroll = false, this.heightHorizontalScroll = 60, this.spacing = 6, this.runSpacing = 14, this.alignment = WrapAlignment.center, this.runAlignment = WrapAlignment.center, this.direction = Axis.horizontal, this.verticalDirection = VerticalDirection.down, this.textDirection = TextDirection.ltr, this.itemBuilder, this.textField, Key key}) : assert(itemCount >= 0), assert(alignment != null), assert(runAlignment != null), assert(direction != null), assert(verticalDirection != null), assert(textDirection != null), super(key: key); ///specific number of columns final int columns; ///numer of item List final int itemCount; /// imposes the same width and the same number of columns for each row final bool symmetry; /// ability to scroll tags horizontally final bool horizontalScroll; /// horizontal spacing of the [ItemTags] final double heightHorizontalScroll; /// horizontal spacing of the [ItemTags] final double spacing; /// vertical spacing of the [ItemTags] final double runSpacing; /// horizontal alignment of the [ItemTags] final WrapAlignment alignment; /// vertical alignment of the [ItemTags] final WrapAlignment runAlignment; /// direction of the [ItemTags] final Axis direction; /// Iterate [Item] from the lower to the upper direction or vice versa final VerticalDirection verticalDirection; /// Text direction of the [ItemTags] final TextDirection textDirection; /// Generates a list of [ItemTags]. /// /// Creates a list with [length] positions and fills it with values created by /// calling [generator] for each index in the range `0` .. `length - 1` /// in increasing order. final ItemBuilder itemBuilder; /// custom TextField final TagsTextField textField; @override TagsState createState() => TagsState(); } class TagsState extends State { final GlobalKey _containerKey = GlobalKey(); Orientation _orientation = Orientation.portrait; double _width = 0; final List _list = List(); List get getAllItem => _list.toList(); //get the current width of the screen void _getWidthContext() { WidgetsBinding.instance.addPostFrameCallback((_) { final keyContext = _containerKey.currentContext; if (keyContext != null) { final RenderBox box = keyContext.findRenderObject(); final size = box.size; setState(() { _width = size.width; }); } }); } @override Widget build(BuildContext context) { // essential to avoid infinite loop of addPostFrameCallback if (widget.symmetry && (MediaQuery.of(context).orientation != _orientation || _width == 0)) { _orientation = MediaQuery.of(context).orientation; _getWidthContext(); } Widget child; if (widget.horizontalScroll && !widget.symmetry) child = Container( height: widget.heightHorizontalScroll, color: Colors.transparent, child: ListView( padding: EdgeInsets.all(0), scrollDirection: Axis.horizontal, shrinkWrap: true, physics: ClampingScrollPhysics(), children: _buildItems(), ), ); else child = CustomWrap( key: _containerKey, alignment: widget.alignment, runAlignment: widget.runAlignment, spacing: widget.spacing, runSpacing: widget.runSpacing, column: widget.columns, symmetry: widget.symmetry, textDirection: widget.textDirection, direction: widget.direction, verticalDirection: widget.verticalDirection, crossAxisAlignment: WrapCrossAlignment.end, children: _buildItems(), ); return DataListInherited( list: _list, symmetry: widget.symmetry, itemCount: widget.itemCount, child: child, ); } List _buildItems() { /*if(_list.length < widget.itemCount) _list.clear();*/ final Widget textField = widget.textField != null ? Container( alignment: Alignment.center, width: widget.symmetry ? _widthCalc() : widget.textField.width, padding: widget.textField.padding, child: SuggestionsTextField( tagsTextField: widget.textField, onSubmitted: (String str) { if (!widget.textField.duplicates) { final List lst = _list.where((l) => l.title == str).toList(); if (lst.isNotEmpty) { lst.forEach((d) => d.showDuplicate = true); return; } } if (widget.textField.onSubmitted != null) widget.textField.onSubmitted(str); }, ), ) : null; List finalList = List(); List itemList = List.generate(widget.itemCount, (i) { final Widget item = widget.itemBuilder(i); if (widget.symmetry) return Container( width: _widthCalc(), child: item, ); else if (widget.horizontalScroll) return Container( margin: EdgeInsets.symmetric(horizontal: widget.spacing), alignment: Alignment.center, child: item, ); return item; }); if (widget.horizontalScroll && widget.textDirection == TextDirection.rtl) itemList = itemList.reversed.toList(); if (textField == null) { finalList.addAll(itemList); return finalList; } if (widget.horizontalScroll && widget.verticalDirection == VerticalDirection.up) { finalList.add(textField); finalList.addAll(itemList); } else { finalList.addAll(itemList); finalList.add(textField); } return finalList; } //Container width divided by the number of columns when symmetry is active double _widthCalc() { int columns = widget.columns ?? 0; int margin = widget.spacing.round(); int subtraction = columns * (margin); double width = (_width > 1) ? (_width - subtraction) / columns : _width; return width; } } /// Inherited Widget class DataListInherited extends InheritedWidget { DataListInherited( {Key key, this.list, this.symmetry, this.itemCount, Widget child}) : super(key: key, child: child); final List list; final bool symmetry; final int itemCount; @override bool updateShouldNotify(DataListInherited old) { //print("inherited"); return false; } /*static DataListInherited of(BuildContext context) => context.inheritFromWidgetOfExactType(DataListInherited);*/ static DataListInherited of(BuildContext context) => context.dependOnInheritedWidgetOfExactType(); } /// Data List class DataList extends ValueNotifier implements Item { DataList( {@required this.title, this.index, bool highlights = false, bool active = true, this.customData}) : _showDuplicate = highlights, _active = active, super(active); final String title; final dynamic customData; final int index; get showDuplicate { final val = _showDuplicate; _showDuplicate = false; return val; } bool _showDuplicate; set showDuplicate(bool a) { _showDuplicate = a; // rebuild only the specific Item that changes its value notifyListeners(); } get active => _active; bool _active; set active(bool a) { _active = a; // rebuild only the specific Item that changes its value notifyListeners(); } } ================================================ FILE: lib/src/util/custom_wrap.dart ================================================ import 'dart:math' as math; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; /// Custom Wrap class CustomWrap extends MultiChildRenderObjectWidget { CustomWrap({ Key key, this.column, this.symmetry, this.direction = Axis.horizontal, this.alignment = WrapAlignment.start, this.spacing = 0.0, this.runAlignment = WrapAlignment.start, this.runSpacing = 0.0, this.crossAxisAlignment = WrapCrossAlignment.start, this.textDirection, this.verticalDirection = VerticalDirection.down, List children = const [], }) : super(key: key, children: children); final int column; final bool symmetry; final Axis direction; final WrapAlignment alignment; final double spacing; final WrapAlignment runAlignment; final double runSpacing; final WrapCrossAlignment crossAxisAlignment; final TextDirection textDirection; final VerticalDirection verticalDirection; @override CustomRenderWrap createRenderObject(BuildContext context) { return CustomRenderWrap( column: column, symmetry: symmetry, direction: direction, alignment: alignment, spacing: spacing, runAlignment: runAlignment, runSpacing: runSpacing, crossAxisAlignment: crossAxisAlignment, textDirection: textDirection ?? Directionality.of(context), verticalDirection: verticalDirection, ); } @override void updateRenderObject(BuildContext context, CustomRenderWrap renderObject) { renderObject ..column = column ..symmetry = symmetry ..direction = direction ..alignment = alignment ..spacing = spacing ..runAlignment = runAlignment ..runSpacing = runSpacing ..crossAxisAlignment = crossAxisAlignment ..textDirection = textDirection ?? Directionality.of(context) ..verticalDirection = verticalDirection; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(EnumProperty('direction', direction)); properties.add(EnumProperty('alignment', alignment)); properties.add(DoubleProperty('spacing', spacing)); properties.add(EnumProperty('runAlignment', runAlignment)); properties.add(DoubleProperty('runSpacing', runSpacing)); properties.add(DoubleProperty('crossAxisAlignment', runSpacing)); properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); properties.add(EnumProperty( 'verticalDirection', verticalDirection, defaultValue: VerticalDirection.down)); } } /// Displays its children in multiple horizontal or vertical runs. class CustomRenderWrap extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { CustomRenderWrap({ List children, int column, bool symmetry, Axis direction = Axis.horizontal, WrapAlignment alignment = WrapAlignment.start, double spacing = 0.0, WrapAlignment runAlignment = WrapAlignment.start, double runSpacing = 0.0, WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.start, TextDirection textDirection, VerticalDirection verticalDirection = VerticalDirection.down, }) : assert(direction != null), assert(alignment != null), assert(spacing != null), assert(runAlignment != null), assert(runSpacing != null), assert(crossAxisAlignment != null), _column = column, _symmetry = symmetry, _direction = direction, _alignment = alignment, _spacing = spacing, _runAlignment = runAlignment, _runSpacing = runSpacing, _crossAxisAlignment = crossAxisAlignment, _textDirection = textDirection, _verticalDirection = verticalDirection { addAll(children); } int get column => _column; int _column; set column(int value) { if (column == value) return; _column = value; markNeedsLayout(); } bool get symmetry => _symmetry; bool _symmetry; set symmetry(bool value) { if (symmetry == value) return; _symmetry = value; markNeedsLayout(); } Axis get direction => _direction; Axis _direction; set direction(Axis value) { assert(value != null); if (_direction == value) return; _direction = value; markNeedsLayout(); } WrapAlignment get alignment => _alignment; WrapAlignment _alignment; set alignment(WrapAlignment value) { assert(value != null); if (_alignment == value) return; _alignment = value; markNeedsLayout(); } double get spacing => _spacing; double _spacing; set spacing(double value) { assert(value != null); if (_spacing == value) return; _spacing = value; markNeedsLayout(); } WrapAlignment get runAlignment => _runAlignment; WrapAlignment _runAlignment; set runAlignment(WrapAlignment value) { assert(value != null); if (_runAlignment == value) return; _runAlignment = value; markNeedsLayout(); } double get runSpacing => _runSpacing; double _runSpacing; set runSpacing(double value) { assert(value != null); if (_runSpacing == value) return; _runSpacing = value; markNeedsLayout(); } WrapCrossAlignment get crossAxisAlignment => _crossAxisAlignment; WrapCrossAlignment _crossAxisAlignment; set crossAxisAlignment(WrapCrossAlignment value) { assert(value != null); if (_crossAxisAlignment == value) return; _crossAxisAlignment = value; markNeedsLayout(); } TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { if (_textDirection != value) { _textDirection = value; markNeedsLayout(); } } VerticalDirection get verticalDirection => _verticalDirection; VerticalDirection _verticalDirection; set verticalDirection(VerticalDirection value) { if (_verticalDirection != value) { _verticalDirection = value; markNeedsLayout(); } } bool get _debugHasNecessaryDirections { assert(direction != null); assert(alignment != null); assert(runAlignment != null); assert(crossAxisAlignment != null); if (firstChild != null && lastChild != firstChild) { // i.e. there's more than one child switch (direction) { case Axis.horizontal: assert(textDirection != null, 'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.'); break; case Axis.vertical: assert(verticalDirection != null, 'Vertical $runtimeType with multiple children has a null verticalDirection, so the layout order is undefined.'); break; } } if (alignment == WrapAlignment.start || alignment == WrapAlignment.end) { switch (direction) { case Axis.horizontal: assert(textDirection != null, 'Horizontal $runtimeType with alignment $alignment has a null textDirection, so the alignment cannot be resolved.'); break; case Axis.vertical: assert(verticalDirection != null, 'Vertical $runtimeType with alignment $alignment has a null verticalDirection, so the alignment cannot be resolved.'); break; } } if (runAlignment == WrapAlignment.start || runAlignment == WrapAlignment.end) { switch (direction) { case Axis.horizontal: assert(verticalDirection != null, 'Horizontal $runtimeType with runAlignment $runAlignment has a null verticalDirection, so the alignment cannot be resolved.'); break; case Axis.vertical: assert(textDirection != null, 'Vertical $runtimeType with runAlignment $runAlignment has a null textDirection, so the alignment cannot be resolved.'); break; } } if (crossAxisAlignment == WrapCrossAlignment.start || crossAxisAlignment == WrapCrossAlignment.end) { switch (direction) { case Axis.horizontal: assert(verticalDirection != null, 'Horizontal $runtimeType with crossAxisAlignment $crossAxisAlignment has a null verticalDirection, so the alignment cannot be resolved.'); break; case Axis.vertical: assert(textDirection != null, 'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.'); break; } } return true; } @override void setupParentData(RenderBox child) { if (child.parentData is! WrapParentData) child.parentData = WrapParentData(); } double _computeIntrinsicHeightForWidth(double width) { assert(direction == Axis.horizontal); int runCount = 0; double height = 0.0; double runWidth = 0.0; double runHeight = 0.0; int childCount = 0; RenderBox child = firstChild; while (child != null) { final double childWidth = child.getMaxIntrinsicWidth(double.infinity); final double childHeight = child.getMaxIntrinsicHeight(childWidth); if (runWidth + childWidth > width) { height += runHeight; if (runCount > 0) height += runSpacing; runCount += 1; runWidth = 0.0; runHeight = 0.0; childCount = 0; } runWidth += childWidth; runHeight = math.max(runHeight, childHeight); if (childCount > 0) runWidth += spacing; childCount += 1; child = childAfter(child); } if (childCount > 0) height += runHeight + runSpacing; return height; } double _computeIntrinsicWidthForHeight(double height) { assert(direction == Axis.vertical); int runCount = 0; double width = 0.0; double runHeight = 0.0; double runWidth = 0.0; int childCount = 0; RenderBox child = firstChild; while (child != null) { final double childHeight = child.getMaxIntrinsicHeight(double.infinity); final double childWidth = child.getMaxIntrinsicWidth(childHeight); if (runHeight + childHeight > height) { width += runWidth; if (runCount > 0) width += runSpacing; runCount += 1; runHeight = 0.0; runWidth = 0.0; childCount = 0; } runHeight += childHeight; runWidth = math.max(runWidth, childWidth); if (childCount > 0) runHeight += spacing; childCount += 1; child = childAfter(child); } if (childCount > 0) width += runWidth + runSpacing; return width; } @override double computeMinIntrinsicWidth(double height) { switch (direction) { case Axis.horizontal: double width = 0.0; RenderBox child = firstChild; while (child != null) { width = math.max(width, child.getMinIntrinsicWidth(double.infinity)); child = childAfter(child); } return width; case Axis.vertical: return _computeIntrinsicWidthForHeight(height); } return null; } @override double computeMaxIntrinsicWidth(double height) { switch (direction) { case Axis.horizontal: double width = 0.0; RenderBox child = firstChild; while (child != null) { width += child.getMaxIntrinsicWidth(double.infinity); child = childAfter(child); } return width; case Axis.vertical: return _computeIntrinsicWidthForHeight(height); } return null; } @override double computeMinIntrinsicHeight(double width) { switch (direction) { case Axis.horizontal: return _computeIntrinsicHeightForWidth(width); case Axis.vertical: double height = 0.0; RenderBox child = firstChild; while (child != null) { height = math.max(height, child.getMinIntrinsicHeight(double.infinity)); child = childAfter(child); } return height; } return null; } @override double computeMaxIntrinsicHeight(double width) { switch (direction) { case Axis.horizontal: return _computeIntrinsicHeightForWidth(width); case Axis.vertical: double height = 0.0; RenderBox child = firstChild; while (child != null) { height += child.getMaxIntrinsicHeight(double.infinity); child = childAfter(child); } return height; } return null; } @override double computeDistanceToActualBaseline(TextBaseline baseline) { return defaultComputeDistanceToHighestActualBaseline(baseline); } double _getMainAxisExtent(RenderBox child) { switch (direction) { case Axis.horizontal: return child.size.width; case Axis.vertical: return child.size.height; } return 0.0; } double _getCrossAxisExtent(RenderBox child) { switch (direction) { case Axis.horizontal: return child.size.height; case Axis.vertical: return child.size.width; } return 0.0; } Offset _getOffset(double mainAxisOffset, double crossAxisOffset) { switch (direction) { case Axis.horizontal: return Offset(mainAxisOffset, crossAxisOffset); case Axis.vertical: return Offset(crossAxisOffset, mainAxisOffset); } return Offset.zero; } double _getChildCrossAxisOffset(bool flipCrossAxis, double runCrossAxisExtent, double childCrossAxisExtent) { final double freeSpace = runCrossAxisExtent - childCrossAxisExtent; switch (crossAxisAlignment) { case WrapCrossAlignment.start: return flipCrossAxis ? freeSpace : 0.0; case WrapCrossAlignment.end: return flipCrossAxis ? 0.0 : freeSpace; case WrapCrossAlignment.center: return freeSpace / 2.0; } return 0.0; } bool _hasVisualOverflow = false; @override void performLayout() { assert(_debugHasNecessaryDirections); _hasVisualOverflow = false; RenderBox child = firstChild; if (child == null) { size = constraints.smallest; return; } BoxConstraints childConstraints; double mainAxisLimit = 0.0; bool flipMainAxis = false; bool flipCrossAxis = false; switch (direction) { case Axis.horizontal: childConstraints = BoxConstraints(maxWidth: constraints.maxWidth); mainAxisLimit = constraints.maxWidth; if (textDirection == TextDirection.rtl) flipMainAxis = true; if (verticalDirection == VerticalDirection.up) flipCrossAxis = true; break; case Axis.vertical: childConstraints = BoxConstraints(maxHeight: constraints.maxHeight); mainAxisLimit = constraints.maxHeight; if (verticalDirection == VerticalDirection.up) flipMainAxis = true; if (textDirection == TextDirection.rtl) flipCrossAxis = true; break; } assert(childConstraints != null); assert(mainAxisLimit != null); final double spacing = this.spacing; final double runSpacing = this.runSpacing; final List<_RunMetrics> runMetrics = <_RunMetrics>[]; double mainAxisExtent = 0.0; double crossAxisExtent = 0.0; double runMainAxisExtent = 0.0; double runCrossAxisExtent = 0.0; int childCount = 0; int cnt = 0; while (child != null) { child.layout(childConstraints, parentUsesSize: true); final double childMainAxisExtent = _getMainAxisExtent(child); final double childCrossAxisExtent = _getCrossAxisExtent(child); if ((childCount > 0 && runMainAxisExtent + spacing + childMainAxisExtent > mainAxisLimit) || (_column != null && _column != 0 && cnt == _column)) { cnt = 0; mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent); crossAxisExtent += runCrossAxisExtent; if (runMetrics.isNotEmpty) crossAxisExtent += runSpacing; runMetrics.add( _RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount)); runMainAxisExtent = 0.0; runCrossAxisExtent = 0.0; childCount = 0; } cnt++; runMainAxisExtent += childMainAxisExtent; if (childCount > 0) runMainAxisExtent += spacing; runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); childCount += 1; final WrapParentData childParentData = child.parentData; childParentData._runIndex = runMetrics.length; child = childParentData.nextSibling; } if (childCount > 0) { mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent); crossAxisExtent += runCrossAxisExtent; if (runMetrics.isNotEmpty) crossAxisExtent += runSpacing; runMetrics .add(_RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount)); } final int runCount = runMetrics.length; assert(runCount > 0); double containerMainAxisExtent = 0.0; double containerCrossAxisExtent = 0.0; switch (direction) { case Axis.horizontal: size = constraints.constrain(Size(mainAxisExtent, crossAxisExtent)); containerMainAxisExtent = size.width; containerCrossAxisExtent = size.height; break; case Axis.vertical: size = constraints.constrain(Size(crossAxisExtent, mainAxisExtent)); containerMainAxisExtent = size.height; containerCrossAxisExtent = size.width; break; } _hasVisualOverflow = containerMainAxisExtent < mainAxisExtent || containerCrossAxisExtent < crossAxisExtent; final double crossAxisFreeSpace = math.max(0.0, containerCrossAxisExtent - crossAxisExtent); double runLeadingSpace = 0.0; double runBetweenSpace = 0.0; switch (runAlignment) { case WrapAlignment.start: break; case WrapAlignment.end: runLeadingSpace = crossAxisFreeSpace; break; case WrapAlignment.center: runLeadingSpace = crossAxisFreeSpace / 2.0; break; case WrapAlignment.spaceBetween: runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; break; case WrapAlignment.spaceAround: runBetweenSpace = crossAxisFreeSpace / runCount; runLeadingSpace = runBetweenSpace / 2.0; break; case WrapAlignment.spaceEvenly: runBetweenSpace = crossAxisFreeSpace / (runCount + 1); runLeadingSpace = runBetweenSpace; break; } runBetweenSpace += runSpacing; double crossAxisOffset = flipCrossAxis ? containerCrossAxisExtent - runLeadingSpace : runLeadingSpace; child = firstChild; for (int i = 0; i < runCount; ++i) { final _RunMetrics metrics = runMetrics[i]; final double runMainAxisExtent = metrics.mainAxisExtent; final double runCrossAxisExtent = metrics.crossAxisExtent; final int childCount = metrics.childCount; final double mainAxisFreeSpace = math.max(0.0, containerMainAxisExtent - runMainAxisExtent); double childLeadingSpace = 0.0; double childBetweenSpace = 0.0; //print(symmetry); switch (alignment) { case WrapAlignment.start: if (symmetry) childLeadingSpace = spacing / 2; break; case WrapAlignment.end: childLeadingSpace = mainAxisFreeSpace; if (symmetry) childLeadingSpace = mainAxisFreeSpace - spacing / 2; break; case WrapAlignment.center: childLeadingSpace = mainAxisFreeSpace / 2.0; break; case WrapAlignment.spaceBetween: childBetweenSpace = childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0; break; case WrapAlignment.spaceAround: childBetweenSpace = mainAxisFreeSpace / childCount; childLeadingSpace = childBetweenSpace / 2.0; break; case WrapAlignment.spaceEvenly: childBetweenSpace = mainAxisFreeSpace / (childCount + 1); childLeadingSpace = childBetweenSpace; break; } childBetweenSpace += spacing; double childMainPosition = flipMainAxis ? containerMainAxisExtent - childLeadingSpace : childLeadingSpace; if (flipCrossAxis) crossAxisOffset -= runCrossAxisExtent; while (child != null) { final WrapParentData childParentData = child.parentData; if (childParentData._runIndex != i) break; final double childMainAxisExtent = _getMainAxisExtent(child); final double childCrossAxisExtent = _getCrossAxisExtent(child); final double childCrossAxisOffset = _getChildCrossAxisOffset( flipCrossAxis, runCrossAxisExtent, childCrossAxisExtent); if (flipMainAxis) childMainPosition -= childMainAxisExtent; childParentData.offset = _getOffset( childMainPosition, crossAxisOffset + childCrossAxisOffset); if (flipMainAxis) childMainPosition -= childBetweenSpace; else childMainPosition += childMainAxisExtent + childBetweenSpace; child = childParentData.nextSibling; } if (flipCrossAxis) crossAxisOffset -= runBetweenSpace; else crossAxisOffset += runCrossAxisExtent + runBetweenSpace; } } @override bool hitTestChildren(HitTestResult result, {Offset position}) { return defaultHitTestChildren(result, position: position); } @override void paint(PaintingContext context, Offset offset) { // TODO(ianh): move the debug flex overflow paint logic somewhere common so // it can be reused here if (_hasVisualOverflow) context.pushClipRect( needsCompositing, offset, Offset.zero & size, defaultPaint); else defaultPaint(context, offset); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(EnumProperty('direction', direction)); properties.add(EnumProperty('alignment', alignment)); properties.add(DoubleProperty('spacing', spacing)); properties.add(EnumProperty('runAlignment', runAlignment)); properties.add(DoubleProperty('runSpacing', runSpacing)); properties.add(DoubleProperty('crossAxisAlignment', runSpacing)); properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); properties.add(EnumProperty( 'verticalDirection', verticalDirection, defaultValue: VerticalDirection.down)); } } class _RunMetrics { _RunMetrics(this.mainAxisExtent, this.crossAxisExtent, this.childCount); final double mainAxisExtent; final double crossAxisExtent; final int childCount; } /// Parent data for use with [CustomRenderWrap]. class WrapParentData extends ContainerBoxParentData { int _runIndex = 0; } ================================================ FILE: pubspec.yaml ================================================ name: flutter_tags description: Creating selectable and input tags (TextField) has never been easier. version: 0.4.9+1 author: Antonino Di Natale homepage: https://github.com/Dn-a/flutter_tags environment: sdk: ">=2.0.0 <3.0.0" dependencies: flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter ================================================ FILE: test/flutter_tags_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; void main() {}