目录
前言
在逛网易新闻时,发现列表中的广告在你滑动的时候会有一个3D旋转的交互引你的注意,不得不说这些产品为了让用户看广告花样百出,那么今天我们就用Flutter也实现这么一个效果。
先看下网易新闻的效果:
OK,先说了我看到这个效果的思路:首先我们看到这个广告卡片在从底部向上滑的时候在完全滑入到显示屏区域内开始3D旋转,到这个卡片顶部到达列表顶部时翻转结束,那我们主要还是需要计算这个广告卡片距离列表底部的距离和距离列表顶部的距离,有了这两个距离,那么我们就可以根据Transform
进行Y轴翻转180°就好了。
实现思路
1、获取各种距离
看图:
思路: 如上图,状态栏高度和AppBar
的高度我们都可以得到,屏幕的高度我们也可以得到,那么自然我们就可以计算出内容区域的高度,拿到内容区域高度我们先放到一边,接下来我们需要获取广告区域距离AppBar
的距离,这是一个进行翻转核心数据,这里我们可以通过GlobalKey
获取这个组件的渲染对象RenderObject
并转化为RenderBox
,通过RenderBox
我们可以获取到这个组件在屏幕上的坐标,这样我们拿到这个坐标Y轴的值就是当前组件距离顶部的距离
核心代码:
// 这里我们获取相对于屏幕左上角组件的坐标y轴 GlobalKey _globalKey = GlobalKey(); RenderBox? renderBox = _globalKey.currentContext?.findRenderObject() as RenderBox?; double? dy = renderBox?.localToGlobal(Offset.zero).dy;
接下来我们就可以计算出几个关键数据:
状态栏高度:stateHeight = MediaQuery.of(context).padding.top;
已知。
AppBar高度:appBarHeight = 56; 默认高度 已知。
内容区域高度:contentHeight = MediaQuery.of(context).size.height - stateHeight -appBarHeight;
假设我们广告区域的高度是200,广告组件的高度一般都是固定的。
得出:广告上方距离顶部的最大距离:maxHeight= contentheight - 200;
还记得我们上面获取的dy值吗,这个值是当前广告上面距离屏幕顶部的距离,那么我们就可以得出当前广告距离AppBar底部的距离: bannerY = dy - appBarHeight - stateHeight;
同理可以得出当前广告的滑动距离:scrollY = contentheight - 200 - bannerY
;
滑动的最大距离就是:maxSrollY = contentHeight - bannerHeight
;
2、翻转
搞定了这些数据,接下来的工作就比较简单了,我们使用Transform
组件来进行180度的翻转就可以了,
获取当前滑动的比例,那就是当前滑动距离/最大滑动距离,也就是 scrollY/maxHeight;
接下来我们看下Transform
这个类,
代码:
Container( padding: EdgeInsetsDirectional.only( start: 20, end: 20, top: 30, bottom: 30), height: bannerHeight, key: _globalKey, child: Transform( alignment: Alignment.center, //相对于坐标系原点的对齐方式 从中间翻转 transform: Matrix4.identity()//这是一个矩阵变换类,可以对组件的坐标进行翻转,有兴趣可以了解下 ..rotateX(0)// 翻转X轴 ..rotateY(angle),// 翻转Y轴 这里需要传入角度 child: Image.asset( "images/img.png", fit: BoxFit.fill, ), ));
通过rotateY
就可以将组件绕着Y轴进行翻转,也就达到了我们想要的3D效果,上面我们得到了滑动比例,那么我们就可以用这个比例乘以PI值
,刷新页面就可以了呗,接下来我们通过滑动监听将这个数字进行更新看下效果:
核心代码:
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; // 主内容区域高度 var contentHeight = h - appBarHeight - stateHeight; if (bannerY + bannerHeight < contentHeight && bannerY > 0) { setState(() { //滑动的距离 angle = pi * ((contentHeight - bannerHeight - bannerY) / (contentHeight - bannerHeight)); }); } }
效果:
翻转效果确实实现了,不过怎么看着有点不对劲呢,这里有两个问题:
1、划上去翻过来的图片直接镜像了。
2、当我们滑动到一半的时候,两边的宽度是一致的,3D效果不明显。
其实这两个问题都很好解决,
第一个滑动角度问题,我们滑动到90度进行翻过来的时候只需要将角度+180度进行翻转即可。这样就相当于翻了360度,最后自然会回到原来的图片的样子。
第二个我们需要设置Transform
的一个属性..setEntry(3, 2, 0.002)
,让卡片翻转过程中看起来远小近大的效果。
我们加上这两个属性再看看效果:
这样看着是不是效果就好多了。
这里我只简单了插入了一条广告,如果有多个广告建议用一个Map
对象将Key
存储起来,因为一个Key
只能对应一个组件。
完整代码
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; // 主内容区域高度 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, "资讯标题$i", imageUrl, )); } // 插入广告 lis.insert(12, NewsListBean(2, "广告", imageUrl)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("仿网易新闻广告卡片翻转"), ), 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, //相对于坐标系原点的对齐方式 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}); }
小结
通过本篇文章我们可以学习到,获取组件的坐标以及3D翻转的效果,实现这种效果可能也有其他更好的方式,本篇文章只提供了一个实现思路。