Flutter - Custom Animation Using CustomPaint - flutter

I created a CustomPaint widget and I want to change the heigth from zero to end of screen with smooth animation as shown in the image.

You can use Flutter's AnimationController to drive explicit CustomPaint animation as follows (DartPad):
class AnimatedHeightCustomPaint extends StatefulWidget {
final AnimationController controller;
final Size size;
final Color color;
final Curve curve;
const AnimatedHeightCustomPaint({Key? key,
required this.controller,
required this.size,
required this.color,
this.curve = Curves.linear}) : super(key: key);
#override
State<StatefulWidget> createState() => AnimatedHeightCustomPaintState();
}
class AnimatedHeightCustomPaintState extends State<AnimatedHeightCustomPaint> {
#override
void initState() {
super.initState();
// Listen to the animation progress and update the custom paint (height in this case)
widget.controller.addListener(() => setState(() { }));
}
#override
Widget build(BuildContext context) => CustomPaint(
painter: AnimatedHeightPainter(
// Here we can apply some fancy animation progress like bounce in
heightProps: widget.curve.transform(widget.controller.value),
color: widget.color,
),
// Since you want to change the height, you need to provide a size
size: widget.size,
);
}
class AnimatedHeightPainter extends CustomPainter
{
// Progress of the animation, i.e. between 0.0 and 1.0
final double heightProps;
final Color color;
AnimatedHeightPainter({required this.heightProps, required this.color});
#override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Offset(0.0, size.height * (1 - heightProps)) & Size(size.width, size.height * heightProps),
Paint()..color = color,
);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
As a complete sample,
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: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
// Flutter's AnimationController
late AnimationController _animationController;
#override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, // the SingleTickerProviderStateMixin
duration: const Duration(seconds: 2),
);
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: AnimatedHeightCustomPaint(
controller: _animationController,
size: MediaQuery.of(context).size,
color: Colors.red,
curve: Curves.bounceInOut,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// In Parent widget, you can control the animation (back and forth here)
if(_animationController.isCompleted) {
_animationController.reverse();
}else if(_animationController.isDismissed) {
_animationController.forward();
}
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class AnimatedHeightCustomPaint extends StatefulWidget {
final AnimationController controller;
final Size size;
final Color color;
final Curve curve;
const AnimatedHeightCustomPaint({Key? key,
required this.controller,
required this.size,
required this.color,
this.curve = Curves.linear}) : super(key: key);
#override
State<StatefulWidget> createState() => AnimatedHeightCustomPaintState();
}
class AnimatedHeightCustomPaintState extends State<AnimatedHeightCustomPaint> {
#override
void initState() {
super.initState();
// Listen to the animation progress and update the custom paint (height in this case)
widget.controller.addListener(() => setState(() { }));
}
#override
Widget build(BuildContext context) => CustomPaint(
painter: AnimatedHeightPainter(
// Here we can apply some fancy animation progress like bounce in
heightProps: widget.curve.transform(widget.controller.value),
color: widget.color,
),
// Since you want to change the height, you need to provide a size
size: widget.size,
);
}
class AnimatedHeightPainter extends CustomPainter
{
// Progress of the animation, i.e. between 0.0 and 1.0
final double heightProps;
final Color color;
AnimatedHeightPainter({required this.heightProps, required this.color});
#override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Offset(0.0, size.height * (1 - heightProps)) & Size(size.width, size.height * heightProps),
Paint()..color = color,
);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Perhaps You can even use AnimatedContainer if there is no specific reason to use CustomPaint widget (DartPad):
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: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
// Flutter's AnimationController
bool isExpanded = true;
#override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: [
Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
curve: Curves.bounceInOut,
width: screenSize.width,
height: screenSize.height * (isExpanded ? 1: 0),
duration: const Duration(seconds: 2),
color: Colors.red,
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() => isExpanded = !isExpanded);
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

Related

As a common appbar widget how to change appbar color when page is scrolled Flutter

