Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## [55.3.0]
- [PictureInPicture] Added `PipService` with `Enter()`, `Enter(ratioWidth, ratioHeight)`, `IsSupported`, and `PipModeChanged` event for Android and iOS. On iOS 15+, uses `AVPictureInPictureController` with `AVSampleBufferDisplayLayer` to display a snapshot of the current window. On Android, uses `EnterPictureInPictureMode`. Includes a sample in the Components app demonstrating usage.

## [55.2.2]
- [iOS26][Tip] Added more padding.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>

<dui:ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:dui="http://dips.com/mobile.ui"
xmlns:pip="clr-namespace:Components.ComponentsSamples.PictureInPicture"
x:Class="Components.ComponentsSamples.PictureInPicture.PictureInPictureSamples"
x:DataType="pip:PictureInPictureSamplesViewModel"
Title="Picture in Picture">
<dui:ContentPage.BindingContext>
<pip:PictureInPictureSamplesViewModel />
</dui:ContentPage.BindingContext>

<dui:ScrollView Padding="{dui:Sizes size_4}">
<dui:VerticalStackLayout Spacing="{dui:Sizes size_3}">

<dui:Label Style="{dui:Styles Label=SectionHeader}"
Text="Picture in Picture (PiP)" />

<dui:Label Style="{dui:Styles Label=UI200}"
Text="Enter PiP mode to keep this content visible in a floating window while you browse other parts of the app." />

<dui:MultiLineInputField HeaderText="Notes"
Text="{Binding Notes}"
MinimumHeightRequest="120" />

<dui:Button Text="Enter PiP mode"
Command="{Binding EnterPipCommand}"
IsEnabled="{Binding IsPipSupported}" />

<dui:Label Style="{dui:Styles Label=UI200}"
Text="{Binding StatusText}"
IsVisible="{Binding HasStatus}" />

<dui:Label Style="{dui:Styles Label=UI200}"
IsVisible="{Binding IsPipSupported, Converter={dui:InvertedBoolConverter}}"
Text="Picture in Picture is not supported on this device or platform." />

</dui:VerticalStackLayout>
</dui:ScrollView>

</dui:ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Components.ComponentsSamples.PictureInPicture;

