Skip to content

Commit

Permalink
Add embedding sample and code (#23642)
Browse files Browse the repository at this point in the history
* Add support for MAUI embedding

* Move things around a bit

* Make these APIs public to CT.Maui.Embedding

* Update EmbeddedWindowHandler.iOS.cs

* Update EmbeddedWindowHandler.iOS.cs

* Embedding things are platform only

* tests use it
  • Loading branch information
mattleibow authored Jul 26, 2024
1 parent db7dd11 commit a2fdea0
Show file tree
Hide file tree
Showing 25 changed files with 1,271 additions and 255 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ namespace Maui.Controls.Sample.Droid;
[Register("com.microsoft.maui.sample.emdedding." + nameof(FirstFragment))]
public class FirstFragment : Fragment
{
EmbeddingScenarios.IScenario? _scenario;
MyMauiContent? _mauiView;
Android.Views.View? _nativeView;

public override View? OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState) =>
inflater.Inflate(Resource.Layout.fragment_first, container, false);

Expand All @@ -26,14 +30,43 @@ public override void OnViewCreated(View view, Bundle? savedInstanceState)

var button3 = view.FindViewById<Button>(Resource.Id.button3)!;
button3.Click += OnMagicClicked;

// Uncomment the scenario to test:
//_scenario = new EmbeddingScenarios.Scenario1_Basic();
//_scenario = new EmbeddingScenarios.Scenario2_Scoped();
_scenario = new EmbeddingScenarios.Scenario3_Correct();

// Create the view and (maybe) the window
(_mauiView, _nativeView) = _scenario!.Embed(Activity!);

// Add the new view to the UI
var rootLayout = view.FindViewById<LinearLayout>(Resource.Id.layout_first)!;
rootLayout.AddView(_nativeView, 1, new LinearLayout.LayoutParams(MatchParent, WrapContent));
}

public override void OnDestroyView()
{
base.OnDestroyView();

// Remove the view from the UI
var rootLayout = View!.FindViewById<LinearLayout>(Resource.Id.layout_first)!;
rootLayout.RemoveView(_nativeView);

// If we used a window, then clean that up
if (_mauiView?.Window is IWindow window)
window.Destroying();

base.OnStop();
}

private void OnMagicClicked(object? sender, EventArgs e)
private async void OnMagicClicked(object? sender, EventArgs e)
{
if (_mauiView?.DotNetBot is not Image bot)
return;

await bot.RotateTo(360, 1000);
bot.Rotation = 0;

bot.HeightRequest = 90;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ namespace Maui.Controls.Sample.Droid;
[Register("com.microsoft.maui.sample.emdedding." + nameof(SettingsFragment))]
public class SettingsFragment : Fragment
{
EmbeddingScenarios.IScenario? _scenario;
MyMauiContent? _mauiView;
Android.Views.View? _nativeView;

public override View? OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState) =>
inflater.Inflate(Resource.Layout.fragment_settings, container, false);

Expand All @@ -26,14 +30,43 @@ public override void OnViewCreated(View view, Bundle? savedInstanceState)

var button3 = view.FindViewById<Button>(Resource.Id.button3)!;
button3.Click += OnMagicClicked;

// Uncomment the scenario to test:
//_scenario = new EmbeddingScenarios.Scenario1_Basic();
//_scenario = new EmbeddingScenarios.Scenario2_Scoped();
_scenario = new EmbeddingScenarios.Scenario3_Correct();

// Create the view and (maybe) the window
(_mauiView, _nativeView) = _scenario!.Embed(Activity!);

// Add the new view to the UI
var rootLayout = view.FindViewById<LinearLayout>(Resource.Id.layout_settings)!;
rootLayout.AddView(_nativeView, 1, new LinearLayout.LayoutParams(MatchParent, WrapContent));
}

public override void OnDestroyView()
{
base.OnDestroyView();

// Remove the view from the UI
var rootLayout = View!.FindViewById<LinearLayout>(Resource.Id.layout_settings)!;
rootLayout.RemoveView(_nativeView);

// If we used a window, then clean that up
if (_mauiView?.Window is IWindow window)
window.Destroying();

base.OnStop();
}

