網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
通過(guò)此篇文章,你將了解到:
Flutter windows和Android桌面應(yīng)用屏幕適配的解決方案;
屏幕適配的相關(guān)知識(shí)和原理;
flutter_screenutil的實(shí)現(xiàn)原理和缺陷。
Flutter桌面應(yīng)用的開(kāi)發(fā)過(guò)程中,勢(shì)必需要適配不同尺寸的屏幕。我們的預(yù)期是在不同尺寸的設(shè)備上,用戶的使用感觀基本一致。 如:在個(gè)人pc上,應(yīng)用與屏幕的高度比是2/3;那么到60寸的大設(shè)備上,應(yīng)用的尺寸依然需要2/3比例。
屏幕適配的一些基礎(chǔ)概念
- 屏幕尺寸:屏幕的實(shí)際大小,主要看屏幕的對(duì)角線的長(zhǎng)度,如:6.6英寸。
- 分辨率:屏幕上像素點(diǎn)的總和,如:2400×1176。設(shè)備的屏幕其實(shí)是由N個(gè)像素格子組合成的,屏幕上顯示的所有元素(圖片、文字)從微觀上都是為特定的像素格子繪制上內(nèi)容。
- 屏幕密度(dpi):每英寸的像素格子數(shù)。每英寸展示160個(gè)像素時(shí)稱(chēng)為一倍素;120個(gè)稱(chēng)為低倍素...
假設(shè)我們需要展示一張800×800的圖片,那么在160dpi的手機(jī)屏幕上,我們只要設(shè)置800×800px的寬高;
但在320dpi的屏幕上,由于每英寸的像素點(diǎn)翻倍了,為了用戶的視覺(jué)感受一致,就需要將圖片設(shè)置的寬高設(shè)為1600×1600px。這就是屏幕適配最基本的原理,我們開(kāi)發(fā)中所用到的適配庫(kù),最基礎(chǔ)的能力就是提供這層轉(zhuǎn)換。
Flutter 移動(dòng)端開(kāi)發(fā)的通用做法
Flutter移動(dòng)端的生態(tài)已經(jīng)很完備,我們一般在開(kāi)發(fā)過(guò)程中會(huì)使用flutter_screenutil這個(gè)插件。這是一個(gè)純Dart的pub,閱讀源碼發(fā)現(xiàn)其做法也很簡(jiǎn)單粗暴。
- 根據(jù)傳入的設(shè)計(jì)稿尺寸,與設(shè)備的邏輯像素尺寸的比值作為縮放倍數(shù);
- 開(kāi)發(fā)者設(shè)置的尺寸都會(huì)去乘以對(duì)應(yīng)的縮放倍數(shù),從而實(shí)現(xiàn)widget大小的轉(zhuǎn)換。
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;
// 高度的縮放比:設(shè)備的邏輯像素的高度/設(shè)計(jì)稿的高度
double get scaleHeight =>
(_splitScreenMode ? max(screenHeight, 700) : screenHeight) /
_uiSize.height;
邏輯像素screenHeight從哪來(lái)?
獲取MediaQueryData的size,即應(yīng)用窗口的分辨率。
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;
存在的問(wèn)題
flutter_screenutil 這個(gè)庫(kù)在移動(dòng)端使用是完全沒(méi)有問(wèn)題的。手機(jī)尺寸雖說(shuō)層出不窮,但是也遵循瘦長(zhǎng)的長(zhǎng)方形規(guī)則,最重要的是應(yīng)用默認(rèn)都是全屏的,那么上面第3點(diǎn)獲取到的應(yīng)用窗口高度screenHeight和設(shè)備的大小剛好是完全吻合的。從而計(jì)算出的縮放比(設(shè)計(jì)稿尺寸/設(shè)備的尺寸 = 縮放比值)是偏差不大的。
我們?cè)贏ndroid的桌面應(yīng)用中,這個(gè)庫(kù)也可以支持各種設(shè)備。
然而在windows等桌面端卻沒(méi)那么簡(jiǎn)單:
- 首先桌面設(shè)備的尺寸層出不窮,從個(gè)人筆記本到演示廳的屏幕,物理大小就已經(jīng)差了幾十倍,而像素密度卻差別不大,這在適配上本身就存在更大難度。
- 且通過(guò)驗(yàn)證,F(xiàn)lutterMediaQueryData獲取的是應(yīng)用窗口的大小,但是桌面設(shè)備屏幕大小跟應(yīng)用窗口大小可不是一樣大的,這就是最大的問(wèn)題所在!
通過(guò)實(shí)踐我們也驗(yàn)證了flutter_screenutil在桌面端的適配基本不起作用,且還會(huì)造成不少問(wèn)題,比如:第一次運(yùn)行字體都會(huì)偏大;當(dāng)有多個(gè)擴(kuò)展屏的時(shí)候,主副屏切換有bug。
桌面端解決方案
一、需求分析
我們希望flutter開(kāi)發(fā)出來(lái)的應(yīng)用,在不同的設(shè)備中:
- 應(yīng)用的大小占比屏幕物理尺寸的比例是一致的;
- 系統(tǒng)顯示設(shè)置中的的縮放倍數(shù)不會(huì)影響應(yīng)用的大小;
- 資源大小可以進(jìn)行適配,讓圖片等資源在不同尺寸的設(shè)備上都能顯示清晰。
分析以上預(yù)期效果,可以提煉出一個(gè)原則:應(yīng)用的尺寸必須跟屏幕的物理大小占比一致,與分辨率、像素密度、縮放比都沒(méi)關(guān)系。
二、實(shí)現(xiàn)原理
由于Android端用了flutter_screenutil這個(gè)庫(kù),F(xiàn)lutter又是跨平臺(tái)的。為了降低開(kāi)發(fā)成本,我試著fork源碼下來(lái)更改,并且做了以下的操作:
- 在構(gòu)造函數(shù)上,我加了一個(gè)參數(shù)app2screenWithWidth,讓用戶告知應(yīng)用窗口寬度與屏幕寬度的比值,如: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;
/// 適用于桌面應(yīng)用,應(yīng)用窗口寬度與設(shè)備屏幕寬度的比例
final double app2screenWithWidth;
@override
State<ScreenUtilInit> createState() => _ScreenUtilInitState();
}
- yaml中引入 screenRetriever,獲取到真實(shí)的設(shè)備屏幕像素,這個(gè)是真實(shí)的屏幕像素,跟應(yīng)用窗口沒(méi)關(guān)系;然后可以計(jì)算出應(yīng)用窗口的大小,得出轉(zhuǎn)換系數(shù);
dependencies: flutter: sdk: flutter # 獲取屏幕物理參數(shù) 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;
// 桌面區(qū)分設(shè)置下窗口大小
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等方法都不需要懂了,因?yàn)槎际悄蒙厦娴霓D(zhuǎn)換系數(shù)去計(jì)算的,系數(shù)對(duì)了轉(zhuǎn)換自然就準(zhǔn)確了。同時(shí)開(kāi)發(fā)過(guò)程中也不需要做任何區(qū)分,該用.w的就用.w。我們看下.w等是如何通過(guò)擴(kuò)展巧妙的把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);
///屏幕寬度的倍數(shù)
///Multiple of screen width
double get sw => ScreenUtil().screenWidth * this;
///屏幕高度的倍數(shù)
///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);
}
- 資源適配,定義三個(gè)設(shè)備類(lèi)型:大、中、小級(jí)別;然后在asset目錄下區(qū)分三套資源,命名規(guī)范區(qū)分下larger、medium、small即可。
- 這個(gè)做法非常硬核,我目前也沒(méi)這個(gè)需求(O(∩_∩)O~。后續(xù)考慮渠道編譯,減少包體積,同時(shí)開(kāi)發(fā)時(shí)也不用區(qū)分名稱(chēng)。
寫(xiě)在最后
以上方案,我在demo項(xiàng)目中驗(yàn)證過(guò)是沒(méi)有問(wèn)題的。接下來(lái)我希望能跟作者溝通下這個(gè)方案,看能否提pr合并進(jìn)去。不然以后就沒(méi)辦法很輕松的享受到flutter_screenutil的更新迭代了。
原文鏈接:https://juejin.cn/post/7147190943018385439
相關(guān)推薦
- 2023-08-28 vscode里面報(bào):‘xxx‘ is assigned a value but never used
- 2022-05-14 詳解react-router-dom?v6版本基本使用介紹_React
- 2023-03-16 Android虛擬機(jī)Dalvik和ART科普_Android
- 2022-03-26 淺談C語(yǔ)言數(shù)組元素下標(biāo)為何從0開(kāi)始_C 語(yǔ)言
- 2024-02-16 SpringBoot 事務(wù)的屬性rollbackFor 與 propagetion
- 2023-03-28 Python表示當(dāng)前時(shí)間的方法合集_python
- 2022-05-24 C++實(shí)現(xiàn)中綴表達(dá)式轉(zhuǎn)化為后綴表達(dá)式詳解_C 語(yǔ)言
- 2023-02-25 從迷你todo?命令行入門(mén)Rust示例詳解_Rust語(yǔ)言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支