Notas Flutter | Animaciones Flutter

Abstracción de animación en Flutter

Para facilitar a los desarrolladores la creación de animaciones, diferentes sistemas de interfaz de usuario abstraen animaciones. Flutter también abstrae animaciones, involucrando principalmente a Animation、Curve、Controller、Tweenestos cuatro personajes. Trabajan juntos para completar una animación completa. Vamos a presentarlos uno por uno. ellos.

1. Animación

AnimationAnimationEs una clase abstracta, que no tiene nada que ver con la representación de la interfaz de usuario en sí, y su función principal es guardar la interpolación y el estado de la animación; una de las clases más utilizadas es Animation<double>.

AnimationTweenUn objeto es una clase que genera secuencialmente valores entre un intervalo ( ) durante un período de tiempo . AnimationEl valor de salida del objeto durante toda la ejecución de la animación puede ser lineal, curvo, una función de paso o cualquier otra función curva, etc., Curvedependiendo de decisión.

Dependiendo de Animationcómo se controle el objeto, la animación puede avanzar (comenzando en el estado inicial y terminando en el estado final), o en reversa, o incluso cambiar de dirección en el medio.

AnimationTambién es posible generar doubleotros tipos de valores además, como: Animation<Color>o Animation<Size>. En cada fotograma de la animación, podemos obtener el valor del estado actual de la animación a través de las propiedades Animationdel objeto .value

notificación de animación

Podemos Animationmonitorear cada cuadro de la animación y el cambio del estado de ejecución Animationa través de los siguientes dos métodos:

  • addListener(); se puede usar para Animationagregar un detector de fotogramas, que se llamará cada fotograma. El comportamiento más común en un detector de marcos se llama después de un cambio de estado setState()para desencadenar una reconstrucción de la interfaz de usuario.
  • addStatusListener(); puede Animationagregar un oyente de "cambio de estado de animación"; el AnimationStatusoyente de cambio de estado se llamará cuando la animación comience, finalice, avance o retroceda (ver definición).

Aquí solo necesita saber la diferencia entre el monitor de marco y el monitor de estado, y lo ilustraremos con un ejemplo más adelante.

2. Curva

El proceso de animación puede ser a una velocidad constante, uniformemente acelerado o primero acelerado y luego desacelerado. Flutter usa Curve(curva) para describir el proceso de animación. Llamamos animación uniforme lineal ( Curves.linear) y animación no uniforme no lineal.

Podemos CurvedAnimationespecificar la curva de la animación por, como:

final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimationy AnimationController(descrito a continuación) son Animation<double>ambos tipos. CurvedAnimationPodemos envolver AnimationControllery Curvegenerar un nuevo objeto de animación, que es como asociamos la animación con la curva que ejecuta la animación. La curva que especificamos para la animación es Curves.easeIn, lo que significa que la animación comienza más lento y termina más rápido. CurvesLa clase es una clase de enumeración preestablecida, que define muchas curvas de uso común. Las siguientes son algunas de las más utilizadas:

Curvas proceso de animación
lineal Uniforme
decelerar Desaceleración uniforme
facilidad Comienza a acelerar, luego reduce la velocidad
facilidad en empezar lento, luego rápido
Facilitarse empezar rápido, luego lento
facilidadEntradaFuera Comience lento, luego acelere, luego disminuya la velocidad nuevamente

Además de las enumeradas anteriormente, Curveshay muchas otras curvas definidas en la clase, puede verificar Curvesla definición de la clase usted mismo.

Por supuesto también podemos crear la nuestra propia Curve, por ejemplo definimos una curva sinusoidal:

class ShakeCurve extends Curve {
    
    
  
  double transform(double t) {
    
    
    return math.sin(t * math.PI * 2);
  }
}

3. Controlador de animación

AnimationControllerSe utiliza para controlar la animación, que incluye métodos para iniciar forward(), detener stop()y reproducir en reversa . Cada cuadro de la animación, se genera un nuevo valor. De forma predeterminada, los números desde hasta (el intervalo predeterminado) se generan linealmente durante el período de tiempo dado .reverse()AnimationControllerAnimationController0.01.0

Por ejemplo, el siguiente código crea un Animationobjeto (pero no inicia una animación):

final AnimationController controller = AnimationController(
	  duration: const Duration(milliseconds: 2000),
	  vsync: this,
);

Entre ellos, durationrepresenta la duración de la ejecución de la animación, a través del cual podemos controlar la velocidad de la animación.

AnimationControllerEl rango de números generados se puede especificar mediante lowerBoundy upperBound, como:

final AnimationController controller = AnimationController( 
	 duration: const Duration(milliseconds: 2000), 
	 lowerBound: 10.0,
	 upperBound: 20.0,
	 vsync: this
);

AnimationControllerDerivado de Animation<double>, por lo que se puede Animationusar en cualquier lugar donde se espere un objeto.

Además, AnimationControllerexisten otros métodos para controlar la animación, como forward()los métodos que pueden iniciar la animación hacia adelante y reverse()pueden iniciar la animación inversa. Después de que la animación comienza a ejecutarse, se genera el cuadro de animación. Cada vez que se actualiza la pantalla, es un cuadro de animación. En cada cuadro de la animación, el valor de animación actual ( ) se generará de acuerdo con la curva de animación, y luego construido de acuerdo con el valor de animación actual Animation.value.UI, cuando todos los cuadros de animación se activan secuencialmente, el valor de la animación cambiará secuencialmente, por lo que la UI construida también cambiará secuencialmente, por lo que finalmente podemos ver una animación completa. Además, en cada fotograma de la animación, Animationel objeto llamará a su oyente de fotogramas, y cuando cambie el estado de la animación (como el final de la animación), llamará al oyente de cambio de estado.

Nota: En algunos casos, los valores de animación pueden estar fuera de AnimationControllerrango [0.0,1.0], dependiendo de la curva específica. Por ejemplo, fling()la función puede simular una animación de lanzamiento de dedos de acuerdo con la velocidad ( velocity) y la fuerza ( force) de nuestro dedo deslizando (lanzando), por lo que su valor de animación puede estar [0.0,1.0]fuera de rango. Es decir, dependiendo de la curva elegida, CurvedAnimationla salida del puede tener un rango mayor que la entrada. Por ejemplo, Curves.elasticInlas curvas isoelásticas generan valores que son mayores o menores que el rango predeterminado.

Corazón

Al crear uno AnimationController, es necesario pasar un vsyncparámetro, que recibe un TickerProvidertipo de objeto cuya principal responsabilidad es crear Ticker, definido de la siguiente manera:

abstract class TickerProvider {
    
     
  Ticker createTicker(TickerCallback onTick); // 通过一个回调创建一个Ticker
}

La aplicación Flutter vinculará una cuando se inicie SchedulerBinding, a través de la cual puede SchedulerBindingagregar una devolución de llamada a cada actualización de pantalla y agregar una devolución de llamada de actualización de pantalla, de modo que se llamará cada vez que se actualice la pantalla .TickerSchedulerBindingTickerCallback

Usar Ticker(en lugar de Timer) para impulsar la animación evitará que la animación fuera de pantalla (cuando la IU animada no esté en la pantalla actual, como cuando la pantalla está bloqueada) consuma recursos innecesarios, porque cuando la pantalla se actualiza en Flutter, ser notificado al atado, peroSchedulerBinding impulsado Sí, dado que la pantalla dejará de actualizarse después de que la pantalla esté bloqueada, no se activará nuevamente.TickerSchedulerBindingTicker

Por lo general, mezclaremos SingleTickerProviderStateMixinla clase en la Stateclase personalizada y luego usaremos el Stateobjeto actual como el valor AnimationControllerdel vsyncparámetro.

4. Interpolación

1. Introducción

Por defecto, AnimationControllerel rango de valores de los objetos es [0.0,1.0]. Si necesitamos construir valores de animación de UI en diferentes rangos o diferentes tipos de datos, podemos usar Tweenpara agregar asignaciones para generar valores de diferentes rangos o tipos de datos.

Por ejemplo, como en el siguiente ejemplo, el valor Tweengenerado :[-200.0,0.0]

final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);

TweenEl constructor toma beginy enddos parámetros. TweenLa única responsabilidad de es definir el mapeo de rangos de entrada a rangos de salida. El rango de entrada suele ser [0.0,1.0], pero esto no es obligatorio, podemos personalizar el rango requerido.

TweenHeredar de Animatable<T>, en lugar de heredar de Animation<T>, Animatabledefine principalmente las reglas de asignación para los valores de animación.

Veamos un ColorTweenejemplo de asignación de un rango de entrada de animación a una salida de transición entre dos valores de color:

final Tween colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

TweenEl objeto no almacena ningún estado, sino que proporciona evaluate(Animation<double> animation)métodos que pueden obtener el valor de asignación actual de la animación. AnimationEl valor actual del objeto value()se puede obtener a través del método. La función también realiza un procesamiento adicional, como garantizar que se devuelvan los estados inicial y final cuando el valor de la animación es y evaluate, respectivamente .0.01.0

2)Entre.animado

Para usar Tweenun objeto, llamas a su animate()método, pasando un AnimationControllerobjeto.

Por ejemplo, el siguiente código genera valores enteros de a en 500milisegundos .0255

final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 500), 
  vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

Tenga en cuenta animate()que se devuelve uno Animation, no uno Animatable.

El siguiente ejemplo crea un controlador, una curva y un Tween:

final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 500), 
  vsync: this,
);
final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

Función lerp de interpolación lineal

El principio de la animación es en realidad dibujar contenido diferente en cada cuadro. Generalmente, se especifican los estados inicial y final, y luego cambian gradualmente del estado inicial al estado final dentro de un período de tiempo, y el valor de estado de un cuadro específico se basará en la animación, por lo tanto, Flutter define lerpmétodos estáticos (interpolación lineal) , como:

// a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);

lerpEl cálculo de generalmente sigue: 返回值 = a + (b - a) * t, y otras lerpclases con métodos:

