Flutter 大小單位詳解

程式設計之路從0到1發表於2020-06-22

關於Flutter 大小所使用的單位,官方文件沒有給出非常明確的解釋,因此一直存在模糊的說法,許多從事安卓開發者直接將之解釋為安卓開發所用的單位dp,我認為這是非常不明智且不準確的說法,這個不準確不在於實質的數值,而在於概念的混淆!這樣極容易對初學者造成誤導,從事web前端或iOS原生開發的人,並沒有dp的概念,當他們學習Flutter時,必須強行去理解dp的概念,且在iOS或web平臺上時也解釋為dp,那就是錯誤的。

應當如何理解Flutter 的大小單位?

官方文件中有對 devicePixelRatio屬性的描述,devicePixelRatio 即每個邏輯畫素的裝置畫素數,其中有一句概括的話

裝置畫素也被稱為物理畫素。邏輯畫素也被稱為與裝置無關或與解析度無關的畫素。

也就是說,物理畫素px = 邏輯畫素 * devicePixelRatio

在另一篇專門寫給Android 開發者的文件中 Flutter for Android developers,有如下說明

Flutter follows a simple density-based format like iOS. Assets might be 1.0x, 2.0x, 3.0x, or any other multiplier. Flutter doesn’t have dps but there are logical pixels, which are basically the same as device-independent pixels.

翻譯過來,就是:Flutter像iOS一樣遵循一個簡單的基於密度的格式。Assets 可能是1.0x,2.0x,3.0x,或者其他任何倍數。Flutter沒有dps,但有邏輯畫素,這與裝置獨立畫素基本相同。

到這裡我們大概能明白Flutter官方的意思,Flutter框架希望提供一個新的尺寸單位的概念,稱為邏輯畫素,然後讓大家忘記原生開發中的單位。這是因為Flutter作為一個跨平臺的框架,必須抽離出一個新的單位,用以適配不同的平臺,如果還去使用原生的單位概念,就會造成混淆或螢幕適配的問題。

結論,在Flutter的語境下,不應該將邏輯畫素直接描述為原生開發中的單位概念

Flutter的邏輯畫素是如何計算出來的?

關於devicePixelRatio屬性的值,一直沒有明確的資料說明,許多人使用比較樸素的辦法,直接列印不同平臺的值用以發現規律。我認為這不是一個好主意,作為一個專業的程式設計師,應該從原始碼中找到答案,實際上devicePixelRatio值的計算很容易找到對應的原始碼

Android 平臺

Flutter 引擎原始碼 shell/platform/android/io/flutter/view/FlutterView.java

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

    Activity activity = getActivity(getContext());
    if (activity == null) {
      throw new IllegalArgumentException("Bad context");
    }

    if (nativeView == null) {
      mNativeView = new FlutterNativeView(activity.getApplicationContext());
    } else {
      mNativeView = nativeView;
    }

    dartExecutor = mNativeView.getDartExecutor();
    flutterRenderer = new FlutterRenderer(mNativeView.getFlutterJNI());
    mIsSoftwareRenderingEnabled = mNativeView.getFlutterJNI().nativeGetIsSoftwareRenderingEnabled();
    mMetrics = new ViewportMetrics();
    // 通過Java程式碼獲取平臺中的density值
    mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density;
    // ...... 省略 ......
}
複製程式碼

獲取到density值後,又通過JNI將值傳給引擎層的C++程式碼 原始碼 shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

// 通過java的jni本地方法傳給C++ 
private native void nativeSetViewportMetrics(
      long nativePlatformViewId,
      float devicePixelRatio,
      int physicalWidth,
      int physicalHeight,
      int physicalPaddingTop,
      int physicalPaddingRight,
      int physicalPaddingBottom,
      int physicalPaddingLeft,
      int physicalViewInsetTop,
      int physicalViewInsetRight,
      int physicalViewInsetBottom,
      int physicalViewInsetLeft,
      int systemGestureInsetTop,
      int systemGestureInsetRight,
      int systemGestureInsetBottom,
      int systemGestureInsetLeft);
複製程式碼

這裡對C++程式碼就不在追蹤,有興趣可以去看engine/shell/platform/android/platform_view_android_jni_impl.cc

在Flutter中,devicePixelRatio屬性由ui.Window類提供,我們知道,這個Window正是Flutter Framework連線宿主作業系統的介面。因此,dart程式碼中獲取的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;
// ....... 省略 .......
}
複製程式碼

可以看到,device_pixel_ratio的值是[UIScreen mainScreen].scale,關於這個值,蘋果的開發者文件有描述 UIScreen 的 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 的解析度

Web 平臺

引擎原始碼 engine/lib/web_ui/lib/src/engine/window.dart


  @override
  double get devicePixelRatio => _debugDevicePixelRatio != null
      ? _debugDevicePixelRatio
      : browserDevicePixelRatio;

  /// Returns device pixel ratio returned by browser.
  static double get browserDevicePixelRatio {
    double ratio = html.window.devicePixelRatio;
    // Guard against WebOS returning 0.
    return (ratio == null || ratio == 0.0) ? 1.0 : ratio;
  }
複製程式碼

可以看到,呼叫的是html.window.devicePixelRatio,這裡的html.window實際上是Dart語言SDK中的類,描述的是瀏覽器中的window。關於瀏覽器中的devicePixelRatio屬性值,可以看Dart 官方文件給出的解釋 devicePixelRatio

  • IE and Firefox don’t support the property at all. I assume the next versions will implement it.
  • Opera desktop on retina devices give 1, while it should be 2. I assume the next Opera version will fix this.
  • Opera Mobile 10 does not support it, but 12 implements the property correctly.
  • UC always gives 1, but UC is quite confused when it comes to viewport properties.
  • Chrome implemented this property only recently on retina devices. Chrome 19 incorrectly returns 1; Chrome 22 correctly returns 2.
  • MeeGo WebKit (Nokia N9/N950) does something horrible: it changes the value from 1 to 1.5 when you apply a meta viewport.

視訊課程

博主釋出的相關視訊課程

Flutter全棧式開發之Dart 程式設計指南

二維碼

Flutter 全棧式開發指南

Flutter 大小單位詳解

公眾號:程式設計之路從0到1

程式設計之路從0到1

相關文章