【Flutter学习笔记】9.6 动画切换组件(AnimatedSwitcher)

参考资料:《Flutter实战·第二版》9.6 动画切换组件(AnimatedSwitcher)


9.6.1 AnimatedSwitcher

AnimatedSwitcher 可以同时对其新、旧子元素添加显示、隐藏动画,在需要切换新旧元素的场景广泛使用。也就是说在AnimatedSwitcher子元素发生变化时,会对其旧元素和新元素做动画。这里的子元素“发生变化”指的就是child widget的类型或者key发生了改变,则旧的child会执行隐藏动画,而新的child会执行显示动画。下面是AnimatedSwitcher 的定义:

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, //布局构建器
})

假设现在有动画执行曲线A(switchOutCurve)和B(switchInCurve)【这里要仅定义动画播放的时间控制形式,例如加速、减速或者子弹时间等效果,并不决定动画的类型】,默认为线性,分别对应旧child和新child,其中animation的定义在transitionBuilder当中。AnimatedSwitcher的默认值是AnimatedSwitcher.defaultTransitionBuilder,其默认返回的是FadeTransition,也就是渐显渐隐动画【很可能是透明度发生改变的动画】。该builder在AnimatedSwitcherchild切换时会分别对新、旧child绑定动画【这里才决定动画的类型】,旧的child会反向(reverse)执行,新的child会正向(forward)执行。

示例

下面是一个简单计数器的例子,在每一次自增的过程中,旧数字执行缩小动画隐藏,新数字执行放大动画显示:

import 'dart:ui';import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({super.key});// This widget is the root of your application.Widget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),useMaterial3: true,),home: const MyHomePage(title: 'TEAL WORLD'),);}
}class MyHomePage extends StatefulWidget {const MyHomePage({super.key, required this.title});final String title;State<MyHomePage> createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {Widget build(BuildContext context) {return Scaffold(appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary,title: Text(widget.title,style: TextStyle(color: Colors.teal.shade800, fontWeight: FontWeight.w900),),actions: [ElevatedButton(child: const Icon(Icons.refresh),onPressed: (){},)],),body: const AnimatedSwitcherCounterRoute(),floatingActionButton: FloatingActionButton(onPressed: () {},tooltip: 'Increment',child: Icon(Icons.add_box,size: 30,color: Colors.teal[400],),), // This trailing comma makes auto-formatting nicer for build methods.);}
}class AnimatedSwitcherCounterRoute extends StatefulWidget {const AnimatedSwitcherCounterRoute({Key? key}) : super(key: key);AnimatedSwitcherCounterRouteState createState() =>AnimatedSwitcherCounterRouteState();
}//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class AnimatedSwitcherCounterRouteStateextends State<AnimatedSwitcherCounterRoute>with SingleTickerProviderStateMixin {int _count = 0; // 计数状态,记录当前的数字Widget build(BuildContext context) {return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[// 用AnimatedSwitcher包裹需要进行变化的部分AnimatedSwitcher(duration: const Duration(milliseconds: 500),transitionBuilder: (Widget child, Animation<double> animation) {return ScaleTransition(scale: animation, child: child);},  // 重新定义为大小改变动画child: Text('$_count',key: ValueKey<int>(_count),  // 注意child变化后的type或者key必须不同style: Theme.of(context).textTheme.headline1,),),const SizedBox(height: 10,),  // 创造一点间隔ElevatedButton(onPressed: () {setState(() {_count += 1;  // 点击按钮更新状态});},child: const Text("+1", style: TextStyle(fontSize: 20),),)],),);}
}

运行效果如下图所示,点击按钮后旧的数字缩小,而新的数字放大:
在这里插入图片描述
如果想将效果改为渐变效果,只需要把transitionBuilder这个属性所返回的切换效果改为FadeTransition即可:

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

效果如下:
在这里插入图片描述

AnimatedSwitcher实现原理

首先,当child子widget的key或类型不同时,会重新执行build()。可以通过继承StatefulWidget来实现AnimatedSwitcher,通过didUpdateWidget回调判断新旧child是否发生了变化。如果变化,则对旧的child执行反向动画,对新的child执行正向动画。下面是AnimatedSwitcher实现的伪代码,流程类似,但是很多实现细节,例如过渡动画是自定义的、执行动画时的布局等等:

Widget _widget; 
void didUpdateWidget(AnimatedSwitcher oldWidget) {
// 该周期回调能够带入旧的widgetsuper.didUpdateWidget(oldWidget);// 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)if (Widget.canUpdate(widget.child, oldWidget.child)) {// child没变化的操作} else {//child发生了变化,构建一个Stack来分别给新旧child执行动画_widget= Stack(alignment: Alignment.center,children:[//旧child应用FadeTransitionFadeTransition(opacity: _controllerOldAnimation,child : oldWidget.child,),//新child应用FadeTransitionFadeTransition(opacity: _controllerNewAnimation,child : widget.child,),]);// 给旧child执行反向退场动画_controllerOldAnimation.reverse();//给新child执行正向入场动画_controllerNewAnimation.forward();}
}//build方法
Widget build(BuildContext context){return _widget; // 其实是对child中的组件进行了再次包装,实际上同时显示了两个widget
}

可以看出,相当于两个子widget叠加在一起,同时进行动画,只不过一个反向,一个正向,在动画结束时只展示最新的组件。controler在定义时提供整个动画的时长设置,而animation在定义时需要设置曲线、提供区间值、设置监听等。可以看出在AnimatedSwitcher组件中已经定义好了controler,分别控制新旧组件。实际上animation应该也是在组件中默认定义好的,其定义中还有animation这个参数,就是用来控制child属性的:

const FadeTransition({super.key,required this.opacity,this.alwaysIncludeSemantics = false,super.child,});

上面opacity的类型是Animation<double>,也就是封装好的可以根据变值设置child的透明度或者其它属性。

另外,Flutter SDK中还提供了一个AnimatedCrossFade组件,它也可以切换两个子元素,切换过程执行渐隐渐显的动画,和AnimatedSwitcher不同的是AnimatedCrossFade是针对两个子元素,而AnimatedSwitcher是在一个子元素的新旧值之间切换。

9.6.2 AnimatedSwitcher高级用法

AnimatedSwitcher的使用过程中可以看出一个明显的问题,就是新旧组件用的同一个切换动画,只能实现对等的镜面动画效果。如果想实现旧页面从右向左←退出,而新页面从右向左←出现,方向是相同时,就做不到了,因为上文中原理部分代码用的是forwardrewerse这两个函数,就是同一个动画效果的正反向。
在这里插入图片描述
由此,可以通过自定义SlideTransition的形式实现,因为不同的过渡动画实现都是根据animation的值进行的,所以在这个组件的内部是可控的。
还是以上面的场景为例,原来的offset值应该是固定的变化,animation通过controller指挥进行正反向操作。animation中有包装动画的状态,如AnimationStatus.reverseAnimationStatus.forward等,则可以根据不同的状态计算不同的offset以达到打破对称性的目的,但是这个方法并不是万能的,需要根据不同的效果需求设计细节:

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,);}
}

使用时,将transitionBuilder的返回设置为MySlideTransition即可,实现代码和效果如下所示:

class AnimatedSwitcherCounterRouteStateextends State<AnimatedSwitcherCounterRoute>with SingleTickerProviderStateMixin {int _count = 0; // 计数状态,记录当前的数字Widget build(BuildContext context) {print('child build');return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[// 用AnimatedSwitcher包裹需要进行变化的部分AnimatedSwitcher(duration: const Duration(milliseconds: 500),transitionBuilder: (Widget child, Animation<double> animation) {var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0));  // 动画Tween的默认是double的[0, 1],务必要改成Offsetreturn MySlideTransition(position: tween.animate(animation),child: child,);}, // 重新定义为大小改变动画child: Text('$_count',key: ValueKey<int>(_count), // 注意child变化后的type或者key必须不同style: Theme.of(context).textTheme.headline1,),),const SizedBox(height: 10,), // 创造一点间隔ElevatedButton(onPressed: () {setState(() {_count += 1; // 点击按钮更新状态});},child: const Text("+1",style: TextStyle(fontSize: 20),),)],),);}
}