Size.lerp(a, b, t)
Rect.lerp(a, b, t)
Offset.lerp(a, b, t)
Decoration.lerp(a, b, t)
Tween.lerp(t) // 起始状态和终止状态在构建 Tween 的时候已经指定了
...

Cabe señalar que lerpes una interpolación lineal, lo que significa que el valor de retorno y el progreso de la animación testán en una y = kx + brelación de función lineal ( ), porque la imagen de una función lineal es una línea recta, por lo que se llama interpolación lineal.

Si queremos que la animación se ejecute de acuerdo con una curva, podemos tasignar a , por ejemplo, para lograr un efecto de aceleración uniforme, luego t' = at²+bt+cespecificar la aceleración ay b(en la mayoría de los casos, es necesario asegurarse t'de que el rango de valores de es [0,1], de Por supuesto, hay algunos casos que pueden exceder este rango de valores, como el bounceefecto de resorte ( )), y Curveel principio de realizar la animación de acuerdo con diferentes curvas se trealiza esencialmente mediante el mapeo de acuerdo con diferentes fórmulas de mapeo.

estructura basica de la animacion

En Flutter, podemos implementar la animación de varias maneras. A continuación, se utiliza una implementación diferente de un ejemplo de acercamiento gradual a una imagen para demostrar la diferencia entre las diferentes implementaciones de animación en Flutter.

1. Versión básica

Demostremos el método de implementación de animación más básico:

class ScaleAnimationRoute extends StatefulWidget {
    
    
  const ScaleAnimationRoute({
    
    Key? key}) : super(key: key);
  
  _ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}

// 需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin {
    
    
  late Animation<double> animation;
  late AnimationController controller;
  
  
  initState() {
    
    
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    // 匀速 图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
    
    
        setState(() => {
    
    });
      });
    // 启动动画(正向执行)
    controller.forward();
  }

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Image.asset(
       "imgs/avatar.png",
        width: animation.value,
        height: animation.value,
      ),
    );
  }
  
  
  dispose() {
    
     
    controller.dispose(); // 路由销毁时需要释放动画资源
    super.dispose();
  }
}

La función en el código anterior addListener()se llama setState(), por lo que cada vez que la animación genera un nuevo número, el marco actual se marca como sucio ( dirty), lo que hará que se vuelva a llamar widgetal método, y en él, el ancho y alto del cambio , porque ahora se usa su altura y el ancho , por lo que se ampliará gradualmente.build()build()Imageanimation.value

Vale la pena señalar que el controlador se libera (llamando dispose()al método) cuando se completa la animación para evitar pérdidas de memoria.

En el ejemplo anterior, no se especifica Curve, por lo que el proceso de acercamiento es lineal (velocidad uniforme). A continuación, especificamos uno Curvepara lograr un proceso de animación similar a un efecto de resorte. Solo necesitamos initStatecambiar el código a continuación para la siguiente:


initState() {
    
    
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 3), vsync: this);
    // 使用弹性曲线
    animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    // 图片宽高从0变到300
    animation = Tween(begin: 0.0, end: 300.0).animate(animation)
      ..addListener(() {
    
    
        setState(() => {
    
    });
      });
    // 启动动画
    controller.forward();
  }

resultado de ejecución:

inserte la descripción de la imagen aquí

2. Use AnimatedWidget para simplificar

Descubrimos que el paso de actualizar la interfaz de usuario a través addListener()y en el ejemplo anterior setState()es realmente común, y sería engorroso agregar una oración de este tipo a cada animación. AnimatedWidgetLa clase encapsula setState()los detalles de la llamada y nos permite separarlos.El widgetcódigo refactorizado es el siguiente:

import 'package:flutter/material.dart';

class AnimatedImage extends AnimatedWidget {
    
    
  const AnimatedImage({
    
    Key? key, required Animation<double> animation,}) : super(key: key, listenable: animation);

  
  Widget build(BuildContext context) {
    
    
    final animation = listenable as Animation<double>;
    return  Center(
      child: Image.asset(
        "imgs/avatar.png",
        width: animation.value,
        height: animation.value,
      ),
    );
  }
}

class ScaleAnimationRoute extends StatefulWidget {
    
    
  const ScaleAnimationRoute1({
    
    Key? key}) : super(key: key);
  
  _ScaleAnimationRouteState createState() =>  _ScaleAnimationRouteState();
}

class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin {
    
    
  late Animation<double> animation;
  late AnimationController controller;

  
  initState() {
    
    
    super.initState();
    controller =  AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // 图片宽高从0变到300
    animation =  Tween(begin: 0.0, end: 300.0).animate(controller);
    // 启动动画
    controller.forward();
  }

  
  Widget build(BuildContext context) {
    
    
    return AnimatedImage(animation: animation);
  }

  
  dispose() {
    
     
    controller.dispose(); // 路由销毁时需要释放动画资源
    super.dispose();
  }
}

3. Refactorización con AnimatedBuilder

El uso AnimatedWidgetse puede separar de la animación widget, y el proceso de renderizado de la animación (es decir, establecer el ancho y el alto) todavía está en AnimatedWidget, asumiendo que si agregamos una widgetanimación de cambio de transparencia, entonces necesitamos implementar otra AnimatedWidget, que es no muy elegante Si podemos poner El proceso de renderizado también se abstrae, será mucho mejor, y AnimatedBuilderes para separar la lógica de renderizado, buildel código en el método anterior se puede cambiar a:


Widget build(BuildContext context) {
    
    
    // return AnimatedImage(animation: animation,);
    return AnimatedBuilder(
      animation: animation,
      child: Image.asset("imgs/avatar.png"),
      builder: (BuildContext ctx, child) {
    
    
        return  Center(
          child: SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          ),
        );
      },
    );
}

Un problema confuso con el código anterior es que childparece que se especifica dos veces. Pero lo que realmente sucede es que la referencia externa childse pasa al constructor anónimo, que luego usa el objeto como su elemento secundario AnimatedBuilder. AnimatedBuilderEl resultado final es que AnimatedBuilderel objeto devuelto se inserta en widgetel árbol.

Tal vez dirá que esto no es muy diferente de nuestro ejemplo inicial, pero traerá tres beneficios:

  1. No hay necesidad de agregar explícitamente un frame listener y luego llamarlo setState(), el beneficio es AnimatedWidgetel mismo que el de .

  2. Mejor rendimiento: debido a que widgetel alcance de cada cuadro de la animación que se debe construir se reduce, si no builder, setState()se llamará en el contexto del componente principal, lo que hará que buildse vuelva a llamar al método del componente principal; después de haber builderello, sólo provocará la animación widgetAuto- buildrecuerdo, evitando innecesarios rebuild.

  3. Las animaciones se pueden reutilizar encapsulando AnimatedBuilderefectos de transición comunes. Ilustremos GrowTransitionencapsulando uno, que puede widgetrealizar una animación de acercamiento para niños:

class GrowTransition extends StatelessWidget {
    
    
  const GrowTransition({
    
    Key? key, required this.animation, this.child,}) : super(key: key);

  final Widget? child;
  final Animation<double> animation;

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (BuildContext context, child) {
    
    
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

De esta manera, el ejemplo original se puede cambiar a:

...
Widget build(BuildContext context) {
    
    
  return GrowTransition(
    child: Image.asset("images/avatar.png"), 
    animation: animation,
  );
}

Es de esta manera que Flutter encapsula muchas animaciones, como: FadeTransition, ScaleTransition, SizeTransitionetc. Estas clases de transición preestablecidas se pueden reutilizar en muchos casos.

monitoreo de estado de animación

Como se mencionó anteriormente, podemos agregar oyentes de cambio de estado de animación a través del método Animation. addStatusListener()En Flutter, hay cuatro estados de animación, AnimationStatusque se definen en la clase de enumeración, y los explicaremos uno por uno a continuación:

valor de enumeración significado
dismissed La animación se detiene en el punto de inicio
forward La animación se está ejecutando en la dirección de avance.
reverse la animación se está realizando al revés
completed la animación se detiene al final

Ejemplo: Cambiemos el ejemplo de acercamiento de la imagen anterior a una animación en bucle que primero acerca el zoom, luego lo aleja y luego lo vuelve a acercar. Para lograr este efecto, solo necesitamos monitorear el cambio del estado de la animación, es decir, invertir la animación al final de la ejecución hacia adelante de la animación y ejecutar la animación hacia adelante al final de la ejecución inversa de la animación. el código se muestra a continuación:

initState() {
    
    
  super.initState();
  controller = AnimationController(
    duration: const Duration(seconds: 1), 
    vsync: this,
  );
  animation = Tween(begin: 0.0, end: 300.0).animate(controller); // 图片宽高从0变到300
  animation.addStatusListener((status) {
    
    
    if (status == AnimationStatus.completed) {
    
     
      controller.reverse();  // 动画执行结束时反向执行动画
    } else if (status == AnimationStatus.dismissed) {
    
     
      controller.forward(); // 动画恢复到初始状态时执行动画(正向)
    }
  }); 
  controller.forward();  // 启动动画(正向)
}

Animación de cambio de ruta personalizada

La biblioteca de componentes Material proporciona un MaterialPageRoutecomponente que puede usar animaciones de cambio de enrutamiento de acuerdo con el estilo de la plataforma, como deslizarse hacia la izquierda y hacia la derecha en iOS y deslizarse hacia arriba y hacia abajo en Android. Ahora, si Androidtambién queremos usar los estilos de cambio izquierdo y derecho en la pantalla, ¿qué debemos hacer? Un enfoque simple se puede utilizar directamente CupertinoPageRoute, como:

 Navigator.push(context, CupertinoPageRoute(  
   builder: (context)=>PageB(),
 ));

CupertinoPageRouteEs Cupertinoun componente de conmutación de enrutamiento de estilo iOS proporcionado por la biblioteca de componentes, que realiza la conmutación deslizante izquierda y derecha. Entonces, ¿cómo personalizamos la animación de cambio de enrutamiento? La respuesta PageRouteBuilderes

Echemos un vistazo a cómo usar PageRouteBuilderpara personalizar la animación de cambio de enrutamiento. Por ejemplo, si queremos realizar la transición de enrutamiento con animación de aparición gradual, el código de implementación es el siguiente:

Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500), // 动画时间为500毫秒
    pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) {
    
    
      return FadeTransition( // 使用渐隐渐入过渡
        opacity: animation,
        child: PageB(), // 路由B
      );
    },
  ),
);

