← Назад к вопросам

Расскажи про самую сложную анимацию которую реализовывал в Flutter

1.3 Junior🔥 141 комментариев
#Flutter виджеты#Анимации

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Сложная анимация в Flutter: Интерактивный Graph Visualizer

Я реализовывал анимированную визуализацию графа зависимостей пакетов в Flutter приложении. Это была действительно сложная задача, требующая комбинации нескольких техник.

Особенности реализации

1. Многоуровневая анимация

  • Анимировались позиции узлов (nodes) по Force-Directed Graph алгоритму
  • Рёбра (edges) между узлами должны были плавно следовать за перемещением
  • Появление/исчезновение элементов с fade-in/fade-out эффектами
  • Масштабирование и поворот узлов при интерактивном взаимодействии

2. Интерактивность + анимация

class AnimatedGraphNode extends StatefulWidget {
  final Offset position;
  final String label;
  final Duration animationDuration;

  const AnimatedGraphNode({
    required this.position,
    required this.label,
    this.animationDuration = const Duration(milliseconds: 500),
  });

  @override
  State<AnimatedGraphNode> createState() => _AnimatedGraphNodeState();
}

class _AnimatedGraphNodeState extends State<AnimatedGraphNode>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Offset> _positionAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: widget.animationDuration);
    _setupAnimation();
  }

  void _setupAnimation() {
    _positionAnimation = Tween<Offset>(
      begin: widget.position,
      end: widget.position,
    ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
    _controller.forward();
  }

  @override
  void didUpdateWidget(AnimatedGraphNode oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.position != widget.position) {
      _positionAnimation = Tween<Offset>(
        begin: _positionAnimation.value,
        end: widget.position,
      ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
      _controller.forward(from: 0);
    }
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _positionAnimation,
      builder: (context, child) {
        return Positioned(
          left: _positionAnimation.value.dx,
          top: _positionAnimation.value.dy,
          child: child!,
        );
      },
      child: GestureDetector(
        onTap: () => _onNodeTap(),
        child: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Colors.blue[400],
            boxShadow: [BoxShadow(blurRadius: 8)],
          ),
          child: Center(child: Text(widget.label)),
        ),
      ),
    );
  }

  void _onNodeTap() {
    // Обработка взаимодействия
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

3. Рисование рёбер графа

class GraphEdgePainter extends CustomPainter {
  final List<Edge> edges;
  final Map<String, Offset> nodePositions;

  GraphEdgePainter(this.edges, this.nodePositions);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.grey[400]!
      ..strokeWidth = 2.0;

    for (final edge in edges) {
      final start = nodePositions[edge.from];
      final end = nodePositions[edge.to];
      
      if (start != null && end != null) {
        canvas.drawLine(start, end, paint);
      }
    }
  }

  @override
  bool shouldRepaint(GraphEdgePainter oldDelegate) {
    return oldDelegate.nodePositions != nodePositions ||
        oldDelegate.edges != edges;
  }
}

4. Физическая симуляция Использовал Simulation и SpringDescription для реалистичного движения узлов:

final spring = SpringDescription.withDampingRatio(
  mass: 1.0,
  stiffness: 100.0,
  ratio: 0.7,
);

final simulation = SpringSimulation(spring, 0, 100, 0);

Вызовы и решения

Производительность: Граф содержал 100+ узлов, поэтому использовал:

  • RepaintBoundary для оптимизации перерисовки
  • Батчинг обновлений позиций узлов
  • CustomPainter вместо виджетов для рёбер

Синхронизация: Анимации узлов и рёбер должны быть синхронизированы:

  • Единый AnimationController для координации
  • Listenable.merge() для слушания нескольких анимаций

Интерактивность: Пользователь мог перетаскивать узлы:

  • Останавливал текущую анимацию при gestures
  • Плавно переходил к новой позиции с инерцией

Этот опыт научил меня глубоко понимать animation в Flutter и оптимизации производительности при работе с комплексными сценами.

Расскажи про самую сложную анимацию которую реализовывал в Flutter | PrepBro