Flutter UI适配详解 —— Flutter开发必看!

Posted Ever69

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter UI适配详解 —— Flutter开发必看!相关的知识,希望对你有一定的参考价值。

Flutter中的宽高单位


不同于android中的dp和ios中的pt,Flutter奉行另外一种单位,即逻辑像素。

Flutter 遵循简单的基于密度的格式,如 iOS。资产可能是1.0x、 2.0x、3.0x或任何其他乘数。Flutter 没有dp 但有逻辑像素,与设备无关像素基本相同。所谓devicePixelRatio 表示物理像素在单个逻辑像素中的比例。

devicePixelRatio表示1逻辑像素在设备上对应的物理像素数(px),不同设备的devicePixelRatio不尽相同,比如我手上的小米8 SE,其devicePixelRatio值为2.75,即在小米8 SE上,1逻辑像素等于2.75物理像素,而在我手中的另一部华为 Mate9上,其devicePixelRatio值为3,即在华为 Mate9上,1逻辑像素等于3物理像素。

(注:devicePixelRatio值可以通过Flutter中的MediaQuery类查看)

这就有点奇怪了,明明小米8 SE比华为 Mate9的分辨率更高,可是小米8 SE的devicePixelRatio值竟然比华为 Mate9的devicePixelRatio值还低,我们以Android中的dp为例,计算这两款手机dp与px的关系。首先,我们需要先计算这两款手机的DPI,根据DPI的计算公式得:

手机型号DPI
小米8 SE√(2244²+1080²)/5.88 ≈ 423.5
华为 Mate9√(1920²+1080²)/5.9 ≈ 373.4

再根据公式得

手机型号DPPX
小米8 SE1423.5/160 ≈ 2.65
华为 Mate91373.4/160 ≈ 2.33

可以看出,同样是1dp,在小米8 SE上对应的像素数是大于华为 Mate9的。

那么,为什么分辨率高的小米8 SE的devicePixelRatio值要比分辨率低的华为Mate 9还小呢?我们来看看devicePixelRatio是如何计算出来的。

通过源码可以发现

在Android中

源码位置>shell/platform/android/io/flutter/view/FlutterView.java

public FlutterView(Context context, AttributeSet attrs, FlutterNativeView nativeView) {
    super(context, attrs);

    // ...... 省略 ......
    mMetrics = new ViewportMetrics();
    // 通过Java代码获取平台中的density值
    mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density;
    // ...... 省略 ......
}

可以看到,devicePixelRatio在Android平台就是DisplayMetrics类中的density,拿density是啥,density是密度的意思,其实就是1dp对应的px数,这就怪了,这怎么跟我们自己算出来的值不一样呢,干脆把DisplayMetrics打印出来看看。

  • 小米8 SE
DisplayMetrics(density=2.75, width= 1080, height=2029,

scaledDensity=2. 75, xdpi=422.03,ydpi=422.204,densityDpi = 440)

  • 华为 Mate9
DisplayMetrics(density=3.0, width=1080, height=1920,

scaledDensity=3.0, xdpi=375.78, ydpi=375.138, densityDpi=480)

我tm…这dpi的值系统是咋算的,和标准计算公式算出来的不能说一模一样,可以说是毫不沾边啊。

佛了,彻底整蒙圈了,看来这devicePixelRatio是不能直接用了。

在IOS中

源码位置>engine/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

- (void)viewDidLayoutSubviews {
  CGSize viewSize = self.view.bounds.size;
  CGFloat scale = [UIScreen mainScreen].scale;

  // Purposefully place this not visible.
  _scrollView.get().frame = CGRectMake(0.0, 0.0, viewSize.width, 0.0);
  _scrollView.get().contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);

  // First time since creation that the dimensions of its view is known.
  bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
  
  // 在iOS 上,device_pixel_ratio 的值是一个缩放比
  _viewportMetrics.device_pixel_ratio = scale;
  _viewportMetrics.physical_width = viewSize.width * scale;
  _viewportMetrics.physical_height = viewSize.height * scale;
// ....... 省略 .......
}

关于这个scale,苹果的官方文档这么介绍

