How to create sticky shop button animation in Flutter? - flutter

How can I create this sticky buy button animation of Adidas app in Flutter. I have tried to use a scroll controller to listen for the position of user and then use an animated container but it is of no use since I have to define my scroll controller in my initstate while the height of my containers are relative to my device's height.
here is the link of video for the animation:
https://drive.google.com/file/d/1TzIUBr6abRQI87xAVu4NOPG67aftzceK/view?usp=sharing
this is what the widget tree looks like:
Scaffold(appbar,FAB,_body),
_body= SingleChildSrollView child:Column[Container(child:Listview)
,Container(child:PageView(children:[GridView])
,Container
,Container(this is where the shop button should be, the one that replaces the FAB)
,GridView,])

Output:
void main() => runApp(MaterialApp(home: Scaffold(body: HomePage(), appBar: AppBar())));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
ScrollController _controller = ScrollController();
double _boxHeight = 200, _screenHeight;
int _itemIndex = 5;
bool _itemVisibility = true;
#override
void initState() {
super.initState();
double offsetEnd;
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderBox box = context.findRenderObject();
_screenHeight = box.globalToLocal(Offset(0, MediaQuery.of(context).size.height)).dy;
offsetEnd = ((_itemIndex + 1) - (_screenHeight / _boxHeight)) * _boxHeight;
});
_controller.addListener(() {
if (_controller.position.pixels >= offsetEnd) {
if (_itemVisibility) setState(() => _itemVisibility = false);
} else {
if (!_itemVisibility) setState(() => _itemVisibility = true);
}
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ListView.builder(
controller: _controller,
itemCount: 8,
itemBuilder: (context, index) {
return _buildBox(
index: index,
color: index == _itemIndex ? Colors.cyan : Colors.blue[((index + 1) * 100) % 900],
);
},
),
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Visibility(
visible: _itemVisibility,
child: _buildBox(index: _itemIndex, color: Colors.cyan),
),
),
],
);
}
Widget _buildBox({int index, Color color}) {
return Container(
height: _boxHeight,
color: color,
alignment: Alignment.center,
child: Text(
"${index}",
style: TextStyle(fontSize: 52, fontWeight: FontWeight.bold),
),
);
}
}

Another answer (variable height boxes)
void main() => runApp(MaterialApp(home: Scaffold(body: HomePage(), appBar: AppBar())));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
ScrollController _controller = ScrollController();
double _screenHeight, _hRatings = 350, _hSize = 120, _hWidth = 130, _hComfort = 140, _hQuality = 150, _hBuy = 130, _hQuestions = 400;
bool _itemVisibility = true;
#override
void initState() {
super.initState();
double offsetEnd;
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderBox box = context.findRenderObject();
_screenHeight = box.globalToLocal(Offset(0, MediaQuery.of(context).size.height)).dy;
offsetEnd = (_hRatings + _hSize + _hWidth + _hComfort + _hQuality + _hBuy) - _screenHeight;
});
_controller.addListener(() {
if (_controller.position.pixels >= offsetEnd) {
if (_itemVisibility) setState(() => _itemVisibility = false);
} else {
if (!_itemVisibility) setState(() => _itemVisibility = true);
}
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ListView(
controller: _controller,
children: <Widget>[
_buildBox(_hRatings, "Ratings box", Colors.blue[200]),
_buildBox(_hSize, "Size box", Colors.blue[300]),
_buildBox(_hWidth, "Width box", Colors.blue[400]),
_buildBox(_hComfort, "Comfort box", Colors.blue[500]),
_buildBox(_hQuality, "Quality box", Colors.blue[600]),
_buildBox(_hBuy, "Buy box", Colors.orange[700]),
_buildBox(_hQuestions, "Questions part", Colors.blue[800]),
],
),
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Visibility(
visible: _itemVisibility,
child: _buildBox(_hBuy, "Buy box", Colors.orange[700]),
),
),
],
);
}
Widget _buildBox(double height, String text, Color color) {
return Container(
height: height,
color: color,
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(
fontSize: 32,
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
);
}
}

