处理旋转Handling Rotation

本主题介绍如何处理 Xamarin 中的设备方向更改。其中介绍了如何使用 Android 资源系统自动为特定设备方向加载资源,以及如何以编程方式处理方向更改。This topic describes how to handle device orientation changes in Xamarin.Android. It covers how to work with the Android resource system to automatically load resources for a particular device orientation as well as how to programmatically handle orientation changes.

概述Overview

因为移动设备易于旋转,所以内置旋转是移动操作系统中的一项标准功能。Because mobile devices are easily rotated, built-in rotation is a standard feature in mobile OSes. Android 提供了一个用于处理应用程序内的旋转的复杂框架,无论用户界面是在 XML 中以声明方式创建还是以编程方式在代码中创建。Android provides a sophisticated framework for dealing with rotation within applications, whether the user interface is created declaratively in XML or programmatically in code. 当自动处理旋转设备上的声明性布局更改时,应用程序可以从与 Android 资源系统紧密集成中获益。When automatically handling declarative layout changes on a rotated device, an application can benefit from the tight integration with the Android resource system. 对于编程布局,必须手动处理更改。For programmatic layout, changes must be handled manually. 这样,就可以更好地控制运行时,但会给开发人员提供更多的工作。This allows finer control at runtime, but at the expense of more work for the developer. 应用程序还可以选择退出活动重启并进行手动控制方向更改。An application can also choose to opt out of the Activity restart and take manual control of orientation changes.

本指南将探讨以下方向主题:This guide examines the following orientation topics:

  • 声明性布局旋转– 如何使用 Android 资源系统来生成可感知方向的应用程序,包括如何加载布局和特定方向的绘图。Declarative Layout Rotation – How to use the Android resource system to build orientation-aware applications, including how to load both layouts and drawables for particular orientations.

  • 编程布局旋转– 如何以编程方式添加控件,以及如何手动处理方向更改。Programmatic Layout Rotation – How to add controls programmatically as well as how to handle orientation changes manually.

用布局以声明方式处理旋转Handling Rotation Declaratively with Layouts

通过在遵循命名约定的文件夹中包含文件,Android 会在方向更改时自动加载相应的文件。By including files in folders that follow naming conventions, Android automatically loads the appropriate files when the orientation changes. 这包括对以下内容的支持:This includes support for:

  • 布局资源– 指定为每个方向放大的布局文件。Layout Resources – Specifying which layout files are inflated for each orientation.

  • 绘制资源– 指定为每个方向加载的绘图。Drawable Resources – Specifying which drawables are loaded for each orientation.

布局资源Layout Resources

默认情况下,资源/布局文件夹中包含的 Android XML (main.axml)文件用于呈现活动的视图。By default, Android XML (AXML) files included in the Resources/layout folder are used for rendering views for an Activity. 如果没有专门为横向提供的其他布局资源,则此文件夹的资源用于纵向和横向方向。This folder's resources are used for both portrait and landscape orientation if no additional layout resources are provided specifically for landscape. 考虑由默认的项目模板创建的项目结构:Consider the project structure created by the default project template:

默认项目模板结构Default project template structure

此项目将在资源/布局文件夹中创建一个main.axml文件。This project creates a single Main.axml file in the Resources/layout folder. 调用活动的 OnCreate 方法时,它会增加 main.axml 中定义的视图 该视图将声明一个按钮,如下图所示:When the Activity's OnCreate method is called, it inflates the view defined in Main.axml, which declares a button as shown in the XML below:

<?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>

如果设备旋转到横向方向,则会再次调用活动的 OnCreate 方法,并对main.axml文件进行膨胀,如以下屏幕截图所示:If the device is rotated to landscape orientation, the Activity's OnCreate method is called again and the same Main.axml file is inflated, as shown in the screenshot below:

相同的屏幕,但在横向方向上Same screen but in landscape orientation

特定于方向的布局Orientation-Specific Layouts

除了布局文件夹(默认为纵向,还可以通过包含名为 layout-land的文件夹显式命名布局端口),应用程序可在无需任何代码更改的情况下,定义其所需的视图。In addition to the layout folder (which defaults to portrait and can also be explicitly named layout-port by including a folder named layout-land), an application can define the views it needs when in landscape without any code changes.

