// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

/// An example of [AnimationController] and [SlideTransition].

// Occupies the same width as the widest single digit used by AnimatedDigit.
//
// By stacking this widget behind AnimatedDigit's visible digit, we
// ensure that AnimatedWidget's width will not change when its value
// changes.  Typically digits like '8' or '9' are wider than '1'.  If
// an app arranges several AnimatedDigits in a centered Row, we don't
// want the Row to wiggle when the digits change because the overall
// width of the Row changes.
class _PlaceholderDigit extends StatelessWidget {
  const _PlaceholderDigit();

  @override
  Widget build(BuildContext context) {
    final TextStyle textStyle = Theme.of(
      context,
    ).textTheme.displayLarge!.copyWith(fontWeight: FontWeight.w500);

    final Iterable<Widget> placeholderDigits =
        <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map<Widget>((int n) {
          return Text('$n', style: textStyle);
        });

    return Opacity(
      opacity: 0,
      child: Stack(children: placeholderDigits.toList()),
    );
  }
}

// Displays a single digit [value].
//
// When the value changes the old value slides upwards and out of sight
// at the same as the new value slides into view.
class AnimatedDigit extends StatefulWidget {
  const AnimatedDigit({super.key, required this.value});

  final int value;

  @override
  State<AnimatedDigit> createState() => _AnimatedDigitState();
}

class _AnimatedDigitState extends State<AnimatedDigit>
    with SingleTickerProviderStateMixin {
  static const Duration defaultDuration = Duration(milliseconds: 300);

  late final AnimationController controller;
  late int incomingValue;
  late int outgoingValue;
  // widget.value updates that occurred while the animation is underway.
  List<int> pendingValues = <int>[];
  Duration duration = defaultDuration;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: duration, vsync: this);
    controller.addStatusListener(handleAnimationCompleted);
    incomingValue = widget.value;
    outgoingValue = widget.value;
  }

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

  void handleAnimationCompleted(AnimationStatus status) {
    if (status.isCompleted) {
      if (pendingValues.isNotEmpty) {
        // Display the next pending value. The duration was scaled down
        // in didUpdateWidget by the total number of pending values so
        // that all of the pending changes are shown within
        // defaultDuration of the last one (the past pending change).
        controller.duration = duration;
        animateValueUpdate(incomingValue, pendingValues.removeAt(0));
      } else {
        controller.duration = defaultDuration;
      }
    }
  }

  void animateValueUpdate(int outgoing, int incoming) {
    setState(() {
      outgoingValue = outgoing;
      incomingValue = incoming;
      controller.forward(from: 0);
    });
  }

  // Rebuilding the widget with a new value causes the animations to run.
  // If the widget is updated while the value is being changed the new
  // value is added to pendingValues and is taken care of when the current
  // animation is complete (see handleAnimationCompleted()).
  @override
  void didUpdateWidget(AnimatedDigit oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.value != oldWidget.value) {
      if (controller.isAnimating) {
        // We're in the middle of animating outgoingValue out and
        // incomingValue in. Shorten the duration of the current
        // animation as well as the duration for animations that
        // will show the pending values.
        pendingValues.add(widget.value);
        final double percentRemaining = 1 - controller.value;
        duration =
            defaultDuration * (1 / (percentRemaining + pendingValues.length));
        controller.animateTo(1.0, duration: duration * percentRemaining);
      } else {
        animateValueUpdate(incomingValue, widget.value);
      }
    }
  }

  // When the controller runs forward both SlideTransitions' children
  // animate upwards. This takes the outgoingValue out of sight and the
  // incoming value into view. See animateValueUpdate().
  @override
  Widget build(BuildContext context) {
    final TextStyle textStyle = Theme.of(context).textTheme.displayLarge!;
    return ClipRect(
      child: Stack(
        children: <Widget>[
          const _PlaceholderDigit(),
          SlideTransition(
            position: controller.drive(
              Tween<Offset>(
                begin: Offset.zero,
                end: const Offset(0, -1), // Out of view above the top.
              ),
            ),
            child: Text(
              key: ValueKey<int>(outgoingValue),
              '$outgoingValue',
              style: textStyle,
            ),
          ),
          SlideTransition(
            position: controller.drive(
              Tween<Offset>(
                begin: const Offset(0, 1), // Out of view below the bottom.
                end: Offset.zero,
              ),
            ),
            child: Text(
              key: ValueKey<int>(incomingValue),
              '$incomingValue',
              style: textStyle,
            ),
          ),
        ],
      ),
    );
  }
}

class AnimatedDigitApp extends StatelessWidget {
  const AnimatedDigitApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(title: 'AnimatedDigit', home: AnimatedDigitHome());
  }
}

class AnimatedDigitHome extends StatefulWidget {
  const AnimatedDigitHome({super.key});

  @override
  State<AnimatedDigitHome> createState() => _AnimatedDigitHomeState();
}

class _AnimatedDigitHomeState extends State<AnimatedDigitHome> {
  int value = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: AnimatedDigit(value: value % 10)),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            value += 1;
          });
        },
        tooltip: 'Increment Digit',
        child: const Icon(Icons.add),
      ),
    );
  }
}

void main() {
  runApp(const AnimatedDigitApp());
}