I would show a floatingActionButton when the embedded button is not visible. Here's a solution based on this thread : How to know if a widget is visible within a viewport?
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => new MyAppState();
}
class MyAppState extends State<MyApp> {
GlobalKey<State> key = new GlobalKey();
double fabOpacity = 1.0;
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Scrolling."),
),
body: NotificationListener<ScrollNotification>(
child: new ListView(
itemExtent: 100.0,
children: [
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
new MyObservableWidget(key: key),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder(),
ContainerWithBorder()
],
),
onNotification: (ScrollNotification scroll) {
var currentContext = key.currentContext;
if (currentContext == null) return false;
var renderObject = currentContext.findRenderObject();
RenderAbstractViewport viewport = RenderAbstractViewport.of(renderObject);
var offsetToRevealBottom = viewport.getOffsetToReveal(renderObject, 1.0);
var offsetToRevealTop = viewport.getOffsetToReveal(renderObject, 0.0);
if (offsetToRevealBottom.offset > scroll.metrics.pixels ||
scroll.metrics.pixels > offsetToRevealTop.offset) {
if (fabOpacity != 1.0) {
setState(() {
fabOpacity = 1.0;
});
}
} else {
if (fabOpacity == 1.0) {
setState(() {
fabOpacity = 0.0;
});
}
}
return false;
},
),
floatingActionButton: new Opacity(
opacity: fabOpacity,
child: Align(
alignment: Alignment.bottomCenter,
child: new FloatingActionButton.extended(
label: Text('sticky buy button'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)),
onPressed: () {
print("YAY");
},
),
),
),
),
);
}
}
class MyObservableWidget extends StatefulWidget {
const MyObservableWidget({Key key}) : super(key: key);
#override
State<StatefulWidget> createState() => new MyObservableWidgetState();
}
class MyObservableWidgetState extends State<MyObservableWidget> {
#override
Widget build(BuildContext context) {
return new RaisedButton(
onPressed: () {
},
color: Colors.lightGreenAccent,
child: Text('This is my buy button', style: TextStyle(color: Colors.blue),),
);
}
}
class ContainerWithBorder extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Container(
decoration: new BoxDecoration(border: new Border.all(), color: Colors.grey),
);
}
}

Related

I'm running into a problem about SilverPersistentHeader obscuring SliverFillRemaining

