Synchronizing ListWheelScrollView in Flutter - flutter

When I scroll through one List Wheel Scroll View, the other list either lags or does not scroll smoothly.
https://pub.dev/packages/linked_scroll_controller allows to sync lists but does not support FixedExtendScrollPhysics.
Output : -
https://pub.dev/packages/linked_scroll_controller works perfectly if we are using ScrollPhysics but throws an error when used with a widget that uses FixedExtendScrollPhysics. I want both the list to move Synchronizing that is if I move green list I want red list to move simultaneously and vice versa
Code :
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const List(),
);
}
}
class List extends StatefulWidget {
const List({Key? key}) : super(key: key);
#override
_ListState createState() => _ListState();
}
class _ListState extends State<List> {
final scrollController = FixedExtentScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("List"),
backgroundColor: Colors.green,
),
body: Row(
children: [
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
itemExtent: 100,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (value) {
setState(() {
scrollController.animateToItem(value,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut);
});
},
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.green,
height: 50,
width: 50,
)
]
]),
),
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
controller: scrollController,
physics: const FixedExtentScrollPhysics(),
itemExtent: 100,
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.red,
height: 50,
width: 50,
)
]
]),
)
],
));
}
}

Really interesting question. The problem was syncing both the scrollviews. I made few changes to your code to achieve the desired result.
The basic idea is to remove listener to the other scroll before forcing pixels. After the scroll, add the same listener. But because it happens instantaneously and actual scroll happens sometimes in future, they don't overlap perfectly.
So I had to introduce CancelableCompleter from the async library to make sure add operation does not happen if another scroll event had happened.
With forcePixels, the scrolling to other wheel is not deferred hence CancelableCompleter is not required.
// ignore_for_file: invalid_use_of_protected_member
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const List(),
);
}
}
class List extends StatefulWidget {
const List({Key? key}) : super(key: key);
#override
_ListState createState() => _ListState();
}
class _ListState extends State<List> {
final _firstScrollController = FixedExtentScrollController();
final _secondScrollController = FixedExtentScrollController();
#override
void initState() {
super.initState();
_firstScrollController.addListener(_firstScrollListener);
_secondScrollController.addListener(_secondScrollListener);
}
#override
void dispose() {
_firstScrollController
..removeListener(_firstScrollListener)
..dispose();
_secondScrollController
..removeListener(_secondScrollListener)
..dispose();
super.dispose();
}
void _firstScrollListener() {
_secondScrollController.removeListener(_secondScrollListener);
_secondScrollController.position.forcePixels(_firstScrollController.offset);
_secondScrollController.addListener(_secondScrollListener);
}
void _secondScrollListener() {
_firstScrollController.removeListener(_firstScrollListener);
_firstScrollController.position.forcePixels(_secondScrollController.offset);
_firstScrollController.addListener(_firstScrollListener);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("List"),
backgroundColor: Colors.green,
),
body: Row(
children: [
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
itemExtent: 100,
controller: _firstScrollController,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: (value) {
print('first wheel : item selected: $value');
},
children: [
for (int i = 0; i < 25; i++) ...[
Container(
color: Colors.green,
height: 50,
width: 50,
)
]
]),
),
SizedBox(
height: 600,
width: 300,
child: ListWheelScrollView(
controller: _secondScrollController,
physics: const FixedExtentScrollPhysics(),
itemExtent: 100,
onSelectedItemChanged: (value) {
print('second wheel : item selected: $value');
},
children: [
for (int i = 0; i < 25; i++) ...[
Container(
color: Colors.red,
height: 50,
width: 50,
)
]
]),
)
],
));
}
}
I am using protective member function forcePixels as Flutter has not provided any way to set pixels without animation without creating subclass of ScrollPosition. If you are fine with this linter warning, it is all good. If not, we will have to extend ListWheelScrollView to use ScrollPosition where we could make changes as per need.

