可伸缩性#

如何开发在不同屏幕配置和UI约定设备上表现良好的应用程序。

当您为几个不同的移动设备平台开发应用程序时,您会遇到以下挑战

  • 移动设备平台支持具有不同屏幕配置的设备:大小、宽高比、方向和密度。

  • 不同的平台有不同的UI约定,您需要满足用户在每个平台的预期。

Qt Quick使您能够开发可在不同类型设备上运行的应用程序,例如平板电脑和手机。特别是,它们可以处理不同的屏幕配置。但是,始终需要对每个目标平台进行一定程度的修复和润色,以创建最佳的用户体验。

您需要考虑可伸缩性时

  • 您想将应用程序部署到多个设备平台,如Android和iOS,或多个设备屏幕配置。

  • 您想为在初始部署后可能出现在市场上的新设备做好准备。

使用Qt Quick实现可伸缩应用程序

  • 使用Qt Quick Controls设计UI,它提供了一组UI控件。

  • 使用Qt Quick Layouts定义布局,它们可以调整其项目的大小。

  • 使用属性绑定实现布局未涵盖的用例。例如,显示具有低和高像素密度的屏幕的图像的备选版本或根据当前屏幕方向自动调整视图内容。

  • 选择一个参考设备,并计算一个缩放比来调整图像和字体大小及边距以适应实际屏幕尺寸。

  • 使用文件选择器加载平台特定的资产。

  • 使用Loader按需加载组件。

在设计应用程序时考虑以下模式

  • 视图的内容在所有屏幕尺寸上可能相当相似,但具有一个扩展的内容区域。如果您使用Qt Quick Controls中的ApplicationWindow QML类型,它将自动根据其内容项的大小计算窗口大小。如果您使用Qt Quick Layouts定位内容项,则它们将自动调整推送到它们的项的大小。

  • 较小设备中的整个页面的内容可以是较大设备中布局的一个组件元素。因此,考虑将其作为一个单独的组件(即在单独的QML文件中定义的),较小设备中的视图将只需包含该组件的一个实例。在较大设备上,可能有足够的空间使用加载器显示额外的项目。例如,在电子邮件查看器中,如果屏幕足够大,则可以并排显示电子邮件列表视图和电子邮件阅读器视图。

  • 对于游戏,通常希望创建一个不进行缩放的棋盘,以便不提供给较大屏幕上的玩家不公平的优势。一个解决方案是定义一个安全区域,该区域适合具有最小支持的宽高比(通常为3:2)的屏幕,并在4:3或16:9屏幕上将隐藏的空间添加仅具有装饰功能的 内容。

动态调整应用程序窗口大小#

Qt Quick Controls提供一组UI控件,用于在Qt Quick中创建用户界面。通常,您将ApplicationWindow控件声明为应用程序的根项目。ApplicationWindow提供了一种在平台无关的方式定位其他控件(如MenuBar、ToolBar和StatusBar)的便利性。当计算实际窗口的有效尺寸约束时,ApplicationWindow使用内容项的尺寸约束作为输入。

除定义应用程序窗口标准部分的控件外,还提供了用于创建视图和菜单、以及向用户展示或接收输入的控件。您可以使用Qt Quick Controls Styles为预定义控件应用自定义样式。

例如,ToolBar之类的Qt Quick Controls不提供自己的布局,但需要您定位其内容。为此,您可以使用Qt Quick Layouts。

动态布局屏幕控件#

Qt Quick Layouts提供使用RowLayout、ColumnLayout和GridLayout QML类型以行、列或网格布局屏幕控件的方法。这些QML类型的属性持有它们的布局方向以及单元格间的间距。

您可以使用Qt Quick Layouts QML类型向布局中推入的项目附加其他属性。例如,您可以指定项的高度、宽度和大小的最小、最大和首选值。

布局确保在窗口和屏幕调整大小时,您的UI能够正确缩放,并始终使用可用的最大空间。

GridLayout类型的特定用例是根据屏幕方向将其用作行或列。

