Article
工作中的数学:不经意间的学有所用,成就感爆棚
大部分的编程工作都是面向对象的、MVC的,调调方法调调接口,或者找找有没有现成的库实现了我们要做的功能,像拼积木。
真正需要拿出笔来进行计算,需要用到数学进行演算的其实很少。就一些比较冷门的、特殊一点的需求,然后还有那么点难度的,才需要自己去实现去笔画笔画思考思考。
记录下实际工作中两个用到数学知识的功能:滚动条上当前页提示 和地图旋转。
# 相似:滚动条上当前页提示
滚动条,我们天天接触的,刷微博看文章都会看到它。
但真正要在滚动条上加点东西,其实也不是一件的容易事情,也没有现成的可以照搬。下面是Google文档的,当鼠标放到滚动条上 时,显示当前视口是第几页 的效果。

滚动条是内容区域的一个缩影,用 滚动条的轨道和滑块 来类比 整个内容和视口区域,实现小小滚动条控制大大的内容的显示。
如果要在滚动条中间显示提示信息,就要找出滑块滚动了多少(thumbTop),以及滑块的长度(thumbHeight)。
转为一个数学问题,用纸笔画一下:

下面在代码中实现,初始化获取 viewportHeight, totalHeight,然后根据推算的公式计算得出滑块当前的偏移量 thumbTop 和高度 thumbHeight :
void updateViewport(double width, double height) { debugPrint("viewportHeight: $height"); if (_viewportHeight == height) return; _viewportHeight = height; SchedulerBinding.instance.addPostFrameCallback((_) { _updateViewportInternal(); }); } void _updateViewportInternal() { if (!_scrollController.position.hasContentDimensions) return; _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; }
注释调试信息,最终效果:

知识、学识,这些在刚开始学习的时刻总觉得既没用又枯燥,但这是一颗种子,种下去了就好好的照看它,长大了会为你遮阳挡雨的。
在我们的程序之路上,除了不懈的努力,有了数学的加持(尽管大部分时间都不怎么用的到),能给我们增添额外的开心和一份持续的坚定。
Related
Related posts
-
有一种自由叫做程序员的自由:把公众号文章镜像回自己的博客
2026-06-02
-
树莓派 OpenClaw Browser 看不见摸不着?给它配个 VNC 图形环境,踏实安心的Debug
2026-03-09
-
从使用者到创造者:用 AI 构建你的专属 VS Code 工具链
2026-02-27
-
杀鸡焉用牛刀:DuckDB 正取代部分 Spark 场景
2026-02-16