Android响应式UI教程

原文:Responsive UI Tutorial for Android
作者:James Nocentini
译者:kmyhy

2017/5/4 更新说明: 由 James Nocentini 更新到 Android Studio 2.2.3。原文作者也是 James。

Android 运行的设备十分广泛,它们的屏幕尺寸和分辨率都不一样。因此,Android app 能够拥有适应各种屏幕的响应式 UI 就显得非常重要。Android 平台很早以来就提供了非常强大的设计响应式 UI 的抽象层,即所谓的自适应布局。

本文是《Android 自适应 UI 教程》的一次升级,本教程中演示了如何用 fragmentation 构建跨设备的 app。这里,你将学习到:

  • 设置限定符
  • 可变布局和 drawable
  • 使 Android Studio 的布局预览——一个非常有用的工具

一个不爱折腾的教程不是好教程。因此,我们将从头开始编写一个简单天气 app 的用户界面。当你做完这一切,屏幕中会用三种不同的配置来显示一张图片、几个文字标签和一张地图。一个看起来酷酷的拥有响应式 UI 的 app 就做好了。

Android响应式UI教程_第1张图片

开始

从此处下载开始项目 Adaptive Weather,用 Android Studio 打开它。然后编译、运行。

app 会显示一个简单的 RecyclerView,列出了几个城市。

Android响应式UI教程_第2张图片

要了解 RecyclerViews,建议阅读我们的Android RecyclerView 教程。

打开 build.gradle 声明下列依赖:

dependencies {
    ...
    compile 'com.google.android:flexbox:0.2.5'
}

谷歌的 FlexBox 为 Android 平台提供了一种弹性盒子的实现。在后面你会看到,对于设计响应式布局来说这是非常有用的工具。它和 Android 的资源限定符系统一起使用,更是威力倍增!

注意:Android 平台更新频繁,当这篇教程发布时,版本号很可能又有增加。你可以查看不同版本的细节,比如去 Android 开发者网站查看最新的支持库页面。

在教程中,你经常需要在项目导航器中在 Android 和 Project 模式间来回切换。通俗地讲:

  • Android 模式是 Android Studio 中的默认工作模式,它提供了一个干净的、简单的文件结构。
  • Project 模式对于编写会变化的布局来说也是必须的。

天气图片

Android 设备的屏幕分辨率各有不同,因此将不同的静态图片导成多种尺寸是一种比较好的做法。Android 系统 API 提供了一种方法创建响应式 UI。根据多屏幕支持指南,屏幕分辨率分成以下几种类别:

  • ldpi (low) ~120dpi
  • mdpi (medium) ~160dpi
  • hdpi (high) ~240dpi
  • xhdpi (extra-high) ~320dpi
  • xxhdpi (extra-extra-high) ~480dpi
  • xxxhdpi (extra-extra-extra-high) ~640dpi

有一些 UI 编辑器轻易就能将图片导出成各种尺寸,在本文中将演示另外一种方法。Android Studio 最近对矢量图进行了支持。也就是说,你所有的图片资源都可以只导出一张,然后在运行时根据设备配置(屏幕尺寸和方向)进行拉伸.

下载天气图片并解压。在 Android Studio 中,在 res/drawable 上右键,点击 New\Vector Asset 菜单:

Android响应式UI教程_第3张图片

Asset Type 选择 Local file(SVG、PSD)。从 Path 右边的文件浏览器中找到 weather-icon 文件夹选择第一个图片 cloud.svg。勾上 Size 设置右边的 Override,否则在后面图片会出现一些失真。点击 Next、Finish:

Android响应式UI教程_第4张图片

在 Android Studio 的 res/drawable/ic_cloud.xml 中你会看到这张图片。在其它图片上重复同样动作:fog,rain,snow,sun,thunder。

最后,在 app 模块的 build.gradle 中启用 Vector Drawable:

android {
    ...

    defaultConfig {
        ...
        vectorDrawables.useSupportLibrary = true
    }
}

现在,项目中的图片已经是可拉伸的了,准备编写布局。

编写布局

声明完依赖后,将工作转移到布局的实现上!

这个 app 只有一个页面,即 MainActivity。在项目导航器中,打开 res/layout/activity_main.xml。点击右下角的 Preview 按钮进行预览。

