how to add animation for theme switching in flutter? - flutter

I want to add animation for switching theme from light to dark or vice versa in flutter like telegram do :
telegram's switch animation
telegram's switch animation
source
can't see any way to do it in flutter, is it possible in flutter?
thx for any answer

It’s not hard, but you need to do several things.
You need to create your own theme styles. I’ve used inherited widget to do it. (If you change ThemeData widget it will animate the change, and we don’t need it, that’s why I’m saving Colors in another class)
Find the button (or in my case switcher) coordinates.
Run animation.
update!
I've converted our code to a package with simple api.
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BrandTheme(
child: Builder(builder: (context) {
return MaterialApp(
title: 'Flutter Demo',
theme: BrandTheme.of(context).themeData,
home: MyHomePage(),
);
}),
);
}
}
GlobalKey switherGlobalKey = GlobalKey();
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_controller.forward();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
int _counter = 0;
BrandThemeModel oldTheme;
Offset switcherOffset;
void _incrementCounter() {
setState(() {
_counter++;
});
}
_getPage(brandTheme, {isFirst = false}) {
return Scaffold(
backgroundColor: brandTheme.color2,
appBar: AppBar(
backgroundColor: brandTheme.color1,
title: Text(
'Flutter Demo Home Page',
style: TextStyle(color: brandTheme.textColor2),
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text(
'You have pushed the button this many times:',
style: TextStyle(
color: brandTheme.textColor1,
),
),
Text(
'$_counter',
style: TextStyle(color: brandTheme.textColor1, fontSize: 200),
),
Switch(
key: isFirst ? switherGlobalKey : null,
onChanged: (needDark) {
oldTheme = brandTheme;
BrandTheme.instanceOf(context).changeTheme(
needDark ? BrandThemeKey.dark : BrandThemeKey.light,
);
},
value: BrandTheme.of(context).brightness == Brightness.dark,
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(
Icons.add,
),
),
);
}
#override
void didUpdateWidget(Widget oldWidget) {
var theme = BrandTheme.of(context);
if (theme != oldTheme) {
_getSwitcherCoodinates();
_controller.reset();
_controller.forward().then(
(_) {
oldTheme = theme;
},
);
}
super.didUpdateWidget(oldWidget);
}
void _getSwitcherCoodinates() {
RenderBox renderObject = switherGlobalKey.currentContext.findRenderObject();
switcherOffset = renderObject.localToGlobal(Offset.zero);
}
#override
Widget build(BuildContext context) {
var brandTheme = BrandTheme.of(context);
if (oldTheme == null) {
return _getPage(brandTheme, isFirst: true);
}
return Stack(
children: <Widget>[
if(oldTheme != null) _getPage(oldTheme),
AnimatedBuilder(
animation: _controller,
child: _getPage(brandTheme, isFirst: true),
builder: (_, child) {
return ClipPath(
clipper: MyClipper(
sizeRate: _controller.value,
offset: switcherOffset.translate(30, 15),
),
child: child,
);
},
),
],
);
}
}
class MyClipper extends CustomClipper<Path> {
MyClipper({this.sizeRate, this.offset});
final double sizeRate;
final Offset offset;
#override
Path getClip(Size size) {
var path = Path()
..addOval(
Rect.fromCircle(center: offset, radius: size.height * sizeRate),
);
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}
class BrandTheme extends StatefulWidget {
final Widget child;
BrandTheme({
Key key,
#required this.child,
}) : super(key: key);
#override
BrandThemeState createState() => BrandThemeState();
static BrandThemeModel of(BuildContext context) {
final inherited =
(context.dependOnInheritedWidgetOfExactType<_InheritedBrandTheme>());
return inherited.data.brandTheme;
}
static BrandThemeState instanceOf(BuildContext context) {
final inherited =
(context.dependOnInheritedWidgetOfExactType<_InheritedBrandTheme>());
return inherited.data;
}
}
class BrandThemeState extends State<BrandTheme> {
BrandThemeModel _brandTheme;
BrandThemeModel get brandTheme => _brandTheme;
#override
void initState() {
final isPlatformDark =
WidgetsBinding.instance.window.platformBrightness == Brightness.dark;
final themeKey = isPlatformDark ? BrandThemeKey.dark : BrandThemeKey.light;
_brandTheme = BrandThemes.getThemeFromKey(themeKey);
super.initState();
}
void changeTheme(BrandThemeKey themeKey) {
setState(() {
_brandTheme = BrandThemes.getThemeFromKey(themeKey);
});
}
#override
Widget build(BuildContext context) {
return _InheritedBrandTheme(
data: this,
child: widget.child,
);
}
}
class _InheritedBrandTheme extends InheritedWidget {
final BrandThemeState data;
_InheritedBrandTheme({
this.data,
Key key,
#required Widget child,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(_InheritedBrandTheme oldWidget) {
return true;
}
}
ThemeData defaultThemeData = ThemeData(
floatingActionButtonTheme: FloatingActionButtonThemeData(
shape: RoundedRectangleBorder(),
),
);
class BrandThemeModel extends Equatable {
final Color color1;
final Color color2;
final Color textColor1;
final Color textColor2;
final ThemeData themeData;
final Brightness brightness;
BrandThemeModel({
#required this.color1,
#required this.color2,
#required this.textColor1,
#required this.textColor2,
#required this.brightness,
}) : themeData = defaultThemeData.copyWith(brightness: brightness);
#override
List<Object> get props => [
color1,
color2,
textColor1,
textColor2,
themeData,
brightness,
];
}
enum BrandThemeKey { light, dark }
class BrandThemes {
static BrandThemeModel getThemeFromKey(BrandThemeKey themeKey) {
switch (themeKey) {
case BrandThemeKey.light:
return lightBrandTheme;
case BrandThemeKey.dark:
return darkBrandTheme;
default:
return lightBrandTheme;
}
}
}
BrandThemeModel lightBrandTheme = BrandThemeModel(
brightness: Brightness.light,
color1: Colors.blue,
color2: Colors.white,
textColor1: Colors.black,
textColor2: Colors.white,
);
BrandThemeModel darkBrandTheme = BrandThemeModel(
brightness: Brightness.dark,
color1: Colors.red,
color2: Colors.black,
textColor1: Colors.blue,
textColor2: Colors.yellow,
);
class ThemeRoute extends PageRouteBuilder {
ThemeRoute(this.widget)
: super(
pageBuilder: (
context,
animation,
secondaryAnimation,
) =>
widget,
transitionsBuilder: transitionsBuilder,
);
final Widget widget;
}
Widget transitionsBuilder(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
var _animation = Tween<double>(
begin: 0,
end: 100,
).animate(animation);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(animation),
child: Container(
child: child,
),
);
}

While #Kherel's answer above works perfectly fine, I wanted to share my version of this effect.
class DarkTransition extends StatefulWidget {
const DarkTransition(
{required this.childBuilder,
Key? key,
this.offset = Offset.zero,
this.themeController,
this.radius,
this.duration = const Duration(milliseconds: 400),
this.isDark = false})
: super(key: key);
/// Deinfe the widget that will be transitioned
/// int index is either 1 or 2 to identify widgets, 2 is the top widget
final Widget Function(BuildContext, int) childBuilder;
/// the current state of the theme
final bool isDark;
/// optional animation controller to controll the animation
final AnimationController? themeController;
/// centeral point of the circular transition
final Offset offset;
/// optional radius of the circle defaults to [max(height,width)*1.5])
final double? radius;
/// duration of animation defaults to 400ms
final Duration? duration;
#override
_DarkTransitionState createState() => _DarkTransitionState();
}
class _DarkTransitionState extends State<DarkTransition>
with SingleTickerProviderStateMixin {
#override
void dispose() {
_darkNotifier.dispose();
super.dispose();
}
final _darkNotifier = ValueNotifier<bool>(false);
#override
void initState() {
super.initState();
if (widget.themeController == null) {
_animationController =
AnimationController(vsync: this, duration: widget.duration);
} else {
_animationController = widget.themeController!;
}
}
double _radius(Size size) {
final maxVal = max(size.width, size.height);
return maxVal * 1.5;
}
late AnimationController _animationController;
double x = 0;
double y = 0;
bool isDark = false;
// bool isBottomThemeDark = true;
bool isDarkVisible = false;
late double radius;
Offset position = Offset.zero;
ThemeData getTheme(bool dark) {
if (dark)
return ThemeData.dark();
else
return ThemeData.light();
}
#override
void didUpdateWidget(DarkTransition oldWidget) {
super.didUpdateWidget(oldWidget);
_darkNotifier.value = widget.isDark;
if (widget.isDark != oldWidget.isDark) {
if (isDark) {
_animationController.reverse();
_darkNotifier.value = false;
} else {
_animationController.reset();
_animationController.forward();
_darkNotifier.value = true;
}
position = widget.offset;
}
if (widget.radius != oldWidget.radius) {
_updateRadius();
}
if (widget.duration != oldWidget.duration) {
_animationController.duration = widget.duration;
}
}
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
_updateRadius();
}
void _updateRadius() {
final size = MediaQuery.of(context).size;
if (widget.radius == null)
radius = _radius(size);
else
radius = widget.radius!;
}
#override
Widget build(BuildContext context) {
isDark = _darkNotifier.value;
Widget _body(int index) {
return ValueListenableBuilder<bool>(
valueListenable: _darkNotifier,
builder: (BuildContext context, bool isDark, Widget? child) {
return Theme(
data: index == 2
? getTheme(!isDarkVisible)
: getTheme(isDarkVisible),
child: widget.childBuilder(context, index));
});
}
return AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget? child) {
return Stack(
children: [
_body(1),
ClipPath(
clipper: CircularClipper(
_animationController.value * radius, position),
child: _body(2)),
],
);
});
}
}
class CircularClipper extends CustomClipper<Path> {
const CircularClipper(this.radius, this.center);
final double radius;
final Offset center;
#override
Path getClip(Size size) {
final Path path = Path();
path.addOval(Rect.fromCircle(radius: radius, center: center));
return path;
}
#override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
return true;
}
}
Here's my medium blog post explaining this effect
You can find a complete code sample with Usage here https://gist.github.com/maheshmnj/815642f5576ebef0a0747db6854c2a74

