Introduction
Social media apps like Instagram use full-page scrollable video controls called "reels" (named after the device used to store film - or garden hoses) which allow the user to swipe or scroll up and down in order to switch between videos (or other content).
In this blog post, I will demonstrate how you can easily add a feature like a video reel to your own .NET MAUI app in a few simple steps. The idea is that we can scroll up and down and the video that becomes visible will start playing while the previously playing video gets stopped. All we need for this is a CollectionView and the MediaElement from the MAUI Community Toolkit (MCT).
After defining the model, we'll first have a look at how we can accomplish the desired layout, which is a full-page view that allows to swipe up and down and always stops at the next element. Afterwards, we'll implement the automatic play/stop functionality.
This post is based on a Stack Overflow answer that I recently wrote.
As always, you can have a look at the full code in the sample repository on GitHub.
Let's do it!
Model
The model for the reel is quite simple, we just need to create a VideoModel
with a few properties, e.g. Title
, VideoUri
and IsPlaying
:
public partial class VideoModel(string title, string videoUri) : ObservableObject
{
public string Title { get; } = title;
public string VideoUri { get; } = videoUri;
[ObservableProperty]
private bool _isPlaying;
}
The IsPlaying
property is an auto-generated observable property, which we will need later on to control the automatic play/stop functionality.
ViewModel
In the ViewModel, we can define several instances of VideoModel
using some sample video files that are stored locally in the repository, like so:
public partial class ReelViewModel : ObservableObject
{
private const string FrogVideo = "https://github.com/ewerspej/maui-samples/blob/main/assets/frog.mp4?raw=true";
private const string BuckVideo = "https://github.com/ewerspej/maui-samples/blob/main/assets/bigbuckbunny.mp4?raw=true";
[ObservableProperty]
private ObservableCollection<VideoModel> _videos;
public ReelViewModel()
{
Videos =
[
new VideoModel("First", FrogVideo),
new VideoModel("Second", BuckVideo),
new VideoModel("Third", FrogVideo),
new VideoModel("Fourth", BuckVideo),
new VideoModel("Fifth", FrogVideo),
new VideoModel("Sixth", BuckVideo)
];
}
}
Later on, we can then bind to the Videos
collection in XAML.
Basic Layout
In order to create the reel, we first need to add a CollectionView inside a Grid. The CollectionView will host the VideoModel
instances. We want only a single item to be visible at a time, so we'll add a LinearItemsLayout and give the layout in the DataTemplate the sameHeightRequest
as the CollectionView. In order to make the scrolling of the CollectionView stop at the next element, we'll also need to use snap points.
<Grid>
<CollectionView
HeightRequest="500"
HorizontalOptions="Fill"
VerticalOptions="Center"
ItemsSource="{Binding Videos}"
Scrolled="ItemsView_OnScrolled">
<CollectionView.ItemsLayout>
<LinearItemsLayout
Orientation="Vertical"
SnapPointsType="MandatorySingle"
SnapPointsAlignment="Center" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:VideoModel">
<Grid
HeightRequest="500"
HorizontalOptions="Fill"
BackgroundColor="LightGreen">
<Label
Text="{Binding Title}"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="Title" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
The result should something like this and we can scroll back and forth between the elements, with always only a single item visible at a time:
That's already pretty neat. Next, let's add the video player and the automatic play/stop functionality.
Automatic Play/Stop
Before we can implement the play/stop functionality, we need to install the CommunityToolkit.Maui.MediaElement package from nuget.org and initialize the MediaElement in our MauiProgram class before we can use it:
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkitMediaElement() // add this line
In order to automatically play a video that is hosted in a MediaElement, we need to work with a little trick. Since the MediaElement doesn't expose any bindable and settable IsPlaying
property, we need to add that ourselves by extending the class with a BindableProperty:
public class ExtendedMediaElement : MediaElement
{
public bool IsPlaying
{
get => (bool)GetValue(IsPlayingProperty);
set => SetValue(IsPlayingProperty, value);
}
public static readonly BindableProperty IsPlayingProperty = BindableProperty.Create(nameof(IsPlaying), typeof(bool), typeof(ExtendedMediaElement), false, propertyChanged: OnIsPlayingPropertyChanged);
private static void OnIsPlayingPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var mediaElement = (ExtendedMediaElement)bindable;
if (newValue is true)
{
mediaElement.Play();
}
else
{
mediaElement.Stop();
}
}
}
By using the OnIsPlayingPropertyChanged
event handler, we can decide whether to play or stop the video whenever the IsPlaying
property gets updated (via a binding).
With this in place, we can now add our ExtendedMediaElement to the XAML:
<DataTemplate x:DataType="models:VideoModel">
<Grid
HeightRequest="{Binding Height, Source={x:Reference Reels}}"
HorizontalOptions="Fill">
<media:ExtendedMediaElement
Aspect="Fill"
Source="{Binding VideoUri}"
ShouldAutoPlay="False"
ShouldLoopPlayback="True"
ShouldShowPlaybackControls="False"
IsPlaying="{Binding IsPlaying}" />
<Label
Text="{Binding Title}"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="Title" />
</Grid>
</DataTemplate>
Now, a video is only played and stopped whenever the IsPlaying
property of the associated VideoModel
instance changes.
However, we're not entirely done yet, one important part is still missing. We want the videos to automatically start and stop whenever an element is scrolled into and out of view. For this, we need to hook up an event handler for the Scrolled
event of the CollectionView, where we then update the IsPlaying
property of each item in the underlying collection:
<CollectionView
HeightRequest="{Binding Height, Source={x:Reference Reels}}"
HorizontalOptions="Fill"
VerticalOptions="Center"
ItemsSource="{Binding Videos}"
Scrolled="ItemsView_OnScrolled">
<!-- ... -->
</CollectionView>
The event handler lives in the code-behind of our page and iterates through the Videos
collection to update the IsPlaying
property of the VideoModel
instances:
public partial class ReelPage : ContentPage
{
private readonly ReelViewModel _vm;
public ReelPage(ReelViewModel vm)
{
InitializeComponent();
BindingContext = _vm = vm;
}
private void ItemsView_OnScrolled(object sender, ItemsViewScrolledEventArgs e)
{
var itemIndex = e.CenterItemIndex;
_vm.Videos[itemIndex].IsPlaying = true;
foreach (var myModel in _vm.Videos)
{
if (myModel != _vm.Videos[itemIndex])
{
myModel.IsPlaying = false;
}
}
}
}
Almost there. Now, the automatic play/stop should already be working. Let's touch up the XAML a little bit and have a look at the final result next.
Final Full-Page Layout
Last, but not least, in order to have the CollectionView take up the entire available space of the page, we can simply bind the HeightRequest
of the CollectionView as well as the Grid in the DataTemplate to the rendered Height
of the page to end up with the following final layout:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="MauiSamples.Views.ReelPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:MauiSamples.Models"
xmlns:viewModels="clr-namespace:MauiSamples.ViewModels"
xmlns:media="clr-namespace:MauiSamples.CustomControls.Media"
x:Name="Reels"
x:DataType="viewModels:ReelViewModel"
Shell.NavBarIsVisible="False">
<Grid>
<CollectionView
HeightRequest="{Binding Height, Source={x:Reference Reels}}"
HorizontalOptions="Fill"
VerticalOptions="Center"
ItemsSource="{Binding Videos}"
Scrolled="ItemsView_OnScrolled">
<CollectionView.ItemsLayout>
<LinearItemsLayout
Orientation="Vertical"
SnapPointsType="MandatorySingle"
SnapPointsAlignment="Center" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:VideoModel">
<Grid
HeightRequest="{Binding Height, Source={x:Reference Reels}}"
HorizontalOptions="Fill">
<media:ExtendedMediaElement
Aspect="AspectFill"
Source="{Binding VideoUri}"
ShouldAutoPlay="False"
ShouldLoopPlayback="True"
ShouldShowPlaybackControls="False"
IsPlaying="{Binding IsPlaying}" />
<Label
Text="{Binding Title}"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="Title" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
Now, when we run the app, we have a full-page reel control:
๐ Fantastic, I'm excited about the result.
Conclusions and next steps
As I demonstrated here, adding a scrollable video control similar to Instagram's reels to your .NET MAUI app is quite simple. A neat addition to this would be to make the entire page fullscreen, but that would be too much for a single blog post. The main focus here was supposed to be on how to combine the CollectionView and the MediaElement to create a reel and that's surprisingly easy to achieve.
Note: I didn't touch on any issues regarding caching or performance in this post. When the list of videos gets very long or when items are incrementally loaded, e.g. when using the infinite scroll capabilities of CollectionView, we might observe performance degradation, which should then be addressed separately.
Thanks to Gerald Versluis for pointing this out.
If you enjoyed this blog post, then follow me on LinkedIn, subscribe to this blog and star the GitHubrepositoryfor this post so you don't miss out on any future posts and developments.
Attributions
Title photo by Markus Spiske on Unsplash