-
Notifications
You must be signed in to change notification settings - Fork 460
Add Support for Multiple ItemViews in MediaElement #2807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4d9cd35
61ea60e
e3293f4
9dead1c
f67c3e6
9c47153
e76acda
0313e75
500c53b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using System; | ||
using Foundation; | ||
using Microsoft.Maui; | ||
using ObjCRuntime; | ||
using UIKit; | ||
|
||
namespace CommunityToolkit.Maui.Sample.Platforms.MacCatalyst; | ||
|
||
[Register("SceneDelegate")] | ||
public class SceneDelegate : MauiUISceneDelegate | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using System; | ||
using Foundation; | ||
using Microsoft.Maui; | ||
using ObjCRuntime; | ||
using UIKit; | ||
|
||
namespace CommunityToolkit.Maui.Sample.Platforms.iOS; | ||
|
||
[Register("SceneDelegate")] | ||
public class SceneDelegate : MauiUISceneDelegate | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,5 +1,4 @@ | ||||||||||||||||||||||
using System.Diagnostics.CodeAnalysis; | ||||||||||||||||||||||
using System.Reflection; | ||||||||||||||||||||||
using AVKit; | ||||||||||||||||||||||
using CommunityToolkit.Maui.Extensions; | ||||||||||||||||||||||
using CommunityToolkit.Maui.Views; | ||||||||||||||||||||||
|
@@ -28,7 +27,7 @@ public MauiMediaElement(AVPlayerViewController playerViewController, MediaElemen | |||||||||||||||||||||
#if IOS16_0_OR_GREATER || MACCATALYST16_1_OR_GREATER | ||||||||||||||||||||||
// On iOS 16+ and macOS 13+ the AVPlayerViewController has to be added to a parent ViewController, otherwise the transport controls won't be displayed. | ||||||||||||||||||||||
|
||||||||||||||||||||||
UIViewController? viewController; | ||||||||||||||||||||||
UIViewController? viewController = null; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// If any of the Parents in the VisualTree of MediaElement uses a UIViewController for their PlatformView, use it as the child ViewController | ||||||||||||||||||||||
// This enables support for UI controls like CommunityToolkit.Maui.Popup whose PlatformView is a UIViewController (e.g. `public class MauiPopup : UIViewController`) | ||||||||||||||||||||||
|
@@ -58,48 +57,17 @@ public MauiMediaElement(AVPlayerViewController playerViewController, MediaElemen | |||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// look for an ItemsView (e.g. CarouselView or CollectionView) on page | ||||||||||||||||||||||
if (TryGetItemsViewOnPage(currentPage, out var itemsView)) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
var parentViewController = itemsView.Handler switch | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
CarouselViewHandler carouselViewHandler => carouselViewHandler.ViewController ?? GetInternalControllerForItemsView(carouselViewHandler), | ||||||||||||||||||||||
CarouselViewHandler2 carouselViewHandler2 => carouselViewHandler2.ViewController ?? GetInternalControllerForItemsView2(carouselViewHandler2), | ||||||||||||||||||||||
CollectionViewHandler collectionViewHandler => collectionViewHandler.ViewController ?? GetInternalControllerForItemsView(collectionViewHandler), | ||||||||||||||||||||||
CollectionViewHandler2 collectionViewHandler2 => collectionViewHandler2.ViewController ?? GetInternalControllerForItemsView2(collectionViewHandler2), | ||||||||||||||||||||||
null => throw new InvalidOperationException("Handler cannot be null"), | ||||||||||||||||||||||
_ => throw new NotSupportedException($"{itemsView.Handler.GetType()} not yet supported") | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
||||||||||||||||||||||
viewController = parentViewController; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// The Controller we need is a `protected internal` property called ItemsViewController in the ItemsViewHandler class: https://github.com/dotnet/maui/blob/cf002538cb73db4bf187a51e4786d7478a7025ee/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.iOS.cs#L39 | ||||||||||||||||||||||
// In this method, we must use reflection to get the value of its backing field | ||||||||||||||||||||||
static ItemsViewController<TItemsView> GetInternalControllerForItemsView<TItemsView>(ItemsViewHandler<TItemsView> handler) where TItemsView : ItemsView | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
var nonPublicInstanceFields = typeof(ItemsViewHandler<TItemsView>).GetFields(BindingFlags.NonPublic | BindingFlags.Instance); | ||||||||||||||||||||||
|
||||||||||||||||||||||
var controllerProperty = nonPublicInstanceFields.Single(x => x.FieldType == typeof(ItemsViewController<TItemsView>)); | ||||||||||||||||||||||
return (ItemsViewController<TItemsView>)(controllerProperty.GetValue(handler) ?? throw new InvalidOperationException($"Unable to get the value for the Controller property on {handler.GetType()}")); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// The Controller we need is a `protected internal` property called ItemsViewController in the ItemsViewHandler2 class: https://github.com/dotnet/maui/blob/70e8ddfd4bd494bc71aa7afb812cc09161cf0c72/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs#L64 | ||||||||||||||||||||||
// In this method, we must use reflection to get the value of its backing field | ||||||||||||||||||||||
static ItemsViewController<TItemsView> GetInternalControllerForItemsView2<TItemsView>(ItemsViewHandler2<TItemsView> handler) where TItemsView : ItemsView | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
var nonPublicInstanceFields = typeof(ItemsViewHandler2<TItemsView>).GetFields(BindingFlags.NonPublic | BindingFlags.Instance); | ||||||||||||||||||||||
|
||||||||||||||||||||||
var controllerProperty = nonPublicInstanceFields.Single(x => x.FieldType == typeof(ItemsViewController2<TItemsView>)); | ||||||||||||||||||||||
return (ItemsViewController<TItemsView>)(controllerProperty.GetValue(handler) ?? throw new InvalidOperationException($"Unable to get the value for the Controller property on {handler.GetType()}")); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
// If we don't find an ItemsView, default to the current UIViewController | ||||||||||||||||||||||
else | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
viewController = Platform.GetCurrentUIViewController(); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
TryGetItemsViewOnPage(currentPage, out var itemsView); | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Set the viewController to the first root view controller. | ||||||||||||||||||||||
viewController = Platform.GetCurrentUIViewController(); | ||||||||||||||||||||||
|
||||||||||||||||||||||
// Check to see if there is a ItemsView in a collection view or CarouselView and replace Shell Renderer with the correct handler | ||||||||||||||||||||||
viewController = itemsView?.Where(item => item.Handler is not null) | ||||||||||||||||||||||
.Select(item => GetUIViewController(item.Handler)) | ||||||||||||||||||||||
.FirstOrDefault(viewController => viewController is not null); | ||||||||||||||||||||||
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unconditionally overriding
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
||||||||||||||||||||||
if (viewController?.View is not null) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
// Zero out the safe area insets of the AVPlayerViewController | ||||||||||||||||||||||
|
@@ -114,27 +82,40 @@ static ItemsViewController<TItemsView> GetInternalControllerForItemsView2<TItems | |||||||||||||||||||||
AddSubview(playerViewController.View); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
static bool TryGetItemsViewOnPage(Page currentPage, [NotNullWhen(true)] out ItemsView? itemsView) | ||||||||||||||||||||||
static UIViewController? GetUIViewController(IViewHandler? handler) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
var itemsViewsOnPage = ((IElementController)currentPage).Descendants().OfType<ItemsView>().ToList(); | ||||||||||||||||||||||
switch (itemsViewsOnPage.Count) | ||||||||||||||||||||||
return handler switch | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
case > 1: | ||||||||||||||||||||||
// We are unable to determine which ItemsView contains the MediaElement when multiple ItemsView are being used in the same page | ||||||||||||||||||||||
// TODO: Add support for MediaElement in an ItemsView on a Page containing multiple ItemsViews | ||||||||||||||||||||||
throw new NotSupportedException("MediaElement does not currently support pages containing multiple ItemsViews (eg multiple CarouselViews + CollectionViews)"); | ||||||||||||||||||||||
case 1: | ||||||||||||||||||||||
itemsView = itemsViewsOnPage[0]; | ||||||||||||||||||||||
return true; | ||||||||||||||||||||||
case <= 0: | ||||||||||||||||||||||
itemsView = null; | ||||||||||||||||||||||
return false; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
CarouselViewHandler carouselViewHandler => carouselViewHandler.ViewController, | ||||||||||||||||||||||
CarouselViewHandler2 carouselViewHandler2 => carouselViewHandler2.ViewController, | ||||||||||||||||||||||
CollectionViewHandler collectionViewHandler => collectionViewHandler.ViewController, | ||||||||||||||||||||||
CollectionViewHandler2 collectionViewHandler2 => collectionViewHandler2.ViewController, | ||||||||||||||||||||||
null => throw new InvalidOperationException("Handler cannot be null"), | ||||||||||||||||||||||
_ => throw new NotSupportedException($"{handler.GetType()} not yet supported") | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
static void TryGetItemsViewOnPage(List<Page> currentPage, out List<ItemsView> itemsView) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
// We are looking for an ItemsView (e.g. CarouselView or CollectionView) on page. | ||||||||||||||||||||||
// To retrieve its CarouselViewHandler / CollectionViewHandler, we must traverse all VisualElements on the current page/ | ||||||||||||||||||||||
// We check if Handler.PlatformView is a View to ensure we are looking at a VisualElement. If not, we continue to the next VisualElement. | ||||||||||||||||||||||
// We need to check both the page and ItemsView to ensure we are looking at the correct VisualElement. | ||||||||||||||||||||||
// Checking both the page and ItemsView is necessary because the ItemsView may be nested inside another VisualElement. | ||||||||||||||||||||||
// We may be using Multi-window support, so we need to traverse all Windows to find the current page in a multi-window application. | ||||||||||||||||||||||
// We then check if the VisualElement is an ItemsView (e.g. CarouselView or CollectionView) and add it to the itemsView list/ | ||||||||||||||||||||||
|
||||||||||||||||||||||
itemsView = []; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The out parameter Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||||
List<ItemsView> itemsViewsOnPage = []; | ||||||||||||||||||||||
currentPage.Where(page => page.Handler?.PlatformView is View) | ||||||||||||||||||||||
.SelectMany(page => ((IElementController)page).Descendants().OfType<ItemsView>()) | ||||||||||||||||||||||
.Where(item => item.Handler?.PlatformView is View) | ||||||||||||||||||||||
.ToList() | ||||||||||||||||||||||
.ForEach(item => itemsViewsOnPage.Add(item)); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
static bool TryGetCurrentPage([NotNullWhen(true)] out Page? currentPage) | ||||||||||||||||||||||
static bool TryGetCurrentPage([NotNullWhen(true)] out List<Page> currentPage) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
currentPage = null; | ||||||||||||||||||||||
currentPage = []; | ||||||||||||||||||||||
|
||||||||||||||||||||||
if (Application.Current?.Windows is null) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
|
@@ -148,31 +129,39 @@ static bool TryGetCurrentPage([NotNullWhen(true)] out Page? currentPage) | |||||||||||||||||||||
|
||||||||||||||||||||||
if (Application.Current.Windows.Count > 1) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
// We are unable to determine which Window contains the ItemsView that contains the MediaElement when multiple ItemsView are being used in the same page | ||||||||||||||||||||||
// TODO: Add support for MediaElement in an ItemsView in a multi-window application | ||||||||||||||||||||||
throw new NotSupportedException("MediaElement is not currently supported in multi-window applications"); | ||||||||||||||||||||||
// We traverse all Windows to find the current page in a multi-window application | ||||||||||||||||||||||
// We check if the Window contains a Page and add it to the currentPage list | ||||||||||||||||||||||
// If the Page is null we continue to the next Window | ||||||||||||||||||||||
// We then return the currentPage list | ||||||||||||||||||||||
var pages = new List<Page>(); | ||||||||||||||||||||||
var list = Application.Current.Windows.ToList(); | ||||||||||||||||||||||
pages.AddRange(from item in list | ||||||||||||||||||||||
where item.Page is not null | ||||||||||||||||||||||
select item.Page); | ||||||||||||||||||||||
currentPage = pages; | ||||||||||||||||||||||
return true; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
var window = Application.Current.Windows[0]; | ||||||||||||||||||||||
|
||||||||||||||||||||||
// If using Shell, return the current page | ||||||||||||||||||||||
if (window.Page is Shell { CurrentPage: not null } shell) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
currentPage = shell.CurrentPage; | ||||||||||||||||||||||
currentPage.Add(shell.CurrentPage); | ||||||||||||||||||||||
return true; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// If not using Shell, use the ModelNavigationStack to check for any pages displayed modally | ||||||||||||||||||||||
if (TryGetModalPage(window, out var modalPage)) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
currentPage = modalPage; | ||||||||||||||||||||||
currentPage.Add(modalPage); | ||||||||||||||||||||||
return true; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
// If not using Shell or a Modal Page, return the visible page in the (non-modal) NavigationStack | ||||||||||||||||||||||
if (window.Navigation.NavigationStack.LastOrDefault() is Page page) | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
currentPage = page; | ||||||||||||||||||||||
currentPage.Add(page); | ||||||||||||||||||||||
return true; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
@@ -183,5 +172,5 @@ static bool TryGetModalPage(Window window, [NotNullWhen(true)] out Page? page) | |||||||||||||||||||||
page = window.Navigation.ModalStack.LastOrDefault(); | ||||||||||||||||||||||
return page is not null; | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
secondWindow
is declared but never initialized in the constructor, soOnAppearing
will always return early. Reintroduce its assignment using your new URL constants.Copilot uses AI. Check for mistakes.