../_images/scalability-gridlayout.png

以下代码片段使用flow属性根据屏幕宽度是否大于屏幕高度将网格的流动方向设置为从左到右(作为行),否则为从上到下(作为列)

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"
            }
        }
    }
}

不断调整大小和重新计算屏幕会产生性能成本。移动和嵌入式设备可能没有足够的功率来每帧重新计算动画对象的尺寸和位置,例如。如果您在布局中使用时遇到性能问题,请考虑使用一些其他方法,例如绑定。

以下是一些不建议的操作

  • 不要将绑定添加到布局中项的x、y、width或height属性,因为这会与传统布局的目标冲突,并导致绑定循环。

  • 不要定义复杂的JavaScript函数,这些函数会被频繁评估。这会导致性能低下,尤其是在动画转换期间。

  • 不要对容器大小或子项的尺寸做出假设。尽量创建灵活的布局定义,以便能够吸收可用空间的变化。

  • 如果您想让设计像素完美,不要使用布局。内容项将根据可用空间自动调整大小和定位。

使用绑定#

如果Qt Quick Layouts不满足您的需求,您可以使用属性绑定。绑定使对象能够自动更新其属性,以响应其他对象中改变的特征或某些外部事件的发生。

当对象的属性被赋予一个值时,它可以是静态值,也可以是被绑定到一个JavaScript表达式中。在前者的情况下,除非将新值赋予属性,否则属性的值不会改变。在后一种情况下,会创建一个属性绑定,并自动更新属性值,由QML引擎确定当计算的表达式值发生变化时。

此类型定位是最动态的。然而,不断评估JavaScript表达式所带来的性能成本。

您可以使用绑定来处理没有自动支持(如Android、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"
        }
    }

在Android、macOS和iOS上,您可以通过使用相应的标识符(例如@2x@3x@4x)为图标和图像提供更高分辨率的替代资源,并将它们放置在资源文件中。将自动选择与屏幕像素密度相匹配的版本用于。

例如,以下代码示例将在Retina显示上尝试加载[email protected]

Image {
    source: "artwork.png"
}

处理像素密度#

一些QML类型,例如Image、BorderImage和Text,会根据它们指定的属性自动缩放。如果未指定Image的宽度和高度,它将自动使用由source属性指定的源图像大小。默认情况下,指定宽度和高度会导致图像缩放至该大小。可以通过设置fillMode属性来更改此行为,允许图像拉伸和平铺。然而,在高DPI显示器上,原始图像大小可能会显得太小。

BorderImage通过缩放或平铺每个图像的的部分来创建图像边框。它将源图像分割成9个区域,这些区域根据属性值进行缩放或平铺。但是角落不会进行缩放,这在高DPI显示器上可能导致结果不是最优的。

Text QML类型尝试确定所需的Room量,并相应地设置widthheight属性,除非它们被明确设置。fontPointSize属性以设备无关的方式设置点大小。但是,使用点来指定字体和在像素中指定其他大小会导致问题,因为点与显示密度无关。在低DPI显示器上看起来正确的字符串周围的框架,在高DPI显示器上可能会变得太小,导致文本被裁剪。

高DPI支持程度以及支持的平台的实现技术各不相同。以下各节描述了在高速DPI显示器上缩放屏幕内容的不同方法。

有关Qt和具体平台中的高DPI支持更多信息,请参阅高DPI

在macOS和iOS上的高DPI缩放#

在macOS和iOS上,应用使用与传统DPI缩放不同的称为高DPI缩放的技术。在传统方法中,应用程序会接收到一个DPI值,该值用于乘以字体大小、布局等。在新方法中,操作系统提供给Qt一个缩放比来缩放图形输出:分配更大的缓冲区并设置缩放转换。

此方法的优点是矢量图形和字体可以自动缩放,并且现有应用程序通常无需修改即可运行。对于位图内容,则需要高分辨率的替代资源。

这种缩放在QtQuick和QtWidgets中实现,以及QtGui和Cocoa平台插件中的常规支持。

