前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter实现电影院选座效果!

Flutter实现电影院选座效果!

原创
作者头像
用户9239674
发布2022-01-11 20:02:47
1.5K0
发布2022-01-11 20:02:47
举报

导语

接到了一个仿电影院的需求,上周几乎是找遍了百度,谷歌,stackoverflow。均没有找到用flutter实现的效果,那只能自己写一个了。本文只讲思路,具体实现还需各位看官自己动手。只要看懂了下面的思路,实现起来非常简单。

直接上效果图

竖屏:

图片
图片

初始化自适应屏幕的放大缩小效果:

图片
图片

布局分析

中间的座位=>矩阵,通过Column嵌套Row实现,不能通过GridView实现(滑动冲突,下文会说明)

左侧导航条=>一个简单的Column(不能用ListView,同样会造成滑动冲突)

交互分析&实现

放大缩小拖动效果:

对于放大缩小拖动的效果,Flutter现在有自带的组件InteractiveViewer

通过这个组件可完美实现放大缩小效果。组件属性这边不展开解释,比较简单,可点击上面链接自行了解。

这里讲下两个重点属性:

1、回调事件

  • 交互开始?onInteractionStart
  • 交互更新?onInteractionUpdate
  • 交互结束?onInteractionEnd

2、变换控制器transformationController

可以通过这个类来通过代码控制放大缩小效果

导航条跟随座位表放大缩小拖动:

左边导航条跟随中间座位的放大缩小,以及行数定位不偏离:

上面讲的那些东西一般大家都能想到,也很好实现。这个交互效果的真正难点是这个跟随滑动效果

由于左边的导航条是固定在最左侧的,而座位表可以全屏拖动,所以这座位表和导航条不能放在一个缩放组件里, 不然座位表放大的时候,直接将导航条放大出屏幕了。所以我们的思路就是将导航条和座位表作为Stack的子组件,然后座位表实现放大缩小效果,并且让导航条能跟随座位表进行放大缩小。笔者在这试了很多方法:

方法一:

左侧导航栏和中间座位表均使用InteractiveViewer

然后通过InteractiveViewer的回调事件和变换控器来实现效果同步

结果:

失败,transformationController的原理是Matrix4泛型的ValueNotifier(四维矩阵),简单的移动放大还能实现,完全克隆一个放大缩小拖动效果,笔者做不到。。各位如果线性代数非常牛逼的可以试试。

方法二:

flutter有一个同步滚动组件叫linked_scroll_controller

他能将两个scrollController绑定在一起,实现同步滚动。

所以让左侧导航栏使用ListView,中间座位表使用InteractiveViewer嵌套GridView, 然后将ListViewGridViewScrollController绑定在一起实现同步滚动。

结果:

失败,InteractiveViewer的滑动是通过Matrix4实现的,和ListView的滑动冲突。

同步滚动实现了,但是放大缩小的拖动无法执行。

方法三:

使用InteractiveViewer是逃不过的,不然自己实现放大缩小效果太头疼, 如果能像上面的linked_scroll_controller一样,将InteractiveViewer的缩放效果复制到另外一个InteractiveViewer中去,那就完美了。

就是方法一的思路,但是用InteractiveViewer开放的接口和控制器,无法完成,这个时候就需要去阅读理解InteractiveViewer的源码,看看有没有什么启发。

代码语言:javascript
复制
@override
??Widget?build(BuildContext?context)?{
????Widget?child?=?Transform(
??????transform:?_transformationController.value,
??????child:?KeyedSubtree(
????????key:?_childKey,
????????child:?widget.child,
??????),
????);

????if?(!widget.constrained)?{
??????child?=?OverflowBox(
????????alignment:?Alignment.topLeft,
????????minWidth:?0.0,
????????minHeight:?0.0,
????????maxWidth:?double.infinity,
????????maxHeight:?double.infinity,
????????//?maxHeight:?220.w,
????????child:?child,
??????);
????}

????if?(widget.clipBehavior?!=?Clip.none)?{
??????child?=?ClipRRect(
????????clipBehavior:?widget.clipBehavior,
????????child:?child,
??????);
????}

????//?A?GestureDetector?allows?the?detection?of?panning?and?zooming?gestures?on
????//?the?child.
????return?Listener(
??????key:?_parentKey,
??????onPointerSignal:?_receivedPointerSignal,
??????child:?GestureDetector(
????????behavior:?HitTestBehavior.opaque,
????????//?Necessary?when?panning?off?screen.
????????dragStartBehavior:?DragStartBehavior.start,
????????onScaleEnd:?onScaleEnd,
????????onScaleStart:?onScaleStart,
????????onScaleUpdate:?onScaleUpdate,
????????child:?child,
??????),
????);
??}

