处理旋转

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

概述

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

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

  • 声明性布局旋转 – 如何使用 Android 资源系统生成方向感知应用程序,包括如何加载特定方向的布局和可绘制对象。

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

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

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

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

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

布局资源

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

默认项目模板结构

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

<?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 文件被膨胀,如以下屏幕截图所示:

横向方向的同一屏幕

方向特定的布局

除了布局文件夹(默认为纵向,也可以通过包括名为 layout-land 的文件夹)实现布局端口显式命名),应用程序可以定义其在横向环境中需要的视图,而无需修改任何代码。

假设 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>

如果名为 layout-land 的文件夹包含另一个 Main.axml 文件,且被添加到项目中,则膨胀横向的布局现在将导致 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/drawableResources/drawable-land 文件夹中获取绘图器。

例如,假设项目在 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 的不同版本包含在 Resources/drawable-land 下。 与布局文件一样,当设备旋转时,给定方向的绘图器也会更改,如下所示:

纵向和横向模式中显示的不同版本的 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 (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 本身,因为当方向更改时,不会重新创建活动。 代码仍使用 OnCreate 中的 surfaceOrientartion 来设置 TextView 的初始布局。 此后,OnConfigurationChanged 会处理所有后续布局更改。

运行应用程序时,Android 会在设备轮换时加载用户界面更改,并且不会重启活动。

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

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

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

在方向更改期间保持状态

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

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

总结

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