Podemos ver pageBuilderque hay un parámetro, proporcionado por el administrador de rutas de Flutter, y se volverá a llamar en cada cuadro de animación animationcuando se cambie la ruta , para que podamos personalizar la animación de transición a través del objeto.pageBuilderanimation

Ya sean MaterialPageRoute, CupertinoPageRouteo PageRouteBuilder, todos heredan de la PageRouteclase, pero PageRouteBuilderen realidad son solo PageRouteun contenedor. Podemos heredar directamente PageRoutela clase para implementar un enrutamiento personalizado. El ejemplo anterior se puede implementar de la siguiente manera:

  1. definir una clase de enrutamientoFadeRoute
class FadeRoute extends PageRoute {
    
    
  FadeRoute({
    
    
    required this.builder,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.opaque = true,
    this.barrierDismissible = false,
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
  });

  final WidgetBuilder builder;

  
  final Duration transitionDuration;

  
  final bool opaque;

  
  final bool barrierDismissible;

  
  final Color barrierColor;

  
  final String barrierLabel;

  
  final bool maintainState;

  
  Widget buildPage(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation) => builder(context);

  
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    
    
     return FadeTransition( 
       opacity: animation,
       child: builder(context),
     );
  }
}
  1. usarFadeRoute
Navigator.push(context, FadeRoute(builder: (context) {
    
    
  return PageB();
}));

Aunque los dos métodos anteriores pueden realizar animaciones de conmutación personalizadas, deben usarse primero en el uso real PageRouteBuilder, de modo que no haya necesidad de definir una nueva clase de enrutamiento, y será más conveniente de usar.

Pero a veces PageRouteBuilderno puede cumplir con los requisitos. Por ejemplo, al aplicar la animación de transición, necesitamos leer algunas propiedades de la ruta actual. En este momento, solo podemos usar la herencia. Por ejemplo, PageRoutesi solo queremos aplicar al abrir una nueva Animación de ruta, pero no use animación al regresar, entonces debemos juzgar isActivesi la propiedad de enrutamiento actual es cuando construimos la animación de transición true, el código es el siguiente:


Widget buildTransitions(BuildContext context, Animation<double> animation,
    Animation<double> secondaryAnimation, Widget child) {
    
     
	 if (isActive) {
    
      // 当前路由被激活,是打开新路由
	   return FadeTransition(
	     opacity: animation,
	     child: builder(context),
	   );
	 } else {
    
     // 是返回,则不应用过渡动画
	   return Padding(padding: EdgeInsets.zero);
	 }
}

Para obtener información detallada sobre los parámetros de enrutamiento, puede consultar la documentación de la API, que es relativamente simple y no se repetirá aquí.

Animación de héroe

HeroSe refiere a la capacidad de "volar" entre rutas (páginas) widgetEn términos simples, Herola animación significa que cuando se cambia de ruta, hay una compartida que widgetpuede cambiar entre rutas antiguas y nuevas. Dado que la widgetposición compartida y la apariencia en las páginas de ruta antigua y nueva pueden ser diferentes, cuando se cambia la ruta, pasará gradualmente de la ruta anterior a la posición especificada en la ruta nueva, lo que generará una animación Hero.

Es posible que haya visto herola animación muchas veces. Por ejemplo, una ruta muestra una lista en miniatura de artículos a la venta y, al seleccionar una entrada, pasa a una nueva ruta que contiene los detalles del artículo y un botón "Comprar". "Volar" una imagen de una ruta a otra en Flutter se denomina animación de héroe , aunque la misma acción a veces se denomina transición de elementos compartidos . Usemos un ejemplo para experimentar herola animación.

ejemplo

Supongamos que hay dos rutas A y B, y su interacción de contenido es la siguiente:

A: Contiene un avatar de usuario, circular, salta a la ruta B después de hacer clic, puedes ver una imagen más grande.

B: Muestra la imagen original del avatar del usuario, un rectángulo.

Al saltar entre las dos rutas de AB, el avatar del usuario pasará gradualmente al avatar de la página de enrutamiento de destino. A continuación, veamos primero el código y luego analicémoslo.

Ruta A:

class HeroAnimationRouteA extends StatelessWidget {
    
    
  const HeroAnimationRouteA({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(title: const Text("Hero动画"),),
      body: Container(
        alignment: Alignment.topCenter,
        child: InkWell(
          child: Hero(
            tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
            child: ClipOval(
              child: Image.asset("images/avatar.png", width: 100.0,),
            ),
          ),
          onTap: () {
    
    
            // 打开B路由
            Navigator.push(context, PageRouteBuilder(
              transitionDuration: const Duration(milliseconds: 500),
              pageBuilder: (context, animation, secondaryAnimation) {
    
    
                return FadeTransition(
                  opacity: animation,
                  child: const HeroAnimationRouteB(),
                );
              },
            ));
          },
        ),
      ),
    );
  }
}

Ruta B:

class HeroAnimationRouteB extends StatelessWidget {
    
    
  const HeroAnimationRouteB({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
     //appBar: AppBar(title: const Text("原图"),),
      body: Center(
        child: Hero(
          tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
          child: Image.asset("images/avatar.png"),
        ),
      ),
    );
  }
}

Efecto:

inserte la descripción de la imagen aquí

Podemos ver que para realizar Herola animación, solo necesita Heroenvolver los componentes compartidos widgety proporcionar los mismos tag, y los marcos de transición en el medio se completan automáticamente con el marco Flutter. Debe tenerse en cuenta que el uso compartido de las páginas de enrutamiento delanteras y traseras debe ser el mismoHerotag , y el marco Flutter se utiliza tagpara determinar widgetla relación correspondiente entre las páginas de enrutamiento antiguas y nuevas.

HeroEl principio de la animación es relativamente simple, el marco Flutter conoce la posición y el tamaño de los elementos compartidos en las páginas de enrutamiento antiguas y nuevas, por lo que de acuerdo con estos dos puntos finales, es suficiente encontrar la interpolación (estado intermedio) durante la ejecución de la animación. Afortunadamente, no es necesario que hagamos estas cosas nosotros mismos, Flutter ya lo ha hecho por nosotros, si está interesado, puede ir al código fuente relacionado con la animación Hero.

animación entrelazada

A veces podemos necesitar algunas animaciones complejas. Estas animaciones pueden consistir en una secuencia de animación o animaciones superpuestas. Por ejemplo: hay un histograma que necesita cambiar de color mientras crece en altura. Después de crecer hasta la altura máxima, necesitamos traducir un cierto distancia en el eje X. Se puede encontrar que la escena anterior contiene una variedad de animaciones en diferentes etapas.Para lograr este efecto, es muy simple usar Stagger Animation. La animación entrelazada debe prestar atención a los siguientes puntos:

  1. Para crear una animación intercalada, se utilizan varios objetos de animación ( ) Animation.
  2. Uno AnimationControllercontrola todos los objetos de animación.
  3. Especifique un intervalo de tiempo para cada objeto de animación ( Interval)

Todas las animaciones están impulsadas por el mismo AnimationController, sin importar cuánto tiempo deba durar la animación, el valor del controlador debe estar 0.0entre 1.0y , y el intervalo ( Interval) de cada animación debe estar entre 0.0y 1.0. Para cada propiedad que anime durante un intervalo, debe crear una separada Tweenque especifique los valores inicial y final de la propiedad. En otras palabras, 0.0para 1.0representar todo el proceso de animación, podemos especificar diferentes puntos de inicio y finalización para diferentes animaciones para determinar su hora de inicio y finalización.

Veamos un ejemplo para realizar una animación del crecimiento de un histograma:

  1. Al principio, la altura crece de 0 a 300 píxeles y el color se desvanece de verde a rojo al mismo tiempo; este proceso ocupa el 60% del tiempo total de animación.
  2. Después de que la altura crezca a 300, comience a trasladar 100 píxeles a la derecha a lo largo del eje X; este proceso ocupa el 40 % del tiempo total de la animación.

Separamos Widgetla implementación de la animación:

class StaggerAnimation extends StatelessWidget {
    
    

  StaggerAnimation({
    
     Key? key, required this.controller }): super(key: key){
    
    

    var animationFirst = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.0, 0.6, curve: Curves.ease,),//间隔,前60%的动画时间
    );

    var animationAfter = CurvedAnimation(
      parent: controller,
      curve: const Interval(0.6, 1.0, curve: Curves.ease,), //间隔,后40%的动画时间
    );

    // 高度动画
    height = Tween<double>(begin:.0, end: 300.0,).animate(animationFirst);
    // 颜色
    color = ColorTween(begin:Colors.green, end:Colors.red,).animate(animationFirst);
    // 边距
    padding = Tween<EdgeInsets>(
      begin: const EdgeInsets.only(left: .0),
      end: const EdgeInsets.only(left: 100.0),).animate(animationAfter);
  }

  final Animation<double> controller;
  late final Animation<double> height;
  late final Animation<EdgeInsets> padding;
  late final Animation<Color?> color;

  Widget _buildAnimation(BuildContext context, Widget? child) {
    
    
    return Container(
      alignment: Alignment.bottomCenter,
      padding:padding.value ,
      child: Container(
        color: color.value,
        width: 50.0,
        height: height.value,
      ),
    );
  }

  
  Widget build(BuildContext context) {
    
    
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

StaggerAnimationSe definen tres animaciones en , que son derecha Container, heighty animaciones de configuración de atributos, y luego especifican el punto de inicio y el punto final para cada animación en todo el colorproceso de animación pasando. Implementemos el enrutamiento para iniciar la animación:paddingInterval

class StaggerRoute extends StatefulWidget {
    
    
  const StaggerRoute({
    
    Key? key}) : super(key: key);

  
  State createState() => _StaggerRouteState();
}

class _StaggerRouteState extends State<StaggerRoute> with TickerProviderStateMixin {
    
    
  late AnimationController _controller;

  
  void initState() {
    
    
    super.initState();
    _controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  }

  Future<void> _playAnimation() async {
    
    
    try {
    
    
      //先正向执行动画
      await _controller.forward().orCancel;
      //再反向执行动画
      await _controller.reverse().orCancel;
    } on TickerCanceled {
    
    
      // the animation got canceled, probably because we were disposed
    }
  }

  
  Widget build(BuildContext context) {
    
    
    return  Scaffold(
      appBar: AppBar(title: const Text("StaggerAnimation"),),
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () => _playAnimation(),
        child: Center(
          child: Container(
            width: 300.0,
            height: 300.0,
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(0.1),
              border: Border.all(color: Colors.black.withOpacity(0.5),),
            ),
            //调用我们定义的交织动画Widget
            child: StaggerAnimation(controller: _controller),
          ),
        ),
      ),
    );
  }

  
  void dispose() {
    
    
    //路由销毁时需要释放动画资源
    _controller.dispose();
    super.dispose();
  }
}

