介绍
预览图
我们可以看到页面下方切换的卡片效果
分析
首先动画以 x轴分为两部分 : 左侧文字 和 右侧图片
右侧图片以 z轴 分为 : 上、下
仔细观察,可以看到它的动画流程大致如下:
上层显示的是当前图片,下层显示的时下一张
1、左侧文字淡入淡出切换
2、右侧图片的上层,与左侧文字同时淡出
3、之后下层图片上移到 上层图片的位置
4、移动完成后,淡入一张下层图片,
5、于此同时新的文字淡入
实现
首先我们定义一个类
class MusicCalendar extends WidgetState with SingleTickerProviderStateMixin{}
因为这个动画比较复杂,实际开发时用了provider,
代码中可能会看到musicCalendarVM, 它主要是用来持有和控制状态及一些数据
我尽量把里面的代码移出来
MusicCalendar
当页面初始完成后,我们会执行init()这个方法:
///这个方法后面还会见到,它的执行会使动画开始,即淡出-移动-淡入
///在这里讲,是让你对流程大概有了解,方便理解后面的代码
init(){
if(streamSubscription.isPaused){
streamSubscription.resume();
}
}
首先创建一些变量
//淡出/淡入动画
AnimationController fadeController;
Animation fadeAnim;
//图片外层是 stack,所以下面两个变量用于定位
//我们根据动画的进度配合下面的两个变量,就可以达到移动图片的效果
//具体可以看下面的实现
double aboveRightMax;
double aboveBottomMax;
我们再看一下布局,代码较多,我把说明写在注释里
去掉一些不必要的代码
//root layout
Container(
child: Stack(
children: <Widget>[
///date 这个不用管,跟咱们做的没关系
Positioned(
top: getWidthPx(10),
child: Text('后天',style: TextStyle(fontSize: getSp(28),color: Colors.black,fontWeight: FontWeight.bold),),
),
///这里是左侧 文字部分,它要做的动画 就是淡入和淡出
Positioned(
top: getWidthPx(60),
child: FadeTransition( //使用flutter 提供的fade组件
opacity: musicCalendarVM.fadeAnim,//这里与我们的 animation 绑定
child: Container(
width: getWidthPx(430),
child: Text('${creatives[musicCalendarVM.currentIndex].uiElement.mainTitle.title}',
style: TextStyle(color: Colors.grey,fontSize: getSp(32)),maxLines: 2,),
),
),
),
///这里是右侧图片区域
Positioned(
top: getWidthPx(30),
right: 0,
child: imageSwitcher(),
),
],
),
);
我们来看一下 这个方法:imageSwitcher()
Widget imageSwitcher(){
return Container(
//这里我们限定一下右侧图片区域的整体大小
//注意,图片要小于这个值
width: getWidthPx(150),height: getWidthPx(150),
child: Stack(
children: <Widget>[
///below
///这是下面那张图片,初始为右下角
Positioned(
right: 0,
bottom: 0,
//Opacity用于控制图片的淡入淡出
child: Opacity(
opacity: musicCalendarVM.opacity,
child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.currentIndex<=creatives.length-2
?musicCalendarVM.currentIndex+1 : 0].uiElement.image.imageUrl
, getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
),
),
///fake
///这里我额外放置了一张假的图片,下面细说
Positioned(
right: musicCalendarVM.right,
bottom: musicCalendarVM.bottom,
child: Visibility(
visible:musicCalendarVM.showFake ,
child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.fakeIndex].uiElement.image.imageUrl
, getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
),
),
///above
///上面那张图片初始为左上角
Positioned(
left: 0,
top: 0,
// right: 0,
// bottom: 0,
///这里用FadeTransition 控制淡入/淡出
child: FadeTransition(
opacity: musicCalendarVM.fadeAnim,//与 animation绑定,与之前的 文字动画一样
child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.currentIndex].uiElement.image.imageUrl
, getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
),
),
],
),
);
}
首先我们可以看到,这个stack中有3个widget:
下方图片 代号below
上方图片 代号above
假图片 代号fake
below & above widget
我们先来看一下 below 和 above,他们虽然都是做淡入和淡出效果,但是用的组件不一样。
below 使用的是Opacity
above 使用的是FadeTransition 与文字一样
这里之所以不同,是因为两者执行的实际和动画方向不同,所以未共用一个动画。above和文字一样,由animation控制,我们不用管它,来看一下below吧。
它的 opacity属性与musicCalendarVM.opacity绑定,而这个opacity属性的刷新主要涉及两个方法
回顾一下它要做的效果,淡入(实际移动由fake来做,我们后面讲)
void showBelow(){
///这里我们每20毫秒更新一下不透明度
Timer timer = Timer.periodic(Duration(milliseconds: 20), (timer){
if(opacity >= 1.0){
///当不透明度>=1.0时,我们结束timer
timer.cancel();
//这个时候也就意味着,below淡入完成,
//我们可以将上层图片也淡入(这个淡入的是 above)
///渐显above和title
fadeController.reverse().whenComplete((){
///当above淡入后,隐藏fake
showFake = false; notifyListeners();
///实际上在below淡入前,fake做了移动,移动到左上角了(冒充 above)
///因此,above完成淡入后,我们要重置fake位置,显示fake(冒充 below)
/// 嘿嘿
right = 0; bottom = 0;
///当然 fake显示的图片也应该是上层图片的下一张
fakeIndex = currentIndex <= creatives.length-2 ? currentIndex+1:0;
showFake = true;
///真正的 below又变成全透明了
opacity = 0;
notifyListeners();
});
return;
}
//每20秒 不透明度+0.1
opacity =(opacity+0.1).clamp(0.0, 1.0);
notifyListeners();
});
}
我们来看一下哪里调用了showBelow()
如果你不太熟悉Provider或者bedrock框架的话,这里简单来讲,
就是页面初试完后,开始执行clock监听并执行相应的动画操作。
///开头的那个方法
init(){
if(streamSubscription.isPaused){
streamSubscription.resume();
}
}
final Duration interval = Duration(seconds: 5);///每5秒切换一次卡片
MusicCalendarVM(this.block3, this.creatives,){
clock = Stream.periodic(interval,(index){
});
///咱们只看这里的方法
streamSubscription = clock.listen((i) async{
if(destroy)return;
if(fadeController.status == AnimationStatus.completed|| fadeController.status == AnimationStatus.dismissed){
///当上层动画淡出完成后
///title和 above 渐隐,同时fake上移
fadeController.forward().whenComplete((){
//这里的right和bottom与fake绑定,我们稍后介绍
//实际上这里的right和bottom理论上已经等于右边的值
//但实际上还是会出现偏差,这里进行校准
right = aboveRightMax;
bottom = aboveBottomMax;
notifyListeners();
///更新index =》 dataList的index
incrementIndex();
///插入新的below
///调用了我们上的方法
showBelow();
//fadeController.reverse();
});
}
});
streamSubscription.pause();
}
fake widget
至此 above和below就介绍完了,我们来说一下 fake,
///fake
Positioned(
right: musicCalendarVM.right,
bottom: musicCalendarVM.bottom,
child: Visibility(
visible:musicCalendarVM.showFake ,
child: ShowImageUtil.showImageWithDefaultError(creatives[musicCalendarVM.fakeIndex].uiElement.image.imageUrl
, getWidthPx(130), getWidthPx(130),borderRadius: getHeightPx(10)),
),
),
fake的工作流程介绍:
当页面初始时:
above图片为第一张,fake和below为第二张
above在右上角,below和fake在右下角,且fake遮挡below(实际below为不可见)
当动画开始时:
above淡出,于此同时fake滑动到左上角。
当淡出动画结束后,我们将 above、below显示的图片index+1, 这里below还是不可见的。
然后,我们将below淡入(它始终在右下角),同时above也是淡入
(因为速度极快,且与fake重合,你是看不出它的淡入的)
操作完成后:
我们将fake(在左上)隐藏,并调整它的right和bottom为0,这样又到了右下,
随后再显示它,这样又遮挡了below,
至此一个轮回就结束了
我们大致看一下fake的主要属性:
//控制它的位置
double right = 0; double bottom = 0;
showFake//控制fake的隐藏,在上面的方法中有出现过
更新这两个属性的位置在:
void updatePosition(){
right = aboveRightMax * (1-fadeAnim.value);
bottom = aboveBottomMax * (1-fadeAnim.value);
notifyListeners();
}
updatePosition()方法则在 animationListener中调用
musicCalendarVM.fadeController.addListener(musicCalendarVM.animationListener);
animationListener(){
if(fadeController.status == AnimationStatus.forward){
if(!showFake) showFake = true;
updatePosition();
}
}
到了这里,整个动画效果就实现了,如果有点乱,可以在demo中对照源码和真机效果来理解。
谢谢大家的阅读,欢迎指出不足支出 :)
Demo
内部搜索即可