Try separate two controller and add listener like this:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'List',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const List(),
);
}
}
class List extends StatefulWidget {
const List({Key? key}) : super(key: key);
#override
_ListState createState() => _ListState();
}
class _ListState extends State<List> {
final scrollController1 = FixedExtentScrollController();
final scrollController2 = FixedExtentScrollController();
#override
void initState() {
super.initState();
scrollController1.addListener(() {
if (scrollController1.position.hasPixels) {
scrollController2.animateTo(
scrollController1.offset,
duration: const Duration(milliseconds: 10), //adjust delay you need
curve: Curves.easeInOut,
);
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("List"),
backgroundColor: Colors.green,
),
body: Row(
children: [
SizedBox(
height: 600,
width: 150,
child: ListWheelScrollView(
itemExtent: 100,
controller: scrollController1,
physics: const FixedExtentScrollPhysics(),
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.green,
height: 50,
width: 50,
)
]
]),
),
SizedBox(
height: 600,
width: 150,
child: ListWheelScrollView(
controller: scrollController2,
physics: const FixedExtentScrollPhysics(),
itemExtent: 100,
children: [
for (int i = 0; i < 5; i++) ...[
Container(
color: Colors.red,
height: 50,
width: 50,
)
]
]),
)
],
));
}
}

import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
MyWidget({Key? key}) : super(key: key);
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late ScrollController _controllerA;
late ScrollController _controllerB;
/// FixedExtentScrollController
/// late FixedExtentScrollController _controllerA;
/// late FixedExtentScrollController _controllerB;
#override
void initState() {
super.initState();
_controllerA = ScrollController();
_controllerB = ScrollController();
/// _controllerA = FixedExtentScrollController();
/// _controllerB = FixedExtentScrollController();
_controllerA.addListener(() {
if (_controllerA.position.hasPixels) {
_controllerB.jumpTo(_controllerA.offset);
}
});
/// if you neet bind _controllerB to _controllerA
/// _controllerB.addListener(() {
/// if (_controllerB.position.hasPixels) {
/// _controllerA.jumpTo(_controllerB.offset);
/// }
///});
}
#override
void dispose() {
_controllerA.dispose();
_controllerB.dispose();
super.dispose();
}
Widget _listView(Color color, ScrollController controller) {
var width = MediaQuery.of(context).size.width / 2;
return Container(
width: width,
child: ListView.builder(
controller: controller,
shrinkWrap: true,
itemCount: 100,
itemExtent: 50,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: color, border: Border.all(color: Colors.white)),
);
}),
);
}
/// ListWheelScrollView
/// Widget _listView(Color color, FixedExtentScrollController controller) {
/// var width = MediaQuery.of(context).size.width / 2;
/// return Container(
/// width: width,
/// child: ListWheelScrollView(
/// physics: FixedExtentScrollPhysics(),
/// controller: controller,
/// itemExtent: 50,
/// children: [
/// ...List.generate(
/// 100,
/// (index) => Container(
/// decoration: BoxDecoration(
/// color: color,
/// border: Border.all(color: Colors.white)),
/// ))
/// ]),
/// );
/// }
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Row(
children: [
_listView(Colors.red, _controllerA),
_listView(Colors.yellow, _controllerB),
],
)),
);
}
}

Related

Flutter scrollbar that is in horizontal scrollview doesnt show correctly inner scrollview