First of all, my English is not good, sorry!
Here is my description of the problem:
consists of the following components: (contained in CustomScrollView)
1: SilverToBoxAdapter
2: SilverPersistentHeader
3: SliverFillRemaining
The SilverPersistentHeader only appears when the SilverToBoxAdapter disappears from the screen, and the SilverToBoxAdapter is hidden by default. After the SilverPersistentHeader appears, it will have the effect of pined=true as the page slides.
When SilverPersistentHeader appeared, I found that SilverPersistentHeader would block part of SliverFillRemaining.
here is my code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late ScrollController _mainController;
late PageController _pageController;
late bool isScrollable = true;
bool isVisiable = false;
#override
void initState() {
super.initState();
_mainController = ScrollController();
_pageController = PageController();
_pageController.addListener(() {
debugPrint('offset:${_pageController.page}');
if (_pageController.page == 0.0) {
setState(() {
isScrollable = true;
});
} else {
setState(() {
isScrollable = false;
});
}
});
_mainController.addListener(() {
// debugPrint('offset:${_mainController.offset}');
if (_mainController.offset >= 200) {
setState(() {
isVisiable = true;
});
} else {
setState(() {
isVisiable = false;
});
}
});
}
#override
void dispose() {
super.dispose();
_mainController.dispose();
_pageController.dispose();
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
physics: isScrollable
? const AlwaysScrollableScrollPhysics()
: const NeverScrollableScrollPhysics(),
controller: _mainController,
slivers: <Widget>[
const SliverAppBar(
title: Text('SliverDemo'),
pinned: true,
),
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.amber,
),
),
SliverPersistentHeader(
delegate: MyPersistentHeader(
isVisiable: isVisiable,
child: Container(
color: Colors.green,
)),
pinned: true,
),
SliverFillRemaining(
hasScrollBody: true,
child: PageView(
controller: _pageController,
children: [
Container(
color: Colors.primaries[0],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[1],
child: Column(
children: [
Container(
color: Colors.black,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[2],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[3],
child: Column(
children: [
Container(
color: Colors.black,
height: 100,
)
],
),
),
Container(
color: Colors.primaries[4],
child: Column(
children: [
Container(
color: Colors.white,
height: 100,
)
],
),
)
],
),
),
],
);
}
}
class MyPersistentHeader extends SliverPersistentHeaderDelegate {
final Widget child;
final bool isVisiable;
MyPersistentHeader({required this.child, required this.isVisiable});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
#override
double get maxExtent => isVisiable ? 60 : 0;
#override
double get minExtent => isVisiable ? 60 : 0;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
I've searched online for a long time and can't find a solution. I hope you can give me a good suggestion, thanks.

Flutter AnimatedList and AnimatedController

One of them should open and close. But when pressed, animatedcontainers all open. How can I fix?
I placed the AnimatedContainer in AnimatedList. All of them don't look nice when opened.
The list slips when they all open.
I set the height and width in two setstate. But I cannot adjust this situation.
It should be very simple but I couldn't see it.
I think it will be useful if edited.
Thank you. For read.
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double _width = 0;
double _height = 0;
int _a=0;
final GlobalKey<AnimatedListState> _key =GlobalKey();
double _state(){
setState(() {
_width=25;
_height=20;
});
}
double _state2(){
setState(() {
_width=0;
_height=0;
});
}
List<String> _controll1=[
"a",
"b",
"c",
"d",
"e",
"f",
"g",
];
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AppBar"),
),
body: AnimatedList(
key: _key,
initialItemCount: _controll1.length,
itemBuilder: (context, index, animation){
return _buildItem(_controll1[index], animation,index);
}
),
);
}
Widget _buildItem(String item, Animation animation, int index){
return SizeTransition(
sizeFactor: animation,
child: Container(
width: 300,
child: Card(
elevation: 2,
child: Column(
children: [
Container(
child: RaisedButton(
color: Colors.black,
child: Text("${index}. button",style: TextStyle(color: Colors.white),),
onPressed: (){
for(;;) {
if (_a == 0) {
_state();
_a = 1;
return;
}
if (_a == 1) {
_state2();
_a = 0;
return;
}
}
},
),
),
AnimatedContainer(duration: Duration(milliseconds: 5),
width: _width,
height: _height,
color: Colors.black,
),
],
)
),
),
);
}
}
From what i understood, you need not use any animation at all.
And what you missed was just that, you were passing same size to all the widgets, so when you click one all would change.
I tried to recreate your use case and achieved it without any animation
class MyApp2 extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp2> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> _controll1 = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
];
List<Size> _size;
#override
void initState() {
_size = [
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
Size(0, 0),
];
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("AppBar"),
),
body: ListView.builder(
itemCount: _controll1.length,
itemBuilder: (context, index) {
return _buildItem(_controll1[index], index);
}),
);
}
Widget _buildItem(String item, int index) {
return Container(
width: 300,
child: Card(
elevation: 2,
child: Column(
children: [
Container(
child: RaisedButton(
color: Colors.black,
child: Text(
"${index}. button",
style: TextStyle(color: Colors.white),
),
onPressed: () {
if (_size[index] == Size(0, 0)) {
setState(() {
_size[index] = Size(25, 30);
});
} else if (_size[index] == Size(25, 30)) {
setState(() {
_size[index] = Size(0, 0);
});
}
},
),
),
Container(
width: _size[index].width,
height: _size[index].height,
color: Colors.black,
),
],
)),
);
}
}
I wanted to do this. Your code has been very useful Nitesh. Thank you. I did.
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double _width = 0;
double _height = 0;
int _a = 0;
var duration = Duration(seconds: 1);
final GlobalKey<AnimatedListState> _key = GlobalKey();
int _c = 0;
int _b = 0;
List<String> _controll1 = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
];
#override
void initState() {
// TODO: implement initState
_a = 0;
_b = 0;
_width = 0;
_height = 0;
_c = 1;
duration = Duration(seconds: 1);
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(
appBar: AppBar(
title: Text("AppBar"),
),
body: AnimatedList(
controller: ScrollController(initialScrollOffset: 5),
scrollDirection: Axis.vertical,
key: _key,
initialItemCount: _controll1.length,
itemBuilder: (context, index, animation) {
return _buildItem(_controll1[index], animation, index);
}),
),
);
}
Widget _buildItem(String item, Animation animation, int index) {
return SizeTransition(
sizeFactor: animation,
child: Center(
child: Column(
children: [
Center(
child: RaisedButton(
color: Colors.black,
onPressed: () {
print(index);
if(_a==0){
setState(() {
duration=Duration(seconds: 2);
_b=index;
_width=80.0;
_height=30.0;
_a=1;
_c=index;
});
}else if(_a==1&&_c==index){
setState(() {
Duration(seconds: 2);
_b=index;
_width=0;
_height=0;
_a=0;
});
}else if(_a==1&&_c!=index){
setState(() {
Duration(seconds: 2);
_b=index;
_width=80;
_height=30;
_a=1;
_c=index;
});
}
},
child: Text(
"${index}. button",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedContainer(
duration: duration,
width: _b!=index?0:_width,
height: _b!=index?0:_height,
curve: Curves.fastOutSlowIn,
decoration: BoxDecoration(
color: Colors.black,
),
child: RaisedButton(
child: Text("Delete"),
onPressed: (){
setState(() {
_removeItem(index);
_b=index;
duration=Duration(seconds: 0);
_width=0;
_height=0;
_a=0;
});
},
),
),
],
),
),
);
}
void _removeItem(int i) {
String removeItem = _controll1.removeAt(i);
AnimatedListRemovedItemBuilder builder = (context, animation) {
return _buildItem(removeItem, animation, i);
};
_key.currentState.removeItem(i, builder);
}
}

how Can i make this Single selection Flutter?

I have an Apps which is having a listview with the reaction button in a flutter . I want to make this when a user clicked any of this love icon then it's filled with red color.
enter image description here
enter image description here
Like this image but the problem is when I clicked one of this love icon all of the icons turned into red color but I only want to change the color of love of icon which one is Selected.
import 'package:flutter/material.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: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool like;
#override
List<String> user = ['Dipto', 'Dipankar', "Sajib", 'Shanto', 'Pranto'];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ListView Demu'),
),
body: Center(
child: Container(
child: ListView.builder(
itemCount: user.length,
itemBuilder: (context, index) {
return Container(
padding: EdgeInsets.all(10),
height: 50,
width: MediaQuery.of(context).size.width * 0.8,
color: Colors.yellowAccent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
user[index],
),
Positioned(
child: IconButton(
icon: _iconControl(like),
onPressed: () {
if (like == false) {
setState(() {
like = true;
_iconControl(like);
});
} else {
setState(() {
like = false;
_iconControl(like);
});
}
},
),
),
],
),
);
},
),
)),
);
}
_iconControl(bool like) {
if (like == false) {
return Icon(Icons.favorite_border);
} else {
return Icon(
Icons.favorite,
color: Colors.red,
);
}
}
}
I also try with using parameter but Its failed Like that :
child: IconButton(
icon: _iconControl(true),
onPressed: () {
if (false) {
setState(() {
_iconControl(true);
});
} else {
setState(() {
_iconControl(false);
});
}
},
),
Can you help me Please. Thanks in advance
You can create a modal class to manage the selection of your list
Just create a modal class and add a boolean variable to maintaining selection using. that boolean variable
SAMPLE CODE
import 'package:flutter/material.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: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool like;
List<Modal> userList = List<Modal>();
#override
void initState() {
userList.add(Modal(name: 'Dipto', isSelected: false));
userList.add(Modal(name: 'Dipankar', isSelected: false));
userList.add(Modal(name: 'Sajib', isSelected: false));
userList.add(Modal(name: 'Shanto', isSelected: false));
userList.add(Modal(name: 'Pranto', isSelected: false));
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ListView Demu'),
),
body: Center(
child: Container(
child: ListView.builder(
itemCount: userList.length,
itemBuilder: (context, index) {
return Container(
padding: EdgeInsets.all(10),
height: 50,
width: MediaQuery
.of(context)
.size
.width * 0.8,
color: Colors.yellowAccent,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
userList[index].name,
),
Positioned(
child: IconButton(
icon: _iconControl( userList[index].isSelected),
onPressed: () {
setState(() {
userList.forEach((element) {
element.isSelected = false;
});
userList[index].isSelected = true;
});
},
),
),
],
),
);
},
),
)),
);
}
_iconControl(bool like) {
if (like == false) {
return Icon(Icons.favorite_border);
} else {
return Icon(
Icons.favorite,
color: Colors.red,
);
}
}
}
class Modal {
String name;
bool isSelected;
Modal({this.name, this.isSelected = false});
}