Efecto de ejecución:

inserte la descripción de la imagen aquí

componente de alternancia de animación

Conmutador animado

En el desarrollo real, a menudo nos encontramos con escenarios de cambio de elementos de la interfaz de usuario, como el cambio de pestañas y el cambio de enrutamiento. Para mejorar la experiencia del usuario, generalmente se especifica una animación al cambiar para que el proceso de cambio parezca fluido. Algunos componentes de conmutación de uso común se han proporcionado en la biblioteca de componentes del SDK de Flutter, como PageView, , TabViewetc. AnimatedSwitcherabstracto.

AnimatedSwitcherMostrar y ocultar animaciones se pueden agregar a sus elementos secundarios nuevos y antiguos al mismo tiempo. Es decir, AnimatedSwitchercuando el subelemento cambia, animará su elemento anterior y su elemento nuevo. Veamos primero AnimatedSwitcherla definición de:

const AnimatedSwitcher({
    
    
  Key? key,
  this.child,
  required this.duration, // 新child显示动画时长
  this.reverseDuration,// 旧child隐藏的动画时长
  this.switchInCurve = Curves.linear, // 新child显示的动画曲线
  this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
  this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
  this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})

Cuando el AnimatedSwitcherde childcambia (tipo o Keydiferente), el antiguo childejecutará la animación oculta y el nuevo childejecutará la animación de visualización. El tipo de efecto de animación a realizar está transitionBuilderdeterminado por el parámetro, que acepta un AnimatedSwitcherTransitionBuildertipo builder, definido de la siguiente manera:

typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);

Los enlaces nuevos y antiguos se animarán por separado al cambiar builder:AnimatedSwitcherchildchild

  1. En el caso de las animaciones enlazadas antiguas child, se ejecutan al revés ( reverse)
  2. Para childlas animaciones nuevas, enlazadas apuntarán hacia adelante ( forward)

De esta manera, se realiza el enlace de animación nuevo y antiguo child. AnimatedSwitcherEl valor predeterminado es AnimatedSwitcher.defaultTransitionBuilder:

Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
    
    
  return FadeTransition(
    opacity: animation,
    child: child,
  );
}

Se puede ver que se devuelve el objeto FadeTransition, es decir, por defecto AnimatedSwitcherse realizarán childlas animaciones de "fading" y "fading" sobre el antiguo y el nuevo.

ejemplo

Veamos un ejemplo a continuación: implemente un contador, y luego, durante cada proceso de autoincremento, el número anterior realiza una animación de alejamiento y se oculta, y el nuevo número realiza una visualización de animación de acercamiento. El código es el siguiente:

import 'package:flutter/material.dart';

class AnimatedSwitcherCounterRoute extends StatefulWidget {
    
    
   const AnimatedSwitcherCounterRoute({
    
    Key key}) : super(key: key); 
   
   State createState() => _AnimatedSwitcherCounterRouteState();
 }

 class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
    
    
   int _count = 0;

   
   Widget build(BuildContext context) {
    
    
     return Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           AnimatedSwitcher(
             duration: const Duration(milliseconds: 500),
             transitionBuilder: (Widget child, Animation<double> animation) {
    
     
               return ScaleTransition(child: child, scale: animation); // 执行缩放动画
             },
             child: Text(
               '$_count',
               // 显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
               key: ValueKey<int>(_count),
               style: Theme.of(context).textTheme.headline4,
             ),
           ),
           ElevatedButton(
             child: const Text('+1',),
             onPressed: () {
    
    
               setState(() {
    
    
                 _count += 1;
               });
             },
           ),
         ],
       ),
     );
   }
 }

Ejecute el código de muestra, cuando se haga clic en el botón "+1", el número original se reducirá gradualmente hasta ocultarse, mientras que el nuevo número se agrandará gradualmente, como se muestra en la figura:

inserte la descripción de la imagen aquí

Nota: AnimatedSwitcherantiguo y nuevo de , no deben ser iguales childsi son del mismo tipo .Key

Principio de implementación de AnimatedSwitcher

De hecho, AnimatedSwitcherel principio de implementación de es relativamente simple, y AnimatedSwitchertambién podemos adivinar según la forma de uso. Para realizar la childanimación de conmutación antigua y nueva, solo se deben aclarar dos preguntas:

  1. ¿Cuándo se ejecuta la animación?
  2. ¿Cómo childanimar lo viejo y lo nuevo?

Por AnimatedSwitcherla forma de uso, podemos ver que cuando childhay un cambio ( si el tipo o widgetel niño keyes diferente, se considera que ha cambiado), se volverá a ejecutar buildy luego la animación comenzará a ejecutarse.

Podemos StatefulWidgetlograr esto mediante la herencia AnimatedSwitcher. El método específico es didUpdateWidgetjuzgar childsi su antiguo y nuevo han cambiado en la devolución de llamada. Si hay un cambio, realice childuna reverseanimación de salida inversa () para el antiguo childy una forwardanimación de entrada hacia adelante () para el nuevo uno. Lo siguiente es AnimatedSwitcherparte del pseudocódigo central de la implementación:

Widget _widget; 
void didUpdateWidget(AnimatedSwitcher oldWidget) {
    
    
  super.didUpdateWidget(oldWidget);
  // 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
  if (Widget.canUpdate(widget.child, oldWidget.child)) {
    
    
    // child没变化,...
  } else {
    
    
    //child发生了变化,构建一个Stack来分别给新旧child执行动画
   _widget= Stack(
      alignment: Alignment.center,
      children:[
        //旧child应用FadeTransition
        FadeTransition(
         opacity: _controllerOldAnimation,
         child : oldWidget.child,
        ),
        //新child应用FadeTransition
        FadeTransition(
         opacity: _controllerNewAnimation,
         child : widget.child,
        ),
      ]
    );
    // 给旧child执行反向退场动画
    _controllerOldAnimation.reverse();
    //给新child执行正向入场动画
    _controllerNewAnimation.forward();
  }
}

//build方法
Widget build(BuildContext context){
    
    
  return _widget;
}

El pseudocódigo anterior muestra AnimatedSwitcherla lógica central de la implementación. Por supuesto, AnimatedSwitcherla implementación real es más complicada que esto. Puede personalizar la animación de transición de entrar y salir de la escena y el diseño al realizar la animación. Aquí, eliminamos lo complicado y lo simplificamos, y podemos ver claramente las principales ideas de implementación a través de la forma de pseudocódigo, y la implementación específica puede referirse al AnimatedSwitchercódigo fuente.

Además, también se proporciona un componente en Flutter SDK AnimatedCrossFade, que también puede cambiar entre dos subelementos. El proceso de cambio realiza una animación de desvanecimiento. La AnimatedSwitcherdiferencia es que AnimatedCrossFadees para dos subelementos , AnimatedSwitcherpero entre los valores antiguo y nuevo. de un interruptor de subelemento . AnimatedCrossFadeEl principio de implementación también es relativamente simple, AnimatedSwitchersimilar a y, por lo que no entraré en detalles, si está interesado, puede consultar su código fuente.

Uso avanzado de AnimatedSwitcher

Supongamos que ahora queremos implementar una animación similar al cambio de traducción de enrutamiento: la pantalla de la página anterior se mueve hacia la izquierda para salir y la página nueva se mueve para ingresar desde el lado derecho de la pantalla. Si queremos usarlo AnimatedSwitcher, pronto encontraremos un problema: ¡no se puede! Podríamos escribir el siguiente código:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    
    
    var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return SlideTransition(
       child: child,
       position: tween.animate(animation),
    );
  },
  ...//省略
)

¿Qué hay de malo en el código de arriba? Anteriormente dijimos que el nuevo se animará hacia adelante ( ) y el antiguo se animará hacia atrás ( ) cuando AnimatedSwitcherse cambia el , por lo que el efecto real es que el nuevo se desplaza desde el lado derecho de la pantalla, pero el antiguo sí. desde la salida en el lado derecho (no el izquierdo). De hecho, también es fácil de entender, porque sin un tratamiento especial, el avance y el reverso de la misma animación son exactamente opuestos (simétricos).childchildforwardchildreversechildchild

Entonces la pregunta es, ¿no se puede usar AnimatedSwitcher? ¡La respuesta es, por supuesto, no! Piense en este problema con cuidado, la razón es que el mismo Animationavance ( forward) y retroceso ( reverse) son simétricos. Entonces, si podemos romper esta simetría, entonces podemos realizar esta función. A continuación, encapsulemos uno MySlideTransition. La SlideTransitionúnica diferencia es que la ejecución inversa de la animación está personalizada (deslice hacia afuera desde la izquierda para ocultar), el código es el siguiente:

