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