private void OnMagicClicked(object? sender, EventArgs e)
private async void OnMagicClicked(object? sender, EventArgs e)
{
if (_mauiView?.DotNetBot is not Image bot)
return;

await bot.RotateTo(360, 1000);
bot.Rotation = 0;

bot.HeightRequest = 90;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

public class MainViewController : UIViewController
{
EmbeddingScenarios.IScenario? _scenario;
MyMauiContent? _mauiView;
UIView? _nativeView;

public override void ViewDidLoad()
{
base.ViewDidLoad();
Expand Down Expand Up @@ -34,7 +38,16 @@ public override void ViewDidLoad()
firstButton.SetTitle("UIKit Button Above MAUI", UIControlState.Normal);
stackView.AddArrangedSubview(firstButton);

// TODO: The MAUI content will go here.
// Uncomment the scenario to test:
//_scenario = new EmbeddingScenarios.Scenario1_Basic();
//_scenario = new EmbeddingScenarios.Scenario2_Scoped();
_scenario = new EmbeddingScenarios.Scenario3_Correct();

// create the view and (maybe) the window
(_mauiView, _nativeView) = _scenario.Embed(ParentViewController!.View!.Window);

// add the new view to the UI
stackView.AddArrangedSubview(new ContainerView(_nativeView));

// Create UIKit button
var secondButton = new UIButton(UIButtonType.System);
Expand All @@ -44,11 +57,23 @@ public override void ViewDidLoad()
// Create UIKit button
var thirdButton = new UIButton(UIButtonType.System);
thirdButton.SetTitle("UIKit Button Magic", UIControlState.Normal);
thirdButton.TouchUpInside += OnMagicClicked;
stackView.AddArrangedSubview(thirdButton);

AddNavBarButtons();
}

private async void OnMagicClicked(object? sender, EventArgs e)
{
if (_mauiView?.DotNetBot is not Image bot)
return;

await bot.RotateTo(360, 1000);
bot.Rotation = 0;

bot.HeightRequest = 90;
}

private void AddNavBarButtons()
{
var addNewWindowButton = new UIBarButtonItem(
Expand Down Expand Up @@ -87,4 +112,33 @@ private void RequestSession(string? activityType = null)
});
}
}