class MySlideTransition extends AnimatedWidget {
    
    
  const MySlideTransition({
    
    
    Key? key,
    required Animation<Offset> position,
    this.transformHitTests = true,
    required this.child,
  }) : super(key: key, listenable: position);

  final bool transformHitTests;

  final Widget child;

  
  Widget build(BuildContext context) {
    
    
    final position = listenable as Animation<Offset>;
    Offset offset = position.value;
    if (position.status == AnimationStatus.reverse) {
    
    
      offset = Offset(-offset.dx, offset.dy);
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}

Al llamar, SlideTransitionreemplácelo con MySlideTransition:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    
    
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return MySlideTransition(
      child: child,
      position: tween.animate(animation),
    );
  },
  ...//省略
)

Efecto:

inserte la descripción de la imagen aquí

Se puede ver que de esta manera ingeniosa, hemos realizado una animación similar al interruptor de entrada de enrutamiento, de hecho, el interruptor de enrutamiento Flutter también se AnimatedSwitcherrealiza a través de este método.

DeslizarTransiciónX

En el ejemplo anterior, realizamos la animación de "izquierda adentro y derecha adentro", entonces, ¿qué sucede si queremos realizar "izquierda adentro y derecha afuera", "arriba adentro y abajo afuera" o "abajo adentro y arriba afuera"? Por supuesto, podemos modificar el código anterior por separado, pero de esta manera, cada animación tiene que definir una "Transición" por separado, lo cual es muy problemático.

Lo siguiente encapsulará un general SlideTransitionXpara lograr esta "animación de entrada y salida", el código es el siguiente:

import 'package:flutter/widgets.dart';

/// 实现同向滑动效果,通常和[AnimatedSwitcher]一起使用
/// Animates the position of a widget relative to its normal position
/// ignoring the animation direction(always slide along one direction).
/// Typically, is used in combination with [AnimatedSwitcher].
class SlideTransitionX extends AnimatedWidget {
    
    
  SlideTransitionX({
    
    
    Key? key,
    required Animation<double> position,
    this.transformHitTests = true,
    this.direction = AxisDirection.down,
    required this.child,
  }) : super(key: key, listenable: position) {
    
    
    _tween = Tween(begin: const Offset(0, 1), end: Offset.zero);
    // 偏移在内部处理
    switch (direction) {
    
    
      case AxisDirection.up:
        _tween.begin = const Offset(0, 1);
        break;
      case AxisDirection.right:
        _tween.begin = const Offset(-1, 0);
        break;
      case AxisDirection.down:
        _tween.begin = const Offset(0, -1);
        break;
      case AxisDirection.left:
        _tween.begin = const Offset(1, 0);
        break;
    }
  }

  final bool transformHitTests;

  final Widget child;

  //退场(出)方向
  final AxisDirection direction;

  late final Tween<Offset> _tween;

  
  Widget build(BuildContext context) {
    
    
    final position = listenable as Animation<double>;
    Offset offset = _tween.evaluate(position);
    //执行反向动画时 再反一下处理
    if (position.status == AnimationStatus.reverse) {
    
    
      switch (direction) {
    
    
        case AxisDirection.up:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.right:
          offset = Offset(-offset.dx, offset.dy);
          break;
        case AxisDirection.down:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.left:
          offset = Offset(-offset.dx, offset.dy);
          break;
      }
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}

Ahora, si queremos lograr varias "animaciones de deslizamiento hacia adentro y hacia afuera", es muy fácil, simplemente pase directiondiferentes valores de dirección, por ejemplo, para lograr "arriba hacia adentro y hacia afuera", luego:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    
    
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return SlideTransitionX(
       child: child,
       direction: AxisDirection.down, //上入下出
       position: animation,
     );
  },
  ...//省略其余代码
)

Efecto:

inserte la descripción de la imagen aquí

Puede intentar SlideTransitionXtomar directiondiferentes valores para ver el efecto de ejecución.

componente de transición de animación

En conjunto, nos referimos a los componentes que realizan la animación de transición cuando las propiedades del widget cambian como "componentes de transición de animación". La característica más obvia de los componentes de transición de animación es que se administran solos internamente AnimationController. Sabemos que para facilitar al usuario la personalización de la curva de animación, el tiempo de ejecución, la dirección, etc., en el método de empaquetado de animación presentado anteriormente, el usuario generalmente necesita proporcionar un objeto para personalizar estos valores de atributo AnimationController. Sin embargo, de esta forma, los usuarios deberán gestionar manualmente AnimationController, lo que aumentará la complejidad de uso. Por lo tanto, si además se puede AnimationControllerempaquetar, mejorará mucho la facilidad de uso de los componentes de animación.

Componentes de transición animados personalizados

Queremos implementar uno AnimatedDecoratedBoxque pueda decorationrealizar una animación de transición del estado anterior al estado nuevo cuando cambia la propiedad. Basándonos en lo que aprendimos anteriormente, implementamos un AnimatedDecoratedBox1componente:

class AnimatedDecoratedBox extends StatefulWidget {
    
    
  const AnimatedDecoratedBox({
    
    
    Key? key,
    required this.decoration,
    required this.child,
    this.curve = Curves.linear,
    required this.duration,
    this.reverseDuration,
  }) : super(key: key);

  final BoxDecoration decoration;
  final Widget child;
  final Duration duration;
  final Curve curve;
  final Duration? reverseDuration;

  
  State createState() => _AnimatedDecoratedBoxState();
}

class _AnimatedDecoratedBoxState extends State<AnimatedDecoratedBox> with SingleTickerProviderStateMixin {
    
    
  
  AnimationController get controller => _controller;
  late AnimationController _controller;

  Animation<double> get animation => _animation;
  late Animation<double> _animation;

  late DecorationTween _tween;

  
  Widget build(BuildContext context) {
    
    
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
    
    
        return DecoratedBox(
          decoration: _tween.animate(_animation).value,
          child: child,
        );
      },
      child: widget.child,
    );
  }

  
  void initState() {
    
    
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      reverseDuration: widget.reverseDuration,
      vsync: this,
    );
    _tween = DecorationTween(begin: widget.decoration);
    _updateCurve();
  }

  void _updateCurve() {
    
    
    _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
  }

  
  void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
    
    
    super.didUpdateWidget(oldWidget);
    if (widget.curve != oldWidget.curve) _updateCurve();
    _controller.duration = widget.duration;
    _controller.reverseDuration = widget.reverseDuration;
    //正在执行过渡动画
    if (widget.decoration != (_tween.end ?? _tween.begin)) {
    
    
      _tween
        ..begin = _tween.evaluate(_animation)
        ..end = widget.decoration;

      _controller
        ..value = 0.0
        ..forward();
    }
  }

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

Usémoslo AnimatedDecoratedBoxpara lograr el efecto de que el color de fondo cambie de azul a rojo después de hacer clic en el botón:

class AnimatedDecoratedBoxExample extends StatefulWidget {
    
    
  const AnimatedDecoratedBoxExample({
    
    Key? key}) : super(key: key);
  
  State createState() => _AnimatedDecoratedBoxExampleState();
}
 
