使用处理程序创建自定义控件
针对应用的一项标准要求是能够播放视频。 本文介绍如何创建 .NET Multi-platform App UI (.NET MAUI) 跨平台 Video
控件,该控件使用处理程序将跨平台控件 API 映射到 Android、iOS 和 Mac Catalyst 上用于播放视频的本机视图。 此控件可以从三个源播放视频:
- 表示远程视频的 URL。
- 嵌入应用中的文件资源。
- 来自设备视频库的文件。
视频控件需要具备传输控件(即用于播放和暂停视频的按钮)和定位条(用于显示视频进度并允许用户快速跳转到其他位置)。 Video
控件可以使用平台提供的传输控件和定位条,也可以使用你提供的自定义传输控件和定位条。 以下屏幕截图展示了 iOS 上的控件,无论是否使用自定义传输控件:
更复杂的视频控件还具备其他功能,例如音量控制、来电时中断视频播放的机制以及在播放期间保持屏幕活动的方式。
Video
控件的体系结构如下图所示:
Video
类为控件提供跨平台 API。 跨平台 API 到本机视图 API 的映射由每个平台上的 VideoHandler
类执行,该类将 Video
类映射到 MauiVideoPlayer
类。 在 iOS 和 Mac Catalyst 上,MauiVideoPlayer
类使用 AVPlayer
类型来提供视频播放。 在 Android 上,MauiVideoPlayer
类使用 VideoView
类型来提供视频播放。 在 Windows 上,MauiVideoPlayer
类使用 MediaPlayerElement
类型来提供视频播放。
重要
.NET MAUI 通过接口将其处理程序与跨平台控件分离。 这使得 Comet 和 Fabulous 等实验性框架能够提供自己的跨平台控件,这些控件可实现接口,同时仍使用 .NET MAUI 的处理程序。 仅当出于类似目的或测试目的而需要将处理程序与其跨平台控件分离时,才需要为跨平台控件创建接口。
创建跨平台 .NET MAUI 自定义控件的过程(其平台实现由处理程序提供)如下所示:
- 为跨平台控件创建一个类,该类提供控件的公共 API。 有关详细信息,请参阅创建跨平台控件。
- 创建任何所需的其他跨平台类型。
- 创建
partial
处理程序类。 有关详细信息,请参阅创建处理程序。 - 在处理程序类中,创建 PropertyMapper 字典,用于定义在发生跨平台属性更改时要执行的操作。 有关详细信息,请参阅创建属性映射器。
- (可选)在处理程序类中创建 CommandMapper 字典,用于定义跨平台控件向实现跨平台控件的本机视图发送指令时要执行的操作。 有关详细信息,请参阅创建命令映射器。
- 为每个平台创建
partial
处理程序类,用于创建实现跨平台控件的本机视图。 有关详细信息,请参阅创建平台控件。 - 在应用的
MauiProgram
类中使用 ConfigureMauiHandlers 和 AddHandler 方法注册处理程序。 有关详细信息,请参阅注册处理程序。
然后,可以使用跨平台控件。 有关详细信息,请参阅使用跨平台控件。
创建跨平台控件
要创建跨平台控件,应创建派生自 View 的类:
using System.ComponentModel;
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
public static readonly BindableProperty AreTransportControlsEnabledProperty =
BindableProperty.Create(nameof(AreTransportControlsEnabled), typeof(bool), typeof(Video), true);
public static readonly BindableProperty SourceProperty =
BindableProperty.Create(nameof(Source), typeof(VideoSource), typeof(Video), null);
public static readonly BindableProperty AutoPlayProperty =
BindableProperty.Create(nameof(AutoPlay), typeof(bool), typeof(Video), true);
public static readonly BindableProperty IsLoopingProperty =
BindableProperty.Create(nameof(IsLooping), typeof(bool), typeof(Video), false);
public bool AreTransportControlsEnabled
{
get { return (bool)GetValue(AreTransportControlsEnabledProperty); }
set { SetValue(AreTransportControlsEnabledProperty, value); }
}
[TypeConverter(typeof(VideoSourceConverter))]
public VideoSource Source
{
get { return (VideoSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public bool AutoPlay
{
get { return (bool)GetValue(AutoPlayProperty); }
set { SetValue(AutoPlayProperty, value); }
}
public bool IsLooping
{
get { return (bool)GetValue(IsLoopingProperty); }
set { SetValue(IsLoopingProperty, value); }
}
...
}
}
该控件应提供一个公共 API,供其处理程序和控件使用者访问。 跨平台控件应派生自 View,它表示用于在屏幕上放置布局和视图的视觉元素。
创建处理程序
创建跨平台控件后,应为处理程序创建 partial
类:
#if IOS || MACCATALYST
using PlatformView = VideoDemos.Platforms.MaciOS.MauiVideoPlayer;
#elif ANDROID
using PlatformView = VideoDemos.Platforms.Android.MauiVideoPlayer;
#elif WINDOWS
using PlatformView = VideoDemos.Platforms.Windows.MauiVideoPlayer;
#elif (NETSTANDARD || !PLATFORM) || (NET6_0_OR_GREATER && !IOS && !ANDROID)
using PlatformView = System.Object;
#endif
using VideoDemos.Controls;
using Microsoft.Maui.Handlers;
namespace VideoDemos.Handlers
{
public partial class VideoHandler
{
}
}
处理程序类是一个分部类,其实现将在每个平台上使用附加分部类完成。
条件性 using
语句在每个平台上定义 PlatformView
类型。 在 Android、iOS、Mac Catalyst 和 Windows 上,本机视图由自定义 MauiVideoPlayer
类提供。 最终条件 using
语句将 PlatformView
定义为等于 System.Object
。 这是必要的,以便可以在处理程序中使用 PlatformView
类型,从而在所有平台上使用。 另一种方法是必须使用条件编译为每个平台定义一次 PlatformView
属性。
创建属性映射器
每个处理程序通常提供一个属性映射器,用于定义在跨平台控件中发生属性更改时要执行的操作。 PropertyMapper 类型是 Dictionary
,用于将跨平台控件的属性映射到其关联的操作。
PropertyMapper 在 .NET MAUI 的 ViewHandler<TVirtualView,TPlatformView> 类中定义,需要提供两个泛型参数:
- 派生自 View 的跨平台控件的类。
- 处理程序的类。
下列代码示例显示使用 PropertyMapper 定义扩展的 VideoHandler
类:
public partial class VideoHandler
{
public static IPropertyMapper<Video, VideoHandler> PropertyMapper = new PropertyMapper<Video, VideoHandler>(ViewHandler.ViewMapper)
{
[nameof(Video.AreTransportControlsEnabled)] = MapAreTransportControlsEnabled,
[nameof(Video.Source)] = MapSource,
[nameof(Video.IsLooping)] = MapIsLooping,
[nameof(Video.Position)] = MapPosition
};
public VideoHandler() : base(PropertyMapper)
{
}
}
PropertyMapper 是 Dictionary
,其键为 string
,值为泛型 Action
。 string
表示跨平台控件的属性名称,Action
表示需要处理程序和跨平台控件作为参数的 static
方法。 例如,MapSource
方法的签名是 public static void MapSource(VideoHandler handler, Video video)
。
每个平台处理程序都必须提供操作的实现,用于操作本机视图 API。 这可确保在跨平台控件上设置属性时,基础本机视图将根据需要进行更新。 此方法的优点是,它允许轻松进行跨平台控件自定义,因为跨平台控件使用者无需子类化即可修改属性映射器。
创建命令映射器
每个处理程序还可以提供一个命令映射器,用于定义跨平台控件向本机视图发送命令时要执行的操作。 命令映射器类似于属性映射器,但允许传递其他数据。 在此上下文中,命令是发送到本机视图的指令及其数据(可选)。 CommandMapper 类型是一个 Dictionary
,用于将跨平台控件成员映射到其关联的操作。
CommandMapper 在 .NET MAUI 的 ViewHandler<TVirtualView,TPlatformView> 类中定义,需要提供两个泛型参数:
- 派生自 View 的跨平台控件的类。
- 处理程序的类。
下列代码示例显示使用 CommandMapper 定义扩展的 VideoHandler
类:
public partial class VideoHandler
{
public static IPropertyMapper<Video, VideoHandler> PropertyMapper = new PropertyMapper<Video, VideoHandler>(ViewHandler.ViewMapper)
{
[nameof(Video.AreTransportControlsEnabled)] = MapAreTransportControlsEnabled,
[nameof(Video.Source)] = MapSource,
[nameof(Video.IsLooping)] = MapIsLooping,
[nameof(Video.Position)] = MapPosition
};
public static CommandMapper<Video, VideoHandler> CommandMapper = new(ViewCommandMapper)
{
[nameof(Video.UpdateStatus)] = MapUpdateStatus,
[nameof(Video.PlayRequested)] = MapPlayRequested,
[nameof(Video.PauseRequested)] = MapPauseRequested,
[nameof(Video.StopRequested)] = MapStopRequested
};
public VideoHandler() : base(PropertyMapper, CommandMapper)
{
}
}
CommandMapper 是一个 Dictionary
,其键为 string
,其值为泛型 Action
。 string
表示跨平台控件的命令名称,Action
表示需要处理程序、跨平台控件和可选数据(作为参数)的 static
方法。 例如,MapPlayRequested
方法的签名为 public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
。
每个平台处理程序都必须提供操作的实现,用于操作本机视图 API。 这可确保从跨平台控件发送命令时,将根据需要操作基础本机视图。 此方法的优势在于,它无需本机视图订阅和取消订阅跨平台控件事件。 此外,它允许轻松进行自定义,因为跨平台控件使用者无需子类化即可修改命令映射器。
创建平台控件
为处理程序创建映射器后,必须在所有平台上提供处理程序实现。 可以通过在 Platforms 文件夹的子文件夹中添加分部类处理程序实现来达成此目的。 或者,可以将项目配置为支持基于文件名的多目标或基于文件夹的多目标,或者同时支持这两者。
将示例应用配置为支持基于文件名的多目标,以便处理程序类全部位于单个文件夹中:
包含映射器的 VideoHandler
类的名称为 VideoHandler.cs。 其平台实现位于 VideoHandler.Android.cs、VideoHandler.MaciOS.cs 和 VideoHandler.Windows.cs 文件中。 通过将以下 XML 添加到项目文件(作为 <Project>
节点的子节点)来配置此基于文件名的多目标:
<!-- Android -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-android')) != true">
<Compile Remove="**\*.Android.cs" />
<None Include="**\*.Android.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<!-- iOS and Mac Catalyst -->
<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0-ios')) != true AND $(TargetFramework.StartsWith('net8.0-maccatalyst')) != true">
<Compile Remove="**\*.MaciOS.cs" />
<None Include="**\*.MaciOS.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
<!-- Windows -->
<ItemGroup Condition="$(TargetFramework.Contains('-windows')) != true ">
<Compile Remove="**\*.Windows.cs" />
<None Include="**\*.Windows.cs" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
</ItemGroup>
有关配置多目标的详细信息,请参阅配置多目标。
每个平台处理程序类应是分部类,派生自 ViewHandler<TVirtualView,TPlatformView> 该类,这需要两个类型参数:
- 派生自 View 的跨平台控件的类。
- 在平台上实现跨平台控件的本机视图的类型。 这应与处理程序中
PlatformView
属性的类型相同。
重要说明
ViewHandler<TVirtualView,TPlatformView> 类提供 VirtualView 和 PlatformView 属性。 VirtualView 属性用于从其处理程序访问跨平台控件。 PlatformView 属性用于访问每个平台上实现跨平台控件的本机视图。
每个平台处理程序实现都应重写以下方法:
- CreatePlatformView,用于创建并返回实现跨平台控件的本机视图。
- ConnectHandler,用于执行任何本机视图设置,例如初始化本机视图和执行事件订阅。
- DisconnectHandler,用于执行任何本机视图清理,例如取消订阅事件和释放对象。
重要
.NET MAUI 有意不调用 DisconnectHandler 方法。 实际上,必须从应用生命周期中的合适位置自行调用它。 有关详细信息,请参阅本机视图清理。
重要
此方法 DisconnectHandler 默认由 .NET MAUI 自动调用,尽管此行为可以更改。 有关详细信息,请参阅 控制处理程序断开连接。
每个平台处理程序还应实现映射器字典中定义的操作。
此外,每个平台处理程序还应根据需要提供代码,以在平台上实现跨平台控件的功能。 或者,可以通过其他类型提供代码,此处采用的正是此方法。
Android
在 Android 上,视频播放是通过 VideoView
进行的。 但在此处,VideoView
被封装在 MauiVideoPlayer
类型中,从而使本机视图与其处理程序保持分离。 以下示例展示了适用于 Android 的 VideoHandler
分部类及其三个替代:
#nullable enable
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.Android;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(Context, VirtualView);
protected override void ConnectHandler(MauiVideoPlayer platformView)
{
base.ConnectHandler(platformView);
// Perform any control setup here
}
protected override void DisconnectHandler(MauiVideoPlayer platformView)
{
platformView.Dispose();
base.DisconnectHandler(platformView);
}
...
}
}
VideoHandler
派生自 ViewHandler<TVirtualView,TPlatformView> 类,所带有的泛型 Video
参数指定跨平台控件类型,而 MauiVideoPlayer
参数指定封装 VideoView
本机视图的类型。
CreatePlatformView 替代会创建并返回 MauiVideoPlayer
对象。 ConnectHandler 替代是执行任何必需的本机视图设置的位置。 DisconnectHandler 替代是执行任何本机视图清理的位置,因此在 MauiVideoPlayer
实例上调用 Dispose
方法。
平台处理程序还必须实现属性映射器字典中定义的操作:
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateTransportControlsEnabled();
}
public static void MapSource(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateSource();
}
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
public static void MapPosition(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdatePosition();
}
...
}
每个操作都是为了响应跨平台控件上的属性更改而执行的,并且是需要处理程序和跨平台控件实例(作为参数)的 static
方法。 在每种情况下,操作都会调用在 MauiVideoPlayer
类型中定义的方法。
平台处理程序还必须实现命令映射器字典中定义的操作:
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PlayRequested(position);
}
public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PauseRequested(position);
}
public static void MapStopRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.StopRequested(position);
}
...
}
每个操作都是为了响应从跨平台控件发送的命令而执行的,并且是需要处理程序和跨平台控件实例以及可选数据(作为参数)的 static
方法。 在每种情况下,操作都会在提取可选数据后调用 MauiVideoPlayer
类中定义的方法。
在 Android 上,MauiVideoPlayer
类会封装 VideoView
,从而使本机视图与其处理程序保持分离:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
MediaController _mediaController;
bool _isPrepared;
Context _context;
Video _video;
public MauiVideoPlayer(Context context, Video video) : base(context)
{
_context = context;
_video = video;
SetBackgroundColor(Color.Black);
// Create a RelativeLayout for sizing the video
RelativeLayout relativeLayout = new RelativeLayout(_context)
{
LayoutParameters = new CoordinatorLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
{
Gravity = (int)GravityFlags.Center
}
};
// Create a VideoView and position it in the RelativeLayout
_videoView = new VideoView(context)
{
LayoutParameters = new RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent)
};
// Add to the layouts
relativeLayout.AddView(_videoView);
AddView(relativeLayout);
// Handle events
_videoView.Prepared += OnVideoViewPrepared;
}
...
}
}
MauiVideoPlayer
派生自 CoordinatorLayout
,因为 Android 上的 .NET MAUI 应用中的根本机视图为 CoordinatorLayout
。 虽然 MauiVideoPlayer
类可以派生自其他本机 Android 类型,但在某些情况下很难控制本机视图定位。
可以直接将 VideoView
添加到 CoordinatorLayout
,并根据需要在布局中进行定位。 但在这里,将一个 Android RelativeLayout
添加到了 CoordinatorLayout
,并且将 VideoView
添加到了 RelativeLayout
。 会同时在 RelativeLayout
和 VideoView
上设置布局参数,从而使 VideoView
在页面上居中,并在保持其纵横比的同时展开以填充可用空间。
此构造函数还会订阅 VideoView.Prepared
事件。 当视频准备好播放并从 Dispose
替代中取消订阅时,将引发此事件:
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
Video _video;
...
protected override void Dispose(bool disposing)
{
if (disposing)
{
_videoView.Prepared -= OnVideoViewPrepared;
_videoView.Dispose();
_videoView = null;
_video = null;
}
base.Dispose(disposing);
}
...
}
除了取消订阅 Prepared
事件外,Dispose
替代还会执行本机视图清理。
注意
Dispose
替代由处理程序的 DisconnectHandler 替代调用。
平台传输控件包括用于播放、暂停和停止播放视频的按钮,并且是由 Android 的 MediaController
类型提供。 如果 Video.AreTransportControlsEnabled
属性被设置为 true
,则 MediaController
会被设置为 VideoView
的媒体播放器。 之所以发生这种情况,是因为在设置 AreTransportControlsEnabled
属性时,处理程序的属性映射器需确保调用 MapAreTransportControlsEnabled
方法,进而调用 MauiVideoPlayer
中的 UpdateTransportControlsEnabled
方法:
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
MediaController _mediaController;
Video _video;
...
public void UpdateTransportControlsEnabled()
{
if (_video.AreTransportControlsEnabled)
{
_mediaController = new MediaController(_context);
_mediaController.SetMediaPlayer(_videoView);
_videoView.SetMediaController(_mediaController);
}
else
{
_videoView.SetMediaController(null);
if (_mediaController != null)
{
_mediaController.SetMediaPlayer(null);
_mediaController = null;
}
}
}
...
}
如果不使用传输控件,它们会淡出,但可以通过点击视频来还原。
如果 Video.AreTransportControlsEnabled
属性被设置为 false
,则将删除作为 VideoView
的媒体播放器的 MediaController
。 在此场景中,可以通过编程方式控制视频播放或提供自己的传输控件。 有关详细信息,请参阅创建自定义传输控件。
iOS 和 Mac Catalyst
在 iOS 和 Mac Catalyst 上,视频播放是通过 AVPlayer
和 AVPlayerViewController
进行的。 但在这里,这些类型封装于 MauiVideoPlayer
类型中,从而使本机视图与其处理程序保持分离。 以下示例展示了适用于 iOS 的 VideoHandler
分部类及其三个替代:
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.MaciOS;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(VirtualView);
protected override void ConnectHandler(MauiVideoPlayer platformView)
{
base.ConnectHandler(platformView);
// Perform any control setup here
}
protected override void DisconnectHandler(MauiVideoPlayer platformView)
{
platformView.Dispose();
base.DisconnectHandler(platformView);
}
...
}
}
VideoHandler
派生自 ViewHandler<TVirtualView,TPlatformView> 类,其中的泛型 Video
参数指定跨平台控件类型,而 MauiVideoPlayer
参数指定封装 AVPlayer
和 AVPlayerViewController
本机视图的类型。
CreatePlatformView 替代会创建并返回 MauiVideoPlayer
对象。 ConnectHandler 替代是执行任何必需的本机视图设置的位置。 DisconnectHandler 替代是执行任何本机视图清理的位置,因此在 MauiVideoPlayer
实例上调用 Dispose
方法。
平台处理程序还必须实现属性映射器字典中定义的操作:
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateTransportControlsEnabled();
}
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
public static void MapPosition(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdatePosition();
}
...
}
每个操作都是为了响应跨平台控件上的属性更改而执行的,并且是需要处理程序和跨平台控件实例(作为参数)的 static
方法。 在每种情况下,操作都会调用在 MauiVideoPlayer
类型中定义的方法。
平台处理程序还必须实现命令映射器字典中定义的操作:
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PlayRequested(position);
}
public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PauseRequested(position);
}
public static void MapStopRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.StopRequested(position);
}
...
}
每个操作都是为了响应从跨平台控件发送的命令而执行的,并且是需要处理程序和跨平台控件实例以及可选数据(作为参数)的 static
方法。 在每种情况下,操作都会在提取可选数据后调用 MauiVideoPlayer
类中定义的方法。
在 iOS 和 Mac Catalyst 上,MauiVideoPlayer
类封装 AVPlayer
和 AVPlayerViewController
类型,以保持本机视图与其处理程序分离:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerViewController _playerViewController;
Video _video;
...
public MauiVideoPlayer(Video video)
{
_video = video;
_playerViewController = new AVPlayerViewController();
_player = new AVPlayer();
_playerViewController.Player = _player;
_playerViewController.View.Frame = this.Bounds;
#if IOS16_0_OR_GREATER || MACCATALYST16_1_OR_GREATER
// On iOS 16 and Mac Catalyst 16, for Shell-based apps, the AVPlayerViewController has to be added to the parent ViewController, otherwise the transport controls won't be displayed.
var viewController = WindowStateManager.Default.GetCurrentUIViewController();
// If there's no view controller, assume it's not Shell and continue because the transport controls will still be displayed.
if (viewController?.View is not null)
{
// Zero out the safe area insets of the AVPlayerViewController
UIEdgeInsets insets = viewController.View.SafeAreaInsets;
_playerViewController.AdditionalSafeAreaInsets = new UIEdgeInsets(insets.Top * -1, insets.Left, insets.Bottom * -1, insets.Right);
// Add the View from the AVPlayerViewController to the parent ViewController
viewController.View.AddSubview(_playerViewController.View);
}
#endif
// Use the View from the AVPlayerViewController as the native control
AddSubview(_playerViewController.View);
}
...
}
}
MauiVideoPlayer
派生自 UIView
,后者是 iOS 和 Mac Catalyst 上的基类,用于显示内容并处理用户与该内容交互的对象。 构造函数创建一个 AVPlayer
对象,该对象管理媒体文件的播放和计时,并将其设置为 AVPlayerViewController
的 Player
属性值。 AVPlayerViewController
显示 AVPlayer
中的内容,并呈现传输控件和其他功能。 然后设置控件的大小和位置,这可确保视频居中位于页面,并在保持其纵横比的同时展开以填充可用空间。 在 iOS 16 和 Mac Catalyst 16 上,必须将 AVPlayerViewController
添加到基于 Shell 的应用的父 ViewController
,否则不会显示传输控件。 然后,将本机视图(即 AVPlayerViewController
中的视图)添加到页面。
Dispose
方法负责执行本机视图清理:
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerViewController _playerViewController;
Video _video;
...
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_player != null)
{
DestroyPlayedToEndObserver();
_player.ReplaceCurrentItemWithPlayerItem(null);
_player.Dispose();
}
if (_playerViewController != null)
_playerViewController.Dispose();
_video = null;
}
base.Dispose(disposing);
}
...
}
在某些情况下,视频在离开视频播放页面后会继续播放。 要停止视频,请在 Dispose
重写函数中将 ReplaceCurrentItemWithPlayerItem
设置为 null
,并执行其他本机视图清理。
注意
Dispose
替代由处理程序的 DisconnectHandler 替代调用。
平台传输控件包括播放、暂停和停止视频的按钮,并由 AVPlayerViewController
类型提供。 如果 Video.AreTransportControlsEnabled
属性设置为 true
,则 AVPlayerViewController
将显示其播放控件。 之所以发生这种情况,是因为在设置 AreTransportControlsEnabled
属性时,处理程序的属性映射器需确保调用 MapAreTransportControlsEnabled
方法,进而调用 MauiVideoPlayer
中的 UpdateTransportControlsEnabled
方法:
public class MauiVideoPlayer : UIView
{
AVPlayerViewController _playerViewController;
Video _video;
...
public void UpdateTransportControlsEnabled()
{
_playerViewController.ShowsPlaybackControls = _video.AreTransportControlsEnabled;
}
...
}
如果不使用传输控件,它们会淡出,但可以通过点击视频来还原。
如果 Video.AreTransportControlsEnabled
属性设置为 false
,则 AVPlayerViewController
不显示其播放控件。 在此场景中,可以通过编程方式控制视频播放或提供自己的传输控件。 有关详细信息,请参阅创建自定义传输控件。
Windows
视频在 Windows 上使用 MediaPlayerElement
播放。 但在此处,MediaPlayerElement
已封装于 MauiVideoPlayer
类型中,以保持本机视图与其处理程序分离。 以下示例显示了 Windows 的 VideoHandler
分部类及其三个重写函数:
#nullable enable
using Microsoft.Maui.Handlers;
using VideoDemos.Controls;
using VideoDemos.Platforms.Windows;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
protected override MauiVideoPlayer CreatePlatformView() => new MauiVideoPlayer(VirtualView);
protected override void ConnectHandler(MauiVideoPlayer platformView)
{
base.ConnectHandler(platformView);
// Perform any control setup here
}
protected override void DisconnectHandler(MauiVideoPlayer platformView)
{
platformView.Dispose();
base.DisconnectHandler(platformView);
}
...
}
}
VideoHandler
派生自 ViewHandler<TVirtualView,TPlatformView> 类,所带有的泛型 Video
参数指定跨平台控件类型,而 MauiVideoPlayer
参数指定封装 MediaPlayerElement
本机视图的类型。
CreatePlatformView 替代会创建并返回 MauiVideoPlayer
对象。 ConnectHandler 替代是执行任何必需的本机视图设置的位置。 DisconnectHandler 替代是执行任何本机视图清理的位置,因此在 MauiVideoPlayer
实例上调用 Dispose
方法。
平台处理程序还必须实现属性映射器字典中定义的操作:
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapAreTransportControlsEnabled(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateTransportControlsEnabled();
}
public static void MapSource(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateSource();
}
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
public static void MapPosition(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdatePosition();
}
...
}
每个操作都是为了响应跨平台控件上的属性更改而执行的,并且是需要处理程序和跨平台控件实例(作为参数)的 static
方法。 在每种情况下,操作都会调用在 MauiVideoPlayer
类型中定义的方法。
平台处理程序还必须实现命令映射器字典中定义的操作:
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
public static void MapPlayRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PlayRequested(position);
}
public static void MapPauseRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.PauseRequested(position);
}
public static void MapStopRequested(VideoHandler handler, Video video, object? args)
{
if (args is not VideoPositionEventArgs)
return;
TimeSpan position = ((VideoPositionEventArgs)args).Position;
handler.PlatformView?.StopRequested(position);
}
...
}
每个操作都是为了响应从跨平台控件发送的命令而执行的,并且是需要处理程序和跨平台控件实例以及可选数据(作为参数)的 static
方法。 在每种情况下,操作都会在提取可选数据后调用 MauiVideoPlayer
类中定义的方法。
在 Windows 上,MauiVideoPlayer
类封装 MediaPlayerElement
,以将本机视图与其处理程序分离:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
...
public MauiVideoPlayer(Video video)
{
_video = video;
_mediaPlayerElement = new MediaPlayerElement();
this.Children.Add(_mediaPlayerElement);
}
...
}
}
MauiVideoPlayer
派生自 Grid,并将 MediaPlayerElement
添加为 Grid 的子级。 这样,MediaPlayerElement
便可以自动调整大小以填充所有可用空间。
Dispose
方法负责执行本机视图清理:
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void Dispose()
{
if (_isMediaPlayerAttached)
{
_mediaPlayerElement.MediaPlayer.MediaOpened -= OnMediaPlayerMediaOpened;
_mediaPlayerElement.MediaPlayer.Dispose();
}
_mediaPlayerElement = null;
}
...
}
除了取消订阅 MediaOpened
事件外,Dispose
重写函数还执行本机视图清理。
注意
Dispose
替代由处理程序的 DisconnectHandler 替代调用。
平台传输控件包括播放、暂停和停止视频的按钮,并由 MediaPlayerElement
类型提供。 如果 Video.AreTransportControlsEnabled
属性设置为 true
,则 MediaPlayerElement
将显示其播放控件。 之所以发生这种情况,是因为在设置 AreTransportControlsEnabled
属性时,处理程序的属性映射器需确保调用 MapAreTransportControlsEnabled
方法,进而调用 MauiVideoPlayer
中的 UpdateTransportControlsEnabled
方法:
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void UpdateTransportControlsEnabled()
{
_mediaPlayerElement.AreTransportControlsEnabled = _video.AreTransportControlsEnabled;
}
...
}
如果 Video.AreTransportControlsEnabled
属性设置为 false
,则 MediaPlayerElement
不显示其播放控件。 在此场景中,可以通过编程方式控制视频播放或提供自己的传输控件。 有关详细信息,请参阅创建自定义传输控件。
将跨平台控件转化为平台控件
任何派生自 Element 的 .NET MAUI 跨平台控件都可以使用 ToPlatform 扩展方法转换为其基础平台控件:
- 在 Android 上,ToPlatform 将 .NET MAUI 控件转换为 Android View 对象。
- 在 iOS 和 Mac Catalyst 上,ToPlatform 将 .NET MAUI 控件转换为 UIView 对象。
- 在 Windows 上,ToPlatform 将 .NET MAUI 控件转换为
FrameworkElement
对象。
注意
ToPlatform 方法位于 Microsoft.Maui.Platform
命名空间中。
在所有平台上,ToPlatform 方法都需要 MauiContext 参数。
ToPlatform 方法可以将跨平台控件从平台代码转换为其基础平台控件,例如在平台的分部处理程序类中:
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using VideoDemos.Controls;
using VideoDemos.Platforms.Android;
namespace VideoDemos.Handlers
{
public partial class VideoHandler : ViewHandler<Video, MauiVideoPlayer>
{
...
public static void MapSource(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateSource();
// Convert cross-platform control to its underlying platform control
MauiVideoPlayer mvp = (MauiVideoPlayer)video.ToPlatform(handler.MauiContext);
...
}
...
}
}
在此示例中,在适用于 Android 的 VideoHandler
分部类中,MapSource
方法将 Video
实例转换为 MauiVideoPlayer
对象。
ToPlatform 方法还可以将跨平台控件从跨平台代码转换为其基础平台控件:
using Microsoft.Maui.Platform;
namespace VideoDemos.Views;
public partial class MyPage : ContentPage
{
...
protected override void OnHandlerChanged()
{
// Convert cross-platform control to its underlying platform control
#if ANDROID
Android.Views.View nativeView = video.ToPlatform(video.Handler.MauiContext);
#elif IOS || MACCATALYST
UIKit.UIView nativeView = video.ToPlatform(video.Handler.MauiContext);
#elif WINDOWS
Microsoft.UI.Xaml.FrameworkElement nativeView = video.ToPlatform(video.Handler.MauiContext);
#endif
...
}
...
}
在此示例中,已在 OnHandlerChanged() 重写函数中将名为 video
的跨平台 Video
控件转换为每个平台上的基础本机视图。 当实现跨平台控件的本机视图可用并初始化时,将调用此重写函数。 ToPlatform 方法返回的对象可以强制转换为其确切的本机类型,此处为 MauiVideoPlayer
。
播放视频
Video
类定义用于指定视频文件源的 Source
属性,以及 AutoPlay
属性。 AutoPlay
默认为 true
,这意味着视频应该在设置 Source
后自动开始播放。 有关这些属性的定义,请参阅创建跨平台控件。
类型 VideoSource
的 Source
属性是由三个静态方法组成的抽象类,这些方法实例化三个派生自 VideoSource
的类:
using System.ComponentModel;
namespace VideoDemos.Controls
{
[TypeConverter(typeof(VideoSourceConverter))]
public abstract class VideoSource : Element
{
public static VideoSource FromUri(string uri)
{
return new UriVideoSource { Uri = uri };
}
public static VideoSource FromFile(string file)
{
return new FileVideoSource { File = file };
}
public static VideoSource FromResource(string path)
{
return new ResourceVideoSource { Path = path };
}
}
}
VideoSource
类包含引用 VideoSourceConverter
的 TypeConverter
属性:
using System.ComponentModel;
namespace VideoDemos.Controls
{
public class VideoSourceConverter : TypeConverter, IExtendedTypeConverter
{
object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
{
if (!string.IsNullOrWhiteSpace(value))
{
Uri uri;
return Uri.TryCreate(value, UriKind.Absolute, out uri) && uri.Scheme != "file" ?
VideoSource.FromUri(value) : VideoSource.FromResource(value);
}
throw new InvalidOperationException("Cannot convert null or whitespace to VideoSource.");
}
}
}
在 XAML 中将 Source
属性设置为字符串时,将调用此类型转换器。 ConvertFromInvariantString
方法尝试将字符串转换为 Uri
对象。 如果成功,并且方案不为 file
,则该方法返回 UriVideoSource
。 否则,将返回 ResourceVideoSource
。
播放 Web 视频
UriVideoSource
类用于指定包含 URI 的远程视频。 它定义类型 string
的 Uri
属性:
namespace VideoDemos.Controls
{
public class UriVideoSource : VideoSource
{
public static readonly BindableProperty UriProperty =
BindableProperty.Create(nameof(Uri), typeof(string), typeof(UriVideoSource));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
}
}
当 Source
属性被设置为 UriVideoSource
时,处理程序的属性映射器将确保调用 MapSource
方法:
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
MapSource
方法反过来调用处理程序的 PlatformView
属性上的 UpdateSource
方法。 类型 MauiVideoPlayer
的 PlatformView
属性表示在每个平台上提供视频播放器实现的本机视图。
Android
在 Android 上,视频播放是通过 VideoView
进行的。 以下代码示例演示 UpdateSource
方法如何处理当类型为 UriVideoSource
时的 Source
属性:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Video _video;
...
public void UpdateSource()
{
_isPrepared = false;
bool hasSetSource = false;
if (_video.Source is UriVideoSource)
{
string uri = (_video.Source as UriVideoSource).Uri;
if (!string.IsNullOrWhiteSpace(uri))
{
_videoView.SetVideoURI(Uri.Parse(uri));
hasSetSource = true;
}
}
...
if (hasSetSource && _video.AutoPlay)
{
_videoView.Start();
}
}
...
}
}
处理类型为 UriVideoSource
的对象时,使用 VideoView
的 SetVideoUri
方法指定要播放的视频,其中包含从字符串 URI 创建的 Android Uri
对象。
AutoPlay
属性在 VideoView
上没有等效项,因此,如果设置了新视频,则调用 Start
方法。
iOS 和 Mac Catalyst
要在 iOS 和 Mac Catalyst 上播放视频,需要创建类型为 AVAsset
的对象来封装视频,并使用该对象创建 AVPlayerItem
,然后将其移交给 AVPlayer
对象。 以下代码示例演示 UpdateSource
方法如何处理当类型为 UriVideoSource
时的 Source
属性:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerItem _playerItem;
Video _video;
...
public void UpdateSource()
{
AVAsset asset = null;
if (_video.Source is UriVideoSource)
{
string uri = (_video.Source as UriVideoSource).Uri;
if (!string.IsNullOrWhiteSpace(uri))
asset = AVAsset.FromUrl(new NSUrl(uri));
}
...
if (asset != null)
_playerItem = new AVPlayerItem(asset);
else
_playerItem = null;
_player.ReplaceCurrentItemWithPlayerItem(_playerItem);
if (_playerItem != null && _video.AutoPlay)
{
_player.Play();
}
}
...
}
}
处理类型为 UriVideoSource
的对象时,使用静态 AVAsset.FromUrl
方法指定要播放的视频,其中包含从字符串 URI 创建的 iOS NSUrl
对象。
AutoPlay
属性在 iOS 视频类中没有等效项,因此在 UpdateSource
方法的末尾检查该属性,以对 AVPlayer
对象调用 Play
方法。
在某些情况下,iOS 上的视频在离开视频播放页后会继续播放。 要停止视频,请在 Dispose
重写函数中将 ReplaceCurrentItemWithPlayerItem
设置为 null
:
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_player != null)
{
_player.ReplaceCurrentItemWithPlayerItem(null);
...
}
...
}
base.Dispose(disposing);
}
Windows
视频在 Windows 上使用 MediaPlayerElement
播放。 以下代码示例演示 UpdateSource
方法如何处理当类型为 UriVideoSource
时的 Source
属性:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public async void UpdateSource()
{
bool hasSetSource = false;
if (_video.Source is UriVideoSource)
{
string uri = (_video.Source as UriVideoSource).Uri;
if (!string.IsNullOrWhiteSpace(uri))
{
_mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri(uri));
hasSetSource = true;
}
}
...
if (hasSetSource && !_isMediaPlayerAttached)
{
_isMediaPlayerAttached = true;
_mediaPlayerElement.MediaPlayer.MediaOpened += OnMediaPlayerMediaOpened;
}
if (hasSetSource && _video.AutoPlay)
{
_mediaPlayerElement.AutoPlay = true;
}
}
...
}
}
处理类型为 UriVideoSource
的对象时,将 MediaPlayerElement.Source
属性设置为 MediaSource
对象,该对象使用要播放的视频的 URI 初始化 Uri
。 设置 MediaPlayerElement.Source
后,将针对 MediaPlayerElement.MediaPlayer.MediaOpened
事件注册 OnMediaPlayerMediaOpened
事件处理程序方法。 此事件处理程序用于设置 Video
控件的 Duration
属性。
在 UpdateSource
方法结束时,将检查 Video.AutoPlay
属性,如果为 true,则将 MediaPlayerElement.AutoPlay
属性设置为 true
以开始视频播放。
播放视频资源
ResourceVideoSource
类用于访问嵌入应用中的视频文件。 它定义了类型为 string
的 Path
属性:
namespace VideoDemos.Controls
{
public class ResourceVideoSource : VideoSource
{
public static readonly BindableProperty PathProperty =
BindableProperty.Create(nameof(Path), typeof(string), typeof(ResourceVideoSource));
public string Path
{
get { return (string)GetValue(PathProperty); }
set { SetValue(PathProperty, value); }
}
}
}
当 Source
属性被设置为 ResourceVideoSource
时,处理程序的属性映射器将确保调用 MapSource
方法:
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
MapSource
方法反过来调用处理程序的 PlatformView
属性上的 UpdateSource
方法。 类型 MauiVideoPlayer
的 PlatformView
属性表示在每个平台上提供视频播放器实现的本机视图。
Android
以下代码示例演示 UpdateSource
方法如何处理当类型为 ResourceVideoSource
时的 Source
属性:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Context _context;
Video _video;
...
public void UpdateSource()
{
_isPrepared = false;
bool hasSetSource = false;
...
else if (_video.Source is ResourceVideoSource)
{
string package = Context.PackageName;
string path = (_video.Source as ResourceVideoSource).Path;
if (!string.IsNullOrWhiteSpace(path))
{
string assetFilePath = "content://" + package + "/" + path;
_videoView.SetVideoPath(assetFilePath);
hasSetSource = true;
}
}
...
}
...
}
}
处理类型为 ResourceVideoSource
的对象时,会使用 VideoView
的 SetVideoPath
方法指定要播放的视频,其中包含将应用的包名称与视频的文件名组合在一起的字符串参数。
资源视频文件存储在包的 assets 文件夹中,需要有内容提供程序才能访问它。 内容提供程序由 VideoProvider
类提供,该类可创建提供视频文件访问权限的 AssetFileDescriptor
对象:
using Android.Content;
using Android.Content.Res;
using Android.Database;
using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
[ContentProvider(new string[] { "com.companyname.videodemos" })]
public class VideoProvider : ContentProvider
{
public override AssetFileDescriptor OpenAssetFile(Uri uri, string mode)
{
var assets = Context.Assets;
string fileName = uri.LastPathSegment;
if (fileName == null)
throw new FileNotFoundException();
AssetFileDescriptor afd = null;
try
{
afd = assets.OpenFd(fileName);
}
catch (IOException ex)
{
Debug.WriteLine(ex);
}
return afd;
}
public override bool OnCreate()
{
return false;
}
...
}
}
iOS 和 Mac Catalyst
以下代码示例演示 UpdateSource
方法如何处理当类型为 ResourceVideoSource
时的 Source
属性:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
Video _video;
...
public void UpdateSource()
{
AVAsset asset = null;
...
else if (_video.Source is ResourceVideoSource)
{
string path = (_video.Source as ResourceVideoSource).Path;
if (!string.IsNullOrWhiteSpace(path))
{
string directory = Path.GetDirectoryName(path);
string filename = Path.GetFileNameWithoutExtension(path);
string extension = Path.GetExtension(path).Substring(1);
NSUrl url = NSBundle.MainBundle.GetUrlForResource(filename, extension, directory);
asset = AVAsset.FromUrl(url);
}
}
...
}
...
}
}
处理类型为 ResourceVideoSource
的对象时,会使用 NSBundle
的 GetUrlForResource
方法从应用包中检索文件。 完整路径必须划分为文件名、扩展名和目录。
在某些情况下,iOS 上的视频在离开视频播放页后会继续播放。 要停止视频,请在 Dispose
重写函数中将 ReplaceCurrentItemWithPlayerItem
设置为 null
:
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_player != null)
{
_player.ReplaceCurrentItemWithPlayerItem(null);
...
}
...
}
base.Dispose(disposing);
}
Windows
以下代码示例演示 UpdateSource
方法如何处理当类型为 ResourceVideoSource
时的 Source
属性:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
...
public async void UpdateSource()
{
bool hasSetSource = false;
...
else if (_video.Source is ResourceVideoSource)
{
string path = "ms-appx:///" + (_video.Source as ResourceVideoSource).Path;
if (!string.IsNullOrWhiteSpace(path))
{
_mediaPlayerElement.Source = MediaSource.CreateFromUri(new Uri(path));
hasSetSource = true;
}
}
...
}
...
}
}
处理类型为 ResourceVideoSource
的对象时,会将 MediaPlayerElement.Source
属性设置为 MediaSource
对象,该对象使用前缀为 ms-appx:///
的视频资源的路径初始化 Uri
。
从设备库播放视频文件
FileVideoSource
类用于访问设备视频库中的视频。 它定义了类型为 string
的 File
属性:
namespace VideoDemos.Controls
{
public class FileVideoSource : VideoSource
{
public static readonly BindableProperty FileProperty =
BindableProperty.Create(nameof(File), typeof(string), typeof(FileVideoSource));
public string File
{
get { return (string)GetValue(FileProperty); }
set { SetValue(FileProperty, value); }
}
}
}
当 Source
属性被设置为 FileVideoSource
时,处理程序的属性映射器将确保调用 MapSource
方法:
public static void MapSource(VideoHandler handler, Video video)
{
handler?.PlatformView.UpdateSource();
}
MapSource
方法反过来调用处理程序的 PlatformView
属性上的 UpdateSource
方法。 类型 MauiVideoPlayer
的 PlatformView
属性表示在每个平台上提供视频播放器实现的本机视图。
Android
以下代码示例演示 UpdateSource
方法如何处理当类型为 FileVideoSource
时的 Source
属性:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Video _video;
...
public void UpdateSource()
{
_isPrepared = false;
bool hasSetSource = false;
...
else if (_video.Source is FileVideoSource)
{
string filename = (_video.Source as FileVideoSource).File;
if (!string.IsNullOrWhiteSpace(filename))
{
_videoView.SetVideoPath(filename);
hasSetSource = true;
}
}
...
}
...
}
}
处理类型为 FileVideoSource
的对象时,会使用 VideoView
的 SetVideoPath
方法指定要播放的视频文件。
iOS 和 Mac Catalyst
以下代码示例演示 UpdateSource
方法如何处理当类型为 FileVideoSource
时的 Source
属性:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
Video _video;
...
public void UpdateSource()
{
AVAsset asset = null;
...
else if (_video.Source is FileVideoSource)
{
string uri = (_video.Source as FileVideoSource).File;
if (!string.IsNullOrWhiteSpace(uri))
asset = AVAsset.FromUrl(NSUrl.CreateFileUrl(new [] { uri }));
}
...
}
...
}
}
处理类型为 FileVideoSource
的对象时,会使用静态 AVAsset.FromUrl
方法指定要播放的视频文件,其中包含从字符串 URI 创建 iOS NSUrl
对象的 NSUrl.CreateFileUrl
方法。
Windows
以下代码示例演示 UpdateSource
方法如何处理当类型为 FileVideoSource
时的 Source
属性:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
...
public async void UpdateSource()
{
bool hasSetSource = false;
...
else if (_video.Source is FileVideoSource)
{
string filename = (_video.Source as FileVideoSource).File;
if (!string.IsNullOrWhiteSpace(filename))
{
StorageFile storageFile = await StorageFile.GetFileFromPathAsync(filename);
_mediaPlayerElement.Source = MediaSource.CreateFromStorageFile(storageFile);
hasSetSource = true;
}
}
...
}
...
}
}
处理类型为 FileVideoSource
的对象时,视频文件名将被转换为 StorageFile
对象。 然后,MediaSource.CreateFromStorageFile
方法返回 MediaSource
对象,该对象被设置为 MediaPlayerElement.Source
属性的值。
循环播放视频
Video
类定义了 IsLooping
属性,该属性支持控件在视频播放结束后自动将其位置设置到开始位置。 它默认为 false
,指示视频不会自动循环播放。
设置 IsLooping
属性后,处理程序的属性映射器将确保调用 MapIsLooping
方法:
public static void MapIsLooping(VideoHandler handler, Video video)
{
handler.PlatformView?.UpdateIsLooping();
}
MapIsLooping
方法进而调用处理程序的 PlatformView
属性上的 UpdateIsLooping
方法。 类型 MauiVideoPlayer
的 PlatformView
属性表示在每个平台上提供视频播放器实现的本机视图。
Android
以下代码示例展示了 UpdateIsLooping
方法是如何在 Android 上启用视频循环播放的:
using Android.Content;
using Android.Media;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout, MediaPlayer.IOnPreparedListener
{
VideoView _videoView;
Video _video;
...
public void UpdateIsLooping()
{
if (_video.IsLooping)
{
_videoView.SetOnPreparedListener(this);
}
else
{
_videoView.SetOnPreparedListener(null);
}
}
public void OnPrepared(MediaPlayer mp)
{
mp.Looping = _video.IsLooping;
}
...
}
}
若要启用视频循环播放,MauiVideoPlayer
类需实现 MediaPlayer.IOnPreparedListener
接口。 此接口定义了在媒体源准备好播放时调用的 OnPrepared
回调。 当 Video.IsLooping
属性为 true
时,UpdateIsLooping
方法会将 MauiVideoPlayer
设置为提供 OnPrepared
回调的对象。 回调将 MediaPlayer.IsLooping
属性设置为 Video.IsLooping
属性的值。
iOS 和 Mac Catalyst
以下代码示例展示了 UpdateIsLooping
方法是如何在 iOS 和 Mac Catalyst 上启用视频循环播放的:
using System.Diagnostics;
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerViewController _playerViewController;
Video _video;
NSObject? _playedToEndObserver;
...
public void UpdateIsLooping()
{
DestroyPlayedToEndObserver();
if (_video.IsLooping)
{
_player.ActionAtItemEnd = AVPlayerActionAtItemEnd.None;
_playedToEndObserver = NSNotificationCenter.DefaultCenter.AddObserver(AVPlayerItem.DidPlayToEndTimeNotification, PlayedToEnd);
}
else
_player.ActionAtItemEnd = AVPlayerActionAtItemEnd.Pause;
}
void PlayedToEnd(NSNotification notification)
{
if (_video == null || notification.Object != _playerViewController.Player?.CurrentItem)
return;
_playerViewController.Player?.Seek(CMTime.Zero);
}
...
}
}
在 iOS 和 Mac Catalyst 上,当视频播放完后,会使用通知来执行回调。 当 Video.IsLooping
属性为 true
时,UpdateIsLooping
方法将为 AVPlayerItem.DidPlayToEndTimeNotification
通知添加观察程序,并在收到通知后执行 PlayedToEnd
方法。 相应的,此方法从视频的开头恢复播放。 如果 Video.IsLooping
属性为 false
,则视频将在播放结束时暂停。
由于 MauiVideoPlayer
为通知添加了观察程序,因此在执行本机视图清理时,还必须删除该观察程序。 可在 Dispose
替代中达成此目的:
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerViewController _playerViewController;
Video _video;
NSObject? _playedToEndObserver;
...
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_player != null)
{
DestroyPlayedToEndObserver();
...
}
...
}
base.Dispose(disposing);
}
void DestroyPlayedToEndObserver()
{
if (_playedToEndObserver != null)
{
NSNotificationCenter.DefaultCenter.RemoveObserver(_playedToEndObserver);
DisposeObserver(ref _playedToEndObserver);
}
}
void DisposeObserver(ref NSObject? disposable)
{
disposable?.Dispose();
disposable = null;
}
...
}
Dispose
替代调用 DestroyPlayedToEndObserver
方法,此方法会删除 AVPlayerItem.DidPlayToEndTimeNotification
通知的观察程序,并在 NSObject
上调用 Dispose
方法。
Windows
以下代码示例展示了 UpdateIsLooping
方法是如何在 Windows 上启用视频循环播放的:
public void UpdateIsLooping()
{
if (_isMediaPlayerAttached)
_mediaPlayerElement.MediaPlayer.IsLoopingEnabled = _video.IsLooping;
}
为了启用视频循环播放,UpdateIsLooping
方法将 MediaPlayerElement.MediaPlayer.IsLoopingEnabled
属性设置为 Video.IsLooping
属性的值。
创建自定义传输控件
视频播放器的传输控件包括用于播放、暂停和停止视频的按钮。 这些按钮通常使用熟悉的图标而不是文本进行标识,并且播放和暂停按钮通常合并为一个按钮。
默认情况下,Video
控件显示每个平台支持的传输控件。 但是,将 AreTransportControlsEnabled
属性设置为 false
时,会禁止这些控件。 然后,可以通过编程方式控制视频播放或提供自己的传输控件。
实现自己的传输控件要求 Video
类能够通知其本机视图播放、暂停或停止视频,并了解视频播放的当前状态。 Video
类定义名为 Play
、Pause
和 Stop
的方法,这些方法引发相应的事件,并向 VideoHandler
发送命令:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public event EventHandler<VideoPositionEventArgs> PlayRequested;
public event EventHandler<VideoPositionEventArgs> PauseRequested;
public event EventHandler<VideoPositionEventArgs> StopRequested;
public void Play()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
PlayRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.PlayRequested), args);
}
public void Pause()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
PauseRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.PauseRequested), args);
}
public void Stop()
{
VideoPositionEventArgs args = new VideoPositionEventArgs(Position);
StopRequested?.Invoke(this, args);
Handler?.Invoke(nameof(Video.StopRequested), args);
}
}
}
VideoPositionEventArgs
类定义可通过其构造函数设置的 Position
属性。 此属性表示视频播放开始、暂停或停止的位置。
Play
、Pause
和 Stop
方法中的最后一行将命令和关联数据发送到 VideoHandler
。 VideoHandler
的 CommandMapper 将命令名称映射到接收命令时执行的操作。 例如,当 VideoHandler
收到 PlayRequested
命令时,它将执行其 MapPlayRequested
方法。 此方法的优势在于,它无需本机视图订阅和取消订阅跨平台控件事件。 此外,它允许轻松进行自定义,因为跨平台控件使用者无需子类化即可修改命令映射器。 有关 CommandMapper 的详细信息,请参阅创建命令映射器。
Android、iOS 和 Mac Catalyst 上的 MauiVideoPlayer
实现包含 PlayRequested
、PauseRequested
和 StopRequested
方法,为响应 Video
控件发送 PlayRequested
、PauseRequested
和 StopRequested
命令,将执行这些方法。 每个方法对其本机视图调用一个方法来播放、暂停或停止视频。 例如,以下代码展示了 iOS 和 Mac Catalyst 上的 PlayRequested
、PauseRequested
和 StopRequested
方法:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
...
public void PlayRequested(TimeSpan position)
{
_player.Play();
Debug.WriteLine($"Video playback from {position.Hours:X2}:{position.Minutes:X2}:{position.Seconds:X2}.");
}
public void PauseRequested(TimeSpan position)
{
_player.Pause();
Debug.WriteLine($"Video paused at {position.Hours:X2}:{position.Minutes:X2}:{position.Seconds:X2}.");
}
public void StopRequested(TimeSpan position)
{
_player.Pause();
_player.Seek(new CMTime(0, 1));
Debug.WriteLine($"Video stopped at {position.Hours:X2}:{position.Minutes:X2}:{position.Seconds:X2}.");
}
}
}
这三种方法都使用随命令发送的数据记录视频播放、暂停或停止的位置。
此机制可确保在 Video
控件上调用 Play
、Pause
或 Stop
方法时,指示其本机视图播放、暂停或停止视频,并记录播放、暂停或停止视频的位置。 上述操作均采用分离方法,无需本机视图订阅跨平台事件。
视频状态
实现播放、暂停和停止功能不足以支持自定义传输控件。 通常,播放和暂停功能应使用同一按钮实现,该按钮会更改其外观以指示视频当前是正在播放还是已暂停。 此外,如果视频尚未加载,则不应启用该按钮。
这些要求意味着视频播放器需要提供一个当前状态,以指示它是否正在播放或暂停,或者是否还没有准备好播放视频。 此状态可由一个枚举表示:
public enum VideoStatus
{
NotReady,
Playing,
Paused
}
Video
类定义了一个名称为 Status
(类型为 VideoStatus
)的只读可绑定属性。 此属性之所以被定义为只读,是因为只能从控件处理器中设置它:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey StatusPropertyKey =
BindableProperty.CreateReadOnly(nameof(Status), typeof(VideoStatus), typeof(Video), VideoStatus.NotReady);
public static readonly BindableProperty StatusProperty = StatusPropertyKey.BindableProperty;
public VideoStatus Status
{
get { return (VideoStatus)GetValue(StatusProperty); }
}
VideoStatus IVideoController.Status
{
get { return Status; }
set { SetValue(StatusPropertyKey, value); }
}
...
}
}
通常,只读可绑定属性在 Status
属性上具有私有的 set
访问器,以允许在类中设置它。 但是,对于由处理器支持的 View 派生项,必须从类外部设置其属性,但只能由控件的处理程序来设置。
出于此原因,另一个属性被定义为 IVideoController.Status
。 这是一个显式接口实现,由 Video
类实现的 IVideoController
接口使之成为可能:
public interface IVideoController
{
VideoStatus Status { get; set; }
TimeSpan Duration { get; set; }
}
该接口使 Video
外部的类可以通过引用 IVideoController
接口来设置 Status
属性。 也可以从其他类和处理程序设置该属性,但不太可能会被无意设置。 最重要的是,不能通过数据绑定设置 Status
属性。
为协助处理程序实现持续更新属性 Status
,Video
类定义了事件 UpdateStatus
和命令:
using System.ComponentModel;
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public event EventHandler UpdateStatus;
IDispatcherTimer _timer;
public Video()
{
_timer = Dispatcher.CreateTimer();
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Tick += OnTimerTick;
_timer.Start();
}
~Video() => _timer.Tick -= OnTimerTick;
void OnTimerTick(object sender, EventArgs e)
{
UpdateStatus?.Invoke(this, EventArgs.Empty);
Handler?.Invoke(nameof(Video.UpdateStatus));
}
...
}
}
OnTimerTick
事件处理程序每隔十分之一秒执行一次,这会引发 UpdateStatus
事件并调用 UpdateStatus
命令。
将 UpdateStatus
命令从 Video
控件发送到其处理程序时,处理程序的命令映射器可确保 MapUpdateStatus
调用该方法:
public static void MapUpdateStatus(VideoHandler handler, Video video, object? args)
{
handler.PlatformView?.UpdateStatus();
}
该方法 MapUpdateStatus
轮流调用 UpdateStatus
处理器 PlatformView
属性上的方法。 该 PlatformView
属性的类型为 MauiVideoPlayer
,封装提供每个平台上视频播放器实现的本机视图。
Android
以下代码示例展示了在 Android 上使用 UpdateStatus
方法设置 Status
属性:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
bool _isPrepared;
Video _video;
...
public MauiVideoPlayer(Context context, Video video) : base(context)
{
_video = video;
...
_videoView.Prepared += OnVideoViewPrepared;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_videoView.Prepared -= OnVideoViewPrepared;
...
}
base.Dispose(disposing);
}
void OnVideoViewPrepared(object sender, EventArgs args)
{
_isPrepared = true;
((IVideoController)_video).Duration = TimeSpan.FromMilliseconds(_videoView.Duration);
}
public void UpdateStatus()
{
VideoStatus status = VideoStatus.NotReady;
if (_isPrepared)
status = _videoView.IsPlaying ? VideoStatus.Playing : VideoStatus.Paused;
((IVideoController)_video).Status = status;
...
}
...
}
}
VideoView.IsPlaying
属性是一个布尔值,用于指示视频是播放还是暂停。 要确定 VideoView
是否无法播放或暂停视频,必须处理 Prepared
事件。 媒体源准备好播放时,会触发此事件。 事件在 MauiVideoPlayer
构造函数中订阅,并从其 Dispose
重写函数取消订阅。 然后,该方法 UpdateStatus
使用 isPrepared
字段和 VideoView.IsPlaying
属性,将该属性强制转换为 IVideoController
,以便在 Video
对象上设置 Status
该属性。
iOS 和 Mac Catalyst
下列代码示例演示 UpdateStatus
方法如何在 iOS 和 Mac Catalyst 上设置 Status
属性:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
Video _video;
...
public void UpdateStatus()
{
VideoStatus videoStatus = VideoStatus.NotReady;
switch (_player.Status)
{
case AVPlayerStatus.ReadyToPlay:
switch (_player.TimeControlStatus)
{
case AVPlayerTimeControlStatus.Playing:
videoStatus = VideoStatus.Playing;
break;
case AVPlayerTimeControlStatus.Paused:
videoStatus = VideoStatus.Paused;
break;
}
break;
}
((IVideoController)_video).Status = videoStatus;
...
}
...
}
}
必须访问两个 AVPlayer
属性,以便设置 Status
属性 - AVPlayerStatus
类型的 Status
属性和 AVPlayerTimeControlStatus
类型的 TimeControlStatus
属性。 然后,通过将 Status
属性强制转换为 IVideoController
属性,在 Video
对象上对其设置 。
Windows
下列代码示例演示 UpdateStatus
方法如何在 Windows 设置 Status
属性:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void UpdateStatus()
{
if (_isMediaPlayerAttached)
{
VideoStatus status = VideoStatus.NotReady;
switch (_mediaPlayerElement.MediaPlayer.CurrentState)
{
case MediaPlayerState.Playing:
status = VideoStatus.Playing;
break;
case MediaPlayerState.Paused:
case MediaPlayerState.Stopped:
status = VideoStatus.Paused;
break;
}
((IVideoController)_video).Status = status;
_video.Position = _mediaPlayerElement.MediaPlayer.Position;
}
}
...
}
}
UpdateStatus
方法使用 MediaPlayerElement.MediaPlayer.CurrentState
属性的值来确定 Status
属性的值。 然后,通过将 Status
属性强制转换为 IVideoController
属性,在 Video
对象上对其设置 。
定位工具栏
由每个平台实现的传输控件都有一个定位工具栏。 该工具栏类似滑块或滚动工具栏,用于显示视频进度。 此外,用户可以操纵该定位工具栏,前移或后移到视频中的新位置。
实现自己的定位工具栏需要 Video
类,以便了解视频的持续时间及其进度。
持续时间
Video
控件支持自定义定位工具栏所需的一项信息是视频的持续时间。 Video
类定义了一个类型为 TimeSpan
的只可绑定属性 Duration
。 此属性之所以被定义为只读,是因为只能从控件处理器中设置它:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey DurationPropertyKey =
BindableProperty.CreateReadOnly(nameof(Duration), typeof(TimeSpan), typeof(Video), new TimeSpan(),
propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());
public static readonly BindableProperty DurationProperty = DurationPropertyKey.BindableProperty;
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(DurationProperty); }
}
TimeSpan IVideoController.Duration
{
get { return Duration; }
set { SetValue(DurationPropertyKey, value); }
}
...
}
}
通常,只读可绑定属性在 Duration
属性上具有私有的 set
访问器,以允许在类中设置它。 但是,对于由处理器支持的 View 派生项,必须从类外部设置其属性,但只能由控件的处理程序来设置。
注意
Duration
可绑定属性的属性更改事件处理程序调用名为 SetTimeToEnd
的方法,该方法在计算结束时间中进行了介绍。
出于此原因,另一个属性被定义为 IVideoController.Duration
。 这是一个显式接口实现,由 Video
类实现的 IVideoController
接口使之成为可能:
public interface IVideoController
{
VideoStatus Status { get; set; }
TimeSpan Duration { get; set; }
}
该接口使 Video
外部的类可以通过引用 IVideoController
接口来设置 Duration
属性。 也可以从其他类和处理程序设置该属性,但不太可能会被无意设置。 最重要的是,不能通过数据绑定设置 Duration
属性。
设置 Video
控件的 Source
属性后,无法立即获取视频持续时间。 必须先下载部分视频文件,然后本机视图才能确定其持续时间。
Android
在 Android 上,VideoView.Duration
属性报告事件触发后 VideoView.Prepared
的有效持续时间(以毫秒为单位)。 MauiVideoPlayer
类使用 Prepared
事件处理程序获取 Duration
属性值:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
Video _video;
...
void OnVideoViewPrepared(object sender, EventArgs args)
{
...
((IVideoController)_video).Duration = TimeSpan.FromMilliseconds(_videoView.Duration);
}
...
}
}
iOS 和 Mac Catalyst
在 iOS 和 Mac Catalyst 中,视频持续时间从 AVPlayerItem.Duration
属性获取,但不是在创建 AVPlayerItem
后立即获得。 可以为 Duration
属性设置 iOS 观察程序,但 MauiVideoPlayer
类通过每秒调用 10 次的 UpdateStatus
方法获取持续时间:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayerItem _playerItem;
...
TimeSpan ConvertTime(CMTime cmTime)
{
return TimeSpan.FromSeconds(Double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
}
public void UpdateStatus()
{
...
if (_playerItem != null)
{
((IVideoController)_video).Duration = ConvertTime(_playerItem.Duration);
...
}
}
...
}
}
ConvertTime
方法将 CMTime
对象转换为 TimeSpan
值。
Windows
在 Windows 上,MediaPlayerElement.MediaPlayer.NaturalDuration
属性为 TimeSpan
值,后者在触发 MediaPlayerElement.MediaPlayer.MediaOpened
事件后变为有效。 MauiVideoPlayer
类使用 MediaOpened
事件处理程序获取 NaturalDuration
属性值:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
void OnMediaPlayerMediaOpened(MediaPlayer sender, object args)
{
MainThread.BeginInvokeOnMainThread(() =>
{
((IVideoController)_video).Duration = _mediaPlayerElement.MediaPlayer.NaturalDuration;
});
}
...
}
}
然后,OnMediaPlayer
事件处理程序调用 MainThread.BeginInvokeOnMainThread
方法来设置 Video
对象上的 Duration
属性,方法是通过将其强制转换为主线程上 IVideoController
。 这是必要的,因为 MediaPlayerElement.MediaPlayer.MediaOpened
事件是在后台线程上处理的。 有关在主线程上运行代码的详细信息,请参阅在 .NET MAUI UI 线程上创建线程。
Position
Video
控件还需要 Position
属性,该属性会在视频播放时从零增加到 Duration
。 Video
类将此属性实现为具有公共 get
和 set
访问器的可绑定属性:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
public static readonly BindableProperty PositionProperty =
BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(Video), new TimeSpan(),
propertyChanged: (bindable, oldValue, newValue) => ((Video)bindable).SetTimeToEnd());
public TimeSpan Position
{
get { return (TimeSpan)GetValue(PositionProperty); }
set { SetValue(PositionProperty, value); }
}
...
}
}
get
访问器返回视频播放时的当前位置。 set
访问器向前或向后移动视频位置,以便响应用户对定位工具栏的操作。
注意
Position
可绑定属性的属性更改事件处理程序调用名为 SetTimeToEnd
的方法,该方法在计算结束时间中进行了介绍。
在 Android、iOS 和 Mac Catalyst 上,获取当前位置的属性只有 get
访问器。 不过,Seek
方法可用于设置位置。 这似乎是一种比使用单个 Position
属性更明智的方法,后者本身存在固有问题。 视频播放时,必须不断更新 Position
属性,以反映新位置。 但你不希望对 Position
属性执行太多更改,导致视频播放器移动到视频中的新位置。 如果发生这种情况,视频播放器将通过查找 Position
属性的最后值来响应,并且视频不会前移。
尽管使用 get
和 set
访问器实现 Position
属性时会遇到困难,但还是采用了这种方法,因为它可以利用数据绑定。 Video
控件的 Position
属性可与 Slider 绑定,后者既用于显示位置,也用于寻找新位置。 但是,实现 Position
属性时,需要采取一些预防措施,以避免出现反馈循环。
Android
在 Android 上,VideoView.CurrentPosition
属性指示视频的当前位置。 MauiVideoPlayer
类在 UpdateStatus
方法中设置 Duration
属性的同时,还设置了 Position
属性:
using Android.Content;
using Android.Views;
using Android.Widget;
using AndroidX.CoordinatorLayout.Widget;
using VideoDemos.Controls;
using Color = Android.Graphics.Color;
using Uri = Android.Net.Uri;
namespace VideoDemos.Platforms.Android
{
public class MauiVideoPlayer : CoordinatorLayout
{
VideoView _videoView;
Video _video;
...
public void UpdateStatus()
{
...
TimeSpan timeSpan = TimeSpan.FromMilliseconds(_videoView.CurrentPosition);
_video.Position = timeSpan;
}
public void UpdatePosition()
{
if (Math.Abs(_videoView.CurrentPosition - _video.Position.TotalMilliseconds) > 1000)
{
_videoView.SeekTo((int)_video.Position.TotalMilliseconds);
}
}
...
}
}
每次 UpdateStatus
方法设置 Position
属性时,Position
属性都会触发 PropertyChanged
事件,这会导致处理程序的属性映射器调用 UpdatePosition
方法。 对于大多数属性更改,UpdatePosition
方法不应执行任何操作。 否则,视频位置每次发生更改时,它就会移动到刚刚到达的同一位置。 为避免这种反馈循环,UpdatePosition
只在 Position
属性与 VideoView
当前位置之差大于一秒时,才调用 VideoView
对象上的 Seek
方法。
iOS 和 Mac Catalyst
在 iOS 和 Mac Catalyst 上,AVPlayerItem.CurrentTime
属性指示视频的当前位置。 MauiVideoPlayer
类在 UpdateStatus
方法中设置 Duration
属性的同时,还设置了 Position
属性:
using AVFoundation;
using AVKit;
using CoreMedia;
using Foundation;
using System.Diagnostics;
using UIKit;
using VideoDemos.Controls;
namespace VideoDemos.Platforms.MaciOS
{
public class MauiVideoPlayer : UIView
{
AVPlayer _player;
AVPlayerItem _playerItem;
Video _video;
...
TimeSpan ConvertTime(CMTime cmTime)
{
return TimeSpan.FromSeconds(Double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
}
public void UpdateStatus()
{
...
if (_playerItem != null)
{
...
_video.Position = ConvertTime(_playerItem.CurrentTime);
}
}
public void UpdatePosition()
{
TimeSpan controlPosition = ConvertTime(_player.CurrentTime);
if (Math.Abs((controlPosition - _video.Position).TotalSeconds) > 1)
{
_player.Seek(CMTime.FromSeconds(_video.Position.TotalSeconds, 1));
}
}
...
}
}
每次 UpdateStatus
方法设置 Position
属性时,Position
属性都会触发 PropertyChanged
事件,这会导致处理程序的属性映射器调用 UpdatePosition
方法。 对于大多数属性更改,UpdatePosition
方法不应执行任何操作。 否则,视频位置每次发生更改时,它就会移动到刚刚到达的同一位置。 为避免这种反馈循环,UpdatePosition
只在 Position
属性与 AVPlayer
当前位置的时间差大于一秒时,才调用 AVPlayer
对象上的 Seek
方法。
Windows
在 Windows 上,MediaPlayerElement.MedaPlayer.Position
属性指示视频的当前位置。 MauiVideoPlayer
类在 UpdateStatus
方法中设置 Duration
属性的同时,还设置了 Position
属性:
using Microsoft.UI.Xaml.Controls;
using VideoDemos.Controls;
using Windows.Media.Core;
using Windows.Media.Playback;
using Windows.Storage;
using Grid = Microsoft.UI.Xaml.Controls.Grid;
namespace VideoDemos.Platforms.Windows
{
public class MauiVideoPlayer : Grid, IDisposable
{
MediaPlayerElement _mediaPlayerElement;
Video _video;
bool _isMediaPlayerAttached;
...
public void UpdateStatus()
{
if (_isMediaPlayerAttached)
{
...
_video.Position = _mediaPlayerElement.MediaPlayer.Position;
}
}
public void UpdatePosition()
{
if (_isMediaPlayerAttached)
{
if (Math.Abs((_mediaPlayerElement.MediaPlayer.Position - _video.Position).TotalSeconds) > 1)
{
_mediaPlayerElement.MediaPlayer.Position = _video.Position;
}
}
}
...
}
}
每次 UpdateStatus
方法设置 Position
属性时,Position
属性都会触发 PropertyChanged
事件,这会导致处理程序的属性映射器调用 UpdatePosition
方法。 对于大多数属性更改,UpdatePosition
方法不应执行任何操作。 否则,视频位置每次发生更改时,它就会移动到刚刚到达的同一位置。 为避免这种反馈循环,UpdatePosition
只在 Position
属性与 MediaPlayerElement
的当前位置的时间差大于一秒时,才设置 MediaPlayerElement.MediaPlayer.Position
属性。
计算结束时间
有时视频播放器会显示视频的剩余时间。 视频开始时,该值从视频的持续时间开始,视频结束时减小到零。
Video
类包含一个只读 TimeToEnd
属性,该属性根据 Duration
和 Position
属性的变化进行计算:
namespace VideoDemos.Controls
{
public class Video : View, IVideoController
{
...
private static readonly BindablePropertyKey TimeToEndPropertyKey =
BindableProperty.CreateReadOnly(nameof(TimeToEnd), typeof(TimeSpan), typeof(Video), new TimeSpan());
public static readonly BindableProperty TimeToEndProperty = TimeToEndPropertyKey.BindableProperty;
public TimeSpan TimeToEnd
{
get { return (TimeSpan)GetValue(TimeToEndProperty); }
private set { SetValue(TimeToEndPropertyKey, value); }
}
void SetTimeToEnd()
{
TimeToEnd = Duration - Position;
}
...
}
}
Duration
和 Position
属性的属性更改事件处理器会调用 SetTimeToEnd
方法。
自定义定位工具栏
自定义定位工具栏可以通过创建派生自 Slider 的类来实现,该类包含类型为 TimeSpan
的 Duration
和 Position
属性:
namespace VideoDemos.Controls
{
public class PositionSlider : Slider
{
public static readonly BindableProperty DurationProperty =
BindableProperty.Create(nameof(Duration), typeof(TimeSpan), typeof(PositionSlider), new TimeSpan(1),
propertyChanged: (bindable, oldValue, newValue) =>
{
double seconds = ((TimeSpan)newValue).TotalSeconds;
((Slider)bindable).Maximum = seconds <= 0 ? 1 : seconds;
});
public static readonly BindableProperty PositionProperty =
BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(PositionSlider), new TimeSpan(0),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
double seconds = ((TimeSpan)newValue).TotalSeconds;
((Slider)bindable).Value = seconds;
});
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(DurationProperty); }
set { SetValue(DurationProperty, value); }
}
public TimeSpan Position
{
get { return (TimeSpan)GetValue(PositionProperty); }
set { SetValue (PositionProperty, value); }
}
public PositionSlider()
{
PropertyChanged += (sender, args) =>
{
if (args.PropertyName == "Value")
{
TimeSpan newPosition = TimeSpan.FromSeconds(Value);
if (Math.Abs(newPosition.TotalSeconds - Position.TotalSeconds) / Duration.TotalSeconds > 0.01)
Position = newPosition;
}
};
}
}
}
Duration
属性的属性更改事件处理程序将 Slider 的 Maximum
属性设置为 TimeSpan
值的 TotalSeconds
属性。 同样,Position
属性的属性更改事件处理程序会设置 Slider 的 Value
属性。 这是 Slider 跟踪 PositionSlider
位置的机制。
只有在一种情况下,即用户操作 Slider,以指示视频应前进或倒退到新位置时,PositionSlider
才会从底层 Slider 更新。 这是在 PositionSlider
构造函数中的 PropertyChanged
处理程序中检测到的。 该事件处理程序检查 Value
属性中的更改,并且如果与 Position
属性不同,则会根据 Value
属性设置 Position
属性。
注册处理程序
自定义控件及其处理程序必须向应用注册,然后才能使用。 这应当发生在应用项目中 MauiProgram
类的 CreateMauiApp
方法中,这是应用的跨平台入口点:
using VideoDemos.Controls;
using VideoDemos.Handlers;
namespace VideoDemos;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(Video), typeof(VideoHandler));
});
return builder.Build();
}
}
处理程序使用 ConfigureMauiHandlers 和 AddHandler 方法注册。 AddHandler 方法的第一个参数是跨平台控件类型,第二个参数是其处理程序类型。
使用跨平台控件
向应用注册处理程序后,可以使用跨平台控件。
播放 Web 视频
如下列示例所示,Video
控件可以播放来自 URL 的视频:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.PlayWebVideoPage"
Unloaded="OnContentPageUnloaded"
Title="Play web video">
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
</ContentPage>
在此示例中,VideoSourceConverter
类将表示 URI 的字符串转换为 UriVideoSource
。 然后,视频会开始加载,并在下载和缓冲足够数量的数据后开始播放。 在每个平台上,如果未使用传输控件,则传输控件会淡出,但可以通过点击视频将其恢复。
播放视频资源
通过 MauiAsset 生成操作嵌入到应用 Resources\Raw 文件夹中的视频文件可由 Video
控件播放:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.PlayVideoResourcePage"
Unloaded="OnContentPageUnloaded"
Title="Play video resource">
<controls:Video x:Name="video"
Source="video.mp4" />
</ContentPage>
在此示例中,VideoSourceConverter
类将表示视频文件名的字符串转换为 ResourceVideoSource
。 对于每个平台,由于文件就在应用包上,不需要下载,因此视频几乎可在视频源设置完成后立即开始播放。 在每个平台上,如果未使用传输控件,则传输控件会淡出,但可以通过点击视频将其恢复。
从设备库播放视频文件
检索存储在设备上的视频文件,然后由 Video
控件播放:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.PlayLibraryVideoPage"
Unloaded="OnContentPageUnloaded"
Title="Play library video">
<Grid RowDefinitions="*,Auto">
<controls:Video x:Name="video" />
<Button Grid.Row="1"
Text="Show Video Library"
Margin="10"
HorizontalOptions="Center"
Clicked="OnShowVideoLibraryClicked" />
</Grid>
</ContentPage>
点击 Button 时,将执行其 Clicked
事件处理程序,如下列代码示例所示:
async void OnShowVideoLibraryClicked(object sender, EventArgs e)
{
Button button = sender as Button;
button.IsEnabled = false;
var pickedVideo = await MediaPicker.PickVideoAsync();
if (!string.IsNullOrWhiteSpace(pickedVideo?.FileName))
{
video.Source = new FileVideoSource
{
File = pickedVideo.FullPath
};
}
button.IsEnabled = true;
}
Clicked
事件处理程序使用 .NET MAUI 的 MediaPicker
类让用户从设备中选择视频文件。 然后,选择的视频文件封装为 FileVideoSource
对象,并设置为 Video
控件的 Source
属性。 有关 MediaPicker
类的详细信息,请参阅媒体选取器。 对于每个平台,由于文件就在设备上,不需要下载,因此视频几乎可在视频源设置完成后立即开始播放。 在每个平台上,如果未使用传输控件,则传输控件会淡出,但可以通过点击视频将其恢复。
配置视频控件
可以通过将 AutoPlay
属性设置为 false
来阻止视频自动启动:
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
AutoPlay="False" />
可以通过将 AreTransportControlsEnabled
属性设置为 false
来禁止显示传输控件:
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
AreTransportControlsEnabled="False" />
如果将 AutoPlay
和 AreTransportControlsEnabled
设置为 false
,则视频不会开始播放,且没有任何方法可以开始播放。 在这种情况下,需要调用代码隐藏文件中的 Play
方法,或者创建自己的传输控件。
此外,还可以通过将 IsLooping
属性设置为 true:
,将视频设置为循环播放
<controls:Video x:Name="video"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4"
IsLooping="true" />
如果将 IsLooping
属性设置为 true
,则可确保 Video
控件在到达其末尾后自动将视频位置设置为起点。
使用自定义传输控件
下列 XAML 示例显示播放、暂停和停止视频的自定义传输控件:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.CustomTransportPage"
Unloaded="OnContentPageUnloaded"
Title="Custom transport controls">
<Grid RowDefinitions="*,Auto">
<controls:Video x:Name="video"
AutoPlay="False"
AreTransportControlsEnabled="False"
Source="https://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4" />
<ActivityIndicator Color="Gray"
IsVisible="False">
<ActivityIndicator.Triggers>
<DataTrigger TargetType="ActivityIndicator"
Binding="{Binding Source={x:Reference video},
Path=Status}"
Value="{x:Static controls:VideoStatus.NotReady}">
<Setter Property="IsVisible"
Value="True" />
<Setter Property="IsRunning"
Value="True" />
</DataTrigger>
</ActivityIndicator.Triggers>
</ActivityIndicator>
<Grid Grid.Row="1"
Margin="0,10"
ColumnDefinitions="0.5*,0.5*"
BindingContext="{x:Reference video}">
<Button Text="▶️ Play"
HorizontalOptions="Center"
Clicked="OnPlayPauseButtonClicked">
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.Playing}">
<Setter Property="Text"
Value="⏸ Pause" />
</DataTrigger>
<DataTrigger TargetType="Button"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.NotReady}">
<Setter Property="IsEnabled"
Value="False" />
</DataTrigger>
</Button.Triggers>
</Button>
<Button Grid.Column="1"
Text="⏹ Stop"
HorizontalOptions="Center"
Clicked="OnStopButtonClicked">
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.NotReady}">
<Setter Property="IsEnabled"
Value="False" />
</DataTrigger>
</Button.Triggers>
</Button>
</Grid>
</Grid>
</ContentPage>
在此示例中,Video
控件将 AreTransportControlsEnabled
属性设置为 false
,并定义了播放和暂停视频的 Button 和停止视频播放的 Button 。 按钮外观使用 Unicode 字符及其文本等效项定义的,以创建由图标和文本组成的按钮:
播放视频时,播放按钮将更新为暂停按钮:
UI 还包括在视频加载时显示的 ActivityIndicator。 数据触发器用于启用和禁用 ActivityIndicator 和按钮,并在“播放”和“暂停”之间切换第一个按钮。 有关数据触发器的详细信息,请参阅数据触发器。
代码隐藏文件定义按钮 Clicked
事件的事件处理程序:
public partial class CustomTransportPage : ContentPage
{
...
void OnPlayPauseButtonClicked(object sender, EventArgs args)
{
if (video.Status == VideoStatus.Playing)
{
video.Pause();
}
else if (video.Status == VideoStatus.Paused)
{
video.Play();
}
}
void OnStopButtonClicked(object sender, EventArgs args)
{
video.Stop();
}
...
}
自定义定位工具栏
下列示例显示了在 XAML 中使用的自定义定位工具栏 PositionSlider
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:VideoDemos.Controls"
x:Class="VideoDemos.Views.CustomPositionBarPage"
Unloaded="OnContentPageUnloaded"
Title="Custom position bar">
<Grid RowDefinitions="*,Auto,Auto">
<controls:Video x:Name="video"
AreTransportControlsEnabled="False"
Source="{StaticResource ElephantsDream}" />
...
<Grid Grid.Row="1"
Margin="10,0"
ColumnDefinitions="0.25*,0.25*,0.25*,0.25*"
BindingContext="{x:Reference video}">
<Label Text="{Binding Path=Position,
StringFormat='{0:hh\\:mm\\:ss}'}"
HorizontalOptions="Center"
VerticalOptions="Center" />
...
<Label Grid.Column="3"
Text="{Binding Path=TimeToEnd,
StringFormat='{0:hh\\:mm\\:ss}'}"
HorizontalOptions="Center"
VerticalOptions="Center" />
</Grid>
<controls:PositionSlider Grid.Row="2"
Margin="10,0,10,10"
BindingContext="{x:Reference video}"
Duration="{Binding Duration}"
Position="{Binding Position}">
<controls:PositionSlider.Triggers>
<DataTrigger TargetType="controls:PositionSlider"
Binding="{Binding Status}"
Value="{x:Static controls:VideoStatus.NotReady}">
<Setter Property="IsEnabled"
Value="False" />
</DataTrigger>
</controls:PositionSlider.Triggers>
</controls:PositionSlider>
</Grid>
</ContentPage>
Video
对象的 Position
属性绑定到 PositionSlider
的 Position
属性,不会产生性能问题,因为 Video.Position
属性由每个平台上的 MauiVideoPlayer.UpdateStatus
方法更改,方法每秒仅调用 10 次。 此外,两个 Label 对象显示 Video
对象的 Position
和 TimeToEnd
属性值。
本机视图清理
每个平台的处理程序实现都会重写 DisconnectHandler 实现,该实现用于执行本地视图清理,如取消订阅事件和释放对象。 但是,.NET MAUI 有意不调用此重写函数。 实际上,必须从应用生命周期中的合适位置自行调用它。 这种情况通常发生在离开包含 Video
控件的页面时,这会触发页面的 Unloaded
事件。
页面的 Unloaded
事件的事件处理程序可在 XAML 中注册:
<ContentPage ...
xmlns:controls="clr-namespace:VideoDemos.Controls"
Unloaded="OnContentPageUnloaded">
<controls:Video x:Name="video"
... />
</ContentPage>
然后,Unloaded
事件的事件处理程序可以在其 Handler
实例上调用 DisconnectHandler 方法:
void OnContentPageUnloaded(object sender, EventArgs e)
{
video.Handler?.DisconnectHandler();
}
除了清理本机视图资源外,调用处理程序 DisconnectHandler 的方法还可确保视频在 iOS 上向后导航时停止播放视频。
控制处理程序断开连接
每个平台的处理程序实现都会重写 DisconnectHandler 实现,该实现用于执行本地视图清理,如取消订阅事件和释放对象。 默认情况下,处理程序会尽可能自动与其控件断开连接,例如在应用中向后导航时。
在某些情况下,你可能希望控制处理程序何时与其控件断开连接,这可以通过附加属性实现 HandlerProperties.DisconnectPolicy
。 此属性需要一个 HandlerDisconnectPolicy 参数,枚举定义以下值:
Automatic
,指示处理程序将自动断开连接。 这是附加属性HandlerProperties.DisconnectPolicy
的默认值。Manual
,指示处理程序必须通过调用 DisconnectHandler() 实现来手动断开连接。
以下示例演示如何设置 HandlerProperties.DisconnectPolicy
附加属性:
<controls:Video x:Name="video"
HandlerProperties.DisconnectPolicy="Manual"
Source="video.mp4"
AutoPlay="False" />
等效 C# 代码如下:
Video video = new Video
{
Source = "video.mp4",
AutoPlay = false
};
HandlerProperties.SetDisconnectPolicy(video, HandlerDisconnectPolicy.Manual);
将附加属性设置为HandlerProperties.DisconnectPolicy
Manual
时,必须从应用的生命周期中的合适位置自行调用处理程序DisconnectHandler的实现。 这可以通过调用 video.Handler?.DisconnectHandler();
来实现。
此外,还有一种 DisconnectHandlers 扩展方法可将处理程序与给定 IView 断开连接:
video.DisconnectHandlers();
断开连接时,DisconnectHandlers 方法将沿控件树向下传播,直到完成或到达已设置手动策略的控件。