目录
- 实现效果如图:
- 实现思路
- 1、底部返回键旋转动画
- 2、底部四个栏目变速上移动画+渐变动画
- 3、中间图片渐变动画
- 4、顶部文案渐变动画+下移动画
- 5、注销动画
- 总结
在Android App的开发项目中,我们需要在点击事件上实现一个动画效果来提高用户的体验度。比如闲鱼底部中间按钮的那种。该怎么实现呢? 一起来看看吧
实现效果如图:
实现思路
根据UI的设计图,对每个模块设计好动画效果,需要实现以下四个效果。
1、底部返回键旋转动画
底部返回按钮动画其实就是个旋转动画,利用Transform.rotate设置angle的值即可,这里使用了GetX来对angle进行动态控制。
//返回键旋转角度,初始旋转度,使其初始样式为 + | |
var angle = (pi /).obs; | |
///关闭按钮旋转动画控制器 | |
late final AnimationController closeController; | |
late final Animation<double> closeAnimation; | |
///返回键旋转动画 | |
closeController = AnimationController( | |
duration: const Duration(milliseconds:), | |
vsync: provider, | |
); | |
///返回键旋转动画 | |
closeController = AnimationController( | |
duration: const Duration(milliseconds:), | |
vsync: provider, | |
); | |
///页面渲染完才开始执行,不然第一次打开不会启动动画 | |
WidgetsBinding.instance.addPostFrameCallback((duration) { | |
closeAnimation = | |
Tween(begin: pi /, end: pi / 2).animate(closeController) | |
..addListener(() { | |
angle.value = closeAnimation.value; | |
}); | |
closeController.forward(); | |
}); | |
///关闭按钮点击事件 | |
void close() { | |
///反转动画,并关闭页面 | |
Future.delayed( | |
const Duration(milliseconds:), () { | |
Get.back(); | |
}); | |
closeController.reverse(); | |
} | |
IconButton( | |
onPressed: null, | |
alignment: Alignment.center, | |
icon: Transform.rotate( | |
angle: controller.angle.value, | |
child: SvgPicture.asset( | |
"assets/user/ic-train-car-close.svg", | |
width:, | |
height:, | |
color: Colors.black, | |
), | |
)) |
2、底部四个栏目变速上移动画+渐变动画
四个栏目其实就是个平移动画,只不过闲鱼是四个栏目一起平移,而我选择了变速平移,这样视觉效果上会好一点。
//透明度变化 | |
List<AnimationController> opacityControllerList = []; | |
//上移动画,由于每个栏目的移动速度不一样,需要用List保存四个AnimationController, | |
//如果想像闲鱼那种整体上移,则只用一个AnimationController即可。 | |
List<AnimationController> offsetControllerList = []; | |
List<Animation<Offset>> offsetAnimationList = []; | |
//之所以用addIf,是因为项目中这几个栏目的显示是动态显示的,这里就直接写成true | |
Column( | |
children: [] | |
..addIf( | |
true, | |
buildItem('assets/user/ic-train-nomal-car.webp',"学车加练","自主预约,快速拿证")) | |
..addIf( | |
true, | |
buildItem('assets/user/ic-train-fuuxn-car.webp',"有证复训","优质陪练,轻松驾车")) | |
..addIf( | |
true, | |
buildItem('assets/user/ic-train-jiaxun-car.webp',"模拟加训","考前加训,临考不惧")) | |
..addIf( | |
true, | |
buildItem('assets/user/ic-train-jiakao-car.webp',"驾考报名","快捷报名无门槛")) | |
..add(playWidget()) | |
..addAll([.space, | |
]), | |
) | |
//仅仅是为了在offsetController全部初始化完后执行play() | |
Widget playWidget() { | |
//执行动画 | |
play(); | |
return Container(); | |
} | |
int i =; | |
Widget buildItem(String img,String tab,String slogan) { | |
//由于底部栏目是动态显示的,需要在创建Widget时一同创建offsetController和offsetAnimation | |
i++; | |
AnimationController offsetController = AnimationController( | |
duration: Duration(milliseconds: + i * 20), | |
vsync: this, | |
); | |
Animation<Offset> offsetAnimation = Tween<Offset>( | |
begin: const Offset(, 2.5), | |
end: const Offset(, 0), | |
).animate(CurvedAnimation( | |
parent: offsetController, | |
// curve: Curves.easeInOutSine, | |
curve: const Cubic(.12, 0.28, 0.48, 1), | |
)); | |
AnimationController opacityController = AnimationController( | |
duration: const Duration(milliseconds:), | |
lowerBound:.2, | |
upperBound:.0, | |
vsync: this); | |
opacityControllerList.add(opacityController); | |
offsetControllerList.add(offsetController); | |
offsetAnimationList.add(offsetAnimation); | |
return SlideTransition( | |
position: offsetAnimation, | |
child: FadeTransition( | |
opacity: opacityController, | |
child: Container( | |
margin: EdgeInsets.only(bottom:), | |
height:, | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.all(Radius.circular()), | |
color: const Color(xfffafafa)), | |
child: | |
Row(mainAxisAlignment: MainAxisAlignment.center, children: [.space, | |
Image.asset(img, width:, height: 44), | |
.space, | |
Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text(tab, | |
style: const TextStyle( | |
color: Color(XFF000000), | |
fontSize:, | |
fontWeight: FontWeight.bold)), | |
Text(slogan, | |
style: const TextStyle( | |
color: Color(XFF6e6e6e), fontSize: 12)), | |
]).expanded, | |
Image.asset("assets/user/ic-train-arrow.webp", | |
width:, height: 44), | |
.space | |
])).inkWell( | |
onTap: () {}, | |
delayMilliseconds:)), | |
); | |
} | |
//执行动画 | |
void play() async { | |
for (int i =; i < offsetControllerList.length; i++) { | |
opacityControllerList[i].forward(); | |
///栏目正序依次延迟( + 2 * i) * i的时间,曲线速率 | |
Future.delayed(Duration(milliseconds: ( + 2 * i) * i), () { | |
offsetControllerList[i] | |
.forward() | |
.whenComplete(() => offsetControllerList[i].stop()); | |
}); | |
} | |
} | |
///关闭按钮点击事件 | |
void close() { | |
///反转动画,并关闭页面 | |
Future.delayed( | |
const Duration(milliseconds:), () { | |
Get.back(); | |
}); | |
for (int i = offsetControllerList.length -; i >= 0; i--) { | |
///栏目倒叙依次延迟( + 2 * (offsetControllerList.length-1-i)) * (offsetControllerList.length-1-i))的时间 | |
Future.delayed( | |
Duration( | |
milliseconds: | |
( + 2 * (offsetControllerList.length-1-i)) * (offsetControllerList.length-1-i)), () { | |
offsetControllerList[i].reverse(); | |
}); | |
} | |
opacityTopController.reverse(); | |
} |
3、中间图片渐变动画
渐变动画使用FadeTransition即可。
///图片透明度渐变动画控制器 | |
late final AnimationController imgController; | |
///图片透明度渐变动画 | |
imgController = AnimationController( | |
duration: const Duration(milliseconds:), | |
lowerBound:.0, | |
upperBound:.0, | |
vsync: provider); | |
imgController.forward().whenComplete(() => imgController.stop()); | |
///渐变过渡 | |
FadeTransition( | |
opacity: imgController, | |
child: | |
Image.asset("assets/user/ic-traincar-guide.webp"), | |
), | |
///关闭按钮点击事件 | |
void close() { | |
imgController.reverse(); | |
} |
4、顶部文案渐变动画+下移动画
///顶部标题下移动画控制器 | |
late final AnimationController offsetTopController; | |
late final Animation<Offset> offsetTopAnimation; | |
///顶部标题渐变动画控制器 | |
late final AnimationController opacityTopController; | |
///顶部标题上移动画 | |
offsetTopController = AnimationController( | |
duration: const Duration(milliseconds:), | |
vsync: provider, | |
); | |
offsetTopController | |
.forward() | |
.whenComplete(() => offsetTopController.stop()); | |
offsetTopAnimation = Tween<Offset>( | |
begin: const Offset(, -0.8), | |
end: const Offset(, 0), | |
).animate(CurvedAnimation( | |
parent: offsetTopController, | |
curve: Curves.easeInOutCubic, | |
)); | |
offsetTopController | |
.forward() | |
.whenComplete(() => offsetTopController.stop()); | |
//UI | |
SlideTransition( | |
position: offsetTopAnimation, | |
child: FadeTransition( | |
opacity: opacityTopController, | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisAlignment: MainAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, | |
children: [.space, | |
const Text( | |
'练车指南', | |
style: TextStyle( | |
color: Color(XFF141414), | |
fontSize:, | |
fontWeight: FontWeight.w, | |
), | |
),.space, | |
const Text('易练只为您提供优质教练,为您的安全保驾护航', | |
style: TextStyle( | |
color: Color(XFF141414), | |
fontSize:)), | |
], | |
))), | |
///关闭按钮点击事件 | |
void close() { | |
offsetTopController.reverse(); | |
opacityTopController.reverse(); | |
} |
5、注销动画
最后,在关闭页面的时候不要忘记注销动画。
///关闭时注销动画 | |
void dispose() { | |
for (int i = offsetControllerList.length -; i > 0; i--) { | |
offsetControllerList[i].dispose(); | |
} | |
offsetTopController.dispose(); | |
opacityTopController.dispose(); | |
imgController.dispose(); | |
closeController.dispose(); | |
} |