操作系统缩放窗口、事件和桌面几何形状。Cocoa平台插件设置缩放比为QWindow::devicePixelRatio()或QScreen::devicePixelRatio(),以及在后端存储上。

对于QtWidgets,QPainter从后端存储中接取 devicePixelRatio() 并将其解释为一个缩放比例。

然而,在OpenGL中像素始终是设备像素。例如,传递给glViewport()的几何形状需要通过设备PixelRatio()进行缩放。

指定的字体大小(以点或像素为单位)不会改变,字符串相对于UI的其他部分保持其相对大小。字体作为绘画的一部分进行缩放,因此12号字体实际上变成了24号字体,具有2倍缩放,无论指定的是点还是像素。 px 单位被解释为设备无关像素,以确保字体在高清显示屏上不会显得更小。

计算缩放比例#

您可以选取一个高DPI设备作为参考设备,并计算出调整图像、字体大小和边距以适应实际屏幕大小的缩放比例。

以下代码片段使用Nexus 5安卓设备的DPI、高度和宽度参考值,QRect类返回的实际屏幕大小,以及全局指针qApp返回的屏幕逻辑DPI值来计算图像大小和边距(m_ratio)的缩放比例,以及另一个用于字体大小(m_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));

对于合理的缩放比例,高度和宽度值必须根据参考设备的默认方向设置,在这个例子中是纵向方向。

以下代码片段将字体缩放比例设置为一,如果它小于一,那么会导致字体大小变得过小。

int tempTimeColumnWidth = 600;
int tempTrackHeaderWidth = 270;
if (m_ratioFont < 1.) {
    m_ratioFont = 1;

应该对目标设备进行实验以找到需要额外计算的边缘情况。一些屏幕可能太短或太窄无法容纳所有计划的内容,因此需要自己的布局。例如,可能需要在具有非典型宽高比(如1:1)的屏幕上隐藏或替换某些内容。

缩放比例可以应用于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)));

以下代码片段中的函数将缩放比例应用于字体、图像和边距

int Theme::applyFontRatio(const int value)
{
    return int(value * m_ratioFont);
}

int Theme::applyRatio(const int value)
{
    return qMax(2, int(value * m_ratio));
}

当目标设备的屏幕大小差异不是很大时,这种技术能给出合理的成果。如果差异很大,考虑为不同的参考值创建几个不同的布局。

根据平台加载文件#

您可以使用QQmlFileSelector将QFileSelector应用于QML文件加载。这使您能够根据应用程序运行的平台加载不同的资源。例如,您可以使用 +android 文件选择器在安卓设备上加载不同的图像文件。

您可以使用文件选择器与单例对象结合使用来访问特定平台上的单个对象实例。

文件选择器是静态的,并强制执行一个文件结构,平台特定的文件存储在以平台命名的子文件夹中。如果您需要一个更动态的解决方案,以按需加载您的UI的某些部分,您可以使用一个Loader。

目标平台可能会以不同的方式自动加载不同显示密度的替代资源。在Android和iOS上,使用@2x文件名后缀来指示图像的高DPI版本。Image QML类型和QIcon类会在提供的情况下自动加载图像和图标的@2x版本。QImage和QPixmap类会自动将@2x版本图像的devicePixelRatio设置为2,但您需要添加代码才能真正使用@2x版本。

if ( QGuiApplication::primaryScreen()->devicePixelRatio() >= 2 ) {
    imageVariant = "@2x";
} else {
    imageVariant = "";
}

Android定义了一般屏幕尺寸(小、普通、大、超大)和密度(ldpi、mdpi、hdpi、xhdpi、xxhdpi和xxxhdpi),您可以为此创建替代资源。Android在运行时检测当前设备配置并加载应用程序的适当资源。然而,从Android 3.2(API级别13)开始,这些尺寸组被废弃,转而使用一种基于可用屏幕宽度的管理屏幕尺寸的新技术。

按需加载组件#

