Trying to create a circular rotating menu with 8 radials - flutter

I have tried several packages and found the following package fulfilling the purpose to some extent. https://pub.dev/packages/circle_list
One requirement missing in this package is the click-on icon and icon rotating in the center.

I figured it out. With RotateMode.stopRotate everything is working as you wanted it to be, but if you plan to allow the user to rotate it, then the order of elements (indexes) becomes messed up, so in that case, you'll need to figure out how to track the latest position of the first element to know where the start is and where it should go (and I'm not sure if it's even possible with this package).
class Sample extends StatefulWidget {
const Sample({Key? key}) : super(key: key);
#override
State<Sample> createState() => _SampleState();
}
class _SampleState extends State<Sample> with SingleTickerProviderStateMixin {
late AnimationController animationController;
#override
void initState() {
animationController = AnimationController(
upperBound: pi * 2,
vsync: this,
duration: const Duration(seconds: 2),
);
super.initState();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
List<int> elements = List.generate(10, (index) => index);
_center(int index) {
final angle = (pi * 2) * (index / 10);
animationController.animateTo(angle);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
drawer: const Drawer(),
body: Center(
child: AnimatedBuilder(
animation: animationController,
builder: ((context, child) {
return CircleList(
onDragEnd: () => {},
initialAngle: -animationController.value - pi / 2,
centerWidget: Text('center'),
rotateMode: RotateMode.stopRotate,
origin: Offset(0, 0),
children: elements
.map(
(index) => IconButton(
onPressed: () => _center(index),
icon: Icon(Icons.notifications),
color: Colors.blue.withOpacity(index * 0.05 + .3),
),
)
.toList(),
);
}),
),
),
);
}
}

Source Code
class RotatingSliderWidget extends StatefulWidget {
const RotatingSliderWidget({Key? key}) : super(key: key);
#override
State<RotatingSliderWidget> createState() => _RotatingSliderWidgetState();
}
class _RotatingSliderWidgetState extends State<RotatingSliderWidget>
with SingleTickerProviderStateMixin {
late AnimationController animationController;
#override
void initState() {
animationController = AnimationController(
upperBound: pi * 2,
vsync: this,
duration: const Duration(seconds: 2),
);
super.initState();
}
#override
void dispose() {
animationController.dispose();
super.dispose();
}
ContentModel? center;
final elements = [
ContentModel(index: 0, backgroundColor: const Color(0xFFFFEC38)),
ContentModel(index: 1, backgroundColor: const Color(0xFFEA1863)),
ContentModel(index: 2, backgroundColor: Colors.black),
ContentModel(index: 3, backgroundColor: const Color(0xFFF44133)),
ContentModel(index: 4, backgroundColor: const Color(0xFF1B97F3)),
ContentModel(index: 5, backgroundColor: Colors.white),
ContentModel(index: 6, backgroundColor: const Color(0xFF58BA61)),
ContentModel(index: 7, backgroundColor: const Color(0xFF9F9F9F)),
];
_center(int index) {
center = elements[index];
final angle = (pi * 2) * (index / 10);
animationController.animateTo(angle);
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
drawer: const Drawer(),
body: Center(
child: AnimatedBuilder(
animation: animationController,
builder: ((context, child) {
return CircleList(
centerWidget: center == null
? null
: Container(
decoration: BoxDecoration(
color: center!.backgroundColor,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => _center(center!.index),
icon: const Icon(Icons.notifications, size: 35),
color: const Color(0xFFA6F4CE),
),
),
onDragStart: (_) {},
onDragEnd: () {},
outerCircleColor: const Color(0xFFA6F4CE),
initialAngle: -animationController.value - pi / 2,
rotateMode: RotateMode.stopRotate,
origin: const Offset(0, 0),
children: elements
.map((index) => Container(
decoration: BoxDecoration(
color: index.backgroundColor,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => _center(index.index),
icon: const Icon(Icons.notifications, size: 35),
color: const Color(0xFFA6F4CE),
),
))
.toList());
}),
),
),
);
}
}
class ContentModel {
final int index;
final Color backgroundColor;
ContentModel({required this.index, required this.backgroundColor});
}
You can see Result in attached video

Related

Persistent Navigation Bar only in some pages

Stackoverflowers!
I'm using an BottomAppBar inside the bottomNavigationBar section of the Scaffold. The problem is that it doesn't persists while I'm navigating. I used the persistent_bottom_nav_bar plugin, but it doesn't work with my custom navigation bar because it has a ripple animation in one button and a bottomSheet that is over the keyboard.
home_page.dart
This file has the CustomNavigationBar and the main pages for each item on it.
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
NavigationProvider? navigationProvider;
AnimationController? rippleController;
AnimationController? scaleController;
Animation<double>? rippleAnimation;
Animation<double>? scaleAnimation;
#override
void initState() {
super.initState();
rippleController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
scaleController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
scaleController!.reverse();
Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: pages.elementAt(2),
childCurrent: widget,
fullscreenDialog: true,
)).whenComplete(() => setState(() {
buttonColor = Colors.black;
}));
}
});
rippleAnimation =
Tween<double>(begin: 80.0, end: 90.0).animate(rippleController!)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
rippleController!.reverse();
} else if (status == AnimationStatus.dismissed) {
rippleController!.forward();
}
});
scaleAnimation =
Tween<double>(begin: 1.0, end: 30.0).animate(scaleController!);
rippleController!.forward();
}
#override
void dispose() {
rippleController!.dispose();
scaleController!.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
navigationProvider = Provider.of<NavigationProvider>(context);
return Scaffold(
body:
pages.elementAt(navigationProvider!.bottomNavigationBarSelectedIndex),
bottomNavigationBar: CustomNavigationBar(
rippleController: rippleController,
scaleController: scaleController,
rippleAnimation: rippleAnimation,
scaleAnimation: scaleAnimation),
);
}
}
custom_navigation_bar.dart
This file contains the properties of the CustomNavigationBar.
class CustomNavigationBar extends StatefulWidget {
const CustomNavigationBar({
super.key,
this.rippleController,
this.scaleController,
this.rippleAnimation,
this.scaleAnimation,
});
final AnimationController? rippleController;
final AnimationController? scaleController;
final Animation<double>? rippleAnimation;
final Animation<double>? scaleAnimation;
#override
State<CustomNavigationBar> createState() => _CustomNavigationBarState();
}
class _CustomNavigationBarState extends State<CustomNavigationBar> {
#override
Widget build(BuildContext context) {
final navigationProvider = Provider.of<NavigationProvider>(context);
return BottomAppBar(
child: IconTheme(
data: const IconThemeData(color: Colors.black, size: 36),
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
direction: Axis.vertical,
children: [
IconButton(
icon: ...,
padding: ...,
constraints: ...,
onPressed: () {
//Here I change the selected index with Provider.
...
},
),
Text(
title,
style: ...,
),
],
),
const Spacer(),
Wrap(...),
const Spacer(),
InkWell(
onTap: () {
setState(
() {
//Executes the ripple animation.
widget.scaleController!.forward();
},
);
},
child: AnimatedBuilder(
animation: widget.scaleAnimation!,
builder: (context, child) => Transform.scale(
scale: widget.scaleAnimation!.value,
child: Container(
width: 50,
height: 50,
margin: const EdgeInsets.all(10),
decoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.blue),
child: Icon(Icons.add,
color: widget.scaleAnimation!.value == 1.0
? Colors.white
: Colors.blue),
),
),
),
),
const Spacer(),
Wrap(...),
const Spacer(),
Wrap(...),
],
),
),
),
);
}
}
As you can see, I use Provider to manage the state of the CustomNavigationBar when it changes the index.
Example of what I want:
This app is Splitwise and it has some pages with the navigation bar and others without it. That ripple animation is similar to mine. Also the bottom sheet has the same effect in my app.
I'll wait for all your suggestions, thanks!

Shake/jitter animation for TabBar Flutter