class _AnimatedDecoratedBoxExampleState
    extends State<AnimatedDecoratedBoxExample> {
    
    
  Color _decorationColor = Colors.blue;
  var duration = const Duration(seconds: 1);

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text("自定义动画过渡组件"),
      ),
      body: Center(
        child: AnimatedDecoratedBox(
          duration: duration,
          decoration: BoxDecoration(color: _decorationColor),
          child: TextButton(
            onPressed: () {
    
    
              setState(() {
    
    
                _decorationColor = (_decorationColor == Colors.blue
                    ? Colors.red
                    : Colors.blue);
              });
            },
            child: const Text(
              "AnimatedDecoratedBox",
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

Efecto:

inserte la descripción de la imagen aquí

Aunque el código anterior logra las funciones que esperamos, el código es más complicado. Después de pensar un poco, podemos encontrar que AnimationControllerla parte de administración y Tweenactualización del código se puede abstraer.Si encapsulamos estas lógicas generales en clases base, luego para implementar componentes de transición de animación, solo necesitamos heredar estas clases base y personalizarnos. Un código diferente (como el método de construcción de cada fotograma de la animación) es suficiente, lo que simplificará el código.

Para facilitar a los desarrolladores la realización de la encapsulación de los componentes de transición de animación, Flutter proporciona una ImplicitlyAnimatedWidgetclase abstracta, que hereda y proporciona una clase StatefulWidgetcorrespondiente , y la gestión está en la clase. Si los desarrolladores quieren encapsular la animación, solo necesitan heredar y clasificar respectivamente. Demostremos cómo lograrlo.ImplicitlyAnimatedWidgetStateAnimationControllerImplicitlyAnimatedWidgetStateImplicitlyAnimatedWidgetImplicitlyAnimatedWidgetState

Necesitamos hacer esto en dos pasos:

  1. Clase de herencia ImplicitlyAnimatedWidget.
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
    
    
  const AnimatedDecoratedBox({
    
    
    Key? key,
    required this.decoration,
    required this.child,
    Curve curve = Curves.easeIn, //动画曲线
    required Duration duration, // 正向动画执行时长
  }) : super(
          key: key,
          curve: curve,
          duration: duration,
        );
  final BoxDecoration decoration;
  final Widget child;

  
  ImplicitlyAnimatedWidgetState createState() => _AnimatedDecoratedBoxState();
}

Tres de estas curve、duration、reverseDurationpropiedades ImplicitlyAnimatedWidgetse definen en . Puede ver que la clase no es diferente de la clase de la que AnimatedDecoratedBoxnormalmente hereda .StatefulWidget

  1. Deje que Statela clase herede AnimatedWidgetBaseState(la clase hereda de ImplicitlyAnimatedWidgetStatela clase).
class _AnimatedDecoratedBoxState
    extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
    
    
  Tween<dynamic>? _decoration; //定义一个Tween

  
  Widget build(BuildContext context) {
    
    
    return DecoratedBox(
      decoration: _decoration?.evaluate(animation),
      child: widget.child,
    );
  }
 
  
  void forEachTween(TweenVisitor<dynamic> visitor) {
    
    
    // 在需要更新Tween时,基类会调用此方法
    _decoration = visitor(_decoration, widget.decoration,
        (value) => DecorationTween(begin: value));
  }
}

Puede ver que hemos implementado builddos forEachTweenmétodos. Durante la ejecución de la animación, buildse llamará al método en cada cuadro (la lógica de llamada está ImplicitlyAnimatedWidgetStateincluida), por lo que buildnecesitamos construir DecoratedBoxel estado de cada cuadro en el método, por lo que tenemos que calcular decorationel estado de cada cuadro, que podemos _decoration.evaluate(animation)calcular por, ¿dónde animationestá? ImplicitlyAnimatedWidgetStateEl objeto definido en la clase base _decorationes un tipo de objeto que personalizamos DecorationTween, por lo que la pregunta ahora es ¿cuándo se asigna?

Para responder a esta pregunta, tenemos que averiguar cuándo necesitamos asignar _decorationun valor. Sabemos _decorationque es uno Tween, y Tweenla responsabilidad principal es definir el estado inicial ( begin) y el estado final ( end) de la animación. Para AnimatedDecoratedBox, decorationel estado final es el valor que le pasa el usuario, y el estado inicial es incierto.Hay dos situaciones:

  1. AnimatedDecoratedBoxPor primera vez build, su valor se establece directamente decorationen el estado inicial en este momento, es decir, _decorationel valor es DecorationTween(begin: decoration).
  2. AnimatedDecoratedBoxCuando la actualización de decoration, el estado inicial es _decoration.animate(animation), es decir, _decorationel valor es DecorationTween(begin: _decoration.animate(animation),end:decoration).

Ahora forEachTweenla función es obvia, se usa para actualizar Tweenel valor inicial, se llamará en los dos casos anteriores, y el desarrollador solo necesita reescribir este método y actualizar el Tweenestado inicial en el valor de este método. Y parte de la lógica de actualización está protegida en visitorla devolución de llamada, solo necesitamos llamarla y pasarle los parámetros correctos, visitorla firma del método es la siguiente:

 Tween<T> visitor(
   Tween<T> tween, //当前的tween,第一次调用为null
   T targetValue, // 终止状态
   TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
 );

Se puede ver que la encapsulación de los componentes de transición de la animación se puede realizar rápidamente a través de la herencia ImplicitlyAnimatedWidgety las clases, lo que simplifica mucho el código en comparación con nuestra implementación manual pura.ImplicitlyAnimatedWidgetState

Componentes de transición de animación preestablecidos de Flutter

También hay muchos componentes de transición de animación preestablecidos en Flutter SDK, y los métodos de implementación son AnimatedDecoratedBoxsimilares a la mayoría de ellos, como se muestra en la siguiente tabla:

Nombre del componente Función
AnimatedPadding paddingLas animaciones de transición se realizan al nuevo estado cuando ocurre un cambio
AnimatedPositioned Usados Stack​​juntos, cuando cambia el estado de posicionamiento, la animación de transición se realizará al nuevo estado
AnimatedOpacity opacityRealice una animación de transición a un nuevo estado cuando cambie la transparencia
AnimatedAlign Cuando alignmentocurre un cambio, se realiza una animación de transición al nuevo estado
AnimatedContainer Cuando Containerla propiedad cambie, la animación de transición se realizará al nuevo estado.
AnimatedDefaultTextStyle Cuando el estilo de fuente cambia, el componente de texto que hereda el estilo en el componente secundario cambiará dinámicamente al nuevo estilo.

Usemos un ejemplo para sentir los efectos de estos componentes de transición de animación preestablecidos:

import 'package:flutter/material.dart';

class AnimatedWidgetsTest extends StatefulWidget {
    
    
  const AnimatedWidgetsTest({
    
    Key? key}) : super(key: key);

  
  State createState() => _AnimatedWidgetsTestState();
}

class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
    
    
  double _padding = 10;
  var _align = Alignment.topRight;
  double _height = 100;
  double _left = 0;
  Color _color = Colors.red;
  TextStyle _style = const TextStyle(color: Colors.black);
  Color _decorationColor = Colors.blue;

  
  Widget build(BuildContext context) {
    
    
    var duration = const Duration(seconds: 1);
    return SingleChildScrollView(
      child: Column(
        children: <Widget>[
          ElevatedButton(
            onPressed: () => setState(() => _padding = 20),
            child: AnimatedPadding(
              duration: duration,
              padding: EdgeInsets.all(_padding),
              child: const Text("AnimatedPadding"),
            ),
          ),
          SizedBox(
            height: 50,
            child: Stack(
              children: <Widget>[
                AnimatedPositioned(
                  duration: duration,
                  left: _left,
                  child: ElevatedButton(
                    onPressed: () => setState(() => _left = 100),
                    child: const Text("AnimatedPositioned"),
                  ),
                )
              ],
            ),
          ),
          Container(
            height: 100,
            color: Colors.grey,
            child: AnimatedAlign(
              duration: duration,
              alignment: _align,
              child: ElevatedButton(
                onPressed: () => setState(() => _align = Alignment.center),
                child: const Text("AnimatedAlign"),
              ),
            ),
          ),
          AnimatedContainer(
            duration: duration,
            height: _height,
            color: _color,
            child: TextButton(
              onPressed: () {
    
    
                setState(() {
    
    
                  _height = 150;
                  _color = Colors.blue;
                });
              },
              child: const Text(
                "AnimatedContainer",
                style: TextStyle(color: Colors.white),
              ),
            ),
          ),
          AnimatedDefaultTextStyle(
            style: _style,
            duration: duration,
            child: GestureDetector(
              child: const Text("hello world"),
              onTap: () {
    
    
                setState(() {
    
    
                  _style = const TextStyle(
                    color: Colors.blue,
                    decorationStyle: TextDecorationStyle.solid,
                    decorationColor: Colors.blue,
                  );
                });
              },
            ),
          ),
          AnimatedDecoratedBox(
            duration: duration,
            decoration: BoxDecoration(color: _decorationColor),
            child: TextButton(
              onPressed: () => setState(() => _decorationColor = Colors.red),
              child: const Text(
                "AnimatedDecoratedBox",
                style: TextStyle(color: Colors.white),
              ),
            ),
          )
        ].map((e) {
    
    
          return Padding(
            padding: const EdgeInsets.symmetric(vertical: 16),
            child: e,
          );
        }).toList(),
      ),
    );
  }
}

resultado de ejecución:

inserte la descripción de la imagen aquí

Análisis de código fuente de animación.

AnimationLas clases clave y sus relaciones se muestran en la Figura 8-5.

inserte la descripción de la imagen aquí

En la Figura 8-5, Animationes la clase clave de animación, que hereda y Listenablemantiene Animationun valor de animación ( value) y un estado de animación ( status) para monitoreo externo. Los usuarios externos actualizarán la interfaz de usuario y otras operaciones en función de estas devoluciones de llamada de monitoreo.

AnimationControllerEs Animationla implementación más común, contiene dos campos clave, Tickery Simulationel primero lo TickerProviderproporciona la interfaz, que se usa para proporcionar el "latido" para impulsar las actualizaciones de animación, se usa principalmente para la animación interpolada; el último proporciona reglas de actualización para los valores de animación, implementado por subclases concretas.

SimulationLa implementación predeterminada de es _InterpolationSimulationque Curvecalcula valores basados ​​en subclases concretas de . Además, Simulationtambién brinda la capacidad de simular varios efectos físicos, como SpringSimulationla simulación de efectos de resorte.

En términos generales, el campo AnimationControllerde valueno se usa directamente, _AnimatedEvaluationcontendrá un Animationobjeto (generalmente una AnimationControllerinstancia de ) y un Animatableobjeto, el primero proporciona el valor original de la animación y el segundo usa el valor de animación del primero ( value) como un parámetro para “tweening” ( Tween), el valor final será expuesto externamente a través del campo _AnimatedEvaluationde la instancia .value

El siguiente es un análisis más específico desde la perspectiva del código fuente.

interpolación de movimiento

La animación de interpolación solo se interpola de acuerdo con una determinada regla dentro de un rango de tiempo específico, y su proceso de uso generalmente se muestra en el Listado 8-28.

// 代码清单8-28 补间动画示例
AnimationController animationController = // 见代码清单8-29
         AnimationController(vsync: this, duration: Duration(milliseconds: 1000));
Animation animation =  // 见代码清单8-30
         Tween(begin: 0.0,end: 10.0).animate(animationController);
animationController.addListener(() {
    
     // 通知发生,见代码清单8-40
 setState(() {
    
     newValue = animation.value });
});
animationController.forward(); // 见代码清单8-32

En la lógica anterior, AnimationControlleres el controlador el que Tweenproporciona el modelo de interpolación de la animación de interpolación Animationcomo salida de llamada final. El siguiente es un análisis en profundidad del código.

Primero analice AnimationControllerla lógica de inicialización, como se muestra en el Listado 8-29.

// 代码清单8-29 flutter/packages/flutter/lib/src/animation/animation_controller.dart
AnimationController({
    
      ......, required TickerProvider vsync,
    }) : _direction = _AnimationDirection.forward {
    
    
  _ticker = vsync.createTicker(_tick); // _tick将在"心跳"时触发,见代码清单8-38
  _internalSetValue(value ?? lowerBound); // 更新当前动画的状态
}
void _internalSetValue(double newValue) {
    
    
  _value = newValue.clamp(lowerBound, upperBound);
  if (_value == lowerBound) {
    
    
    _status = AnimationStatus.dismissed; // 动画尚未开始
  } else if (_value == upperBound) {
    
    
    _status = AnimationStatus.completed; // 已经结束
  } else {
    
    
    _status = (_direction == _AnimationDirection.forward) ? // 动画进行中
            AnimationStatus.forward : AnimationStatus.reverse;
  }
}  

AnimationControllerEn la lógica de inicialización de , primero cree un Tickerobjeto, que es el controlador central de la animación de interpolación de movimiento, que se analizará en detalle más adelante. _internalSetValueResponsable de actualizar el estado de la animación actual, por lo que no entraré en detalles aquí.

En el Listado de Código 8-28, animation.valueel valor actual se obtendrá cuando se actualice cada cuadro de animación, y el proceso de cálculo se muestra en el Listado de Código 8-30.

// 代码清单8-30 flutter/packages/flutter/lib/src/animation/tween.dart 
abstract class Animatable<T> {
    
    
  T transform(double t); // 见代码清单8-31
  T evaluate(Animation<double> animation) => transform(animation.value);
  Animation<T> animate(Animation<double> parent) {
    
    
    return _AnimatedEvaluation<T>(parent, this);
  }
} // Animatable
class _AnimatedEvaluation<T> extends Animation<T>
             with AnimationWithParentMixin<double> {
    
    
  _AnimatedEvaluation(this.parent, this._evaluatable);
  
  final Animation<double> parent; // 通常为AnimationController 
  final Animatable<T> _evaluatable; // 见代码清单8-31
    // 代码清单8-28中获取的值
  T get value => _evaluatable.evaluate(parent);  // 见前面内容
}

TweenEs Animatableuna subclase cuyo animatemétodo es implementado por la clase padre y principalmente devuelve _AnimatedEvaluationel objeto, por lo que animation.valueal llamar, la esencia es llamar al modelo de interpolación específico, es decir, el método de Animatablela subclase específica (como ) , pero de hecho llama al método, como ejemplo, its La lógica se muestra en el Listado 8-31.TweenevaluateevaluatetransformTween

// 代码清单8-31 flutter/packages/flutter/lib/src/animation/tween.dart
class Tween<T extends dynamic> extends Animatable<T> {
    
    
  Tween({
    
    this.begin, this.end,});
  
  T lerp(double t) {
    
     // Linear Interpolation
    return begin + (end - begin) * t as T;
  }
  
  T transform(double t) {
    
     // t 即AnimationController的值,在代码清单8-33中进行定义
    if (t == 0.0) return begin as T;
    if (t == 1.0) return end as T;
    return lerp(t); // 线性差值的逻辑
  }
}

La lógica anterior es un proceso de interpolación lineal típico. El parámetro es AnimationController.valueque el valor se actualizará en cada cuadro, y Tweenla interpolación lineal se completará en función de este valor, y el resultado será _AnimatedEvaluationel valor del objeto.

A continuación, analice AnimationController.valueel mecanismo impulsor actualizado, es decir, forwardel método, como se muestra en el Listado 8-32.

// 代码清单8-32 flutter/packages/flutter/lib/src/animation/animation_controller.dart
TickerFuture forward({
    
     double? from }) {
    
     // AnimationController
  _direction = _AnimationDirection.forward;
  if (from != null) value = from;
  return _animateToInternal(upperBound);
}
TickerFuture _animateToInternal(double target, // 即upperBound
    {
    
     Duration? duration, Curve curve = Curves.linear }) {
    
    
  double scale = 1.0;
  if (SemanticsBinding.instance!.disableAnimations) {
    
     ...... } // SKIP 禁用动画的逻辑
  Duration? simulationDuration = duration; // 第1步,计算动画的执行时长
  if (simulationDuration == null) {
    
     // 没有指定动画时长
    final double range = upperBound - lowerBound; // 根据当前进度,以1s为基准计算
    final double remainingFraction = range.isFinite ? (target - _value).abs() / 
       range : 1.0;
    final Duration directionDuration = // 根据动画是顺序还是逆序来计算最终的执行时长
      (_direction == _AnimationDirection.reverse && reverseDuration != null)
      ? reverseDuration! : this.duration!;
    simulationDuration = directionDuration * remainingFraction;
  } else if (target == value) {
    
     // 动画已完成,对应第2步
    simulationDuration = Duration.zero;
  }
  stop(); // 见代码清单8-41
  if (simulationDuration == Duration.zero) {
    
     // 第2步,动画结束,完成字段更新,并通知
    if (value != target) {
    
    
      _value = target.clamp(lowerBound, upperBound); // 修正值到合法的目标值
      notifyListeners(); // 通知值变化,见代码清单8-40
    }
    _status = (_direction == _AnimationDirection.forward) ?
           AnimationStatus.completed : AnimationStatus.dismissed;
    _checkStatusChanged(); // 更新动画状态,见代码清单8-40
    return TickerFuture.complete();
  } // if
  return _startSimulation(_InterpolationSimulation( // 第3步,开始动画
          _value, target, simulationDuration, curve, scale));
}

La lógica anterior se divide principalmente en 3 pasos. El primer paso es simulationDurationel cálculo, que se calculará en función de la relación entre el valor restante y el valor total, stopy la lógica se introducirá más adelante. El segundo paso es principalmente lidiar simulationDurationcon 0la situación de que la animación ha terminado, y en este momento se realiza la asignación del campo y la notificación del estado. El tercer paso es comenzar realmente la animación. Tenga en cuenta que Simulationla clase de implementación específica aquí es _InterpolationSimulationque es un simulador de interpolación lineal, que se analizará en detalle más adelante. La lógica del primer análisis _startSimulationse muestra en el Listado 8-33.

// 代码清单8-33 flutter/packages/flutter/lib/src/animation/animation_controller.dart

double get value => _value;
TickerFuture _startSimulation(Simulation simulation) {
    
     // AnimationController
  _simulation = simulation;
  _lastElapsedDuration = Duration.zero; // 截止到上一帧动画,已消耗的时间
  _value = simulation.x(0.0).clamp(lowerBound, upperBound); 
// 起始值,x方法见代码清单8-39
  final TickerFuture result = _ticker!.start(); // 开始请求"心跳",驱动动画
  _status = (_direction == _AnimationDirection.forward) ?
                AnimationStatus.forward : AnimationStatus.reverse;
  _checkStatusChanged();
  return result;
}

La lógica anterior usa _ticker!.starel método t para iniciar la animación y actualizar _statusel estado al mismo tiempo.La startlógica del método se muestra en el Listado 8-34.

// 代码清单8-34 flutter/packages/flutter/lib/src/scheduler/ticker.dart
TickerFuture start() {
    
    
  _future = TickerFuture._(); // 表示一个待完成的动画
  if (shouldScheduleTick) {
    
     scheduleTick(); } // 请求"心跳"
  if (SchedulerBinding.instance!.schedulerPhase.index
          > SchedulerPhase.idle.index
      && SchedulerBinding.instance!.schedulerPhase.index
          < SchedulerPhase.postFrameCallbacks.index) // 在这个阶段内应修正动画开始时间
    _startTime = SchedulerBinding.instance!.currentFrameTimeStamp;
  return _future!;
}

La lógica anterior es principalmente para scheduleTickiniciar un "latido" a través del método, y hay un detalle que necesita atención: si un marco se está procesando actualmente, se _startTimecontará desde Vsyncel tiempo de llegada de la señal del marco actual. una pequeña corrección, a saber, si un cuadro ya se está procesando cuando comienza la animación, el estado de animación para el siguiente cuadro debe ser equivalente al estado de la animación dos cuadros más tarde. scheduleTickLa lógica del método se muestra en el Listado 8-35.

// 代码清单8-35 flutter/packages/flutter/lib/src/scheduler/ticker.dart
bool get muted => _muted; // 是否为静默状态
bool _muted = false;
bool get isActive => _future != null; // 存在动画

bool get scheduled => _animationId != null; // 已经在等待"心跳"
 // 调用逻辑见代码清单8-37,用于判断是否需要"心跳"
bool get shouldScheduleTick => !muted && isActive && !scheduled;

void scheduleTick({
    
     bool rescheduling = false }) {
    
    
  _animationId = SchedulerBinding.instance!.scheduleFrameCallback(
                      _tick, rescheduling: rescheduling); // 见代码清单8-36
}

En la lógica anterior, se marcaron las funciones de varios campos de atributos clave y scheduleFrameCallbackel método _tickregistrará la función en la lista de devolución de llamada cuando Vsyncllegue la siguiente señal, como se muestra en el Listado 8-36.

// 代码清单8-36 flutter/packages/flutter/lib/src/scheduler/ticker.dart
int scheduleFrameCallback(FrameCallback callback, {
    
     bool rescheduling = false }) {
    
    
  scheduleFrame(); // 见代码清单5-20
  _nextFrameCallbackId += 1;
  _transientCallbacks[_nextFrameCallbackId] // 处理逻辑见代码清单5-36
      = _FrameCallbackEntry(callback, rescheduling: rescheduling);
  return _nextFrameCallbackId;
}

_transientCallbackscallbackSu devolución de llamada , es decir, el método, se procesará después de que llegue la señal Vsync _tick, como se muestra en el Listado 8-37.

// 代码清单8-37 flutter/packages/flutter/lib/src/scheduler/ticker.dart
void _tick(Duration timeStamp) {
    
     // Ticker
  _animationId = null;
  _startTime ??= timeStamp; // 首次"心跳"的时间戳,timeStamp是当次"心跳"的时间戳
  // 注意这里的 ??= 语法表明只会记录首次"心跳"发生时的时间戳
  _onTick(timeStamp - _startTime!); // 见代码清单8-38,参数为动画已经执行的时间
  if (shouldScheduleTick) scheduleTick(rescheduling: true); 
// 如有必要,等待下一次"心跳"
}

Hasta ahora, se puede decir con certeza que el llamado "latido del corazón" ( tick) es en realidad Vsyncuna señal, _startTimeque solo se asignará una vez, indicando la marca de tiempo del inicio de la animación, para luego llamar _onTickal método con la diferencia de tiempo. como parámetro (es decir, el parámetro del contenido anterior - _tickfunción), si la lógica shouldScheduleTickDespués de true, continuará registrando Vsyncla señal para impulsar la generación del siguiente "latido". La lógica del método de análisis a continuación _tickse muestra en el Listado 8-38.

// 代码清单8-38 flutter/packages/flutter/lib/src/animation/animation_controller.dart
// 该方法的注册逻辑位于代码清单8-29的createTicker方法中
void _tick(Duration elapsed) {
    
     // AnimationController
  _lastElapsedDuration = elapsed; // 动画已经执行的时间
  final double elapsedInSeconds = // 毫秒转秒
    elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
  assert(elapsedInSeconds >= 0.0);
  _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound); // 插值
  if (_simulation!.isDone(elapsedInSeconds)) {
    
     // 判断是否完成,见代码清单8-39
    _status = (_direction == _AnimationDirection.forward) ? // 完成则更新状态
                AnimationStatus.completed :AnimationStatus.dismissed;
    stop(canceled: false); // 停止动画,见代码清单8-41
  } // 否则,代码清单8-37中的shouldScheduleTick为true,将继续等待"心跳"(请求Vsync)
  notifyListeners(); // 见代码清单8-40
  _checkStatusChanged();
}