不看不知道,一看吓一跳,其实InteractiveViewer已经将所有的方法都替我们封装好了。

注意上面的GestureDetector,整个InteractiveViewer的手势交互方法,其实就是onScaleEndonScaleStartonScaleUpdate这三个方法。

那我们只需要将座位表组件回调的的这三个方法中的参数,传入到导航条组件中去就行,然后删掉导航条组件的GestureDetector,让导航条组件只接受来自座位表组件的手势交互参数。

我们只需重写两个InteractiveViewer,一个为主组件(座位表),一个为从组件(导航条),并开放InteractiveViewerState,当座位表组件回调手势的三个方法时,通过key将三个方法的参数传入导航条组件就OK。

代码语言:javascript
复制
_onInteractionUpdate(ScaleUpdateDetails?details)?{
????if?(controller.fromInteractiveViewKey.currentState?!=?null)?{
??????controller.fromInteractiveViewKey.currentState.onScaleUpdate(details);
????}
??}

??_onInteractionStart(ScaleStartDetails?details)?{
????if?(controller.fromInteractiveViewKey.currentState?!=?null)?{
??????controller.fromInteractiveViewKey.currentState.onScaleStart(details);
????}
??}

??_onInteractionEnd(ScaleEndDetails?details)?{
????if?(controller.fromInteractiveViewKey.currentState?!=?null)?{
??????controller.fromInteractiveViewKey.currentState.onScaleEnd(details);
????}
??}

完全无需任何加工,将参数照搬照抄的传入导航条组件。我们就能实现同步缩放拖动的效果!

这里必须特别注意:座位表和导航条组件的单个item的高度必须完全相同,包括margin,padding,不然还是会出现错位现象

至此,最大的难点同步缩放和滑动就解决了。

底部弹框悬浮在座位表上方:

点击座位后弹出底部弹框,遮盖部分座位表,但是座位表能持续向上拖动显示完最后一行的数据

这个乍一看没啥难的,但细细一想也有点复杂。首先, 明确座位表的显示区域是包含底部弹框的,因为底部弹框是悬浮在座位表上面的,那么我们就只能使用margin而不是padding,所以根据设计图底部弹框的height,我们将marginBottom设成这个height就行,但是会有个问题:

当整个座位表放大margin部分也会同步放大,这样就会导致放的越大,座位表距离下面空出的间距就越大。

解决思路:

我们需要拿到当前放大的倍数,动态调整margin, 当前放大X倍,原始margin为Y,则当前放大后的margin=Y/X,Y已知,我们只需要知道X就行。但是在_onInteractionUpdate接口中,X并非当前放大几倍,而是较上次缩放后的缩放倍数。即:

  • 初始1.0倍。
  • 第一次放大至2倍,接口回调的放大倍数为2
  • 第二次放大至3倍,接口回调的放大倍数为1.5(较第一次又放大了1.5倍)。

并且更严重的是当放大到maxScale后,接口仍会持续回调放大倍数。这就很困扰我们,后来阅读源码后发现,我们所要的较原始放大倍数的当前放大倍数参数在InteractiveViewer类中的。

代码语言:javascript
复制
//?Return?a?new?matrix?representing?the?given?matrix?after?applying?the?given
??//?scale.
??Matrix4?_matrixScale(Matrix4?matrix,?double?scale)?{
????if?(scale?==?1.0)?{
??????return?matrix.clone();
????}
????assert(scale?!=?0.0);

????//?Don't?allow?a?scale?that?results?in?an?overall?scale?beyond?min/max
????//?scale.
????final?double?currentScale?=
????????_transformationController.value.getMaxScaleOnAxis();
????final?double?totalScale?=currentScale?*?scale;
????//改了算法
????//?final?double?totalScale?=?math.max(
????//???currentScale?*?scale,
????//???//?Ensure?that?the?scale?cannot?make?the?child?so?big?that?it?can't?fit
????//???//?inside?the?boundaries?(in?either?direction).
????//???math.max(
????//?????_viewport.width?/?_boundaryRect.width,
????//?????_viewport.height?/?_boundaryRect.height,
????//???),
????//?);
????final?double?clampedTotalScale?=?totalScale.clamp(
??????widget.minScale,
??????widget.maxScale,
????);

????widget.scaleCallback?.call(clampedTotalScale);

????final?double?clampedScale?=?clampedTotalScale?/?currentScale;
????return?matrix.clone()..scale(clampedScale);
??}