一个 Activity 是一个 Java 类——这里,也就是 MainActivty.java 了——外加一个布局文件。事实上,一个 activity 也可以有多个布局的,等会你就会看到。就目前来说,终点是记住这个文件 activity_main.xml 是我们的默认布局。

Forecast 网格视图

首先,为主 activity 定义默认布局。打开 res/values/colors.xml 将内容替换成:


<resources>
  <color name="color_primary">#9B26AFcolor>
  <color name="color_primary_dark">#89229bcolor>
  <color name="text_color_primary">#ffffffcolor>
  <color name="forecast_grid_background">#89bef2color>
resources>

这里,我们将 forecast 网格的默认的材料设计的主题色改成了另外一个背景色。然后,在 values 文件夹上右键,选择 New\Value resource file :

Android响应式UI教程_第5张图片

文件名取名为 fractions.xml,输入如下内容:


<resources>
  <item name="weather_icon" type="fraction">33%item>
resources>

这里,我们指定了每个图标的宽度应当是整个宽度的 1/3。

然后,创建一个新的布局文件,名为 forecast_grid.xml,在其中添加:


<com.google.android.flexbox.FlexboxLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/forecast"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/forecast_grid_background"
    app:alignItems="center"
    app:flexWrap="wrap"
    app:justifyContent="space_around">

  <android.support.v7.widget.AppCompatImageView
      android:id="@+id/day1"
      android:layout_width="wrap_content"
      android:layout_height="60dp"
      app:layout_flexBasisPercent="@fraction/weather_icon"
      app:srcCompat="@drawable/ic_thunder"/>
  <android.support.v7.widget.AppCompatImageView
      android:id="@+id/day2"
      android:layout_width="wrap_content"
      android:layout_height="60dp"
      app:layout_flexBasisPercent="@fraction/weather_icon"
      app:srcCompat="@drawable/ic_fog"/>
  <android.support.v7.widget.AppCompatImageView
      android:id="@+id/day3"
      android:layout_width="wrap_content"
      android:layout_height="60dp"
      app:layout_flexBasisPercent="@fraction/weather_icon"
      app:srcCompat="@drawable/ic_rain"/>
  <android.support.v7.widget.AppCompatImageView
      android:id="@+id/day4"
      android:layout_width="wrap_content"
      android:layout_height="60dp"
      app:layout_flexBasisPercent="@fraction/weather_icon"
      app:srcCompat="@drawable/ic_snow"/>
  <android.support.v7.widget.AppCompatImageView
      android:id="@+id/day5"
      android:layout_width="wrap_content"
      android:layout_height="60dp"
      app:layout_flexBasisPercent="@fraction/weather_icon"
      app:srcCompat="@drawable/ic_cloud"/>
  <android.support.v7.widget.AppCompatImageView
      android:id="@+id/day6"
      android:layout_width="wrap_content"
      android:layout_height="60dp"
      app:layout_flexBasisPercent="@fraction/weather_icon"
      app:srcCompat="@drawable/ic_sun"/>

com.google.android.flexbox.FlexboxLayout>

这里有几个地方值得注意:

  1. 我们将使用 com.google.android.flexbox.FlexboxLayout 标签来布局屏幕上的图标。
  2. 我们将使用 android.support.v7.widget.AppCompatImageView 标签来绘制天气图片。对于一般的图片(.png、.jpg),我们通常会使用 ImageView 标签,但对于矢量图片,我们必须换成前者。

在 Preview 面板中,你会看到完美对齐的天气图标:

Android响应式UI教程_第6张图片

已经尝到了响应式的滋味了吧?不需要用 margin 来控制图片的位置,或者用我们常用的相对布局,FlexBox 属性会对称地展开它们。例如,你删除中间的一个图标,剩余的图片会自动向左移动以补上空出来的地方。在布局中使用 FlexBox 的威力非常强大。 forecast 网格已经就绪,可以在主 activity 的默认布局中使用了。

主 Activity

打开 res/layout/activity_main.xml 将内容替换成:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

  <include layout="@layout/forecast_grid"
           android:layout_width="match_parent"
           android:layout_height="0dp"
           android:layout_weight="1"/>

  <android.support.v7.widget.RecyclerView
      android:id="@+id/list"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="1"/>

LinearLayout>

