返回博客首页
← 所有文章

使用 NativeScript 构建响应式应用

2019 年 4 月 9 日 — 作者:Tiago Alves

NativeScript 在允许开发者从同一个代码库创建 Android 和 iOS 应用方面做得非常出色。但手机和平板电脑呢?我们是否也可以使用相同的代码库来适应不同的屏幕尺寸和比例?如果不这样做,那真是太傻了。在本文中,我们将探讨一些调整布局以适应所有类型设备的策略。

如果您来自 Web 开发领域,那么您可能熟悉“响应式布局”这个术语,即能够适应浏览器尺寸的布局。不过,原生应用的情况并不完全相同。用户可以根据自己的意愿调整浏览器大小,但应用(几乎)总是占据屏幕的全部尺寸。屏幕尺寸有数百种,但我们可以将其大致分为两种类型:**手机**和平板电脑。区别不在于屏幕尺寸本身(有 6.5 英寸的手机和 7 英寸的平板电脑),而在于用户与设备的交互方式。例如,人们通常用一只手以纵向模式握住手机,而平板电脑则通常以横向模式放在桌子上。

并非每个应用都需要响应式。您的应用可能只针对手机。但是,如果您需要使其具有响应性,那么本文就是为您准备的!

本文中的示例使用 NativeScript-Vue,但这些技术适用于任何“风格”的 NativeScript。

在 NativeScript 中,没有构建响应式应用的“标准”方法。值得庆幸的是,NativeScript 非常灵活且强大,我们可以通过多种方式来实现我们的目标。让我们深入了解其中一些技术。

使用 CSS

事实上,NativeScript 的 CSS 没有提供等同于媒体查询的功能。但是,我们拥有非常接近的东西:插件nativescript-platform-css。让我们看看它将如何帮助我们。

您需要使用以下命令添加插件:

tns plugin add nativescript-platform-css

然后在app.js中用以下代码初始化它:

require( "nativescript-platform-css" );

首先,我们必须了解此插件的作用:它根据平台和屏幕尺寸向我们的应用添加顶级 CSS 类。其中一些类是

  • .androidXXX.iosXXX,其中XXX可以是 1280、1024、800、600、540、480、400、360 或 320。
  • .phone.tablet,根据设备类型。

此插件具有更多功能和自定义选项。请查看此处

考虑一下这个假想食谱应用的登录屏幕

1-login-before 

这是屏幕的主要模板

<FlexboxLayout alignItems="stretch" flexDirection="column">
  <Image marginTop="46" width="250" height="183" alignSelf="center" src="res://loginlogo"/>
  <Label flexGrow="1"/>
  <Button class="social-login fb" text="Log in with Facebook"/>
  <Button class="social-login google" text="Log in with Google"/>
</FlexboxLayout>


它在手机上看起来不错,但在平板电脑上就不太理想了,尤其是在横向模式下。这里的问题是按钮太宽了。要解决此问题,我们可以添加 CSS 样式来限制按钮宽度,并将其范围限定为.tablet,以便它仅应用于平板电脑,如下所示

.tablet button.social-login {
  align-self: center;
  width: 400;
}


