Flutter Web:根据浏览器窗口改变布局大小

2,424 阅读4分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

前言

之前我们通过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描述不太一致,当窗口缩小的时候,内部组件同步缩小了,当缩小到一定程度后才不会继续缩小,这时候页面才会开始显示部分。虽然与期望效果不一致,不过也无所谓,也算达到了需求要求。