在这里插入图片描述
补充内容,关于FractionalTranslationtransformHitTests参数的用法:

在Flutter框架中,FractionalTranslation是一个用于对其子widget应用一个平移变换的widget。这个平移可以是分数形式的,也就是说它可以是基于其自身大小的百分比。
第二个参数transformHitTests是一个布尔值,用于指示是否应该应用变换到点击测试(hit tests)中。点击测试是Flutter框架中用于确定哪个widget应该响应一个特定的触摸或点击事件的过程。
具体解释如下:
如果transformHitTests设置为true,则当进行点击测试时,会考虑FractionalTranslation的平移变换。这意味着,如果你平移了一个widget,并且用户点击了平移后的位置,那么该widget将会捕获这个点击事件,即使点击的位置在原始(未平移)的widget布局中是空的
如果transformHitTests设置为false,则点击测试不会考虑FractionalTranslation的平移变换。这意味着,即使widget被平移了,点击测试仍然会在原始的widget布局上进行。
这个参数在处理平移动画或其他动态变换时非常有用,因为它可以帮助你确定用户与界面交互时应该响应哪个widget。例如,如果你有一个可以滑动的卡片,你可能希望在卡片滑动的过程中,点击测试也跟随卡片移动,这样用户可以在卡片移动时继续与其交互。在这种情况下,你会将transformHitTests设置为true。然而,在某些情况下,你可能希望点击测试保持在原始位置,不受平移的影响,那么你就应该将其设置为false。

9.6.3 SlideTransitionX

除了上面的实现方式,如果想要定制性更高一些,例如上入下出、下入上出、左入右出的效果,可以输入一个参数来定制不同的Tween值,代表不同的滑动方向的变化。当然这要配合build函数中对动画状态的判断进行,否则动画还是对称的。这类组件可以实现高度自由化的定制,以满足不同的效果需求:

class SlideTransitionX extends AnimatedWidget {SlideTransitionX({Key? key,required Animation<double> position,  // 外部传入的animation对象this.transformHitTests = true,  // 命中测试参与模式this.direction = AxisDirection.down,  // 自定义滑动方向required this.child,}) : super(key: key, listenable: position) {switch (direction) {case AxisDirection.up:_tween = Tween(begin: const Offset(0, 1), end: const Offset(0, 0));break;case AxisDirection.right:_tween = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));break;case AxisDirection.down:_tween = Tween(begin: const Offset(0, -1), end: const Offset(0, 0));break;case AxisDirection.left:_tween = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));break;}}final bool transformHitTests;final Widget child;final AxisDirection direction;late final Tween<Offset> _tween;  // 根据入参设置Tween,而不是在外面直接定义animationWidget build(BuildContext context) {final position = listenable as Animation<double>;Offset offset = _tween.evaluate(position);  // 通过evaluate函数可以获得offset的对应值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,);}
}

上述代码Offset offset = _tween.evaluate(position);这句可以看出,Tween对象不是在外面直接定义再产生Animation值的,而是在build的过程中根据默认Animation<double>的值映射产生的。
使用时只要替换成SlideTransitionX组件过渡并设置想要的参数即可,下面是上入下出效果的代码实现和效果:

class AnimatedSwitcherCounterRouteStateextends State<AnimatedSwitcherCounterRoute>with SingleTickerProviderStateMixin {int _count = 0; // 计数状态,记录当前的数字Widget build(BuildContext context) {print('child build');return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[// 用AnimatedSwitcher包裹需要进行变化的部分AnimatedSwitcher(duration: const Duration(milliseconds: 500),transitionBuilder: (Widget child, Animation<double> animation) {return SlideTransitionX(position: animation,direction: AxisDirection.down,child: child,);}, // 重新定义为大小改变动画child: Text('$_count',key: ValueKey<int>(_count), // 注意child变化后的type或者key必须不同style: Theme.of(context).textTheme.headline1,),),const SizedBox(height: 10,), // 创造一点间隔ElevatedButton(onPressed: () {setState(() {_count += 1; // 点击按钮更新状态});},child: const Text("+1",style: TextStyle(fontSize: 20),),)],),);}
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/745565.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2.MongoDB与关系数据库对比

MongoDB的简单操作与比较 与关系数据库对比 MySQL与MongoDB都是开源的常用数据库&#xff0c;但是MySQL是传统的关系型数据库&#xff0c;MongoDB则是非关系型数据库&#xff0c;也叫文档型数据库&#xff0c;是一种NoSQL的数据库。它们各有各的优点&#xff0c;来看看他们之…

数字排列 - 华为OD统一考试(C卷)

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 200分 题解&#xff1a; Java / Python / C 题目描述 小明负责公司年会&#xff0c;想出一个趣味游戏: 屏幕给出 1−9 中任意 4 个不重复的数字,大家以最快时间给出这几个数字可拼成的数字从小到大排列位于第 n 位置…

Turbo C++ v3.7.8.9的下载和安装(C语言编辑器完整安装步骤详细图文教程)·跟老吕学C语言(C语言必学教程之一)

[TOC](Turbo C v3.7.8.9的下载和安装(C语言编辑器完整安装步骤详细图文教程)) 跟老吕学C语言&#xff08;C语言必学教程之一&#xff09; 老吕是通过 Turbo C 编译器开始了 C 编程培训和开发。 本文中&#xff0c;老吕将带你了解如何下载 Turbo C&#xff0c;如何在任何最新 W…

二.递归及实例(汉诺塔问题)

目录 5.递归 6-递归实例:汉诺塔问题 思路: 详细过程: 代码: 5.递归 调用自身 结束条件 6-递归实例:汉诺塔问题 思路: 结果: 详细过程: 代码: #n为盘子的个数 a,b,c分别为3个地方. def hannuta(n,a,b,c): ​if n>0:hannuta(n-1,a,c,b) #将n-1个从a经过c移到到b(a…

23.1 微服务理论基础

23.1 微服务基础 1. 微服务介绍2. 微服务特点3. 微服务优缺点4. 微服务两大门派5. 微服务拆分6. 微服务扩展6.1 服务扩展6.2 按需扩展7. 微服务重要模块******************************************************************************************************************

Qt之格栅布局(QGridLayout)控件填满整个单元格

Qt专栏&#xff1a;http://t.csdnimg.cn/GQN1M 目录 1.现象1 2.解决方案 3.现象2 4.解决方案 5.总结 1.现象1 今天在用QGridLayout布局的时候&#xff0c;添加到布局的QWidget有文本框、标签、组合框和按钮等等&#xff0c;布局两列&#xff0c;通过下面的方式添加进去的&…

气压计LPS25HB开发(1)----轮询获取气压计数据

气压计LPS25HB开发----1.轮询获取气压计数据 概述视频教学样品申请源码下载产品特性通信模式速率生成STM32CUBEMX串口配置IIC配置SA0地址设置串口重定向参考程序SA0设置模块地址获取ID复位操作BDU设置设置速率轮询读取数据演示 概述 本文将介绍如何使用 LPS25HB 传感器来读取数…

Ansible自动化运维Inventory与Ad-Hoc

前言 自动化运维是指利用自动化工具和技术来简化、自动化和优化IT基础设施的管理和运维过程&#xff0c;从而提高效率、降低成本&#xff0c;并减少人为错误。在当今复杂的IT环境中&#xff0c;自动化运维已经成为许多组织和企业提高生产力和保证系统稳定性的重要手段。Ansibl…

[数据集][目标检测]零售柜零食检测数据集VOC+YOLO格式5422张113类

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;5422 标注数量(xml文件个数)&#xff1a;5422 标注数量(txt文件个数)&#xff1a;5422 标注…

Docker简介与安装

简介 用来快速构建、运行、管理应用的工具简单说&#xff0c;帮助我们部署项目以及项目所依赖的各种组件典型的运维工具 安装 1.卸载旧版 首先如果系统中已经存在旧的Docker&#xff0c;则先卸载&#xff1a; yum remove docker \docker-client \docker-client-latest \dock…

数字逻辑-时序逻辑电路二——沐雨先生

一、实验目的 &#xff08;1&#xff09;熟悉计数器的逻辑功能及特性。 &#xff08;2&#xff09;掌握计数器的应用。 &#xff08;3&#xff09;掌握时序逻辑电路的分析和设计方法。 二、实验仪器及材料 三、实验原理 1、集成4位计数器74LS161&#xff08;74LS160&#…

自动控制原理--matlab/simulink建模与仿真

第一讲 自动控制引论 第二讲 线性系统的数学模型 第三讲 控制系统的复域数学模型(传递函数) 第四讲 控制系统的方框图 /video/BV1L7411a7uL/?p35&spm_id_frompageDriver pandas, csv数据处理 numpy&#xff0c;多维数组的处理 Tensor&#xff0c;PyTorch张量 工作原理图…

数据仓库为什么要分层建设?每一层的作用是什么?

在数字化时代&#xff0c;数据已成为企业最宝贵的资产之一。为了更好地管理和利用这些数据&#xff0c;许多企业都建立了数据仓库。然而&#xff0c;数据仓库并非简单的数据存储工具&#xff0c;而是一个复杂的数据处理和分析系统。其中&#xff0c;分层建设是数据仓库设计的重…

稀碎从零算法笔记Day17-LeetCode:有效的括号

题型&#xff1a;栈 链接&#xff1a;20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述&#xff08;红字为笔者添加&#xff09; 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 …

SQLiteC/C++接口详细介绍之sqlite3类(六)

快速前往文章列表&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;五&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;七&#xff09; 19. sqlite3_changes与sqlite3_changes64 是SQLite中用…

【MyBatis-Plus】逻辑删除、乐观锁、防全表更新和删除实现 MyBatisX插件 高级扩展

文章目录 一、逻辑删除实现二、乐观锁实现2.1 悲观锁和乐观锁场景和介绍2.2 具体技术和方案:2.3 版本号乐观锁技术的实现流程2.4 使用mybatis-plus数据使用乐观锁 三、防全表更新和删除实现三、代码生成器(MyBatisX插件) 一、逻辑删除实现 物理删除&#xff1a;真实删除&#…

Flink on Yarn安装配置

前言 Apache Flink&#xff0c;作为一个开源的分布式处理引擎&#xff0c;近年来在大数据处理领域崭露头角&#xff0c;其独特的流处理和批处理一体化模型&#xff0c;使得它能够在处理无界和有界数据流时展现出卓越的性能。本文旨在对Flink进行简要的前言性介绍&#xff0c;以…

Oracle登录错误ERROR: ORA-01031: insufficient privileges解决办法

这个问题困扰了我三个星期&#xff0c;我在网上找的解决办法&#xff1a; 1.控制面板->管理工具->计算机管理->系统工具->本地用户和组->ORA_DBA组。 但我电脑上根本找不到。 2.在oracle安装目录下找到oradba.exe运行。 最开始我都不到这个oradba.exe文件在哪…

云计算 3月11号 (NFS远程共享存储及vsftpd配置)

构建NFS远程共享存储 一、NFS介绍 文件系统级别共享&#xff08;是NAS存储&#xff09; --------- 已经做好了格式化&#xff0c;可以直接用。 速度慢比如&#xff1a;nfs&#xff0c;sambaNFS NFS&#xff1a;Network File System 网络文件系统&#xff0c;NFS 和其他文件…

(008)Unity StateMachineBehaviour的坑

文章目录 StateMachineBehaviour同名函数的调用问题StateMachineBehaviour 的 OnState*、OnStateMachine* 的区别 StateMachineBehaviour同名函数的调用问题 1.如果脚本中&#xff0c;两个同名的函数都存在&#xff0c;那么两个函数都会被调用&#xff1b;如果只有其中一个同名…