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、Tween
estos cuatro personajes. Trabajan juntos para completar una animación completa. Vamos a presentarlos uno por uno. ellos.
1. Animación
Animation
Animation
Es 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>
.
Animation
Tween
Un objeto es una clase que genera secuencialmente valores entre un intervalo ( ) durante un período de tiempo . Animation
El 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., Curve
dependiendo de decisión.
Dependiendo de Animation
có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.
Animation
También es posible generar double
otros 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 Animation
del objeto .value
notificación de animación
Podemos Animation
monitorear cada cuadro de la animación y el cambio del estado de ejecución Animation
a través de los siguientes dos métodos:
addListener()
; se puede usar paraAnimation
agregar 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 estadosetState()
para desencadenar una reconstrucción de la interfaz de usuario.addStatusListener()
; puedeAnimation
agregar un oyente de "cambio de estado de animación"; elAnimationStatus
oyente 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 CurvedAnimation
especificar la curva de la animación por, como:
final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
CurvedAnimation
y AnimationController
(descrito a continuación) son Animation<double>
ambos tipos. CurvedAnimation
Podemos envolver AnimationController
y Curve
generar 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. Curves
La 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, Curves
hay muchas otras curvas definidas en la clase, puede verificar Curves
la 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
AnimationController
Se 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()
AnimationController
AnimationController
0.0
1.0
Por ejemplo, el siguiente código crea un Animation
objeto (pero no inicia una animación):
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
Entre ellos, duration
representa la duración de la ejecución de la animación, a través del cual podemos controlar la velocidad de la animación.
AnimationController
El rango de números generados se puede especificar mediante lowerBound
y upperBound
, como:
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
AnimationController
Derivado de Animation<double>
, por lo que se puede Animation
usar en cualquier lugar donde se espere un objeto.
Además, AnimationController
existen 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, Animation
el 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
AnimationController
rango[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,CurvedAnimation
la salida del puede tener un rango mayor que la entrada. Por ejemplo,Curves.elasticIn
las curvas isoelásticas generan valores que son mayores o menores que el rango predeterminado.
Corazón
Al crear uno AnimationController
, es necesario pasar un vsync
parámetro, que recibe un TickerProvider
tipo 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 SchedulerBinding
agregar 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 .Ticker
SchedulerBinding
TickerCallback
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.Ticker
SchedulerBinding
Ticker
Por lo general, mezclaremos SingleTickerProviderStateMixin
la clase en la State
clase personalizada y luego usaremos el State
objeto actual como el valor AnimationController
del vsync
parámetro.
4. Interpolación
1. Introducción
Por defecto, AnimationController
el 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 Tween
para agregar asignaciones para generar valores de diferentes rangos o tipos de datos.
Por ejemplo, como en el siguiente ejemplo, el valor Tween
generado :[-200.0,0.0]
final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);
Tween
El constructor toma begin
y end
dos parámetros. Tween
La ú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.
Tween
Heredar de Animatable<T>
, en lugar de heredar de Animation<T>
, Animatable
define principalmente las reglas de asignación para los valores de animación.
Veamos un ColorTween
ejemplo 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);
Tween
El 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. Animation
El 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.0
1.0
2)Entre.animado
Para usar Tween
un objeto, llamas a su animate()
método, pasando un AnimationController
objeto.
Por ejemplo, el siguiente código genera valores enteros de a en 500
milisegundos .0
255
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 lerp
métodos estáticos (interpolación lineal) , como:
// a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);
lerp
El cálculo de generalmente sigue: 返回值 = a + (b - a) * t
, y otras lerp
clases 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 lerp
es una interpolación lineal, lo que significa que el valor de retorno y el progreso de la animación t
están en una y = kx + b
relació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 t
asignar a , por ejemplo, para lograr un efecto de aceleración uniforme, luego t' = at²+bt+c
especificar la aceleración a
y 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 bounce
efecto de resorte ( )), y Curve
el principio de realizar la animación de acuerdo con diferentes curvas se t
realiza 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 widget
al 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()
Image
animation.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 Curve
para lograr un proceso de animación similar a un efecto de resorte. Solo necesitamos initState
cambiar 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:
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. AnimatedWidget
La clase encapsula setState()
los detalles de la llamada y nos permite separarlos.El widget
có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 AnimatedWidget
se 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 widget
animació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 AnimatedBuilder
es para separar la lógica de renderizado, build
el 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 child
parece que se especifica dos veces. Pero lo que realmente sucede es que la referencia externa child
se pasa al constructor anónimo, que luego usa el objeto como su elemento secundario AnimatedBuilder
. AnimatedBuilder
El resultado final es que AnimatedBuilder
el objeto devuelto se inserta en widget
el árbol.
Tal vez dirá que esto no es muy diferente de nuestro ejemplo inicial, pero traerá tres beneficios:
-
No hay necesidad de agregar explícitamente un frame listener y luego llamarlo
setState()
, el beneficio esAnimatedWidget
el mismo que el de . -
Mejor rendimiento: debido a que
widget
el alcance de cada cuadro de la animación que se debe construir se reduce, si nobuilder
,setState()
se llamará en el contexto del componente principal, lo que hará quebuild
se vuelva a llamar al método del componente principal; después de haberbuilder
ello, sólo provocará la animaciónwidget
Auto-build
recuerdo, evitando innecesariosrebuild
. -
Las animaciones se pueden reutilizar encapsulando
AnimatedBuilder
efectos de transición comunes. IlustremosGrowTransition
encapsulando uno, que puedewidget
realizar 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
, SizeTransition
etc. 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, AnimationStatus
que 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 MaterialPageRoute
componente 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 Android
tambié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(),
));
CupertinoPageRoute
Es Cupertino
un 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 PageRouteBuilder
es
Echemos un vistazo a cómo usar PageRouteBuilder
para 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 pageBuilder
que hay un parámetro, proporcionado por el administrador de rutas de Flutter, y se volverá a llamar en cada cuadro de animación animation
cuando se cambie la ruta , para que podamos personalizar la animación de transición a través del objeto.pageBuilder
animation
Ya sean MaterialPageRoute
, CupertinoPageRoute
o PageRouteBuilder
, todos heredan de la PageRoute
clase, pero PageRouteBuilder
en realidad son solo PageRoute
un contenedor. Podemos heredar directamente PageRoute
la clase para implementar un enrutamiento personalizado. El ejemplo anterior se puede implementar de la siguiente manera:
- definir una clase de enrutamiento
FadeRoute
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),
);
}
}
- usar
FadeRoute
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 PageRouteBuilder
no 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, PageRoute
si solo queremos aplicar al abrir una nueva Animación de ruta, pero no use animación al regresar, entonces debemos juzgar isActive
si 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
Hero
Se refiere a la capacidad de "volar" entre rutas (páginas) widget
En términos simples, Hero
la animación significa que cuando se cambia de ruta, hay una compartida que widget
puede cambiar entre rutas antiguas y nuevas. Dado que la widget
posició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 hero
la 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 hero
la 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:
Podemos ver que para realizar Hero
la animación, solo necesita Hero
envolver los componentes compartidos widget
y 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 mismoHero
tag
, y el marco Flutter se utiliza tag
para determinar widget
la relación correspondiente entre las páginas de enrutamiento antiguas y nuevas.
Hero
El 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:
- Para crear una animación intercalada, se utilizan varios objetos de animación ( )
Animation
. - Uno
AnimationController
controla todos los objetos de animación. - 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.0
entre 1.0
y , y el intervalo ( Interval
) de cada animación debe estar entre 0.0
y 1.0
. Para cada propiedad que anime durante un intervalo, debe crear una separada Tween
que especifique los valores inicial y final de la propiedad. En otras palabras, 0.0
para 1.0
representar 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:
- 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.
- 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 Widget
la 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,
);
}
}
StaggerAnimation
Se definen tres animaciones en , que son derecha Container
, height
y animaciones de configuración de atributos, y luego especifican el punto de inicio y el punto final para cada animación en todo el color
proceso de animación pasando. Implementemos el enrutamiento para iniciar la animación:padding
Interval
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:
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
, , TabView
etc. AnimatedSwitcher
abstracto.
AnimatedSwitcher
Mostrar y ocultar animaciones se pueden agregar a sus elementos secundarios nuevos y antiguos al mismo tiempo. Es decir, AnimatedSwitcher
cuando el subelemento cambia, animará su elemento anterior y su elemento nuevo. Veamos primero AnimatedSwitcher
la 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 AnimatedSwitcher
de child
cambia (tipo o Key
diferente), el antiguo child
ejecutará la animación oculta y el nuevo child
ejecutará la animación de visualización. El tipo de efecto de animación a realizar está transitionBuilder
determinado por el parámetro, que acepta un AnimatedSwitcherTransitionBuilder
tipo 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
:AnimatedSwitcher
child
child
- En el caso de las animaciones enlazadas antiguas
child
, se ejecutan al revés (reverse
) - Para
child
las animaciones nuevas, enlazadas apuntarán hacia adelante (forward
)
De esta manera, se realiza el enlace de animación nuevo y antiguo child
. AnimatedSwitcher
El 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 AnimatedSwitcher
se realizarán child
las 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:
Nota:
AnimatedSwitcher
antiguo y nuevo de , no deben ser igualeschild
si son del mismo tipo .Key
Principio de implementación de AnimatedSwitcher
De hecho, AnimatedSwitcher
el principio de implementación de es relativamente simple, y AnimatedSwitcher
también podemos adivinar según la forma de uso. Para realizar la child
animación de conmutación antigua y nueva, solo se deben aclarar dos preguntas:
- ¿Cuándo se ejecuta la animación?
- ¿Cómo
child
animar lo viejo y lo nuevo?
Por AnimatedSwitcher
la forma de uso, podemos ver que cuando child
hay un cambio ( si el tipo o widget
el niño key
es diferente, se considera que ha cambiado), se volverá a ejecutar build
y luego la animación comenzará a ejecutarse.
Podemos StatefulWidget
lograr esto mediante la herencia AnimatedSwitcher
. El método específico es didUpdateWidget
juzgar child
si su antiguo y nuevo han cambiado en la devolución de llamada. Si hay un cambio, realice child
una reverse
animación de salida inversa () para el antiguo child
y una forward
animación de entrada hacia adelante () para el nuevo uno. Lo siguiente es AnimatedSwitcher
parte 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 AnimatedSwitcher
la lógica central de la implementación. Por supuesto, AnimatedSwitcher
la 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 AnimatedSwitcher
có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 AnimatedSwitcher
diferencia es que AnimatedCrossFade
es para dos subelementos , AnimatedSwitcher
pero entre los valores antiguo y nuevo. de un interruptor de subelemento . AnimatedCrossFade
El principio de implementación también es relativamente simple, AnimatedSwitcher
similar 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 AnimatedSwitcher
se 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).child
child
forward
child
reverse
child
child
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 Animation
avance ( 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, SlideTransition
reemplá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:
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 AnimatedSwitcher
realiza 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 SlideTransitionX
para 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 direction
diferentes 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:
Puede intentar SlideTransitionX
tomar direction
diferentes 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 AnimationController
empaquetar, mejorará mucho la facilidad de uso de los componentes de animación.
Componentes de transición animados personalizados
Queremos implementar uno AnimatedDecoratedBox
que pueda decoration
realizar 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 AnimatedDecoratedBox1
componente:
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 AnimatedDecoratedBox
para 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:
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 AnimationController
la parte de administración y Tween
actualizació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 ImplicitlyAnimatedWidget
clase abstracta, que hereda y proporciona una clase StatefulWidget
correspondiente , 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.ImplicitlyAnimatedWidgetState
AnimationController
ImplicitlyAnimatedWidgetState
ImplicitlyAnimatedWidget
ImplicitlyAnimatedWidgetState
Necesitamos hacer esto en dos pasos:
- 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、reverseDuration
propiedades ImplicitlyAnimatedWidget
se definen en . Puede ver que la clase no es diferente de la clase de la que AnimatedDecoratedBox
normalmente hereda .StatefulWidget
- Deje que
State
la clase heredeAnimatedWidgetBaseState
(la clase hereda deImplicitlyAnimatedWidgetState
la 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 build
dos forEachTween
métodos. Durante la ejecución de la animación, build
se llamará al método en cada cuadro (la lógica de llamada está ImplicitlyAnimatedWidgetState
incluida), por lo que build
necesitamos construir DecoratedBox
el estado de cada cuadro en el método, por lo que tenemos que calcular decoration
el estado de cada cuadro, que podemos _decoration.evaluate(animation)
calcular por, ¿dónde animation
está? ImplicitlyAnimatedWidgetState
El objeto definido en la clase base _decoration
es 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 _decoration
un valor. Sabemos _decoration
que es uno Tween
, y Tween
la responsabilidad principal es definir el estado inicial ( begin
) y el estado final ( end
) de la animación. Para AnimatedDecoratedBox
, decoration
el estado final es el valor que le pasa el usuario, y el estado inicial es incierto.Hay dos situaciones:
AnimatedDecoratedBox
Por primera vezbuild
, su valor se establece directamentedecoration
en el estado inicial en este momento, es decir,_decoration
el valor esDecorationTween(begin: decoration)
.AnimatedDecoratedBox
Cuando la actualización dedecoration
, el estado inicial es_decoration.animate(animation)
, es decir,_decoration
el valor esDecorationTween(begin: _decoration.animate(animation),end:decoration)
.
Ahora forEachTween
la función es obvia, se usa para actualizar Tween
el valor inicial, se llamará en los dos casos anteriores, y el desarrollador solo necesita reescribir este método y actualizar el Tween
estado inicial en el valor de este método. Y parte de la lógica de actualización está protegida en visitor
la devolución de llamada, solo necesitamos llamarla y pasarle los parámetros correctos, visitor
la 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 ImplicitlyAnimatedWidget
y 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 AnimatedDecoratedBox
similares a la mayoría de ellos, como se muestra en la siguiente tabla:
Nombre del componente | Función |
---|---|
AnimatedPadding |
padding Las 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 |
opacity Realice una animación de transición a un nuevo estado cuando cambie la transparencia |
AnimatedAlign |
Cuando alignment ocurre un cambio, se realiza una animación de transición al nuevo estado |
AnimatedContainer |
Cuando Container la 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:
Análisis de código fuente de animación.
Animation
Las clases clave y sus relaciones se muestran en la Figura 8-5.
En la Figura 8-5, Animation
es la clase clave de animación, que hereda y Listenable
mantiene Animation
un 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.
AnimationController
Es Animation
la implementación más común, contiene dos campos clave, Ticker
y Simulation
el primero lo TickerProvider
proporciona 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.
Simulation
La implementación predeterminada de es _InterpolationSimulation
que Curve
calcula valores basados en subclases concretas de . Además, Simulation
también brinda la capacidad de simular varios efectos físicos, como SpringSimulation
la simulación de efectos de resorte.
En términos generales, el campo AnimationController
de value
no se usa directamente, _AnimatedEvaluation
contendrá un Animation
objeto (generalmente una AnimationController
instancia de ) y un Animatable
objeto, 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 _AnimatedEvaluation
de 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, AnimationController
es el controlador el que Tween
proporciona el modelo de interpolación de la animación de interpolación Animation
como salida de llamada final. El siguiente es un análisis en profundidad del código.
Primero analice AnimationController
la 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;
}
}
AnimationController
En la lógica de inicialización de , primero cree un Ticker
objeto, que es el controlador central de la animación de interpolación de movimiento, que se analizará en detalle más adelante. _internalSetValue
Responsable 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.value
el 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); // 见前面内容
}
Tween
Es Animatable
una subclase cuyo animate
método es implementado por la clase padre y principalmente devuelve _AnimatedEvaluation
el objeto, por lo que animation.value
al llamar, la esencia es llamar al modelo de interpolación específico, es decir, el método de Animatable
la subclase específica (como ) , pero de hecho llama al método, como ejemplo, its La lógica se muestra en el Listado 8-31.Tween
evaluate
evaluate
transform
Tween
// 代码清单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.value
que el valor se actualizará en cada cuadro, y Tween
la interpolación lineal se completará en función de este valor, y el resultado será _AnimatedEvaluation
el valor del objeto.
A continuación, analice AnimationController.value
el mecanismo impulsor actualizado, es decir, forward
el 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 simulationDuration
el cálculo, que se calculará en función de la relación entre el valor restante y el valor total, stop
y la lógica se introducirá más adelante. El segundo paso es principalmente lidiar simulationDuration
con 0
la 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 Simulation
la clase de implementación específica aquí es _InterpolationSimulation
que es un simulador de interpolación lineal, que se analizará en detalle más adelante. La lógica del primer análisis _startSimulation
se 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!.star
el método t para iniciar la animación y actualizar _status
el estado al mismo tiempo.La start
ló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 scheduleTick
iniciar un "latido" a través del método, y hay un detalle que necesita atención: si un marco se está procesando actualmente, se _startTime
contará desde Vsync
el 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. scheduleTick
La 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 scheduleFrameCallback
el método _tick
registrará la función en la lista de devolución de llamada cuando Vsync
llegue 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;
}
_transientCallbacks
callback
Su 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 Vsync
una señal, _startTime
que solo se asignará una vez, indicando la marca de tiempo del inicio de la animación, para luego llamar _onTick
al método con la diferencia de tiempo. como parámetro (es decir, el parámetro del contenido anterior - _tick
función), si la lógica shouldScheduleTick
Después de true
, continuará registrando Vsync
la señal para impulsar la generación del siguiente "latido". La lógica del método de análisis a continuación _tick
se 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 _simulation
al x
método para calcular el valor actual y finalmente juzga si la animación está completa y transmite _value
los cambios de sus propios campos.
Primero analice x
el método, Tween
la 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, _curve
el valor predeterminado es Curves.linear
devolverse t
a sí mismo, por lo que el valor devuelto es t
una función lineal y el coeficiente es ( _end - _begin
).
En segundo lugar, analice _value
la 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, notifyListeners
el método es responsable del _value
cambio de la notificación, que básicamente se llama cada cuadro; notifyStatusListeners
es 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 _InterpolationSimulation
ello, 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 stop
có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 build
a 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+setState
Animation
Resumir:
La esencia de la animación de interpolación es registrar Vsync
la devolución de llamada de la señal a la capa inferior:
-
ticker.start
Después de que el método inicie la animación, llamescheduleTick
--> llameSchedulerBinding.instance!.scheduleFrameCallback
--> llamescheduleFrame()
para pasar por el proceso de dibujo, -
Y
FrameCallback
ejecutará_tick
el método, queonTick
llamaránotifyListeners()
a la animación de notificación para monitorear y recuperarvalue
el cambio de la animación actual, que se llamará básicamente en cada cuadro;notifyStatusListeners
es responsable del cambio del estado de notificación, que solo se activará cuando el estado de la animación cambia.
Suplemento: scheduleFrame
La 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.scheduleFrame
la 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).
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, pixelsPerSecond
es 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. _dragAlignment
Representa la posición en tiempo real del bloque, por lo que cuando _runAnimation
el 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 SpringSimulation
parámetros calculados, unitVelocity
es decir, la velocidad de deslizamiento del resorte en la dirección lineal, opuesta a la dirección del resorte.
El siguiente animateWith
mé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 _startSimulation
se ha analizado el contenido anterior de la lógica, el método Simulation
del x
mé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 SpringSimulation
que la lógica principal del campo se transfiere al _solution
campo, 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í. SpringSimulation
El isDone
mé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: isDone
la 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: