前言
之前我们通过flutter开发web应用,然后用electron打包成可执行文件在pc端使用,因为electron可以设置最小宽高,所以布局不会越界,但是如果直接在浏览器中打开,因为浏览器的大小无法控制,如果用户缩小浏览器会导致布局越界。根据大部分网站的经验来看,当窗口缩小到一定程度后,布局就不会再改变,反而是增加了滚动,这样就保证了布局的正确性,所以我们也打算这么做。
监听窗口改变
经过测试,当浏览器窗口改变的时候,并不会执行页面的重绘,所以我们需要想办法监听窗口的改变。
这就需要用到window,先引入
import 'dart:html'; 复制代码
然后通过window的onResize来监听,如下:
window.onResize.listen((event) { ... }); 复制代码
这里我们实现了监听,但是如何获取窗口的大小,同样还是用window
window.innerWidth window.innerHeight 复制代码
这样就得到了窗口的大小。
实现滚动布局
滚动布局很简单,通过SingleChildScrollView即可,但是因为可能是两个方向的滚动,所以需要两个SingleChildScrollView嵌套。
Scrollbar( isAlwaysShown: true, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SingleChildScrollView( child: appContent, ), ), ) 复制代码
这里第一个SingleChildScrollView是横向滚动,并且在外层给它添加了一个滚动条(因为windows机器的鼠标可能不支持横向滚动),然后里面在嵌套一个竖向滚动,最里面就是页面布局。
这样有一个问题,因为两层滚动所以整个布局是无限大的,加载真正的页面的时候就会报错:
RenderSemanticsAnnotations object was given an infinite size during layout.
所以我们要为页面大小加上宽高限制,首先想到的就是在Container的constraints中加入minHeight和minWidth,这是我们的目的
Scrollbar( isAlwaysShown: true, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SingleChildScrollView( child: Container( constraints: BoxConstraints( minHeight: 720, minWidth: 1280 ), child: widget.content, ), ), ), ) 复制代码
但是结果还是报错,因为虽然限制了宽高的最小值,最大值依然是无限大。所以还要加上最大值的限制?
我们可以继续在BoxConstraints中加上maxHeight和maxWidth,但是因为两个SingleChildScrollView导致整个空间是无限大的,所以页面会一直保持maxHeight和maxWidth,这样即使我们缩小一点浏览器而空间依然足够的情况下,页面就会需要滚动而不是压缩排列。
最终我们决定,通过计算来获取宽高,整体代码如下:
class AppContent extends StatefulWidget{ Widget content; AppContent(this.content); @override State<StatefulWidget> createState() { return _AppContent(); } } class _AppContent extends State<AppContent>{ @override Widget build(BuildContext context) { return Scrollbar( isAlwaysShown: true, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: SingleChildScrollView( child: Container( width: max(window.innerWidth.toDouble(), 1280), height: max(window.innerHeight.toDouble(), 720), child: widget.content, ), ), ), ); } @override void initState() { window.onResize.listen((event) { setState(() { }); }); } } 复制代码
可以看到,我们通过窗口大小与我们设置的最小尺寸进行比较取较大值,这样就保证了及时窗口很小页面也不会过小而引起越界,而且可以通过滚动来浏览整个页面。
然后我们在App中用AppContent包裹整个app的页面即可。
FittedBox替代
上面有两层滚动布局虽然可以实现我们的效果,但是在浏览器中实际效果不尽人意,因为两个方向上都有滚动,所以在操作时页面很容易移动,而且如果页面上有js拖动效果的(即在HtmlElementView中有一个div通过js实现拖动),在拖动div的同时页面也会跟着动,所以我尝试了更多的widget,最终决定用FittedBox替代,最终代码如下:
class AppContent extends StatefulWidget{ Widget content; AppContent(this.content); @override State<StatefulWidget> createState() { return _AppContent(); } } class _AppContent extends State<AppContent>{ @override Widget build(BuildContext context) { return FittedBox( fit: BoxFit.cover, alignment: Alignment.topLeft, child: Container( width: max(window.innerWidth.toDouble(), 1280), height: max(window.innerHeight.toDouble(), 720), child: widget.content, ), ); } @override void initState() { window.onResize.listen((event) { setState(() { }); }); } } 复制代码
BoxFit.cover是按原尺寸填充,所以child可以超出FittedBox的范围。这样虽然不能滑动(如果窗口过小,即使可以滑动也不符合我们的业务需求,所以我们会建议用户全屏,这个仅仅是为了防止用户缩小窗口导致页面溢出错乱),但是页面不会出现溢出的现象。
替换成FittedBox后,滑动问题解决了。但是在浏览器中测试的实际效果与BoxFit.cover描述不太一致,当窗口缩小的时候,内部组件同步缩小了,当缩小到一定程度后才不会继续缩小,这时候页面才会开始显示部分。虽然与期望效果不一致,不过也无所谓,也算达到了需求要求。