Home > OS >  Why are my animations skipping steps sometimes?
Why are my animations skipping steps sometimes?

Time:02-06

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.

  • Related