该值反映了从默认逻辑坐标空间转换到本界面设备坐标空间所需的比例系数。默认的逻辑坐标空间是用点来衡量的。对于Retina显示器,比例因子可能是3.0或2.0,一个点可以分别用9个或4个像素表示。对于标准分辨率显示器,比例系数为1.0,一个点等于一个像素。

简单讲就是

scale == 1 :代表320 x 480 的分辨率(iphone4之前的设备,非Retain屏幕)
scale == 2 :代表640 x 960 的分辨率(Retain屏幕)
scale == 3 :代表1242 x 2208 的分辨率

这个跟IOS中pt的计算方式也不一样吧(我百度的,看到这的IOS的小伙伴,评论里吱一声)。

得,费尽周折,最后发现devicePixelRatio不靠谱,导致Flutter中使用其作为转换的逻辑像素也不靠谱,适配还得另想办法。

flutter_screenUtil


这是我在pub.dev上发现的一个Flutter开源工具,与我设想的适配方案一致,秉着不重复造轮子的原则,就直接用它了,它可以解决Flutter在不同端上的适配问题,保持Flutter在不同设备上的UI一致。

flutter_screenUtil的使用


简单讲一下flutter_scrrenUtil的使用,官方的介绍点这里

只需在main或跟路由(第一个flutter页面)中调用一次Screen.init()即可
init()方法接收三个参数,一个必要参数和两个可选参数

static void init(
    BoxConstraints constraints, {
    Orientation orientation = Orientation.portrait,
    Size designSize = defaultSize,
  }) 
  • BoxConstraints
    运行设备的屏幕宽高
  • Orientation
    屏幕方向(默认竖屏)
  • Size
    UI设计图的宽高尺寸(默认360*690,单位dp)

其中Size中的宽高尺寸可以使用任何单位,dp、pt、px都可以,只要和后面设置使用的单位保持一致就可以。

例如,我这设计图尺寸如下

代码则为

ScreenUtil.init(
      BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width,
          maxHeight: MediaQuery.of(context).size.height),
      designSize: Size(375, 667),
    );

初始化完成后怎么使用呢?
比如,现在我们要实现这个按钮

代码如下

Container(
                  width: 325.w,
                  height: 40.h,
                  alignment: Alignment.center,
                  child:
                      ElevatedButton(onPressed: () {}, child: Text("手机一键登录")),
                )

注意,因为我们在init()的时候,Size使用的单位是dp,所以这里width和height的使用的也是dp单位对应的值,如果这里使用的是px单位对应的值的话,那就错了。

width对应的值后面要加上.w,height对应的值后面要加上.h。

ok,宽高除了.w和.h,其实flutter_screenUtil还提供了另外一种设置方式,有时候,我不关心宽高的具体值,我只需要设置某个控件的宽高为手机屏幕宽高的一半,那么这个时候就可以用.sw和.sh这两个属性。

  • .sw代表屏幕的宽度
  • .sh代表屏幕的高度
Container(
    width: 0.5.sw,
    height: 0.5.sh,
 )

到这里,宽高就适配完成了,那么字体呢?

字体也简单,在字体大小值的后面加.sp就可。

Text(
                        "手机一键登录",
                        style: TextStyle(fontSize: 14.sp),
                      )

处了上面这些,flutter_screenUtil还有一个.r属性。

这个属性干啥用呢,radius?角度?

不是的,它其实也是宽高的单位,只不过在特殊的情况下才会使用,比如我们要一个宽高都为100dp的按钮,这时候我们再设置width、height为100.w和100.h时,运行起来后会得到一个长方形,惊不惊喜意不意外,这种情况,设置width、height为100.r和100.r即可解决。

flutter_screenUtil的原理


flutter_screenUtil的适配方案非常简单粗暴,那就是 — ’比例
init()方法中传入的屏幕宽高和设计图宽高以及保持单位统一就是为了搞定比例这个事儿的。

拿宽度为例,假如我设计图的宽高尺寸为100dp*200dp,其中有一个10dp宽度的按钮,那么它对应在设备中的宽度就是 > 设备宽度 * 10(控件宽度) / 100(设计图宽度),这个很容易理解吧。