Trying to get desired output of the Animation above. Tried with AnimatedBuilder along with Transform of Matrix of z-axis for this animation. But failed to do so. Here's my code. I couldn't get the angle, shrinking or the shaking part right.
MyHomePage
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late TabController _tabController;
late void Function() _memberCaller;
late void Function() _voucherCaller;
late void Function() _rewardCaller;
late void Function() _drinksCaller;
double degree = 0;
#override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
void _callMethoCaller(int index) {
switch (index) {
case 0:
_memberCaller.call();
break;
case 1:
_voucherCaller.call();
break;
case 2:
_rewardCaller.call();
break;
case 3:
_drinksCaller.call();
break;
}
}
#override
Widget build(BuildContext context) {
List<Widget> _tabs(int index) => [
AnimationTab(
iconName: Icons.card_giftcard,
label: 'Membership',
functionCaller: (void Function() method) {
_memberCaller = method;
}),
AnimationTab(
iconName: Icons.confirmation_num_outlined,
label: 'Voucher',
functionCaller: (void Function() method) {
_voucherCaller = method;
}),
AnimationTab(
iconName: Icons.emoji_events_outlined,
label: 'Rewards',
functionCaller: (void Function() method) {
_rewardCaller = method;
}),
AnimationTab(
iconName: Icons.wine_bar_outlined,
label: 'Drinks',
functionCaller: (void Function() method) {
_drinksCaller = method;
}),
];
return Scaffold(
appBar: AppBar(
centerTitle: true,
bottom: PreferredSize(
child: Container(
child: TabBar(
onTap: _callMethoCaller,
controller: _tabController,
labelPadding: EdgeInsets.only(top: 5.0, bottom: 2.0),
indicatorColor: Colors.black,
tabs: List.generate(4, (index) => _tabs(index)[index]),
),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: const [
BoxShadow(
color: Colors.white,
spreadRadius: 5.0,
offset: Offset(0, 3))
],
),
),
preferredSize: Size.fromHeight(30),
),
),
body: TabBarView(
controller: _tabController,
children: const [
Center(child: Text('1')),
Center(child: Text('2')),
Center(child: Text('3')),
Center(child: Text('4')),
],
),
);
}
}
AnimationTab
class AnimationTab extends StatefulWidget {
final String label;
final IconData iconName;
final FunctionCaller functionCaller;
const AnimationTab({
Key? key,
required this.iconName,
required this.label,
required this.functionCaller,
}) : super(key: key);
#override
_AnimationTabState createState() => _AnimationTabState();
}
class _AnimationTabState extends State<AnimationTab>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
final _curvedAnimation = CurvedAnimation(
parent: _animationController,
curve: Curves.bounceIn,
reverseCurve: Curves.bounceOut);
_animation = TweenSequence<double>([
TweenSequenceItem<double>(tween: Tween(begin: 0, end: 12.5), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 12.5, end: 0), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 0, end: -12.5), weight: 1),
]).animate(_curvedAnimation)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reverse();
}
});
}
void _animationExecution() {
_animationController.forward();
}
#override
Widget build(BuildContext context) {
const _tabTextStyle = TextStyle(
fontWeight: FontWeight.w300, fontSize: 12, color: Colors.black);
widget.functionCaller.call(_animationExecution);
return AnimatedBuilder(
animation: _animationController,
builder: (ctx, _) {
return Transform(
transform: Matrix4.rotationZ(math.pi * _animation.value / 180),
alignment: Alignment.center,
child: Tab(
icon: Icon(widget.iconName, color: Colors.black),
child: Text(widget.label, style: _tabTextStyle),
),
);
},
);
}
}
you dont need those TweenSequences, "status listeners" etc, also instead of Matrix4.rotationZ use ordinary Transform.rotate, check this:
class TabTest extends StatefulWidget {
#override
_TabTestState createState() => _TabTestState();
}
class _TabTestState extends State<TabTest> with TickerProviderStateMixin {
late TabController tabController;
late List<AnimationController> animationControllers;
#override
void initState() {
super.initState();
tabController = TabController(length: 4, vsync: this)
..addListener(_listener);
animationControllers = List.generate(4, (i) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 750),
reverseDuration: Duration(milliseconds: 350),
));
}
#override
Widget build(BuildContext context) {
// timeDilation = 5;
return Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: List.generate(4, (i) => AnimatedBuilder(
animation: animationControllers[i],
builder: (context, child) {
final child = Tab(
icon: Icon(Icons.cast),
child: Text('tab $i'),
);
final value = animationControllers[i].value;
if (animationControllers[i].status == AnimationStatus.forward) {
final angle = sin(4 * pi * value) * pi * 0.3;
return Transform.rotate(angle: angle, child: child);
} else {
final dy = sin(2 * pi * value) * 0.2;
return FractionalTranslation(translation: Offset(0, dy), child: child);
}
},
),
),
controller: tabController,
),
),
body: TabBarView(
children: List.generate(4, (i) =>
FittedBox(
child: Text('tab $i'),
),
),
controller: tabController,
),
);
}
void _listener() {
if (tabController.indexIsChanging) {
animationControllers[tabController.previousIndex].reverse();
} else {
animationControllers[tabController.index].forward();
}
}
#override
void dispose() {
super.dispose();
tabController
..removeListener(_listener)
..dispose();
animationControllers.forEach((ac) => ac.dispose());
}
}

