采用 C# 的本机视图

可以从使用 C# 创建的 Xamarin.Forms 页面中直接引用 iOS、Android 和 UWP 的本机视图。 本文演示了如何在使用 C# 创建的 Xamarin.Forms 布局中添加本机视图,以及如何替代自定义视图的布局以纠正其度量 API 的使用。

概述

任何允许设置 Content 或拥有 Children 集合的 Xamarin.Forms 控件都可以添加特定于平台的视图。 例如,iOS UILabel 可以直接添加到 ContentView.Content 属性或 StackLayout.Children 集合中。 但是请注意,此功能需要使用在 Xamarin.Forms 共享项目解决方案中定义的 #if,Xamarin.Forms .NET Standard 库解决方案中不提供该功能。

以下屏幕截图演示了已添加到 Xamarin.FormsStackLayout 的特定于平台的视图:

包含特定于平台的视图的 StackLayout

向 Xamarin.Forms 布局添加特定于平台的视图的功能由每个平台上的两种扩展方法启用:

  • Add - 将特定于平台的视图添加到布局的 Children 集合中。
  • ToView - 采用一个特定于平台的视图并包装为 Xamarin.FormsView,可以将其设置为控件的 Content 属性。

在 Xamarin.Forms 共享项目中使用这些方法,需要导入相应的特定于平台的 Xamarin.Forms 命名空间:

  • iOS – Xamarin.Forms.Platform.iOS
  • Android – Xamarin.Forms.Platform.Android
  • 通用 Windows 平台 (UWP) – Xamarin.Forms.Platform.UWP

在每个平台上添加特定于平台的视图

以下部分演示如何在每个平台上将特定于平台的视图添加到 Xamarin.Forms 布局。

iOS

以下代码示例演示如何将 UILabel 添加到 StackLayoutContentView

var uiLabel = new UILabel {
  MinimumFontSize = 14f,
  Lines = 0,
  LineBreakMode = UILineBreakMode.WordWrap,
  Text = originalText,
};
stackLayout.Children.Add (uiLabel);
contentView.Content = uiLabel.ToView();

该示例假定以前已在 XAML 或 C# 中创建 stackLayoutcontentView 实例。

Android

以下代码示例演示如何将 TextView 添加到 StackLayoutContentView

var textView = new TextView (MainActivity.Instance) { Text = originalText, TextSize = 14 };
stackLayout.Children.Add (textView);
contentView.Content = textView.ToView();

该示例假定以前已在 XAML 或 C# 中创建 stackLayoutcontentView 实例。

通用 Windows 平台

以下代码示例演示如何将 TextBlock 添加到 StackLayoutContentView

var textBlock = new TextBlock
{
    Text = originalText,
    FontSize = 14,
    FontFamily = new FontFamily("HelveticaNeue"),
    TextWrapping = TextWrapping.Wrap
};
stackLayout.Children.Add(textBlock);
contentView.Content = textBlock.ToView();

该示例假定以前已在 XAML 或 C# 中创建 stackLayoutcontentView 实例。

替代自定义视图的平台度量

每个平台上的自定义视图通常仅针对为其设计的布局方案正确实现了度量。 例如,自定义视图可能设计为只占用设备可用宽度的一半。 不过,在与其他用户共享后,自定义视图可能需要占用设备的全部可用宽度。 因此,在 Xamarin.Forms 布局中重复使用时,有必要替代自定义视图度量实现。 因此,AddToView 扩展方法提供了替代,支持指定度量委托,当自定义视图布局添加到 Xamarin.Forms 布局时,可以替代自定义视图布局。

以下各部分将演示如何替代自定义视图的布局,以纠正其度量 API 的使用。

iOS

下面的代码示例显示了继承自 UILabelCustomControl 类:

public class CustomControl : UILabel
{
  public override string Text {
    get { return base.Text; }
    set { base.Text = value.ToUpper (); }
  }

  public override CGSize SizeThatFits (CGSize size)
  {
    return new CGSize (size.Width, 150);
  }
}

此视图的实例将添加到 StackLayout,如以下代码示例所示:

var customControl = new CustomControl {
  MinimumFontSize = 14,
  Lines = 0,
  LineBreakMode = UILineBreakMode.WordWrap,
  Text = "This control has incorrect sizing - there's empty space above and below it."
};
stackLayout.Children.Add (customControl);

但是,由于 CustomControl.SizeThatFits 替代总是返回 150 的高度,因此视图显示时上下会留有空白,如下面的屏幕截图所示:

具有错误的 SizeThatFits 实现的 iOS CustomControl

此问题的解决方案是提供 GetDesiredSizeDelegate 实现,如以下代码示例所示:

SizeRequest? FixSize (NativeViewWrapperRenderer renderer, double width, double height)
{
  var uiView = renderer.Control;

  if (uiView == null) {
    return null;
  }

  var constraint = new CGSize (width, height);

  // Let the CustomControl determine its size (which will be wrong)
  var badRect = uiView.SizeThatFits (constraint);

  // Use the width and substitute the height
  return new SizeRequest (new Size (badRect.Width, 70));
}

此方法使用 CustomControl.SizeThatFits 方法提供的宽度,但将高度 150 替换为高度 70。 当 CustomControl 实例添加到 StackLayout 中时,FixSize 方法可以指定为 GetDesiredSizeDelegate,以修复 CustomControl 类提供的不良度量:

stackLayout.Children.Add (customControl, FixSize);

