跳到正文
W Winse Blog
dev 3 min read

工作中的数学:不经意间的学有所用,成就感爆棚

大部分的编程工作都是面向对象的、MVC的,调调方法调调接口,或者找找有没有现成的库实现了我们要做的功能,像拼积木。

真正需要拿出笔来进行计算,需要用到数学进行演算的其实很少。就一些比较冷门的、特殊一点的需求,然后还有那么点难度的,才需要自己去实现去笔画笔画思考思考。

记录下实际工作中两个用到数学知识的功能:滚动条上当前页提示地图旋转

# 相似:滚动条上当前页提示

滚动条,我们天天接触的,刷微博看文章都会看到它。

但真正要在滚动条上加点东西,其实也不是一件的容易事情,也没有现成的可以照搬。下面是Google文档的,当鼠标放到滚动条上 时,显示当前视口是第几页 的效果。

滚动条是内容区域的一个缩影,用 滚动条的轨道和滑块 来类比 整个内容和视口区域,实现小小滚动条控制大大的内容的显示。

如果要在滚动条中间显示提示信息,就要找出滑块滚动了多少(thumbTop),以及滑块的长度(thumbHeight)。

转为一个数学问题,用纸笔画一下:

下面在代码中实现,初始化获取 viewportHeighttotalHeight,然后根据推算的公式计算得出滑块当前的偏移量 thumbTop 和高度 thumbHeight :

  void updateViewport(double widthdouble height) {
    debugPrint("viewportHeight: $height");
    if (_viewportHeight == heightreturn;
    _viewportHeight = height;
    SchedulerBinding.instance.addPostFrameCallback((_) {
      _updateViewportInternal();
    });
  }
  
  void _updateViewportInternal() {
    if (!_scrollController.position.hasContentDimensionsreturn;
    _totalHeight = _scrollController.position.maxScrollExtent + _scrollController.position.viewportDimension;
    debugPrint('contentHeight: $_totalHeight');
    _isShowScrollbar = _totalHeight > _viewportHeight;
    _thumbHeight = (_viewportHeight / _totalHeight) * _viewportHeight;
    _thumbTop = (_scrollTop / _totalHeight) * _viewportHeight;
    setState(() {});
  }

当页面滚动时,重新计算滑块的偏移量thumbTop,并更新提示信息。

  _scrollController.addListener(() {
    setState(() {
      _scrollTop = _scrollController.position.pixels;
      _thumbTop = (_scrollTop / _totalHeight) * _viewportHeight;
      final page = ((_scrollTop + EditorPage.A4HeightInDp / 4) / (EditorPage.gap + EditorPage.A4HeightInDp)).toInt() + 1;
      if (currentPage != page) {
        _isAutoShown = true;
      }
      currentPage = page;
      _updateTooltip();
      _debouncer.value++;
    });
  });

实现的效果如下,增加了页面跨页时和鼠标放到滚动条上时显示当前页提示信息

# 旋转和投影:地图旋转

日常我们都使用地图进行导航,手机地图会根据我们走的路线旋转,屏幕保持我们向前走的方向。大厂提供的SDK都带了这个功能,但如果是自绘的地图引擎,那仅仅转一下角度 是不够的,旋转后四个角会出现空隙

为了让视口填满地图瓦片,那么就需要扩大加载瓦片的边界。沿着视口的四个角,画平行于旋转后的矩形。理论上,根据扩大后的边界来加载瓦片就能完全的填满视口了。

旋转后(参见下图),需要多加载一圈的瓦片新偏移量 也得往左上角移。新的有效范围 就变成了:新偏移量 到 中心点 的距离 加上 扩大后的新矩形一半的宽高(也可以通过 新偏移量 到 新矩形顶点 加上 新矩形的宽高)。

增加旋转逻辑,计算新的偏移量瓦片的索引坐标 和需要加载瓦片的数量

  List<Widget> _buildTiles() {
    List<Widget> tiles = [];
    int zoomInt = _zoom.floor();
    double rootOffsetX = this._rootOffsetX;
    double rootOffsetY = this._rootOffsetY;
    int rootTileX = this._rootTileX;
    int rootTileY = this._rootTileY;
    double w = mapWidth;
    double h = mapHeight;
    int tilesXNum = ((w - rootOffsetX) / 256).ceil();
    int tilesYNum = ((h - rootOffsetY) / 256).ceil();
    print("original tiles: $tilesXNum * $tilesYNum, rootTile: ($rootTileX$rootTileY), rootOffset: ($rootOffsetX$rootOffsetY)");
    if (rotationRad != 0) {
      final cosAngle = math.cos(rotationRad).abs();
      final sinAngle = math.sin(rotationRad).abs();
      final rotatedWidth = (w * cosAngle) + (h * sinAngle);
      final rotatedHeight = (h * cosAngle) + (w * sinAngle);
      // 扩展的尺寸(旋转后的覆盖视口矩形变大)
      finaldouble extraWidth = (rotatedWidth - w) / 2;
      finaldouble extraHeight = (rotatedHeight - h) / 2;
      // 扩展的瓦片数(向上取整)
      finalint extraTilesX = ((extraWidth + rootOffsetX) / _tileSize).ceil();
      finalint extraTilesY = ((extraHeight + rootOffsetY) / _tileSize).ceil();
      rootTileX -= extraTilesX;
      rootTileY -= extraTilesY;
      rootOffsetX -= extraTilesX * _tileSize;
      rootOffsetY -= extraTilesY * _tileSize;
      // 有效宽度的瓦片数
      tilesXNum = ((w / 2 - rootOffsetX + rotatedWidth / 2) / 256).ceil();
      tilesYNum = ((h / 2 - rootOffsetY + rotatedHeight / 2) / 256).ceil();
      print(           "rotated ${((rotationRad * 180 / math.pi) % 360).toStringAsFixed(1)} tiles: $tilesXNum * $tilesYNum, rootTile: ($rootTileX$rootTileY), rootOffset: ($rootOffsetX$rootOffsetY)",         );
    }
    for (int y = 0; y < tilesYNum; y++) {
      for (int x = 0; x < tilesXNum; x++) {
        double offsetX = rootOffsetX + x * _tileSize;
        double offsetY = rootOffsetY + y * _tileSize;
        int tileX = rootTileX + x;
        int tileY = rootTileY + y;
        tiles.add(_buildTile(tileX, tileY, zoomInt, offsetX, offsetY));
      }
    }
    return tiles;
  }

注释调试信息,最终效果:

知识、学识,这些在刚开始学习的时刻总觉得既没用又枯燥,但这是一颗种子,种下去了就好好的照看它,长大了会为你遮阳挡雨的。

在我们的程序之路上,除了不懈的努力,有了数学的加持(尽管大部分时间都不怎么用的到),能给我们增添额外的开心和一份持续的坚定。

在 GitHub 上讨论

欢迎通过 GitHub Issue 留言或反馈。每条讨论都会关联到对应文章的源文件路径。

2025-07-31-工作中的数学:不经意间的学有所用,成就感爆棚.md

Related posts