How to update Draggable child when entering DragTarget in Flutter?

I have a number of Draggables and DragTargets. On the Draggables I have specified child and feedback, however I also want it to change it's look when the Draggable enters the DragTarget. I can't see any way to do this.
Whenever I drag the Draggable around, I change it's color to be red, however as soon as it enters the DragTarget I want to update the Draggable color to green.
I am aware of DragTarget.OnWillAccept, this method is called whenever the Draggable enters the DragTarget, but I only have the data. I tried updating the data with new color and then calling setState, but that seemed not to work.
Any suggestions on how to get this behaviour?
I want something like the following callback Draggable.onEnteringDragTarget and Draggable.onLeavingDragTarget.
The only way I can think of is using a streambuilder and a stream that will carry the information of whether the draggable is on a drag target. This code provides a basic solution.
class _MyHomePageState extends State<MyHomePage> {
BehaviorSubject<bool> willAcceptStream;
#override
void initState() {
willAcceptStream = new BehaviorSubject<bool>();
willAcceptStream.add(false);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
Container(
height: 400,
width: double.infinity,
child: Container(
width: 100,
height: 100,
child: Center(
child: Draggable(
feedback: StreamBuilder(
initialData: false,
stream: willAcceptStream,
builder: (context, snapshot) {
return Container(
height: 100,
width: 100,
color: snapshot.data ? Colors.green : Colors.red,
);
},
),
childWhenDragging: Container(),
child: Container(
height: 100,
width: 100,
color: this.willAcceptStream.value ?? false
? Colors.green
: Colors.blue,
),
onDraggableCanceled: (v, f) => setState(
() {
this.willAcceptStream.add(false);
},
),
),
),
),
),
DragTarget(
builder: (context, list, list2) {
return Container(
height: 50,
width: double.infinity,
color: Colors.blueGrey,
child: Center(child: Text('TARGET ZONE'),),
);
},
onWillAccept: (item) {
debugPrint('will accept');
this.willAcceptStream.add(true);
return true;
},
onLeave: (item) {
debugPrint('left the target');
this.willAcceptStream.add(false);
},
),
],
),
);
}
}
Edit: The first example doesn't handle multiple drags at the same time this on does and it's a little cleaner.
import 'package:rxdart/rxdart.dart';
import 'package:flutter/material.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: 'Draggable Test',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
MyDraggableController<String> draggableController;
#override
void initState() {
this.draggableController = new MyDraggableController<String>();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Draggable Test'),
),
body: Column(
children: <Widget>[
Container(
height: 400,
width: double.infinity,
child: Container(
width: 100,
height: 100,
child: Center(
child: Stack(
children: <Widget>[
Positioned(
left: 30,
top: 30,
child: MyDraggable<String>(draggableController, 'Test1'),
),
Positioned(
left: 230,
top: 230,
child: MyDraggable<String>(draggableController, 'Test2'),
)
],
),
),
),
),
DragTarget<String>(
builder: (context, list, list2) {
return Container(
height: 50,
width: double.infinity,
color: Colors.blueGrey,
child: Center(
child: Text('TARGET ZONE'),
),
);
},
onWillAccept: (item) {
debugPrint('draggable is on the target');
this.draggableController.onTarget(true, item);
return true;
},
onLeave: (item) {
debugPrint('draggable has left the target');
this.draggableController.onTarget(false, item);
},
),
],
),
);
}
}
class MyDraggable<T> extends StatefulWidget {
final MyDraggableController<T> controller;
final T data;
MyDraggable(this.controller, this.data);
#override
_MyDraggableState createState() =>
_MyDraggableState<T>(this.controller, this.data);
}
class _MyDraggableState<T> extends State<MyDraggable> {
BehaviorSubject<DraggableInfo<T>> willAcceptStream;
MyDraggableController<T> controller;
T data;
_MyDraggableState(this.controller, this.data);
#override
void initState() {
willAcceptStream = this.controller._isOnTarget;
willAcceptStream.add(new DraggableInfo<T>(false, this.data));
super.initState();
}
#override
Widget build(BuildContext context) {
return Draggable<T>(
data: this.data,
feedback: StreamBuilder<DraggableInfo<T>>(
initialData: DraggableInfo<T>(false, this.data),
stream: willAcceptStream,
builder: (context, snapshot) {
return Container(
height: 100,
width: 100,
color: snapshot.data.isOnTarget && snapshot.data.data == this.data ? Colors.green : Colors.red,
);
},
),
childWhenDragging: Container(),
child: Container(
height: 100,
width: 100,
color: (this.willAcceptStream.value.isOnTarget ?? this.willAcceptStream.value.data == this.data)
? Colors.green
: Colors.blue,
),
onDraggableCanceled: (v, f) => setState(
() {
this.willAcceptStream.add(DraggableInfo(false, null));
},
),
);
}
}
class DraggableInfo<T> {
bool isOnTarget;
T data;
DraggableInfo(this.isOnTarget, this.data);
}
class MyDraggableController<T> {
BehaviorSubject<DraggableInfo<T>> _isOnTarget;
MyDraggableController() {
this._isOnTarget = new BehaviorSubject<DraggableInfo<T>>();
}
void onTarget(bool onTarget, T data) {
_isOnTarget.add(new DraggableInfo(onTarget, data));
}
}
Edit 2: Solution without using streams and streambuilder. The important part is the feedback widget is stateful.
import 'package:flutter/material.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: 'Draggable Test',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
MyDraggableController<String> draggableController;
#override
void initState() {
this.draggableController = new MyDraggableController<String>();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Draggable Test'),
),
body: Column(
children: <Widget>[
Container(
height: 400,
width: double.infinity,
child: Container(
width: 100,
height: 100,
child: Center(
child: Stack(
children: <Widget>[
Positioned(
left: 30,
top: 30,
child: MyDraggable<String>(
draggableController,
'Test1',
),
),
Positioned(
left: 230,
top: 230,
child: MyDraggable<String>(
draggableController,
'Test2',
),
),
],
),
),
),
),
DragTarget<String>(
builder: (context, list, list2) {
return Container(
height: 100,
width: double.infinity,
color: Colors.blueGrey,
child: Center(
child: Text('TARGET ZONE'),
),
);
},
onWillAccept: (item) {
debugPrint('draggable is on the target $item');
this.draggableController.onTarget(true, item);
return true;
},
onLeave: (item) {
debugPrint('draggable has left the target $item');
this.draggableController.onTarget(false, item);
},
),
],
),
);
}
}
class MyDraggable<T> extends StatefulWidget {
final MyDraggableController<T> controller;
final T data;
MyDraggable(this.controller, this.data, {Key key}) : super(key: key);
#override
_MyDraggableState createState() =>
_MyDraggableState<T>(this.controller, this.data);
}
class _MyDraggableState<T> extends State<MyDraggable> {
MyDraggableController<T> controller;
T data;
bool isOnTarget;
_MyDraggableState(this.controller, this.data);
FeedbackController feedbackController;
#override
void initState() {
feedbackController = new FeedbackController();
this.controller.subscribeToOnTargetCallback(onTargetCallbackHandler);
super.initState();
}
void onTargetCallbackHandler(bool t, T data) {
this.isOnTarget = t && data == this.data;
this.feedbackController.updateFeedback(this.isOnTarget);
}
#override
void dispose() {
this.controller.unSubscribeFromOnTargetCallback(onTargetCallbackHandler);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Draggable<T>(
data: this.data,
feedback: FeedbackWidget(feedbackController),
childWhenDragging: Container(
height: 100,
width: 100,
color: Colors.blue[50],
),
child: Container(
height: 100,
width: 100,
color: (this.isOnTarget ?? false) ? Colors.green : Colors.blue,
),
onDraggableCanceled: (v, f) => setState(
() {
this.isOnTarget = false;
this.feedbackController.updateFeedback(this.isOnTarget);
},
),
);
}
}
class FeedbackController {
Function(bool) feedbackNeedsUpdateCallback;
void updateFeedback(bool isOnTarget) {
if (feedbackNeedsUpdateCallback != null) {
feedbackNeedsUpdateCallback(isOnTarget);
}
}
}
class FeedbackWidget extends StatefulWidget {
final FeedbackController controller;
FeedbackWidget(this.controller);
#override
_FeedbackWidgetState createState() => _FeedbackWidgetState();
}
class _FeedbackWidgetState extends State<FeedbackWidget> {
bool isOnTarget;
#override
void initState() {
this.isOnTarget = false;
this.widget.controller.feedbackNeedsUpdateCallback = feedbackNeedsUpdateCallbackHandler;
super.initState();
}
void feedbackNeedsUpdateCallbackHandler(bool t) {
setState(() {
this.isOnTarget = t;
});
}
#override
Widget build(BuildContext context) {
return Container(
height: 100,
width: 100,
color: this.isOnTarget ?? false ? Colors.green : Colors.red,
);
}
#override
void dispose() {
this.widget.controller.feedbackNeedsUpdateCallback = null;
super.dispose();
}
}
class DraggableInfo<T> {
bool isOnTarget;
T data;
DraggableInfo(this.isOnTarget, this.data);
}
class MyDraggableController<T> {
List<Function(bool, T)> _targetUpdateCallbacks = new List<Function(bool, T)>();
MyDraggableController();
void onTarget(bool onTarget, T data) {
if (_targetUpdateCallbacks != null) {
_targetUpdateCallbacks.forEach((f) => f(onTarget, data));
}
}
void subscribeToOnTargetCallback(Function(bool, T) f) {
_targetUpdateCallbacks.add(f);
}
void unSubscribeFromOnTargetCallback(Function(bool, T) f) {
_targetUpdateCallbacks.remove(f);
}
}

