如何在颤动中添加主题切换动画?
Posted
技术标签:
【中文标题】如何在颤动中添加主题切换动画?【英文标题】:how to add animation for theme switching in flutter? 【发布时间】:2020-07-08 21:21:09 【问题描述】:我想在 flutter 中添加动画以将主题从浅色切换到深色,反之亦然,就像 telegram 一样:
telegram's switch animation
telegram's switch animation
source
在flutter中看不到任何方法,在flutter中有可能吗?
感谢任何答案
【问题讨论】:
【参考方案1】:这并不难,但你需要做几件事。
-
您需要创建自己的主题样式。我已经使用继承的小部件来做到这一点。 (如果您更改 ThemeData 小部件,它将为更改设置动画,而我们不需要它,这就是我将 Colors 保存在另一个类中的原因)
找到按钮(或者在我的情况下是切换器)坐标。
运行动画。
更新! 我已经使用简单的 api 将我们的代码转换为 a package。
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,
),
);
【讨论】:
感谢您的回答和您的好方法,我认为使用主题小部件是处理主页中小部件主题的更好方法,在您的 github 页面中创建了一个 PR,请查看。 我已将我们的代码转换为库:pub.dev/packages/animated_theme_switcher 是否有任何可用的反应替代品? 我正在尝试在我的 sliverappbar 中创建一个类似的动画,我不想切换主题我只想更改小部件,即我将创建一个抽屉。 这曾经可以正常工作。但是从颤振 2.8 开始,当切换主题发生时,整个应用程序都会被重建。它不像以前那样工作了【参考方案2】:虽然@Kherel 上面的回答效果很好,但我想分享我的这种效果版本。
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;
这是我的medium blog post 解释这种效果
你可以在这里找到完整的代码示例https://gist.github.com/maheshmnj/815642f5576ebef0a0747db6854c2a74
【讨论】:
以上是关于如何在颤动中添加主题切换动画?的主要内容,如果未能解决你的问题,请参考以下文章