// UIStackView uses IntrinsicContentSize instead of SizeThatFits
// so we need to create a container view to wrap the Maui view and
// redirect the IntrinsicContentSize to the Maui view's SizeThatFits.
class ContainerView : UIView
{
public ContainerView(UIView view)
{
AddSubview(view);
}

public override CGSize IntrinsicContentSize =>
SizeThatFits(new CGSize(nfloat.MaxValue, nfloat.MaxValue));

public override void LayoutSubviews()
{
if (Subviews?.FirstOrDefault() is {} view)
view.Frame = Bounds;
}

public override void SetNeedsLayout()
{
base.SetNeedsLayout();
InvalidateIntrinsicContentSize();
}

public override CGSize SizeThatFits(CGSize size) =>
Subviews?.FirstOrDefault()?.SizeThatFits(size) ?? CGSize.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,54 @@ namespace Maui.Controls.Sample.WinUI;

public sealed partial class MainWindow : Microsoft.UI.Xaml.Window
{
EmbeddingScenarios.IScenario? _scenario;
MyMauiContent? _mauiView;
FrameworkElement? _nativeView;

public MainWindow()
{
InitializeComponent();
}

private void OnRootLayoutLoaded(object? sender, RoutedEventArgs e)
private async void OnRootLayoutLoaded(object? sender, RoutedEventArgs e)
{
// Sometimes Loaded fires twice...
if (_nativeView is not null)
return;

await Task.Yield();

// Uncomment the scenario to test:
//_scenario = new EmbeddingScenarios.Scenario1_Basic();
//_scenario = new EmbeddingScenarios.Scenario2_Scoped();
_scenario = new EmbeddingScenarios.Scenario3_Correct();

// create the view and (maybe) the window
(_mauiView, _nativeView) = _scenario.Embed(this);

// add the new view to the UI
RootLayout.Children.Insert(1, _nativeView);
}

private void OnWindowClosed(object? sender, WindowEventArgs args)
{
// Remove the view from the UI
RootLayout.Children.Remove(_nativeView);

// If we used a window, then clean that up
if (_mauiView?.Window is IWindow window)
window.Destroying();
}

private void OnMagicClicked(object? sender, RoutedEventArgs e)
private async void OnMagicClicked(object? sender, RoutedEventArgs e)
{
if (_mauiView?.DotNetBot is not Image bot)
return;

await bot.RotateTo(360, 1000);
bot.Rotation = 0;

bot.HeightRequest = 90;
}

private void OnNewWindowClicked(object? sender, RoutedEventArgs e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

public class MainViewController : UIViewController
{
EmbeddingScenarios.IScenario? _scenario;
MyMauiContent? _mauiView;
UIView? _nativeView;

public override void ViewDidLoad()
{
base.ViewDidLoad();
Expand Down Expand Up @@ -34,7 +38,16 @@ public override void ViewDidLoad()
firstButton.SetTitle("UIKit Button Above MAUI", UIControlState.Normal);
stackView.AddArrangedSubview(firstButton);

// TODO: The MAUI content will go here.
// Uncomment the scenario to test:
// _scenario = new EmbeddingScenarios.Scenario1_Basic();
//_scenario = new EmbeddingScenarios.Scenario2_Scoped();
_scenario = new EmbeddingScenarios.Scenario3_Correct();

// create the view and (maybe) the window
(_mauiView, _nativeView) = _scenario.Embed(ParentViewController!.View!.Window);

// add the new view to the UI
stackView.AddArrangedSubview(new ContainerView(_nativeView));

// Create UIKit button
var secondButton = new UIButton(UIButtonType.System);
Expand All @@ -44,12 +57,24 @@ public override void ViewDidLoad()
// Create UIKit button
var thirdButton = new UIButton(UIButtonType.System);
thirdButton.SetTitle("UIKit Button Magic", UIControlState.Normal);
thirdButton.TouchUpInside += OnMagicClicked;
stackView.AddArrangedSubview(thirdButton);

if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad)
AddNavBarButtons();
}

private async void OnMagicClicked(object? sender, EventArgs e)
{
if (_mauiView?.DotNetBot is not Image bot)
return;

await bot.RotateTo(360, 1000);
bot.Rotation = 0;

bot.HeightRequest = 90;
}

private void AddNavBarButtons()
{
var addNewWindowButton = new UIBarButtonItem(
Expand Down Expand Up @@ -88,4 +113,33 @@ private void RequestSession(string? activityType = null)
});
}
}

// UIStackView uses IntrinsicContentSize instead of SizeThatFits
// so we need to create a container view to wrap the Maui view and
// redirect the IntrinsicContentSize to the Maui view's SizeThatFits.
class ContainerView : UIView
{
public ContainerView(UIView view)
{
AddSubview(view);
}

public override CGSize IntrinsicContentSize =>
SizeThatFits(new CGSize(nfloat.MaxValue, nfloat.MaxValue));

public override void LayoutSubviews()
{
if (Subviews?.FirstOrDefault() is {} view)
view.Frame = Bounds;
}

public override void SetNeedsLayout()
{
base.SetNeedsLayout();
InvalidateIntrinsicContentSize();
}

public override CGSize SizeThatFits(CGSize size) =>
Subviews?.FirstOrDefault()?.SizeThatFits(size) ?? CGSize.Empty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#if ANDROID
using PlatformView = Android.Views.View;
using PlatformWindow = Android.App.Activity;
#elif IOS || MACCATALYST
using PlatformView = UIKit.UIView;
using PlatformWindow = UIKit.UIWindow;
#elif WINDOWS
using PlatformView = Microsoft.UI.Xaml.FrameworkElement;
using PlatformWindow = Microsoft.UI.Xaml.Window;
#endif

namespace Microsoft.Maui.Controls.Embedding;

/// <summary>
/// This class is just here to serve as a reflection-based bridge to the EmbeddingExtensions class.
/// </summary>
static class EmbeddingReflection
{
public static MauiAppBuilder UseMauiEmbeddedApp<TApp>(this MauiAppBuilder builder)
where TApp : class, IApplication
{
typeof(DynamicResourceExtension).Assembly // Controls.Xaml
.GetType("Microsoft.Maui.Controls.Embedding.EmbeddingExtensions")!
.GetMethod("UseMauiEmbeddedApp", 1, [ typeof(MauiAppBuilder) ])!
.MakeGenericMethod(typeof(TApp))
.Invoke(null, [ builder ]);
return builder;
}

public static IMauiContext CreateEmbeddedWindowContext(this MauiApp mauiApp, PlatformWindow platformWindow)
{
return (IMauiContext)typeof(Application).Assembly // Controls.Core
.GetType("Microsoft.Maui.Controls.Embedding.EmbeddingExtensions")!
.GetMethod("CreateEmbeddedWindowContext", [ typeof(MauiApp), typeof(PlatformWindow) ])!
.Invoke(null, [ mauiApp, platformWindow ])!;
}

public static PlatformView ToPlatformEmbedded(this IElement element, IMauiContext context)
{
return (PlatformView)typeof(Application).Assembly // Controls.Core
.GetType("Microsoft.Maui.Controls.Embedding.EmbeddingExtensions")!
.GetMethod("ToPlatformEmbedded", [ typeof(IElement), typeof(IMauiContext) ])!
.Invoke(null, [ element, context ])!;
}

public static PlatformView ToPlatformEmbedded(this IElement element, MauiApp mauiApp, PlatformWindow platformWindow)
{
return (PlatformView)typeof(Application).Assembly // Controls.Core
.GetType("Microsoft.Maui.Controls.Embedding.EmbeddingExtensions")!
.GetMethod("ToPlatformEmbedded", [ typeof(IElement), typeof(MauiApp), typeof(PlatformWindow) ])!
.Invoke(null, [ element, mauiApp, platformWindow ])!;
}
}
Loading

0 comments on commit a2fdea0

Please sign in to comment.