So, I have bigger scroll view that scrolls horizontally, and inside - little box (red color) and smaller scrollview (orange color) that scrolls vertically.
There are two scrollbars on the bigger scrollview (1 - for horizontal), and second - for vertical inner.
And the problem - vertical scrollbar doesnt look right, because it can go only like blue arrow shows, and I want it to have either full height of the bigger scrollview, or be right near scrollable vertical part, but then dont hide itself if scrolled in horizontal direction.
Run on dartPad
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: ScrollSizingWidget(),
),
),
);
}
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({
Key? key,
}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
final ScrollController _horizontal = ScrollController();
final ScrollController _vertical = ScrollController();
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scrollbar(
controller: _vertical,
notificationPredicate: (notification) => notification.depth == 1,
child: Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
child: SizedBox(
height: 500,
width: 1000,
child: Column(
children:[
Container(width: 1000, height: 200, color: Colors.green),
Flexible(
child: SingleChildScrollView(
controller: _vertical,
child: Container(
height: 700,
width: 1000,
color: Colors.yellow,
)
)
),
]
)
),
),
),
);
}
}
I have used your code to reproduce the issue. If I understood your needs right, here is the fix:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: SafeArea(
child: ScrollSizingWidget(),
),
),
),
);
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({Key? key}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
late final ScrollController _horizontal;
late final ScrollController _vertical;
#override
void initState() {
super.initState();
_horizontal = ScrollController();
_vertical = ScrollController();
}
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
child: SizedBox(
height: 500,
width: 1000,
child: Scrollbar(
controller: _vertical,
scrollbarOrientation: ScrollbarOrientation.right,
child: SingleChildScrollView(
controller: _vertical,
scrollDirection: Axis.vertical,
child: Column(
children: [
Container(
height: 200,
width: 1000,
color: Colors.red,
),
Container(
height: 1000,
width: 1000,
color: Colors.orange,
),
],
),
),
),
),
),
);
}
Firstly, you say that your main SingleChildScrollView scrolls horizontally, but your widget tree starts with a Scrollbar which uses a vertical ScrollController. So you should create your widgets step by step, as you explained.
Also, since you want to see the vertical Scrollbar through your main SingleChildScrollView, I wrapped both(red and orange Containers) with Scrollbar and SingleChildScrollView to have the effect you want. Furthermore, I connected these Scrollbar and SingleChildScrollView with the same horizontal ScrollController. So now, not only the orange Container, but both are scrollable and stick together, not independent.
If you don't want the red Container being scrolled along with the orange Container, check this:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: SafeArea(
child: ScrollSizingWidget(),
),
),
),
);
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({Key? key}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
late final ScrollController _horizontal;
late final ScrollController _vertical;
#override
void initState() {
super.initState();
_horizontal = ScrollController();
_vertical = ScrollController();
}
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
child: SizedBox(
height: 500,
width: 1000,
child: Column(
children: [
Container(
height: 200,
width: 1000,
color: Colors.red,
),
Expanded(
child: Scrollbar(
controller: _vertical,
scrollbarOrientation: ScrollbarOrientation.right,
child: SingleChildScrollView(
controller: _vertical,
scrollDirection: Axis.vertical,
child: Container(
height: 700,
width: 1000,
color: Colors.orange,
),
),
),
),
],
),
),
),
);
}
Lastly, Scrollbar's position in iOS is a bit buggy because of the notch, etc. So I wrapped your ScrollSizingWidget with SafeArea to fix the issue in iOS.
If these answers are not what you expect, please don't hesitate to write.
Edit: After your explanations in the comments below, I have created another fix. I believe CustomScrollView and Sliver widgets are fits here perfectly. The red Container, which you want to stay in its position, should be wrapped with the SliverAppBar. Lastly, the orange Container, which you want to be able to scroll vertically, could be wrapped with SliverFixedExtentList. Please check the code below:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: SafeArea(
child: ScrollSizingWidget(),
),
),
),
);
}
class ScrollSizingWidget extends StatefulWidget {
const ScrollSizingWidget({Key? key}) : super(key: key);
#override
State<ScrollSizingWidget> createState() => _ScrollSizingWidgetState();
}
class _ScrollSizingWidgetState extends State<ScrollSizingWidget> {
late final ScrollController _horizontal;
late final ScrollController _vertical;
#override
void initState() {
super.initState();
_horizontal = ScrollController();
_vertical = ScrollController();
}
#override
void dispose() {
_horizontal.dispose();
_vertical.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Scrollbar(
controller: _horizontal,
scrollbarOrientation: ScrollbarOrientation.bottom,
child: SingleChildScrollView(
controller: _horizontal,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
child: SizedBox(
height: 500,
width: 1000,
child: Scrollbar(
controller: _vertical,
scrollbarOrientation: ScrollbarOrientation.right,
child: CustomScrollView(
controller: _vertical,
scrollDirection: Axis.vertical,
slivers: [
SliverAppBar(
toolbarHeight: 200.0,
collapsedHeight: 200.0,
pinned: true,
stretch: true,
elevation: 0.0,
backgroundColor: Colors.transparent,
title: Container(
height: 200.0,
color: Colors.red,
),
titleSpacing: 0,
),
SliverFixedExtentList(
itemExtent: 1200.0,
delegate: SliverChildBuilderDelegate(
(_, __) => Container(
color: Colors.orange,
),
childCount: 1,
),
),
],
),
),
),
),
);
}

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 hero animation between widgets not screens

Hero animation is the best for navigating between screen, but I need same animation between widgets. Like one card moving another place for example: Product Card moves to shoppingcart and something else. Thanks for answers!
Try this one, add_to_cart_animation:
import 'package:add_to_cart_animation/add_to_cart_animation.dart';
import 'package:add_to_cart_animation/add_to_cart_icon.dart';
import 'package:flutter/material.dart';
import 'list_item.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: 'Add To Cart Animation',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Add To Cart Animation'),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// We can detech the location of the card by this GlobalKey<CartIconKey>
GlobalKey<CartIconKey> gkCart = GlobalKey<CartIconKey>();
late Function(GlobalKey) runAddToCardAnimation;
var _cartQuantityItems = 0;
#override
Widget build(BuildContext context) {
return AddToCartAnimation(
// To send the library the location of the Cart icon
gkCart: gkCart,
rotation: true,
dragToCardCurve: Curves.easeIn,
dragToCardDuration: const Duration(milliseconds: 1000),
previewCurve: Curves.linearToEaseOut,
previewDuration: const Duration(milliseconds: 500),
previewHeight: 30,
previewWidth: 30,
opacity: 0.85,
initiaJump: false,
receiveCreateAddToCardAnimationMethod: (addToCardAnimationMethod) {
// You can run the animation by addToCardAnimationMethod, just pass trough the the global key of the image as parameter
this.runAddToCardAnimation = addToCardAnimationMethod;
},
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: false,
actions: [
// Improvement/Suggestion 4.4 -> Adding 'clear-cart-button'
IconButton(
icon: Icon(Icons.cleaning_services),
onPressed: () {
_cartQuantityItems = 0;
gkCart.currentState!.runClearCartAnimation();
},
),
SizedBox(width: 16),
AddToCartIcon(
key: gkCart,
icon: Icon(Icons.shopping_cart),
colorBadge: Colors.red,
),
SizedBox(
width: 16,
)
],
),
body: ListView(
children: [
AppListItem(onClick: listClick, index: 1),
AppListItem(onClick: listClick, index: 2),
AppListItem(onClick: listClick, index: 3),
AppListItem(onClick: listClick, index: 4),
AppListItem(onClick: listClick, index: 5),
AppListItem(onClick: listClick, index: 6),
AppListItem(onClick: listClick, index: 7),
],
),
),
);
}
// Improvement/Suggestion 4.4 -> Running AddTOCartAnimation BEFORE runCArtAnimation
void listClick(GlobalKey gkImageContainer) async {
await runAddToCardAnimation(gkImageContainer);
await gkCart.currentState!.runCartAnimation((++_cartQuantityItems).toString());
}
}
OR
[not null safety]
this is a sample of add to cart, add_cart_parabola:
import 'dart:ui';
import 'package:add_cart_parabola/add_cart_parabola.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#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> {
int _counter = 0;
GlobalKey floatKey = GlobalKey();
GlobalKey rootKey = GlobalKey();
Offset floatOffset ;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){
RenderBox renderBox = floatKey.currentContext.findRenderObject();
floatOffset = renderBox.localToGlobal(Offset.zero);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
key: rootKey,
width: double.infinity,
height: double.infinity,
color: Colors.grey,
child: ListView(
children: List.generate(40, (index){
return generateItem(index);
}).toList(),
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.yellow,
key: floatKey,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
Widget generateItem(int index){
Text text = Text("item $index",style: TextStyle(fontSize:
25),);
Offset temp;
return GestureDetector(
onPanDown: (details){
temp = new Offset(details.globalPosition.dx, details.globalPosition
.dy);
},
onTap: (){
Function callback ;
setState(() {
OverlayEntry entry = OverlayEntry(
builder: (ctx){
return ParabolaAnimateWidget(rootKey,temp,floatOffset,
Icon(Icons.cancel,color: Colors.greenAccent,),callback,);
}
);
callback = (status){
if(status == AnimationStatus.completed){
entry?.remove();
}
};
Overlay.of(rootKey.currentContext).insert(entry);
});
},
child: Container(
color: Colors.orange,
child: text,
),
);
}
}
For animating widget in the same screen you can use AnimatedPositioned widget see the below code
import 'dart:math';
import 'package:flutter/material.dart';
class AnimatedPositionedDemo extends StatefulWidget {
const AnimatedPositionedDemo({Key? key}) : super(key: key);
static String routeName = 'animated_positioned';
#override
_AnimatedPositionedDemoState createState() => _AnimatedPositionedDemoState();
}
class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
late double topPosition;
late double leftPosition;
double generateTopPosition(double top) => Random().nextDouble() * top;
double generateLeftPosition(double left) => Random().nextDouble() * left;
#override
void initState() {
super.initState();
topPosition = generateTopPosition(30);
leftPosition = generateLeftPosition(30);
}
void changePosition(double top, double left) {
setState(() {
topPosition = generateTopPosition(top);
leftPosition = generateLeftPosition(left);
});
}
#override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final appBar = AppBar(title: const Text('AnimatedPositioned'));
final topPadding = MediaQuery.of(context).padding.top;
// AnimatedPositioned animates changes to a widget's position within a Stack
return Scaffold(
appBar: appBar,
body: SizedBox(
height: size.height,
width: size.width,
child: Stack(
children: [
AnimatedPositioned(
top: topPosition,
left: leftPosition,
duration: const Duration(seconds: 1),
child: InkWell(
onTap: () => changePosition(
size.height -
(appBar.preferredSize.height + topPadding + 50),
size.width - 150),
child: Container(
alignment: Alignment.center,
width: 150,
height: 50,
child: Text(
'Click Me',
style: TextStyle(
color:
Theme.of(context).buttonTheme.colorScheme!.onPrimary,
),
),
color: Theme.of(context).primaryColor,
),
),
),
],
),
),
);
}
}
I hope it works for you
For Animated widgets, flutter team has provided a video on youtube here
And you can read all about them on their website here