La lógica anterior primero calcula la duración de la animación que se ha ejecutado en segundos, luego llama _simulational xmétodo para calcular el valor actual y finalmente juzga si la animación está completa y transmite _valuelos cambios de sus propios campos.

Primero analice xel método, Tweenla animación por defecto es _InterpolationSimulation, como se muestra en el Listado 8-39.

// 代码清单8-39 flutter/packages/flutter/lib/src/animation/animation_controller.dart
 // _InterpolationSimulation
double x(double timeInSeconds) {
    
    
  final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
  if (t == 0.0) return _begin;
  else if (t == 1.0) return _end;
  else return _begin + (_end - _begin) * _curve.transform(t);
}
 // 是否完成完全取决于动画执行的时间,注意与代码清单8-44中介绍的物理动画进行区分
bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;

En la lógica anterior, _curveel valor predeterminado es Curves.lineardevolverse ta sí mismo, por lo que el valor devuelto es tuna función lineal y el coeficiente es ( _end - _begin).

En segundo lugar, analice _valuela lógica de notificación después de completar el cálculo, como se muestra en la lista de códigos 8-40.

// 代码清单8-40 flutter/packages/flutter/lib/src/animation/animation_controller.dart
void notifyListeners() {
    
    
  final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
  for (final VoidCallback listener in localListeners) {
    
    
    InformationCollector? collector;
    try {
    
    
      if (_listeners.contains(listener)) listener(); // 通知监听器
    } catch (exception, stack) {
    
     ...... }
  }
}
void _checkStatusChanged() {
    
    
  final AnimationStatus newStatus = status;
  if (_lastReportedStatus != newStatus) {
    
     // 状态发生改变才需要通知
    _lastReportedStatus = newStatus;
    notifyStatusListeners(newStatus);
  }
}

