How to acces a DragTarget with overlaying Draggable? - flutter

I am trying to create a game in Flutter where items are dragged and dropped into targets. As soon as an item is dropped on the target it will be repositioned at the target.
This works up untill the next item being dropped on the original target. The onAccept is not triggered anymore because the first item is 'on top' of the target.
What is the best way to work around this?
I basically rewrote this question/answer : How to update Draggable child when entering DragTarget in Flutter?
Part of Main:
child: Stack(
children: <Widget>[
MyDragTarget(Offset(50, 500), draggableController, 'target 1'),
MyDragTarget(Offset(250, 500), draggableController, 'target 2'),
MyDraggable(
Offset(50, 100),
draggableController,
'ball 1',
),
MyDraggable(
Offset(150, 100),
draggableController,
'ball 2',
),
MyDraggable(
Offset(250, 100),
draggableController,
'ball 3',
),
],
),
Draggable
class MyDraggable<T> extends StatefulWidget {
final Offset initPos;
final MyDraggableController<T> controller;
final T data;
MyDraggable(this.initPos, this.controller, this.data, {Key key})
: super(key: key);
#override
_MyDraggableState createState() =>
_MyDraggableState<T>(this.initPos, this.controller, this.data);
}
class _MyDraggableState<T> extends State<MyDraggable> {
MyDraggableController<T> controller;
T data;
Offset position = Offset(0.0, 0.0);
_MyDraggableState(this.position, this.controller, this.data);
#override
void initState() {
this.controller.subscribeToOnTargetCallback(onTargetCallbackHandler);
super.initState();
position = widget.initPos;
}
void onTargetCallbackHandler(T data, Offset targetPosition) {
debugPrint("dropped inside target: " + data.toString());
if (this.data == data) {
debugPrint("DRAGGABLE is ACCEPTED " +
this.data.toString() +
" " +
this.isOnTarget.toString());
setState(() {
this.position = targetPosition;
});
} else {
debugPrint("DRAGGABLE is NOT ACCEPTED " +
this.data.toString() +
" " +
this.isOnTarget.toString());
if (this.position == targetPosition) {
debugPrint(this.data.toString() + " is occupying this spot!");
}
setState(() {});
}
}
#override
void dispose() {
this.controller.unSubscribeFromOnTargetCallback(onTargetCallbackHandler);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Positioned(
left: position.dx,
top: position.dy,
child: Draggable<T>(
data: this.data,
child: CirkleWidget(this.data,0.5),
feedback: CirkleWidget(this.data,1.2),
childWhenDragging: new Container(),
onDraggableCanceled: (v, f) => setState(
() {
this.isOnTarget = false;
this.position = widget.initPos;
},
),
));
}
}
DragTarget
class MyDragTarget<T> extends StatefulWidget {
final Offset inPos;
final MyDraggableController<T> controller;
final T data;
MyDragTarget(this.inPos, this.controller, this.data, {Key key})
: super(key: key);
#override
_MyDragTargetState createState() =>
_MyDragTargetState(this.inPos, this.controller, this.data);
}
class _MyDragTargetState<T> extends State<MyDragTarget> {
Offset position = Offset(0.0, 0.0);
MyDraggableController<T> controller;
T data;
T currentBall;
_MyDragTargetState(this.position, this.controller, this.data);
#override
void initState() {
position = widget.inPos;
data = widget.data;
//this.controller.subscribeToOnTargetCallback(onTargetCallbackHandler);
super.initState();
}
#override
Widget build(BuildContext context) {
debugPrint(position.toString());
return Positioned(
left: position.dx-10,
top: position.dy-10,
child: DragTarget<T>(
builder: (context, list, list2) {
return Container(
decoration: BoxDecoration(
color: Colors.blueGrey,
borderRadius: BorderRadius.circular(50.0)),
height: 120,
width: 120,
child: Center(
child: Text(data.toString().toUpperCase()),
),
);
},
onWillAccept: (item){
debugPrint("will accept");
return true;
},
onAccept: (item) {
debugPrint('TARGET accepted $item');
//this.draggableController.onTarget(true, item);
//debugPrint("set currentball from "+ currentBall.toString() + " to" + item.toString());
//currentBall = item;
this.controller.onDropped(item,this.position);
return true;
},
),
);
}
}
controller
class MyDraggableController<T> {
List<Function(T,Offset)> _targetUpdateCallbacks = new List<Function(T,Offset)>();
//List<Function( )> _targetMoveCallbacks = new List<Function( )>();
MyDraggableController();
void onDropped(T draggableData,Offset targetPosition) {
debugPrint("dropped" + draggableData.toString());
_targetUpdateCallbacks.forEach((f) {
f(draggableData,targetPosition);
});
}
void subscribeToOnTargetCallback(Function(T,Offset) f) {
_targetUpdateCallbacks.add(f);
}
void unSubscribeFromOnTargetCallback(Function(T,Offset) f) {
_targetUpdateCallbacks.remove(f);
}
}