原理理解后,我们去看看.w的代码实现

extension SizeExtension on num {
  ///[ScreenUtil.setWidth]
  double get w => ScreenUtil().setWidth(this);

	/.../
	
}

可以看出.w是通过给num增加扩展方法实现的,其调用了ScreenUtil().setWidth(this)方法作为返回值。

double setWidth(num width) => width * scaleWidth;

setWidth()接受一个num作为参数,并将num与scaleWidth的乘积做返回值返回。

/// 实际尺寸与UI设计的比例
/// The ratio of actual width to UI design
double get scaleWidth => _screenWidth / uiSize.width;

scaleWidth即为屏幕宽度与设计图宽度的比例。

所以最后

w = _screenWidth * width / uiSize.width;

w = 设备宽度 * 控件宽度 / 设计图宽度

其他属性,也是如此计算的。

flutter_screenUtil中的问题


当你的App在不考虑横屏切换时,以上方式的UI适配基本不会有问题,但是,一旦有横屏的状态,.sp和.r就萎了。

竖屏

横屏

大家一起来找茬。。
眼尖的可能已经发现,红蓝色块儿中的字体大小和绿色快儿的宽高出现了明显变小。

这是红色块儿中文字的代码,字体大小用了.sp做适配。

Text(
    '我的实际宽度:${180.w}dp \\n'
    '我的实际高度:${200.h}dp',
     style: TextStyle(color: Colors.white, fontSize: 12.sp),
)

这是绿色块儿的代码,宽高用了.r做适配

Container(
    padding: EdgeInsets.all(ScreenUtil().setWidth(10)),
    width: 100.r,
    height: 100.r,
    color: Colors.green,
    child: Text(
        '我是正方形,边长是100',
         style: TextStyle(
         color: Colors.white,
         fontSize: 12,
         ),
    ),
)

直接看.sp和.r的代码,发现这两个方法都使用了scaleText这个字段做乘数。

///根据宽度或高度中的较小值进行适配
  double radius(num r) => r * scaleText;

  ///字体大小适配方法
  ///- [fontSize] UI设计上字体的大小,单位dp.
  double setSp(num fontSize) => fontSize * scaleText;

scaleText怎么来的?它是scaleWidth和scaleHeight其中较小的内个值。

double get scaleWidth => _screenWidth / uiSize.width;

double get scaleHeight => _screenHeight / uiSize.height;

double get scaleText => min(scaleWidth, scaleHeight);

看到这,你明白了吗?为什么横屏比竖屏,发生了文字和宽高的缩小。

因为scaleWidth和scaleHeight是屏幕宽高度和设计图宽高度的比值,横屏状态下手机屏幕的宽高值发生变化,而设计图的宽高值却是写死的,就导致了横屏与竖屏下计算出来的的scaleWidth和scaleHeight值不一样。而scaleWidth和scaleHeight的不一样,又导致了scaleText的不一样,而scaleText的不一样又导致了.sp和.r的不一样,最后,这不一样就体现在了横竖屏下的UI上。

最后


如果你的App不考虑横屏,那么flutter_screenUtil中的属性放心用。

如果你的App要考虑横屏的情况,那么.sp和.r属性慎用,字体大小推荐就按flutter中的逻辑像素来,控件长宽相同的情况下推荐都使用.w或.h。

当然,你也可以对.sp和.r的源码进行修改,换成你想要的算法。

参考文章

[1] Flutter大小单位详解
[2] 支持不同的像素密度
[3] DPI、PPI、DP、PX 的详细计算方法及算法来源是什么?
[4] 手机屏幕的DPI是什么和PPI又有什么区别

以上是关于Flutter UI适配详解 —— Flutter开发必看!的主要内容,如果未能解决你的问题,请参考以下文章

Flutter UI适配详解 —— Flutter开发必看!

flutter 屏幕尺寸适配字体大小适配

一个全新 Flutter UI 适配方案,低入侵& 100% 还原设计稿!

11-Flutter移动电商实战-首页_屏幕适配方案和制作

flutter也能适配了!

Flutter 屏幕适配 -- 百分比