这个布局主要是:

  • 将 LinearLayout 的方向设置为竖向。
  • 尺寸:用 layout_weight 属性分别将两个子视图的高度设置为占据屏幕高度的一半。
  • 布局的重用:通过 include 标签,引用了 forecast_grid.xml 布局,并将 forecast 网格放在屏幕上部。这是不需要重复代码就能创建不同布局的核心能力。

注意编辑器的预览窗口立即发生了变化。很神奇吧?我们并没有将 app 部署到设备或模拟器。

Android响应式UI教程_第7张图片

运行 App,你会发现在城市列表上的天气图标。

Android响应式UI教程_第8张图片

刷新天气数据

看一下 assets/data.json 中的静态 JSON 数据。每个城市的天气数据用一个字符串数组来保存。你可以用一个 GridLayout 来创建另外一个 RecyclerView 以动态显示天气数据,但那太麻烦了。相反,你可以写一个方法,将每个可能的天气状态映射成对应的 drawable 图标。

在 MainActivity.java 中新增方法:

private Drawable mapWeatherToDrawable(String forecast) {
  int drawableId = 0;
  switch (forecast) {
    case "sun":
      drawableId = R.drawable.ic_sun;
      break;
    case "rain":
      drawableId = R.drawable.ic_rain;
      break;
    case "fog":
      drawableId = R.drawable.ic_fog;
      break;
    case "thunder":
      drawableId = R.drawable.ic_thunder;
      break;
    case "cloud":
      drawableId = R.drawable.ic_cloud;
      break;
    case "snow":
      drawableId = R.drawable.ic_snow;
      break;
  }
  return getResources().getDrawable(drawableId);
}

接下来我们编写响应 RecyclerView 单元格点击事件的代码。在 MainActivity 中新增方法:

private void loadForecast(List forecast) {
  FlexboxLayout forecastView = (FlexboxLayout) findViewById(R.id.forecast);
  for (int i = 0; i < forecastView.getChildCount(); i++) {
    AppCompatImageView dayView = (AppCompatImageView) forecastView.getChildAt(i);
    dayView.setImageDrawable(mapWeatherToDrawable(forecast.get(i)));
  }
}

然后找到 MainActivity 中的 // TODO 注释,将它替换成:

loadForecast(location.getForecast());

运行 App。点击一个城市,注意天气数据发生了变化:

Android响应式UI教程_第9张图片

不错,App 看起来很漂亮!只是旧金山的天气看起来不太好啊:]

创建响应式 UI:横向布局

我们以默认竖屏的模式创建了 App,让我们来看一下当手机转成横屏时会发生些什么。打开 activity_main.xml,在布局编辑器中点击 orientation 图标:

Android响应式UI教程_第10张图片

然后在各种 Android 设备和模拟器上运行 app。这种方法测试可变布局不仅费事,而且容易出现问题。我们应该用别的方法。

幸好,Android Studio 拥有扩展式预览功能。打开默认的 activity_main.xml,将鼠标放到屏幕右下角,然后拖动布局的大小。注意,点击手柄的时候,Android Studio 会自动显示各种设备尺寸的导线。

Android响应式UI教程_第11张图片

呃——横屏布局对你的设计来说不太友好。我们应该将两个视图并排而列。要告诉系统某个尺寸使用不同的资源,你应当将布局资源放到一个特殊命名的文件夹中。系统会自动为这个尺寸使用对应的 acitivity 布局。这样,你的 app 就变成了响应式 UI了。

布局限定符

回到 Android Studio,在 res/layout and 右键,然后点击 New\Layout resource file:

Android响应式UI教程_第12张图片

文件取名为 activity_main ,并添加 landscape 资源限定符:

Android响应式UI教程_第13张图片

布局编辑器将显示一个空白窗口,因为这是一个新建的布局文件,位于 layout-land/activity_main.xml。它只有一个空的 LinearLayout 在里面,当然很快就不是了。添加两个重用的布局 weather forecast 和 Recycler View,但这次 orientation 变成了 horizontal:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="horizontal">

  <include layout="@layout/forecast_grid" 
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

  <android.support.v7.widget.RecyclerView
      android:id="@+id/list"
      android:layout_width="0dp"
      android:layout_height="match_parent"
      android:layout_weight="1"/>

LinearLayout>

布局编辑器现在将所有元素以横向的方式显示。

Android响应式UI教程_第14张图片

不错!我们在这个 app 中第一次试用了布局限定符。还有许多别的布局限定符(比如屏幕宽度、高度、宽高比等)。在下一部分,我们将用一句代码来修改横向布局。

