日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

詳解Flutter桌面應用如何進行多分辨率適配_Android

作者:Karl_wei ? 更新時間: 2023-04-26 編程語言

前言

通過此篇文章,你將了解到:

Flutter windows和Android桌面應用屏幕適配的解決方案;

屏幕適配的相關知識和原理;

flutter_screenutil的實現原理和缺陷。

Flutter桌面應用的開發過程中,勢必需要適配不同尺寸的屏幕。我們的預期是在不同尺寸的設備上,用戶的使用感觀基本一致。 如:在個人pc上,應用與屏幕的高度比是2/3;那么到60寸的大設備上,應用的尺寸依然需要2/3比例。

屏幕適配的一些基礎概念

  • 屏幕尺寸:屏幕的實際大小,主要看屏幕的對角線的長度,如:6.6英寸。
  • 分辨率:屏幕上像素點的總和,如:2400×1176。設備的屏幕其實是由N個像素格子組合成的,屏幕上顯示的所有元素(圖片、文字)從微觀上都是為特定的像素格子繪制上內容。
  • 屏幕密度(dpi):每英寸的像素格子數。每英寸展示160個像素時稱為一倍素;120個稱為低倍素...

假設我們需要展示一張800×800的圖片,那么在160dpi的手機屏幕上,我們只要設置800×800px的寬高;

但在320dpi的屏幕上,由于每英寸的像素點翻倍了,為了用戶的視覺感受一致,就需要將圖片設置的寬高設為1600×1600px。這就是屏幕適配最基本的原理,我們開發中所用到的適配庫,最基礎的能力就是提供這層轉換。

Flutter 移動端開發的通用做法

Flutter移動端的生態已經很完備,我們一般在開發過程中會使用flutter_screenutil這個插件。這是一個純Dart的pub,閱讀源碼發現其做法也很簡單粗暴。

  • 根據傳入的設計稿尺寸,與設備的邏輯像素尺寸的比值作為縮放倍數;
  • 開發者設置的尺寸都會去乘以對應的縮放倍數,從而實現widget大小的轉換。
extension SizeExtension on num {
  ///[ScreenUtil.setWidth]
  double get w => ScreenUtil().setWidth(this);
  ///[ScreenUtil.setHeight]
  double get h => ScreenUtil().setHeight(this);
  ......
)
double get screenHeight =>
    _context != null ? MediaQuery.of(_context!).size.height : _screenHeight;
double setHeight(num height) => height * scaleHeight;
// 高度的縮放比:設備的邏輯像素的高度/設計稿的高度
double get scaleHeight =>
    (_splitScreenMode ? max(screenHeight, 700) : screenHeight) /
    _uiSize.height;

邏輯像素screenHeight從哪來?

獲取MediaQueryData的size,即應用窗口的分辨率。

extension on MediaQueryData? {
  MediaQueryData? nonEmptySizeOrNull() {
    if (this?.size.isEmpty ?? true)
      return null;
    else
      return this;
  }
}
/// The size of the media in logical pixels (e.g, the size of the screen).
///
/// Logical pixels are roughly the same visual size across devices. Physical
/// pixels are the size of the actual hardware pixels on the device. The
/// number of physical pixels per logical pixel is described by the
/// [devicePixelRatio].
final Size size;

存在的問題

flutter_screenutil 這個庫在移動端使用是完全沒有問題的。手機尺寸雖說層出不窮,但是也遵循瘦長的長方形規則,最重要的是應用默認都是全屏的,那么上面第3點獲取到的應用窗口高度screenHeight和設備的大小剛好是完全吻合的。從而計算出的縮放比(設計稿尺寸/設備的尺寸 = 縮放比值)是偏差不大的。
我們在Android的桌面應用中,這個庫也可以支持各種設備。

然而在windows等桌面端卻沒那么簡單:

  • 首先桌面設備的尺寸層出不窮,從個人筆記本到演示廳的屏幕,物理大小就已經差了幾十倍,而像素密度卻差別不大,這在適配上本身就存在更大難度。
  • 且通過驗證,FlutterMediaQueryData獲取的是應用窗口的大小,但是桌面設備屏幕大小跟應用窗口大小可不是一樣大的,這就是最大的問題所在!

通過實踐我們也驗證了flutter_screenutil在桌面端的適配基本不起作用,且還會造成不少問題,比如:第一次運行字體都會偏大;當有多個擴展屏的時候,主副屏切換有bug。

桌面端解決方案

一、需求分析

我們希望flutter開發出來的應用,在不同的設備中:

  • 應用的大小占比屏幕物理尺寸的比例是一致的;
  • 系統顯示設置中的的縮放倍數不會影響應用的大小;
  • 資源大小可以進行適配,讓圖片等資源在不同尺寸的設備上都能顯示清晰。

分析以上預期效果,可以提煉出一個原則:應用的尺寸必須跟屏幕的物理大小占比一致,與分辨率、像素密度、縮放比都沒關系。

二、實現原理

由于Android端用了flutter_screenutil這個庫,Flutter又是跨平臺的。為了降低開發成本,我試著fork源碼下來更改,并且做了以下的操作:

  • 在構造函數上,我加了一個參數app2screenWithWidth,讓用戶告知應用窗口寬度與屏幕寬度的比值,如:70%傳入0.7;
