網站首頁 編程語言 正文
前言
通過此篇文章,你將了解到:
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
相關推薦
- 2022-12-28 Android?ViewPager2?+?Fragment?聯動效果的實現思路_Android
- 2022-08-16 R語言繪制維恩圖ggvenn示例詳解_R語言
- 2022-04-10 關于C#中GUI編程的標準事件問題_C#教程
- 2022-05-18 python中leastsq函數的使用方法_python
- 2023-05-20 Python?seek()和tell()函數的具體使用_python
- 2022-04-14 Qt自定義控件實現儀表盤_C 語言
- 2022-06-14 jquery實現點擊按鈕顯示與隱藏效果_jquery
- 2022-05-20 淺談redis的過期時間設置和過期刪除機制_Redis
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支