我们将按钮与(FlexLayout)[https://docs.nativescript.cn/ui/layouts/layout-containers#flexboxlayout] 的中心对齐,并为其指定了 400dpi 的固定宽度。现在好多了

2-login-after 

这是一个简单的示例,但它展示了许多可能性。如果您习惯使用 CSS 媒体查询,那么此技术非常适合您。这都要感谢nativescript-platform-css插件!

动态更改 GridLayout

强大的<GridLayout>也可以帮助我们使应用具有响应性。这是因为当我们在运行时更改其结构时,此布局会做出很好的响应。我们可以利用这一点,根据屏幕宽度重新排列布局。

查看我们在假想食谱应用中使用<GridLayout>实现的食谱屏幕

3-grid-layout-before 

在横向平板电脑上,文本在水平方向上拉伸得太多。我们可以通过将配料与制作说明并排放置,更好地利用可用的屏幕宽度。横向网格与纵向网格的区别在于,我们少了一行,因为配料与说明位于同一行。让我们看看如何做到这一点。

首先,我们将<GridLayout>的行设置为响应式,以及网格单元格的rowcolcolSpan,这些单元格会更改其位置。模板最终变成了这样

<GridLayout
  columns="*, *, *, *"
  :rows="gridLayout.rows"
  ref="layout"
  @layoutChanged="updateLayout"
  >

(...)

  <StackLayout
    :row="gridLayout.ingredients.row"
    :colSpan="gridLayout.ingredients.colSpan"
    textWrap="true"
    id="ingredientsText"
    ref="ingredientsText"
  >
    <!-- Ingredients text added here -->
  </StackLayout>

  <StackLayout
    :row="gridLayout.instructions.row"
    :col="gridLayout.instructions.col"
    colSpan="4"
    textWrap="true"
    id="instructionsText"
    ref="instructionsText"
  >
    <!-- Recipe text added here -->
  </StackLayout>

  (...)

</GridLayout>

(...)


我们在组件的数据中添加了一个gridLayout对象,以便井然有序地管理这些布局更改

data() {
  return {
    gridLayout: {
      rows: "40, auto, auto, 40, auto",
      ingredients: {
        row: 1,
        colSpan: 4
      },
      instructions: {
        row: 2,
        col: 0
      }
    },

(...)


gridLayout数据默认情况下在纵向模式下初始化。然后,我们创建了一个方法updateLayout(),如果空间足够,则将此对象的值更改为横向模式。请看

updateLayout() {
  const width = utils.layout.toDeviceIndependentPixels(
    this.$refs.layout.nativeView.getMeasuredWidth()
  );

  if (width < 1000) {
    this.gridLayout = {
      rows: "40, auto, auto, 40, auto",
      ingredients: {
        row: 1,
        colSpan: 4
      },
      instructions: {
        row: 2,
        col: 0
      }
    };
  } else {
    this.gridLayout = {
      rows: "40, auto, 40, auto",
      ingredients: {
        row: 1,
        colSpan: 1
      },
      instructions: {
        row: 1,
        col: 1
      }
    };
  }
}


在此函数中,我们首先使用getMeasuredWidth()获取<GridLayout>的宽度。此值可能以像素为单位,因此我们必须使用函数toDeviceIndependentPixels()将其转换为设备无关像素 (DIP)。

警告!函数utils.layout.toDeviceIndependentPixels()来自 NativeScript utils 包,因此您需要使用以下代码导入它:import * as utils from "utils/utils";

然后,如果屏幕宽度大于 1000 DPI,我们将通过更改gridLayout数据对象来重新排列网格布局。

另一个重要的细节是我们将方法updateLayout()附加到@layoutChanged事件。每当重新计算<GridLayout>布局时,此事件就会触发,这发生在第一次渲染布局时,以及屏幕方向发生变化时。没错,布局会自动调整以适应屏幕旋转!

我们选择 1000 DPI 作为断点,因为横向平板电脑大约从这里开始。设备差异很大,但以下近似值可能有助于您确定断点

  • 纵向模式下的手机宽度约为 350 至 450 DPI;
  • 横向模式下的手机宽度约为 600 至 800 DPI;
  • 纵向模式下的平板电脑宽度约为 700 至 900 DPI;
  • 横向模式下的平板电脑宽度约为 1000 至 1200 DPI;

此技术看起来很复杂,但肯定比重新实现平板电脑的布局要好。此外,<GridLayout>的性能完美无瑕:没有明显的“布局跳动”、屏幕闪烁或延迟。亲眼看看

hubby_chef 

RadListView 网格布局

<RadListView>是一个功能丰富的组件,它提供了许多比<ListView>更好的改进。其中一项功能是能够使用网格布局而不是堆叠布局。那么,此功能将如何帮助我们使应用具有响应性?好吧,根据文档,我们可以使用参数gridSpanCount轻松定义列数。

这就是响应式网格列表视图在我们的应用中的样子

 5-rad-list-view


请注意,在手机上,网格有两列,在纵向平板电脑上,网格有四列,在横向平板电脑上,网格有五列。这是组件的代码

<template>
  <RadListView
    for="recipe in recipes"
    layout="grid"
    itemHeight="200"
    :gridSpanCount="gridColumns"
    ref="layout"
    @layoutChanged="updateLayout">
    <v-template>
      <RecipeCard :recipe="recipe"/>
    </v-template>
  </RadListView>
</template>

<script>
import * as utils from "utils/utils";
import RecipeCard from "../components/RecipeCard";

export default {
  props: ["recipes"],
  components: { RecipeCard },
  data() {
    return {
      gridColumns: 4
    };
  },
  methods: {
    updateLayout() {
      const width = utils.layout.toDeviceIndependentPixels(
        this.$refs.layout.nativeView.getMeasuredWidth()
      );

      this.gridColumns = parseInt(width / 180);
    }
  }
};
</script>


“响应式魔法”发生在gridSpanCount属性中。我们将列数设置为响应变量gridColumns。然后,我们在updateLayout方法中更改了gridColumns变量。我们使用了上一节中的方法来获取可用的宽度。这也意味着布局会自动调整以适应屏幕旋转。我们将总宽度除以 180,以允许卡片的宽度在 180 到 250 之间变化,因此它们在每个设备上看起来都大致是方形的。

结论

在本文中,我们以三种不同的方式实现了“响应性”。这些技术将使您能够使用最少的代码,使应用在更宽的屏幕(如平板电脑)上看起来更美观。

第一种技术使用 CSS,这使得它对经验丰富的 Web 开发人员很有吸引力。其他方法使用了(并滥用了)NativeScript 的响应式系统,展示了此框架的延展性。只要有创意,我们就可以做到任何事情

可能还有上千种不同的方法可以使应用具有响应性。您是否使用过其他方法?我非常乐意在评论中听到您的想法!