资源限定符

另一个改进是将天气图标从 3 列 2 行修改为 2 列 3 行。我们可以复制 forecast_grid.xml 布局,但重复的代码更难维护。每个天气图标的宽度想相对于 FlexBox view 的宽度的,这个比例是通过 layout_flexBasisPercent 属性指定:

<android.support.v7.widget.AppCompatImageView
    android:id="@+id/day1"
    android:layout_width="wrap_content"
    android:layout_height="60dp"
    app:layout_flexBasisPercent="@fraction/weather_icon"
    app:srcCompat="@drawable/ic_thunder"/>

这个值是分数类型,当前值为 33%,在资源文件 res/values/fractions.xml 中定义。用创建横向布局的方式,我们可以创建横屏设置的资源文件。在 res/values 文件夹上右键,选择 New\Values resource file。文件取名为 fractions 并添加一个 landscape 限定符:

Android响应式UI教程_第15张图片

在这个文件中添加:

49%

返回 main activity 布局,你会看到天气图标现在排成了 2 列 3 行:

Android响应式UI教程_第16张图片

OK! 你可以停下来欣赏一小会儿,你没有必要部署 app。当然,你可以 build & run,确认一切正常。

配置限定符能够使用在任何 XML 布局的属性类型上(比如字体大小、颜色、边距等)。

超大布局

在布局编辑器中,回到竖向布局,将屏幕尺寸拖到 X-Large 尺寸。

Android响应式UI教程_第17张图片

因为设备拥有更多的屏幕空间,你可以将所有天气图标放在一行显示。右键点击 res/values,然后选择 New\Values resource file。文件命名为 fractions 并添加 X-Large size 限定符:

Android响应式UI教程_第18张图片

在这个文件中,添加 XML 标签:

16%

回到布局编辑器,你会看到天气图标排成了 1 行。

Android响应式UI教程_第19张图片

配置计算

别害怕,这部分内容没有标题看起来那么可怕。当用户和 App 交互时,布局状态会随时改变(行被选中,输入字段改变等等)。当布局发生变化时(例如方向变化),现有布局被抛弃,新的布局创建。但是,系统不知道如何恢复状态,因为这两个布局完全不同,除非你告诉它。

要实际看一下这个例子,可以运行 app。选择一个城市然后改变方向,你会看到这个城市不再是被选中状态了!

Android响应式UI教程_第20张图片

如果你对于 London 整整一个星期都是大晴天感到毫不奇怪,那么当你切换到横屏后,被选中的行又变成未选中状态了。

要解决这个问题,我们要用到 activity 的生命周期方法,将所选城市保存到 bundle,然后在屏幕发生旋转后重新获取这个状态。

在 MainActivity.java 添加变量:

private static final String SELECTED_LOCATION_INDEX = "selectedLocationIndex";

然后是这个方法:

@Override
protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  outState.putInt(SELECTED_LOCATION_INDEX, mLocationAdapter.getSelectedLocationIndex());
}

在 onCreate() 方法最后加入:

if (savedInstanceState != null) {
  int index = savedInstanceState.getInt(SELECTED_LOCATION_INDEX);
  mLocationAdapter.setSelectedLocationIndex(index);
  loadForecast(mLocations.get(index).getForecast());
}

运行 app,这次在切换方向之后,选中的城市保持了选中状态!欢呼吧!

结束

太好了!你编写了自己第一个自适应布局 app,学习了如何让 activity 使用多个布局。还学习了如何让 drawable 适配不同的显示尺寸,如何使你的 app 在所有 android 设备上都能适应。

对于编写自适应 UI 来说,没有什么比本教程更好的方法了,当然,Android 除了布局还有别的,如果想学习更多内容,请参阅谷歌的 最佳 UI 体检指南 。

如果你愿意,你可以尝试如下挑战:

  • 用其它限定符来使用其它类型的布局。例如,用 locale 限定符显示不同的背景色。
  • 或者,在其它资源上添加 size 限定符,比如字符串。你可以用一个 TextView 来显示短字符串,当屏幕转变为横屏后又显示长字符串?

这里下载本教程的示例项目的源代码,也可以访问它的 Github 库。

请留下你的足迹,发现任何问题或提问请在下面留言。期望与你交流!

你可能感兴趣的:(Android,Android,Studio,入门系列)