Quantcast
Viewing all articles
Browse latest Browse all 5930

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 就做好了。

Image may be NSFW.
Clik here to view.

开始

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

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

Image may be NSFW.
Clik here to view.

要了解 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 模式对于编写会变化的布局来说也是必须的。

Image may be NSFW.
Clik here to view.

天气图片

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 菜单:

Image may be NSFW.
Clik here to view.

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

Image may be NSFW.
Clik here to view.

在 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 将内容替换成:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="color_primary">#9B26AF</color>
  <color name="color_primary_dark">#89229b</color>
  <color name="text_color_primary">#ffffff</color>
  <color name="forecast_grid_background">#89bef2</color>
</resources>

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

Image may be NSFW.
Clik here to view.

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

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <item name="weather_icon" type="fraction">33%</item>
</resources>

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

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

<?xml version="1.0" encoding="utf-8"?>
<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 面板中,你会看到完美对齐的天气图标:

Image may be NSFW.
Clik here to view.

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

主 Activity

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

<?xml version="1.0" encoding="utf-8"?>
<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 部署到设备或模拟器。

Image may be NSFW.
Clik here to view.

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

Image may be NSFW.
Clik here to view.

刷新天气数据

看一下 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<String> 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。点击一个城市,注意天气数据发生了变化:

Image may be NSFW.
Clik here to view.

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

创建响应式 UI:横向布局

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

Image may be NSFW.
Clik here to view.

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

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

Image may be NSFW.
Clik here to view.

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

布局限定符

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

Image may be NSFW.
Clik here to view.

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

Image may be NSFW.
Clik here to view.

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

<?xml version="1.0" encoding="utf-8"?>
<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>

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

Image may be NSFW.
Clik here to view.

不错!我们在这个 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 限定符:

Image may be NSFW.
Clik here to view.

在这个文件中添加:

<item name="weather_icon" type="fraction">49%</item>

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

Image may be NSFW.
Clik here to view.

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

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

超大布局

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

Image may be NSFW.
Clik here to view.

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

Image may be NSFW.
Clik here to view.

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

<item name="weather_icon" type="fraction">16%</item>

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

Image may be NSFW.
Clik here to view.

配置计算

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

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

Image may be NSFW.
Clik here to view.

如果你对于 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 库

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

作者:kmyhy 发表于2017/7/20 10:29:05 原文链接
阅读:206 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles