Flutter仿網(wǎng)易實(shí)現(xiàn)廣告卡片3D翻轉(zhuǎn)效果
前言
在逛網(wǎng)易新聞時(shí),發(fā)現(xiàn)列表中的廣告在你滑動(dòng)的時(shí)候會(huì)有一個(gè)3D旋轉(zhuǎn)的交互引你的注意,不得不說這些產(chǎn)品為了讓用戶看廣告花樣百出,那么今天我們就用Flutter也實(shí)現(xiàn)這么一個(gè)效果。
先看下網(wǎng)易新聞的效果:
OK,先說了我看到這個(gè)效果的思路:首先我們看到這個(gè)廣告卡片在從底部向上滑的時(shí)候在完全滑入到顯示屏區(qū)域內(nèi)開始3D旋轉(zhuǎn),到這個(gè)卡片頂部到達(dá)列表頂部時(shí)翻轉(zhuǎn)結(jié)束,那我們主要還是需要計(jì)算這個(gè)廣告卡片距離列表底部的距離和距離列表頂部的距離,有了這兩個(gè)距離,那么我們就可以根據(jù)Transform
進(jìn)行Y軸翻轉(zhuǎn)180°就好了。
實(shí)現(xiàn)思路
1、獲取各種距離
看圖:
思路: 如上圖,狀態(tài)欄高度和AppBar
的高度我們都可以得到,屏幕的高度我們也可以得到,那么自然我們就可以計(jì)算出內(nèi)容區(qū)域的高度,拿到內(nèi)容區(qū)域高度我們先放到一邊,接下來我們需要獲取廣告區(qū)域距離AppBar
的距離,這是一個(gè)進(jìn)行翻轉(zhuǎn)核心數(shù)據(jù),這里我們可以通過GlobalKey
獲取這個(gè)組件的渲染對(duì)象RenderObject
并轉(zhuǎn)化為RenderBox
,通過RenderBox
我們可以獲取到這個(gè)組件在屏幕上的坐標(biāo),這樣我們拿到這個(gè)坐標(biāo)Y軸的值就是當(dāng)前組件距離頂部的距離
核心代碼:
// 這里我們獲取相對(duì)于屏幕左上角組件的坐標(biāo)y軸 GlobalKey _globalKey = GlobalKey(); RenderBox? renderBox = _globalKey.currentContext?.findRenderObject() as RenderBox?; double? dy = renderBox?.localToGlobal(Offset.zero).dy;
接下來我們就可以計(jì)算出幾個(gè)關(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值嗎,這個(gè)值是當(dāng)前廣告上面距離屏幕頂部的距離,那么我們就可以得出當(dāng)前廣告距離AppBar底部的距離: bannerY = dy - appBarHeight - stateHeight;
同理可以得出當(dāng)前廣告的滑動(dòng)距離:scrollY = contentheight - 200 - bannerY
;
滑動(dòng)的最大距離就是:maxSrollY = contentHeight - bannerHeight
;
2、翻轉(zhuǎn)
搞定了這些數(shù)據(jù),接下來的工作就比較簡(jiǎn)單了,我們使用Transform
組件來進(jìn)行180度的翻轉(zhuǎn)就可以了,
獲取當(dāng)前滑動(dòng)的比例,那就是當(dāng)前滑動(dòng)距離/最大滑動(dòng)距離,也就是 scrollY/maxHeight;
接下來我們看下Transform
這個(gè)類,
代碼:
Container( padding: EdgeInsetsDirectional.only( start: 20, end: 20, top: 30, bottom: 30), height: bannerHeight, key: _globalKey, child: Transform( alignment: Alignment.center, //相對(duì)于坐標(biāo)系原點(diǎn)的對(duì)齊方式 從中間翻轉(zhuǎn) transform: Matrix4.identity()//這是一個(gè)矩陣變換類,可以對(duì)組件的坐標(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效果,上面我們得到了滑動(dòng)比例,那么我們就可以用這個(gè)比例乘以PI值
,刷新頁(yè)面就可以了唄,接下來我們通過滑動(dòng)監(jiān)聽將這個(gè)數(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(() { //滑動(dòng)的距離 angle = pi * ((contentHeight - bannerHeight - bannerY) / (contentHeight - bannerHeight)); }); } }
效果:
翻轉(zhuǎn)效果確實(shí)實(shí)現(xiàn)了,不過怎么看著有點(diǎn)不對(duì)勁呢,這里有兩個(gè)問題:
1、劃上去翻過來的圖片直接鏡像了。
2、當(dāng)我們滑動(dòng)到一半的時(shí)候,兩邊的寬度是一致的,3D效果不明顯。
其實(shí)這兩個(gè)問題都很好解決,
第一個(gè)滑動(dòng)角度問題,我們滑動(dòng)到90度進(jìn)行翻過來的時(shí)候只需要將角度+180度進(jìn)行翻轉(zhuǎn)即可。這樣就相當(dāng)于翻了360度,最后自然會(huì)回到原來的圖片的樣子。
第二個(gè)我們需要設(shè)置Transform
的一個(gè)屬性..setEntry(3, 2, 0.002)
,讓卡片翻轉(zhuǎn)過程中看起來遠(yuǎn)小近大的效果。
我們加上這兩個(gè)屬性再看看效果:
這樣看著是不是效果就好多了。
這里我只簡(jiǎn)單了插入了一條廣告,如果有多個(gè)廣告建議用一個(gè)Map
對(duì)象將Key
存儲(chǔ)起來,因?yàn)橐粋€(gè)Key
只能對(duì)應(yīng)一個(gè)組件。
完整代碼
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(() { //滑動(dòng)的距離 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, //相對(duì)于坐標(biāo)系原點(diǎn)的對(duì)齊方式 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:資訊無(wú)圖 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)這種效果可能也有其他更好的方式,本篇文章只提供了一個(gè)實(shí)現(xiàn)思路。
到此這篇關(guān)于Flutter仿網(wǎng)易實(shí)現(xiàn)廣告卡片3D翻轉(zhuǎn)效果的文章就介紹到這了,更多相關(guān)Flutter廣告卡片翻轉(zhuǎn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
正確在Flutter中添加webview實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了正確在Flutter中添加webview實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Android 自定義 View 中使用 Spannable的實(shí)例詳解
這篇文章主要介紹了Android 自定義 View 中使用 Spannable的相關(guān)知識(shí),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下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í)踐過程中使用的一些方法,很多都是不太成熟的項(xiàng)目,只是將其作為一種處理方式分享給大家。2016-08-08Android Room數(shù)據(jù)庫(kù)容易遇到的問題以及解決方法
這篇文章給大家介紹了我們?cè)贏ndroid Room數(shù)據(jù)庫(kù)容易遇到的坑以及解決方法,文中有詳細(xì)的代碼示例供我們參考,具有一定的參考價(jià)值,需要的朋友可以參考下2023-09-09Flutter?EventBus事件總線的應(yīng)用詳解
這篇文章主要為大家介紹了Flutter?EventBus事件總線的應(yīng)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08Android apk 插件啟動(dòng)內(nèi)存釋放問題
這篇文章主要介紹了Android apk 插件啟動(dòng)內(nèi)存釋放問題的相關(guān)資料,需要的朋友可以參考下2017-06-06教你五分鐘實(shí)現(xiàn)Android超漂亮的刻度輪播控件實(shí)例教程
說到輪播圖,想必大家都不陌生,下面這篇文章主要給大家介紹了關(guān)于如何利用五分鐘快速實(shí)現(xiàn)一款超漂亮的Android刻度輪播控件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧2018-09-09Android Studio項(xiàng)目適配AndroidX(Android 9.0)的方法步驟
這篇文章主要介紹了Android Studio項(xiàng)目適配AndroidX(Android 9.0)的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(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