Flutter How to get size of dynamic widget - flutter

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

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

How to increase the width of a PopupMenuEntry in flutter

I created a horizontal popup menu but i cant increase the width of the popupMenuEntry because the default width is too short for my use case. I tried to set the child of my popupMenuEntry to double.infinity and MediaQuery.of(context).sized.width but nothing is happening..
It looks like ok in the emulator but when i tested out in the actual device, its too small thats why i need to resize it to at least 90-95% of the screen width.
here is my implementation of my popupMenu.
class ReactionPopupMenu extends StatefulWidget {
const ReactionPopupMenu(
{Key? key, required this.onSelect, required this.child, this.onTap})
: super(key: key);
final void Function(String) onSelect;
final Widget child;
final VoidCallback? onTap;
#override
State<ReactionPopupMenu> createState() => _ReactionPopupMenuState();
}
class _ReactionPopupMenuState extends State<ReactionPopupMenu>
with CustomPopupMenu {
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onTap,
onTapDown: storePosition,
onLongPress: () => this.showMenu(
context: context,
shape: const StadiumBorder(),
items: [
CustomedPopupEntry(
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.max,
children: [
for (var i = 0; i < kEmojies.length; i++)
ReactiveEmoji(
emoji: kEmojies[i],
onTap: () =>
Navigator.pop(context, kValue[i]))
]),
),
)
]).then(
(value) => value == null ? null : widget.onSelect(value)),
child: widget.child);
}
}
mixin CustomPopupMenu<T extends StatefulWidget> on State<T> {
Offset? _tapPosition;
/// Pass this method to an onTapDown parameter to record the tap position.
void storePosition(TapDownDetails details) =>
_tapPosition = details.globalPosition;
/// Use this method to show the menu.
// ignore: avoid_shadowing_type_parameters
Future<T?> showMenu<T>({
required BuildContext context,
required List<PopupMenuEntry<T>> items,
T? initialValue,
double? elevation,
String? semanticLabel,
ShapeBorder? shape,
Color? color,
bool captureInheritedThemes = true,
bool useRootNavigator = false,
}) {
// final RenderObject? overlay =
// Overlay.of(context)!.context.findRenderObject();
return material.showMenu<T>(
context: context,
position: RelativeRect.fromLTRB(
_tapPosition!.dx,
_tapPosition!.dy,
_tapPosition!.dx,
_tapPosition!.dy,
),
items: items,
initialValue: initialValue,
elevation: elevation,
semanticLabel: semanticLabel,
shape: shape,
color: color,
useRootNavigator: useRootNavigator,
);
}
}
class CustomedPopupEntry<T> extends PopupMenuEntry<T> {
const CustomedPopupEntry({Key? key, required this.child}) : super(key: key);
final Widget child;
#override
State<StatefulWidget> createState() => _CustomedPopupEntryState();
#override
double get height => 100;
#override
bool represents(T? value) => false;
}
class _CustomedPopupEntryState extends State<CustomedPopupEntry> {
#override
Widget build(BuildContext context) => widget.child;
}
class ReactiveEmoji extends StatelessWidget {
const ReactiveEmoji({Key? key, required this.emoji, required this.onTap})
: super(key: key);
final String emoji;
final VoidCallback onTap;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: CircleAvatar(
backgroundColor: Colors.transparent,
radius: MediaQuery.of(context).size.width * 0.046,
backgroundImage: AssetImage(emoji),
),
);
}
}
Your solution is not with adding more width to popUpMenu. you should make your emoji list scrollable, so when use it in small device it could be scrolled. So wrap your emoji row with SingleChildScrollView and set its scrollDirection to horizontal.

Why red lines painted?

I expected size to be a child widget's size, and the result to be a whole black screen.
But it seems not. My _TestBox doesn't cover all child area. Why this happens?
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: HomePage());
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SingleChildScrollView(
child: Column(
children: [
SizedBox(height: 300),
_TestBox(
child: Container(width: 200, height: 200, color: Colors.red),
),
SizedBox(height: 800),
],
),
));
}
}
#immutable
class _TestBox extends SingleChildRenderObjectWidget {
const _TestBox({Widget? child}) : super(child: child);
#override
_TestFilter createRenderObject(BuildContext context) {
return _TestFilter();
}
#override
void updateRenderObject(BuildContext context, _TestFilter filter) {
filter.markNeedsPaint();
}
}
class _TestFilter extends RenderProxyBox {
#override
ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?;
#override
bool get alwaysNeedsCompositing => child != null;
#override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
assert(needsCompositing);
const color = Colors.black;
layer ??= ShaderMaskLayer();
layer!
..shader = LinearGradient(
colors: [color, color],
).createShader(Rect.fromLTWH(0, 0, 1, 1))
..maskRect = offset & size
// The following has no problem.
// ..maskRect = (offset & size).inflate(1)
..blendMode = BlendMode.srcIn;
context.pushLayer(layer!, super.paint, offset);
} else {
layer = null;
}
}
}
It came out as device specific issue.

Transparent background behind ClipPath

So, I'm pretty new to flutter, I'm trying to have a AppBar that looks like this :
And I'm actually able to do it using ClipPath like this :
class Header extends StatelessWidget {
final String page;
const Header({required this.page});
#override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top;
return ClipPath(
child: HeaderContent(page: page, padding: topPadding),
clipper: Clipper(page: page));
}
}
class HeaderContent extends StatelessWidget {
final String page;
final double padding;
const HeaderContent({required this.page, required this.padding});
#override
Widget build(BuildContext context) {
final int headerHeight = page == "inscription" ? 250 : 150;
return Container(
height: headerHeight + padding,
padding: EdgeInsets.all(15),
width: double.infinity,
color: Theme.of(context).primaryColor,
child: AppBar(
title: Text("NOTIFICATIONS", style: Theme.of(context).textTheme.headline6),
centerTitle: true,
leading: Icon(
Icons.arrow_back,
size: 40,
),
),
);
}
}
class Clipper extends CustomClipper<Path> {
final String page;
const Clipper({required this.page});
#override
Path getClip(Size size) {
final Path path = Path();
selectHeaderClip(size, page, path);
return path;
}
#override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
Where the selectHeaderClip is a custom function that return the appropriate clip for a given page.
However, when I have a scrollable page (list of notifications for example), I see the notifications hiding too early as if there was a white rectangular container underneath my appBar. So my question basically is: how can I make that container transparent ?
Ps: sorry for the bad formulation of the question, don't hesitate to edit.

how to add animation for theme switching in 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