// 文件路徑:flutter_screenutil/lib/src/screenutil_init.dart
class ScreenUtilInit extends StatefulWidget {
  /// A helper widget that initializes [ScreenUtil]
  const ScreenUtilInit({
    Key? key,
    required this.builder,
    this.child,
    this.rebuildFactor = RebuildFactors.size,
    this.designSize = ScreenUtil.defaultSize,
    this.app2screenWithWidth = 1,
    this.splitScreenMode = false,
    this.minTextAdapt = false,
    this.useInheritedMediaQuery = false,
  }) : super(key: key);
  final ScreenUtilInitBuilder builder;
  final Widget? child;
  final bool splitScreenMode;
  final bool minTextAdapt;
  final bool useInheritedMediaQuery;
  final RebuildFactor rebuildFactor;
  /// The [Size] of the device in the design draft, in dp
  final Size designSize;
  /// 適用于桌面應用,應用窗口寬度與設備屏幕寬度的比例
  final double app2screenWithWidth;
  @override
  State<ScreenUtilInit> createState() => _ScreenUtilInitState();
}
  • yaml中引入 screenRetriever,獲取到真實的設備屏幕像素,這個是真實的屏幕像素,跟應用窗口沒關系;然后可以計算出應用窗口的大小,得出轉換系數;
dependencies:
  flutter:
    sdk: flutter
  # 獲取屏幕物理參數
  screen_retriever: ^0.1.2
// 文件路徑:flutter_screenutil/lib/src/screen_util.dart
/// Initializing the library.
static Future<void> init(
  BuildContext context, {
  Size designSize = defaultSize,
  double app2screenWithWidth = 1,
  bool splitScreenMode = false,
  bool minTextAdapt = false,
}) async {
  final navigatorContext = Navigator.maybeOf(context)?.context as Element?;
  final mediaQueryContext =
      navigatorContext?.getElementForInheritedWidgetOfExactType<MediaQuery>();
  final initCompleter = Completer<void>();
  WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) {
    mediaQueryContext?.visitChildElements((el) => _instance._context = el);
    if (_instance._context != null) initCompleter.complete();
  });
  // ** 我修改的代碼 **
  Orientation orientation = Orientation.landscape;
  Size deviceSize = Size.zero;
  if (isDesktop) {
    Display primaryDisplay = await screenRetriever.getPrimaryDisplay();
    deviceSize = primaryDisplay.size;
    orientation = deviceSize.width > deviceSize.height
        ? Orientation.landscape
        : Orientation.portrait;
  } else {
    final deviceData = MediaQuery.maybeOf(context).nonEmptySizeOrNull();
    deviceSize = deviceData?.size ?? designSize;
    orientation = deviceData?.orientation ??
        (deviceSize.width > deviceSize.height
            ? Orientation.landscape
            : Orientation.portrait);
  }
  _instance
    .._context = context
    .._uiSize = designSize
    .._splitScreenMode = splitScreenMode
    .._minTextAdapt = minTextAdapt
    .._orientation = orientation
    .._screenWidth = deviceSize.width
    .._screenHeight = deviceSize.height;
  // 桌面區分設置下窗口大小
  if (isDesktop) {
    double appWidth = deviceSize.width * app2screenWithWidth;
    double appHeight = appWidth / (designSize.width / designSize.height);
    _instance._uiSize = Size(appWidth, appHeight);
  }
  _instance._elementsToRebuild?.forEach((el) => el.markNeedsBuild());
  return initCompleter.future;
}
  • 之后setWidth等方法都不需要懂了,因為都是拿上面的轉換系數去計算的,系數對了轉換自然就準確了。同時開發過程中也不需要做任何區分,該用.w的就用.w。我們看下.w等是如何通過擴展巧妙的把setWidth這些接口 做的輕量的。
extension SizeExtension on num {
  ///[ScreenUtil.setWidth]
  double get w => ScreenUtil().setWidth(this);
  ///[ScreenUtil.setHeight]
  double get h => ScreenUtil().setHeight(this);
  ///[ScreenUtil.radius]
  double get r => ScreenUtil().radius(this);
  ///[ScreenUtil.setSp]
  double get sp => ScreenUtil().setSp(this);
  ///smart size :  it check your value - if it is bigger than your value it will set your value
  ///for example, you have set 16.sm() , if for your screen 16.sp() is bigger than 16 , then it will set 16 not 16.sp()
  ///I think that it is good for save size balance on big sizes of screen
  double get sm => min(toDouble(), sp);
  ///屏幕寬度的倍數
  ///Multiple of screen width
  double get sw => ScreenUtil().screenWidth * this;
  ///屏幕高度的倍數
  ///Multiple of screen height
  double get sh => ScreenUtil().screenHeight * this;
  ///[ScreenUtil.setHeight]
  Widget get verticalSpace => ScreenUtil().setVerticalSpacing(this);
  ///[ScreenUtil.setVerticalSpacingFromWidth]
  Widget get verticalSpaceFromWidth =>
      ScreenUtil().setVerticalSpacingFromWidth(this);
  ///[ScreenUtil.setWidth]
  Widget get horizontalSpace => ScreenUtil().setHorizontalSpacing(this);
  ///[ScreenUtil.radius]
  Widget get horizontalSpaceRadius =>
      ScreenUtil().setHorizontalSpacingRadius(this);
  ///[ScreenUtil.radius]
  Widget get verticalSpacingRadius =>
      ScreenUtil().setVerticalSpacingRadius(this);
}
  • 資源適配,定義三個設備類型:大、中、小級別;然后在asset目錄下區分三套資源,命名規范區分下larger、medium、small即可。
  • 這個做法非常硬核,我目前也沒這個需求(O(∩_∩)O~。后續考慮渠道編譯,減少包體積,同時開發時也不用區分名稱。

寫在最后

以上方案,我在demo項目中驗證過是沒有問題的。接下來我希望能跟作者溝通下這個方案,看能否提pr合并進去。不然以后就沒辦法很輕松的享受到flutter_screenutil的更新迭代了。

原文鏈接:https://juejin.cn/post/7147190943018385439

欄目分類
最近更新