Make the overlaying Draggable widget a child of the DragTarget.
class _MyDragTargetState<T> extends State<MyDragTarget> {
...
#override
Widget build(BuildContext context) {
debugPrint(position.toString());
return Positioned(
left: position.dx-10,
top: position.dy-10,
child: DragTarget<T>(
builder: (context, list, list2) {
return Stack(
children: [
Container(
decoration: BoxDecoration(
color: Colors.blueGrey,
borderRadius: BorderRadius.circular(50.0)
),
height: 120,
width: 120,
child: Center(
child: Text(data.toString().toUpperCase()
),
),
MyDraggable(...) // Put your Draggable widgets here onwards
]
);
},
...
}

Use Provider architecture.
Constants.HOME_SCREEN: (BuildContext context) => ChangeNotifierProvider(
builder: (context) => Data(), child: HomePage())
Initialize getters and setters for the variables and inside setter functions notifyListeners() will be called so that the Widgets listening for these variables can rebuild.
removeLastItem() method is created which will remove the last item from the list of Draggable.
removeLastItem() {
items.removeLast();
notifyListeners();
}
The best example is by Manik Gupta.
Follow this link.
https://medium.com/flutterdevs/draggable-and-drag-target-in-flutter-2513ea7c09f2
class DragTargetWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return DragTarget(onWillAccept: (data) {
return true;
}, onAccept: (CardItem data) {
if (Provider.of<Data>(context).itemsList.length >= 1) {
Provider.of<Data>(context).removeLastItem();
Provider.of<Data>(context).changeSuccessDrop(true);
Provider.of<Data>(context).changeAcceptedData(data);
}

Related

How can I achieve "child of a widget which is inside a scroll widget acting like sticky header" in Flutter?

I'm trying to find a way to implement a functionality in which, in a horizontally scrollable list, there are widgets that I will call P, (which are denoted as P1, P2 and P3 in the diagram) and their children C, (which are denoted as C1, C2 and C3). As the user scrolls the list horizontally, I want C's inside P's to act like sticky headers, until they reach the boundary of their parent.
I'm sorry if the description & diagram is not enough, I will try to clarify anything unclear.
Diagram of the problem
As I'm thinking of a way to implement this, I can't seem to find a plausible solution. Also if there is a package that can help with this issue, I would really appreciate any suggestions.
I am not sure about your picture, but maybe this is do you want?
our tools :
BuildOwners -> to measure size of the widget before rebuild,
NotificationListeners -> to trigger rebuild based on ScrollNotification. i use stateful Widget, but you can tweak it into ValueNotifier and Build the Sticker with ValueListenableBuilder instead.
ListView.Builder -> actually you can replace this with any kind of Scrollable, we only need to listen scroll event.
how its work?
its simple :
we need to know the P dx Offset, check if C offset small than P, then use that value to adjust x Positioned of C in Stack. and clamp it with max value (P.width)
double _calculateStickerXPosition(
{required double px, required double cx, required double cw}) {
if (cx < px) {
return widget.stickerHorizontalPadding + (px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding*2));
}
return widget.stickerHorizontalPadding;
}
full code :
main.dart :
import 'dart:ui';
import 'package:flutter/material.dart';
import 'scrollable_sticker.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
// i use chrome to test it, so igrone this
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown
},
),
home: const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: ScrollableSticker(
children: List.generate(10, (index) => Container(
width: 500,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.orange)),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 50.0, horizontal: 50.0),
child: Text(
"P1",
textDirection: TextDirection.ltr,
),
),
)),
stickerBuilder: (index) => Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: Colors.red),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
'C$index',
),
),
)),
),
);
}
}
scrollable_sticker.dart :
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ScrollableSticker extends StatefulWidget {
final List<Widget> children;
final Widget Function(int index) stickerBuilder;
final double stickerHorizontalPadding;
const ScrollableSticker(
{Key? key,
required this.children,
required this.stickerBuilder,
this.stickerHorizontalPadding = 10.0})
: super(key: key);
#override
State<ScrollableSticker> createState() => _ScrollableStickerState();
}
class _ScrollableStickerState extends State<ScrollableSticker> {
late List<GlobalKey> _keys;
late GlobalKey _parentKey;
#override
void initState() {
super.initState();
_keys = List.generate(widget.children.length, (index) => GlobalKey());
_parentKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (sc) {
setState(() {});
return true;
},
child: ListView.builder(
key: _parentKey,
scrollDirection: Axis.horizontal,
itemCount: widget.children.length,
itemBuilder: (context, index) {
final itemSize = measureWidget(Directionality(
textDirection: TextDirection.ltr, child: widget.children[index]));
final stickerSize = measureWidget(Directionality(
textDirection: TextDirection.ltr,
child: widget.stickerBuilder(index)));
final BuildContext? itemContext = _keys[index].currentContext;
double x = widget.stickerHorizontalPadding;
if (itemContext != null) {
final pcontext = _parentKey.currentContext;
Offset? pOffset;
if (pcontext != null) {
RenderObject? obj = pcontext.findRenderObject();
if (obj != null) {
final prb = obj as RenderBox;
pOffset = prb.localToGlobal(Offset.zero);
}
}
final obj = itemContext.findRenderObject();
if (obj != null) {
final rb = obj as RenderBox;
final cx = rb.localToGlobal(pOffset ?? Offset.zero).dx;
x = _calculateStickerXPosition(
px: pOffset != null ? pOffset.dx : 0.0,
cx: cx,
cw: (itemSize.width - stickerSize.width));
}
}
return SizedBox(
key: _keys[index],
height: itemSize.height,
width: itemSize.width,
child: Stack(
children: [
widget.children[index],
Positioned(
top: itemSize.height / 2,
left: x,
child: FractionalTranslation(
translation: const Offset(0.0, -0.5),
child: widget.stickerBuilder(index)))
],
),
);
},
),
);
}
double _calculateStickerXPosition(
{required double px, required double cx, required double cw}) {
if (cx < px) {
return widget.stickerHorizontalPadding +
(px - cx).clamp(0.0, cw - (widget.stickerHorizontalPadding * 2));
}
return widget.stickerHorizontalPadding;
}
}
Size measureWidget(Widget widget) {
final PipelineOwner pipelineOwner = PipelineOwner();
final MeasurementView rootView = pipelineOwner.rootNode = MeasurementView();
final BuildOwner buildOwner = BuildOwner(focusManager: FocusManager());
final RenderObjectToWidgetElement<RenderBox> element =
RenderObjectToWidgetAdapter<RenderBox>(
container: rootView,
debugShortDescription: '[root]',
child: widget,
).attachToRenderTree(buildOwner);
try {
rootView.scheduleInitialLayout();
pipelineOwner.flushLayout();
return rootView.size;
} finally {
// Clean up.
element.update(RenderObjectToWidgetAdapter<RenderBox>(container: rootView));
buildOwner.finalizeTree();
}
}
class MeasurementView extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
#override
void performLayout() {
assert(child != null);
child!.layout(const BoxConstraints(), parentUsesSize: true);
size = child!.size;
}
#override
void debugAssertDoesMeetConstraints() => true;
}
you could try to use c padding dynamically
padding: EdgeInsets.only(left: 0.1 * [index], right: 1 * [index])
for example, I hope it helps.

Load more datas when scrolling on a GridView Flutter

I am making an application where I can run through items in a GridView on a profile page, like on Instagram when we scroll our posts.
I want to load more items (15 per 15) when I scroll on my GridView.
I want an infinite loading.
So I added a ScrollListener to my GridView.
If I put an "initialScrollOffset" to "5.0" in attribute to my ScrollListener, it will load the 15 first items and make one loading, so it's add 15 items (work only 1 time), but if I let the default value, it loads no items.
I would like to have an infinite loading.
My GridView code :
import 'dart:developer';
import 'package:dresskip/model/item_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ItemSection extends StatefulWidget {
const ItemSection({Key? key}) : super(key: key);
#override
_ItemSectionState createState() => _ItemSectionState();
}
class _ItemSectionState extends State<ItemSection> {
List<Item> items = [];
bool isLoading = false;
int pageCount = 1;
ScrollController _scrollController = ScrollController(initialScrollOffset: 5.0);
#override
void initState() {
super.initState();
///LOADING FIRST DATA
addItemsToList(1);
_scrollController.addListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Row(),
GridView.count(
controller: _scrollController,
crossAxisCount: 3,
shrinkWrap: true,
//physics: const ScrollPhysics() /*AlwaysScrollableScrollPhysics()*/,
mainAxisSpacing: 0,
children: items.map((value) {
return Image.network(value.picture);
}).toList(),
)
],
);
}
//// ADDING THE SCROLL LISTINER
_scrollListener() {
//inspect(_scrollController.offset);
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
!_scrollController.position.outOfRange) {
setState(() {
print("comes to bottom $isLoading");
isLoading = true;
if (isLoading) {
print("RUNNING LOAD MORE");
pageCount = pageCount + 1;
addItemsToList(pageCount);
}
});
}
}
addItemsToList(int page) {
//if (page < 5) {}
Item myItem = Item(
name: "test",
brand: "test",
color: ["0xFF39BDC8", "0xFFdb8abc", ""],
picture:
"https://images.pexels.com/photos/9676177/pexels-photo-9676177.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
//"https://scontent.fcdg2-1.fna.fbcdn.net/v/t1.6435-9/171944671_3950113148381954_7059062044076097927_n.jpg?_nc_cat=110&ccb=1-5&_nc_sid=09cbfe&_nc_ohc=gxbPXmRQmN8AX9V5Bx5&_nc_ht=scontent.fcdg2-1.fna&oh=ac2a57c8c1d1b0b01fcf131ac42c4023&oe=6190A9BF",
solo: false,
clean: true,
type: "test");
for (int i = (pageCount * 15) - 15; i < pageCount * 15; i++) {
items.add(myItem);
isLoading = false;
}
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Item model class
class Item {
String name;
String brand;
List<String> color;
String picture;
bool solo;
bool clean;
String type;
Item({
required this.name,
required this.brand,
required this.color,
required this.picture,
required this.solo,
required this.clean,
required this.type,
});
}
The first part (profile section) code
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '/assets/constants.dart' as constants;
import '../../assets/dresskip_icon_icons.dart' as DresskipIcons;
class ProfileSection extends StatelessWidget {
final List<String> description;
final VoidCallback onClicked;
const ProfileSection({
Key? key,
required this.description,
required this.onClicked,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment
.spaceBetween, //pour que chaque colonnes dans la ligne ait leurs propres tailles
crossAxisAlignment: CrossAxisAlignment
.start, //pour tout coller en haut du container
children: <Widget>[
const Icon(Icons.local_laundry_service),
Stack(children: [
buildImage(),
Positioned(
bottom: 0,
right: 4,
child:
buildEditIcon(Color(constants.COLOR_BLUE_DRESSKIP)))
]),
const Icon(Icons.settings),
],
),
// Partie description
Container(
child: Text(description[0]),
margin: EdgeInsets.fromLTRB(50, 5, 50, 5)),
// Partie Instagram
Container(
child: Row(
children: <Widget>[
Container(
child: const Icon(DresskipIcons.DresskipIcon.instagram),
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
),
Expanded(child: Text(description[1]))
],
),
margin: const EdgeInsets.fromLTRB(20, 5, 20, 5)),
// Partie Facebook
Container(
child: Row(
children: <Widget>[
Container(
child: const Icon(Icons.facebook),
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
),
Expanded(child: Text(description[2]))
],
),
margin: const EdgeInsets.fromLTRB(20, 5, 20, 5)),
// Partie Twitter
Container(
child: Row(
children: <Widget>[
Container(
child:
const Icon(DresskipIcons.DresskipIcon.twitter_square),
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
),
Expanded(child: Text(description[3]))
],
),
margin: const EdgeInsets.fromLTRB(20, 5, 20, 5)),
],
));
}
// Widget pour afficher l'image
Widget buildImage() {
// use if is an image on the web with the link : final image = NetworkImage(imagePath);
return ClipOval(
child: Material(
color: Colors.transparent,
child: Ink.image(
image: const AssetImage("assets/undraw_female_avatar.png"),
fit: BoxFit.cover,
width: 128,
height: 128,
child: InkWell(onTap: onClicked),
)));
}
// Widget pour l'ajout de l'icone à coté de l'image
// Ici, il y a 2 fois buildCircle pour arrondir l'icone et ensuite mettre le trait blanc arrondi entre la photo et l'icône
Widget buildEditIcon(Color color) => buildCircle(
color: Colors.white,
all: 1,
child: buildCircle(
color: color,
all: 8,
child: Icon(Icons.add_a_photo, color: Colors.white, size: 20)));
// Widget permettant d'arrondir l'image
Widget buildCircle({
required Widget child,
required double all,
required Color color,
}) =>
ClipOval(
child: Container(
padding: EdgeInsets.all(all), color: color, child: child));
}
The parent page code
import 'package:dresskip/model/user_model.dart';
import 'package:flutter/material.dart';
import 'itemSection_widget.dart';
import 'profileSection_widget.dart';
import '/assets/constants.dart' as constants;
import 'dart:convert';
class AccountPage extends StatelessWidget {
const AccountPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
ProfileSection(
description: getUserInformation().properties,
onClicked: () async {}),
const Divider(
color: Color(constants.COLOR_BLUE_DRESSKIP),
thickness: 2,
indent: 50,
endIndent: 50,
),
ItemSection()
],
)));
}
getUserInformation() {
User myUser = User(username: "test", email: "test#test.test", properties: [
"22 yo\nFlexeur/Epicurien/Philanthrope\nJ'adore la vie\nEFREI Paris",
"instagram_test",
"facebook_test",
"twitter"
]);
return myUser;
}
}
There are 2 screenshots of my App.
The problem here, it's just loading 15 + 15 items (the first 30) and I can't load more data on scrolling.
EDIT
I find a way to resolv this problem. The attribute "shrinkwrap" block the possibility to scroll more because my widget which contains the gridview is into a Column Widget.
So i removed it, but just the Gridview is scrolling, I would like to do like Instagram's profil, when you scroll on your pictures, all the page scroll and not only the GridView.
Do you have an idea ?
Endless / Infinite Scroll GridView
This example uses a GridView.builder & doesn't need a ScrollController.
When the end of the current dataset is reached, it will request more data and rebuild the GridView.
We can pad the end of the dataset with a special item. When this special item is built by GridView.builder, it will:
show a loading indicator
request more data from datasource
rebuild the GridView when data arrives
import 'package:flutter/material.dart';
class InfiniteScrollPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Infinite Scroll'),
),
body: EndlessGrid());
}
}
class EndlessGrid extends StatefulWidget {
#override
_EndlessGridState createState() => _EndlessGridState();
}
class _EndlessGridState extends State<EndlessGrid> {
NumberGenerator _numGen = NumberGenerator();
List<int> _data = [];
#override
void initState() {
super.initState();
_data = _numGen.nextPage();
}
#override
Widget build(BuildContext context) {
return GridView.builder(
itemCount: _data.length + 1, // pad data with an extra item at end
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: 10, mainAxisSpacing: 10, crossAxisCount: 2),
itemBuilder: (context, i) {
if (i < _data.length) {
return gridItem(i);
}
else { // extra item will request next page & rebuild widget
getNextPage();
return CircularProgressIndicator();
}
},
);
}
Widget gridItem(int i) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.lightBlue
),
padding: EdgeInsets.all(5),
child: Text('$i'),
);
}
/// Request next page of data and call setState to rebuild GridView with
/// new data.
Future<void> getNextPage() async {
var _nextPage = await Future.delayed(Duration(seconds: 2), _numGen.addPage);
setState(() {
_data = _nextPage;
});
}
}
/// Mock data to fill GridView
class NumberGenerator {
static const PAGESIZE = 15;
List<int> dataset = [];
final int start;
NumberGenerator({this.start = 0});
List<int> addItem() {
dataset.add(lastItem + 1);
return dataset;
}
List<int> addPage() {
dataset.addAll(nextPage());
return dataset;
}
int get lastItem => dataset.isNotEmpty ? dataset.last : start;
List<int> nextPage({int start, int size = PAGESIZE}) {
start ??= lastItem;
return List<int>.generate(size, (i) => start + i + 1);
}
}
Firstly, I'm not aware of Instagram UI.
The problem is here with parent, while SingleChildScrollView is the parent and you want to scroll the full page and generate GridItem based on it, therefore set we don't need to ScrollController for GridView instead use it on SingleChildScrollView.
The code structure will be
parent class generate data for GridView and will be StatefulWidget.
while there are two scrollable widgets so that GridView < physics: NeverScrollableScrollPhysics(),>
AccountPage: SingleChildScrollView will have that _scrollController
ItemSection can be StatelessWidget
Full Code snippet with dummyHeaderWidget
import 'package:flutter/material.dart';
class AccountPage extends StatefulWidget {
const AccountPage({Key? key}) : super(key: key);
#override
State<AccountPage> createState() => _AccountPageState();
}
class _AccountPageState extends State<AccountPage> {
ScrollController _scrollController = ScrollController();
bool isLoading = true; //
int pageCount = 1;
List items = [];
//// ADDING THE SCROLL LISTINER
void _scrollListener() {
print(
"current ${_scrollController.offset} max: ${_scrollController.position.maxScrollExtent}");
if (_scrollController.offset >=
_scrollController.position.maxScrollExtent &&
!_scrollController.position.outOfRange) {
setState(() {
print("comes to bottom $isLoading");
isLoading = true;
if (isLoading) {
print("RUNNING LOAD MORE");
pageCount = pageCount + 1;
addItemsToList(pageCount);
}
});
}
}
addItemsToList(int page) {
//if (page < 5) {}
Item myItem = Item(
name: "test",
brand: "test",
color: ["0xFF39BDC8", "0xFFdb8abc", ""],
picture:
"https://images.pexels.com/photos/9676177/pexels-photo-9676177.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
//"https://scontent.fcdg2-1.fna.fbcdn.net/v/t1.6435-9/171944671_3950113148381954_7059062044076097927_n.jpg?_nc_cat=110&ccb=1-5&_nc_sid=09cbfe&_nc_ohc=gxbPXmRQmN8AX9V5Bx5&_nc_ht=scontent.fcdg2-1.fna&oh=ac2a57c8c1d1b0b01fcf131ac42c4023&oe=6190A9BF",
solo: false,
clean: true,
type: "test");
for (int i = (pageCount * 15) - 15; i < pageCount * 15; i++) {
items.add(myItem);
isLoading = false;
}
}
#override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
addItemsToList(1);
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: [
Container(
height: 300,
color: Colors.green,
),
const Divider(
thickness: 2,
indent: 50,
endIndent: 50,
),
ItemSection(
items: items,
),
],
),
),
);
}
}
class ItemSection extends StatelessWidget {
final List items;
const ItemSection({
Key? key,
required this.items,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
mainAxisSpacing: 0,
children: items.map((value) {
return Image.network(value.picture);
}).toList(),
);
}
}
class Item {
String name;
String brand;
List<String> color;
String picture;
bool solo;
bool clean;
String type;
Item({
required this.name,
required this.brand,
required this.color,
required this.picture,
required this.solo,
required this.clean,
required this.type,
});
}
Read the code, hope you will get concept and make changes on your project.

