Search code examples
.netmauicardviewswipeview

.NET MAUI Vertically SwipeView (tiktok slide etc.)


I need to view the items in my list by scrolling up or down in .NET MAUI. There are videos in my list that I show using YoutubeApi and MediaElement. (Tiktok, Instagram Reels, etc.) What I want to do here;

Whichever item is currently visible, the MediaElement.Play() of that item should work. I couldn't find a proper scrolling event anyway. Does anyone have any ideas about this?

enter image description here

I have tried all the attached ways to do this but without success.


Solution

  • Layout

    That kind of layout and scrolling behavior can easily be achieved using a CollectionView with a linear layout and snap points:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage
      x:Class="FullPageSnapCollection.MainPage"
      xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
      xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
      xmlns:fullPageSnapCollection="clr-namespace:FullPageSnapCollection"
      x:DataType="fullPageSnapCollection:MainViewModel">
    
      <Grid>
    
        <CollectionView
          HeightRequest="500"
          HorizontalOptions="Fill"
          VerticalOptions="Center"
          ItemsSource="{Binding Items}">
    
          <CollectionView.ItemsLayout>
            <LinearItemsLayout
              Orientation="Vertical"
              SnapPointsType="MandatorySingle"
              SnapPointsAlignment="Start" />
          </CollectionView.ItemsLayout>
    
          <CollectionView.ItemTemplate>
            <DataTemplate x:DataType="fullPageSnapCollection:MyModel">
              <Grid
                HeightRequest="500"
                BackgroundColor="{Binding BackgroundColor}"
                HorizontalOptions="Fill">
                <Label
                  Text="{Binding Name}"
                  VerticalOptions="Center"
                  HorizontalOptions="Center"
                  FontSize="Title" />
              </Grid>
            </DataTemplate>
          </CollectionView.ItemTemplate>
    
        </CollectionView>
    
      </Grid>
    
    </ContentPage>
    

    You'll need to provide a HeightRequest for the CollectionView as well as for the layout in the DataTemplate, so that the items have the same height as the CollectionView. Together with the snap points, this should result in what you are looking for. I've also whipped up a small sample repo for this.

    If you need to detect which item is currently visible in the center (of the view), you can subscribe to the Scrolled event and get the CenterItemIndex from the event args as described here.

    Automatic play/stop

    In order to 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 as follows using 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();
            }
        }
    }
    

    This now allows us to bind to any property of the model class to control the play/stop functionality by updating the property.

    Next, we'll need to add an observable property to our model that is used to update the bindable property, e.g.:

    public partial class MyModel(string videoUri) : ObservableObject
    {
        public string VideoUri { get; } = videoUri;
    
        [ObservableProperty]
        private bool _isPlaying;
    }
    

    Instead of using the MediaElement, we'll use our ExtendedMediaElement and in the DataTemplate add a binding to the IsPlaying bindable property that we've added:

    <DataTemplate x:DataType="fullPageSnapCollection:MyModel">
      <Grid
        HeightRequest="500"
        BackgroundColor="{Binding BackgroundColor}"
        HorizontalOptions="Fill">
        <fullPageSnapCollection:ExtendedMediaElement
          Aspect="Fill"
          Source="{Binding VideoUri}"
          ShouldAutoPlay="False"
          ShouldLoopPlayback="True"
          ShouldShowPlaybackControls="False"
          IsPlaying="{Binding IsPlaying}" />
      </Grid>
    </DataTemplate>
    

    Last, but not least, we can now use an event handler for the Scrolled event of the CollectionView in order to update the IsPlaying property of each of our model instances in the code-behind of the page, which will subsequently trigger the play/stop functionality:

    public partial class MainPage : ContentPage
    {
        private readonly MainViewModel _vm;
    
        public MainPage()
        {
            InitializeComponent();
            BindingContext = _vm = new MainViewModel();
        }
    
        private void ItemsView_OnScrolled(object? sender, ItemsViewScrolledEventArgs e)
        {
            var itemIndex = e.CenterItemIndex;
    
            _vm.Items[itemIndex].IsPlaying = true;
    
            foreach (var myModel in _vm.Items)
            {
                if (myModel != _vm.Items[itemIndex])
                {
                    myModel.IsPlaying = false;
                }
            }
        }
    }