I'm trying to animate a property of a CustomPainter widget that get's its values from an item provided by a Riverpod Notifier.
In this broken down example of my real app, I trigger the Notifier by changing the data of the second Item which then should resize the circle in front of the ListTile.
It seems to work for changes where the value increases but when the value decreases, it often jumps over parts of the animation.
I'm not sure if I'm doing the whole animation part right here.
The code is also on Dartpad: https://dartpad.dev/?id=e3916b47603988efabd7a08712b98287
// ignore_for_file: avoid_print
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod animated CustomPainter',
home: const Example3(),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.orange,
brightness: MediaQueryData.fromWindow(WidgetsBinding.instance.window).platformBrightness,
surface: Colors.deepOrange[600],
),
),
);
}
}
class ItemPainter extends CustomPainter {
final double value;
ItemPainter(this.value);
final itemPaint = Paint()..color = Colors.orange;
@override
void paint(Canvas canvas, Size size) {
// draw a circle with a size depending on the value
double radius = size.width / 10 * value / 2;
canvas.drawCircle(
Offset(
size.width / 2,
size.height / 2,
),
radius,
itemPaint,
);
}
@override
bool shouldRepaint(covariant ItemPainter oldDelegate) => oldDelegate.value != value;
}
CustomPaint itemIcon(double value) {
return CustomPaint(
painter: ItemPainter(value),
size: const Size(40, 40),
);
}
@immutable
class Item {
const Item({required this.id, required this.value});
final String id;
final double value;
}
// notifier that provides a list of items
class ItemsNotifier extends Notifier<List<Item>> {
@override
List<Item> build() {
return [
const Item(id: 'A', value: 1.0),
const Item(id: 'B', value: 5.0),
const Item(id: 'C', value: 10.0),
];
}
void randomize(String id) {
// replace the state with a new list of items where the value is randomized from 0.0 to 10.0
state = [
for (final item in state)
if (item.id == id) Item(id: item.id, value: Random().nextInt(100).toDouble() / 10.0) else item,
];
}
}
class AnimatedItem extends StatefulWidget {
final Item item;
const AnimatedItem(this.item, {super.key});
@override
State<AnimatedItem> createState() => _AnimatedItemState();
}
class _AnimatedItemState extends State<AnimatedItem> with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late Animation<double> animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
value: widget.item.value,
vsync: this,
duration: const Duration(milliseconds: 3000),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
void didUpdateWidget(AnimatedItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.item.value != widget.item.value) {
print('didUpdateWidget: ${oldWidget.item.value} -> ${widget.item.value}');
_animationController.value = oldWidget.item.value / 10;
_animationController.animateTo(widget.item.value / 10);
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return itemIcon((widget.item.value * _animationController.value));
},
);
}
}
final itemsProvider = NotifierProvider<ItemsNotifier, List<Item>>(() => ItemsNotifier());
class Example3 extends ConsumerWidget {
const Example3({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(itemsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Animated CustomPainter Problem'),
),
// iterate over the item list in ItemsNotifier
body: ListView.separated(
separatorBuilder: (context, index) => const Divider(),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items.elementAt(index);
return ListTile(
key: Key(item.id),
leading: AnimatedItem(item),
title: Text('${item.value}'),
);
},
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
ref.read(itemsProvider.notifier).randomize('B'); // randomize the value of the second item
},
child: const Icon(Icons.change_circle),
),
],
),
);
}
}
CodePudding user response:
Your issues lie completely in your implementation of the AnimationController
. I don't really understand your intent with the original code, but the reason it jumped was because your were doing widget.item.value * _animationController.value
in the build
function. When you updated your item's value, it suddenly changed widget.item.value
, creating the jump, then animating a small change with the AnimationController
.
This code will work:
class _AnimatedItemState extends State<AnimatedItem> with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late Animation<double> animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
value: widget.item.value,
vsync: this,
duration: const Duration(milliseconds: 3000),
lowerBound: 0.0,
upperBound: 10.0
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
void didUpdateWidget(AnimatedItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.item.value != widget.item.value) {
print('didUpdateWidget: ${oldWidget.item.value} -> ${widget.item.value}');
_animationController.animateTo(widget.item.value);
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return itemIcon(_animationController.value);
},
);
}
}
This code adjusts the bounds of your AnimationController
to accommodate the range of values you want to animate and only uses _animationController.value
in build
. I also removed a redundant line from didUpdateWidget
, but that had no effect on functionality.