Flutter - select only single item in list view

In my app I am generating a ListView and items can be highlighted by tapping on them. That works fine and I also have a callback function that gives me the key for the just selected item. I can currently manually deselect the item by tapping on it again, but will ultimately take that functionality out.
My problem is that I want one and only one item to be selected at a time. In order to create the list I currently take some initial content in the form of a list, generate the tiles and add them to another list. I then use that list to create the ListView. My plan was on the callback from a new selection, run through the list of tiles and deselect them before highlighting the new chosen tile and carrying out the other functions. I have tried various methods to tell each tile to deselect itself but have not found any way to address each of the tiles. Currently I get the error:
Class 'OutlineTile' has no instance method 'deselect'.
Receiver: Instance of 'OutlineTile'
Tried calling: deselect()
I have tried to access a method within the tile class and to use a setter but neither worked so far. I am quite new to flutter so it could be something simple I am missing. My previous experience was with Actionscript where this system would have worked fine and I could access a method of an object (in this case the tile) easily as long s it is a public method.
I'd be happy to have another way to unselect the old item or to find a way to access a method within the tile. The challenge is to make the tiles show not highlighted without them being tapped themselves but when a different tile is tapped.
The code in my parent class is as follows:
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
final _formKey = GlobalKey<FormState>();
final myController = TextEditingController();
//String _startType;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
List _outLineTiles = [];
int _counter = 0;
#override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
void initState() {
super.initState();
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_startContent.forEach((element) {
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
onTileSelected: clearHilights,
));
_counter++;
});
}
dynamic clearHilights(Key myKey) {
_outLineTiles.forEach((element) {
element.deselect(); // this throws an error Class 'OutlineTile' has no instance method 'deselect'.
Key _foundKey = element.key;
print("Element Key $_foundKey");
});
}
.......
and further down within the widget build scaffold:
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
Then the tile class is as follows:
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final Function(Key) onTileSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.onTileSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
Key _myKey;
#override
void initState() {
super.initState();
color = Colors.transparent;
}
bool _isSelected = false;
set isSelected(bool value) {
_isSelected = value;
print("set is selected to $_isSelected");
}
void changeSelection() {
setState(() {
_myKey = widget.key;
_isSelected = !_isSelected;
if (_isSelected) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
void deselect() {
setState(() {
isSelected = false;
color = Colors.transparent;
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
widget.onTileSelected(_myKey);
changeSelection();
},
),
........
Thanks for any help.
Amended Code sample and explanation, that builds to a complete project, from here:
Following the advice from phimath I have created a full buildable sample of the relevant part of my project.
The problem is that the tiles in my listview are more complex with several elements, many of which are buttons in their own right so whilst phimath's solution works for simple text tiles I have not been able to get it working inside my own project. My approach is trying to fundamentally do the same thing as phimath's but when I include these more complex tiles it fails to work.
This sample project is made up of three files. main.dart which simply calls the project and passes in some dummy data in the way my main project does. working_draft.dart which is the core of this issue. And outline_tile.dart which is the object that forms the tiles.
Within working draft I have a function that returns an updated list of the tiles which should show which tile is selected (and later any other changes from the other buttons). This gets called when first going to the screen. When the tile is tapped it uses a callback function to redraw the working_draft class but this seems to not redraw the list as I would expect it to. Any further guidance would be much appreciated.
The classes are:
first class is main.dart:
import 'package:flutter/material.dart';
import 'package:listexp/working_draft.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: WorkingDraft(
startType: "Basic",
name: "Draft",
currentID: "anID",
startContent: [
["Heading", "New Heading"],
["Paragraph", "New Text"],
["Image", "placeholder"],
["Signature", "placeholder"]
],
));
}
}
Next file is working_draft.dart:
import 'package:flutter/material.dart';
import 'package:listexp/outline_tile.dart';
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
final int selectedIndex;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent,
this.selectedIndex});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
int selectedIndex;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
var _outLineTiles = [];
int _counter = 0;
int _selectedIndex;
bool _isSelected;
dynamic clearHilights(int currentIndex) {
setState(() {
_selectedIndex = currentIndex;
});
}
updatedTiles() {
if (_selectedIndex == null) {
_selectedIndex = 0;
}
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_outLineTiles = [];
_startContent.forEach((element) {
_isSelected = _selectedIndex == _counter ? true : false;
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
myIndex: _counter,
onTileSelected: clearHilights,
isSelected: _isSelected,
));
_counter++;
});
}
#override
Widget build(BuildContext context) {
updatedTiles();
return Scaffold(
body: Center(
child: Column(children: [
SizedBox(height: 100),
Text("Outline", style: new TextStyle(fontSize: 15)),
Container(
height: 215,
width: 300,
decoration: BoxDecoration(
border: Border.all(
color: Colors.lightGreenAccent,
width: 2,
),
borderRadius: BorderRadius.circular(2),
),
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
),
]),
));
}
}
and finally is outline_tile.dart
import 'package:flutter/material.dart';
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final int myIndex;
final Function(int) onTileSelected;
final bool isSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.myIndex,
this.onTileSelected,
this.isSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
// Key _myKey;
bool _isSelected;
#override
void initState() {
super.initState();
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
}
void deselect() {
setState(() {
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else if (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
} else if (widget.outlineName == "Signature") {
Text("Called Signature");
} else {
Text("Called Image");
}
var _myIndex = widget.myIndex;
widget.onTileSelected(_myIndex);
deselect();
},
),
),
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_up),
onPressed: () {
print("Move Up");
}),
),
SizedBox(height: 5),
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_down),
onPressed: () {
print("Move Down");
}),
),
],
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.add_box),
onPressed: () {
print("Add another");
}),
),
SizedBox(
height: 10,
),
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.delete),
onPressed: () {
print("Delete");
}),
),
],
),
),
],
),
);
}
}
Thanks again
Instead of manually deselecting tiles, just keep track of which tile is currently selected.
I've made a simple example for you. When we click a tile, we just set the selected index to the index we clicked, and each tile looks at that to see if its the currently selected tile.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Home()),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int selectedIndex;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item: $index'),
tileColor: selectedIndex == index ? Colors.blue : null,
onTap: () {
setState(() {
selectedIndex = index;
});
},
);
},
);
}
}