Related

Flutter How to get size of dynamic widget

What I've done:
The black rectangle is the size of the canvas.
const double radius = 50;
class TableShape extends StatelessWidget {
final String name;
final Color color;
const TableShape({
Key? key,
required this.name,
required this.color,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap:(){debugPrint("ok");},
child: LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final textPainter = TextPainter(
text: TextSpan(
text: name,
style: const TextStyle(fontFamily: 'Graphik', fontSize: 30, color: Colors.white),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center
);
textPainter.layout(maxWidth: maxWidth);
return CustomPaint(
size: Size(textPainter.width>radius*2?textPainter.width:radius*2, radius*2),
painter: MyPainter(color: color, txt: textPainter),
);
})
);
}
}
class MyPainter extends CustomPainter {
TextPainter txt;
Color color;
MyPainter({
required this.txt,
required this.color,
});
#override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height));
var paint = Paint()..color = color;
bool txtLarger = txt.width>radius*2;
canvas.drawCircle(Offset(txtLarger?txt.width/2:radius,radius), radius, paint);
//table name:
txt.paint(canvas, Offset(txtLarger?0:radius-txt.width/2,radius-txt.height/2));
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
#override
bool hitTest(Offset position) {
return sqrt(pow(txt.width/2-position.dx,2)+pow(radius-position.dy,2)) <= radius;
}
}
I need to get the width because I place the widget on my screen according to its width. The width is dynamic: the bigger the text, the wider the canvas. Is it possible ? Or maybe you have an other approach to get this widget than the way I did ?
get widget size by global key:
final GlobalKey _widgetKey = GlobalKey();
Size _getSize(GlobalKey key){
final State state = key.currentState;
final BuildContext context = key.currentContext;
final RenderBox box = state.context.findRenderObject();
return context.size;
}
Widget build(BuildContext context) {
return GestureDetector(
key: _widgetKey,
onTap:(){_getSize(_widgetKey);},
child: LayoutBuilder(
builder: (context, constraints) {
Use GlobalKey to find RenderBox then get the size. Remember you need to make sure the widget was rendered.
Example:
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) => const MaterialApp(home: Home());
}
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
HomeState createState() => HomeState();
}
class HomeState extends State<Home> {
var key = GlobalKey();
Size? redboxSize;
#override
void initState() {
WidgetsBinding.instance?.addPostFrameCallback((_) {
setState(() {
redboxSize = getRedBoxSize(key.currentContext!);
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Column(
children: [
SizedBox(
height: 100,
child: Center(
child: Container(
key: key,
child: const Text('Hello oooooooooooooooo'),
color: Colors.redAccent,
),
),
),
if (redboxSize != null) Text('Redbox size: $redboxSize')
],
),
);
}
Size getRedBoxSize(BuildContext context) {
final box = context.findRenderObject() as RenderBox;
return box.size;
}
}

any way to have multiple tabs(screens) in flutter with a preview like in a browsers do for example?

any idea where to start? what I want to archive is dynamic tabs(can open and close) with ability to see tab overview like in any browser.
It is very easy to do with custom widget,
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(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> data = ['Page 0', 'Page 1', 'Page 2'];
int initPosition = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomTabView(
initPosition: initPosition,
itemCount: data.length,
tabBuilder: (context, index) => Tab(child: Row(children: [Text("${data[index]}"),IconButton(onPressed: (){
setState(() {
data.removeAt(index);
});
},icon: Icon(Icons.face),),],),),
pageBuilder: (context, index) => Center(child: Text(data[index])),
onPositionChange: (index){
print('current position: $index');
initPosition = index;
},
onScroll: (position) => print('$position'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
data.add('Page ${data.length}');
});
},
child: Icon(Icons.add),
),
);
}
}
/// Implementation
class CustomTabView extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final Widget stub;
final ValueChanged<int> onPositionChange;
final ValueChanged<double> onScroll;
final int initPosition;
CustomTabView({
#required this.itemCount,
#required this.tabBuilder,
#required this.pageBuilder,
this.stub,
this.onPositionChange,
this.onScroll,
this.initPosition,
});
#override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
TabController controller;
int _currentCount;
int _currentPosition;
#override
void initState() {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
_currentCount = widget.itemCount;
super.initState();
}
#override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 :
_currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_){
if(mounted) {
widget.onPositionChange(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.itemCount < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll(controller.animation.value);
}
}
}
i put face Icon as close ;).
You should use StatefulWidget on TabBarView.
The main idea is using setState when you do anything dynamically.
Design your custom Tab.
When new tab is added, store data into list. Method: add
Remove specific item when close button is tapped. Method: remove
class BrowserLikedTabView extends StatefulWidget {
BrowserLikedTabView({Key? key}) : super(key: key);
#override
BrowserLikedTabViewState createState() => BrowserLikedTabViewState();
}
class BrowserLikedTabViewState extends State<BrowserLikedTabView> {
final data = <String>[];
#override
Widget build(BuildContext context) {
final tabBar = TabBar(tabs: [
for (var item in data)
// pass method `remove` to the tab
Tab(child: BrowserLikedTab(item: item, onPressed: remove))
]);
final tabView = TabBarView(children:[ for (var item in data)
Text(item)
]);
return Column(children: <Widget>[
tabBar,
Expanded(child: tabView),
]);
}
void add(String value) {
// notice the `setState` method
setState(() => data.add(value));
}
void remove(String value) {
/// In practice, you should have the index of `item`.
/// Use `removeAt` for better performance,
/// since `removeWhere` will loop all the element in array.
setState(() => data.removeWhere((e) => e == value));
}
}
The tab design is something likes:
class BrowserLikedTab extends StatelessWidget {
final String item;
final void Function(String) onPressed;
const BrowserLikedTab({this.item, this.onPressed});
#override
Widget build(BuildContext context) {
/// You may need MainAxisAlignment.spaceBetween to
/// move your icon in correct place.
/// Try the options to fit your design.
return Row(children: [
IconButton(
onPressed: () => onPressed(item),
icon: Icon(Icons.cancel),
),
// You may need truncate overflow for text.
Text(item),
]);
}
}
The pattern I used for loop is recommended in Dart document.
TabBar property isScrollable is a property you might want to look for if you have dynamic tabs.
Now you can call the add outside the TabBarView.
class AddButton extends StatelessWidget {
final tabBarView = GlobalKey<BrowserLikedTabViewState>();
AddButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children:[
TextButton(
onPressed: () => tabBarView.currentState?.add('new item'),
child: Text('Add now'),
),
Expanded(child: BrowserLikedTabView(key: tabBarView)),
]),
);
}
}

