Одной из самых крутых тенденций в дизайне мобильных пользовательских интерфейсов, смело можно назвать использование видео в качестве фона для предоставления. Как пример, приложения Tumblr, Spotify и Vine. В этой статье мы разберём то, как реализовать аналогичное решение в приложении Xamarin.Forms, а в конце расскажем о меророиятии, которое скоро пройдёт в СПб. Всё, что нам нужно, это реализовать два пользовательских рендерера для Android и для iOS по отдельности.
BackgroundVideo
. Теперь давайте перейдём к библиотеке PCL и создадим новый класс с именем Video
, унаследованный от Xamarin.Forms.View
.using System;
using Xamarin.Forms;
namespace BackgroundVideo.Controls
{
public class Video : View
{
}
}
Source
. Это строка для определения того, какой видеофайл следует воспроизвести. В iOS свойство Source
имеет отношение к каталогу Resources
, тогда как в Android оно относится к каталогу Assets
.public static readonly BindableProperty SourceProperty =
BindableProperty.Create(
nameof(Source),
typeof(string),
typeof(Video),
string.Empty,
BindingMode.TwoWay);
public string Source
{
get { return (string)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
Loop
. Изначально это значение задано как true
, поэтому при задании свойства источника видео — Source
это свойство будет зацикливаться по умолчанию.Action
под названием OnFinishedPlaying
. Можно изменить его на событие или на то, что будет удобно.public static readonly BindableProperty LoopProperty =
BindableProperty.Create(
nameof(Loop),
typeof(bool),
typeof(Video),
true,
BindingMode.TwoWay);
public bool Loop
{
get { return (bool)GetValue(LoopProperty); }
set { SetValue(LoopProperty, value); }
}
public Action OnFinishedPlaying { get; set; }
VideoRenderer
, который будет наследовать от ViewRenderer<Video, UIView>
. Идея состоит в том, чтобы использовать нативный видеопроигрыватель iOS с помощью класса MPMoviePlayerController
и установить его нативный элемент управления в наше настраиваемое Video
представление. Нам также понадобится NSObject
, чтобы анализировать событие от видеопроигрывателя, определяя закончилось оно или нет.using System;
using System.IO;
using BackgroundVideo.Controls;
using BackgroundVideo.iOS.Renderers;
using Foundation;
using MediaPlayer;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(Video), typeof(VideoRenderer))]
namespace BackgroundVideo.iOS.Renderers
{
public class VideoRenderer : ViewRenderer<Video, UIView>
{
MPMoviePlayerController videoPlayer;
NSObject notification = null;
}
}
Source
в узле Resources или нет. Если его там нет, тогда отобразится пустое представление.MPMoviePlayerController
и интерпретировать расположение файла видео как NSUrl
. Чтобы сделать пользовательский элемент управления ясным, без границы или чего-либо ещё, нужно установить ControlStyle
на MPMovieControlStyle.None
, а цвет фона на UIColor.Clear
.ScalingMode
у видеопроигрывателя на MPMovieScalingMode.AspectFill
.Loop
, определяющее, будет ли воспроизведение видео циклическим или нет. Чтобы установить цикл, нужно изменить RepeatMode
у видеопроигрывателя на MPMovieRepeatMode.One
. В противном случае установите его на MPMovieRepeatMode.None
.PrepareToPlay()
. Чтобы отобразить видео в пользовательском элементе управления, необходимо использовать функцию SetNativeControl()
.void InitVideoPlayer()
{
var path = Path.Combine(NSBundle.MainBundle.BundlePath, Element.Source);
if (!NSFileManager.DefaultManager.FileExists(path))
{
Console.WriteLine("Video not exist");
videoPlayer = new MPMoviePlayerController();
videoPlayer.ControlStyle = MPMovieControlStyle.None;
videoPlayer.ScalingMode = MPMovieScalingMode.AspectFill;
videoPlayer.RepeatMode = MPMovieRepeatMode.One;
videoPlayer.View.BackgroundColor = UIColor.Clear;
SetNativeControl(videoPlayer.View);
return;
}
// Load the video from the app bundle.
NSUrl videoURL = new NSUrl(path, false);
// Create and configure the movie player.
videoPlayer = new MPMoviePlayerController(videoURL);
videoPlayer.ControlStyle = MPMovieControlStyle.None;
videoPlayer.ScalingMode = MPMovieScalingMode.AspectFill;
videoPlayer.RepeatMode = Element.Loop ? MPMovieRepeatMode.One : MPMovieRepeatMode.None;
videoPlayer.View.BackgroundColor = UIColor.Clear;
foreach (UIView subView in videoPlayer.View.Subviews)
{
subView.BackgroundColor = UIColor.Clear;
}
videoPlayer.PrepareToPlay();
SetNativeControl(videoPlayer.View);
}
OnElementChanged
и OnElementPropertyChanged
так, чтобы с кодом можно было функционально работать из проекта Xamarin.Forms. В OnElementChanged
мы должны ожидать события окончания воспроизведения видеопроигрывателя и вызывать команду OnFinishedPlaying
. Следующий фрагмент является простейшим кодом, необходимым для того, чтобы это все работало.protected override void OnElementChanged(ElementChangedEventArgs<Video> e)
{
base.OnElementChanged(e);
if (Control == null)
{
InitVideoPlayer();
}
if (e.OldElement != null)
{
// Unsubscribe
notification?.Dispose();
}
if (e.NewElement != null)
{
// Subscribe
notification = MPMoviePlayerController.Notifications.ObservePlaybackDidFinish((sender, args) =>
{
/* Access strongly typed args */
Console.WriteLine("Notification: {0}", args.Notification);
Console.WriteLine("FinishReason: {0}", args.FinishReason);
Element?.OnFinishedPlaying?.Invoke();
});
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (Element == null || Control == null)
return;
if (e.PropertyName == Video.SourceProperty.PropertyName)
{
InitVideoPlayer();
}
else if (e.PropertyName == Video.LoopProperty.PropertyName)
{
var liveImage = Element as Video;
if (videoPlayer != null)
videoPlayer.RepeatMode = Element.Loop ? MPMovieRepeatMode.One : MPMovieRepeatMode.None;
}
}
VideoRenderer
. Мы наследуем этот рендерер с помощью ViewRenderer<Video, FrameLayout>
, и это означает, что он будет отображаться как FrameLayout
в нативном элементе управления Android.TextureView
, если же вам этого недостаточно, тогда нужно будет также осуществить реализацию с помощью VideoView
.VideoView
здесь не оптимальна. Возможно, Вы заметите некоторое мерцание. Вот поэтому я добавил пустое представление под названием _placeholder
. Оно будет отображаться в том случае, когда видео не воспроизводится или при изменении источника видео. Если видеофайл готов для воспроизведения и отображения, тогда _placeholder
будет скрыт.using System;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Media;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using BackgroundVideo.Controls;
using BackgroundVideo.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(Video), typeof(VideoRenderer))]
namespace BackgroundVideo.Droid.Renderers
{
public class VideoRenderer : ViewRenderer<Video, FrameLayout>,
TextureView.ISurfaceTextureListener,
ISurfaceHolderCallback
{
private bool _isCompletionSubscribed = false;
private FrameLayout _mainFrameLayout = null;
private Android.Views.View _mainVideoView = null;
private Android.Views.View _placeholder = null;
}
}
MediaPlayer
. Нам следует использовать этот объект и убедиться в том, что он создан только единожды. Мы можем повторно использовать один и тот же объект при изменении источника видео.Completion
, чтобы реализовать обратный вызов OnFinishedPlaying
. Кроме того, необходимо задать значение Looping
для настраиваемого свойства Loop
.AdjustTextureViewAspect()
. Эта функция будет вызываться в обратном вызове VideoSizeChanged
. Мы расскажем об этой реализации позже.private MediaPlayer _videoPlayer = null;
internal MediaPlayer VideoPlayer
{
get
{
if (_videoPlayer == null)
{
_videoPlayer = new MediaPlayer();
if (!_isCompletionSubscribed)
{
_isCompletionSubscribed = true;
_videoPlayer.Completion += Player_Completion;
}
_videoPlayer.VideoSizeChanged += (sender, args) =>
{
AdjustTextureViewAspect(args.Width, args.Height);
};
_videoPlayer.Info += (sender, args) =>
{
Console.WriteLine("onInfo what={0}, extra={1}", args.What, args.Extra);
if (args.What == MediaInfo.VideoRenderingStart)
{
Console.WriteLine("[MEDIA_INFO_VIDEO_RENDERING_START] placeholder GONE");
_placeholder.Visibility = ViewStates.Gone;
}
};
_videoPlayer.Prepared += (sender, args) =>
{
_mainVideoView.Visibility = ViewStates.Visible;
_videoPlayer.Start();
if (Element != null)
_videoPlayer.Looping = Element.Loop;
};
}
return _videoPlayer;
}
}
private void Player_Completion(object sender, EventArgs e)
{
Element?.OnFinishedPlaying?.Invoke();
}
Source
. Пожалуйста, помните, что видеофайл на Android должен храниться в каталоге Assets
. Этот файл можно открыть с помощью функции Assets.OpenFd(fullPath)
.Java.IO.IOException
. Это значит, что в видеоконтейнере ничего отображать не нужно.Prepared
и отображается видео в одном из реализованных на предыдущем этапе видеопредставлений.private void PlayVideo(string fullPath)
{
Android.Content.Res.AssetFileDescriptor afd = null;
try
{
afd = Context.Assets.OpenFd(fullPath);
}
catch (Java.IO.IOException ex)
{
Console.WriteLine("Play video: " + Element.Source + " not found because " + ex);
_mainVideoView.Visibility = ViewStates.Gone;
}
catch (Exception ex)
{
Console.WriteLine("Error openfd: " + ex);
_mainVideoView.Visibility = ViewStates.Gone;
}
if (afd != null)
{
Console.WriteLine("Lenght " + afd.Length);
VideoPlayer.Reset();
VideoPlayer.SetDataSource(afd.FileDescriptor, afd.StartOffset, afd.Length);
VideoPlayer.PrepareAsync();
}
}
TextureView
. Плохая новость состоит в том, что на данный момент я не знаю, как это реализовать с VideoView
. Но это лучше, чем ничего, верно?TextureView
. Таким образом, масштабирование видео происходит по верхней или по нижней части в зависимости от размеров видео и предоставления. Затем, после масштабирования, видео располагается в центре представления.TextureView
и VideoView
. Это будет осуществляться в рамках функции OnElementChanged
. В случае с обеими реализациями используются одни и те же свойства. Мы сделаем цвет фона прозрачным и приведем параметры макета в соответствие с родительским элементом. Таким образом, у фона не будет цвета, который мог бы отображаться при отсутствии видео, и этот фон заполнит весь контейнер. private void AdjustTextureViewAspect(int videoWidth, int videoHeight)
{
if (!(_mainVideoView is TextureView))
return;
if (Control == null)
return;
var control = Control;
var textureView = _mainVideoView as TextureView;
var controlWidth = control.Width;
var controlHeight = control.Height;
var aspectRatio = (double)videoHeight / videoWidth;
int newWidth, newHeight;
if (controlHeight <= (int)(controlWidth * aspectRatio))
{
// limited by narrow width; restrict height
newWidth = controlWidth;
newHeight = (int)(controlWidth * aspectRatio);
}
else
{
// limited by short height; restrict width
newWidth = (int)(controlHeight / aspectRatio);
newHeight = controlHeight;
}
int xoff = (controlWidth - newWidth) / 2;
int yoff = (controlHeight - newHeight) / 2;
Console.WriteLine("video=" + videoWidth + "x" + videoHeight +
" view=" + controlWidth + "x" + controlHeight +
" newView=" + newWidth + "x" + newHeight +
" off=" + xoff + "," + yoff);
var txform = new Matrix();
textureView.GetTransform(txform);
txform.SetScale((float)newWidth / controlWidth, (float)newHeight / controlHeight);
txform.PostTranslate(xoff, yoff);
textureView.SetTransform(txform);
}
protected override void OnElementChanged(ElementChangedEventArgs<Video> e)
{
base.OnElementChanged(e);
if (Control == null)
{
_mainFrameLayout = new FrameLayout(Context);
_placeholder = new Android.Views.View(Context)
{
Background = new ColorDrawable(Xamarin.Forms.Color.Transparent.ToAndroid()),
LayoutParameters = new LayoutParams(
ViewGroup.LayoutParams.MatchParent,
ViewGroup.LayoutParams.MatchParent),
};
if (Build.VERSION.SdkInt < BuildVersionCodes.IceCreamSandwich)
{
Console.WriteLine("Using VideoView");
var videoView = new VideoView(Context)
{
Background = new ColorDrawable(Xamarin.Forms.Color.Transparent.ToAndroid()),
Visibility = ViewStates.Gone,
LayoutParameters = new LayoutParams(
ViewGroup.LayoutParams.MatchParent,
ViewGroup.LayoutParams.MatchParent),
};
ISurfaceHolder holder = videoView.Holder;
if (Build.VERSION.SdkInt < BuildVersionCodes.Honeycomb)
{
holder.SetType(SurfaceType.PushBuffers);
}
holder.AddCallback(this);
_mainVideoView = videoView;
}
else
{
Console.WriteLine("Using TextureView");
var textureView = new TextureView(Context)
{
Background = new ColorDrawable(Xamarin.Forms.Color.Transparent.ToAndroid()),
Visibility = ViewStates.Gone,
LayoutParameters = new LayoutParams(
ViewGroup.LayoutParams.MatchParent,
ViewGroup.LayoutParams.MatchParent),
};
textureView.SurfaceTextureListener = this;
_mainVideoView = textureView;
}
_mainFrameLayout.AddView(_mainVideoView);
_mainFrameLayout.AddView(_placeholder);
SetNativeControl(_mainFrameLayout);
PlayVideo(Element.Source);
}
if (e.OldElement != null)
{
// Unsubscribe
if (_videoPlayer != null && _isCompletionSubscribed)
{
_isCompletionSubscribed = false;
_videoPlayer.Completion -= Player_Completion;
}
}
if (e.NewElement != null)
{
// Subscribe
if (_videoPlayer != null && !_isCompletionSubscribed)
{
_isCompletionSubscribed = true;
_videoPlayer.Completion += Player_Completion;
}
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (Element == null || Control == null)
return;
if (e.PropertyName == Video.SourceProperty.PropertyName)
{
Console.WriteLine("Play video: " + Element.Source);
PlayVideo(Element.Source);
}
else if (e.PropertyName == Video.LoopProperty.PropertyName)
{
Console.WriteLine("Is Looping? " + Element.Loop);
VideoPlayer.Looping = Element.Loop;
}
}
TextureView
и VideoView
, тут следует реализовать некоторые функции из интерфейсов. Одна из них предназначается для удаления видео при разрушении texture (текстуры) или surface (поверхности). Чтобы это сделать, нам нужно установить видимость >_placeholder
на visible.private void RemoveVideo()
{
_placeholder.Visibility = ViewStates.Visible;
}
TextureView.ISurfaceTextureListener
. Мы установили surface видеопроигрывателя на тот случай, когда texture доступна и указали сокрытие surface при уничтожении texture. В следующем фрагменте показано, как это реализовать.#region Surface Texture Listener
public void OnSurfaceTextureAvailable(SurfaceTexture surface, int width, int height)
{
Console.WriteLine("Surface.TextureAvailable");
VideoPlayer.SetSurface(new Surface(surface));
}
public bool OnSurfaceTextureDestroyed(SurfaceTexture surface)
{
Console.WriteLine("Surface.TextureDestroyed");
RemoveVideo();
return false;
}
public void OnSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)
{
Console.WriteLine("Surface.TextureSizeChanged");
}
public void OnSurfaceTextureUpdated(SurfaceTexture surface)
{
Console.WriteLine("Surface.TextureUpdated");
}
#endregion
VideoView
необходимо реализовать интерфейс ISurfaceHolderCallback
. Аналогично TextureView
, мы установили дисплей видеопроигрывателя на создание surface и указали его сокрытие при уничтожении surface. Полную реализация этого интерфейса можно рассмотреть на следующем фрагменте.#region Surface Holder Callback
public void SurfaceChanged(ISurfaceHolder holder, [GeneratedEnum] Format format, int width, int height)
{
Console.WriteLine("Surface.Changed");
}
public void SurfaceCreated(ISurfaceHolder holder)
{
Console.WriteLine("Surface.Created");
VideoPlayer.SetDisplay(holder);
}
public void SurfaceDestroyed(ISurfaceHolder holder)
{
Console.WriteLine("Surface.Destroyed");
RemoveVideo();
}
#endregion
ffmpeg
и изменить базовый профиль по своему вкусу. Обратитесь к следующей таблице, чтобы проверить совместимость базовых профилей с iOS. Ознакомьтесь также с материалом Поддерживаемые форматы носителей из официального руководства Android.Assets
. В iOS его следует поместить в каталог Resources
. В этом примере я поместил файл в раздел Assets/Videos
у Android и в Resources/Videos
у iOS.<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BackgroundVideo"
xmlns:controls="clr-namespace:BackgroundVideo.Controls"
x:Class="BackgroundVideo.BackgroundVideoPage">
<Grid Padding="0" RowSpacing="0" ColumnSpacing="0">
<controls:Video x:Name="video" Source="Videos/Orchestra.mp4" Loop="true"
HorizontalOptions="Fill" VerticalOptions="Fill" />
<StackLayout VerticalOptions="Center" HorizontalOptions="FillAndExpand" Padding="20,10,10,20">
<Entry Placeholder="username" FontSize="Large"
FontFamily="Georgia" HeightRequest="50">
<Entry.PlaceholderColor>
<OnPlatform x:TypeArguments="Color" Android="Silver" />
</Entry.PlaceholderColor>
<Entry.TextColor>
<OnPlatform x:TypeArguments="Color" Android="White" />
</Entry.TextColor>
</Entry>
<Entry Placeholder="password" FontSize="Large"
FontFamily="Georgia" HeightRequest="50" IsPassword="true">
<Entry.PlaceholderColor>
<OnPlatform x:TypeArguments="Color" Android="Silver" />
</Entry.PlaceholderColor>
<Entry.TextColor>
<OnPlatform x:TypeArguments="Color" Android="White" />
</Entry.TextColor>
</Entry>
<BoxView Color="Transparent" HeightRequest="10" />
<Button Text="sign in" BackgroundColor="#3b5998" TextColor="#ffffff"
FontSize="Large" />
<Button Text="sign up" BackgroundColor="#fa3c4c" TextColor="#ffffff"
FontSize="Large" />
</StackLayout>
</Grid>
</ContentPage>
Loop
. Если требуется сделать что-то по завершении видео, просто установите OnFinishedPlaying
из кода C#. Теперь посмотрим, как это все работает.К сожалению, не доступен сервер mySQL