public partial class PictureInPictureSamples
{
public PictureInPictureSamples()
{
InitializeComponent();
}

protected override void OnDisappearing()
{
base.OnDisappearing();
if (BindingContext is PictureInPictureSamplesViewModel vm)
{
vm.Unsubscribe();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Windows.Input;
using DIPS.Mobile.UI.API.PictureInPicture;
using DIPS.Mobile.UI.MVVM;

namespace Components.ComponentsSamples.PictureInPicture;

public class PictureInPictureSamplesViewModel : ViewModel
{
private string m_notes = string.Empty;
private string m_statusText = string.Empty;

public PictureInPictureSamplesViewModel()
{
EnterPipCommand = new Command(EnterPip);
PipService.PipModeChanged += OnPipModeChanged;
}

private void EnterPip()
{
PipService.Enter();
}

private void OnPipModeChanged(object? sender, bool isInPipMode)
{
StatusText = isInPipMode
? "App is in Picture in Picture mode."
: "App returned from Picture in Picture mode.";
}

public void Unsubscribe()
{
PipService.PipModeChanged -= OnPipModeChanged;
}

public bool IsPipSupported => PipService.IsSupported;

public string Notes
{
get => m_notes;
set => RaiseWhenSet(ref m_notes, value);
}

public string StatusText
{
get => m_statusText;
set
{
RaiseWhenSet(ref m_statusText, value);
RaisePropertyChanged(nameof(HasStatus));
}
}

public bool HasStatus => !string.IsNullOrEmpty(StatusText);

public ICommand EnterPipCommand { get; }
}
12 changes: 11 additions & 1 deletion src/app/Components/Platforms/Android/MainActivity.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using AndroidX.Activity;
using AndroidX.Core.View;
using DIPS.Mobile.UI.API.PictureInPicture;

namespace Components;

[Activity(Theme = "@style/DIPS.Mobile.UI.Style", MainLauncher = true,
ScreenOrientation = ScreenOrientation.Portrait,
SupportsPictureInPicture = true,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode |
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity;
public class MainActivity : MauiAppCompatActivity
{
public override void OnPictureInPictureModeChanged(bool isInPictureInPictureMode, Configuration newConfig)
{
base.OnPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
PipService.NotifyPipModeChanged(isInPictureInPictureMode);
}
}
2 changes: 2 additions & 0 deletions src/app/Components/REGISTER_YOUR_SAMPLES_HERE.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Components.AccessibilitySamples;
using Components.AccessibilitySamples.VoiceOverSamples;
using Components.ComponentsSamples.Alerting;
using Components.ComponentsSamples.PictureInPicture;
using Components.ComponentsSamples.AmplitudeView;
using Components.ComponentsSamples.BarcodeScanning;
using Components.ComponentsSamples.BottomSheets;
Expand Down Expand Up @@ -73,6 +74,7 @@ public static List<Sample> RegisterSamples()
new(SampleType.Components, "Zoom Container", () => new PanZoomContainerSample()),
new(SampleType.Components, "Gallery", () => new GallerySample()),
new(SampleType.Components, "TIFF Viewer", () => new TiffViewerSample()),
new(SampleType.Components, "Picture in Picture", () => new PictureInPictureSamples()),
new(SampleType.Accessibility, "VoiceOver/TalkBack", () => new VoiceOverSamples()),


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Android.Content.PM;
using Android.OS;
using Android.Util;

namespace DIPS.Mobile.UI.API.PictureInPicture;

public static partial class PipService
{
public static partial bool IsSupported
{
get
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
return false;

return Platform.AppContext.PackageManager
?.HasSystemFeature(PackageManager.FeaturePictureInPicture) == true;
}
}

public static partial void Enter() => Enter(9, 16);

public static partial void Enter(int ratioWidth, int ratioHeight)
{
if (!IsSupported)
return;

var activity = Platform.CurrentActivity;
if (activity is null)
return;

var paramsBuilder = new Android.App.PictureInPictureParams.Builder();

if (Build.VERSION.SdkInt >= BuildVersionCodes.S)
{
paramsBuilder.SetSeamlessResizeEnabled(true);
}

paramsBuilder.SetAspectRatio(new Rational(ratioWidth, ratioHeight));

activity.EnterPictureInPictureMode(paramsBuilder.Build());
}
}
57 changes: 57 additions & 0 deletions src/library/DIPS.Mobile.UI/API/PictureInPicture/PipService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace DIPS.Mobile.UI.API.PictureInPicture;

/// <summary>
/// Service for entering Picture in Picture (PiP) mode, allowing the app to be displayed
/// in a small floating window while the user interacts with other apps or navigates away.
/// </summary>
public static partial class PipService
{
/// <summary>
/// Gets a value indicating whether Picture in Picture mode is supported on this device.
/// </summary>
public static partial bool IsSupported { get; }

/// <summary>
/// Enters Picture in Picture mode. The app will be displayed in a small floating window
/// with a default 9:16 (portrait) aspect ratio.
/// </summary>
/// <remarks>
/// On Android, the activity must have <c>android:supportsPictureInPicture="true"</c> in the manifest,
/// or <c>SupportsPictureInPicture = true</c> in the <c>[Activity]</c> attribute.
/// On iOS 15+, this captures a snapshot of the current window and displays it in a PiP window using
/// <c>AVPictureInPictureController</c> with <c>AVSampleBufferDisplayLayer</c>.
/// </remarks>
public static partial void Enter();

/// <summary>
/// Enters Picture in Picture mode with a specific aspect ratio for the PiP window.
/// </summary>
/// <param name="ratioWidth">The width component of the desired aspect ratio.</param>
/// <param name="ratioHeight">The height component of the desired aspect ratio.</param>
/// <remarks>
/// On Android, the activity must have <c>android:supportsPictureInPicture="true"</c> in the manifest,
/// or <c>SupportsPictureInPicture = true</c> in the <c>[Activity]</c> attribute.
/// On iOS 15+, this captures a snapshot of the current window and displays it in a PiP window using
/// <c>AVPictureInPictureController</c> with <c>AVSampleBufferDisplayLayer</c>.
/// </remarks>
public static partial void Enter(int ratioWidth, int ratioHeight);

/// <summary>
/// Raised when the app enters or exits Picture in Picture mode.
/// </summary>
/// <remarks>
/// On Android, override <c>OnPictureInPictureModeChanged</c> in your <c>MainActivity</c> and call
/// <see cref="NotifyPipModeChanged"/> for this event to fire.
/// </remarks>
public static event EventHandler<bool>? PipModeChanged;

/// <summary>
/// Notifies the service that the PiP mode has changed.
/// Call this from your <c>MainActivity.OnPictureInPictureModeChanged</c> override on Android.
/// </summary>
/// <param name="isInPipMode"><c>true</c> if the app entered PiP mode; <c>false</c> if it exited.</param>
public static void NotifyPipModeChanged(bool isInPipMode)
{
PipModeChanged?.Invoke(null, isInPipMode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using DIPS.Mobile.UI.Exceptions;

namespace DIPS.Mobile.UI.API.PictureInPicture;

public static partial class PipService
{
public static partial bool IsSupported => throw new Only_Here_For_UnitTests();

public static partial void Enter() => throw new Only_Here_For_UnitTests();

public static partial void Enter(int ratioWidth, int ratioHeight) => throw new Only_Here_For_UnitTests();
}
Loading
Loading