处理旋转

本主题介绍如何在 Xamarin.Android 中处理设备方向更改。 它介绍如何使用 Android 资源系统自动加载特定设备方向的资源,以及如何以编程方式处理方向更改。

概述

由于移动设备易于旋转,因此内置旋转是移动 OS 中的一项标准功能。 Android 提供了一个复杂的框架,用于处理应用程序中的轮换,无论用户界面是在 XML 中以声明方式创建的,还是在代码中以编程方式创建的。 在旋转设备上自动处理声明性布局更改时,应用程序可以从与 Android 资源系统的紧密集成中获益。 对于编程布局,必须手动处理更改。 这允许在运行时进行更精细的控制,但代价是开发人员需要更多工作。 应用程序还可以选择退出活动重启并手动控制方向更改。

本指南介绍以下方向主题:

  • 声明性布局轮换 - 如何使用 Android 资源系统构建方向感知应用程序,包括如何加载特定方向的布局和可绘制内容。

  • 编程布局旋转 - 如何以编程方式添加控件以及如何手动处理方向更改。

使用布局以声明方式处理旋转

通过将文件包含在遵循命名约定的文件夹中,Android 会在方向更改时自动加载相应的文件。 这包括对以下内容的支持:

  • 布局资源 - 指定为每个方向膨胀哪些布局文件。

  • 可绘制资源 – 指定为每个方向加载哪些可绘制资源。

布局资源

默认情况下,Android XML (AXML) 包含在“资源/布局 ”文件夹中的文件用于呈现活动的视图。 如果没有专门为横向提供其他布局资源,则此文件夹的资源将用于纵向和横向。 请考虑默认项目模板创建的项目结构:

默认项目模板结构

此项目在 Resources/layout 文件夹中创建一个 Main.axml 文件。 调用 Activity 的 OnCreate 方法时,它会扩充 Main.axml 中定义的视图,该视图声明按钮,如下面的 XML 所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
<Button  
  android:id="@+id/myButton"
  android:layout_width="fill_parent" 
  android:layout_height="wrap_content" 
  android:text="@string/hello"/>
</LinearLayout>

如果设备旋转到横向方向,则再次调用 Activity 的 OnCreate 方法,并且相同的 Main.axml 文件将被膨胀,如以下屏幕截图所示:

相同的屏幕,但处于横向方向

Orientation-Specific布局

除了布局文件夹 (默认为纵向,也可以通过包含名为 layout-land) 的文件夹来显式命名 layout-port,应用程序还可以定义在横向时所需的视图,而无需更改任何代码。

假设 Main.axml 文件包含以下 XML:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <TextView
    android:text="This is portrait"
    android:layout_height="wrap_content"
    android:layout_width="fill_parent" />
</RelativeLayout>

如果将包含其他 Main.axml 文件的名为 layout-land 的文件夹添加到项目中,在横向布局中膨胀布局现在将导致 Android 加载新添加的 Main.axml。 为简单起见,请考虑包含以下代码的 Main.axml 文件的横向版本 (,此 XML 类似于代码的默认纵向版本,但在) 中 TextView 使用不同的字符串:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <TextView
    android:text="This is landscape"
    android:layout_height="wrap_content"
    android:layout_width="fill_parent" />
</RelativeLayout>

运行此代码并将设备从纵向旋转到横向演示了新的 XML 加载,如下所示:

纵向和横向打印纵向模式的屏幕截图

可绘制资源

在轮换期间,Android 处理可绘制资源与布局资源类似。 在这种情况下,系统分别从 “资源/可绘制 ”和 “资源/可绘制土地 ”文件夹中获取可绘制的可绘制项。

例如,假设项目在 Resources/drawable 文件夹中包含名为 Monkey.png 的图像,其中绘制对象是从 XML 中的 引用 ImageView 的,如下所示:

<ImageView
  android:layout_height="wrap_content"
  android:layout_width="wrap_content"
  android:src="@drawable/monkey"
  android:layout_centerVertical="true"
  android:layout_centerHorizontal="true" />