假设main.axml文件包含以下 XML:Suppose the Main.axml file contained the following 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 的文件夹添加到项目中,则在横向的情况下,因为这样做布局现在会导致 Android 加载新添加的main.axml。If a folder named layout-land that contains an additional Main.axml file is added to the project, inflating the layout when in landscape will now result in Android loading the newly added Main.axml. 请考虑包含以下代码的main.axml文件的横向版本(为了简单起见,此 XML 类似于默认的纵向版本代码,但在 TextView中使用了不同的字符串):Consider the landscape version of the Main.axml file that contains the following code (for simplicity, this XML is similar to the default portrait version of the code, but uses a different string in the 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 加载,如下所示:Running this code and rotating the device from portrait to landscape demonstrates the new XML loading, as shown below:

纵向和横向屏幕快照打印纵向模式Portrait and landscape screenshots printing the portrait mode

绘制资源Drawable Resources

旋转时,Android 将可绘制资源视为布局资源。During rotation, Android treats drawable resources similarly to layout resources. 在这种情况下,系统将分别从资源/ 每个 "绘图" 和 "资源"-土地文件夹中获取。In this case, the system gets the drawables from the Resources/drawable and Resources/drawable-land folders, respectively.

例如,假设项目在资源/可绘制文件夹中包含一个名为 "猴子" 的映像,其中的可绘制从 XML 中的 ImageView 引用,如下所示:For example, say the project includes an image named Monkey.png in the Resources/drawable folder, where the drawable is referenced from an ImageView in XML like this:

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

接下来,我们将假定不同版本的猴子。 png包含在资源/可绘制-土地下。Let's further assume that a different version of Monkey.png is included under Resources/drawable-land. 与布局文件类似,当设备旋转时,为给定方向进行的可绘制更改,如下所示:Just like with the layout files, when the device is rotated, the drawable changes for the given orientation, as shown below:

不同版本的猴子,以纵向模式和横向模式显示Different version of Monkey.png shown in portrait and landscape modes

以编程方式处理旋转Handling Rotation Programmatically

有时我们在代码中定义布局。Sometimes we define layouts in code. 出现这种情况的原因有多种,包括技术限制、开发人员首选项等。以编程方式添加控件时,应用程序必须手动考虑设备方向,当我们使用 XML 资源时,它会自动处理。This can happen for a variety of reasons, including technical limitations, developer preference, etc. When we add controls programmatically, an application must manually account for device orientation, which is handled automatically when we use XML resources.

在代码中添加控件Adding Controls in Code

若要以编程方式添加控件,应用程序需要执行以下步骤:To add controls programmatically, an application needs to perform the following steps:

  • 创建布局。Create a layout.
  • 设置布局参数。Set layout parameters.
  • 创建控件。Create controls.
  • 设置控件布局参数。Set control layout parameters.
  • 将控件添加到布局。Add controls to the layout.
  • 将布局设置为内容视图。Set the layout as the content view.

例如,假设有一个用户界面,该用户界面由添加到 RelativeLayout的单个 TextView 控件组成,如下面的代码所示。For example, consider a user interface consisting of a single TextView control added to a RelativeLayout, as shown in the following code.

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 属性。This code creates an instance of a RelativeLayout class and sets its LayoutParameters property. LayoutParams 类是 Android 方法,它封装了如何以可重用的方式定位控件。The LayoutParams class is Android's way of encapsulating how controls are positioned in a reusable way. 创建布局的实例后,可以创建控件并将其添加到其中。Once an instance of a layout is created, controls can be created and added to it. 控件也具有 LayoutParameters,如本示例中的 TextViewControls also have LayoutParameters, such as the TextView in this example. 创建 TextView 后,将其添加到 RelativeLayout 并将 RelativeLayout 设置为内容视图会导致应用程序显示 TextView,如下所示:After the TextView is created, adding it to the RelativeLayout and setting the RelativeLayout as the content view results in the application displaying the TextView as shown:

纵向和横向模式下显示递增计数器按钮Increment counter button shown in both portrait and landscape modes

在代码中检测方向Detecting Orientation in Code

如果应用程序在调用 OnCreate 时尝试为每个方向加载不同的用户界面(这将在每次设备旋转时出现),则它必须检测方向,然后加载所需的用户界面代码。If an application tries to load a different user interface for each orientation when OnCreate is called (this will happen each time a device is rotated), it must detect the orientation, and then load the desired user interface code. Android 具有称为 WindowManager的类,可用于通过 WindowManager.DefaultDisplay.Rotation 属性确定当前设备旋转,如下所示:Android has a class called the WindowManager, which can be used to determine the current device rotation via the WindowManager.DefaultDisplay.Rotation property, as shown below:

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像素,在旋转到横向时,会自动以新布局为动画显示,如下所示:This code sets the TextView to be positioned 100 pixels from the top left of the screen, automatically animating to the new layout, when rotated to landscape, as shown here:

跨纵向模式和横向模式保留的视图状态View state is preserved across portrait and landscape modes

阻止活动重新启动Preventing Activity Restart

除了处理 OnCreate中的所有内容外,应用程序还可以通过在 ActivityAttribute 设置 ConfigurationChanges 来防止在方向更改时重新启动活动,如下所示:In addition to handling everything in OnCreate, an application can also prevent an Activity from being restarted when the orientation changes by setting ConfigurationChanges in the ActivityAttribute as follows:

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

现在设备旋转后,不会重新启动活动。Now when the device is rotated, the Activity is not restarted. 为了在此情况下手动处理方向更改,活动可以重写 OnConfigurationChanged 方法,并确定传入的 Configuration 对象的方向,如以下活动的新实现所示:In order to manually handle the orientation change in this case, an Activity can override the OnConfigurationChanged method and determine the orientation from the Configuration object that is passed in, as in the new implementation of the Activity below:

[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 布局参数。Here the TextView's layout parameters are initialized for both landscape and portrait. 类变量与 TextView 本身一起保存参数,因为在方向更改时将不会重新创建活动。Class variables hold the parameters, along with the TextView itself, since the Activity will not be re-created when orientation changes. 代码仍使用 OnCreate 中的 surfaceOrientartion 来设置 TextView的初始布局。The code still uses the surfaceOrientartion in OnCreate to set the initial layout for the TextView. 之后 OnConfigurationChanged 处理所有后续的布局更改。After that, OnConfigurationChanged handles all subsequent layout changes.

当我们运行应用程序时,Android 会在设备轮换发生时加载用户界面更改,且不会重新启动该活动。When we run the application, Android loads the user interface changes as device rotation occurs, and does not restart the Activity.

阻止声明性布局的活动重启Preventing Activity Restart for Declarative Layouts

如果在 XML 中定义布局,还可以阻止设备旋转引起的活动重启。Activity restarts caused by device rotation can also be prevented if we define the layout in XML. 例如,如果我们想要防止活动重启(可能出于性能原因),则我们可以使用这种方法,而不需要为不同方向加载新的资源。For example, we can use this approach if we want to prevent an Activity restart (for performance reasons, perhaps) and we don't need to load new resources for different orientations.

为此,我们将遵循与编程布局一起使用的相同过程。To do this, we follow the same procedure that we use with a programmatic layout. 只需在 ActivityAttribute中设置 ConfigurationChanges,就像先前在 CodeLayoutActivity 中所做的那样。Simply set ConfigurationChanges in the ActivityAttribute, as we did in the CodeLayoutActivity earlier. 任何需要针对方向更改运行的代码都可以在 OnConfigurationChanged 方法中再次实现。Any code that does need to run for the orientation change can again be implemented in the OnConfigurationChanged method.

在方向更改期间保持状态Maintaining State During Orientation Changes

无论是以声明方式还是以编程方式处理旋转,所有 Android 应用程序都应该实现相同的方法,以便在设备方向发生变化时管理状态。Whether handling rotation declaratively or programmatically, all Android applications should implement the same techniques for managing state when device orientation changes. 管理状态很重要,因为在对 Android 设备进行旋转时系统会重新启动正在运行的活动。Managing state is important because the system restarts a running Activity when an Android device is rotated. Android 实现此目的是为了方便地加载替代资源,如布局和绘图,这些资源是专门为特定方向设计的。Android does this to make it easy to load alternate resources, such as layouts and drawables that are designed specifically for a particular orientation. 当它重新启动时,活动将丢失它可能已存储在局部类变量中的任何暂时性状态。When it restarts, the Activity loses any transient state it may have stored in local class variables. 因此,如果活动是状态相关的,则必须在应用程序级别保持其状态。Therefore, if an Activity is state reliant, it must persist its state at the application level. 应用程序需要处理保存和还原要跨方向更改保留的任何应用程序状态。An application needs to handle saving and restoring any application state that it wants to preserve across orientation changes.

有关保存 Android 状态的详细信息,请参阅活动生命周期指南。For more information on persisting state in Android, refer to the Activity Lifecycle guide.

总结Summary

本文介绍如何使用 Android 内置功能处理旋转。This article covered how to use Android's built-in capabilities to work with rotation. 首先,它介绍了如何使用 Android 资源系统创建面向定向的应用程序。First, it explained how to use the Android resource system to create orientation aware applications. 然后,它介绍了如何在代码中添加控件,以及如何手动处理方向更改。Then it presented how to add controls in code as well as how to handle orientation changes manually.