Good morning friends, I'm trying to make the appbar transparent or white in scrollable
parts.
For me, this solution An efficient way in Flutter to change appbar color when scrolled works, but as the person said, I don't want to use setState continuously and do it in every separate component, so I'm trying to do what is mentioned in the comment. For this reason, I created a common appbar widget so that I can use it in other components. I made the CustomAppBar widget statefull, but I don't know where to add the scrollController. Therefore, I see errors. If anyone has time, can you help?
The code below is the widget where I call CustomAppBar.
import ...
const ExtractionBody({Key? key, required this.goal}) : super(key: key);
final Objective goal;
#override
ExtractionBodyState createState() => ExtractionBodyState();
}
class ExtractionBodyState extends ConsumerState<ExtractionBody> {
#override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(100),
child: CustomAppBar(
icon: IconButton(
icon: const Icon(PhosphorIcons.xBold),
onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil(
HomePage.routeName,
(route) => false,
),
),
),
),
body: ExtractionRequestContent(
goal: widget.goal, scrollController: _scrollController),
);
}
}
Finally, this is my CustomAppBar code. Thank you very much in advance. and have a good weekend everyone
class CustomAppBar extends StatefulHookConsumerWidget {
static String routeName = "/extraction_body";
const CustomAppBar({Key? key, this.icon})
: preferredSize = const Size.fromWidth(50),
super(key: key);
final Widget? icon;
#override
final Size preferredSize; // default is 56.0
#override
CustomAppBarState createState() => CustomAppBarState();
}
class CustomAppBarState extends ConsumerState<CustomAppBar> {
bool isAppbarCollapsing = false;
final ScrollController _scrollController = ScrollController();
#override
void initState() {
super.initState();
_initializeController();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _initializeController() {
_scrollController.addListener(() {
if (_scrollController.offset == 0.0 &&
!_scrollController.position.outOfRange) {
//Fully expanded situation
if (!mounted) return;
setState(() => isAppbarCollapsing = false);
}
if (_scrollController.offset >= 9.0 &&
!_scrollController.position.outOfRange) {
//Collapsing situation
if (!mounted) return;
setState(() => isAppbarCollapsing = true);
}
});
}
#override
Widget build(BuildContext context) {
return AppBar(
elevation: 0,
backgroundColor:
isAppbarCollapsing ? AppColors.monochromeWhite : Colors.transparent,
title: Text(context.l10n.buttonCancel),
titleSpacing: 4,
leading: widget.icon,
);
}
}
Thanks!
Instead of define ScrollController in CustomAppBar, pass it in constructor like this:
class CustomAppBar extends StatefulHookConsumerWidget {
static String routeName = "/extraction_body";
const CustomAppBar({Key? key, this.icon, required this.scrollController})
: preferredSize = const Size.fromWidth(50),
super(key: key);
final Widget? icon;
final ScrollController scrollController;
#override
final Size preferredSize; // default is 56.0
#override
CustomAppBarState createState() => CustomAppBarState();
}
and use it like this:
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final ScrollController scrollController = ScrollController(); //<---- define scrollController here
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(100),
child: CustomAppbar(scrollController: scrollController)),
body: ListView.builder(
controller: scrollController,
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 100,
width: 100,
color: Colors.red,
margin: EdgeInsets.all(12),
);
},
),
));
}
}

Simple BounceIn Image Flutter Animation