让我们进一步假设不同版本的 Monkey.png 包含在 “资源/可绘制土地”下。 与布局文件一样,旋转设备时,给定方向的可绘制更改,如下所示:

不同版本的Monkey.png以纵向模式和横向模式显示

以编程方式处理旋转

有时我们在代码中定义布局。 发生这种情况的原因有多种,包括技术限制、开发人员首选项等。当我们以编程方式添加控件时,应用程序必须手动考虑设备方向,当我们使用 XML 资源时会自动处理该设备方向。

在代码中添加控件

若要以编程方式添加控件,应用程序需要执行以下步骤:

  • 创建布局。
  • 设置布局参数。
  • 创建控件。
  • 设置控件布局参数。
  • 将控件添加到布局。
  • 将布局设置为内容视图。

例如,考虑由添加到 RelativeLayout的单个TextView控件组成的用户界面,如以下代码所示。

protected override void OnCreate (Bundle bundle)
{
  base.OnCreate (bundle);
                        
  // create a layout
  var rl = new RelativeLayout (this);

  // set layout parameters
  var layoutParams = new RelativeLayout.LayoutParams (ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.FillParent);
  rl.LayoutParameters = layoutParams;
        
  // create TextView control
  var tv = new TextView (this);

  // set TextView's LayoutParameters
  tv.LayoutParameters = layoutParams;
  tv.Text = "Programmatic layout";

  // add TextView to the layout
  rl.AddView (tv);
        
  // set the layout as the content view
  SetContentView (rl);
}

此代码创建类的 RelativeLayout 实例并设置其 LayoutParameters 属性。 类 LayoutParams 是 Android 封装控件以可重用方式定位的方式。 创建布局的实例后,可以创建控件并将其添加到其中。 控件还具有 LayoutParameters,如 TextView 此示例中的 。 TextView创建 后,将其添加到 并将 RelativeLayout 设置为 RelativeLayout 内容视图会导致应用程序显示 TextView ,如下所示:

纵向模式和横向模式中显示的递增计数器按钮

在代码中检测方向

如果应用程序尝试在调用 时 OnCreate 为每个方向加载不同的用户界面, (每次设备旋转) 时都会发生这种情况,它必须检测方向,然后加载所需的用户界面代码。 Android 有一个名为 的 WindowManager类,可用于通过 WindowManager.DefaultDisplay.Rotation 属性确定当前设备旋转,如下所示:

protected override void OnCreate (Bundle bundle)
{
  base.OnCreate (bundle);
                        
  // create a layout
  var rl = new RelativeLayout (this);

  // set layout parameters
  var layoutParams = new RelativeLayout.LayoutParams (ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.FillParent);
  rl.LayoutParameters = layoutParams;
                        
  // get the initial orientation
  var surfaceOrientation = WindowManager.DefaultDisplay.Rotation;
  // create layout based upon orientation
  RelativeLayout.LayoutParams tvLayoutParams;
                
  if (surfaceOrientation == SurfaceOrientation.Rotation0 || surfaceOrientation == SurfaceOrientation.Rotation180) {
    tvLayoutParams = new RelativeLayout.LayoutParams (ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent);
  } else {
    tvLayoutParams = new RelativeLayout.LayoutParams (ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent);
    tvLayoutParams.LeftMargin = 100;
    tvLayoutParams.TopMargin = 100;
  }
                        
  // create TextView control
  var tv = new TextView (this);
  tv.LayoutParameters = tvLayoutParams;
  tv.Text = "Programmatic layout";
        
  // add TextView to the layout
  rl.AddView (tv);
        
  // set the layout as the content view
  SetContentView (rl);
}

此代码将 设置为 TextView 从屏幕左上角定位 100 像素,当旋转到横向时,会自动对新布局进行动画处理,如下所示:

视图状态在纵向和横向模式下保留

阻止活动重启

除了处理 中OnCreate的所有内容外,应用程序还可以通过在 中ActivityAttribute设置ConfigurationChanges,防止在方向更改时重新启动活动,如下所示:

[Activity (Label = "CodeLayoutActivity", ConfigurationChanges=Android.Content.PM.ConfigChanges.Orientation | Android.Content.PM.ConfigChanges.ScreenSize)]

