本文翻譯自 Qt 的官方文章 Scalability,本文只是提供簡單的翻譯,本人的英文水平有限,有不合理的地方希望大家交流指正。
可伸縮性
當我們開發(fā)的應(yīng)用程序要適配到不同的移動設(shè)備的時候,我們常常會面臨如下的挑戰(zhàn):
- 移動設(shè)備平臺支持具有不同屏幕配置的設(shè)備:尺寸,寬高比,方向和密度。
- 不同的平臺有不同的UI約定,你需要在每一個平臺上滿足用戶的期望。
Qt Quick (亦稱 QML,譯者注) 使我們可以開發(fā)在不同類型設(shè)備(如平板電腦和手機)上運行的應(yīng)用程序。 特別是,這些程序可以應(yīng)付不同的屏幕配置。 當然,為了實現(xiàn)在每個目標平臺上有最佳的用戶體驗,我們也需要對我們的程序進行一些細節(jié)上的調(diào)整。
我們在遇到如下情形時需要考慮可伸縮性:
- 希望將應(yīng)用程序部署到多個設(shè)備平臺(如Android和iOS)或多個設(shè)備屏幕配置。
- 希望為初步部署后可能出現(xiàn)在市場上的新設(shè)備做好準備。
我們可以使用 Qt Quick 來實現(xiàn)可擴展的應(yīng)用程序:
- 使用 Qt Quick Controls 或Qt Quick Controls 2 提供的 UI 控件集。
- 使用可以調(diào)整其項目的大小的 Qt Quick Layouts 來定義布局。
- 使用屬性綁定來實現(xiàn)未被布局覆蓋的用例。 例如,要在具有低和高像素密度的屏幕上顯示圖像的替代版本,或根據(jù)當前屏幕方向自動調(diào)整視圖內(nèi)容。
- 選擇一個參考設(shè)備并計算一個縮放比例,以便圖像、字體大小以及邊距能夠與實際屏幕大小相適應(yīng)。
- 使用文件選擇器加載平臺相關(guān)的資源文件。
- 通過使用Loader在需要時再加載組件。
在設(shè)計應(yīng)用程序時,請考慮以下模式:
- 視圖的內(nèi)容在所有屏幕尺寸上應(yīng)盡可能相似,除非它包含可擴展的內(nèi)容區(qū)域。 如果您使用 Qt Quick Controls 中的 ApplicationWindow QML 類型,它將根據(jù)其內(nèi)容項的大小自動計算窗口大小。 如果您使用 Qt Quick Layouts 來定位內(nèi)容項目,Qt Quick Layouts 會自動調(diào)整推送給他們的項目的大小。
- 在較小的設(shè)備中作為整個頁面顯示的組件,可以在較大的設(shè)備中作為整個頁面布局的一部分。 因此,可以考慮在大設(shè)備中使用分割器組件(將在小設(shè)備中顯示的整個頁面放入分割器 QML 文件中),而在較小的設(shè)備中,視圖將僅包含該組件的實例。 在較大的設(shè)備上,可能有足夠的空間來使用動態(tài)加載來顯示其他項目。 例如,在電子郵件查看器中,如果屏幕足夠大,則可以并排顯示電子郵件列表視圖和電子郵件閱讀器視圖。
- 對于游戲來說,我們通常可以創(chuàng)建一個不縮放的游戲視圖,以免給較大屏幕上的玩家?guī)韺е掠螒虿还降膬?yōu)勢。 一個解決方案是定義一個固定的區(qū)域,以適應(yīng)屏幕的最小支持的寬高比(通常為3:2),并添加一些在 4:3 或 16:9 的屏幕上將被隱藏的僅用于裝飾的內(nèi)容。
動態(tài)調(diào)整應(yīng)用程序的大小
Qt Quick Controls 提供了一組可在 Qt Quick 中創(chuàng)建用戶界面的 UI 控件。通常,我們將 ApplicationWindow控件聲明為應(yīng)用程序的根項目。ApplicationWindow 增加了以平臺獨立的方式定位其他控件(如MenuBar,ToolBar 和 StatusBar)的便利。當計算實際窗口的有效大小約束時,ApplicationWindow 使用內(nèi)容項的大小約束作為輸入。
除了定義應(yīng)用程序窗口的標準部分的控件之外,還提供了用于創(chuàng)建視圖和菜單以及呈現(xiàn)或接收用戶輸入的控件。您可以使用 Qt Quick Controls Styles 將自定義樣式應(yīng)用于預(yù)定義的控件。 有關(guān)使用樣式的示例,請參閱 Qt Quick Controls - Touch Gallery。
Qt Quick Controls(如ToolBar)不提供自己的布局,但要求您定位其內(nèi)容。 為此,您可以使用Qt Quick Layouts。
動態(tài)布局屏幕控件
Qt Quick Layouts 提供了使用 RowLayout,ColumnLayout 和 GridLayout QML 類型在行,列或網(wǎng)格中布置屏幕控件的方法。
我們可以使用 Layout QML 類型來將附加屬性附加到已經(jīng)被放置在布局的項目中。例如,我們可以指定最小值,最大值,以及項目高度,寬度和尺寸的首選值。
布局確保在窗口和屏幕調(diào)整大小時,始終使用最大可用空間來適當?shù)乜s放我們的 UI。
一個使用場景的示例如:將 GridLayout 類型根據(jù)屏幕方向?qū)⑵溆米餍谢蛄胁季郑?/p>
如下代碼片段使用了 flow 屬性來設(shè)置網(wǎng)格的布局流,當屏幕的寬度大于高度的時候,從左到右布局(成為一行),否則,將從上到下布局(成為一列):
ApplicationWindow {
id: root
visible: true
width: 480
height: 620
GridLayout {
anchors.fill: parent
anchors.margins: 20
rowSpacing: 20
columnSpacing: 20
flow: width > height ? GridLayout.LeftToRight : GridLayout.TopToBottom
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#5d5b59"
Label {
anchors.centerIn: parent
text: "Top or left"
color: "white"
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#1e1b18"
Label {
anchors.centerIn: parent
text: "Bottom or right"
color: "white"
}
}
}
}
根據(jù)屏幕的調(diào)整實時地調(diào)整和重新計算可能帶來性能和功耗上的問題。 例如,移動和嵌入式設(shè)備可能沒有重新計算每個幀的動畫對象的大小和位置所需的性能。 如果在使用布局時遇到性能問題,請考慮使用其他方法,例如綁定。
以下是在使用布局時的一些注意事項:
- 不要去綁定已經(jīng)在布局中的項目的 x,y,width 或 height 屬性,因為這將與 Layout 的目標相沖突,也會導致循環(huán)綁定問題。
- 不要定義需要根據(jù)屬性變化經(jīng)常重新進行計算的復(fù)雜 JavaScript 函數(shù)。這將導致性能不佳,特別是在動畫運行期間。
- 不要對容器尺寸或子項目的尺寸做出預(yù)想的限定。嘗試使用靈活的布局定義,以便適應(yīng)可能的空間變化。即不將容器和子項目的大小限制太死,而是將其放在布局中,讓其與布局相對存在。
- 如果我們希望我們的設(shè)計在像素級別上分毫不差,那么請勿使用布局。因為布局中的項目將根據(jù)可用空間自動調(diào)整大小并進行定位。在這個過程中,我們的設(shè)計可能發(fā)生改變。但是,這種在像素級別上分毫不差的代價通常是犧牲可移植性。
使用綁定
如果Qt Quick Layouts不符合您的需求,我們可以試試使用屬性綁定。屬性綁定使對象能夠自動更新其屬性以響應(yīng)其他對象屬性的變化或某些外部事件。
當一個對象的屬性被分配一個值時,它可以被分配一個靜態(tài)值,或者被綁定到一個 JavaScript 表達式。在前一種情況下,除非為該屬性分配新值,否則該屬性的值將不會更改。在后一種情況下,創(chuàng)建屬性綁定,并且每當表達式的值更改時,屬性的值將由 QML 引擎自動更新。
這種定位是最高效的。 然而,不斷檢測和重新計算 JavaScript 表達式的改變將帶來性能上的開銷。
我們可以使用綁定來處理沒有自動支持的平臺上的低和高像素密度變化(如macOS和iOS)。以下代碼片段使用 Screen.PixelDensity 附加屬性指定不同的圖像以在具有低,高或正常像素密度的屏幕上顯示:
Image {
source: {
if (Screen.PixelDensity < 40)
"image_low_dpi.png"
else if (Screen.PixelDensity > 300)
"image_high_dpi.png"
else
"image.png"
}
}
在macOS和iOS上,您可以為圖標和圖像提供兩倍大小和 @ 2x 標識符的替代資源,并將它們放置在資源文件中。 在Retina顯示屏上,@ 2x 版本會自動使用。
例如,以下代碼片段將嘗試在Retina顯示器上加載artwork@2x.png:
Image {
source: "artwork.png"
}
處理像素密度
某些QML類型(如 Image,BorderImage 和 Text)會根據(jù)為它們指定的屬性自動縮放。如果沒有指定圖像的寬度和高度,它將自動使用 source 屬性指定的源圖像的大小。默認情況下,指定寬度和高度會使圖像縮放到該大小??梢酝ㄟ^設(shè)置 fillMode 屬性來更改此行為,從而允許圖像被拉伸和平鋪。但是,在高 DPI 顯示屏上,原始圖像尺寸可能會顯得太小。
BorderImage 用于通過縮放或平鋪每個圖像的部分來創(chuàng)建圖像的邊框。它將源圖像分解為9個按照屬性值進行縮放或平鋪的區(qū)域。然而,重疊的角落是根本沒有縮放的,這可能使得結(jié)果在高 DPI 顯示屏上看起來不那么令人滿意。
Text QML 類型會嘗試自適應(yīng)確定需要多少空間并相應(yīng)地設(shè)置寬度和高度屬性,除非它的寬高被明確地設(shè)置。fontPointSize 屬性可以以設(shè)備無關(guān)的方式設(shè)置點大小。然而,指定 font 屬性用點大小,但是指定其他尺寸使用像素大小會導致問題,因為點大小與顯示密度無關(guān)。這種情況下,在低 DPI 顯示屏上看起來正確的字符串的范圍可能在高 DPI 顯示屏上變得太小,因此導致文本被剪切而顯示不全。
支持平臺的高 DPI 支持水平和技術(shù)使用的平臺各不相同。以下部分介紹了在高DPI顯示屏上縮放屏幕內(nèi)容的不同方法。
有關(guān)Qt 和受支持平臺中的高 DPI 支持的更多信息,請參閱 High DPI Displays。
macOS 和 iOS 上的高 DPI 縮放
在 macOS 和 iOS 上,應(yīng)用程序使用高 DPI 擴展,這是傳統(tǒng) DPI 縮放的替代方案。在傳統(tǒng)的方法中,應(yīng)用程序會被提供一個用于乘以字體大小,布局等的 DPI 值。在新的方法中,操作系統(tǒng)為 Qt 提供了縮放比例,用于縮放圖形輸出:分配較大的緩沖區(qū)并設(shè)置縮放變換。
這種方法的優(yōu)點是矢量圖形和字體自動縮放,現(xiàn)有應(yīng)用程序傾向于未修改。然而,對于光柵內(nèi)容,需要高分辨率的替代資源。
QtQuick 和 QtWidgets 堆棧實現(xiàn)了縮放,以及 QtGui 和 Cocoa 平臺插件的一般支持。
OS 縮放窗口,事件和桌面幾何圖形。Cocoa 平臺插件將縮放比例設(shè)置為 QWindow::devicePixelRatio() 或 QScreen::devicePixelRatio() 以及后備存儲。
對于 QtWidgets, QPainter 從后臺存儲器中拾取 devicePixelRatio() ,并將其當作縮放比例。
然而,在 OpenGL 中,像素總是設(shè)備像素。例如,傳遞給 glViewport() 的幾何圖形需要通過 devicePixelRatio() 進行縮放。
與 UI 的其余部分相比,指定的字體大?。ㄒ渣c或像素為單位)不會更改,字符串保留其相對大小。字體被縮放為繪畫的一部分,因此無論是以點或像素指定大小,尺寸為 12 的字體都會有效地以 2 倍縮放為尺寸為 24 的字體。px 單位被解釋為與設(shè)備無關(guān)的像素,以確保在高 DPI 顯示屏上字體不顯得更小。
計算縮放比例
我們可以選擇一個高 DPI 參考設(shè)備并計算一個縮放比例,以便圖像、字體大小以及邊距能夠與實際屏幕大小相適應(yīng)。
以下代碼段使用 Nexus 5 Android 設(shè)備的 DPI,高度和寬度的參考值, QRect 類返回的實際屏幕尺寸以及 qApp 全局指針返回的屏幕的邏輯DPI值,以計算縮放比例,用于圖像尺寸和邊距(m_ratio),以及用于字體大?。╩_ratioFont):
qreal refDpi = 216.;
qreal refHeight = 1776.;
qreal refWidth = 1080.;
QRect rect = QGuiApplication::primaryScreen()->geometry();
qreal height = qMax(rect.width(), rect.height());
qreal width = qMin(rect.width(), rect.height());
qreal dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch();
m_ratio = qMin(height/refHeight, width/refWidth);
m_ratioFont = qMin(height*refDpi/(dpi*refHeight), width*refDpi/(dpi*refWidth));
對于合理的縮放比例,高度和寬度值必須根據(jù)參考設(shè)備的默認方向進行設(shè)置,在上面的示例中就是縱向方向。
以下代碼片段將字體縮放比例設(shè)置為1,因為如果它小于1,會導致字體大小變得太?。?/p>
int tempTimeColumnWidth = 600;
int tempTrackHeaderWidth = 270;
if (m_ratioFont < 1.) {
m_ratioFont = 1;
我們應(yīng)該嘗試使用目標設(shè)備來查找需要額外計算的情況。一些屏幕可能太短或狹窄,為了適應(yīng)所有規(guī)劃好的內(nèi)容,因此需要自己的布局。例如,我們可能需要隱藏或替換非典型寬高比的屏幕上的某些內(nèi)容,例如寬高比為 1:1 的屏幕。
縮放比例可以應(yīng)用于 QQmlPropertyMap 中的所有尺寸以縮放圖像,字體和邊距:
m_sizes = new QQmlPropertyMap(this);
m_sizes->insert(QLatin1String("trackHeaderHeight"), QVariant(applyRatio(270)));
m_sizes->insert(QLatin1String("trackHeaderWidth"), QVariant(applyRatio(tempTrackHeaderWidth)));
m_sizes->insert(QLatin1String("timeColumnWidth"), QVariant(applyRatio(tempTimeColumnWidth)));
m_sizes->insert(QLatin1String("conferenceHeaderHeight"), QVariant(applyRatio(158)));
m_sizes->insert(QLatin1String("dayWidth"), QVariant(applyRatio(150)));
m_sizes->insert(QLatin1String("favoriteImageHeight"), QVariant(applyRatio(76)));
m_sizes->insert(QLatin1String("favoriteImageWidth"), QVariant(applyRatio(80)));
m_sizes->insert(QLatin1String("titleHeight"), QVariant(applyRatio(60)));
m_sizes->insert(QLatin1String("backHeight"), QVariant(applyRatio(74)));
m_sizes->insert(QLatin1String("backWidth"), QVariant(applyRatio(42)));
m_sizes->insert(QLatin1String("logoHeight"), QVariant(applyRatio(100)));
m_sizes->insert(QLatin1String("logoWidth"), QVariant(applyRatio(286)));
m_fonts = new QQmlPropertyMap(this);
m_fonts->insert(QLatin1String("six_pt"), QVariant(applyFontRatio(9)));
m_fonts->insert(QLatin1String("seven_pt"), QVariant(applyFontRatio(10)));
m_fonts->insert(QLatin1String("eight_pt"), QVariant(applyFontRatio(12)));
m_fonts->insert(QLatin1String("ten_pt"), QVariant(applyFontRatio(14)));
m_fonts->insert(QLatin1String("twelve_pt"), QVariant(applyFontRatio(16)));
m_margins = new QQmlPropertyMap(this);
m_margins->insert(QLatin1String("five"), QVariant(applyRatio(5)));
m_margins->insert(QLatin1String("seven"), QVariant(applyRatio(7)));
m_margins->insert(QLatin1String("ten"), QVariant(applyRatio(10)));
m_margins->insert(QLatin1String("fifteen"), QVariant(applyRatio(15)));
m_margins->insert(QLatin1String("twenty"), QVariant(applyRatio(20)));
m_margins->insert(QLatin1String("thirty"), QVariant(applyRatio(30)));
以下代碼段中的函數(shù)將縮放比例應(yīng)用于字體,圖像和邊距:
int Theme::applyFontRatio(const int value)
{
return int(value * m_ratioFont);
}
int Theme::applyRatio(const int value)
{
return qMax(2, int(value * m_ratio));
}
根據(jù)平臺加載文件資源
我們可以使用 QQmlFileSelector 將 QFileSelector 應(yīng)用于 QML 文件加載。這使您能夠根據(jù)運行應(yīng)用程序的平臺加載替代資源。例如,我們可以使用 + Android 文件選擇器在 Android 設(shè)備上運行時加載不同的圖像文件。
我們可以使用文件選擇器和單例對象來訪問特定平臺上的對象的單個實例。
文件選擇器是靜態(tài)的,并執(zhí)行文件結(jié)構(gòu),其中特定于平臺的文件存儲在以平臺命名的子文件夾中。 如果您需要一個更加動態(tài)的解決方案來按需加載 UI 的部件,則可以使用 Loader 組件。
目標平臺可以以各種方式自動加載不同顯示密度的替代資源。在iOS上,@2x 文件名后綴用于指示圖像的高 DPI 版本。Image QML 類型和 QIcon 類自動加載 @2x 版本的圖像和圖標(如果提供)。QImage 和 QPixmap 類自動將 @2x 版本的圖像的 devicePixelRatio 設(shè)置為 2,但是我們需要添加實際使用 @2x 版本的代碼:
if ( QGuiApplication::primaryScreen()->devicePixelRatio() >= 2 ) {
imageVariant = "@2x";
} else {
imageVariant = "";
}
Android 定義了可以創(chuàng)建替代資源的廣義屏幕尺寸(small,normal,large,xlarge)和密度(ldpi,mdpi,hdpi,xhdpi,xxhdpi 和 xxxhdpi)。Android 會在運行時檢測當前的設(shè)備配置,并為應(yīng)用程序加載適當?shù)馁Y源。然而,從Android 3.2(API級別13)開始,這些大小組已被棄用,有利于基于可用屏幕寬度來管理屏幕尺寸的新技術(shù)。
按需加載組件
Loader 可以加載 QML 文件(使用source屬性)或 Component 對象(使用 sourceComponent 屬性)。對于延遲組件的創(chuàng)建直到需要才有用。例如,在需要時再創(chuàng)建組件,或者由于性能原因,不應(yīng)該創(chuàng)建不必要的組件時。
您也可以使用加載程序?qū)μ囟ㄆ脚_上不需要部分UI的情況做出反應(yīng),在這些平臺不支持某些功能時。應(yīng)用程序正在運行的設(shè)備上不去顯示不需要的視圖的時候,我們可以將視圖隱藏并使用加載器在其位置顯示其他內(nèi)容。
切換方向
Screen.orientation 附加屬性包含從加速度計(如果可用)獲取的屏幕當前方向。在臺式機上,此值通常不會改變。
如果 primaryOrientation 屬性的值隨方向改變,則表示屏幕會自動旋轉(zhuǎn)顯示的所有內(nèi)容,具體取決于我們?nèi)绾挝兆≡O(shè)備。如果方向更改,而 primaryOrientation 不更改,設(shè)備可能不會旋轉(zhuǎn)自身的顯示內(nèi)容。在這種情況下,我們可能需要使用 Item.rotation 或 Item.transform 來旋轉(zhuǎn)內(nèi)容。
應(yīng)用程序頂級頁面定義和可重用組件定義應(yīng)為布局結(jié)構(gòu)使用一個 QML 布局定義。該單一定義應(yīng)包括用于單獨的設(shè)備方向和寬高比的布局設(shè)計。原因是在方向切換期間的性能至關(guān)重要,因此,當方向改變時,確保兩個方向所需的所有組件都被加載是一個好主意。
相反,如果您選擇使用加載程序來加載單獨方向所需的其他 QML,則應(yīng)執(zhí)行徹底測試,因為這將影響方向更改的性能。
為了啟用方向之間的布局動畫,錨定義必須駐留在相同的包含組件中。因此,頁面或組件的結(jié)構(gòu)應(yīng)包含一組共同的子組件,一組常用的錨定義,以及一組狀態(tài)(在 StateGroup 中定義),表示該組件支持的不同寬高比。
如果頁面中包含的組件需要托管在許多不同形式因子的定義中,則視圖的布局狀態(tài)應(yīng)取決于頁面(其直接容器)的寬高比。類似地,組件的不同實例可能位于 UI 中的多個不同容器中,因此其布局狀態(tài)應(yīng)由其父級的寬高比來確定。結(jié)論是布局狀態(tài)應(yīng)該始終遵循直接容器的寬高比(而不是當前設(shè)備屏幕的“方向”)。
在每個布局狀態(tài)中,我們應(yīng)該使用本地 QML 布局定義來定義項目之間的關(guān)系。有關(guān)詳細信息,請參閱下文。在狀態(tài)之間(由頂級方向改變觸發(fā))過渡期間,在錨定布局的情況下,AnchorAnimation 元素可用于控制轉(zhuǎn)換。在某些情況下,我們也可以使用例如 NumberAnimation 于項目的 width 屬性。記住在每個動畫幀中避免復(fù)雜的JavaScript計算。在大多數(shù)情況下,使用簡單的錨定義和錨點動畫可以幫助我們。
還有一些特定情況下的開發(fā)建議:
我們是否有一個頁面在橫屏和豎屏之間看起來完全不同,包括所有的子項目是不同的?對于每個頁面,我們可以定義兩個具有單獨的布局的子組件,并使每個項目中的一個或多個項目在不同狀態(tài)時設(shè)置其透明度為零。您可以通過簡單地對 opacity 屬性應(yīng)用NumberAnimation 來實現(xiàn)交叉漸變動畫。
我們是否有一個頁面在縱向和橫向之間共享30%或更多相同的布局內(nèi)容?在這種情況下,請考慮使用具有橫向和縱向狀態(tài)的一個組件,以及 opacity(或 position)取決于方向狀態(tài)的單獨子項的集合。這將使我們能夠在方向切換時,對共享的項目的使用布局動畫,而其他項目則則使用淡入/淡出或開/關(guān)屏幕動畫。
如果手持設(shè)備上有兩頁需要同時在屏幕上,例如在較大的外形設(shè)備上,該怎么辦?在這種情況下,需要注意使我們的視圖組件將不再占據(jù)全屏。因此,要記住的最重要一點是所有組件(特別是列表委托項目)的尺寸應(yīng)該取決于包含這些組件的容器組件的寬度,而不是屏幕的寬度。 在這種情況下,可能需要在 Component.onCompleted() 中設(shè)置寬度,以確保在設(shè)置值之前已經(jīng)構(gòu)造了列表項委托。
如果出現(xiàn)必須同時加載兩個方向的組件,但是又特別占用內(nèi)存怎么辦呢?如果我們不能將視圖的兩個版本一次保存在內(nèi)存中,則必要時可以使用 Loader ,但請注意布局切換期間交叉漸變動畫的性能。一個解決方案是定義兩個"splash screen" 項目作為頁面的子項,然后在旋轉(zhuǎn)的時候進行淡入淡出。我們可以使用 Loader 加載另一個正在將實際模型數(shù)據(jù)加載到其子項的子組件,并在 Loader 加載完成時運行淡出淡入效果。