最近一个项目要实现可以无限循环的PageView,主要思路是在初始化pageview的list的时候在开始和结尾多加一个结尾和开头的widget,当滑动到开头和结尾的时候手动进行页面的切换,详细可以搜索pageview无限轮播。
发展壮大离不开广大客户长期以来的信赖与支持,我们将始终秉承“诚信为本、服务至上”的服务理念,坚持“二合一”的优良服务模式,真诚服务每家企业,认真做好每个细节,不断完善自我,成就企业,实现共赢。行业涉及成都咖啡厅设计等,在成都网站建设、成都营销网站建设、WAP手机网站、VI设计、软件开发等项目上具有丰富的设计经验。
这种方法有一个要点就是要维护两个索引,一个是内部list的索引,一个是外部显示的索引,由于list的容量是比显示的数量多2的,所以如果要在外部进行一些比如指示器或者计时器功能要进行和页面同步显示或者切换页面操作时,需要将显示的索引转换成list的索引。
不过网上说的都是一些比较简单的实现,看到比较多的就是当滑动到要手动切换的时候进行一个时延,这样可以避免直接切换页面造成的卡顿和跳动现象。但是存在一个问题,如果要同时实现一个跟随页面切换的指示器,就会出现当页面切换过去之后指示器才会跟着过去,因为页面切换的时候执行了时延,而时延之后才会真正改变索引,此时才会setstate,之后指示器才能响应到索引的切换,但是如果在时延之前就切换的话又会出现指示器先行的情况。因此这种方法其实是存在一些问题的。
所以解决这个问题的关键在于如何进行页面切换的判断。这里可以有两种思路实现,第一种是实现viewpage的onpagechanged方法,在里面进行逻辑的判断,然后用controller来进行页面跳转,不过这种方法存在当controller跳转的时候又会回调onpagechanged,所以就会出现多次对索引不必要操作,而且如果有比如计时器等额外的功能的话可能不方便将页面逻辑分开,而且依旧无法解决指示器延迟问题,同时也很难进行细粒度的操作。
第二种方法我们就要去看pageview的源码了,从源码的角度来解决问题才是正确的方法。首先我们点进去pageview的源码
看到这里其实已经有一些思路了,我们之前难点在于重写了onpagechanged方法导致问题无法很好的解决,现在我们找到了onpagechanged调用的地方,只要找办法避免掉就可以实现了。
当然这里我们要说到NotificationListener,以及flutter对应的冒泡事件传输机制,这里大家可以去看看这篇 文章 。
我来总结一下,其实就是flutter对于notification这个组件,有一中事件规则叫冒泡传递,底层的notification如果在它的 onNotification写的逻辑中返回是false以及它不是根结点,就会去向上遍历寻找它的祖先notification组件,知道遇到root节点或者某一个返回true,则事件传递结束。
而且在onNotification中可以对多种事件进行监听和处理,所以我们可以把对viewpage页面跳转对索引处理的逻辑写在这里,而且我们可以分别处理比如滑动开始的start事件和结束的end事件,分别进行细粒度的逻辑的处理,这样就可以在外部进行操作和别的功能实现了。
因此不仅无限轮播事件可以通过这种方法来解决,如果有其他的操作也可以这样进行处理,而且因为我们没有传入onpagechanged方法,所以不存在多次调用的问题,pageview那里判断onpagechanged是null方法就不会进去了,会直接我们写在pageview外面的notification的逻辑。
最后的结构大概这样
对动画系统而言,为了实现动画,它需要做三件事儿:1.确定画面变化的规律;2.根据这个规律,设定动画周期,启动动画;3.定期获取当前动画的值,不断地微调、重绘画面。
这三件事情对应到 Flutter 中,就是 Animation、AnimationController 与 Listener:
1.Animation 是 Flutter 动画库中的核心类,会根据预定规则,在单位时间内持续输出动画的当前状态。Animation 知道当前动画的状态(比如,动画是否开始、停止、前进或者后退,以及动画的当前值),但却不知道这些状态究竟应用在哪个组件对象上。换句话说,Animation 仅仅是用来提供动画数据,而不负责动画的渲染。
2.AnimationController 用于管理 Animation,可以用来设置动画的时长、启动动画、暂停动画、反转动画等。
3.Listener 是 Animation 的回调函数,用来监听动画的进度变化,我们需要在这个回调函数中,根据动画的当前值重新渲染组件,实现动画的渲染。
class NormalAnimateWidget extends StatefulWidget {
@override
StatecreateState()=_NormalAnimateState();
}
class _NormalAnimateState extends Statewith SingleTickerProviderStateMixin{
AnimationController?controller;
Animation?animation;
@override
void initState() {
// TODO: implement initState
super.initState();
/*
* AnimationController
AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。
* AnimationController会在动画的每一帧,就会生成一个新的值。
* 默认情况下,AnimationController在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字。
* */
/*Ticker
当创建一个AnimationController时,需要传递一个vsync参数,
它接收一个TickerProvider类型的对象,它的主要职责是创建Ticker,定义如下:
abstract class TickerProvider {
//通过一个回调创建一个Ticker
Ticker createTicker(TickerCallback onTick);
}
Flutter 应用在启动时都会绑定一个SchedulerBinding,
通过SchedulerBinding可以给每一次屏幕刷新添加回调,
而Ticker就是通过SchedulerBinding来添加屏幕刷新回调,这样一来,
每次屏幕刷新都会调用TickerCallback。
使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)
消耗不必要的资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding,
而Ticker是受SchedulerBinding驱动的,
由于锁屏后屏幕会停止刷新,所以Ticker就不会再触发。
*/
// 创建动画周期为1秒的AnimationController对象
controller =AnimationController(
vsync:this, duration:const Duration(milliseconds:3000));
/*
* Curve
* 动画过程可以是匀速的、匀加速的或者先加速后减速等。
* Flutter中通过Curve(曲线)来描述动画过程,
* 我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的。
* 我们可以通过CurvedAnimation来指定动画的曲线,如:
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
*
Curves曲线 动画过程
linear 匀速的
decelerate 匀减速
ease 开始加速,后面减速
easeIn 开始慢,后面快
easeOut 开始快,后面慢
easeInOut 开始慢,然后加速,最后再减速
*
* 当然我们也可以创建自己Curve,例如我们定义一个正弦曲线:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
* */
final CurvedAnimation curve =CurvedAnimation(
parent:controller!, curve:Curves.linear);
/*
* Animation
*Animation是一个抽象类,它本身和UI渲染没有任何关系,
* 而它主要的功能是保存动画的插值和状态;其中一个比较常用的Animation类是Animation。
* Animation对象是一个在一段时间内依次生成一个区间(Tween)之间值的类。
* Animation对象在整个动画执行过程中输出的值可以是线性的、曲线的、一个步进函数或者任何其他曲线函数等等,
* 这由Curve来决定。 根据Animation对象的控制方式,
* 动画可以正向运行(从起始状态开始,到终止状态结束),
* 也可以反向运行,甚至可以在中间切换方向。
* Animation还可以生成除double之外的其他类型值
* ,如:Animation 或Animation。
* 在动画的每一帧中,我们可以通过Animation对象的value属性获取动画的当前状态值。
#动画通知
我们可以通过Animation来监听动画每一帧以及执行状态的变化,Animation有如下两个方法:
addListener();它可以用于给Animation添加帧监听器,
* 在每一帧都会被调用。
* 帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建。
addStatusListener();
* 它可以给Animation添加“动画状态改变”监听器;
* 动画开始、结束、正向或反向(见AnimationStatus定义)时会调用状态改变的监听器。
* */
// 创建从50到200线性变化的Animation对象
// 普通动画需要手动监听动画状态,刷新UI
animation =Tween(begin:10.0, end:200.0).animate(curve)
..addListener(()=setState((){}));
/*
* Tween
* 默认情况下,AnimationController对象值的范围是[0.0,1.0]。
* 如果我们需要构建UI的动画值在不同的范围或不同的数据类型,
* 则可以使用Tween来添加映射以生成不同的范围或数据类型的值。
*Tween构造函数需要begin和end两个参数。
* Tween的唯一职责就是定义从输入范围到输出范围的映射。
* 输入范围通常为[0.0,1.0],但这不是必须的,我们可以自定义需要的范围。
* */
// 启动动画
controller!.repeat(reverse:true);
//
// 第二段
// animation!.addStatusListener((status) {
// if (status == AnimationStatus.completed) {
// controller!.reverse();// 动画结束时反向执行
// } else if (status == AnimationStatus.dismissed) {
// controller!.forward();// 动画反向执行完毕时,重新执行
// }
// });
// controller!.forward();// 启动动画
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(
body:Center(
child:Container(
width:animation!.value,// 将动画的值赋给 widget 的宽高
height:animation!.value,//
child:FlutterLogo(),
)
)
)
);
}
@override
void dispose() {
// 释放资源
controller!.dispose();
super.dispose();
}
}
同上篇文章《九、Flutter水波动画》画水波原理是一样的,都是通过画笔呈现不规则图形。this.offsetList是存储加载的动画里面的实心圆的坐标,通过drawCircle方法把坐标画到画布上。
RoundProgress继承StatefulWidget通过 Timer刷新页面,来达到动画的效果。this.w * i / this.numOfMoveView是计算每个实心圆的宽度的,i越小实心圆的宽度越小,实心圆就越小。(pi * 2.0 / this.numOfMoveView) * i计算出弧度,通过弧度计算出每个实心圆的坐标,保存在this.offsetList里。每个实心圆初始化的弧度都保存在了 this.radianList里,循环第二次,第三次......累计弧度,每个实心圆的坐标逐一按弧度偏移,从而所有的实心圆绕着一个点旋转。
RoundProgress最重的一个参数loading,当loading = true加载动画会在stack里面呈现出来,当loading = false加载动画不会出现在stack里面。
由于RoundProgress继承了StatefulWidget,从外部就没有办法更新RoundProgress数据了,可以通过组件间通信。这里使用的是Global Key通信,可以访问State对象的公共属性和方法,从而让加载动画停止旋转,刷新组件。另外还有一种通信方法是ValueNotifier通信,ValueNotifier是一个包含单个值的变更通知器,当它的值改变的时候,会通知它的监听。
总结:这个加载动画算是初步完成了,基本使用还是可以的,封装的不够灵活,可自定义程度比较低。下一篇文章将进一步对加载动画优化,并上传到pub.dev方便大家使用。谢谢收看,点个赞吧!
对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
比如视图滚动到底部时,我们可能希望做上拉加载更多;
比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
比如监听滚动什么时候开始,什么时候结束;
在Flutter中监听滚动相关的内容由两部分组成:ScrollController和ScrollNotification。
ScrollController
在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:
jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
在移动端,各个平台或 UI 系统的原始指针事件模型基本都是一致,即:一次完整的事件分为三个阶段:手指按下、手指移动、和手指抬起,而更高级别的手势(如点击、双击、拖动等)都是基于这些原始事件的。
Flutter 中可以使用 Listener widget 来监听原始触摸事件,它也是一个功能性 widget。
Listener 的常见属性
用法如下:
加载更多需要对 ListView 进行监听,所以需要进行监听器的设置,在 State 中进行监听器的初始化。
2、使用上述的 Listener 来监听,通过 Listener 的 onPointerMove(手指在屏幕上滑动)来监听滑动的距离,当滑动到底部时加载更多数据
Flutter的 StatefulWidget StatelessWidget 生命周期中没有组件出现或者消失的回调,主要是要靠路由的监听