Flutter Reworked question: Issue sharing states between widget with button and widget with countdown timer

I am trying since some days to connect an animated stateful child widget with a countdown timer to the parent stateful widget with the user interaction. I found this answer from Andrey on a similar question (using Tween which I do not) that already helped a lot, but I still don't get it to work. My assumption is, the child's initState could be the reason. The timer's code comes from here.
I have removed quite some code including some referenced functions/classes. This should provide a clearer picture on the logic:
In MainPageState I declare and init the _controller of the animation
In MainPageState I call the stateless widget CreateKeypad hosting among others the "go" key
When go is clicked, this event is returned to MainPageState and _controller.reverse(from: 1.0); executed
In MainPageState I call the stateful widget CountDownTimer to render the timer
In _CountDownTimerState I am not sure if my initState is correct
In _CountDownTimerState I build the animation with CustomTimerPainter from the timer code source
The animation shall render a white donut and a red, diminishing arc on top. However, I only see the white donut, not the red timer's arc. Any hint is highly appreciated.
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
AnimationController _controller;
var answer="0", correctAnswer = true, result = 0;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 7));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
),
child: SafeArea(
child: Container(
child: Column(
children: <Widget>[
CreateKeypad( // creates a keypad with a go button. when go is clicked, countdown shall start
prevInput: int.parse((answer != null ? answer : "0")),
updtedInput: (int val) {
setState(() => answer = val.toString());
},
goSelected: () {
setState(() {
if (answer == result.toString()) {
correctAnswer = true;
}
final problem = createProblem();
result = problem.result;
});
_controller.reverse(from: 1.0); // start the countdown animation
Future.delayed(const Duration(milliseconds: 300,),
() => setState(() => correctAnswer = true));
},
),
CountDownTimer(_controller), // show countdown timer
]
),
),
)
);
}
}
// CREATE KEYPAD - all keys but "1! and "go" removed
class CreateKeypad extends StatelessWidget {
final int prevInput;
final VoidCallback goSelected;
final Function(int) updtedInput;
CreateKeypad({#required this.prevInput, #required this.updtedInput, this.goSelected});
#override
Widget build(BuildContext context) {
return Row(
children: <Widget> [
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0, height: 80.0,
child: CupertinoButton(
child: Text("1", style: TextStyle(color: CupertinoColors.black)),
onPressed: () {
updtedInput(1);
},
),
),
),
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0, height: 80.0,
child: CupertinoButton(
child: Text("Go!", style: TextStyle(color: CupertinoColors.black)),
onPressed: () => goSelected(),
),
),
),
],
),
]
);
}
}
// CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
class CountDownTimer extends StatefulWidget {
CountDownTimer(this._controller);
final AnimationController _controller;
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer> with TickerProviderStateMixin {
#override
void initState() {
super.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
String get timerString {
Duration duration = widget._controller.duration * widget._controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60)
.toString()
.padLeft(2, '0')}';
}
#override
Widget build(BuildContext context) {
return Container(
child: AnimatedBuilder(
animation: widget._controller,
builder:
(BuildContext context, Widget child) {
return CustomPaint(
painter: CustomTimerPainter( // this draws a white donut and a red diminishing arc on top
animation: widget._controller,
backgroundColor: Colors.white,
color: Colors.red,
));
},
),
);
}
}
You can copy paste run full code below
Step 1: You can put controller inside CountDownTimerState
Step 2: Use GlobalKey
CountDownTimer(key: _key)
Step 3: Call function start() inside _CountDownTimerState with _key.currentState
goSelected: () {
setState(() {
...
_controller.reverse(from: 10.0); // start the countdown animation
final _CountDownTimerState _state = _key.currentState;
_state.start();
...
class _CountDownTimerState extends State<CountDownTimer>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
super
.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
...
void start() {
setState(() {
_controller.reverse(from: 1.0);
});
}
working demo
full code
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;
class MainPage extends StatefulWidget {
MainPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> with TickerProviderStateMixin {
AnimationController _controller;
var answer = "0", correctAnswer = true, result = 0;
GlobalKey _key = GlobalKey();
#override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
//navigationBar: CupertinoNavigationBar(),
child: SafeArea(
child: Container(
color: Colors.blue,
child: Column(children: <Widget>[
CreateKeypad(
// creates a keypad with a go button. when go is clicked, countdown shall start
prevInput: int.parse((answer != null ? answer : "0")),
updtedInput: (int val) {
setState(() => answer = val.toString());
},
goSelected: () {
setState(() {
if (answer == result.toString()) {
correctAnswer = true;
}
/*final problem = createProblem();
result = problem.result;*/
});
print("go");
_controller.reverse(from: 10.0); // start the countdown animation
final _CountDownTimerState _state = _key.currentState;
_state.start();
/* Future.delayed(
const Duration(
milliseconds: 300,
),
() => setState(() => correctAnswer = true));*/
},
),
Container(
height: 400,
width: 400,
child: CountDownTimer(key: _key)), // show countdown timer
]),
),
));
}
}
// CREATE KEYPAD - all keys but "1! and "go" removed
class CreateKeypad extends StatelessWidget {
final int prevInput;
final VoidCallback goSelected;
final Function(int) updtedInput;
CreateKeypad(
{#required this.prevInput, #required this.updtedInput, this.goSelected});
#override
Widget build(BuildContext context) {
return Row(children: <Widget>[
Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0,
height: 80.0,
child: CupertinoButton(
child:
Text("1", style: TextStyle(color: CupertinoColors.black)),
onPressed: () {
updtedInput(1);
},
),
),
),
Padding(
padding: const EdgeInsets.all(2.0),
child: SizedBox(
width: 80.0,
height: 80.0,
child: CupertinoButton(
child:
Text("Go!", style: TextStyle(color: CupertinoColors.black)),
onPressed: () => goSelected(),
),
),
),
],
),
]);
}
}
// CREATE COUNTDOWN https://medium.com/flutterdevs/creating-a-countdown-timer-using-animation-in-flutter-2d56d4f3f5f1
class CountDownTimer extends StatefulWidget {
CountDownTimer({Key key}) : super(key: key);
//final AnimationController _controller;
#override
_CountDownTimerState createState() => _CountDownTimerState();
}
class _CountDownTimerState extends State<CountDownTimer>
with TickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 7));
super
.initState(); // here I have some difference to Andrey's answer because I do not use Tween
}
String get timerString {
Duration duration = _controller.duration * _controller.value;
return '${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
}
void start() {
setState(() {
_controller.reverse(from: 1.0);
});
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return CustomPaint(
painter: CustomTimerPainter(
// this draws a white donut and a red diminishing arc on top
animation: _controller,
backgroundColor: Colors.green,
color: Colors.red,
));
},
),
);
}
}
class CustomTimerPainter extends CustomPainter {
CustomTimerPainter({
this.animation,
this.backgroundColor,
this.color,
}) : super(repaint: animation);
final Animation<double> animation;
final Color backgroundColor, color;
#override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = backgroundColor
..strokeWidth = 10.0
..strokeCap = StrokeCap.butt
..style = PaintingStyle.stroke;
canvas.drawCircle(size.center(Offset.zero), size.width / 2.0, paint);
paint.color = color;
double progress = (1.0 - animation.value) * 2 * math.pi;
//print("progress ${progress}");
canvas.drawArc(Offset.zero & size, math.pi * 1.5, -progress, false, paint);
}
#override
bool shouldRepaint(CustomTimerPainter old) {
//print(animation.value);
return true;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MainPage(),
);
}
}

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 manage dx of Offset by swiping to right or left