Flutter - How to Make floating action button animation like gmail?

I am able to make quite a similar floating action button animation like Gmail app, but I am getting a little bit of margin when I isExpanded is false. Any solution?
Here is my code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isExpanded = false;
Widget build(context) {
return Scaffold(
floatingActionButton: AnimatedContainer(
width: isExpanded ? 150 : 56,
height: 56,
duration: Duration(milliseconds: 300),
child: FloatingActionButton.extended(
onPressed: () {},
icon: Icon(Icons.ac_unit),
label: isExpanded ? Text("Start chat") : SizedBox(),
),
),
appBar: AppBar(),
body: FlatButton(
onPressed: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: Text('Press here to change FAB')));
}
}
Looks like FloatingActionButton has some hardcoded padding set for an icon. To fix that, you could do the following:
FloatingActionButton.extended(
onPressed: () {},
icon: isExpanded ? Icon(Icons.ac_unit) : null,
label: isExpanded ? Text("Start chat") : Icon(Icons.ac_unit),
)
If you want an animation then you have to write your own custom fab:
class ScrollingExpandableFab extends StatefulWidget {
const ScrollingExpandableFab({
Key? key,
this.controller,
required this.label,
required this.icon,
this.onPressed,
this.scrollOffset = 50.0,
this.animDuration = const Duration(milliseconds: 500),
}) : super(key: key);
final ScrollController? controller;
final String label;
final Widget icon;
final VoidCallback? onPressed;
final double scrollOffset;
final Duration animDuration;
#override
State<ScrollingExpandableFab> createState() => _ScrollingExpandableFabState();
}
class _ScrollingExpandableFabState extends State<ScrollingExpandableFab>
with TickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: widget.animDuration,
vsync: this,
);
late final Animation<double> _anim = Tween<double>(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
Color get _backgroundColor =>
Theme.of(context).floatingActionButtonTheme.backgroundColor ??
Theme.of(context).colorScheme.secondary;
ScrollController? get _scrollController => widget.controller;
_scrollListener() {
final position = _scrollController!.position;
if (position.pixels > widget.scrollOffset &&
position.userScrollDirection == ScrollDirection.reverse) {
_controller.forward();
} else if (position.pixels <= widget.scrollOffset &&
position.userScrollDirection == ScrollDirection.forward) {
_controller.reverse();
}
}
#override
void initState() {
super.initState();
_scrollController?.addListener(_scrollListener);
}
#override
void dispose() {
_scrollController?.removeListener(_scrollListener);
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 48.0),
child: AnimatedBuilder(
animation: _anim,
builder: (context, child) => Material(
elevation: 4.0,
type: MaterialType.button,
color: _backgroundColor,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: widget.onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
widget.icon,
ClipRect(
child: Align(
alignment: AlignmentDirectional.centerStart,
widthFactor: 1 - _anim.value,
child: Opacity(
opacity: 1 - _anim.value,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: SimpleclubText.button(widget.label),
),
),
),
),
],
),
),
),
),
),
);
}
}
Please follow example below:
FloatingActionButton.extended(
onPressed: () {
setState(() {
expanded = !expanded;
});
},
backgroundColor: context.theme.colorScheme.secondary,
label: expanded ? Text(t.addPlan) : Icon(Icons.add),
icon: expanded ? Icon(Icons.add) : null,
shape: expanded ? null : CircleBorder(),
),
);
It's important that you don't set isExtended on the fab and you need to set CircleBorder when the fab is not expanded to make sure the fab still has a circular shape.
I hope this helps!