How to access list item when built with builder?

In the following Flutter app, I'm trying to show a LinearProgressIndicator in each card only when that card is counting. The the correct progression is printed to the console, but I can't figure out how to access "stepProgress" variable from the LinearProgressIndicator widget to update the view.
The cards are being built with a builder because they will change based on the input List (Array) of Maps (Objects).
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
final key = new GlobalKey<_MyHomePageState>();
List<Widget> cards = [];
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Title',
theme: ThemeData(
primarySwatch: Colors.blue,
canvasColor: Colors.grey[350],
),
home: MyHomePage(title: 'Title', key: key),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List _sequence = [];
double stepProgress = 0.0;
#override
initState() {
super.initState();
setState(() => _sequence = [
{'iterations': 1, 'time': 10},
{'iterations': 3, 'time': 7},
{'iterations': 2, 'time': 5},
]);
setState(() {
cards = getRun();
});
_countdown(_sequence, null);
}
getRun() {
List<Widget> runCards = [];
for (var group in _sequence) {
runCards.add(_buildCard(CardModel(
iterationsInGroup: group['iterations'],
timeEach: group['time'],
)));
}
return runCards;
}
void _countdown(seq, iters) async {
if (seq.length > 0) {
int i = iters == null ? seq[0]['iterations'] : iters;
if (i > 0) {
int duration = seq[0]["time"];
Timer.periodic(Duration(seconds: 1), (timer) {
if (timer.tick < duration) {
setState(() {
stepProgress = timer.tick / duration;
});
print('Iteration $i: ${timer.tick} / $duration = $stepProgress');
} else {
print('Finished iteration $i');
timer.cancel();
i = i - 1;
if (i > 0) {
_countdown(seq, i); // Next iteration
} else {
print('Finished group ${seq.length}');
timer.cancel();
if (seq.length > 1) {
_countdown(seq.sublist(1), null); // Next group
} else {
print('Done');
}
}
}
});
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
'Header',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: ListView(
children: cards,
padding: const EdgeInsets.all(8.0),
),
),
],
),
);
}
}
Widget _buildCard(CardModel card) {
List<Widget> columnData = <Widget>[];
columnData.add(
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
card.timeEach.toString() +
' seconds ' +
card.iterationsInGroup.toString() +
' times',
style: TextStyle(fontSize: 22.0),
),
),
true //key.currentState.activeStep == card.cardStep //TODO: This doesn't work
? LinearProgressIndicator(
value: key.currentState.stepProgress,
)
: Container(width: 0.0, height: 0.0),
],
),
);
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0),
child: Column(children: columnData),
),
);
}
class CardModel {
final int iterationsInGroup;
final int timeEach;
CardModel({
this.iterationsInGroup,
this.timeEach,
});
}
I modified your code a little, this is a little messy therefore I recommend you the following:
Create a StatefulWidget for your ChildView (Like my code below).
Keep the progress logic into the ChildView, it would be easy to change the status in that way.
I had to keep track the Globalkey in order to refresh the changes into the child view, but if you handle the logic into each child, you don't need the GlobalKey.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
final key = new GlobalKey<_MyHomePageState>();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Title',
theme: ThemeData(
primarySwatch: Colors.blue,
canvasColor: Colors.grey[350],
),
home: MyHomePage(title: 'Title', key: key),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List _sequence = [];
List<ChildView> runCards = [];
#override
initState() {
super.initState();
setState(() => _sequence = [
{'iterations': 1, 'time': 10, 'progress': 0.0},
{'iterations': 3, 'time': 7, 'progress': 0.0},
{'iterations': 2, 'time': 5, 'progress': 0.0},
]);
getRun();
_countdown(_sequence, null);
}
getRun() {
for (var group in _sequence) {
var cardModel = CardModel(
iterationsInGroup: group['iterations'],
timeEach: group['time'],
progress: group['progress'],
);
runCards.add(new ChildView(cardModel,new GlobalKey<_ChildViewState>()));
}
setState(() {
});
return runCards;
}
void _countdown(seq, iters) async {
if (seq.length > 0) {
int i = iters == null ? seq[0]['iterations'] : iters;
if (i > 0) {
int duration = seq[0]["time"];
Timer.periodic(Duration(seconds: 1), (timer) {
if (timer.tick <= duration) {
var childView = runCards[i-1];
double stepProgress = 0.0;
stepProgress = timer.tick / duration;
childView.key.currentState.updateProgress(stepProgress);
print('Iteration $i: ${timer.tick} / $duration = $stepProgress');
} else {
print('Finished iteration $i');
timer.cancel();
i = i - 1;
if (i > 0) {
_countdown(seq, i); // Next iteration
} else {
print('Finished group ${seq.length}');
timer.cancel();
if (seq.length > 1) {
_countdown(seq.sublist(1), null); // Next group
} else {
print('Done');
}
}
}
});
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
'Header',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: ListView(
children: runCards,
padding: const EdgeInsets.all(8.0),
),
),
],
),
);
}
}
class CardModel {
final int iterationsInGroup;
final int timeEach;
double progress;
CardModel({
this.iterationsInGroup,
this.timeEach,
this.progress,
});
}
class ChildView extends StatefulWidget {
final CardModel card;
final GlobalKey<_ChildViewState> key;
ChildView(this.card, this.key) : super(key: key);
#override
_ChildViewState createState() => _ChildViewState();
}
class _ChildViewState extends State<ChildView> {
void updateProgress(double progress){
setState(() {
widget.card.progress = progress;
});
}
#override
Widget build(BuildContext context) {
List<Widget> columnData = <Widget>[];
columnData.add(
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
widget.card.timeEach.toString() +
' seconds ' +
widget.card.iterationsInGroup.toString() +
' times',
style: TextStyle(fontSize: 22.0),
),
),
widget.card.progress < 1 //key.currentState.activeStep == card.cardStep //TODO: This doesn't work
? LinearProgressIndicator(
value: widget.card.progress,
)
: Container( child: new Text("Completed"),),
],
),
);
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0),
child: Column(children: columnData),
),
);
}
}