En la lógica anterior, notifyListenersel método es responsable del _valuecambio de la notificación, que básicamente se llama cada cuadro; notifyStatusListenerses responsable del cambio del estado de la notificación y solo se activará cuando cambie el estado de la animación.

La lógica del análisis final isDone. Para _InterpolationSimulationello, su lógica se muestra en la lista de códigos 8-39, es decir, se detiene cuando el tiempo de ejecución supera el tiempo objetivo. Esta es también la característica de la animación interpolada, es decir, el tiempo es el factor determinante del "latido" de la animación, y los usuarios solo pueden personalizar reglas de interpolación específicas.

Continuemos analizando stopcómo el método detiene la animación, como se muestra en el Listado 8-41.

// 代码清单8-41 flutter/packages/flutter/lib/src/animation/animation_controller.dart
void stop({
    
     bool canceled = false }) {
    
    
  if (!isActive) return;
  final TickerFuture localFuture = _future!; 
  _future = null;
  _startTime = null;
  unscheduleTick(); // 停止等待"心跳"
  if (canceled) {
    
    
    localFuture._cancel(this);
  } else {
    
    
    localFuture._complete();
  }
}

La lógica anterior es principalmente para restablecer los campos relacionados con la animación, por lo que no entraré en detalles aquí.

En general, los desarrolladores también pueden impulsar la ejecución del siguiente cuadro builda través del método en el método para lograr el efecto de la animación, pero proporciona un marco de recursos flexible, escalable y fácil de administrar, lo que ahorra mucho de recursos para desarrolladores energía.Future+setStateAnimation

Resumir:

La esencia de la animación de interpolación es registrar Vsyncla devolución de llamada de la señal a la capa inferior:

  • ticker.startDespués de que el método inicie la animación, llame scheduleTick--> llame SchedulerBinding.instance!.scheduleFrameCallback--> llame scheduleFrame()para pasar por el proceso de dibujo,

  • Y FrameCallbackejecutará _tickel método, que onTickllamará notifyListeners()a la animación de notificación para monitorear y recuperar valueel cambio de la animación actual, que se llamará básicamente en cada cuadro; notifyStatusListenerses responsable del cambio del estado de notificación, que solo se activará cuando el estado de la animación cambia.

Suplemento: scheduleFrameLa lógica del método se muestra en el Listado de Código 5-20.Iniciará una solicitud al motor a través de la interfaz, solicitando renderizado cuando llegue window.scheduleFramela siguiente señal.Vsync

// 代码清单5-20 flutter/packages/flutter/lib/src/scheduler/binding.dart
void scheduleFrame() {
    
    
  if (_hasScheduledFrame || !framesEnabled) return;
  ensureFrameCallbacksRegistered();  
  window.scheduleFrame(); 
  _hasScheduledFrame = true;
}

animación física

Aunque la animación de interpolación es flexible, su tiempo suele ser fijo, lo que no es adecuado para algunas escenas. Tome la Figura 8-6 como ejemplo.Cuando el usuario arrastra el cuadro fuera del punto central, si desea que el cuadro vuelva a su posición original con un efecto de animación arrastrado por un resorte, es difícil realizar la animación de interpolación, porque el tiempo de finalización de la animación depende de la distancia de deslizamiento del usuario, la velocidad y varias propiedades del resorte. En este punto, debe usar la animación Physics (Física).

inserte la descripción de la imagen aquí

La lista de códigos 8-42 es el código de implementación clave para el efecto de animación de la figura 8-6.

// 代码清单8-42 物理动画示例
void _runAnimation(Offset pixelsPerSecond,  Size size) {
    
    
  // pixelsPerSecond表示拖曳手势结束、动画开始时
  // 方块由于拖曳手势而在X、Y方向上因物理惯性而产生的移动速度
  _animation = _controller.drive( // 触发AlignmentTween的animate方法
    AlignmentTween(begin: _dragAlignment, end: Alignment.center,));
  // 由6.2节可知,由于Alignment的坐标系是[0,1]形式的,因此需要除以屏幕大小,转为比例
  final unitsPerSecondX = pixelsPerSecond.dx / size.width;
  final unitsPerSecondY = pixelsPerSecond.dy / size.height;
  final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
  final unitVelocity = unitsPerSecond.distance;
  const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1,); 
	// 弹簧的各种属性
  final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); 
	// 构造一个弹簧物理模型
  _controller.animateWith(simulation);
}

void initState() {
    
    
  super.initState();
  _controller = AnimationController(vsync: this);
  _controller.addListener(() {
    
    
    setState(() {
    
     _dragAlignment = _animation.value;});
  });
}

En la lógica anterior, pixelsPerSecondes un parámetro transportado por la devolución de llamada del gesto de arrastre, que indica la velocidad de deslizamiento del usuario actual, es decir, la velocidad a la que se extrae la caja. _dragAlignmentRepresenta la posición en tiempo real del bloque, por lo que cuando _runAnimationel método comienza a ejecutarse, su valor es exactamente la posición real de la animación. En este punto, sabemos dónde comienza y termina el cubo, y la velocidad a la que termina el gesto de arrastre. A continuación se encuentra la construcción del modelo físico del proceso de deslizamiento, principalmente los SpringSimulationparámetros calculados, unitVelocityes decir, la velocidad de deslizamiento del resorte en la dirección lineal, opuesta a la dirección del resorte.

El siguiente animateWithmétodo de análisis directo, como se muestra en la lista de códigos 8-43.

// 代码清单8-43 flutter/packages/flutter/lib/src/animation/animation_controller.dart
TickerFuture animateWith(Simulation simulation) {
    
    
  stop(); // 见代码清单8-41
  _direction = _AnimationDirection.forward;
  return _startSimulation(simulation); // 见代码清单8-33
}

Dado que _startSimulationse ha analizado el contenido anterior de la lógica, el método Simulationdel xmétodo se puede analizar directamente isDone, como se muestra en la lista de códigos 8-44.

// 代码清单8-44 flutter/packages/flutter/lib/src/physics/spring_simulation.dart
class SpringSimulation extends Simulation {
    
    
  final _SpringSolution _solution; // 弹簧物理模型的抽象表示
  
  double x(double time) => _endPosition + _solution.x(time);
  
  double dx(double time) => _solution.dx(time);
  
  bool isDone(double time) {
    
    
    return nearZero(_solution.x(time), tolerance.distance) &&
           nearZero(_solution.dx(time), tolerance.velocity);
  }
}

De la lógica anterior, podemos ver SpringSimulationque la lógica principal del campo se transfiere al _solutioncampo, que es creado por el código en el Listado 8-42 SpringDescription, y el modelo físico del resorte no se analizará específicamente aquí. SpringSimulationEl isDonemétodo también es relativamente fácil de entender, es decir, para juzgar si la posición actual ha alcanzado la posición de destino. Esta es también una diferencia clave entre la animación de interpolación y la animación física: isDonela realización del método: juzgue si terminar según si la posición física cumple con la condición en lugar del factor tiempo.


referencia:

Supongo que te gusta

Origin blog.csdn.net/lyabc123456/article/details/130913731
Recomendado
Clasificación