Flutter仿網(wǎng)易實(shí)現(xiàn)廣告卡片3D翻轉(zhuǎn)效果
前言
在逛網(wǎng)易新聞時,發(fā)現(xiàn)列表中的廣告在你滑動的時候會有一個3D旋轉(zhuǎn)的交互引你的注意,不得不說這些產(chǎn)品為了讓用戶看廣告花樣百出,那么今天我們就用Flutter也實(shí)現(xiàn)這么一個效果。
先看下網(wǎng)易新聞的效果:

OK,先說了我看到這個效果的思路:首先我們看到這個廣告卡片在從底部向上滑的時候在完全滑入到顯示屏區(qū)域內(nèi)開始3D旋轉(zhuǎn),到這個卡片頂部到達(dá)列表頂部時翻轉(zhuǎn)結(jié)束,那我們主要還是需要計算這個廣告卡片距離列表底部的距離和距離列表頂部的距離,有了這兩個距離,那么我們就可以根據(jù)Transform進(jìn)行Y軸翻轉(zhuǎn)180°就好了。
實(shí)現(xiàn)思路
1、獲取各種距離
看圖:

思路: 如上圖,狀態(tài)欄高度和AppBar的高度我們都可以得到,屏幕的高度我們也可以得到,那么自然我們就可以計算出內(nèi)容區(qū)域的高度,拿到內(nèi)容區(qū)域高度我們先放到一邊,接下來我們需要獲取廣告區(qū)域距離AppBar的距離,這是一個進(jìn)行翻轉(zhuǎn)核心數(shù)據(jù),這里我們可以通過GlobalKey獲取這個組件的渲染對象RenderObject并轉(zhuǎn)化為RenderBox,通過RenderBox我們可以獲取到這個組件在屏幕上的坐標(biāo),這樣我們拿到這個坐標(biāo)Y軸的值就是當(dāng)前組件距離頂部的距離
核心代碼:
// 這里我們獲取相對于屏幕左上角組件的坐標(biāo)y軸
GlobalKey _globalKey = GlobalKey();
RenderBox? renderBox =
_globalKey.currentContext?.findRenderObject() as RenderBox?;
double? dy = renderBox?.localToGlobal(Offset.zero).dy;接下來我們就可以計算出幾個關(guān)鍵數(shù)據(jù):
狀態(tài)欄高度:stateHeight = MediaQuery.of(context).padding.top;已知。
AppBar高度:appBarHeight = 56; 默認(rèn)高度 已知。
內(nèi)容區(qū)域高度:contentHeight = MediaQuery.of(context).size.height - stateHeight -appBarHeight;
假設(shè)我們廣告區(qū)域的高度是200,廣告組件的高度一般都是固定的。
得出:廣告上方距離頂部的最大距離:maxHeight= contentheight - 200;
還記得我們上面獲取的dy值嗎,這個值是當(dāng)前廣告上面距離屏幕頂部的距離,那么我們就可以得出當(dāng)前廣告距離AppBar底部的距離: bannerY = dy - appBarHeight - stateHeight;
同理可以得出當(dāng)前廣告的滑動距離:scrollY = contentheight - 200 - bannerY;
滑動的最大距離就是:maxSrollY = contentHeight - bannerHeight;
2、翻轉(zhuǎn)
搞定了這些數(shù)據(jù),接下來的工作就比較簡單了,我們使用Transform組件來進(jìn)行180度的翻轉(zhuǎn)就可以了,
獲取當(dāng)前滑動的比例,那就是當(dāng)前滑動距離/最大滑動距離,也就是 scrollY/maxHeight; 接下來我們看下Transform這個類,
代碼:
Container(
padding: EdgeInsetsDirectional.only(
start: 20, end: 20, top: 30, bottom: 30),
height: bannerHeight,
key: _globalKey,
child: Transform(
alignment: Alignment.center, //相對于坐標(biāo)系原點(diǎn)的對齊方式 從中間翻轉(zhuǎn)
transform: Matrix4.identity()//這是一個矩陣變換類,可以對組件的坐標(biāo)進(jìn)行翻轉(zhuǎn),有興趣可以了解下
..rotateX(0)// 翻轉(zhuǎn)X軸
..rotateY(angle),// 翻轉(zhuǎn)Y軸 這里需要傳入角度
child: Image.asset(
"images/img.png",
fit: BoxFit.fill,
),
));通過rotateY就可以將組件繞著Y軸進(jìn)行翻轉(zhuǎn),也就達(dá)到了我們想要的3D效果,上面我們得到了滑動比例,那么我們就可以用這個比例乘以PI值,刷新頁面就可以了唄,接下來我們通過滑動監(jiān)聽將這個數(shù)字進(jìn)行更新看下效果:
核心代碼:
double h = MediaQuery.of(context).size.height; //屏幕高度
RenderBox? renderBox =
_globalKey.currentContext?.findRenderObject() as RenderBox?;
double? dy = renderBox?.localToGlobal(Offset.zero).dy;
// 56 AppBar 高度
if (dy != null) {
// 廣告距離AppBar Y軸距離
var bannerY = dy - appBarHeight - stateHeight;
// 主內(nèi)容區(qū)域高度
var contentHeight = h - appBarHeight - stateHeight;
if (bannerY + bannerHeight < contentHeight && bannerY > 0) {
setState(() {
//滑動的距離
angle = pi * ((contentHeight - bannerHeight - bannerY) /
(contentHeight - bannerHeight));
});
}
}效果:

翻轉(zhuǎn)效果確實(shí)實(shí)現(xiàn)了,不過怎么看著有點(diǎn)不對勁呢,這里有兩個問題:
1、劃上去翻過來的圖片直接鏡像了。

2、當(dāng)我們滑動到一半的時候,兩邊的寬度是一致的,3D效果不明顯。

其實(shí)這兩個問題都很好解決,
第一個滑動角度問題,我們滑動到90度進(jìn)行翻過來的時候只需要將角度+180度進(jìn)行翻轉(zhuǎn)即可。這樣就相當(dāng)于翻了360度,最后自然會回到原來的圖片的樣子。
第二個我們需要設(shè)置Transform的一個屬性..setEntry(3, 2, 0.002),讓卡片翻轉(zhuǎn)過程中看起來遠(yuǎn)小近大的效果。
我們加上這兩個屬性再看看效果:

這樣看著是不是效果就好多了。
這里我只簡單了插入了一條廣告,如果有多個廣告建議用一個Map對象將Key存儲起來,因?yàn)橐粋€Key只能對應(yīng)一個組件。
完整代碼
class ListViewWidgetDemo extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return ListViewState();
}
}
class ListViewState extends State<ListViewWidgetDemo> {
List<NewsListBean> lis = <NewsListBean>[];
late ScrollController _scrollController = ScrollController();
String imageUrl =
"https://images.unsplash.com/photo-1451187580459-43490279c0fa?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=60";
GlobalKey _globalKey = GlobalKey();
double angle = 0;
double bannerHeight = 200;
@override
void initState() {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
_scrollController.addListener(() {
double appBarHeight = 56;
double stateHeight = MediaQuery.of(context).padding.top;
double h = MediaQuery.of(context).size.height; //屏幕高度
RenderBox? renderBox =
_globalKey.currentContext?.findRenderObject() as RenderBox?;
double? dy = renderBox?.localToGlobal(Offset.zero).dy;
// 56 AppBar 高度
if (dy != null) {
// 廣告距離AppBar Y軸距離
var bannerY = dy - appBarHeight - stateHeight;
// 主內(nèi)容區(qū)域高度
var contentHeight = h - appBarHeight - stateHeight;
if (bannerY + bannerHeight < contentHeight && bannerY > 0) {
setState(() {
//滑動的距離
angle = pi *
((contentHeight - bannerHeight - bannerY) /
(contentHeight - bannerHeight));
// 前半部分 0-90 后半部分 270-360
if (angle >= (pi / 2)) {
angle = angle + pi;
}
});
}
}
});
});
super.initState();
for (int i = 0; i < 40; i++) {
lis.add(NewsListBean(
i.isEven ? 0 : 1,
"資訊標(biāo)題$i",
imageUrl,
));
}
// 插入廣告
lis.insert(12, NewsListBean(2, "廣告", imageUrl));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("仿網(wǎng)易新聞廣告卡片翻轉(zhuǎn)"),
),
body: ListView.builder(
controller: _scrollController,
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: lis.length,
itemBuilder: (context, index) {
return _listWidget(lis[index]);
}));
}
Widget _listWidget(NewsListBean bean) {
late Widget widget;
switch (bean.type) {
case 0:
widget = Container(
height: 50,
padding: EdgeInsetsDirectional.only(start: 20),
alignment: Alignment.centerLeft,
color: Colors.blue[200],
child: Text(
bean.title,
style: TextStyle(),
));
break;
case 1:
widget = Row(
children: [
Expanded(
child: Container(
height: 80,
alignment: Alignment.center,
color: Colors.red[200],
margin: EdgeInsets.all(10),
child: Text(bean.title)),
),
Image.network(
bean.image,
width: 40,
height: 40,
)
],
);
break;
case 2:
widget = Container(
padding: EdgeInsetsDirectional.only(
start: 20, end: 20, top: 30, bottom: 30),
height: bannerHeight,
key: _globalKey,
child: Transform(
alignment: Alignment.center, //相對于坐標(biāo)系原點(diǎn)的對齊方式
transform: Matrix4.identity()
..setEntry(3, 2, 0.002)
..rotateX(0)
..rotateY(angle),
child: Image.asset(
"images/img.png",
fit: BoxFit.fill,
),
));
break;
default:
widget = SizedBox();
break;
}
return widget;
}
}
class NewsListBean {
//資訊類型 0:資訊無圖 1:資訊有圖 2:3d廣告
final int type;
final bool isFirst;
final String title;
final String image;
NewsListBean(this.type, this.title, this.image, {this.isFirst = false});
}小結(jié)
通過本篇文章我們可以學(xué)習(xí)到,獲取組件的坐標(biāo)以及3D翻轉(zhuǎn)的效果,實(shí)現(xiàn)這種效果可能也有其他更好的方式,本篇文章只提供了一個實(shí)現(xiàn)思路。
到此這篇關(guān)于Flutter仿網(wǎng)易實(shí)現(xiàn)廣告卡片3D翻轉(zhuǎn)效果的文章就介紹到這了,更多相關(guān)Flutter廣告卡片翻轉(zhuǎn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
正確在Flutter中添加webview實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了正確在Flutter中添加webview實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
Android 自定義 View 中使用 Spannable的實(shí)例詳解
這篇文章主要介紹了Android 自定義 View 中使用 Spannable的相關(guān)知識,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05
總結(jié)Android App內(nèi)存優(yōu)化之圖片優(yōu)化
網(wǎng)上有很多大拿分享的關(guān)于Android性能優(yōu)化的文章,主要是通過各種工具分析,使用合理的技巧優(yōu)化APP的體驗(yàn),提升APP的流暢度,但關(guān)于內(nèi)存優(yōu)化的文章很少有看到。下面是我在實(shí)踐過程中使用的一些方法,很多都是不太成熟的項目,只是將其作為一種處理方式分享給大家。2016-08-08
Android Room數(shù)據(jù)庫容易遇到的問題以及解決方法
這篇文章給大家介紹了我們在Android Room數(shù)據(jù)庫容易遇到的坑以及解決方法,文中有詳細(xì)的代碼示例供我們參考,具有一定的參考價值,需要的朋友可以參考下2023-09-09
Flutter?EventBus事件總線的應(yīng)用詳解
這篇文章主要為大家介紹了Flutter?EventBus事件總線的應(yīng)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
教你五分鐘實(shí)現(xiàn)Android超漂亮的刻度輪播控件實(shí)例教程
說到輪播圖,想必大家都不陌生,下面這篇文章主要給大家介紹了關(guān)于如何利用五分鐘快速實(shí)現(xiàn)一款超漂亮的Android刻度輪播控件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧2018-09-09
Android Studio項目適配AndroidX(Android 9.0)的方法步驟
這篇文章主要介紹了Android Studio項目適配AndroidX(Android 9.0)的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
另外兩種Android沉浸式狀態(tài)欄實(shí)現(xiàn)思路
這篇文章主要為大家介紹了另外兩種Android沉浸式狀態(tài)欄實(shí)現(xiàn)思路,android5.0及以后版本都支持給狀態(tài)欄著色,而目前android主流版本還是4.4,想要深入了解的朋友可以參考一下2016-01-01