How to use two global keys for two widgets sharing the same state class

I am trying to create my own custom segment in flutter. That segment has two buttons, one for teachers and other for students. What I am trying to do, it's encapsulate the buttons in one Stateful Widget to handle the setState of both buttons, because I want the buttons to be an AnimatedContainer and if I rebuild the childrens (the buttons) from the parent the transition doesn't works.
Note that the buttons are Stack positioned and I reorder the content to get the tapped button over the other (that will has effect when I set more width in the tapped button, now this is not created yet).
Here is my code:
import 'package:flutter/cupertino.dart';
import '../../app_localizations.dart';
import '../../styles.dart';
GlobalKey<_ButtonState> teachersButtonKey = GlobalKey();
GlobalKey<_ButtonState> studentsButtonKey = GlobalKey();
String _globalTappedButtonId = 'teachersButton';
class FiltersAppBarSegment extends StatefulWidget {
#override
_FiltersAppBarSegmentState createState() => _FiltersAppBarSegmentState();
}
class _FiltersAppBarSegmentState extends State<FiltersAppBarSegment> {
List<Widget> buildStackChildren(SegmentChangedCallBack handleSegmentChanged) {
if (_globalTappedButtonId == 'teachersButton') {
return <Widget>[
Container(
key: UniqueKey(),
child: _Button(
key: studentsButtonKey,
id: 'studentsButton',
label: 'seeStudents',
rightPosition: 1,
onSegmentChanged: handleSegmentChanged,
),
),
Container(
key: UniqueKey(),
child: _Button(
key: teachersButtonKey,
id: 'teachersButton',
label: 'amTeacher',
rightPosition: null,
onSegmentChanged: handleSegmentChanged,
),
),
];
} else {
return <Widget>[
Container(
key: UniqueKey(),
child: _Button(
key: driverButtonKey,
id: 'driverButton',
label: 'amDriver',
rightPosition: null,
onSegmentChanged: handleSegmentChanged,
),
),
Container(
key: UniqueKey(),
child: _Button(
key: studentsButtonKey,
id: 'studentButton',
label: 'amStudent',
rightPosition: 1,
onSegmentChanged: handleSegmentChanged,
),
),
];
}
}
void handleSegmentChanged(String clickedButtonId) {
teachersButtonKey.currentState._handleButtonTapped();
studentsButtonKey.currentState._handleButtonTapped();
}
#override
Widget build(BuildContext context) {
return Container(
height: 42,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Stack(children: buildStackChildren(handleSegmentChanged)),
);
}
}
class _Button extends StatefulWidget {
final String id;
final String label;
final double rightPosition;
final void onSegmentChanged;
_Button({
Key key,
this.id,
this.label,
this.rightPosition,
this.onSegmentChanged,
}) : super(key: key);
#override
_ButtonState createState() => _ButtonState();
}
class _ButtonState extends State<_Button> {
bool _tapped;
double _topPosition;
double _width;
double _height;
double _getTopPosition() => _tapped ? 0 : 5;
double _getHeight() => _tapped ? 42 : 32;
Gradient _getGradient() {
if (_tapped) {
return Styles.darkAccentColorGradient;
} else {
return Styles.darkAccentColorGradientDisabled;
}
}
void _handleButtonTapped() {
setState(() {
_globalTappedButtonId = widget.id;
_tapped = (widget.id == _globalTappedButtonId);
_topPosition = _getTopPosition();
_height = _getHeight();
});
}
#override
void initState() {
super.initState();
_tapped = (widget.id == _globalTappedButtonId);
_topPosition = _getTopPosition();
_height = _getHeight();
}
#override
Widget build(BuildContext context) {
return Positioned(
top: _topPosition,
right: widget.rightPosition,
child: GestureDetector(
onTap: () {
widget.onSegmentChanged('test');
},
child: AnimatedContainer(
duration: Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
width: _width,
height: _height,
decoration: BoxDecoration(
gradient: _getGradient(),
borderRadius: BorderRadius.circular(13),
),
child: Center(
child: Text(
AppLocalizations.of(context).translate(widget.label),
style: Styles.bodyWhiteText,
textAlign: TextAlign.center,
),
),
),
),
);
}
}
I'm sure you have already found a solution to your problem by now, but this question is one of the first search results when looking at this error.
As you already know, per the Flutter doc on GlobalKey:
"You cannot simultaneously include two widgets in the tree with the
same global key. Attempting to do so will assert at runtime."
You can define your own individual keys like:
import 'package:flutter/widgets.dart';
class TestKeys{
static final testKey1 = const Key('__TESTKEY1__');
static final testKey2 = const Key('__TESTKEY2__');
...
}
And then reference them in the widget with key: TestKeys.testKey1
This was described in this question here so perhaps it can help someone with the need for a similar use case.
There are also a few solutions listed in this GitHub issue