What i want?
I want to do simple animation with this image.
What does it mean?
If i click on it, it should make smooth for example bounce in animation. Im open in ways through which we can achieve this effect.
What i tried?
I thought AnimatedContainer will get done but curve: parameter is not doing anything.
ps. I am beginner
import 'package:google_fonts/google_fonts.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Poppy App',
theme: ThemeData(
primarySwatch: Colors.brown,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
double _imgwidth = 200;
double _imgheight = 200;
class _MyHomePageState extends State<MyHomePage> {
void poopAnimationIn() async {
setState(() {
_imgheight = 300;
_imgwidth = 300;
});
await Future.delayed(const Duration(milliseconds: 500), () {
setState(() {
_imgheight = 200;
_imgwidth = 200;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: const Color.fromARGB(164, 117, 81, 1),
child: Center(
child: GestureDetector(
onTap: () {
poopAnimationIn();
},
child: AnimatedContainer(
curve: Curves.bounceIn,
duration: const Duration(milliseconds: 200),
child: Image.network(
'https://picsum.photos/250?image=9',
width: _imgwidth,
height: _imgheight,
)),
),
),
));
}
}
You need to specify the size for AnimatedContainer and change that size in setState.
Here is the edited code:
I suggest you use other curves like easeIn instead of bounceIn to have a smoother animation
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(
debugShowCheckedModeBanner: false,
title: 'Poppy App',
theme: ThemeData(
primarySwatch: Colors.brown,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
double initialHeight = 200;
double initialImageHeight = initialHeight;
double expandedImageHeight = 300;
void poopAnimationIn() async {
setState(() {
initialImageHeight = expandedImageHeight;
});
await Future.delayed(const Duration(milliseconds: 500), () {
setState(() {
initialImageHeight = initialHeight;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: const Color.fromARGB(164, 117, 81, 1),
child: Center(
child: GestureDetector(
onTap: () async {
poopAnimationIn();
},
child: AnimatedContainer(
height: initialImageHeight,
curve: Curves.bounceIn,
duration: const Duration(milliseconds: 200),
child: Image.network(
'https://picsum.photos/250?image=9',
// width: _imgwidth,
// height: _imgheight,
))),
),
),
// ),
);
}
}

Flutter ClipRect and Align : how to compute the Alignment values

I want to display part of an image (a rectangle of 128 * 128 inside this image which is 512 * 512) in a rectangle in Flutter app.
Like this:
I want to select a specific zone in the source image, here from x=115, y=12 with width=128,height=128.
So here's my Flutter code to do this:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
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> {
#override
Widget build(BuildContext context) {
var myWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
width: myWidth,
height: myWidth,
color: Colors.yellow,
child: FittedBox(
fit: BoxFit.fill,
child: ClipRect(
child: Align(
alignment: Alignment(2.0*115.0 / 512.0 - 1, 2.0*12.0 / 512.0 -1.0),
widthFactor: 0.25,
heightFactor: 0.25,
child: Image.network(
"https://homepages.cae.wisc.edu/~ece533/images/baboon.png"),
),
),
)));
}
}
I suppose that the widthFactor and heightFactor represent the percentage of the original image I want to display - so here, because I want to display 128 pixels, it's 25% of the original width/height.
However, I just can't figure out out to calculate the values for the alignment.
First I tried for x :
alignment: Alignment(2.0*115.0 / 512.0 - 1, 2.0*12.0 / 512.0 -1.0),
Which almost gives the desired result, but not quite, see below:
I tried many other combinations to calculate the Alignment values and read all I could find with the help of Google... Is this even possible to accomplish this with a ClipRect and an Align?
Any help is welcome ;-)
NOTE : I don't want to crop the original image (and hence produce a new image), I want to keep it as downloaded to avoid having N copies of the same bitmap in memory. What I want is a specific view on the image - something that is absolutely trivial to do with OpenGL/DirectX and UV coordinates for example, but not so with Flutter.
Based on pskink comment, provided a working solution in the original post.
import 'package:flutter/material.dart';
import 'package:network_to_file_image/image_for_canvas.dart';
import 'package:network_to_file_image/network_to_file_image.dart';
import 'dart:ui' as ui;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
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> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
var myWidth = MediaQuery.of(context).size.width;
User user = User(
filename: null,
url: "https://homepages.cae.wisc.edu/~ece533/images/baboon.png",
);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
width: myWidth,
height: myWidth,
color: Colors.yellow,
child: CustomPaint(
painter: ImagePainter(user, loadCallback: (_, __, ___) {
setState(() {});
})),
));
}
}
class ImagePainter extends CustomPainter {
final User user;
final LoadCallback<User> loadCallback;
ImagePainter(
this.user, {
required this.loadCallback,
});
#override
void paint(Canvas canvas, Size size) {
canvas.save();
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = Colors.lightGreenAccent);
canvas.restore();
var imageForCanvas = _imageForCanvas();
ui.Image? image = imageForCanvas.image(user);
if (image != null) {
canvas.save();
canvas.drawImageRect(
image,
Rect.fromLTWH(
12,
12,
256,
256,
),
Rect.fromLTWH(
0,
0,
size.width,
size.height,
),
Paint()..imageFilter = ui.ImageFilter.blur(sigmaX: .5, sigmaY: .5),
);
canvas.restore();
}
}
ImageForCanvas<User> _imageForCanvas() => ImageForCanvas<User>(
imageProviderSupplier: (User user) => NetworkToFileImage(
file: null,
url: user.url,
),
keySupplier: (User user) => user.url!,
loadCallback: loadCallback,
);
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class User {
final String? filename;
final String? url;
User({
this.filename,
this.url,
});
}

