Android Flutter實(shí)現(xiàn)仿閑魚(yú)動(dòng)畫(huà)效果
前言
目前正在做的項(xiàng)目,為了增加用戶的體驗(yàn)度,準(zhǔn)備增加一些動(dòng)畫(huà)效果,其中底部欄中間按鈕的點(diǎn)擊事件參考了閑魚(yú)的動(dòng)效,便在此基礎(chǔ)上仿寫(xiě)了該動(dòng)效,并增加了一些新的效果。
動(dòng)效
閑魚(yú)動(dòng)效

仿寫(xiě)效果

思路
根據(jù)UI的設(shè)計(jì)圖,對(duì)每個(gè)模塊設(shè)計(jì)好動(dòng)畫(huà)效果,本人主要設(shè)計(jì)了以下四個(gè)效果。
1、底部返回鍵旋轉(zhuǎn)動(dòng)畫(huà)
底部返回按鈕動(dòng)畫(huà)其實(shí)就是個(gè)旋轉(zhuǎn)動(dòng)畫(huà),利用Transform.rotate設(shè)置angle的值即可,這里使用了GetX來(lái)對(duì)angle進(jìn)行動(dòng)態(tài)控制。
//返回鍵旋轉(zhuǎn)角度,初始旋轉(zhuǎn)45度,使其初始樣式為 +
var angle = (pi / 4).obs;
///關(guān)閉按鈕旋轉(zhuǎn)動(dòng)畫(huà)控制器
late final AnimationController closeController;
late final Animation<double> closeAnimation;
///返回鍵旋轉(zhuǎn)動(dòng)畫(huà)
closeController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: provider,
);
///返回鍵旋轉(zhuǎn)動(dòng)畫(huà)
closeController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: provider,
);
///頁(yè)面渲染完才開(kāi)始執(zhí)行,不然第一次打開(kāi)不會(huì)啟動(dòng)動(dòng)畫(huà)
WidgetsBinding.instance.addPostFrameCallback((duration) {
closeAnimation =
Tween(begin: pi / 4, end: pi / 2).animate(closeController)
..addListener(() {
angle.value = closeAnimation.value;
});
closeController.forward();
});
///關(guān)閉按鈕點(diǎn)擊事件
void close() {
///反轉(zhuǎn)動(dòng)畫(huà),并關(guān)閉頁(yè)面
Future.delayed(
const Duration(milliseconds: 120), () {
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: 18,
height: 18,
color: Colors.black,
),
))2、底部四個(gè)欄目變速上移動(dòng)畫(huà)+漸變動(dòng)畫(huà)
四個(gè)欄目其實(shí)就是個(gè)平移動(dòng)畫(huà),只不過(guò)閑魚(yú)是四個(gè)欄目一起平移,而我選擇了變速平移,這樣視覺(jué)效果上會(huì)好一點(diǎn)。
//透明度變化
List<AnimationController> opacityControllerList = [];
//上移動(dòng)畫(huà),由于每個(gè)欄目的移動(dòng)速度不一樣,需要用List保存四個(gè)AnimationController,
//如果想像閑魚(yú)那種整體上移,則只用一個(gè)AnimationController即可。
List<AnimationController> offsetControllerList = [];
List<Animation<Offset>> offsetAnimationList = [];
//之所以用addIf,是因?yàn)轫?xiàng)目中這幾個(gè)欄目的顯示是動(dòng)態(tài)顯示的,這里就直接寫(xiě)成true
Column(
children: []
..addIf(
true,
buildItem('assets/user/ic-train-nomal-car.webp',"學(xué)車(chē)加練","自主預(yù)約,快速拿證"))
..addIf(
true,
buildItem('assets/user/ic-train-fuuxn-car.webp',"有證復(fù)訓(xùn)","優(yōu)質(zhì)陪練,輕松駕車(chē)"))
..addIf(
true,
buildItem('assets/user/ic-train-jiaxun-car.webp',"模擬加訓(xùn)","考前加訓(xùn),臨考不懼"))
..addIf(
true,
buildItem('assets/user/ic-train-jiakao-car.webp',"駕考報(bào)名","快捷報(bào)名無(wú)門(mén)檻"))
..add(playWidget())
..addAll([
17.space,
]),
)
//僅僅是為了在offsetController全部初始化完后執(zhí)行play()
Widget playWidget() {
//執(zhí)行動(dòng)畫(huà)
play();
return Container();
}
int i = 0;
Widget buildItem(String img,String tab,String slogan) {
//由于底部欄目是動(dòng)態(tài)顯示的,需要在創(chuàng)建Widget時(shí)一同創(chuàng)建offsetController和offsetAnimation
i++;
AnimationController offsetController = AnimationController(
duration: Duration(milliseconds: 100 + i * 20),
vsync: this,
);
Animation<Offset> offsetAnimation = Tween<Offset>(
begin: const Offset(0, 2.5),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: offsetController,
// curve: Curves.easeInOutSine,
curve: const Cubic(0.12, 0.28, 0.48, 1),
));
AnimationController opacityController = AnimationController(
duration: const Duration(milliseconds: 500),
lowerBound: 0.2,
upperBound: 1.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: 16),
height: 62,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: const Color(0xfffafafa)),
child:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
24.space,
Image.asset(img, width: 44, height: 44),
12.space,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(tab,
style: const TextStyle(
color: Color(0XFF000000),
fontSize: 16,
fontWeight: FontWeight.bold)),
Text(slogan,
style: const TextStyle(
color: Color(0XFF6e6e6e), fontSize: 12)),
]).expanded,
Image.asset("assets/user/ic-train-arrow.webp",
width: 44, height: 44),
17.space
])).inkWell(
onTap: () {},
delayMilliseconds: 50)),
);
}
//執(zhí)行動(dòng)畫(huà)
void play() async {
for (int i = 0; i < offsetControllerList.length; i++) {
opacityControllerList[i].forward();
///欄目正序依次延遲(40 + 2 * i) * i的時(shí)間,曲線速率
Future.delayed(Duration(milliseconds: (40 + 2 * i) * i), () {
offsetControllerList[i]
.forward()
.whenComplete(() => offsetControllerList[i].stop());
});
}
}
///關(guān)閉按鈕點(diǎn)擊事件
void close() {
///反轉(zhuǎn)動(dòng)畫(huà),并關(guān)閉頁(yè)面
Future.delayed(
const Duration(milliseconds: 120), () {
Get.back();
});
for (int i = offsetControllerList.length - 1; i >= 0; i--) {
///欄目倒敘依次延遲(40 + 2 * (offsetControllerList.length-1-i)) * (offsetControllerList.length-1-i))的時(shí)間
Future.delayed(
Duration(
milliseconds:
(40 + 2 * (offsetControllerList.length-1-i)) * (offsetControllerList.length-1-i)), () {
offsetControllerList[i].reverse();
});
}
opacityTopController.reverse();
}3、中間圖片漸變動(dòng)畫(huà)
漸變動(dòng)畫(huà)使用FadeTransition即可。
///圖片透明度漸變動(dòng)畫(huà)控制器
late final AnimationController imgController;
///圖片透明度漸變動(dòng)畫(huà)
imgController = AnimationController(
duration: const Duration(milliseconds: 500),
lowerBound: 0.0,
upperBound: 1.0,
vsync: provider);
imgController.forward().whenComplete(() => imgController.stop());
///漸變過(guò)渡
FadeTransition(
opacity: imgController,
child:
Image.asset("assets/user/ic-traincar-guide.webp"),
),
///關(guān)閉按鈕點(diǎn)擊事件
void close() {
imgController.reverse();
}4、頂部文案漸變動(dòng)畫(huà)+下移動(dòng)畫(huà)
///頂部標(biāo)題下移動(dòng)畫(huà)控制器
late final AnimationController offsetTopController;
late final Animation<Offset> offsetTopAnimation;
///頂部標(biāo)題漸變動(dòng)畫(huà)控制器
late final AnimationController opacityTopController;
///頂部標(biāo)題上移動(dòng)畫(huà)
offsetTopController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: provider,
);
offsetTopController
.forward()
.whenComplete(() => offsetTopController.stop());
offsetTopAnimation = Tween<Offset>(
begin: const Offset(0, -0.8),
end: const Offset(0, 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: [
80.space,
const Text(
'練車(chē)指南',
style: TextStyle(
color: Color(0XFF141414),
fontSize: 32,
fontWeight: FontWeight.w800,
),
),
2.space,
const Text('易練只為您提供優(yōu)質(zhì)教練,為您的安全保駕護(hù)航',
style: TextStyle(
color: Color(0XFF141414),
fontSize: 15)),
],
))),
///關(guān)閉按鈕點(diǎn)擊事件
void close() {
offsetTopController.reverse();
opacityTopController.reverse();
}5、注銷(xiāo)動(dòng)畫(huà)
最后,在關(guān)閉頁(yè)面的時(shí)候不要忘記注銷(xiāo)動(dòng)畫(huà)。
///關(guān)閉時(shí)注銷(xiāo)動(dòng)畫(huà)
void dispose() {
for (int i = offsetControllerList.length - 1; i > 0; i--) {
offsetControllerList[i].dispose();
}
offsetTopController.dispose();
opacityTopController.dispose();
imgController.dispose();
closeController.dispose();
}以上就是Android Flutter實(shí)現(xiàn)仿閑魚(yú)動(dòng)畫(huà)效果的詳細(xì)內(nèi)容,更多關(guān)于Android Flutter仿閑魚(yú)動(dòng)畫(huà)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android圖片的Base64編碼與解碼及解碼Base64圖片方法
Base64是網(wǎng)絡(luò)上最常見(jiàn)的用于傳輸8Bit字節(jié)碼的編碼方式之一,Base64就是一種基于64個(gè)可打印字符來(lái)表示二進(jìn)制數(shù)據(jù)的方法。接下來(lái)通過(guò)本文給大家分享Android圖片的Base64編碼與解碼及解碼Base64圖片,需要的朋友參考下吧2017-12-12
Android游戲開(kāi)發(fā)實(shí)踐之人物移動(dòng)地圖的平滑滾動(dòng)處理
玩過(guò)rpg游戲的朋友應(yīng)該都知道RPG的游戲地圖一般都比較大 今天我和大家分享一下在RPG游戲中如何來(lái)處理超出手機(jī)屏幕大小的游戲地圖。2014-06-06
Android GestureDetector用戶手勢(shì)檢測(cè)實(shí)例講解
這篇文章主要為大家詳細(xì)介紹了Android GestureDetector用戶手勢(shì)檢測(cè)實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android自定義Gradle插件的詳細(xì)過(guò)程
Groovy語(yǔ)言是一種jvm語(yǔ)言,最終也是編譯成class文件然后在jvm上執(zhí)行,所以所有的Java語(yǔ)言的特性Groovy都支持,我們可以完全混寫(xiě)Java和Groovy,對(duì)Android自定義Gradle插件相關(guān)知識(shí),感興趣的朋友跟隨小編一起看看吧2021-07-07
Service與Activity之間的通信(同一進(jìn)程)
這篇文章主要介紹了Service與Activity之間的通信(同一進(jìn)程)的相關(guān)資料,需要的朋友可以參考下2016-03-03

