詳解Flutter桌面應(yīng)用如何進(jìn)行多分辨率適配
前言
通過此篇文章,你將了解到:
Flutter windows和Android桌面應(yīng)用屏幕適配的解決方案;
屏幕適配的相關(guān)知識(shí)和原理;
flutter_screenutil的實(shí)現(xiàn)原理和缺陷。
Flutter桌面應(yīng)用的開發(fā)過程中,勢必需要適配不同尺寸的屏幕。我們的預(yù)期是在不同尺寸的設(shè)備上,用戶的使用感觀基本一致。 如:在個(gè)人pc上,應(yīng)用與屏幕的高度比是2/3;那么到60寸的大設(shè)備上,應(yīng)用的尺寸依然需要2/3比例。
屏幕適配的一些基礎(chǔ)概念
- 屏幕尺寸:屏幕的實(shí)際大小,主要看屏幕的對角線的長度,如:6.6英寸。
- 分辨率:屏幕上像素點(diǎn)的總和,如:2400×1176。設(shè)備的屏幕其實(shí)是由N個(gè)像素格子組合成的,屏幕上顯示的所有元素(圖片、文字)從微觀上都是為特定的像素格子繪制上內(nèi)容。
- 屏幕密度(dpi):每英寸的像素格子數(shù)。每英寸展示160個(gè)像素時(shí)稱為一倍素;120個(gè)稱為低倍素...
假設(shè)我們需要展示一張800×800的圖片,那么在160dpi的手機(jī)屏幕上,我們只要設(shè)置800×800px的寬高;
但在320dpi的屏幕上,由于每英寸的像素點(diǎn)翻倍了,為了用戶的視覺感受一致,就需要將圖片設(shè)置的寬高設(shè)為1600×1600px。這就是屏幕適配最基本的原理,我們開發(fā)中所用到的適配庫,最基礎(chǔ)的能力就是提供這層轉(zhuǎn)換。
Flutter 移動(dòng)端開發(fā)的通用做法
Flutter移動(dòng)端的生態(tài)已經(jīng)很完備,我們一般在開發(fā)過程中會(huì)使用flutter_screenutil這個(gè)插件。這是一個(gè)純Dart的pub,閱讀源碼發(fā)現(xiàn)其做法也很簡單粗暴。
- 根據(jù)傳入的設(shè)計(jì)稿尺寸,與設(shè)備的邏輯像素尺寸的比值作為縮放倍數(shù);
- 開發(fā)者設(shè)置的尺寸都會(huì)去乘以對應(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從哪來?
獲取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;
存在的問題
flutter_screenutil 這個(gè)庫在移動(dòng)端使用是完全沒有問題的。手機(jī)尺寸雖說層出不窮,但是也遵循瘦長的長方形規(guī)則,最重要的是應(yīng)用默認(rèn)都是全屏的,那么上面第3點(diǎn)獲取到的應(yīng)用窗口高度screenHeight和設(shè)備的大小剛好是完全吻合的。從而計(jì)算出的縮放比(設(shè)計(jì)稿尺寸/設(shè)備的尺寸 = 縮放比值)是偏差不大的。
我們在Android的桌面應(yīng)用中,這個(gè)庫也可以支持各種設(shè)備。
然而在windows等桌面端卻沒那么簡單:
- 首先桌面設(shè)備的尺寸層出不窮,從個(gè)人筆記本到演示廳的屏幕,物理大小就已經(jīng)差了幾十倍,而像素密度卻差別不大,這在適配上本身就存在更大難度。
- 且通過驗(yàn)證,F(xiàn)lutterMediaQueryData獲取的是應(yīng)用窗口的大小,但是桌面設(shè)備屏幕大小跟應(yīng)用窗口大小可不是一樣大的,這就是最大的問題所在!
通過實(shí)踐我們也驗(yàn)證了flutter_screenutil在桌面端的適配基本不起作用,且還會(huì)造成不少問題,比如:第一次運(yùn)行字體都會(huì)偏大;當(dāng)有多個(gè)擴(kuò)展屏的時(shí)候,主副屏切換有bug。
桌面端解決方案
一、需求分析
我們希望flutter開發(fā)出來的應(yīng)用,在不同的設(shè)備中:
- 應(yīng)用的大小占比屏幕物理尺寸的比例是一致的;
- 系統(tǒng)顯示設(shè)置中的的縮放倍數(shù)不會(huì)影響應(yīng)用的大小;
- 資源大小可以進(jìn)行適配,讓圖片等資源在不同尺寸的設(shè)備上都能顯示清晰。
分析以上預(yù)期效果,可以提煉出一個(gè)原則:應(yīng)用的尺寸必須跟屏幕的物理大小占比一致,與分辨率、像素密度、縮放比都沒關(guān)系。
二、實(shí)現(xiàn)原理
由于Android端用了flutter_screenutil這個(gè)庫,F(xiàn)lutter又是跨平臺(tái)的。為了降低開發(fā)成本,我試著fork源碼下來更改,并且做了以下的操作:
- 在構(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)用窗口沒關(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ù)對了轉(zhuǎn)換自然就準(zhǔn)確了。同時(shí)開發(fā)過程中也不需要做任何區(qū)分,該用.w的就用.w。我們看下.w等是如何通過擴(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è)備類型:大、中、小級(jí)別;然后在asset目錄下區(qū)分三套資源,命名規(guī)范區(qū)分下larger、medium、small即可。
- 這個(gè)做法非常硬核,我目前也沒這個(gè)需求(O(∩_∩)O~。后續(xù)考慮渠道編譯,減少包體積,同時(shí)開發(fā)時(shí)也不用區(qū)分名稱。
寫在最后
以上方案,我在demo項(xiàng)目中驗(yàn)證過是沒有問題的。接下來我希望能跟作者溝通下這個(gè)方案,看能否提pr合并進(jìn)去。不然以后就沒辦法很輕松的享受到flutter_screenutil的更新迭代了。
期待Flutter桌面社區(qū)越來越豐富!更多關(guān)于Flutter桌面應(yīng)用多分辨率適配的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android?利用ImageView屬性實(shí)現(xiàn)選中和未選中效果
這篇文章主要介紹了Android巧用ImageView屬性實(shí)現(xiàn)選中和未選中效果,實(shí)現(xiàn)思路通常我們會(huì)選擇在布局里加個(gè)ImageView,然后通過代碼層面加個(gè)判斷去讓ImageView加載不同狀態(tài)的圖片,需要的朋友可以參考下2023-06-06Android優(yōu)化方案之Fragment的懶加載實(shí)現(xiàn)代碼
本篇文章主要介紹了Android優(yōu)化方案之Fragment的懶加載實(shí)現(xiàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-03-03Android補(bǔ)間動(dòng)畫基本使用(位移、縮放、旋轉(zhuǎn)、透明)
這篇文章主要介紹了Android補(bǔ)間動(dòng)畫基本使用(位移、縮放、旋轉(zhuǎn)、透明),補(bǔ)間動(dòng)畫就是原形態(tài)變成新形態(tài)時(shí)為了過渡變形過程,生成的動(dòng)畫2018-05-05Android的Fragment的生命周期各狀態(tài)和回調(diào)函數(shù)使用
這篇文章主要介紹了Android的Fragments的生命周期各狀態(tài)和回調(diào)函數(shù)使用,Fragments的生命周期與Activity息息相關(guān),需要的朋友可以參考下2016-02-02Android String類型轉(zhuǎn)換為float、double和int的工具類方法
今天小編就為大家分享一篇Android String類型轉(zhuǎn)換為float、double和int的工具類方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07android 使用OkHttp上傳多張圖片的實(shí)現(xiàn)代碼
這篇文章主要介紹了android 使用OkHttp上傳多張圖片的相關(guān)資料,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07