Loader可以加载一个QML文件(使用source属性)或组件对象(使用sourceComponent属性)。这对于延迟创建组件直到需要时非常有用。例如,当组件需要按需创建,或者出于性能原因不应不必要地创建组件时。

您还可以使用loader来应对在特定平台上不需要UI部分的情况,因为平台不支持某些功能。而无需在运行应用程序的设备上显示不需要的视图,您可以确定该视图将被隐藏,并使用loader在其位置显示其他内容。

切换方向#

Screen.orientation关联属性包含当前屏幕方向,来自加速度计(如果可用)。在桌面电脑上,此值通常不会改变。

如果primaryOrientation遵循orientation,这意味着屏幕会根据您持有设备的方式自动旋转显示的所有内容。即使primaryOrientation没有改变,方向发生了变化,设备可能不会旋转自身的显示。在这种情况下,您可能需要使用Item.rotation或Item.transform来旋转您的内容。

应该使用单个QML布局定义来定义顶级页面定义和可复用组件定义的布局结构。这个单独的定义应包括对不同设备方向和宽高比的布局设计。原因是方向切换时的性能至关重要,因此最好确保在方向改变时加载两种方向都需要的所有组件。

相反,如果您选择使用Loader加载在各自方向上需要的额外QML,那么您应该进行全面测试,因为这将影响方向切换的性能。

为了在方向之间启用布局动画,锚定义必须位于同一包含组件中。因此,页面或组件的结构应该由一组子组件、一组锚定义以及一组表示组件支持的各个宽高比的状态(在StateGroup中定义)组成。

如果一个页面中的组件需要在多种不同的形态定义中托管,那么视图的布局状态应该依赖于页面(其直接容器)的宽高比。同样,组件的不同实例可能会在UI中的许多不同容器中定位,因此其布局状态应由其父容器的宽高比决定。结论是,布局状态应始终遵循直接容器的宽高比(而不是当前设备的屏幕“方向”)。

在每个布局状态中,您应该使用原生的QML布局定义来定义项目之间的关系。有关更多信息,请参见下文。在状态之间的转换(由顶级方向变更触发)期间,对于锚点布局,可以使用锚点动画元素来控制转换。在某些情况下,您还可以对例如项目宽度使用数字动画。请记住,在动画的每一帧中避免复杂的JavaScript计算。使用简单的锚点和锚点动画可以在大多数情况下帮助解决这个问题。

还有几个额外的案例需要考虑

  • 如果你有一个看起来在横向和纵向之间完全不同的单页,也就是说所有子项目都不同,那么该怎么办?对于每个页面,都有两个子组件,具有不同的布局定义,并在每个状态中使一个或另一个项具有零不透明度。您可以通过简单地应用数字动画过渡到不透明度来使用跨淡动画。

  • 如果你有一个在纵向和横向之间共享30%或更多相同布局内容的单页,那么该怎么办?在这种情况下,考虑有一个有横向和纵向状态的单个组件,以及一个包含不透明度(或位置)取决于方向状态的单独子项集合。这将允许您使用布局动画来处理在两种方向间共享的项目,而其他项目则是淡入/淡出,或在整个屏幕上动画显示。

  • 如果你在手持设备上有两个页面需要同时显示在屏幕上,例如在较大的形态设备上,那么该怎么办?在这种情况下,请注意您的视图组件不会占据整个屏幕。因此,在所有组件(特别是列表代理项)中,应依赖于包含组件的宽度,而不是屏幕宽度。在这种情况下可能需要在Component.onCompleted()处理程序中设置宽度,以确保在设置值之前已构造列表项代理。

  • 如果两个方向占用太多内存而无法同时保持在内存中,那么在需要的情况下使用Loader,如果无法同时保持视图的两个版本在内存中,但在布局切换期间要注意跨淡动画的性能。一个解决方案可以是有一个“启动屏幕”项作为页面的子项,然后在旋转时在这些项之间进行跨淡。然后您可以使用Loader来加载另一个子组件,后者加载实际的模型数据到另一个子Item中,并在Loader完成时跨淡到该组件。