Stack position not accurate

I want to add red blinking dot on the container when it is tapped, but the dot position is not accurate.
How to fix?
MyApp
import 'package:flutter/material.dart';
import 'blinking_dot.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;
double posx;
double posy;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Stack(
children: <Widget>[
Container(
height: double.infinity,width: double.infinity,
padding: EdgeInsets.all(10),
child: Image.asset("assets/img.jpg")),
Positioned(
child: BlinkingDot(),
left: posx,
top: posy,
)
],
)));
}
}
blinking_dot.dart
import 'package:flutter/material.dart';
class BlinkingDot extends StatefulWidget {
#override
_BlinkingDotState createState() => _BlinkingDotState();
}
class _BlinkingDotState extends State<BlinkingDot>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
#override
void initState() {
_animationController =
new AnimationController(vsync: this, duration: Duration(seconds: 1));
_animationController.repeat();
super.initState();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animationController,
child: Container(
height: 15,
width: 15,
child: FloatingActionButton(
backgroundColor: Colors.redAccent,
)));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
Output
posy = localOffset.dy- MediaQuery.of(context).padding.top - kToolbarHeight;
also you need to decrease offset by half of the red dot size
in your case if will something like this
posx = localOffset.dx - 7.5;
posy = localOffset.dy- MediaQuery.of(context).padding.top - kToolbarHeight - 7.5;
Are you looking like this?
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;
double posx;
double posy;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy-70.0;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("widget.title"),
),
body: GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Stack(
children: <Widget>[
Container(
height: double.infinity,
width: double.infinity,
padding: EdgeInsets.all(10),
child: Image.asset("assets/img.jpg")),
Positioned(
child: BlinkingDot(),
left: posx,
top: posy,
)
],
),
));
}
}
BlinkingDot page
class BlinkingDot extends StatefulWidget {
#override
_BlinkingDotState createState() => _BlinkingDotState();
}
class _BlinkingDotState extends State<BlinkingDot>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
#override
void initState() {
_animationController =
new AnimationController(vsync: this, duration: Duration(seconds: 1));
_animationController.repeat();
super.initState();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animationController,
child: Container(
height: 15,
width: 15,
child: FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.redAccent,
)));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
It's because you did not consider the following
You need to subtract AppBar height from dy.
You need to subtract the circle radius from both dx and dy.
You need to subtract the top padding from dy and left padding from
dx.
Do the following to get the expected result
posx = localOffset.dx - MediaQuery.of(context).padding.left - circleRadius;
posy = localOffset.dy -MediaQuery.of(context).padding.top - circleRadius - kToolbarHeight;
Here is the complete snippet
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;
double posx;
double posy;
final circleRadius = 7.5;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx =
localOffset.dx - MediaQuery.of(context).padding.left - circleRadius;
posy = localOffset.dy -MediaQuery.of(context).padding.top - circleRadius - kToolbarHeight;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: Stack(
children: <Widget>[
Container(
height: double.infinity,
width: double.infinity,
padding: EdgeInsets.all(10),
child: Image.asset("assets/img.jpg")),
Positioned(
child: BlinkingDot(circleRadius: circleRadius),
left: posx,
top: posy,
)
],
)));
}
}
class BlinkingDot extends StatefulWidget {
final double circleRadius;
const BlinkingDot({Key key, this.circleRadius}) : super(key: key);
#override
_BlinkingDotState createState() => _BlinkingDotState();
}
class _BlinkingDotState extends State<BlinkingDot>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
#override
void initState() {
_animationController =
new AnimationController(vsync: this, duration: Duration(seconds: 1));
_animationController.repeat();
super.initState();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animationController,
child: Container(
height: widget.circleRadius * 2,
width: widget.circleRadius * 2,
child: FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.redAccent,
)));
}
#override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
See the live demo here.

Flutter add custom SlideTransition to ModalRoute

