从劝退 flutter_screenutil 聊到不同尺寸 UI 适配的最佳实践
先说优点
💡 先说优点叠个甲,毕竟库本身没有太大问题,往往都是使用的人有问题。
由于是基于设计稿进行屏幕适配的框架,在处理不同尺寸的屏幕时,都可以使用相同的 尺寸数值+单位
,实现对设计稿等比例的适配,同时保真程度一般很高。
在有设计稿的情况下,只使用 Container + GestureDetector 都可以做到快速的开发,可谓是十分的无脑梭哈。
在:只考虑移动端、可以接受使用大屏幕手机看小屏幕 ui、不考虑大字体的模式、被强烈要求还原设计稿、急着开发。的情况下,还是挺好用的。
为什么劝退?
来到我劝退师最喜欢的一个问题,为什么劝退。如果做得不好,瞎搞乱搞,那就是我劝退的对象。
在亲身使用了两个项目并结合群里的各种疑惑,我遇到常见的有如下问题:
如何实现对平板甚至是桌面设备的适配?
由于基于设计稿尺寸,平板、桌面等设备的适配基本上是没法做的,要做也是费力不讨好的事。
千万不要想着说,我通过屏幕宽度断点来使用不同的设计稿,当用户拉动边框来修改页面的宽度时,体验感是很崩溃的。而且三套设计稿要写三遍不同的代码,就更不提了。(这里说三遍代码的原因是,计算 .w
.h
的布局,数据会跟随设计稿变化)
如何适配大字体无障碍?
因为大字体缩放在满屏的 .w
.h
下,也就是写死了尺寸的情况下,字体由于随系统字体放大,布局是绝对会溢出的。很多项目开发到最后上线才意识到自己有大字体无障碍的用户,甚至某些博客上,使用了一句:
MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
来处理掉自己的用户,强制所有屏幕字体不可缩放。一时的勉强敷衍过去,最后只能等项目慢慢腐烂。
为什么在 1.w 的情况下会很糊?同样是 16.sp 为什么肉眼可见的不一样大?
库的原理很简单,提供了一堆的 api 相对于设计图的宽高去做等比例计算,所以必然存在一个问题,计算结果是浮点数。可是?浮点数有什么问题吗?
梳理一下原理:已知屏幕设计图宽度 sdw
、组件设计图宽度 dw
,根据屏幕实际宽度 sw
,去计算得出组件实际宽度 w
。
w = sw / sdw * dw
可是设计图的屏幕宽度 sdw
作为分母时,并不能保证总是可以被表示为有限小数。举个例子:库的文档中给的示例是 const Size(360, 690),
的尺寸,如果我需要一个 100.w
会得到多少?在屏幕宽度为 420 的情况下,得到组件宽度应该为 116.6666... 的无限小数。
这会导致最终在栅格化时会面临消除小数点像素的锯齿问题。一旦有像素点的偏差,就会导致边缘模糊。
字体对尺寸大小更为敏感,一些非矢量的字体甚至只有几个档位的大小,当使用 14.5、15、15.5 的字体大小时,可能会得到一样的视觉大小,再加上 .sp 去计算一道,误差更是放大。
具体是否会发生在栅格化阶段,哪怕文章有误也无所谓,小数点像素在物理意义上就是不存在的,总是会面临锯齿平滑的处理,导致无法像素级还原 UI。
为什么部分屏幕下会溢出?
我们知道了有小数点问题,那么不得不说起计算机编程常见的一个不等式:
0.1 + 0.2 != 0.3
由于底层表示浮点数本身就有的精度问题,现在让 Flutter 去做这个加法,一样会溢出。考虑以下代码:
Row(
children: [
SizedBox(width: 60.w),
SizedBox(width: 100.w),
SizedBox(width: 200.w),
],
);
在一个总共宽度 360.w 的设计图上,可能出现了溢出,如果不去使用多个屏幕来调试,根本不会觉得异常,毕竟设计图是这样做的,我也是这样写的,怎么可能有错呢?
然而恰恰是库本身的小数问题,加上编程届常见的底层浮点数精度问题,导致边缘溢出一点点像素。
我使用了 screenutil 为什么和真实的单位 1px 1rem 1dp 的大小不同呢?
哪怕是 .sp
都是基于设计图等比例缩放的,使用 screenutil 就从来不存在真实大小,计算的结果都是基于设计稿的相对大小。就连 .w
和 .h
都没法保证比例相同,导致所有布局优先使用 .w
来编写代码的库,还想保证和真实尺寸相等?
为什么需要响应式 UI?
说个题外话:在面试淘菜菜的时候真的会有点崩不住,他们问如何做好不同屏幕的适配,我说首先这是 UI 出图的问题,如果 UI 出的图是响应式的,那没问题,照着写,闭着眼都能适配。
但是如果设计图不是响应式的,使用 flutter_screenutil 可以做到和设计图高保真等比还原,但是如果做多平台就需要 UI 根据屏幕断点出不同平台的设计图。
面试官立即就打断我说他们的 UI 只会出一份图。我当场就沉默了,然后呢?也不说话了?是因为只有移动端用户,或者说贵公司 UI 太菜了,还是说都太菜了。菜就给我往下学 ⏬
首先 UI 的响应式设计是 UI 的责任
抛开国情不谈,因为国内的 UI 能做到设计的同时,UI 还是响应式的,这样的 UI 设计师很少很少,他们能把主题规范好,约定好,已经是不得了的了。
但即使如此,响应式 UI 设计也还是应该归于 UI 设计中,在设计图中去根据不同的尺寸,拖动验证不同的布局效果是很容易的。在不同的尺寸下,应该怎么调整元素个数,应该如何去布局元素,只有 UI 使用响应式的写法去实现了,UI 和开发之间的无效交流才会减少。
响应式的 UI 可以避免精度问题
早在 19 年我就有幸翻阅了一本 iOS 的 UI 设计规范,当时有个特别的点特别印象深刻:尺寸大小应该为 2 的整数次幂,或者 4 的倍数。因为这样做,在显示和计算上会较为友好。
💡 这其实是有点历史原因的,之前的 UI 在栅格化上做得并不是很好,锯齿化严重也是常态,所以使用可以被 2 整除的尺寸,一方面使用起来只有几个档位,方便调整;另一方面这样的尺寸可以在像素的栅格化上把小数除尽。
举个例子,在屏幕中间显示一个 300 宽度的卡片,和边距 16 的卡片,哪一个更响应式,无疑是后者,前者由于需要计算 300 相对与设计稿屏幕的宽度,后者只需要准确的执行 16 的边距就好,中间的卡片宽度随屏幕的宽度自动变化。
同样的例子,带有 Expanded 布局的 Row 组件,相比直接给定每个子组件尺寸导致精度问题的布局,更能适配不同���屏幕。因为 Row 会先放置固定大小的组件,剩余空间由 Expanded 去计算好传给子组件,原理和 Web 开发中的 flex 布局一样。
响应式布局是通用的规范
如果有 Web 开发经验的,应该会知道 Web 的屏幕是最多变的,但是设计起来也可以很规范,常见的 bootstrap 框架就提到了断点这个观点,指出了当我们去做 UI 适配的时候,需要根据不同的屏幕大小去做适配。同时 flex 布局也是 Web 布局中常用的响应式布局手段。
在设计工具中,响应式 UI 也没有那么遥远,去下载一份 Material Design 的 demo,对里面的组件自由的拉伸缩放,再对比一下自己通过输入尺寸大小拼凑在一起的 UI,找找参数里面哪里有差异。
怎么做响应式 UI
这里直接放一个谷歌大会的演讲,我相信下面的总结其实都可以不用看了,毕竟本实验室没有什么可补充的,但是我们还是通过从外到内、从整体到局部的顺序来梳理一下如何去做一个响应式的 UI,从而彻底告别使用 flutter_screenutil。
SafeArea
一个简单的组件,可以确保内部的 UI 不会因为愚蠢的设备圆角、前置挖孔摄像头、折叠屏链接脚、全面屏边框等原因而被意外的裁剪,将重要的内容,显示在“安全区”中。
屏幕断点
让 UI 根据不同的尺寸的窗口变化而变化,首先就要使用 MediaQuery.sizeOf(context);
和 LayoutBuilder()
来实现对窗口的宽度的获取,然后通过不同的屏幕断点,去构建不同情况下的 UI。
其中 LayoutBuilder
还能获取当前约束下的宽度,以实现页面中子区域的布局,比如 Drawer
的宽度,对话框的宽度,导航的宽度。
这里举了个例子,使用媒体查询获得窗口宽度之后,展示不同的 Dialog
:
写出如此优雅的断点代码只需要三步:
- 抽象:找到全屏对话框和普通对话框中共同的属性,并将功能页面提取出来。
- 测量:思考应该使用窗口级别的宽度(MediaQuery),还是某个约束下的宽度(LayoutBuilder)。
- 分支:编写如上图所示的带有断点逻辑的代码。
GridView
熟悉了移动端的 ListView 布局之后,切换到 GridView 布局并适配到平板、桌面端,是一件十分自然的事,只需要根据情况使用不同的 gridDelegate
属性来设置布局方式,就能简单的适配。
这里一般使用 SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: )
方法来适配,传入一个期望的最大宽度,使其在任何屏幕上看到的子组件都自然清晰,GridView 会根据宽度计算出合适的一行里合适的列数。
Flex 布局,但是 Flutter 版
前面说过了尽量不要去写固定尺寸的几个元素加起来等于屏幕宽度,没有那么巧合的事情。在 Row/Column 中,善用 Expanded 去展开子组件占用剩余空间,善用 Flexible 去缩紧子组件,最后善用 Spacer 去占用空白,结合 MainAxisAlignment 的属性,你会发现布局是那样的自然。
只有部分组件是固定尺寸的
例如 Icon 一般默认 24,AppBar 和 BottomNavigationBar 高度为 56,这些是写在 MD 设计中的固定尺寸,但是一般不去修改。图片或许是固定尺寸的,但是一般也使用 AspectRatio 来固定宽高比。
我曾经也说过一个普遍的公理,因为有太多初学者容易因为这个问题而出错了。
当你去动态计算宽高的时候,可能是布局思路有问题了。
在大多数情况下,你的布局都不应该计算宽高,交给响应式布局,让组件通过自己的能力去得出自己的位置、约束、尺寸。
举一个遇到过的群友问题,他使用了 stack 布局包裹了应用栏和一个滚动布局,由于SliverAppBar 拉伸后的高度会变化,他想去动态的计算下方的滚动布局的组件起始位置。这个问题就连描述出来都是不可思议的,然后他问我,我应该如何去获取这个 AppBar 的高度,因为我想计算下方组件的高度。(原问题记不清了,但是这样的需求是不成立的)
最后,多看文档
最后补上关于 MD3 设计中,关于布局的文档,仔细学习:
最后的最后,响应式布局其实是一个很宽的话题,这里没法三言两语说完,只能先暂时在某些领域劝退使用这个库。任何觉得可能布局困难的需求,都可以发到评论区讨论,下一篇文章我们将根据几个案例来谈谈具体的实践。
转载自:https://juejin.cn/post/7386947074640298038