介绍
上周接到一个电影院模拟的请求电影院选座系统代码,搜了百度、Google、stackoverflow,没找到用flutter能实现的效果,只好自己写一个了。本文只讲思路,具体实现还是要靠读者自己去实现。只要理解了下面的思路,实现起来就很简单了。
直接上效果图
竖屏:
横屏:
初始化自适应屏幕的缩放效果:
布局分析
中间的位子=>矩阵,这是通过Column嵌套Row来实现的,无法通过GridView来实现(滑动冲突,下面会讲解)
左侧导航栏 => 一个简单的Column(不能使用ListView,同样会造成滑动冲突)
交互分析与实施
放大缩小拖拽效果:
对于放大缩小和拖动的效果,Flutter 现在有自己的组件 InteractiveViewer
该组件可以完美实现放大缩小的效果,组件属性这里就不解释了,比较简单,可以点击上面的链接详细了解。
以下是两个关键属性:
1. 回调事件
2. 转换控制器
该类可用于通过代码控制缩放效果
导航栏随着座位表的拖动而放大和缩小:
左侧导航栏跟随中间座位缩放,且排号定位不发生偏差:
上面说的这些事情,一般都是很容易想到的,也很容易实现的,这个交互效果真正的难点在于后续的滑动效果。
由于左侧导航栏固定在最左边,而座位图可以全屏拖动,所以座位图和导航栏不能放在一个缩放组件里,否则当座位图放大的时候,导航栏会缩小到屏幕外。所以我们的想法是将导航栏和座位图都作为 Stack 的子组件,然后座位图就可以实现缩放效果,导航栏可以随着座位图放大缩小。这里笔者尝试了很多方法:
方法 1:
左侧导航栏和中间座位表均使用 InteractiveViewer
然后使用InteractiveViewer的回调事件和转换控制器来同步效果。
结果:
失败了。transformationController的原理是Matrix4泛型类型的ValueNotifier(四维矩阵)。可以实现简单的移动和缩放,但是缩放和拖动效果我不能完全克隆出来。。如果你对线性代数很在行,可以试试。
方法 2:
Flutter 有一个同步滚动组件,名为 linked_scroll_controller
它可以将两个scrollController绑定在一起,实现同步滚动。
所以让左侧导航栏使用ListView,中间的座位表使用InteractiveViewer嵌套GridView,然后将ListView和GridView的ScrollController绑定在一起,实现同步滚动。
结果:
失败,InteractiveViewer的滑动是通过Matrix4实现的,和ListView的滑动冲突。
实现了同步滚动,但是无法进行拖动放大、缩小。
方法 3:
你不能避免使用 InteractiveViewer,否则自己实现缩放效果太麻烦了。如果你能将 InteractiveViewer 的缩放效果复制到另一个 InteractiveViewer 上,就像上面的 linked_scroll_controller 一样,那就完美了。
这是方法1的思路,但是无法使用InteractiveViewer开放的界面和控制器来完成,这时候就需要去阅读和了解InteractiveViewer的源码,看看是否有启发。
@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的手势交互方法其实就是onScaleEnd、onScaleStart、onScaleUpdate这三个方法。
那么我们只需要将座位图组件回调的三个方法的参数传入导航栏组件即可,然后删除导航栏组件的GestureDetector,这样导航栏组件只接受座位图组件发来的手势交互参数即可。
我们只需要重写两个InteractiveViewer,一个作为主组件(座位图),一个作为从组件(导航栏),并开启InteractiveViewerState,当座位图组件回调这三个手势方法时,我们通过key将这三个方法的参数传递给导航栏组件即可。
_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。所以根据设计中底部弹出框的高度,我们可以将 marginBottom 设置为这个高度,但是会有一个问题:
当整个座位表被放大时,边缘部分也会同步被放大,这样就导致座位表与底部之间的空间变得更大。
解决方案:
我们需要获取当前的放大倍数,并动态调整边距。假设当前放大倍数为X倍,原始边距为Y,则当前放大边距=Y/X。Y是已知的,所以我们只需要知道X即可。但在_onInteractionUpdate接口中,X并不是当前的放大倍数,而是上次缩放后的放大倍数。即:
更严重的是当放大到maxScale的时候,接口还会不断回调放大倍数,这个对我们来说很麻烦,后来看了源码才发现,我们想要的当前放大倍数参数就在InteractiveViewer类中。
// 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,比如座位表的上下左右边距,只要拿到上面的clampedTotalScale就可以动态计算出来,非常方便。
横竖屏适配效果
上面gif是横屏效果,横竖屏切换也是用官方的API OrientationBuilder,使用起来也很简单,下面说一下UI适配需要注意的几点:
由于笔者的项目使用的是ScreenUtil(UI自适应),所以在竖屏时,传入竖屏的UI尺寸图,size以.w结尾进行适配。在横屏时,传入横屏的UI尺寸图(其实就是把竖屏的宽高倒过来),size以.h结尾进行适配。这样横竖屏就能完美适配了,其余细节可以再进行微调。
初始放大倍数
如上效果图,首次进入或者横竖屏切换时,如果座位图布局过大(默认显示不出来),会尽量缩小以显示更多内容(下限缩小到 minScale);如果座位图布局过小(默认显示出来屏幕很空),会尽量放大,直至占满屏幕(上限放大到 maxScale)。
上述效果可以概括为:尽可能的大,同时尽可能的完整地显示出来。
InteractiveViewer没有初始放大倍数参数,默认放大倍数是1.0,这里我们需要自己计算一下这个初始放大倍数。
计算
如果使用screenUtil,后面计算时注意区分横竖屏,横屏适配结尾使用.w,竖屏适配结尾使用.h。异形屏的padding不需要区分横竖屏,系统会自动更改。
屏幕高度-异形屏padding-竖屏下底部浮动框高度(横屏下浮动框若不在底部则为0)-标题栏高度和您添加的一些其他布局高度。屏幕宽度-异形屏padding-横屏下右侧浮动框宽度(竖屏下浮动框若不在右侧则为0)-导航栏宽度(导航栏宽度也需要根据放大缩小倍数动态计算)-您添加的其他布局宽度。
即用上面步骤1得到的座位图显示区域的宽度和高度除以座位图的x和y,
将2.的高度除以4.的高度,即Y轴全显示时需要缩放的SY值。
飞涨
使用 transformationController 将 InteractiveViewer 缩放到 defaultS
// 座位表
mainTransformationController.value = Matrix4.identity()..scale(defaultS);
// 导航条
fromTransformationController.value = Matrix4.identity()..scale(defaultS);
这里请注意,座位表和导航栏都需要缩放。
缩放动态边距
最后,不要忘记将所有需要动态计算的边距缩放到 defaultS 值。
如果有横竖屏切换特效,每次横竖屏切换时都会动态计算初始放大倍数值,需要注意的是,每次计算时都要将动态计算的margin设置为初始值(也就是缩放尺寸为1.0时的margin值)。
结论
到现在为止所有的效果已经实现了,剩下的相信大家都可以自己处理了,很简单。
由于此组件的灵活性以及公司项目保密性,所以就不上传代码了,如有疑问请留言。
有时候当你不明白某件事时,只要看一下源代码,你就会立即得到一些启发。
---END---
西哥好友位开放,还没有加西哥好友的,可以扫下面二维码加个好友,有职场、技术相关问题,随时咨询
暂无评论内容