in Flutter application i want to divide width of screen to 10 part and when user swipe to right or left i could detect each part of this swipe, for example
after divide screen to 10 part i have a variable named screenParts as double which that has 0.0 by default, when user swipe to right the variable value should be plus 1 part and swipe to left should be minus the variable value minus
this variable value should be between 0.0 and 1, you could consider Tween<double>
double screenParts = 0.0;
final double screenWidth = MediaQuery.of(context).size.width / 10;
i want to use this value inside into this part of code:
return GestureDetector(
onPanUpdate: (details) {
if (details.delta.dx > 0) {
if (screenParts / screenWidth == 0) screenParts = screenParts += 0.1;
} else if (details.delta.dx < 0) {
if (screenParts / screenWidth == 0) screenParts = screenParts -= 0.1;
}
setState(() {});
},
child: SafeArea(
child: FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Curves.fastLinearToSlowEaseIn),
child: SlideTransition(
position: Tween<Offset>(
begin: animateDirection,
end: Offset(screenParts, 0), //<---- this part
).animate(CurvedAnimation(
parent: animation,
curve: Curves.fastLinearToSlowEaseIn,
)),
// ignore: void_checks
child: Material(elevation: 8.0, child: child)),
),
),
);
it means i try to manage dx of Offset by screenParts value with swiping to right or left
is this what you are looking for?
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: Scaffold(
body: SafeArea(
child: MyHomePage(),
),
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
double position = 0.0;
#override
void initState() {
super.initState();
}
void handleXChange(double deltaX) {
setState(() {
position = deltaX / MediaQuery.of(context).size.width;
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Text(position.toString()),
DragArea(
handleXChange: handleXChange,
),
],
);
}
}
class DragArea extends StatefulWidget {
const DragArea({Key key, #required this.handleXChange}) : super(key: key);
final void Function(double newX) handleXChange;
#override
_DragAreaState createState() => _DragAreaState();
}
class _DragAreaState extends State<DragArea> {
double initX;
void onPanStart(DragStartDetails details) {
initX = details.globalPosition.dx;
}
void onPanUpdate(DragUpdateDetails details) {
var x = details.globalPosition.dx;
var deltaX = x - initX;
widget.handleXChange(deltaX);
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: onPanStart,
onPanUpdate: onPanUpdate,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueAccent,
),
),
),
);
}
}