in this below implemented code i can show dialog on bottom of page with Fade animation and now, i want to add SlideTransition to ModalRoute of this implementation to slide dialog from bottom, but i can't to do that
for example, what i want to have:
source code:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Open the popup window',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showPopup(context, _popupBody(), 'Popup Demo');
},
tooltip: 'Open Popup',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
showPopup(BuildContext context, Widget widget, String title, {BuildContext popupContext}) {
Navigator.push(
context,
PopupLayout(
top: MediaQuery.of(context).size.height * 0.300,
left: 0,
right: 0,
bottom: 0,
child: PopupContent(
content: Scaffold(
body: widget,
),
),
),
);
}
Widget _popupBody() {
return Container(
child: Text('This is a popup window'),
);
}
}
class PopupLayout extends ModalRoute {
double top;
double bottom;
double left;
double right;
Color bgColor;
final Widget child;
#override
Duration get transitionDuration => Duration(milliseconds: 200);
#override
bool get opaque => false;
#override
bool get barrierDismissible => false;
#override
Color get barrierColor => bgColor == null ? Colors.black.withOpacity(0.5) : bgColor;
#override
String get barrierLabel => null;
#override
bool get maintainState => false;
PopupLayout({Key key, this.bgColor, #required this.child, this.top, this.bottom, this.left, this.right});
#override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
if (top == null) this.top = 10;
if (bottom == null) this.bottom = 20;
if (left == null) this.left = 20;
if (right == null) this.right = 20;
return GestureDetector(
onTap: () {
// call this method here to hide soft keyboard
SystemChannels.textInput.invokeMethod('TextInput.hide');
},
child: Material(
// This makes sure that text and other content follows the material style
type: MaterialType.transparency,
//type: MaterialType.canvas,
// make sure that the overlay content is not cut off
child: SafeArea(
bottom: true,
child: _buildOverlayContent(context),
),
),
);
}
Widget _buildOverlayContent(BuildContext context) {
return Container(
margin: EdgeInsets.only(bottom: this.bottom, left: this.left, right: this.right, top: this.top),
child: SlideTransition(child: child),
);
}
#override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: animation, child: child);
}
}
class PopupContent extends StatefulWidget {
final Widget content;
PopupContent({
Key key,
this.content,
}) : super(key: key);
_PopupContentState createState() => _PopupContentState();
}
class _PopupContentState extends State<PopupContent> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: widget.content,
);
}
}
Here is a working example
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',
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>
with TickerProviderStateMixin {
void showPopup() {
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 400), vsync: this);
showDialog(
context: context,
builder: (_) => PopUp(
controller: controller,
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: showPopup,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class PopUp extends StatefulWidget {
final AnimationController controller;
PopUp({this.controller});
#override
State<StatefulWidget> createState() => PopUpState();
}
class PopUpState extends State<PopUp> {
Animation<double> opacityAnimation;
Tween<double> opacityTween = Tween<double>(begin: 0.0, end: 1.0);
Tween<double> marginTopTween = Tween<double>(begin: 600, end: 200);
Animation<double> marginTopAnimation;
AnimationStatus animationStatus;
#override
void initState() {
super.initState();
marginTopAnimation = marginTopTween.animate(widget.controller)
..addListener(() {
animationStatus = widget.controller.status;
if (animationStatus == AnimationStatus.dismissed) {
Navigator.of(context).pop();
}
if(this.mounted) {
setState(() {
});
}
});
widget.controller.forward();
}
#override
Widget build(BuildContext context) {
return FadeTransition(
opacity: opacityTween.animate(widget.controller),
child: GestureDetector(
onTap: () {
widget.controller.reverse();
},
child: Material(
color: Colors.transparent,
child: Container(
margin: EdgeInsets.only(
top: marginTopAnimation.value,
),
color: Colors.red,
child: Text("Container"),
),
),
),
);
}
#override
void dispose() {
widget.controller.dispose();
super.dispose();
}
}
UPDATE 1: Added Material as a child of Container to fix the barrier not dismissing bug.
UPDATE 2: Made a few more changes which reverses the animation when the barrier is dismissed.
NOTE: The screenshot does not reflect the updated changes. It is a demo of the original answer.