What should I do if the nodes of a multlevel list have content that should be displayed when clicked? (Advanced ExpansionTile)

For a flutter project I needed a tree structure in which when clicking on a node, not only the entries below it are displayed, but - as with the file manager in Windows - also the content: On a smartphone as a new screen and on a tablet as an additional area to the right of the list.
Unfortunately, the standard ExpansionTile does not have this capability.
Since I'm a newcomer to Flutter, I first looked at the source code and tried to understand the most important parts (I'm still at it ;-). Then I made the following changes:
A property 'AlignOpener' now decides whether the open/close icon is displayed on the left or right.
I added the properties 'onTap' and 'onLongPress' as callback. Once one of these properties is assigned in the calling widget, the '_isInAdvancedMode' property is set to true and an IconButton is inserted as leading or trailing.
Clicking this button will open/close the directory tree. A click on the remaining part is forwarded to the calling widget via callback.
Finally, I added a property 'indentListTile' to control the indent of each layer.
If none of the properties are assigned, the AvancedExpansionTile behaves like the standard ExpansionTile.
Since I'm a newbie, there's still a lot of uncertainty as to whether the code is really correct. As far as I can see, it works, but I would be happy if experienced developers among you could check the code and make suggestions for improvements if necessary.
Maybe the code solves a problem that others also had?
Her is the code:
master_list_item.dart
import 'package:meta/meta.dart';
class ItemMaster {
ItemMaster(
{this.id,
#required this.title,
this.subtitle,
this.children = const <ItemMaster>[]});
final int id;
final String title;
final String subtitle;
final List<ItemMaster> children;
}
master_list.dart
import 'master_list_item.dart';
class MasterList {
List<ItemMaster> get items {
return _items;
}
final List<ItemMaster> _items = <ItemMaster>[
ItemMaster(
title: 'Chapter Master List',
id: 1,
children: <ItemMaster>[
ItemMaster(
title: 'Scene A0',
id: 2,
children: <ItemMaster>[
ItemMaster(title: 'Item A0.1', id: 3),
ItemMaster(title: 'Item A0.2', id: 4),
ItemMaster(title: 'Item A0.3', id: 5),
],
),
ItemMaster(title: 'Scene A1', id: 6),
ItemMaster(title: 'Scene A2', id: 7),
],
),
ItemMaster(
title: 'Chapter B',
id: 8,
children: <ItemMaster>[
ItemMaster(title: 'Scene B0', id: 9),
ItemMaster(title: 'Scene B1', id: 10),
],
),
ItemMaster(
title: 'Chapter C',
id: 11,
children: <ItemMaster>[
ItemMaster(title: 'Scene C0', id: 12),
ItemMaster(title: 'Scene C1', id: 13),
ItemMaster(
title: 'Scene C2',
id: 14,
children: <ItemMaster>[
ItemMaster(title: 'Item C2.0', id: 15),
ItemMaster(title: 'Item C2.1', id: 16),
ItemMaster(title: 'Item C2.2', id: 17),
ItemMaster(title: 'Item C2.3', id: 18),
],
),
],
),
];
}
main.dart
import 'package:flutter/material.dart';
import 'advanced_expansion_tile.dart';
import 'master_list_item.dart';
import 'master_list.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 Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<ItemMaster> items;
#override
void initState() {
// TODO: implement initState
super.initState();
items = MasterList().items;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, int index) => MasterListEntry(items[index]),
)),
),
);
}
}
class MasterListEntry extends StatelessWidget {
const MasterListEntry(this.entry);
final ItemMaster entry;
Widget _buildTiles(ItemMaster root) {
if (root.children.isEmpty)
return ListTile(
title: Text(root.title),
onTap: () => print("onTap listTile"),
);
return AdvancedExpansionTile(
key: PageStorageKey<ItemMaster>(root),
title: Text(root.title),
children: root.children.map(_buildTiles).toList(),
onTap: () => print("onTap AdvancedExpansionTile"),
alignOpener: AlignOpener.Right,
indentListTile: 15.0,
// isInAdvancedMode: true,
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(entry);
}
}
advanced_expansion_tile.dart (based on the source code from the Flutter team)
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
const Duration _kExpand = Duration(milliseconds: 200);
enum AlignOpener { Left, Right }
class AdvancedExpansionTile extends StatefulWidget {
const AdvancedExpansionTile({
Key key,
this.leading,
this.trailing,
#required this.title,
this.backgroundColor,
this.onExpansionChanged,
this.onTap,
this.onLongPress,
this.alignOpener,
this.indentListTile,
this.children = const <Widget>[],
this.initiallyExpanded = false,
}) : assert(initiallyExpanded != null),
super(key: key);
/// A widget to display before the title.
///
/// Typically a [CircleAvatar] widget.
final Widget leading;
/// The primary content of the list item.
///
/// Typically a [Text] widget.
final Widget title;
/// Called when the tile expands or collapses.
///
/// When the tile starts expanding, this function is called with the value
/// true. When the tile starts collapsing, this function is called with
/// the value false.
final ValueChanged<bool> onExpansionChanged;
/// The widgets that are displayed when the tile expands.
///
/// Typically [ListTile] widgets.
final List<Widget> children;
/// The color to display behind the sublist when expanded.
final Color backgroundColor;
/// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
final bool initiallyExpanded;
/// A widget to display instead of a rotating arrow icon.
final Widget trailing;
/// A callback for onTap and onLongPress on the listTile
final GestureTapCallback onTap;
final GestureLongPressCallback onLongPress;
/// The side where the Open/Close-Icon/IconButton will be placed
final AlignOpener alignOpener;
/// indent of listTile (left)
final indentListTile;
#override
_AdvancedExpansionTileState createState() => _AdvancedExpansionTileState();
}
class _AdvancedExpansionTileState extends State<AdvancedExpansionTile>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeOutTween =
CurveTween(curve: Curves.easeOut);
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static final Animatable<double> _halfTween =
Tween<double>(begin: 0.0, end: 0.5);
final ColorTween _borderColorTween = ColorTween();
final ColorTween _headerColorTween = ColorTween();
final ColorTween _iconColorTween = ColorTween();
final ColorTween _backgroundColorTween = ColorTween();
AnimationController _controller;
Animation<double> _iconTurns;
Animation<double> _heightFactor;
Animation<Color> _borderColor;
Animation<Color> _headerColor;
Animation<Color> _iconColor;
Animation<Color> _backgroundColor;
bool _isExpanded = false;
/// If set to true an IconButton will be created. This button will open/close the children
bool _isInAdvancedMode;
AlignOpener _alignOpener;
double _indentListTile;
#override
void initState() {
super.initState();
_controller = AnimationController(duration: _kExpand, vsync: this);
_heightFactor = _controller.drive(_easeInTween);
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
_borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
_backgroundColor =
_controller.drive(_backgroundColorTween.chain(_easeOutTween));
_isExpanded =
PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
if (_isExpanded) _controller.value = 1.0;
/// OnTap or onLongPress are handled in the calling widget --> AdvancedExpansionTile is in Advanced Mode
if (widget.onTap != null || widget.onLongPress != null) {
_isInAdvancedMode = true;
} else {
_isInAdvancedMode = false;
}
/// fallback to standard behaviour if aligning isn't set
_alignOpener = widget.alignOpener ?? AlignOpener.Right;
/// if no indent is set the indent will be 0.0
_indentListTile = widget.indentListTile ?? 0.0;
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleTap() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) return;
setState(() {
// Rebuild without widget.children.
});
});
}
PageStorage.of(context)?.writeState(context, _isExpanded);
});
if (widget.onExpansionChanged != null)
widget.onExpansionChanged(_isExpanded);
}
Widget _buildChildren(BuildContext context, Widget child) {
final Color borderSideColor = _borderColor.value ?? Colors.transparent;
return Container(
decoration: BoxDecoration(
color: _backgroundColor.value ?? Colors.transparent,
border: Border(
top: BorderSide(color: borderSideColor),
bottom: BorderSide(color: borderSideColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTileTheme.merge(
iconColor: _iconColor.value,
textColor: _headerColor.value,
child: ListTile(
onTap: () {
_isInAdvancedMode ? widget.onTap() : _handleTap();
}, // in AdvancedMode a callback will handle the gesture inside the calling widget
onLongPress: () {
_isInAdvancedMode ? widget.onLongPress() : _handleTap();
}, // in AdvancedMode a callback will handle the gesture inside the calling widget
leading: getLeading(),
title: widget.title,
trailing: getTrailing(),
),
),
ClipRect(
child: Padding(
padding: EdgeInsets.only(left: _indentListTile), // set the indent
child: Align(
heightFactor: _heightFactor.value,
child: child,
),
)),
],
),
);
}
#override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
_borderColorTween..end = theme.dividerColor;
_headerColorTween
..begin = theme.textTheme.subhead.color
..end = theme.accentColor;
_iconColorTween
..begin = theme.unselectedWidgetColor
..end = theme.accentColor;
_backgroundColorTween..end = widget.backgroundColor;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : Column(children: widget.children),
);
}
/// A method to decide what will be shown in the leading part of the lisTile
getLeading() {
if (_alignOpener.toString() == AlignOpener.Left.toString() &&
_isInAdvancedMode == true) {
return buildIcon(); //IconButton will be created
} else if (_alignOpener.toString() == AlignOpener.Left.toString() &&
_isInAdvancedMode == false) {
return widget.leading ??
RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
);
} else {
return widget.leading;
}
}
/// A method to decide what will be shown in the trailing part of the lisTile
getTrailing() {
if (_alignOpener.toString() == AlignOpener.Right.toString() &&
_isInAdvancedMode == true) {
return buildIcon(); //IconButton will be created
} else if (_alignOpener.toString() == AlignOpener.Right.toString() &&
_isInAdvancedMode == false) {
return widget.trailing ??
RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
);
} else {
return widget.leading;
}
}
/// A widget to build the IconButton for the leading or trailing part of the listTile
Widget buildIcon() {
return Container(
child: RotationTransition(
turns: _iconTurns,
child: IconButton(
icon: Icon(Icons.expand_more),
onPressed: () {
_handleTap();
//toDo: open/close is working but not the animation
},
),
));
}
}