How can I add another part to this page and make it so when I press on one question it opens up a different set of questions? Complex UI

Marcinus created this:
https://user-images.githubusercontent.com/16286046/72802272-afed4e00-3c4b-11ea-80f7-e98717babbb2.gif
I want to add another page to the sets of questions and make it so that when I pick a certain question, it opens up a certain question. Im quite new to flutter so any help would be appreciated
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
theme: ThemeData(primarySwatch: Colors.blue, brightness: Brightness.dark),
home: FlightsStepper(),
));
}
class FlightsStepper extends StatefulWidget {
#override
_FlightsStepperState createState() => _FlightsStepperState();
}
class _FlightsStepperState extends State<FlightsStepper> {
int pageNumber = 1;
#override
Widget build(BuildContext context) {
Widget page = pageNumber == 1
? Page(
key: Key('page1'),
onOptionSelected: () => setState(() => pageNumber = 2),
question:
'Do you typically fly for business, personal reasons, or some other reason?',
answers: <String>['Business', 'Personal', 'Others'],
number: 1,
)
: Page(
key: Key('page2'),
onOptionSelected: () => setState(() => pageNumber = 1),
question: 'How many hours is your average flight?',
answers: <String>[
'Less than two hours',
'More than two but less than five hours',
'Others'
],
number: 2,
);
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
decoration: backgroundDecoration,
child: SafeArea(
child: Stack(
children: <Widget>[
ArrowIcons(),
Plane(),
Line(),
Positioned.fill(
left: 32.0 + 8,
child: AnimatedSwitcher(
child: page,
duration: Duration(milliseconds: 250),
),
),
],
),
),
),
);
}
}
class Line extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Positioned(
left: 32.0 + 32 + 8,
top: 40,
bottom: 0,
width: 1,
child: Container(color: Colors.white.withOpacity(0.5)),
);
}
}
class Page extends StatefulWidget {
final int number;
final String question;
final List<String> answers;
final VoidCallback onOptionSelected;
const Page(
{Key key,
#required this.onOptionSelected,
#required this.number,
#required this.question,
#required this.answers})
: super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> with SingleTickerProviderStateMixin {
List<GlobalKey<_ItemFaderState>> keys;
int selectedOptionKeyIndex;
AnimationController _animationController;
#override
void initState() {
super.initState();
keys = List.generate(
2 + widget.answers.length,
(_) => GlobalKey<_ItemFaderState>(),
);
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
onInit();
}
Future<void> animateDot(Offset startOffset) async {
OverlayEntry entry = OverlayEntry(
builder: (context) {
double minTop = MediaQuery.of(context).padding.top + 52;
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Positioned(
left: 26.0 + 32 + 8,
top: minTop +
(startOffset.dy - minTop) * (1 - _animationController.value),
child: child,
);
},
child: Dot(),
);
},
);
Overlay.of(context).insert(entry);
await _animationController.forward(from: 0);
entry.remove();
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 32),
ItemFader(key: keys[0], child: StepNumber(number: widget.number)),
ItemFader(
key: keys[1],
child: StepQuestion(question: widget.question),
),
Spacer(),
...widget.answers.map((String answer) {
int answerIndex = widget.answers.indexOf(answer);
int keyIndex = answerIndex + 2;
return ItemFader(
key: keys[keyIndex],
child: OptionItem(
name: answer,
onTap: (offset) => onTap(keyIndex, offset),
showDot: selectedOptionKeyIndex != keyIndex,
),
);
}),
SizedBox(height: 64),
],
);
}
void onTap(int keyIndex, Offset offset) async {
for (GlobalKey<_ItemFaderState> key in keys) {
await Future.delayed(Duration(milliseconds: 40));
key.currentState.hide();
if (keys.indexOf(key) == keyIndex) {
setState(() => selectedOptionKeyIndex = keyIndex);
animateDot(offset).then((_) => widget.onOptionSelected());
}
}
}
void onInit() async {
for (GlobalKey<_ItemFaderState> key in keys) {
await Future.delayed(Duration(milliseconds: 40));
key.currentState.show();
}
}
}
class StepNumber extends StatelessWidget {
final int number;
const StepNumber({Key key, #required this.number}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 64, right: 16),
child: Text(
'0$number',
style: TextStyle(
fontSize: 64,
fontWeight: FontWeight.bold,
color: Colors.white.withOpacity(0.5),
),
),
);
}
}
class StepQuestion extends StatelessWidget {
final String question;
const StepQuestion({Key key, #required this.question}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 64, right: 16),
child: Text(
question,
style: TextStyle(fontSize: 24),
),
);
}
}
class OptionItem extends StatefulWidget {
final String name;
final void Function(Offset dotOffset) onTap;
final bool showDot;
const OptionItem(
{Key key, #required this.name, #required this.onTap, this.showDot = true})
: super(key: key);
#override
_OptionItemState createState() => _OptionItemState();
}
class _OptionItemState extends State<OptionItem> {
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
RenderBox object = context.findRenderObject();
Offset globalPosition = object.localToGlobal(Offset.zero);
widget.onTap(globalPosition);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: <Widget>[
SizedBox(width: 26),
Dot(visible: widget.showDot),
SizedBox(width: 26),
Expanded(
child: Text(
widget.name,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 26),
),
)
],
),
),
);
}
}
class ItemFader extends StatefulWidget {
final Widget child;
const ItemFader({Key key, #required this.child}) : super(key: key);
#override
_ItemFaderState createState() => _ItemFaderState();
}
class _ItemFaderState extends State<ItemFader>
with SingleTickerProviderStateMixin {
//1 means its below, -1 means its above
int position = 1;
AnimationController _animationController;
Animation _animation;
void show() {
setState(() => position = 1);
_animationController.forward();
}
void hide() {
setState(() => position = -1);
_animationController.reverse();
}
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 600),
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, 64 * position * (1 - _animation.value)),
child: Opacity(
opacity: _animation.value,
child: child,
),
);
},
child: widget.child,
);
}
}
class Dot extends StatelessWidget {
final bool visible;
const Dot({Key key, this.visible = true}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: visible ? Colors.white : Colors.transparent,
),
);
}
}
class ArrowIcons extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Positioned(
left: 8,
bottom: 0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.arrow_upward),
onPressed: () {},
),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
),
child: IconButton(
color: Color.fromRGBO(120, 58, 183, 1),
icon: Icon(Icons.arrow_downward),
onPressed: () {},
),
),
],
),
);
}
}
class Plane extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Positioned(
left: 32.0 + 8,
top: 32,
child: RotatedBox(
quarterTurns: 2,
child: Icon(
Icons.airplanemode_active,
size: 64,
),
),
);
}
}
const backgroundDecoration = BoxDecoration(
gradient: LinearGradient(
colors: [
Color.fromRGBO(76, 61, 243, 1),
Color.fromRGBO(120, 58, 183, 1),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
);
Navigator push() and pop() could be what you are looking for if you want to open a new page, pass the particular questions data as arguments
Flutter Official Doc

Flutter adding more options for dialogs

is any solution to make drag and drop dialogs in flutter? for example after showing dialog in center of screen i would like to drag it to top of screen to make fullscreen dialog over current cover, for example this code is simple implementation to show dialog and i'm not sure, how can i do that
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(title: 'Flutter Demo', theme: ThemeData(), home: Page());
}
}
class Page extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton.icon(
onPressed: () {
showDialog(
context: context,
builder: (_) => FunkyOverlay(),
);
},
icon: Icon(Icons.message),
label: Text("PopUp!")),
),
);
}
}
class FunkyOverlay extends StatefulWidget {
#override
State<StatefulWidget> createState() => FunkyOverlayState();
}
class FunkyOverlayState extends State<FunkyOverlay>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> scaleAnimation;
#override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 450));
scaleAnimation =
CurvedAnimation(parent: controller, curve: Curves.elasticInOut);
controller.addListener(() {
setState(() {});
});
controller.forward();
}
#override
Widget build(BuildContext context) {
return Center(
child: Material(
color: Colors.transparent,
child: ScaleTransition(
scale: scaleAnimation,
child: Container(
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0))),
child: Padding(
padding: const EdgeInsets.all(50.0),
child: Text("Well hello there!"),
),
),
),
),
);
}
}
This is one way to do it ,
import 'package:flutter/material.dart';
main() {
runApp(MaterialApp(
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: App(),
));
}
class App extends StatefulWidget {
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.open_in_new),
onPressed: () {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: "hi",
barrierColor: Colors.black.withOpacity(0.2),
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (context, pAnim, sAnim) {
return SafeArea(child: FloatingDialog());
},
transitionBuilder: (context, pAnim, sAnim, child) {
if (pAnim.status == AnimationStatus.reverse) {
return FadeTransition(
opacity: Tween(begin: 0.0, end: 0.0).animate(pAnim),
child: child,
);
} else {
return FadeTransition(
opacity: pAnim,
child: child,
);
}
},
);
},
),
);
}
}
class FloatingDialog extends StatefulWidget {
#override
_FloatingDialogState createState() => _FloatingDialogState();
}
class _FloatingDialogState extends State<FloatingDialog>
with TickerProviderStateMixin {
double _dragStartYPosition;
double _dialogYOffset;
Widget myContents = MyScaffold();
AnimationController _returnBackController;
Animation<double> _dialogAnimation;
#override
void initState() {
super.initState();
_dialogYOffset = 0.0;
_returnBackController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1300))
..addListener(() {
setState(() {
_dialogYOffset = _dialogAnimation.value;
print(_dialogYOffset);
});
});
}
#override
void dispose() {
_returnBackController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
top: 100.0,
bottom: 10.0,
left: 10.0,
right: 10.0,
),
child: Transform.translate(
offset: Offset(0.0, _dialogYOffset),
child: Column(
children: <Widget>[
Icon(
Icons.keyboard_arrow_up,
color: Colors.white,
),
Expanded(
child: GestureDetector(
onVerticalDragStart: (dragStartDetails) {
_dragStartYPosition = dragStartDetails.globalPosition.dy;
print(dragStartDetails.globalPosition);
},
onVerticalDragUpdate: (dragUpdateDetails) {
setState(() {
_dialogYOffset = (dragUpdateDetails.globalPosition.dy) -
_dragStartYPosition;
});
print(_dialogYOffset);
if (_dialogYOffset < -90.0) {
Navigator.of(context).pop();
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, pAnim, sAnim) => myContents,
transitionDuration: Duration(milliseconds: 500),
transitionsBuilder: (context, pAnim, sAnim, child) {
if (pAnim.status == AnimationStatus.forward) {
return ScaleTransition(
scale: Tween(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
parent: pAnim,
curve: Curves.elasticOut)),
child: child,
);
} else {
return FadeTransition(
opacity: pAnim,
child: child,
);
}
}),
);
}
},
onVerticalDragEnd: (dragEndDetails) {
_dialogAnimation = Tween(begin: _dialogYOffset, end: 0.0)
.animate(CurvedAnimation(
parent: _returnBackController,
curve: Curves.elasticOut));
_returnBackController.forward(from: _dialogYOffset);
_returnBackController.forward(from: 0.0);
},
child: myContents,
),
),
],
),
),
);
}
}
class MyScaffold extends StatelessWidget {
const MyScaffold({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Channels"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(),
body: Placeholder(),
),
),
);
},
),
),
);
}
}
Output:
You can try this.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool _shown = false;
double _topOffset = 20, _dialogHeight = 400;
Duration _duration = Duration(milliseconds: 400);
Offset _offset, _initialOffset;
#override
void didChangeDependencies() {
super.didChangeDependencies();
var size = MediaQuery.of(context).size;
_offset = Offset(size.width, (size.height - _dialogHeight) / 2);
_initialOffset = _offset;
}
#override
Widget build(BuildContext context) {
var appBarColor = Colors.blue[800];
return Scaffold(
floatingActionButton: FloatingActionButton(onPressed: () => setState(() => _shown = !_shown)),
body: SizedBox.expand(
child: Stack(
children: <Widget>[
Container(
color: appBarColor,
child: SafeArea(
bottom: false,
child: Align(
child: Column(
children: <Widget>[
MyAppBar(
title: "Image",
color: appBarColor,
icon: Icons.home,
onPressed: () {},
),
Expanded(child: Image.asset("assets/images/landscape.jpeg", fit: BoxFit.cover)),
],
),
),
),
),
AnimatedOpacity(
opacity: _shown ? 1 : 0,
duration: _duration,
child: Material(
elevation: 8,
color: Colors.grey[900].withOpacity(0.5),
child: _shown
? GestureDetector(
onTap: () => setState(() => _shown = !_shown),
child: Container(color: Colors.transparent, child: SizedBox.expand()),
)
: SizedBox.shrink(),
),
),
// this shows our dialog
Positioned(
top: _offset.dy,
left: 10,
right: 10,
height: _shown ? null : 0,
child: AnimatedOpacity(
duration: _duration,
opacity: _shown ? 1 : 0,
child: GestureDetector(
onPanUpdate: (details) => setState(() => _offset += details.delta),
onPanEnd: (details) {
// when tap is lifted and current y position is less than set _offset, navigate to the next page
if (_offset.dy < _topOffset) {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, anim1, anim2) => Screen2(),
transitionDuration: _duration,
transitionsBuilder: (context, anim1, anim2, child) {
bool isForward = anim1.status == AnimationStatus.forward;
Tween<double> tween = Tween(begin: isForward ? 0.9 : 0.5, end: 1);
return ScaleTransition(
scale: tween.animate(
CurvedAnimation(
parent: anim1,
curve: isForward ? Curves.bounceOut : Curves.easeOut,
),
),
child: child,
);
},
),
).then((_) {
_offset = _initialOffset;
});
}
// make the dialog come back to the original position
else {
Timer.periodic(Duration(milliseconds: 5), (timer) {
if (_offset.dy < _initialOffset.dy - _topOffset) {
_offset = Offset(_offset.dx, _offset.dy + 15);
setState(() {});
} else if (_offset.dy > _initialOffset.dy + _topOffset) {
_offset = Offset(_offset.dx, _offset.dy - 15);
setState(() {});
} else
timer.cancel();
});
}
},
child: Column(
children: <Widget>[
Icon(Icons.keyboard_arrow_up, color: Colors.white, size: 32),
Hero(
tag: "MyTag",
child: SizedBox(
height: _dialogHeight, // makes sure we don't exceed than our specified height
child: SingleChildScrollView(child: CommonWidget(appBar: MyAppBar(title: "FlutterLogo", color: Colors.orange))),
),
),
],
),
),
),
),
],
),
),
);
}
}
// this app bar is used in 1st and 2nd screen
class MyAppBar extends StatelessWidget {
final String title;
final Color color;
final IconData icon;
final VoidCallback onPressed;
const MyAppBar({Key key, #required this.title, #required this.color, this.icon, this.onPressed}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
height: kToolbarHeight,
color: color,
width: double.maxFinite,
alignment: Alignment.centerLeft,
child: Row(
children: <Widget>[
icon != null ? IconButton(icon: Icon(icon), onPressed: onPressed, color: Colors.white,) : SizedBox(width: 16),
Text(
title,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white),
),
],
),
);
}
}
// this is the one which is shown in both Dialog and Screen2
class CommonWidget extends StatelessWidget {
final bool isFullscreen;
final Widget appBar;
const CommonWidget({Key key, this.isFullscreen = false, this.appBar}) : super(key: key);
#override
Widget build(BuildContext context) {
var child = Container(
width: double.maxFinite,
color: Colors.blue,
child: FlutterLogo(size: 300, colors: Colors.orange),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
appBar,
isFullscreen ? Expanded(child: child) : child,
],
);
}
}
class Screen2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
var appBarColor = Colors.orange;
return Scaffold(
body: Container(
color: appBarColor,
child: SafeArea(
bottom: false,
child: CommonWidget(
isFullscreen: true,
appBar: MyAppBar(
title: "FlutterLogo",
color: appBarColor,
icon: Icons.arrow_back,
onPressed: () => Navigator.pop(context),
),
),
),
),
);
}
}