How to have the drawer push the content instead of going on top of it?

I'm trying to build something similar to the slack app (see screenshot below) where the navigation drawer pushes the screen away instead of going on top.
I've been trying with the Drawer component without success. I've also looked at PageView but it seems that the children need to take 100% of the width.
Does someone have an idea of how to implement it?
EDIT
A similar result can be achieved with a Stack and AnimatedPositioned
class SlidingDrawer extends StatefulWidget {
final Widget drawer;
final Widget child;
final int swipeSensitivity;
final double drawerRatio;
final Color overlayColor;
final double overlayOpacity;
final int animationDuration;
final Curve animationCurve;
SlidingDrawer({
Key key,
#required this.drawer,
#required this.child,
this.swipeSensitivity = 25,
this.drawerRatio = 0.8,
this.overlayColor = Colors.black,
this.overlayOpacity = 0.5,
this.animationDuration = 500,
this.animationCurve = Curves.ease,
}) : super(key: key);
#override
_SlidingDrawerState createState() => _SlidingDrawerState();
}
class _SlidingDrawerState extends State<SlidingDrawer> {
bool _opened = false;
void open() {
setState(() {
_opened = true;
});
}
void close() {
setState(() {
_opened = false;
});
}
#override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final height = MediaQuery.of(context).size.height;
final drawerWidth = width * widget.drawerRatio;
return GestureDetector(
onHorizontalDragUpdate: (details) {
if (details.delta.dx > widget.swipeSensitivity) {
open();
} else if (details.delta.dx < -widget.swipeSensitivity) {
close();
}
},
child: SizedBox(
width: width,
height: height,
child: Stack(
children: [
AnimatedPositioned(
width: drawerWidth,
height: height,
left: _opened ? 0 : -drawerWidth,
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.animationCurve,
child: Container(
color: Colors.amber,
child: widget.drawer,
),
),
AnimatedPositioned(
height: height,
width: width,
left: _opened ? drawerWidth : 0,
duration: Duration(milliseconds: widget.animationDuration),
curve: widget.animationCurve,
child: Stack(
fit: StackFit.expand,
children: [
widget.child,
AnimatedSwitcher(
duration: Duration(milliseconds: widget.animationDuration),
switchInCurve: widget.animationCurve,
switchOutCurve: widget.animationCurve,
child: _opened
? GestureDetector(
onTap: () {
setState(() {
_opened = false;
});
},
child: Container(
color: widget.overlayColor.withOpacity(
widget.overlayOpacity,
),
),
)
: null,
)
],
),
),
],
),
),
);
}
}
ORIGINAL ANSWER
As pointed out by #Yadu in the comment
you could use Single child horizontal scroll view (with disabled scroll physics) with Scrollable.ensureVisible(context) to show the menu
using an horizontal scroll view is working.
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: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _drawerOpened = false;
final drawerKey = new GlobalKey();
final mainKey = new GlobalKey();
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
children: [
Container(
key: drawerKey,
color: Colors.green,
width: MediaQuery.of(context).size.width * 0.8,
),
SizedBox(
key: mainKey,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Scaffold(
appBar: AppBar(
title: Text("My Page"),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleDrawer,
),
),
body: Container(
color: Colors.yellow,
width: MediaQuery.of(context).size.width,
),
),
),
],
),
);
}
void _toggleDrawer() {
setState(() {
_drawerOpened = !_drawerOpened;
});
if (_drawerOpened) {
Scrollable.ensureVisible(drawerKey.currentContext);
} else {
Scrollable.ensureVisible(mainKey.currentContext);
}
}
}

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);
}
}