这样就可以正确显示自定义视图,而不会出现上下空白的情况,如下面的屏幕截图所示:

具有 GetDesiredSize 重写的 iOS CustomControl

Android

下面的代码示例显示了继承自 TextViewCustomControl 类:

public class CustomControl : TextView
{
  public CustomControl (Context context) : base (context)
  {
  }

  protected override void OnMeasure (int widthMeasureSpec, int heightMeasureSpec)
  {
    int width = MeasureSpec.GetSize (widthMeasureSpec);

    // Force the width to half of what's been requested.
    // This is deliberately wrong to demonstrate providing an override to fix it with.
    int widthSpec = MeasureSpec.MakeMeasureSpec (width / 2, MeasureSpec.GetMode (widthMeasureSpec));

    base.OnMeasure (widthSpec, heightMeasureSpec);
  }
}

此视图的实例将添加到 StackLayout,如以下代码示例所示:

var customControl = new CustomControl (MainActivity.Instance) {
  Text = "This control has incorrect sizing - it doesn't occupy the available width of the device.",
  TextSize = 14
};
stackLayout.Children.Add (customControl);

但是,由于 CustomControl.OnMeasure 替代总是返回所请求宽度的一半,因此显示的视图将只占用设备可用宽度的一半,如以下屏幕截图所示:

具有错误的 OnMeasure 实现的 Android CustomControl

此问题的解决方案是提供 GetDesiredSizeDelegate 实现,如以下代码示例所示:

SizeRequest? FixSize (NativeViewWrapperRenderer renderer, int widthConstraint, int heightConstraint)
{
  var nativeView = renderer.Control;

  if ((widthConstraint == 0 && heightConstraint == 0) || nativeView == null) {
    return null;
  }

  int width = Android.Views.View.MeasureSpec.GetSize (widthConstraint);
  int widthSpec = Android.Views.View.MeasureSpec.MakeMeasureSpec (
    width * 2, Android.Views.View.MeasureSpec.GetMode (widthConstraint));
  nativeView.Measure (widthSpec, heightConstraint);
  return new SizeRequest (new Size (nativeView.MeasuredWidth, nativeView.MeasuredHeight));
}

此方法使用 CustomControl.OnMeasure 方法提供的宽度,但将其乘以 2。 当 CustomControl 实例添加到 StackLayout 中时,FixSize 方法可以指定为 GetDesiredSizeDelegate,以修复 CustomControl 类提供的不良度量:

stackLayout.Children.Add (customControl, FixSize);

这样,自定义视图就能正确显示,占用设备的宽度,如以下屏幕截图所示:

具有自定义 GetDesiredSize 委托的 Android CustomControl

通用 Windows 平台

下面的代码示例显示了继承自 PanelCustomControl 类:

public class CustomControl : Panel
{
  public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register(
      "Text", typeof(string), typeof(CustomControl), new PropertyMetadata(default(string), OnTextPropertyChanged));

  public string Text
  {
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value.ToUpper()); }
  }

  readonly TextBlock textBlock;

  public CustomControl()
  {
    textBlock = new TextBlock
    {
      MinHeight = 0,
      MaxHeight = double.PositiveInfinity,
      MinWidth = 0,
      MaxWidth = double.PositiveInfinity,
      FontSize = 14,
      TextWrapping = TextWrapping.Wrap,
      VerticalAlignment = VerticalAlignment.Center
    };

    Children.Add(textBlock);
  }

  static void OnTextPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
  {
    ((CustomControl)dependencyObject).textBlock.Text = (string)args.NewValue;
  }

  protected override Size ArrangeOverride(Size finalSize)
  {
      // This is deliberately wrong to demonstrate providing an override to fix it with.
      textBlock.Arrange(new Rect(0, 0, finalSize.Width/2, finalSize.Height));
      return finalSize;
  }

  protected override Size MeasureOverride(Size availableSize)
  {
      textBlock.Measure(availableSize);
      return new Size(textBlock.DesiredSize.Width, textBlock.DesiredSize.Height);
  }
}

此视图的实例将添加到 StackLayout,如以下代码示例所示:

var brokenControl = new CustomControl {
  Text = "This control has incorrect sizing - it doesn't occupy the available width of the device."
};
stackLayout.Children.Add(brokenControl);

但是,由于 CustomControl.ArrangeOverride 替代总是返回所请求宽度的一半,因此视图将被裁剪为设备可用宽度的一半,如以下屏幕截图所示:

具有错误的 ArrangeOverride 实现的 UWP CustomControl

此问题的解决方案是在将视图添加到 StackLayout 时提供 ArrangeOverrideDelegate 实现,如以下代码示例所示:

stackLayout.Children.Add(fixedControl, arrangeOverrideDelegate: (renderer, finalSize) =>
{
    if (finalSize.Width <= 0 || double.IsInfinity(finalSize.Width))
    {
        return null;
    }
    var frameworkElement = renderer.Control;
    frameworkElement.Arrange(new Rect(0, 0, finalSize.Width * 2, finalSize.Height));
    return finalSize;
});

此方法使用 CustomControl.ArrangeOverride 方法提供的宽度,但将其乘以 2。 这样,自定义视图就能正确显示,占用设备的宽度,如以下屏幕截图所示:

具有 ArrangeOverride 委托的 UWP CustomControl

总结

本文说明了如何在使用 C# 创建的 Xamarin.Forms 布局中添加本机视图,以及如何替代自定义视图的布局以纠正其度量 API 的使用。