注意上面的scaleCallback,这是笔者自己实现的回调方法,其中的clampedTotalScale就是我们想要的较初始缩放倍数的当前放大倍数, 即:初始1.0倍,第一次放大至2倍,接口回调的放大倍数为2,第二次放大至3倍,接口回调的放大倍数为3(较初始放大了3倍)。

且clampedTotalScale永远在minScale和maxScale的区间内。拿来即用非常方便。

上面代码中有一段算法被我注释掉了,这段代码的效果是:

InteractiveViewer中的child已经完全显示的时候,则无法再缩小,即minScale不仅仅取决于我们设置的值,还取决于InteractiveViewer的child显示效果,这里我不需要这个限制,则将他注释掉了。

其实如果要完美实现UI给出的效果,有很多地方要用到margin,比如座位表的上下左右margin,只要拿到了上面的clampedTotalScale,均可以动态计算,很方便。

横竖屏适配效果

上面的gif图有横屏效果,横竖屏切换用的也是官方API,OrientationBuilder,这个用起来也很简单。这里讲一个UI适配的注意事项:

由于笔者项目用了ScreenUtil(UI自适应),所以在竖屏的时候,传入竖屏的UI尺寸图,且尺寸结尾使用.w进行适配,当横屏时,传入横屏的UI尺寸图(其实就是将竖屏的width和height倒置),然后尺寸结尾使用.h进行适配。这样就基本能完美适配横竖屏,剩余的细节就可以微调。

初始放大倍数

如上面的效果图, 在第一次进入或横竖屏切换时,当座位表布局过多(默认显示不下时),尽可能缩小以显示更多的内容(下限缩小至minScale),当座位表布局过少(默认显示时屏幕很空),尽可能放大直至显示满屏幕(上限放大至maxScale)。

上面效果可总结为:在尽可能显示完全的前提下尽可能大。

InteractiveViewer并没有初始放大倍数参数,默认进入都是放大1.0倍。这里就需要我们自己来算出这个初始放大倍数。

计算

如果有用screenUtil,以下计算注意区分横竖屏,横屏时适配结尾用.w,竖屏用.h,其中异形屏的padding不用区分横竖屏,系统会自动更改

  • 1、整个座位表的显示区域:

屏幕高-异形屏上下padding-竖屏时底部悬浮框的height(横屏悬浮框如果不在底部,则为0)-标题栏高度以及自己加的一些其他布局的高度。屏幕宽-异形屏左右padding-横屏时右侧悬浮框width(竖屏时悬浮框如不在右侧,则为0)- 导航条宽度(这个导航栏宽度也需要根据放大缩小倍数动态计算)-其他自己加的布局宽。

  • 2、计算初始放大倍数(1.0)下的座位表item的width和height以及padding,这个直接按设计图的就行。
  • 3、获得当前座位表的x轴和y轴。即每行几个座位,一共有几行座位。
  • 4、计算假设要将所有座位表显示下,每个item的width和height。

即用上面1.所得的座位表显示区域的宽高分别除以座位表的x和y,

  • 5、将2.的width除以4.width,即如X轴完全显示下需要缩放的值SX,

将2.的height除以4.height,即如Y轴完全显示下需要缩放的值SY,

  • 6、比较SX和SY两值,取小值defaultS(在尽可能显示完全的前提下尽可能大)
  • 7、如果defaultS在minScale和maxScale区间内,则取defaultS,反之取区间边界值。

缩放

transformationControllerInteractiveViewer缩放到defaultS

代码语言:javascript
复制
//?座位表
mainTransformationController.value?=?Matrix4.identity()..scale(defaultS);
//?导航条
fromTransformationController.value?=?Matrix4.identity()..scale(defaultS);

这里注意座位表和导航条都要进行缩放。

缩放动态margin

最后别忘记将各种需要动态计算的margin也缩放到defaultS值。

如果有横竖屏切换效果的,在每次横竖屏切换的时候都动态计算初始放大值,需要注意,每次计算的时候都要将动态计算的margin置为初始值(即当缩放大小为1.0时的margin值)。

有时候想不出来就看源码,立马就会醍醐灌顶。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 直接上效果图
  • 布局分析
  • 交互分析&实现
    • 导航条跟随座位表放大缩小拖动:
    • 底部弹框悬浮在座位表上方:
    • 横竖屏适配效果
    • 计算
    • 缩放
    • 缩放动态margin
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
    http://www.vxiaotou.com