现在,当设备旋转时,活动不会重启。 为了在这种情况下手动处理方向更改,Activity 可以替代 OnConfigurationChanged 方法并确定传入对象的方向 Configuration ,如下面的 Activity 的新实现所示:

[Activity (Label = "CodeLayoutActivity", ConfigurationChanges=Android.Content.PM.ConfigChanges.Orientation | Android.Content.PM.ConfigChanges.ScreenSize)]
public class CodeLayoutActivity : Activity
{
  TextView _tv;
  RelativeLayout.LayoutParams _layoutParamsPortrait;
  RelativeLayout.LayoutParams _layoutParamsLandscape;
                
  protected override void OnCreate (Bundle bundle)
  {
    // create a layout
    // set layout parameters
    // get the initial orientation

    // create portrait and landscape layout for the TextView
    _layoutParamsPortrait = new RelativeLayout.LayoutParams (ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent);
                
    _layoutParamsLandscape = new RelativeLayout.LayoutParams (ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent);
    _layoutParamsLandscape.LeftMargin = 100;
    _layoutParamsLandscape.TopMargin = 100;
                        
    _tv = new TextView (this);
                        
    if (surfaceOrientation == SurfaceOrientation.Rotation0 || surfaceOrientation == SurfaceOrientation.Rotation180) {
      _tv.LayoutParameters = _layoutParamsPortrait;
    } else {
      _tv.LayoutParameters = _layoutParamsLandscape;
    }
                        
    _tv.Text = "Programmatic layout";
    rl.AddView (_tv);
    SetContentView (rl);
  }
                
  public override void OnConfigurationChanged (Android.Content.Res.Configuration newConfig)
  {
    base.OnConfigurationChanged (newConfig);
                        
    if (newConfig.Orientation == Android.Content.Res.Orientation.Portrait) {
      _tv.LayoutParameters = _layoutParamsPortrait;
      _tv.Text = "Changed to portrait";
    } else if (newConfig.Orientation == Android.Content.Res.Orientation.Landscape) {
      _tv.LayoutParameters = _layoutParamsLandscape;
      _tv.Text = "Changed to landscape";
    }
  }
}

此处为 TextView's 横向和纵向初始化布局参数。 类变量包含参数以及 TextView 本身,因为当方向更改时,不会重新创建 Activity。 代码仍使用 surfaceOrientartion 中的 OnCreate 来设置 的初始布局 TextView。 之后, OnConfigurationChanged 处理所有后续布局更改。

运行应用程序时,Android 会随着设备旋转而加载用户界面更改,并且不会重启活动。

阻止声明性布局的活动重启

如果我们在 XML 中定义布局,也可以阻止由设备旋转引起的活动重启。 例如,如果出于性能原因而想要阻止活动重启 ((可能) ,并且无需为不同的方向加载新资源,则可以使用此方法。

为此,我们遵循与编程布局一起使用的相同过程。 只需在 中ActivityAttribute设置 ConfigurationChanges ,就像前面一CodeLayoutActivity样。 任何需要运行方向更改的代码都可以在 方法中 OnConfigurationChanged 再次实现。

在方向更改期间维护状态

无论是以声明方式还是以编程方式处理旋转,所有 Android 应用程序都应实现相同的技术,以便在设备方向更改时管理状态。 管理状态非常重要,因为当 Android 设备轮换时,系统会重启正在运行的活动。 Android 这样做是为了方便加载备用资源,例如专为特定方向设计的布局和可绘制物。 重新启动时,活动会丢失它可能存储在本地类变量中的任何暂时性状态。 因此,如果活动依赖于状态,它必须在应用程序级别保留其状态。 应用程序需要处理要跨方向更改保留的任何应用程序状态的保存和还原。

有关在 Android 中保留状态的详细信息,请参阅 活动生命周期 指南。

总结

本文介绍如何使用 Android 的内置功能来处理旋转。 首先,它介绍了如何使用 Android 资源系统创建方向感知应用程序。 然后介绍了如何在代码中添加控件,以及如何手动处理方向更改。