一个全新 Flutter UI 适配方案,低入侵& 100% 还原设计稿!
Posted 刘望舒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个全新 Flutter UI 适配方案,低入侵& 100% 还原设计稿!相关的知识,希望对你有一定的参考价值。
微信改了推动机制,真爱请星标本公号 公众号回复 加入BATcoder技术群 BAT
一、序
今天介绍一个我最近开源的 Flutter UI 方案,可以做到在全设备上 100% 还原设计稿,其背后的思想类似 android 侧今日头条基于 density 的 UI 适配方案,接下来进入正题。
由于 Android 的碎片化,全设备的 UI 一致性,一直是开发者和设计师所追求的。后续也衍生出各种不同的适配方案以及对应的开源库,例如:AndroidAutoLayout、smallestWidth,以及最后今日头条基于修改 density 的 UI 适配方案。到后期对于 Android 的 UI 适配,基本上不存在什么新的问题了,只有方案的选择和使用问题。
而在 Flutter 中,由于要支持 Android 和 ios 多系统的设备,UI 适配遇到的场景将更加复杂,这一切又要重新来一遍。对于各个系统的屏幕适配原理都差不多,Flutter 也是如此,我们在 Flutter 中直接写在 Widget 的尺寸,会被乘以一个倍数去放大,最终对应到的是物理设备屏幕的像素尺寸。
这个倍数,在 Flutter 中就是 devicePixelRatio
。它是设备上一个固定的值,不同设备的取值会不相同。
主流的 Flutter UI 适配方案,就是使用 flutter_screenutils
这个 Package,它也是基于 devicePixelRatio 对设置给 Widget 的尺寸进行调整,但入侵还是比较高的,需要遵循它的一些约束,如果想在成熟的项目上迁移使用,改动不会小。并且也有一些小问题,例如:无法适应 const Widget 的优化()。
那么针对我的一些需求,我开源了一个 Flutter package:screen_autosize
,对于成熟的项目也可以做到低入侵的引入。
既然是 UI 适配,直接上效果说话。
对于一套相同的代码,使用 1080 宽度的设备,但从左到右的 dpi 分别是 375 → 392 → 440,运行后的效果如下。
引入 screen_autosize 后,运行效果如下。
该有的元素都有,可以看到在不同的参数设备下,可以做到 100% 的一致性。
二、screen_autosize
接下来我们正式介绍 screen_autosize。
2.1 优势 & 劣势
优势:
-
适配效果:100% 还原 UI; -
稳定性高:原理简单,无任何 Framework 的 Hook; -
侵入性低:修改点少且统一,日常编写 UI 无需特殊注意; -
使用成本低:统一修改,全局有效; -
性能损耗:几乎没有; -
不破坏 const Widget 常量优化 ( );
劣势:
-
全局有效,无法单页面设置,需统一同项目不同模块的设计和开发标准; -
编码注意点:不能使用 window 类 Api 获取尺寸信息; -
需要重写 WidgetsFlutterBinding,可能与部分库存在冲突,需特殊处理;
2.2 使用方法
1. 安装依赖
安装之前,请查看最新版本(新版本如有问题,请使用上一版本)。
dependencies:
flutter:
sdk: flutter
# 添加依赖
screen_autosize: ^{latest version}
2. 使用导包
import 'package:screen_autosize/screen_autosize.dart';
3. 初始化
Step1:设定基准屏幕宽度。
需要在 runApp()
之前设置,使用 AutoSizeUtils 的 initConfig() 设置设计稿的基准宽度。
void main() {
// 这里使用 iPhone 一倍的宽度作为基准宽度;
AutoSizeUtils.instance.initConfig(375);
// ...
}
Step2:替换 runApp()。
将原本的 runApp()
用 runAutoSizeApp(MyApp())
替换。
void main() {
// 这里使用 iPhone 一倍的宽度作为基准宽度;
AutoSizeUtils.instance.initConfig(375);
// runApp(MyApp());
runAutoSizeApp(MyApp());
}
Step3: 替换 MaterialApp 生成的 MediaQuery
MaterialApp 内部会生成 MediaQuery,需要将其通过 MediaQueryWrapper 替换。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
builder: (context, widget) {
// 替换根的 MediaQuery
return MediaQueryWrapper(builder: (BuildContext context){
return widget;
},);
},
home: HomePage(title: 'Flutter ScreenAutoSize示例'),
);
}
}
// ...
4. 开始写 UI
编码时,无任何注意点,直接按照设计图的尺寸写就行。
eg. 在 375 宽度(iPhone 1 倍尺寸)的设计稿下,一个 100x100 黄色区域,直接写参数就行,无需任何注意点。
class ColorsWidget extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10), // 不需要转换,设计稿是多少,就写多少
width: 100, // 不需要转换,设计稿是多少,就写多少
height: 100, // 不需要转换,设计稿是多少,就写多少
color: Colors.orange,
child: Text(
'我是正方形,边长是100',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
);
}
}
如果要写满屏的尺寸,例如某个 Widget 宽度需要撑满屏幕,可以使用 2 种方法。
-
利用 MediaQuery
获取:MediaQuery.of(context).size.width
; -
利用 AutoSizeUtils
获取:AutoSizeUtils.instance.mediaWidth
;
与宽度(width)对应的还有高度(height)的尺寸。
使用上有 2 点需要注意:
-
不能从 window
获取屏幕尺寸。因为库里改写了devicePixelRatio
,所以不能直接从ui.window
里获取对应参数; -
如果有多个 MaterialApp
,也需要同步使用MediaQueryWrapper
包装。MaterialApp
内部会从 window 中获取参数生成 MediaQuery,所以需要特殊处理;
除了这 2 点,其他暂时未发现其他需要特殊注意的。
2.3 示例 & 效果
示例代码:
void main() {
AutoSizeUtils.instance.initConfig(baseWidth: 375);
runAutoSizeApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
builder: (context, widget) {
return MediaQueryWrapper(builder: (BuildContext context){
return widget;
},);
},
// ...
home: HomePage(title: 'Flutter ScreenAutoSize示例'),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
printScreenInformation();
var sysMediaData = MediaQueryData.fromWindow(ui.window);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.only(left: 10),
width: MediaQuery.of(context).size.width,
height: AutoSizeUtils.instance.statusBarHeight,
color: Colors.orange,
alignment: Alignment.centerLeft,
child: Text(
'我和 StatusBar 的高度一致,高度是:${AutoSizeUtils.instance.statusBarHeight} dp',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
Row(
children: <Widget>[
Container(
padding: EdgeInsets.all(10),
width: 180,
height: 80,
color: Colors.red,
child: Text(
'实际宽度:${180}dp \n'
'实际高度:${80}dp',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
Container(
padding: EdgeInsets.all(10),
width: 180,
height: 80,
color: Colors.blue,
child: Text(
'设计稿宽度: 180dp \n'
'设计稿高度: 80dp',
style: TextStyle(
color: Colors.white,
fontSize: 12)),
),
],
),
Container(
padding: EdgeInsets.all(10),
width: 375,
height: 80,
color: Colors.blueGrey,
child: Text(
'设计稿宽度: 375dp \n'
'设计稿高度: 80dp',
style: TextStyle(
color: Colors.white,
fontSize: 12)),
),
Container(
padding: EdgeInsets.all(10),
width: MediaQuery.of(context).size.width,
height: 80,
color: Colors.green,
child: Text(
'设计稿要求宽度撑满屏幕,使用 MediaQuery.of().size.width 设置 \n'
'设计稿高度: 80dp',
style: TextStyle(
color: Colors.white,
fontSize: 12)),
),
Container(
padding: EdgeInsets.all(10),
width: 100,
height: 100,
color: Colors.orange,
child: Text(
'我是正方形,边长是100',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
Text('设备原始像素尺寸: ${ui.window.physicalSize.width} x ${ui.window.physicalSize.height} (px)'),
Text('设备原始 density: ${sysMediaData.devicePixelRatio}dp'),
Text('设备原始宽度: ${sysMediaData.size.width} dp'),
Text('设备原始高度: ${sysMediaData.size.height} dp'),
Text("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="),
Text('调整后的的像素密度: ${AutoSizeUtils.instance.devicePixelRatio}'),
Text('调整后的宽度: ${AutoSizeUtils.instance.mediaWidth} dp'),
Text('调整后的高度: ${AutoSizeUtils.instance.mediaHeight} dp'),
Text('状态栏高度: ${AutoSizeUtils.instance.statusBarHeight} dp'),
Text('底部安全区距离: ${AutoSizeUtils.instance.bottomBarHeight} dp'),
Visibility(
visible: AutoSizeUtils.instance.bottomBarHeight > 0,
child: Container(
padding: EdgeInsets.only(left: 10),
width: MediaQuery.of(context).size.width,
height: AutoSizeUtils.instance.bottomBarHeight,
color: Colors.orange,
alignment: Alignment.centerLeft,
child: Text(
'我和 BottomBar 的高度一致,高度是:${AutoSizeUtils.instance.bottomBarHeight} dp',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
),
],
),
),
);
}
}
3.2 适配效果
未适配前:从左到右的 dpi 分别是 375 → 392 → 440。
使用 screen_autosize 适配后:从左到右的 dpi 分别是 375 → 392 → 440。
三、小结
今天的分享就到这里。
对于 screen_autosize 来说,收敛了配置,只需要在固定位置进行简单的配置,之后在项目中写 Flutter 代码,就无需额外的任何代码,做到低入侵,对一些已经开发成熟,比较友好。
这次 Package 开源,有一些细节还没准备好,例如还不支持 2.0 的空安全,后面有需要会继续迭代。不过基本功能都包含了,原则上可以直接使用。
设计思想和原理讲解的文章,后续会补上,涉及的代码比较简单,有兴趣也可以直接看源码。
要是有任何不对的地方,各位老铁轻喷。有任何问题欢迎在留言区讨论,或提 issues。
文末「阅读原文」可直接跳转到 pub.dev (screen_autosize)。
推荐阅读
•
•
•
•
BATcoder技术群,让一部分人先进大厂
大家好,我是刘望舒,腾讯TVP,著有三本技术畅销书,连续四年蝉联电子工业出版社年度优秀作者,谷歌开发者社区特邀讲师。
前华为技术专家,现大厂技术负责人。
想要加入 BATcoder技术群,公号回复BAT
即可。
为了防止失联,欢迎关注我的小号
以上是关于一个全新 Flutter UI 适配方案,低入侵& 100% 还原设计稿!的主要内容,如果未能解决你的问题,请参考以下文章
Flutter MediaQuery获取屏幕信息以及屏幕适配