Skip to content

Instantly share code, notes, and snippets.

@maheshmnj
Last active March 29, 2024 13:55
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save maheshmnj/815642f5576ebef0a0747db6854c2a74 to your computer and use it in GitHub Desktop.
Save maheshmnj/815642f5576ebef0a0747db6854c2a74 to your computer and use it in GitHub Desktop.
Sample code showing dark mode transition similar to Telegram in flutter with circular transition. Blog Post: https://maheshmnj.medium.com/leveraging-clippaths-in-flutter-a5f34c795ae5
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;
}
}
@maheshmnj
Copy link
Author

maheshmnj commented Aug 29, 2021

Widget Usage

import 'dart:math';
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: DarkSample());
  }
}

class DarkSample extends StatefulWidget {
  @override
  _DarkSampleState createState() => _DarkSampleState();
}

class _DarkSampleState extends State<DarkSample> {
  bool isDark = false;
  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final circleOffset = Offset(size.width - 20, size.height - 20);
    return DarkTransition(
      childBuilder: (context, x) => MyHomePage(
        title: 'Flutter Demo Home Page',
        onIncrement: () {
          setState(() {
            isDark = !isDark;
          });
        },
      ),
      offset: circleOffset,
      isDark: isDark,
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;
  final Function()? onIncrement;

  const MyHomePage({
    Key? key,
    this.onIncrement,
    required this.title,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    widget.onIncrement!();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Output

ezgif com-gif-maker

@bahaa599599
Copy link

hello , how can i save the value using shard preferences

@maheshmnj
Copy link
Author

@bahaa599599 You can try using this class https://stackoverflow.com/a/71135052/8253662 to save and retrieve theme from your shared preferences

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment