diff --git a/.nuspec/Xamarin.Forms.DualScreen.nuspec b/.nuspec/Xamarin.Forms.DualScreen.nuspec
new file mode 100644
index 000000000..921584416
--- /dev/null
+++ b/.nuspec/Xamarin.Forms.DualScreen.nuspec
@@ -0,0 +1,56 @@
+
+
+
+ Xamarin.Forms.DualScreen
+ $version$
+ Microsoft
+ microsoft xamarin
+ xamarin forms twopaneview DualScreen xamarinforms xamarinformsdualscreen xamarin.forms.dualscreen
+ MIT
+ https://raw.githubusercontent.com/xamarin/Xamarin.Forms/master/Assets/xamarin_128x128.png
+ http://xamarin.com/forms
+
+ true
+ DualScreen support for Xamarin.Forms
+ © Microsoft Corporation. All rights reserved.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UWP.Build.targets b/UWP.Build.targets
index ba65bfc82..8fd6159a1 100644
--- a/UWP.Build.targets
+++ b/UWP.Build.targets
@@ -7,10 +7,6 @@
Windows Mobile Extensions for the UWP
-
-
-
-
diff --git a/Visual Studio 2019/Visualizers/attribcache140.bin b/Visual Studio 2019/Visualizers/attribcache140.bin
new file mode 100644
index 000000000..c779bdbe3
Binary files /dev/null and b/Visual Studio 2019/Visualizers/attribcache140.bin differ
diff --git a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj
index 07f066b40..e55604234 100644
--- a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj
+++ b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj
@@ -187,6 +187,10 @@
{4dcd0420-1168-4b77-86db-6196ee4bd491}
Xamarin.Forms.CustomAttributes
+
+ {fb4a866a-5721-4545-9e5d-b7f7d59875a4}
+ Xamarin.Forms.DualScreen
+
{bd50b39a-ebc5-408f-9c5e-923a8ebae473}
Xamarin.Forms.Maps.Android
diff --git a/Xamarin.Forms.ControlGallery.GTK/Xamarin.Forms.ControlGallery.GTK.csproj b/Xamarin.Forms.ControlGallery.GTK/Xamarin.Forms.ControlGallery.GTK.csproj
index 212b639a3..b1a2decb4 100644
--- a/Xamarin.Forms.ControlGallery.GTK/Xamarin.Forms.ControlGallery.GTK.csproj
+++ b/Xamarin.Forms.ControlGallery.GTK/Xamarin.Forms.ControlGallery.GTK.csproj
@@ -115,6 +115,10 @@
{57b8b73d-c3b5-4c42-869e-7b2f17d354ac}
Xamarin.Forms.Core
+
+ {fb4a866a-5721-4545-9e5d-b7f7d59875a4}
+ Xamarin.Forms.DualScreen
+
{a9772bb1-0e17-42f5-a6db-60bfccbfdb9d}
Xamarin.Forms.Maps.GTK
diff --git a/Xamarin.Forms.ControlGallery.MacOS/Xamarin.Forms.ControlGallery.MacOS.csproj b/Xamarin.Forms.ControlGallery.MacOS/Xamarin.Forms.ControlGallery.MacOS.csproj
index defb109af..c0f2af330 100644
--- a/Xamarin.Forms.ControlGallery.MacOS/Xamarin.Forms.ControlGallery.MacOS.csproj
+++ b/Xamarin.Forms.ControlGallery.MacOS/Xamarin.Forms.ControlGallery.MacOS.csproj
@@ -120,6 +120,10 @@
{57B8B73D-C3B5-4C42-869E-7B2F17D354AC}
Xamarin.Forms.Core
+
+ {fb4a866a-5721-4545-9e5d-b7f7d59875a4}
+ Xamarin.Forms.DualScreen
+
{7D13BAC2-C6A4-416A-B07E-C169B199E52B}
Xamarin.Forms.Maps
diff --git a/Xamarin.Forms.ControlGallery.WPF/Xamarin.Forms.ControlGallery.WPF.csproj b/Xamarin.Forms.ControlGallery.WPF/Xamarin.Forms.ControlGallery.WPF.csproj
index f12c3a09f..f727a0ea1 100644
--- a/Xamarin.Forms.ControlGallery.WPF/Xamarin.Forms.ControlGallery.WPF.csproj
+++ b/Xamarin.Forms.ControlGallery.WPF/Xamarin.Forms.ControlGallery.WPF.csproj
@@ -123,6 +123,10 @@
{57b8b73d-c3b5-4c42-869e-7b2f17d354ac}
Xamarin.Forms.Core
+
+ {fb4a866a-5721-4545-9e5d-b7f7d59875a4}
+ Xamarin.Forms.DualScreen
+
{89b0db73-a32e-447c-9390-a2a59d89b2e4}
Xamarin.Forms.Maps.WPF
diff --git a/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj b/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj
index 458603d43..c1714e0cc 100644
--- a/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj
+++ b/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj
@@ -105,6 +105,10 @@
{57b8b73d-c3b5-4c42-869e-7b2f17d354ac}
Xamarin.Forms.Core
+
+ {fb4a866a-5721-4545-9e5d-b7f7d59875a4}
+ Xamarin.Forms.DualScreen
+
{04d89a60-78ef-4a32-ae17-87e47e0233a5}
Xamarin.Forms.Maps.UWP
@@ -272,4 +276,4 @@
-->
-
\ No newline at end of file
+
diff --git a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj
index 05467fbb0..57a45e172 100644
--- a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj
+++ b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj
@@ -142,6 +142,10 @@
{cb9c96ce-125c-4a68-b6a1-c3ff1fbf93e1}
Xamarin.Forms.Controls
+
+ {fb4a866a-5721-4545-9e5d-b7f7d59875a4}
+ Xamarin.Forms.DualScreen
+
{aba078c4-f9bb-4924-8b2b-10fe0d2f5491}
Xamarin.Forms.Maps.iOS
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8779.xaml b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8779.xaml
new file mode 100644
index 000000000..8891067e2
--- /dev/null
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8779.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8779.xaml.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8779.xaml.cs
new file mode 100644
index 000000000..4f0272c09
--- /dev/null
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue8779.xaml.cs
@@ -0,0 +1,73 @@
+using System;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Xaml;
+using System.Windows.Input;
+
+#if UITEST
+using Xamarin.UITest;
+using Xamarin.UITest.Queries;
+using NUnit.Framework;
+using Xamarin.Forms.Core.UITests;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+ [Category(UITestCategories.SwipeView)]
+#endif
+#if APP
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+#endif
+ [Preserve(AllMembers = true)]
+ [Issue(IssueTracker.Github, 8779, "[iOS][Android] Entry in custom SwipeItemView can't gain focus", PlatformAffected.Android | PlatformAffected.iOS)]
+ public partial class Issue8779 : TestContentPage
+ {
+ public Issue8779()
+ {
+#if APP
+ Title = "Issue 8779";
+ InitializeComponent();
+
+ CheckAnswerCommand = new Command(CheckAnswer);
+ BindingContext = this;
+#endif
+ }
+
+ public ICommand CheckAnswerCommand { get; private set; }
+
+ protected override void Init()
+ {
+
+ }
+
+#if APP
+ async void OnIncorrectAnswerInvoked(object sender, EventArgs e)
+ {
+ ((SwipeView)sender).Close();
+ await DisplayAlert("Incorrect!", "Try again.", "OK");
+ }
+
+ async void OnCorrectAnswerInvoked(object sender, EventArgs e)
+ {
+ ((SwipeView)sender).Close();
+ await DisplayAlert("Correct!", "The answer is 4.", "OK");
+ }
+
+ void CheckAnswer(string result)
+ {
+ if (!string.IsNullOrWhiteSpace(result))
+ {
+ Int32.TryParse(resultEntry.Text, out int number);
+
+ if (number.Equals(4))
+ OnCorrectAnswerInvoked(swipeView, EventArgs.Empty);
+ else
+ OnIncorrectAnswerInvoked(swipeView, EventArgs.Empty);
+
+ resultEntry.Text = string.Empty;
+ }
+ }
+#endif
+ }
+}
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
index 03595f023..92203ce60 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
@@ -1257,6 +1257,7 @@
+
@@ -1413,6 +1414,7 @@
+
diff --git a/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml b/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml
index 3ff9a1e57..4ab577b54 100644
--- a/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml
+++ b/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml
@@ -26,37 +26,40 @@
Text="Test"
BackgroundColor="Red" />
-
-
+ Text="This is the RightItems Content"/>
+
+
-
-
+
+
-
+
+
+
+
diff --git a/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml.cs
index 306f10e37..bce7217af 100644
--- a/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml.cs
+++ b/Xamarin.Forms.Controls/GalleryPages/SwipeViewGalleries/CustomSizeSwipeViewGallery.xaml.cs
@@ -1,4 +1,5 @@
-using Xamarin.Forms.Internals;
+using System;
+using Xamarin.Forms.Internals;
namespace Xamarin.Forms.Controls.GalleryPages.SwipeViewGalleries
{
@@ -9,5 +10,21 @@ namespace Xamarin.Forms.Controls.GalleryPages.SwipeViewGalleries
{
InitializeComponent();
}
+
+ void OnContentClicked(object sender, EventArgs args)
+ {
+ DisplayAlert("OnClicked", "The Content Button has been clicked.", "Ok");
+ }
+
+ void OnRightItemsClicked(object sender, EventArgs args)
+ {
+ DisplayAlert("OnClicked", "The RightItems Button has been clicked.", "Ok");
+ }
+
+ void OnButtonClicked(object sender, EventArgs e)
+ {
+ DisplayAlert("Custom SwipeItem", "Button Clicked!", "Ok");
+ }
}
+
}
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj
index a8684232e..9c5d027f8 100644
--- a/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj
+++ b/Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj
@@ -31,6 +31,7 @@
+
diff --git a/Xamarin.Forms.Core.UnitTests/MotionTests.cs b/Xamarin.Forms.Core.UnitTests/MotionTests.cs
index a5aa8c775..d4db59376 100644
--- a/Xamarin.Forms.Core.UnitTests/MotionTests.cs
+++ b/Xamarin.Forms.Core.UnitTests/MotionTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Threading.Tasks;
using NUnit.Framework;
using Xamarin.Forms.Internals;
@@ -174,6 +175,12 @@ namespace Xamarin.Forms.Core.UnitTests
((AsyncTicker)Ticker.Default).SetEnabled(false);
}
+ static async Task EnableTicker()
+ {
+ await Task.Delay(32);
+ ((AsyncTicker)Ticker.Default).SetEnabled(true);
+ }
+
async Task SwapFadeViews(View view1, View view2)
{
await view1.FadeTo(0, 1000);
@@ -237,5 +244,41 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.That(view.RotationY, Is.EqualTo(200));
}
+
+ [Test]
+ public async Task AnimationExtensionsReturnTrueIfAnimationsDisabled()
+ {
+ await DisableTicker();
+
+ var label = new Label { Text = "Foo" };
+ var result = await label.ScaleTo(2, 500);
+
+ Assert.That(result, Is.True);
+ }
+
+ [Test, Timeout(2000)]
+ public async Task CanExitAnimationLoopIfAnimationsDisabled()
+ {
+ await DisableTicker();
+
+ var run = true;
+ var label = new Label { Text = "Foo" };
+
+ while (run)
+ {
+ await label.ScaleTo(2, 500);
+ run = !(await label.ScaleTo(0.5, 500));
+ }
+ }
+
+ [Test]
+ public async Task CanCheckThatAnimationsAreEnabled()
+ {
+ await EnableTicker();
+ Assert.That(Animation.IsEnabled, Is.True);
+
+ await DisableTicker();
+ Assert.That(Animation.IsEnabled, Is.False);
+ }
}
}
diff --git a/Xamarin.Forms.Core/Animation.cs b/Xamarin.Forms.Core/Animation.cs
index 05348e315..40452cac6 100644
--- a/Xamarin.Forms.Core/Animation.cs
+++ b/Xamarin.Forms.Core/Animation.cs
@@ -140,5 +140,13 @@ namespace Xamarin.Forms
_children.Add(child);
return this;
}
+
+ public static bool IsEnabled
+ {
+ get
+ {
+ return Internals.Ticker.Default.SystemEnabled;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/AnimationExtensions.cs b/Xamarin.Forms.Core/AnimationExtensions.cs
index 84706706e..49cf090eb 100644
--- a/Xamarin.Forms.Core/AnimationExtensions.cs
+++ b/Xamarin.Forms.Core/AnimationExtensions.cs
@@ -239,7 +239,9 @@ namespace Xamarin.Forms
var repeat = false;
// If the Ticker has been disabled (e.g., by power save mode), then don't repeat the animation
- if (info.Repeat != null && Ticker.Default.SystemEnabled)
+ var animationsEnabled = Ticker.Default.SystemEnabled;
+
+ if (info.Repeat != null && animationsEnabled)
repeat = info.Repeat();
if (!repeat)
@@ -249,7 +251,7 @@ namespace Xamarin.Forms
tweener.Finished -= HandleTweenerFinished;
}
- info.Finished?.Invoke(tweener.Value, false);
+ info.Finished?.Invoke(tweener.Value, !animationsEnabled);
if (info.Owner.TryGetTarget(out owner))
owner.BatchCommit();
diff --git a/Xamarin.Forms.Core/Properties/AssemblyInfo.cs b/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
index ad0283e08..686c21b0a 100644
--- a/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
+++ b/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
@@ -32,6 +32,7 @@ using Xamarin.Forms.StyleSheets;
[assembly: InternalsVisibleTo("Xamarin.Forms.Pages")]
[assembly: InternalsVisibleTo("Xamarin.Forms.Pages.UnitTests")]
[assembly: InternalsVisibleTo("Xamarin.Forms.CarouselView")]
+[assembly: InternalsVisibleTo("Xamarin.Forms.DualScreen")]
[assembly: Preserve]
[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms", "Xamarin.Forms")]
diff --git a/Xamarin.Forms.DualScreen.UnitTests/BaseTestFixture.cs b/Xamarin.Forms.DualScreen.UnitTests/BaseTestFixture.cs
new file mode 100644
index 000000000..8119b2c4c
--- /dev/null
+++ b/Xamarin.Forms.DualScreen.UnitTests/BaseTestFixture.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Globalization;
+using System.Threading;
+
+using NUnit.Framework;
+
+namespace Xamarin.Forms.DualScreen.UnitTests
+{
+ public class BaseTestFixture
+ {
+ CultureInfo _defaultCulture;
+ CultureInfo _defaultUICulture;
+
+ [SetUp]
+ public virtual void Setup()
+ {
+ _defaultCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
+ _defaultUICulture = System.Threading.Thread.CurrentThread.CurrentUICulture;
+ Device.PlatformServices = new MockPlatformServices();
+ }
+
+ [TearDown]
+ public virtual void TearDown()
+ {
+ Device.PlatformServices = null;
+ System.Threading.Thread.CurrentThread.CurrentCulture = _defaultCulture;
+ System.Threading.Thread.CurrentThread.CurrentUICulture = _defaultUICulture;
+ }
+ }
+}
diff --git a/Xamarin.Forms.DualScreen.UnitTests/MockPlatformServices.cs b/Xamarin.Forms.DualScreen.UnitTests/MockPlatformServices.cs
new file mode 100644
index 000000000..97206e657
--- /dev/null
+++ b/Xamarin.Forms.DualScreen.UnitTests/MockPlatformServices.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Reflection;
+using System.IO.IsolatedStorage;
+using System.Collections.Generic;
+using Xamarin.Forms;
+using System.Security.Cryptography;
+using System.Text;
+using FileMode = System.IO.FileMode;
+using FileAccess = System.IO.FileAccess;
+using FileShare = System.IO.FileShare;
+using Stream = System.IO.Stream;
+using Xamarin.Forms.DualScreen.UnitTests;
+
+[assembly: Dependency(typeof(MockDeserializer))]
+[assembly: Dependency(typeof(MockResourcesProvider))]
+
+namespace Xamarin.Forms.DualScreen.UnitTests
+{
+ internal class MockPlatformServices : Internals.IPlatformServices
+ {
+ Action invokeOnMainThread;
+ Action openUriAction;
+ Func> getStreamAsync;
+ Func getNativeSizeFunc;
+ readonly bool useRealisticLabelMeasure;
+ readonly bool _isInvokeRequired;
+
+ public MockPlatformServices(Action invokeOnMainThread = null, Action openUriAction = null,
+ Func> getStreamAsync = null,
+ Func getNativeSizeFunc = null,
+ bool useRealisticLabelMeasure = false, bool isInvokeRequired = false)
+ {
+ this.invokeOnMainThread = invokeOnMainThread;
+ this.openUriAction = openUriAction;
+ this.getStreamAsync = getStreamAsync;
+ this.getNativeSizeFunc = getNativeSizeFunc;
+ this.useRealisticLabelMeasure = useRealisticLabelMeasure;
+ _isInvokeRequired = isInvokeRequired;
+ }
+
+ static MD5CryptoServiceProvider checksum = new MD5CryptoServiceProvider();
+
+ public string GetMD5Hash(string input)
+ {
+ var bytes = checksum.ComputeHash(Encoding.UTF8.GetBytes(input));
+ var ret = new char[32];
+ for (int i = 0; i < 16; i++)
+ {
+ ret[i * 2] = (char)hex(bytes[i] >> 4);
+ ret[i * 2 + 1] = (char)hex(bytes[i] & 0xf);
+ }
+ return new string(ret);
+ }
+ static int hex(int v)
+ {
+ if (v < 10)
+ return '0' + v;
+ return 'a' + v - 10;
+ }
+
+ public double GetNamedSize(NamedSize size, Type targetElement, bool useOldSizes)
+ {
+ switch (size)
+ {
+ case NamedSize.Default:
+ return 10;
+ case NamedSize.Micro:
+ return 4;
+ case NamedSize.Small:
+ return 8;
+ case NamedSize.Medium:
+ return 12;
+ case NamedSize.Large:
+ return 16;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(size));
+ }
+ }
+
+ public void OpenUriAction(Uri uri)
+ {
+ if (openUriAction != null)
+ openUriAction(uri);
+ else
+ throw new NotImplementedException();
+ }
+
+ public bool IsInvokeRequired
+ {
+ get { return _isInvokeRequired; }
+ }
+
+ public string RuntimePlatform { get; set; }
+
+ public void BeginInvokeOnMainThread(Action action)
+ {
+ if (invokeOnMainThread == null)
+ action();
+ else
+ invokeOnMainThread(action);
+ }
+
+ public Internals.Ticker CreateTicker()
+ {
+ return new MockTicker();
+ }
+
+ public void StartTimer(TimeSpan interval, Func callback)
+ {
+ Timer timer = null;
+ TimerCallback onTimeout = o => BeginInvokeOnMainThread(() =>
+ {
+ if (callback())
+ return;
+
+ timer.Dispose();
+ });
+ timer = new Timer(onTimeout, null, interval, interval);
+ }
+
+ public Task GetStreamAsync(Uri uri, CancellationToken cancellationToken)
+ {
+ if (getStreamAsync == null)
+ throw new NotImplementedException();
+ return getStreamAsync(uri, cancellationToken);
+ }
+
+ public Assembly[] GetAssemblies()
+ {
+ return AppDomain.CurrentDomain.GetAssemblies();
+ }
+
+ public Internals.IIsolatedStorageFile GetUserStoreForApplication()
+ {
+ return new MockIsolatedStorageFile(IsolatedStorageFile.GetUserStoreForAssembly());
+ }
+
+ public class MockIsolatedStorageFile : Internals.IIsolatedStorageFile
+ {
+ readonly IsolatedStorageFile isolatedStorageFile;
+ public MockIsolatedStorageFile(IsolatedStorageFile isolatedStorageFile)
+ {
+ this.isolatedStorageFile = isolatedStorageFile;
+ }
+
+ public Task GetDirectoryExistsAsync(string path)
+ {
+ return Task.FromResult(isolatedStorageFile.DirectoryExists(path));
+ }
+
+ public Task CreateDirectoryAsync(string path)
+ {
+ isolatedStorageFile.CreateDirectory(path);
+ return Task.FromResult(true);
+ }
+
+ public Task OpenFileAsync(string path, FileMode mode, FileAccess access)
+ {
+ Stream stream = isolatedStorageFile.OpenFile(path, mode, access);
+ return Task.FromResult(stream);
+ }
+
+ public Task OpenFileAsync(string path, FileMode mode, FileAccess access, FileShare share)
+ {
+ Stream stream = isolatedStorageFile.OpenFile(path, mode, access, share);
+ return Task.FromResult(stream);
+ }
+
+ public Task GetFileExistsAsync(string path)
+ {
+ return Task.FromResult(isolatedStorageFile.FileExists(path));
+ }
+
+ public Task GetLastWriteTimeAsync(string path)
+ {
+ return Task.FromResult(isolatedStorageFile.GetLastWriteTime(path));
+ }
+ }
+
+ public void QuitApplication()
+ {
+
+ }
+
+ public SizeRequest GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint)
+ {
+ if (getNativeSizeFunc != null)
+ return getNativeSizeFunc(view, widthConstraint, heightConstraint);
+ // EVERYTHING IS 100 x 20
+
+ var label = view as Label;
+ if (label != null && useRealisticLabelMeasure)
+ {
+ var letterSize = new Size(5, 10);
+ var w = label.Text.Length * letterSize.Width;
+ var h = letterSize.Height;
+ if (!double.IsPositiveInfinity(widthConstraint) && w > widthConstraint)
+ {
+ h = ((int)w / (int)widthConstraint) * letterSize.Height;
+ w = widthConstraint - (widthConstraint % letterSize.Width);
+
+ }
+ return new SizeRequest(new Size(w, h), new Size(Math.Min(10, w), h));
+ }
+
+ return new SizeRequest(new Size(100, 20));
+ }
+ }
+
+ internal class MockDeserializer : Internals.IDeserializer
+ {
+ public Task> DeserializePropertiesAsync()
+ {
+ return Task.FromResult>(new Dictionary());
+ }
+
+ public Task SerializePropertiesAsync(IDictionary properties)
+ {
+ return Task.FromResult(false);
+ }
+ }
+
+ internal class MockResourcesProvider : Internals.ISystemResourcesProvider
+ {
+ public Internals.IResourceDictionary GetSystemResources()
+ {
+ var dictionary = new ResourceDictionary();
+ Style style;
+ style = new Style(typeof(Label));
+ dictionary[Device.Styles.BodyStyleKey] = style;
+
+ style = new Style(typeof(Label));
+ style.Setters.Add(Label.FontSizeProperty, 50);
+ dictionary[Device.Styles.TitleStyleKey] = style;
+
+ style = new Style(typeof(Label));
+ style.Setters.Add(Label.FontSizeProperty, 40);
+ dictionary[Device.Styles.SubtitleStyleKey] = style;
+
+ style = new Style(typeof(Label));
+ style.Setters.Add(Label.FontSizeProperty, 30);
+ dictionary[Device.Styles.CaptionStyleKey] = style;
+
+ style = new Style(typeof(Label));
+ style.Setters.Add(Label.FontSizeProperty, 20);
+ dictionary[Device.Styles.ListItemTextStyleKey] = style;
+
+ style = new Style(typeof(Label));
+ style.Setters.Add(Label.FontSizeProperty, 10);
+ dictionary[Device.Styles.ListItemDetailTextStyleKey] = style;
+
+ return dictionary;
+ }
+ }
+
+ public class MockApplication : Application
+ {
+ public MockApplication()
+ {
+ }
+ }
+
+ internal class MockTicker : Internals.Ticker
+ {
+ bool _enabled;
+
+ protected override void EnableTimer()
+ {
+ _enabled = true;
+
+ while (_enabled)
+ {
+ SendSignals(16);
+ }
+ }
+
+ protected override void DisableTimer()
+ {
+ _enabled = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.DualScreen.UnitTests/TwoPaneViewTests.cs b/Xamarin.Forms.DualScreen.UnitTests/TwoPaneViewTests.cs
new file mode 100644
index 000000000..b0ba3315d
--- /dev/null
+++ b/Xamarin.Forms.DualScreen.UnitTests/TwoPaneViewTests.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.DualScreen.UnitTests
+{
+ [TestFixture]
+ public class TwoPaneViewTests : BaseTestFixture
+ {
+ [SetUp]
+ public override void Setup()
+ {
+ base.Setup();
+ Device.PlatformServices = new MockPlatformServices();
+ Device.info = new TestDeviceInfo();
+ }
+
+ TwoPaneView CreateTwoPaneView(View pane1 = null, View pane2 = null)
+ {
+ TwoPaneView view = new TwoPaneView()
+ {
+ IsPlatformEnabled = true,
+ Pane1 = pane1,
+ Pane2 = pane2
+ };
+
+ if (pane1 != null)
+ pane1.IsPlatformEnabled = true;
+
+ if (pane2 != null)
+ pane2.IsPlatformEnabled = true;
+
+ view.Children[0].IsPlatformEnabled = true;
+ view.Children[1].IsPlatformEnabled = true;
+
+ return view;
+ }
+
+ [Test]
+ public void GettersAndSetters()
+ {
+ var Pane1 = new StackLayout();
+ var Pane2 = new Grid();
+
+ TwoPaneView twoPaneView = new TwoPaneView()
+ {
+ TallModeConfiguration = TwoPaneViewTallModeConfiguration.SinglePane,
+ WideModeConfiguration = TwoPaneViewWideModeConfiguration.SinglePane,
+ Pane1 = Pane1,
+ Pane2 = Pane2,
+ PanePriority = TwoPaneViewPriority.Pane2,
+ MinTallModeHeight = 1000,
+ MinWideModeWidth = 2000,
+ };
+
+ Assert.AreEqual(TwoPaneViewTallModeConfiguration.SinglePane, twoPaneView.TallModeConfiguration);
+ Assert.AreEqual(TwoPaneViewWideModeConfiguration.SinglePane, twoPaneView.WideModeConfiguration);
+ Assert.AreEqual(Pane1, twoPaneView.Pane1);
+ Assert.AreEqual(Pane2, twoPaneView.Pane2);
+ Assert.AreEqual(TwoPaneViewPriority.Pane2, twoPaneView.PanePriority);
+ Assert.AreEqual(1000, twoPaneView.MinTallModeHeight);
+ Assert.AreEqual(2000, twoPaneView.MinWideModeWidth);
+ }
+
+ [Test]
+ public void BasicLayoutTest()
+ {
+ TwoPaneView twoPaneView = new TwoPaneView();
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+
+ Assert.AreEqual(300, twoPaneView.Height);
+ Assert.AreEqual(300, twoPaneView.Width);
+ }
+
+ [Test]
+ public void ModeSwitchesWithMinWideModeWidth()
+ {
+ TwoPaneView twoPaneView = new TwoPaneView();
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+
+ twoPaneView.MinWideModeWidth = 400;
+ Assert.AreEqual(TwoPaneViewMode.SinglePane, twoPaneView.Mode);
+ twoPaneView.MinWideModeWidth = 100;
+ Assert.AreEqual(TwoPaneViewMode.Wide, twoPaneView.Mode);
+ }
+
+ [Test]
+ public void ModeSwitchesWithMinTallModeHeight()
+ {
+ TwoPaneView twoPaneView = new TwoPaneView();
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+
+ twoPaneView.MinTallModeHeight = 400;
+ Assert.AreEqual(TwoPaneViewMode.SinglePane, twoPaneView.Mode);
+ twoPaneView.MinTallModeHeight = 100;
+ Assert.AreEqual(TwoPaneViewMode.Tall, twoPaneView.Mode);
+ }
+
+ [Test]
+ public void Pane1LengthTallMode()
+ {
+ var pane1 = new BoxView();
+ var pane2 = new BoxView();
+
+ TwoPaneView twoPaneView = CreateTwoPaneView(pane1, pane2);
+
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+ twoPaneView.MinTallModeHeight = 100;
+ Assert.AreNotEqual(100, twoPaneView.Pane1.Height);
+ twoPaneView.Pane1Length = 100;
+ Assert.AreEqual(100, twoPaneView.Pane1.Height);
+ }
+
+
+ [Test]
+ public void Pane1LengthWideMode()
+ {
+ var pane1 = new BoxView();
+ var pane2 = new BoxView();
+
+ TwoPaneView twoPaneView = CreateTwoPaneView(pane1, pane2);
+
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+ twoPaneView.MinWideModeWidth = 100;
+ Assert.AreNotEqual(100, twoPaneView.Pane1.Width);
+ twoPaneView.Pane1Length = 100;
+ Assert.AreEqual(100, twoPaneView.Pane1.Width);
+ }
+
+
+ [Test]
+ public void Pane2LengthTallMode()
+ {
+ var pane1 = new BoxView();
+ var pane2 = new BoxView();
+
+ TwoPaneView twoPaneView = CreateTwoPaneView(pane1, pane2);
+
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+ twoPaneView.MinTallModeHeight = 100;
+ Assert.AreNotEqual(100, twoPaneView.Pane2.Height);
+ twoPaneView.Pane2Length = 100;
+ Assert.AreEqual(100, twoPaneView.Pane2.Height);
+ }
+
+
+ [Test]
+ public void Pane2LengthWideMode()
+ {
+ var pane1 = new BoxView();
+ var pane2 = new BoxView();
+
+ TwoPaneView twoPaneView = CreateTwoPaneView(pane1, pane2);
+
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+ twoPaneView.MinWideModeWidth = 100;
+ Assert.AreNotEqual(100, twoPaneView.Pane2.Width);
+ twoPaneView.Pane2Length = 100;
+ Assert.AreEqual(100, twoPaneView.Pane2.Width);
+ }
+
+
+ [Test]
+ public void PanePriority()
+ {
+ var pane1 = new BoxView();
+ var pane2 = new BoxView();
+
+ TwoPaneView twoPaneView = CreateTwoPaneView(pane1, pane2);
+
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+ twoPaneView.MinWideModeWidth = 500;
+ twoPaneView.MinTallModeHeight = 500;
+
+ Assert.IsFalse(twoPaneView.Children[1].IsVisible);
+ Assert.IsTrue(twoPaneView.Children[0].IsVisible);
+
+ Assert.AreEqual(pane1.Height, 300);
+ Assert.AreEqual(pane1.Width, 300);
+
+ twoPaneView.PanePriority = TwoPaneViewPriority.Pane2;
+
+ Assert.AreEqual(pane2.Height, 300);
+ Assert.AreEqual(pane2.Width, 300);
+
+ Assert.IsFalse(twoPaneView.Children[0].IsVisible);
+ Assert.IsTrue(twoPaneView.Children[1].IsVisible);
+ }
+
+ [Test]
+ public void TallModeConfiguration()
+ {
+ var pane1 = new BoxView();
+ var pane2 = new BoxView();
+ TwoPaneView twoPaneView = CreateTwoPaneView(pane1, pane2);
+ twoPaneView.MinTallModeHeight = 100;
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+
+ Assert.AreEqual(0, twoPaneView.Children[0].Bounds.Y);
+ Assert.AreEqual(150, twoPaneView.Children[1].Bounds.Y);
+
+ twoPaneView.TallModeConfiguration = TwoPaneViewTallModeConfiguration.BottomTop;
+
+ Assert.AreEqual(150, twoPaneView.Children[0].Bounds.Y);
+ Assert.AreEqual(0, twoPaneView.Children[1].Bounds.Y);
+
+ twoPaneView.TallModeConfiguration = TwoPaneViewTallModeConfiguration.SinglePane;
+
+ Assert.IsTrue(twoPaneView.Children[0].IsVisible);
+ Assert.IsFalse(twoPaneView.Children[1].IsVisible);
+
+ twoPaneView.PanePriority = TwoPaneViewPriority.Pane2;
+
+ Assert.IsFalse(twoPaneView.Children[0].IsVisible);
+ Assert.IsTrue(twoPaneView.Children[1].IsVisible);
+
+ twoPaneView.PanePriority = TwoPaneViewPriority.Pane1;
+
+ Assert.IsTrue(twoPaneView.Children[0].IsVisible);
+ Assert.IsFalse(twoPaneView.Children[1].IsVisible);
+ }
+
+ [Test]
+ public void WideModeConfiguration()
+ {
+ var pane1 = new BoxView();
+ var pane2 = new BoxView();
+ TwoPaneView twoPaneView = CreateTwoPaneView(pane1, pane2);
+ twoPaneView.MinWideModeWidth = 100;
+ twoPaneView.Layout(new Rectangle(0, 0, 300, 300));
+
+ Assert.AreEqual(0, twoPaneView.Children[0].Bounds.X);
+ Assert.AreEqual(150, twoPaneView.Children[1].Bounds.X);
+
+ twoPaneView.WideModeConfiguration = TwoPaneViewWideModeConfiguration.RightLeft;
+
+ Assert.AreEqual(150, twoPaneView.Children[0].Bounds.X);
+ Assert.AreEqual(0, twoPaneView.Children[1].Bounds.X);
+
+ twoPaneView.WideModeConfiguration = TwoPaneViewWideModeConfiguration.SinglePane;
+
+ Assert.IsTrue(twoPaneView.Children[0].IsVisible);
+ Assert.IsFalse(twoPaneView.Children[1].IsVisible);
+
+ twoPaneView.PanePriority = TwoPaneViewPriority.Pane2;
+
+ Assert.IsFalse(twoPaneView.Children[0].IsVisible);
+ Assert.IsTrue(twoPaneView.Children[1].IsVisible);
+
+ twoPaneView.PanePriority = TwoPaneViewPriority.Pane1;
+
+ Assert.IsTrue(twoPaneView.Children[0].IsVisible);
+ Assert.IsFalse(twoPaneView.Children[1].IsVisible);
+ }
+
+
+ internal class TestDeviceInfo : DeviceInfo
+ {
+ public TestDeviceInfo()
+ {
+ CurrentOrientation = DeviceOrientation.Portrait;
+ }
+ public override Size PixelScreenSize
+ {
+ get { return new Size(1000, 2000); }
+ }
+
+ public override Size ScaledScreenSize
+ {
+ get { return new Size(500, 1000); }
+ }
+
+ public override double ScalingFactor
+ {
+ get { return 2; }
+ }
+ }
+
+ }
+}
diff --git a/Xamarin.Forms.DualScreen.UnitTests/Xamarin.Forms.DualScreen.UnitTests.csproj b/Xamarin.Forms.DualScreen.UnitTests/Xamarin.Forms.DualScreen.UnitTests.csproj
new file mode 100644
index 000000000..bbb155ebe
--- /dev/null
+++ b/Xamarin.Forms.DualScreen.UnitTests/Xamarin.Forms.DualScreen.UnitTests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net47;
+ netcoreapp2.1
+ false
+
+
+
+
+
+
+
+ 3.16.1
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.DualScreen/Directory.Build.Props b/Xamarin.Forms.DualScreen/Directory.Build.Props
new file mode 100644
index 000000000..17c2d03dc
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/Directory.Build.Props
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.DualScreen/Directory.Build.targets b/Xamarin.Forms.DualScreen/Directory.Build.targets
new file mode 100644
index 000000000..4e7a3d2c6
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/Directory.Build.targets
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.DualScreen/DualScreenInfo.shared.cs b/Xamarin.Forms.DualScreen/DualScreenInfo.shared.cs
new file mode 100644
index 000000000..e6f4871ad
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/DualScreenInfo.shared.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Xamarin.Forms.DualScreen
+{
+ public class DualScreenInfo : INotifyPropertyChanged
+ {
+ static Lazy _dualScreenInfo { get; } = new Lazy(OnCreate);
+ public event PropertyChangedEventHandler PropertyChanged;
+ Rectangle[] _spanningBounds;
+ Rectangle _hingeBounds;
+ bool _isLandscape;
+ TwoPaneViewMode _spanMode;
+
+ public static DualScreenInfo Current => _dualScreenInfo.Value;
+ public Rectangle[] SpanningBounds
+ {
+ get => GetSpanningBounds();
+ set
+ {
+ SetProperty(ref _spanningBounds, value);
+ }
+ }
+
+ public Rectangle HingeBounds
+ {
+ get => GetHingeBounds();
+ set
+ {
+ SetProperty(ref _hingeBounds, value);
+ }
+ }
+
+ public bool IsLandscape
+ {
+ get => GetIsLandscape();
+ set
+ {
+ SetProperty(ref _isLandscape, value);
+ }
+ }
+
+ public TwoPaneViewMode SpanMode
+ {
+ get => GetSpanMode();
+ set
+ {
+ SetProperty(ref _spanMode, value);
+ }
+ }
+
+
+ Rectangle[] GetSpanningBounds()
+ {
+ var guide = TwoPaneViewLayoutGuide.Instance;
+ var hinge = guide.Hinge;
+ guide.UpdateLayouts();
+
+ if (hinge == Rectangle.Zero)
+ return new Rectangle[0];
+
+ if (guide.Pane2 == Rectangle.Zero)
+ return new Rectangle[0];
+
+ return new[] { guide.Pane1, guide.Pane2 };
+ }
+
+ Rectangle GetHingeBounds()
+ {
+ var guide = TwoPaneViewLayoutGuide.Instance;
+ guide.UpdateLayouts();
+ return guide.Hinge;
+ }
+
+ bool GetIsLandscape() => TwoPaneViewLayoutGuide.Instance.IsLandscape;
+
+ TwoPaneViewMode GetSpanMode() => TwoPaneViewLayoutGuide.Instance.Mode;
+
+ static DualScreenInfo OnCreate()
+ {
+ DualScreenInfo dualScreenInfo = new DualScreenInfo();
+ TwoPaneViewLayoutGuide.Instance.PropertyChanged += dualScreenInfo.OnTwoPaneViewLayoutGuideChanged;
+ return dualScreenInfo;
+ }
+
+ void OnTwoPaneViewLayoutGuideChanged(object sender, PropertyChangedEventArgs e)
+ {
+ SpanningBounds = GetSpanningBounds();
+ IsLandscape = GetIsLandscape();
+ HingeBounds = GetHingeBounds();
+ SpanMode = GetSpanMode();
+ }
+
+ protected bool SetProperty(ref T backingStore, T value,
+ [CallerMemberName]string propertyName = "",
+ Action onChanged = null)
+ {
+ if (EqualityComparer.Default.Equals(backingStore, value))
+ return false;
+
+ backingStore = value;
+ onChanged?.Invoke();
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ return true;
+ }
+ }
+}
diff --git a/Xamarin.Forms.DualScreen/DualScreenService.android.cs b/Xamarin.Forms.DualScreen/DualScreenService.android.cs
new file mode 100644
index 000000000..4a748796f
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/DualScreenService.android.cs
@@ -0,0 +1,149 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using Android.App;
+using Android.Views;
+using Microsoft.Device.Display;
+using Xamarin.Forms;
+using Xamarin.Forms.DualScreen;
+using Xamarin.Forms.Platform.Android;
+
+[assembly: Dependency(typeof(DualScreenService.DualScreenServiceImpl))]
+
+namespace Xamarin.Forms.DualScreen
+{
+ public class DualScreenService
+ {
+ public static void Init(Activity activity)
+ {
+ DependencyService.Register();
+ DualScreenServiceImpl.Init(activity);
+ }
+
+ internal class DualScreenServiceImpl : IDualScreenService, IDisposable
+ {
+ ScreenHelper _helper;
+ bool _isDuo = false;
+ HingeSensor _hingeSensor;
+ static Activity _mainActivity;
+ static DualScreenServiceImpl _HingeService;
+
+ int _hingeAngle;
+ Rectangle _hingeLocation;
+
+ Activity MainActivity
+ {
+ get => _mainActivity;
+ set => _mainActivity = value;
+ }
+
+ public DualScreenServiceImpl()
+ {
+ _HingeService = this;
+
+ if (_mainActivity != null)
+ Init(_mainActivity);
+ }
+
+ public static void Init(Activity activity)
+ {
+ if (_HingeService == null)
+ {
+ _mainActivity = activity;
+ return;
+ }
+
+ if (activity == _HingeService.MainActivity && _HingeService._helper != null)
+ return;
+
+ _mainActivity = activity;
+ if (_HingeService._helper == null)
+ _HingeService._helper = new ScreenHelper();
+
+ if (_HingeService._hingeSensor != null)
+ {
+ _HingeService._hingeSensor.OnSensorChanged -= _HingeService.OnSensorChanged;
+ _HingeService._hingeSensor.StopListening();
+ }
+
+ _HingeService._isDuo = _HingeService._helper.Initialize(_HingeService.MainActivity);
+
+ if (_HingeService._isDuo)
+ {
+ _HingeService._hingeSensor = new HingeSensor(_HingeService.MainActivity);
+ _HingeService._hingeSensor.OnSensorChanged += _HingeService.OnSensorChanged;
+ _HingeService._hingeSensor.StartListening();
+ }
+ }
+
+ void OnSensorChanged(object sender, HingeSensor.HingeSensorChangedEventArgs e)
+ {
+ if (_hingeLocation != GetHinge())
+ {
+ _hingeLocation = GetHinge();
+ }
+
+ if (_hingeAngle != e.HingeAngle)
+ OnScreenChanged?.Invoke(this, EventArgs.Empty);
+
+ _hingeAngle = e.HingeAngle;
+ }
+
+ public void Dispose()
+ {
+ if (_hingeSensor != null)
+ {
+ _hingeSensor.OnSensorChanged -= OnSensorChanged;
+ _hingeSensor.StopListening();
+ }
+ }
+
+ public bool IsSpanned
+ => _isDuo && (_helper?.IsDualMode ?? false);
+
+ public Rectangle GetHinge()
+ {
+ if (!_isDuo || _helper == null)
+ return Rectangle.Zero;
+
+ var rotation = ScreenHelper.GetRotation(_helper.Activity);
+ var hinge = _helper.DisplayMask.GetBoundingRectsForRotation(rotation).FirstOrDefault();
+ var hingeDp = new Rectangle(PixelsToDp(hinge.Left), PixelsToDp(hinge.Top), PixelsToDp(hinge.Width()), PixelsToDp(hinge.Height()));
+
+ return hingeDp;
+ }
+
+ public bool IsLandscape
+ {
+ get
+ {
+ if (!_isDuo || _helper == null)
+ return false;
+
+ var rotation = ScreenHelper.GetRotation(_helper.Activity);
+
+ return (rotation == SurfaceOrientation.Rotation270 || rotation == SurfaceOrientation.Rotation90);
+ }
+ }
+
+ double PixelsToDp(double px)
+ => px / MainActivity.Resources.DisplayMetrics.Density;
+
+ public event EventHandler OnScreenChanged;
+
+
+ public Point? GetLocationOnScreen(VisualElement visualElement)
+ {
+ var view = Platform.Android.Platform.GetRenderer(visualElement);
+
+ if (view?.View == null)
+ return null;
+
+ int[] location = new int[2];
+ view.View.GetLocationOnScreen(location);
+ return new Point(view.View.Context.FromPixels(location[0]), view.View.Context.FromPixels(location[1]));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.DualScreen/DualScreenService.uwp.cs b/Xamarin.Forms.DualScreen/DualScreenService.uwp.cs
new file mode 100644
index 000000000..8acd8abab
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/DualScreenService.uwp.cs
@@ -0,0 +1,90 @@
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Windows.Graphics.Display;
+using Windows.UI.ViewManagement;
+
+#if UWP_18362
+using Windows.UI.WindowManagement;
+#endif
+
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Hosting;
+using Xamarin.Forms;
+using Xamarin.Forms.DualScreen;
+using Xamarin.Forms.Platform.UWP;
+
+[assembly: Dependency(typeof(DualScreenService))]
+namespace Xamarin.Forms.DualScreen
+{
+ internal partial class DualScreenService : IDualScreenService
+ {
+#pragma warning disable CS0067
+ public event EventHandler OnScreenChanged;
+ public event PropertyChangedEventHandler PropertyChanged;
+#pragma warning restore CS0067
+
+ public DualScreenService()
+ {
+ }
+
+ public bool IsSpanned
+ {
+ get
+ {
+ var visibleBounds = ApplicationView.GetForCurrentView().VisibleBounds;
+
+ if (visibleBounds.Height > 1200 || visibleBounds.Width > 1200)
+ return true;
+
+ return false;
+ }
+ }
+
+ public bool IsLandscape
+ {
+ get
+ {
+ if (IsSpanned)
+ return ApplicationView.GetForCurrentView().Orientation == ApplicationViewOrientation.Portrait;
+ else
+ return ApplicationView.GetForCurrentView().Orientation == ApplicationViewOrientation.Landscape;
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public Rectangle GetHinge()
+ {
+ var screen = DisplayInformation.GetForCurrentView();
+
+ if (IsLandscape)
+ {
+ if (IsSpanned)
+ return new Rectangle(0, 664 + 24, ScaledPixels(screen.ScreenWidthInRawPixels), 0);
+ else
+ return new Rectangle(0, 664, ScaledPixels(screen.ScreenWidthInRawPixels), 0);
+ }
+ else
+ return new Rectangle(720, 0, 0, ScaledPixels(screen.ScreenHeightInRawPixels));
+ }
+
+ double ScaledPixels(double n)
+ => n / DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
+
+ public Point? GetLocationOnScreen(VisualElement visualElement)
+ {
+ var view = Platform.UWP.Platform.GetRenderer(visualElement);
+
+ if (view?.ContainerElement == null)
+ return null;
+
+ var ttv = view.ContainerElement.TransformToVisual(Window.Current.Content);
+ Windows.Foundation.Point screenCoords = ttv.TransformPoint(new Windows.Foundation.Point(0, 0));
+
+ return new Point(screenCoords.X, screenCoords.Y);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.DualScreen/IDualScreenService.shared.cs b/Xamarin.Forms.DualScreen/IDualScreenService.shared.cs
new file mode 100644
index 000000000..6934bc4f3
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/IDualScreenService.shared.cs
@@ -0,0 +1,16 @@
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Xamarin.Forms;
+
+namespace Xamarin.Forms.DualScreen
+{
+ internal interface IDualScreenService : IDisposable
+ {
+ event EventHandler OnScreenChanged;
+ bool IsSpanned { get; }
+ bool IsLandscape { get; }
+ Rectangle GetHinge();
+ Point? GetLocationOnScreen(VisualElement visualElement);
+ }
+}
diff --git a/Xamarin.Forms.DualScreen/NoDualScreenServiceImpl.shared.cs b/Xamarin.Forms.DualScreen/NoDualScreenServiceImpl.shared.cs
new file mode 100644
index 000000000..29962520b
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/NoDualScreenServiceImpl.shared.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.DualScreen
+{
+ internal class NoDualScreenServiceImpl : IDualScreenService
+ {
+ static Lazy _Instance = new Lazy(() => new NoDualScreenServiceImpl());
+ public static NoDualScreenServiceImpl Instance => _Instance.Value;
+
+ public NoDualScreenServiceImpl()
+ {
+ }
+
+ public bool IsSpanned => false;
+
+ public bool IsLandscape => Device.info.CurrentOrientation.IsLandscape();
+
+ public event EventHandler OnScreenChanged
+ {
+ add
+ {
+
+ }
+ remove
+ {
+
+ }
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public Rectangle GetHinge()
+ {
+ return Rectangle.Zero;
+ }
+
+ public Point? GetLocationOnScreen(VisualElement visualElement)
+ {
+ return null;
+ }
+ }
+}
diff --git a/Xamarin.Forms.DualScreen/TwoPaneView.shared.cs b/Xamarin.Forms.DualScreen/TwoPaneView.shared.cs
new file mode 100644
index 000000000..8469909fc
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/TwoPaneView.shared.cs
@@ -0,0 +1,471 @@
+using System;
+
+namespace Xamarin.Forms.DualScreen
+{
+ public enum TwoPaneViewMode
+ {
+ SinglePane, Wide, Tall
+ }
+
+ public enum TwoPaneViewTallModeConfiguration
+ {
+ SinglePane,
+ TopBottom,
+ BottomTop,
+ }
+
+ public enum TwoPaneViewWideModeConfiguration
+ {
+ SinglePane,
+ LeftRight,
+ RightLeft,
+ }
+
+ public enum TwoPaneViewPriority
+ {
+ Pane1,
+ Pane2
+ }
+
+ [ContentProperty("")]
+ public partial class TwoPaneView : Grid
+ {
+
+ static TwoPaneView()
+ {
+#if UWP
+ DependencyService.Register();
+#elif !ANDROID
+ DependencyService.Register();
+#endif
+ }
+
+ enum ViewMode
+ {
+ Pane1Only,
+ Pane2Only,
+ LeftRight,
+ RightLeft,
+ TopBottom,
+ BottomTop,
+ None
+ };
+
+ TwoPaneViewLayoutGuide _twoPaneViewLayoutGuide;
+ VisualStateGroup _modeStates;
+ ContentView _content1;
+ ContentView _content2;
+ ViewMode _currentMode;
+ bool _hasMeasured = false;
+ bool _updatingMode = false;
+ bool _performingLayout = false;
+ bool _processPendingChange = false;
+
+ public static readonly BindableProperty TallModeConfigurationProperty
+ = BindableProperty.Create("TallModeConfiguration", typeof(TwoPaneViewTallModeConfiguration), typeof(TwoPaneView), defaultValue: TwoPaneViewTallModeConfiguration.TopBottom, propertyChanged: OnJustInvalidateLayout);
+
+ public static readonly BindableProperty WideModeConfigurationProperty
+ = BindableProperty.Create("WideModeConfiguration", typeof(TwoPaneViewWideModeConfiguration), typeof(TwoPaneView), defaultValue: TwoPaneViewWideModeConfiguration.LeftRight, propertyChanged: OnJustInvalidateLayout);
+
+ public static readonly BindableProperty Pane1Property
+ = BindableProperty.Create("Pane1", typeof(View), typeof(TwoPaneView), propertyChanged: (b, o, n) => OnPanePropertyChanged(b, o, n, 0));
+
+ public static readonly BindableProperty Pane2Property
+ = BindableProperty.Create("Pane2", typeof(View), typeof(TwoPaneView), propertyChanged: (b, o, n) => OnPanePropertyChanged(b, o, n, 1));
+
+ public static readonly BindablePropertyKey ModePropertyKey
+ = BindableProperty.CreateReadOnly("Mode", typeof(TwoPaneViewMode), typeof(TwoPaneView), defaultValue: TwoPaneViewMode.SinglePane, propertyChanged: OnModePropertyChanged);
+
+ public static readonly BindableProperty ModeProperty = ModePropertyKey.BindableProperty;
+
+ public static readonly BindableProperty PanePriorityProperty
+ = BindableProperty.Create("PanePriority", typeof(TwoPaneViewPriority), typeof(TwoPaneView), defaultValue: TwoPaneViewPriority.Pane1, propertyChanged: OnJustInvalidateLayout);
+
+ public static readonly BindableProperty MinTallModeHeightProperty
+ = BindableProperty.Create("MinTallModeHeight", typeof(double), typeof(TwoPaneView), defaultValueCreator:OnMinModePropertyCreate, propertyChanged: OnJustInvalidateLayout);
+
+ public static readonly BindableProperty MinWideModeWidthProperty
+ = BindableProperty.Create("MinWideModeWidth", typeof(double), typeof(TwoPaneView), defaultValueCreator: OnMinModePropertyCreate, propertyChanged: OnJustInvalidateLayout);
+
+ public static readonly BindableProperty Pane1LengthProperty
+ = BindableProperty.Create("Pane1Length", typeof(GridLength), typeof(TwoPaneView), defaultValue: GridLength.Star, propertyChanged: OnJustInvalidateLayout);
+
+ public static readonly BindableProperty Pane2LengthProperty
+ = BindableProperty.Create("Pane2Length", typeof(GridLength), typeof(TwoPaneView), defaultValue: GridLength.Star, propertyChanged: OnJustInvalidateLayout);
+
+ public event EventHandler ModeChanged;
+
+ static object OnMinModePropertyCreate(BindableObject bindable)
+ {
+ if (Device.info == null)
+ return 641;
+
+ return 641 / Device.info.ScalingFactor;
+ }
+
+
+ static void OnModePropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ ((TwoPaneView)bindable).ModeChanged?.Invoke(bindable, EventArgs.Empty);
+ }
+
+ static void OnJustInvalidateLayout(BindableObject bindable, object oldValue, object newValue)
+ {
+ var b = (TwoPaneView)bindable;
+ if (!b._performingLayout && !b._updatingMode)
+ {
+ b.UpdateMode();
+ }
+ else
+ {
+ b._processPendingChange = true;
+ }
+ }
+
+ static void OnPanePropertyChanged(BindableObject bindable, object oldValue, object newValue, int paneIndex)
+ {
+ TwoPaneView twoPaneView = (TwoPaneView)bindable;
+ var newView = (View)newValue;
+
+ if (paneIndex == 0)
+ twoPaneView._content1.Content = newView;
+ else
+ twoPaneView._content2.Content = newView;
+
+ OnJustInvalidateLayout(bindable, null, null);
+ }
+
+ public double MinTallModeHeight
+ {
+ get { return (double)GetValue(MinTallModeHeightProperty); }
+ set { SetValue(MinTallModeHeightProperty, value); }
+ }
+ public double MinWideModeWidth
+ {
+ get { return (double)GetValue(MinWideModeWidthProperty); }
+ set { SetValue(MinWideModeWidthProperty, value); }
+ }
+
+ public GridLength Pane1Length
+ {
+ get { return (GridLength)GetValue(Pane1LengthProperty); }
+ set { SetValue(Pane1LengthProperty, value); }
+ }
+ public GridLength Pane2Length
+ {
+ get { return (GridLength)GetValue(Pane2LengthProperty); }
+ set { SetValue(Pane2LengthProperty, value); }
+ }
+
+ public TwoPaneViewMode Mode { get => (TwoPaneViewMode)GetValue(ModeProperty); }
+
+ public TwoPaneViewTallModeConfiguration TallModeConfiguration
+ {
+ get { return (TwoPaneViewTallModeConfiguration)GetValue(TallModeConfigurationProperty); }
+ set { SetValue(TallModeConfigurationProperty, value); }
+ }
+
+ public TwoPaneViewWideModeConfiguration WideModeConfiguration
+ {
+ get { return (TwoPaneViewWideModeConfiguration)GetValue(WideModeConfigurationProperty); }
+ set { SetValue(WideModeConfigurationProperty, value); }
+ }
+
+ public View Pane1
+ {
+ get { return (View)GetValue(Pane1Property); }
+ set { SetValue(Pane1Property, value); }
+ }
+
+ public View Pane2
+ {
+ get { return (View)GetValue(Pane2Property); }
+ set { SetValue(Pane2Property, value); }
+ }
+
+ public TwoPaneViewPriority PanePriority
+ {
+ get { return (TwoPaneViewPriority)GetValue(PanePriorityProperty); }
+ set { SetValue(PanePriorityProperty, value); }
+ }
+
+ public TwoPaneView() : base()
+ {
+ _content1 = new ContentView();
+ _content2 = new ContentView();
+
+ Children.Add(_content1);
+ Children.Add(_content2);
+
+ this.VerticalOptions = LayoutOptions.Fill;
+ this.HorizontalOptions = LayoutOptions.Fill;
+ ColumnSpacing = 0;
+ RowSpacing = 0;
+
+ _modeStates = new VisualStateGroup()
+ {
+ Name = "ModeStates"
+ };
+
+ _modeStates.States.Add(new VisualState() { Name = "ViewMode_OneOnly" });
+ _modeStates.States.Add(new VisualState() { Name = "ViewMode_TwoOnly" });
+ _modeStates.States.Add(new VisualState() { Name = "ViewMode_LeftRight" });
+ _modeStates.States.Add(new VisualState() { Name = "ViewMode_RightLeft" });
+ _modeStates.States.Add(new VisualState() { Name = "ViewMode_TopBottom" });
+ _modeStates.States.Add(new VisualState() { Name = "ViewMode_BottomTop" });
+
+ VisualStateManager.SetVisualStateGroups(this, new VisualStateGroupList() { _modeStates });
+
+ this.RowDefinitions = new RowDefinitionCollection() { new RowDefinition(), new RowDefinition(), new RowDefinition() };
+ this.ColumnDefinitions = new ColumnDefinitionCollection() { new ColumnDefinition(), new ColumnDefinition(), new ColumnDefinition() };
+ }
+
+ internal override void OnIsPlatformEnabledChanged()
+ {
+ base.OnIsPlatformEnabledChanged();
+ if (IsPlatformEnabled)
+ {
+ TwoPaneViewLayoutGuide.WatchForChanges();
+ TwoPaneViewLayoutGuide.PropertyChanged += OnTwoPaneViewLayoutGuide;
+ }
+ else
+ {
+ TwoPaneViewLayoutGuide.WatchForChanges();
+ TwoPaneViewLayoutGuide.PropertyChanged += OnTwoPaneViewLayoutGuide;
+ }
+ }
+
+ TwoPaneViewLayoutGuide TwoPaneViewLayoutGuide
+ {
+ get
+ {
+ if (_twoPaneViewLayoutGuide == null)
+ {
+ _twoPaneViewLayoutGuide = new TwoPaneViewLayoutGuide(this);
+ }
+
+ return _twoPaneViewLayoutGuide;
+ }
+ }
+
+ void OnTwoPaneViewLayoutGuide(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ UpdateMode();
+ }
+
+ protected override void LayoutChildren(double x, double y, double width, double height)
+ {
+ if (_updatingMode)
+ return;
+
+ if (_hasMeasured)
+ base.LayoutChildren(x, y, width, height);
+ else
+ UpdateMode();
+ }
+
+ void UpdateMode()
+ {
+ _updatingMode = true;
+ try
+ {
+ double controlWidth = this.Width;
+ double controlHeight = this.Height;
+
+ ViewMode newMode = (PanePriority == TwoPaneViewPriority.Pane1) ? ViewMode.Pane1Only : ViewMode.Pane2Only;
+
+ _hasMeasured = true;
+
+ this.TwoPaneViewLayoutGuide.UpdateLayouts();
+
+ if (TwoPaneViewLayoutGuide.Mode != TwoPaneViewMode.SinglePane)
+ {
+ if (TwoPaneViewLayoutGuide.Mode == TwoPaneViewMode.Wide)
+ {
+ // Regions are laid out horizontally
+ if (WideModeConfiguration != TwoPaneViewWideModeConfiguration.SinglePane)
+ {
+ newMode = (WideModeConfiguration == TwoPaneViewWideModeConfiguration.LeftRight) ? ViewMode.LeftRight : ViewMode.RightLeft;
+ }
+ }
+ else if (TwoPaneViewLayoutGuide.Mode == TwoPaneViewMode.Tall)
+ {
+ // Regions are laid out vertically
+ if (TallModeConfiguration != TwoPaneViewTallModeConfiguration.SinglePane)
+ {
+ newMode = (TallModeConfiguration == TwoPaneViewTallModeConfiguration.TopBottom) ? ViewMode.TopBottom : ViewMode.BottomTop;
+ }
+ }
+ }
+ else
+ {
+ // One region
+ if (controlWidth > MinWideModeWidth && WideModeConfiguration != TwoPaneViewWideModeConfiguration.SinglePane)
+ {
+ // Split horizontally
+ newMode = (WideModeConfiguration == TwoPaneViewWideModeConfiguration.LeftRight) ? ViewMode.LeftRight : ViewMode.RightLeft;
+ }
+ else if (controlHeight > MinTallModeHeight && TallModeConfiguration != TwoPaneViewTallModeConfiguration.SinglePane)
+ {
+ // Split vertically
+ newMode = (TallModeConfiguration == TwoPaneViewTallModeConfiguration.TopBottom) ? ViewMode.TopBottom : ViewMode.BottomTop;
+ }
+ }
+
+ // Update row/column sizes (this may need to happen even if the mode doesn't change)
+ UpdateRowsColumns(newMode);
+
+ // Update mode if necessary
+ if (newMode != _currentMode)
+ {
+ _currentMode = newMode;
+
+ TwoPaneViewMode newViewMode = TwoPaneViewMode.SinglePane;
+
+ switch (_currentMode)
+ {
+ case ViewMode.Pane1Only:
+ VisualStateManager.GoToState(this, "ViewMode_OneOnly"); break;
+ case ViewMode.Pane2Only:
+ VisualStateManager.GoToState(this, "ViewMode_TwoOnly"); break;
+ case ViewMode.LeftRight:
+ VisualStateManager.GoToState(this, "ViewMode_LeftRight"); newViewMode = TwoPaneViewMode.Wide; break;
+ case ViewMode.RightLeft:
+ VisualStateManager.GoToState(this, "ViewMode_RightLeft"); newViewMode = TwoPaneViewMode.Wide; break;
+ case ViewMode.TopBottom:
+ VisualStateManager.GoToState(this, "ViewMode_TopBottom"); newViewMode = TwoPaneViewMode.Tall; break;
+ case ViewMode.BottomTop:
+ VisualStateManager.GoToState(this, "ViewMode_BottomTop"); newViewMode = TwoPaneViewMode.Tall; break;
+ }
+
+ if (newViewMode != Mode)
+ {
+ _updatingMode = false;
+ SetValue(ModePropertyKey, newViewMode);
+ }
+ }
+
+ _updatingMode = false;
+
+ if (_processPendingChange)
+ {
+ _processPendingChange = false;
+ UpdateMode();
+ }
+ else
+ {
+ InvalidateLayout();
+ }
+ }
+ finally
+ {
+ _updatingMode = false;
+ }
+ }
+
+ void UpdateRowsColumns(ViewMode newMode)
+ {
+ var _columnLeft = ColumnDefinitions[0];
+ var _columnMiddle = ColumnDefinitions[1];
+ var _columnRight = ColumnDefinitions[2];
+
+ var _rowTop = RowDefinitions[0];
+ var _rowMiddle = RowDefinitions[1];
+ var _rowBottom = RowDefinitions[2];
+
+ _columnMiddle.Width = new GridLength(0, GridUnitType.Absolute);
+ _rowMiddle.Height = new GridLength(0, GridUnitType.Absolute);
+
+ if (newMode == ViewMode.LeftRight || newMode == ViewMode.RightLeft)
+ {
+ _columnLeft.Width = ((newMode == ViewMode.LeftRight) ? Pane1Length : Pane2Length);
+ _columnRight.Width = ((newMode == ViewMode.LeftRight) ? Pane2Length : Pane1Length);
+ }
+ else
+ {
+ _columnLeft.Width = new GridLength(1, GridUnitType.Star);
+ _columnRight.Width = new GridLength(0, GridUnitType.Absolute);
+ }
+
+ if (newMode == ViewMode.TopBottom || newMode == ViewMode.BottomTop)
+ {
+ _rowTop.Height = ((newMode == ViewMode.TopBottom) ? Pane1Length : Pane2Length);
+ _rowBottom.Height = ((newMode == ViewMode.TopBottom) ? Pane2Length : Pane1Length);
+ }
+ else
+ {
+ _rowTop.Height = new GridLength(1, GridUnitType.Star);
+ _rowBottom.Height = new GridLength(0, GridUnitType.Absolute);
+ }
+
+ if (TwoPaneViewLayoutGuide.Mode != TwoPaneViewMode.SinglePane && newMode != ViewMode.Pane1Only && newMode != ViewMode.Pane2Only)
+ {
+ Rectangle rc1 = _twoPaneViewLayoutGuide.Pane1;
+ Rectangle rc2 = _twoPaneViewLayoutGuide.Pane2;
+ Rectangle hinge = _twoPaneViewLayoutGuide.Hinge;
+
+ if (TwoPaneViewLayoutGuide.Mode == TwoPaneViewMode.Wide)
+ {
+ _columnMiddle.Width = new GridLength(hinge.Width, GridUnitType.Absolute);
+ _columnLeft.Width = new GridLength(rc1.Width, GridUnitType.Absolute);
+ _columnRight.Width = new GridLength(rc2.Width, GridUnitType.Absolute);
+ }
+ else
+ {
+ _rowMiddle.Height = new GridLength(hinge.Height, GridUnitType.Absolute);
+ _rowTop.Height = new GridLength(rc1.Height, GridUnitType.Absolute);
+ _rowBottom.Height = new GridLength(rc2.Height, GridUnitType.Absolute);
+ }
+ }
+
+ switch (newMode)
+ {
+ case ViewMode.LeftRight:
+ SetRowColumn(_content1, 0, 0);
+ SetRowColumn(_content2, 0, 2);
+ _content2.IsVisible = true;
+ _content2.IsVisible = true;
+ break;
+ case ViewMode.RightLeft:
+ SetRowColumn(_content1, 0, 2);
+ SetRowColumn(_content2, 0, 0);
+ _content2.IsVisible = true;
+ _content2.IsVisible = true;
+ break;
+ case ViewMode.TopBottom:
+ SetRowColumn(_content1, 0, 0);
+ SetRowColumn(_content2, 2, 0);
+ _content2.IsVisible = true;
+ _content2.IsVisible = true;
+ break;
+ case ViewMode.BottomTop:
+ SetRowColumn(_content1, 2, 0);
+ SetRowColumn(_content2, 0, 0);
+ _content2.IsVisible = true;
+ _content2.IsVisible = true;
+ break;
+ case ViewMode.Pane1Only:
+ SetRowColumn(_content1, 0, 0);
+ SetRowColumn(_content2, 0, 2);
+ _content1.IsVisible = true;
+ _content2.IsVisible = false;
+ break;
+ case ViewMode.Pane2Only:
+ SetRowColumn(_content1, 0, 2);
+ SetRowColumn(_content2, 0, 0);
+ _content1.IsVisible = false;
+ _content2.IsVisible = true;
+ break;
+ }
+
+ void SetRowColumn(BindableObject bo, int row, int column)
+ {
+ if (bo == null)
+ return;
+
+ Grid.SetColumn(bo, column);
+ Grid.SetRow(bo, row);
+ }
+ }
+ }
+}
diff --git a/Xamarin.Forms.DualScreen/TwoPaneViewLayoutGuide.shared.cs b/Xamarin.Forms.DualScreen/TwoPaneViewLayoutGuide.shared.cs
new file mode 100644
index 000000000..5f1eeaa23
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/TwoPaneViewLayoutGuide.shared.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Xamarin.Forms.DualScreen
+{
+ internal class TwoPaneViewLayoutGuide : INotifyPropertyChanged
+ {
+ public static TwoPaneViewLayoutGuide Instance => _twoPaneViewLayoutGuide.Value;
+ static Lazy _twoPaneViewLayoutGuide = new Lazy(() => new TwoPaneViewLayoutGuide());
+
+ IDualScreenService DualScreenService =>
+ DependencyService.Get() ?? NoDualScreenServiceImpl.Instance;
+
+ Rectangle _hinge;
+ Rectangle _leftPage;
+ Rectangle _rightPane;
+ TwoPaneViewMode _mode;
+ Layout _layout;
+ bool _isLandscape;
+ public event PropertyChangedEventHandler PropertyChanged;
+ List _pendingPropertyChanges = new List();
+
+ TwoPaneViewLayoutGuide()
+ {
+ }
+
+ public TwoPaneViewLayoutGuide(Layout layout)
+ {
+ _layout = layout;
+ }
+
+ public void WatchForChanges()
+ {
+ StopWatchingForChanges();
+ DualScreenService.OnScreenChanged += OnScreenChanged;
+
+ if (_layout != null)
+ {
+ _layout.SizeChanged += OnLayoutChanged;
+ }
+ if (Device.Info is INotifyPropertyChanged npc)
+ {
+ npc.PropertyChanged += OnDeviceInfoChanged;
+ }
+ }
+
+ public void StopWatchingForChanges()
+ {
+ DualScreenService.OnScreenChanged -= OnScreenChanged;
+
+ if (_layout != null)
+ {
+ _layout.SizeChanged -= OnLayoutChanged;
+ }
+ if (Device.Info is INotifyPropertyChanged npc)
+ {
+ npc.PropertyChanged -= OnDeviceInfoChanged;
+ }
+ }
+
+ void OnLayoutChanged(object sender, EventArgs e)
+ {
+ UpdateLayouts();
+ }
+
+ void OnScreenChanged(object sender, EventArgs e)
+ {
+ UpdateLayouts();
+ }
+
+ void OnDeviceInfoChanged(object sender, PropertyChangedEventArgs args)
+ {
+ if (args.PropertyName == nameof(Device.Info.CurrentOrientation))
+ {
+ UpdateLayouts();
+ }
+ }
+
+ public bool IsLandscape
+ {
+ get => DualScreenService.IsLandscape;
+ set => SetProperty(ref _isLandscape, value);
+ }
+
+ public TwoPaneViewMode Mode
+ {
+ get
+ {
+ return GetTwoPaneViewMode();
+ }
+ private set
+ {
+ SetProperty(ref _mode, value);
+ }
+ }
+
+ public Rectangle Pane1
+ {
+ get
+ {
+ return _leftPage;
+ }
+ private set
+ {
+ SetProperty(ref _leftPage, value);
+ }
+ }
+
+ public Rectangle Pane2
+ {
+ get
+ {
+ return _rightPane;
+ }
+ private set
+ {
+ SetProperty(ref _rightPane, value);
+ }
+ }
+
+ public Rectangle Hinge
+ {
+ get
+ {
+ return DualScreenService.GetHinge();
+ }
+ private set
+ {
+ SetProperty(ref _hinge, value);
+ }
+ }
+
+ internal void UpdateLayouts()
+ {
+ Rectangle containerArea;
+ if (_layout == null)
+ {
+ containerArea = new Rectangle(Point.Zero, Device.info.ScaledScreenSize);
+ }
+ else
+ {
+ containerArea = _layout.Bounds;
+ }
+
+ if (containerArea.Width <= 0)
+ {
+ return;
+ }
+
+ Rectangle _newPane1 = Pane1;
+ Rectangle _newPane2 = Pane2;
+
+ if (!DualScreenService.IsLandscape)
+ {
+ if (DualScreenService.IsSpanned)
+ {
+ var paneWidth = (containerArea.Width - Hinge.Width) / 2;
+ _newPane1 = new Rectangle(0, 0, paneWidth, containerArea.Height);
+ _newPane2 = new Rectangle(paneWidth + Hinge.Width, 0, paneWidth, Pane1.Height);
+ }
+ else
+ {
+ _newPane1 = new Rectangle(0, 0, containerArea.Width, containerArea.Height);
+ _newPane2 = Rectangle.Zero;
+ }
+ }
+ else
+ {
+ if (DualScreenService.IsSpanned)
+ {
+ Point displayedScreenAbsCoordinates = Point.Zero;
+
+ if (_layout != null)
+ displayedScreenAbsCoordinates = DualScreenService.GetLocationOnScreen(_layout) ?? Point.Zero;
+
+ var screenSize = Device.info.ScaledScreenSize;
+ var topStuffHeight = displayedScreenAbsCoordinates.Y;
+ var bottomStuffHeight = screenSize.Height - topStuffHeight - containerArea.Height;
+ var paneWidth = containerArea.Width;
+ var leftPaneHeight = Hinge.Y - topStuffHeight;
+ var rightPaneHeight = screenSize.Height - topStuffHeight - leftPaneHeight - bottomStuffHeight - Hinge.Height;
+
+ _newPane1 = new Rectangle(0, 0, paneWidth, leftPaneHeight);
+ _newPane2 = new Rectangle(0, Hinge.Y + Hinge.Height - topStuffHeight, paneWidth, rightPaneHeight);
+ }
+ else
+ {
+ _newPane1 = new Rectangle(0, 0, containerArea.Width, containerArea.Height);
+ _newPane2 = Rectangle.Zero;
+ }
+ }
+
+ if (_newPane2.Height < 0 || _newPane2.Width < 0)
+ _newPane2 = Rectangle.Zero;
+
+ if (_newPane1.Height < 0 || _newPane1.Width < 0)
+ _newPane1 = Rectangle.Zero;
+
+ Pane1 = _newPane1;
+ Pane2 = _newPane2;
+ Mode = GetTwoPaneViewMode();
+ Hinge = DualScreenService.GetHinge();
+ IsLandscape = DualScreenService.IsLandscape;
+
+ var properties = _pendingPropertyChanges.ToList();
+ _pendingPropertyChanges.Clear();
+
+ foreach(var property in properties)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
+ }
+ }
+
+ TwoPaneViewMode GetTwoPaneViewMode()
+ {
+ if (!DualScreenService.IsSpanned)
+ return TwoPaneViewMode.SinglePane;
+
+ if (DualScreenService.IsLandscape)
+ return TwoPaneViewMode.Tall;
+
+ return TwoPaneViewMode.Wide;
+ }
+
+ protected bool SetProperty(ref T backingStore, T value,
+ [CallerMemberName]string propertyName = "",
+ Action onChanged = null)
+ {
+ if (EqualityComparer.Default.Equals(backingStore, value))
+ return false;
+
+ backingStore = value;
+ onChanged?.Invoke();
+ _pendingPropertyChanges.Add(propertyName);
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.DualScreen/Xamarin.Forms.DualScreen.csproj b/Xamarin.Forms.DualScreen/Xamarin.Forms.DualScreen.csproj
new file mode 100644
index 000000000..b3e6c077b
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/Xamarin.Forms.DualScreen.csproj
@@ -0,0 +1,81 @@
+
+
+ Xamarin.Forms.DualScreen
+ Xamarin.Forms.DualScreen
+ Xamarin.Forms.DualScreen
+ $(TargetFrameworks);$(AndroidTargetFrameworks);netstandard1.0;netstandard2.0
+ true
+ Debug
+ AnyCPU
+ false
+ ..\
+ true
+ false
+
+
+ $(DefineConstants);UWP_14393
+
+
+ $(DefineConstants);UWP_18362
+
+
+ $(DefineConstants);UWP
+
+
+ $(DefineConstants);ANDROID
+
+
+ true
+
+
+ false
+ bin\$(Configuration)\$(TargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Xamarin.Forms.DualScreen/global.json b/Xamarin.Forms.DualScreen/global.json
new file mode 100644
index 000000000..80d84a257
--- /dev/null
+++ b/Xamarin.Forms.DualScreen/global.json
@@ -0,0 +1,5 @@
+{
+ "msbuild-sdks": {
+ "MSBuild.Sdk.Extras": "2.0.54"
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Maps.UWP/Directory.Build.targets b/Xamarin.Forms.Maps.UWP/Directory.Build.targets
index 4e7a3d2c6..48c14d66e 100644
--- a/Xamarin.Forms.Maps.UWP/Directory.Build.targets
+++ b/Xamarin.Forms.Maps.UWP/Directory.Build.targets
@@ -1,4 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.Android/AndroidTicker.cs b/Xamarin.Forms.Platform.Android/AndroidTicker.cs
index 6b2c38971..8c17c2832 100644
--- a/Xamarin.Forms.Platform.Android/AndroidTicker.cs
+++ b/Xamarin.Forms.Platform.Android/AndroidTicker.cs
@@ -3,14 +3,15 @@ using Android.Animation;
using Android.Content;
using Android.OS;
using Xamarin.Forms.Internals;
+using GlobalSettings = Android.Provider.Settings.Global;
+using SystemSettings = Android.Provider.Settings.System;
namespace Xamarin.Forms.Platform.Android
{
internal class AndroidTicker : Ticker, IDisposable
{
ValueAnimator _val;
- bool _energySaveModeDisabled;
- readonly bool _animatorEnabled;
+ bool _animatorEnabled;
public AndroidTicker()
{
@@ -19,37 +20,46 @@ namespace Xamarin.Forms.Platform.Android
_val.RepeatCount = ValueAnimator.Infinite;
_val.Update += OnValOnUpdate;
_animatorEnabled = IsAnimatorEnabled();
- CheckPowerSaveModeStatus();
+ CheckAnimationEnabledStatus();
}
- public override bool SystemEnabled => _energySaveModeDisabled && _animatorEnabled;
+ public override bool SystemEnabled => _animatorEnabled;
- internal void CheckPowerSaveModeStatus()
+ internal void CheckAnimationEnabledStatus()
{
- // Android disables animations when it's in power save mode
- // So we need to keep track of whether we're in that mode and handle animations accordingly
- // We can't just check ValueAnimator.AreAnimationsEnabled() because there's no event for that, and it's
- // only supported on API >= 26
+ var animatorEnabled = IsAnimatorEnabled();
- if (!Forms.IsLollipopOrNewer)
+ if (animatorEnabled != _animatorEnabled)
{
- _energySaveModeDisabled = true;
- return;
+ _animatorEnabled = animatorEnabled;
+
+ // Notify the ticker that this value has changed, so it can manage animations in progress
+ OnSystemEnabledChanged();
}
-
- var powerManager = (PowerManager)Forms.ApplicationContext.GetSystemService(Context.PowerService);
-
- var powerSaveOn = powerManager.IsPowerSaveMode;
-
- // If power saver is active, then animations will not run
- _energySaveModeDisabled = !powerSaveOn;
-
- // Notify the ticker that this value has changed, so it can manage animations in progress
- OnSystemEnabledChanged();
}
static bool IsAnimatorEnabled()
{
+ if (Forms.IsOreoOrNewer)
+ {
+ // For more recent API levels, we can just check this method and be done with it
+ return ValueAnimator.AreAnimatorsEnabled();
+ }
+
+ if (Forms.IsLollipopOrNewer)
+ {
+ // For API levels which support power saving but not AreAnimatorsEnabled, we can check the
+ // power save mode; for these API levels, power saving == ON will mean that animations are disabled
+ var powerManager = (PowerManager)Forms.ApplicationContext.GetSystemService(Context.PowerService);
+ if (powerManager.IsPowerSaveMode)
+ {
+ return false;
+ }
+ }
+
+ // If we're not in power save mode (or don't support it), we still need to check the AnimatorDurationScale;
+ // animations might be disabled by developer mode
+
var resolver = global::Android.App.Application.Context?.ContentResolver;
if (resolver == null)
{
@@ -58,14 +68,14 @@ namespace Xamarin.Forms.Platform.Android
float animationScale;
- if (Build.VERSION.SdkInt >= BuildVersionCodes.JellyBeanMr1)
+ if (Forms.IsJellyBeanMr1OrNewer)
{
- animationScale = global::Android.Provider.Settings.Global.GetFloat(resolver, global::Android.Provider.Settings.Global.AnimatorDurationScale, 1);
+ animationScale = GlobalSettings.GetFloat(resolver, GlobalSettings.AnimatorDurationScale, 1);
}
else
{
#pragma warning disable 0618
- animationScale = global::Android.Provider.Settings.System.GetFloat(resolver, global::Android.Provider.Settings.System.AnimatorDurationScale, 1);
+ animationScale = SystemSettings.GetFloat(resolver, SystemSettings.AnimatorDurationScale, 1);
#pragma warning restore 0618
}
diff --git a/Xamarin.Forms.Platform.Android/Forms.cs b/Xamarin.Forms.Platform.Android/Forms.cs
index 7952f6745..4a5b62ddc 100644
--- a/Xamarin.Forms.Platform.Android/Forms.cs
+++ b/Xamarin.Forms.Platform.Android/Forms.cs
@@ -20,7 +20,6 @@ using Xamarin.Forms.Internals;
using Xamarin.Forms.Platform.Android;
using Resource = Android.Resource;
using Trace = System.Diagnostics.Trace;
-using ALayoutDirection = Android.Views.LayoutDirection;
using System.ComponentModel;
namespace Xamarin.Forms
@@ -36,9 +35,9 @@ namespace Xamarin.Forms
public InitializationOptions(Context activity, Bundle bundle, Assembly resourceAssembly)
{
this = default(InitializationOptions);
- this.Activity = activity;
- this.Bundle = bundle;
- this.ResourceAssembly = resourceAssembly;
+ Activity = activity;
+ Bundle = bundle;
+ ResourceAssembly = resourceAssembly;
}
public Context Activity;
public Bundle Bundle;
@@ -50,7 +49,6 @@ namespace Xamarin.Forms
public static class Forms
{
-
const int TabletCrossover = 600;
static BuildVersionCodes? s_sdkInt;
@@ -58,6 +56,8 @@ namespace Xamarin.Forms
static bool? s_is29OrNewer;
static bool? s_isMarshmallowOrNewer;
static bool? s_isNougatOrNewer;
+ static bool? s_isOreoOrNewer;
+ static bool? s_isJellyBeanMr1OrNewer;
[Obsolete("Context is obsolete as of version 2.5. Please use a local context instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -73,8 +73,10 @@ namespace Xamarin.Forms
static Color _ColorButtonNormal = Color.Default;
public static Color ColorButtonNormalOverride { get; set; }
- internal static BuildVersionCodes SdkInt {
- get {
+ internal static BuildVersionCodes SdkInt
+ {
+ get
+ {
if (!s_sdkInt.HasValue)
s_sdkInt = Build.VERSION.SdkInt;
return (BuildVersionCodes)s_sdkInt;
@@ -90,13 +92,23 @@ namespace Xamarin.Forms
return s_is29OrNewer.Value;
}
}
+
+ internal static bool IsJellyBeanMr1OrNewer
+ {
+ get
+ {
+ if (!s_isJellyBeanMr1OrNewer.HasValue)
+ s_isJellyBeanMr1OrNewer = SdkInt >= BuildVersionCodes.JellyBeanMr1;
+ return s_isJellyBeanMr1OrNewer.Value;
+ }
+ }
internal static bool IsLollipopOrNewer
{
get
{
if (!s_isLollipopOrNewer.HasValue)
- s_isLollipopOrNewer = (int)SdkInt >= 21;
+ s_isLollipopOrNewer = SdkInt >= BuildVersionCodes.Lollipop;
return s_isLollipopOrNewer.Value;
}
}
@@ -106,7 +118,7 @@ namespace Xamarin.Forms
get
{
if (!s_isMarshmallowOrNewer.HasValue)
- s_isMarshmallowOrNewer = (int)SdkInt >= 23;
+ s_isMarshmallowOrNewer = SdkInt >= BuildVersionCodes.M;
return s_isMarshmallowOrNewer.Value;
}
}
@@ -116,11 +128,21 @@ namespace Xamarin.Forms
get
{
if (!s_isNougatOrNewer.HasValue)
- s_isNougatOrNewer = (int)Build.VERSION.SdkInt >= 24;
+ s_isNougatOrNewer = SdkInt >= BuildVersionCodes.N;
return s_isNougatOrNewer.Value;
}
}
+ internal static bool IsOreoOrNewer
+ {
+ get
+ {
+ if (!s_isOreoOrNewer.HasValue)
+ s_isOreoOrNewer = SdkInt >= BuildVersionCodes.O;
+ return s_isOreoOrNewer.Value;
+ }
+ }
+
public static float GetFontSizeNormal(Context context)
{
float size = 50;
diff --git a/Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs b/Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs
index 631ff94a5..c170106b4 100644
--- a/Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs
+++ b/Xamarin.Forms.Platform.Android/FormsApplicationActivity.cs
@@ -185,6 +185,7 @@ namespace Xamarin.Forms.Platform.Android
PowerManager.ActionPowerSaveModeChanged));
_powerSaveReceiverRegistered = true;
+ _powerSaveModeBroadcastReceiver.CheckAnimationEnabledStatus();
}
OnStateChanged();
diff --git a/Xamarin.Forms.Platform.Android/PowerSaveModeBroadcastReceiver.cs b/Xamarin.Forms.Platform.Android/PowerSaveModeBroadcastReceiver.cs
index bfec1bbe4..3a927734d 100644
--- a/Xamarin.Forms.Platform.Android/PowerSaveModeBroadcastReceiver.cs
+++ b/Xamarin.Forms.Platform.Android/PowerSaveModeBroadcastReceiver.cs
@@ -8,7 +8,12 @@ namespace Xamarin.Forms.Platform.Android
{
public override void OnReceive(Context context, Intent intent)
{
- ((AndroidTicker)Ticker.Default).CheckPowerSaveModeStatus();
+ CheckAnimationEnabledStatus();
+ }
+
+ public void CheckAnimationEnabledStatus()
+ {
+ ((AndroidTicker)Ticker.Default).CheckAnimationEnabledStatus();
}
}
}
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.Android/Renderers/SwipeViewRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/SwipeViewRenderer.cs
index 1ca6007db..87dc391db 100644
--- a/Xamarin.Forms.Platform.Android/Renderers/SwipeViewRenderer.cs
+++ b/Xamarin.Forms.Platform.Android/Renderers/SwipeViewRenderer.cs
@@ -31,7 +31,7 @@ namespace Xamarin.Forms.Platform.Android
const int SwipeThresholdMargin = 0;
const int SwipeItemWidth = 100;
const long SwipeAnimationDuration = 200;
- const double SwipeMinimumDelta = 10;
+ const float SwipeMinimumDelta = 10f;
readonly Context _context;
GestureDetector _detector;
@@ -220,6 +220,12 @@ namespace Xamarin.Forms.Platform.Android
_actionView.Dispose();
_actionView = null;
}
+
+ if (_initialPoint != null)
+ {
+ _initialPoint.Dispose();
+ _initialPoint = null;
+ }
}
_isDisposed = true;
@@ -231,23 +237,42 @@ namespace Xamarin.Forms.Platform.Android
{
base.OnTouchEvent(e);
- var density = Resources.DisplayMetrics.Density;
- float x = Math.Abs((_downX - e.GetX()) / density);
- float y = Math.Abs((_downY - e.GetY()) / density);
+ float x = Math.Abs((_downX - e.GetX()) / _density);
+ float y = Math.Abs((_downY - e.GetY()) / _density);
if (e.Action != MotionEventActions.Move | (x > SwipeMinimumDelta || y > SwipeMinimumDelta))
{
_detector.OnTouchEvent(e);
}
-
+
ProcessSwipingInteractions(e);
return true;
}
- public override bool OnInterceptTouchEvent(MotionEvent ev)
+ public override bool OnInterceptTouchEvent(MotionEvent e)
{
- return true;
+ switch (e.Action)
+ {
+ case MotionEventActions.Down:
+ _downX = e.RawX;
+ _downY = e.RawY;
+ _initialPoint = new APointF(e.GetX() / _density, e.GetY() / _density);
+ break;
+ case MotionEventActions.Move:
+ float x = Math.Abs((_downX - e.GetX()) / _density);
+ float y = Math.Abs((_downY - e.GetY()) / _density);
+
+ if (x > SwipeMinimumDelta || y > SwipeMinimumDelta)
+ ProcessSwipingInteractions(e);
+ break;
+ case MotionEventActions.Cancel:
+ case MotionEventActions.Up:
+ ProcessSwipingInteractions(e);
+ break;
+ }
+
+ return false;
}
public bool OnDown(MotionEvent e)
@@ -457,7 +482,7 @@ namespace Xamarin.Forms.Platform.Android
{
_isTouchDown = false;
- if(!TouchInsideContent(point))
+ if (!TouchInsideContent(point))
ProcessTouchSwipeItems(point);
if (!_isSwiping)
@@ -466,7 +491,7 @@ namespace Xamarin.Forms.Platform.Android
_isSwiping = false;
RaiseSwipeEnded();
-
+
if (!ValidateSwipeDirection())
return false;
@@ -559,7 +584,7 @@ namespace Xamarin.Forms.Platform.Android
_actionView.LayoutParameters = layoutParams;
_actionView.Orientation = LinearLayoutCompat.Horizontal;
-
+
var swipeItems = new List();
foreach (var item in items)
@@ -617,7 +642,7 @@ namespace Xamarin.Forms.Platform.Android
child.Layout(previousWidth, 0, (i + 1) * swipeItemWidth, swipeItemHeight);
break;
}
-
+
i++;
previousWidth += swipeItemWidth;
}
@@ -687,7 +712,7 @@ namespace Xamarin.Forms.Platform.Android
return luminosity < 0.75 ? Color.White : Color.Black;
}
-
+
void DisposeSwipeItems()
{
if (_actionView != null)
diff --git a/Xamarin.Forms.Platform.UAP/Directory.Build.targets b/Xamarin.Forms.Platform.UAP/Directory.Build.targets
index 4e7a3d2c6..48c14d66e 100644
--- a/Xamarin.Forms.Platform.UAP/Directory.Build.targets
+++ b/Xamarin.Forms.Platform.UAP/Directory.Build.targets
@@ -1,4 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Platform.UAP/Xamarin.Forms.Platform.UAP.csproj b/Xamarin.Forms.Platform.UAP/Xamarin.Forms.Platform.UAP.csproj
index 65e381b02..f156ac08f 100644
--- a/Xamarin.Forms.Platform.UAP/Xamarin.Forms.Platform.UAP.csproj
+++ b/Xamarin.Forms.Platform.UAP/Xamarin.Forms.Platform.UAP.csproj
@@ -1,7 +1,4 @@
-
- $(DefineConstants);UWP_14393
-
Xamarin.Forms.Platform.UAP
Xamarin.Forms.Platform.UAP
diff --git a/Xamarin.Forms.Platform.iOS/Renderers/SwipeViewRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/SwipeViewRenderer.cs
index bfde94e99..08eb2af21 100644
--- a/Xamarin.Forms.Platform.iOS/Renderers/SwipeViewRenderer.cs
+++ b/Xamarin.Forms.Platform.iOS/Renderers/SwipeViewRenderer.cs
@@ -206,6 +206,41 @@ namespace Xamarin.Forms.Platform.iOS
base.Dispose(disposing);
}
+ public override UIView HitTest(CGPoint point, UIEvent uievent)
+ {
+ if (!UserInteractionEnabled || Hidden)
+ return null;
+
+ foreach (var subview in Subviews)
+ {
+ var view = HitTest(subview, point, uievent);
+
+ if (view != null)
+ return view;
+ }
+
+ return base.HitTest(point, uievent);
+ }
+
+ UIView HitTest(UIView view, CGPoint point, UIEvent uievent)
+ {
+ if (view.Subviews == null)
+ return null;
+
+ foreach (var subview in view.Subviews)
+ {
+ CGPoint subPoint = subview.ConvertPointFromView(point, this);
+ UIView result = subview.HitTest(subPoint, uievent);
+
+ if (result != null)
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
var navigationController = GetUINavigationController(GetViewController());
diff --git a/Xamarin.Forms.sln b/Xamarin.Forms.sln
index fdd776d63..03c508464 100644
--- a/Xamarin.Forms.sln
+++ b/Xamarin.Forms.sln
@@ -15,7 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Xamarin.Forms.Xaml", "Xamar
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Platform.iOS", "Xamarin.Forms.Platform.iOS\Xamarin.Forms.Platform.iOS.csproj", "{271193C1-6E7C-429C-A36D-3F1BE5267231}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Platform.Android", "Xamarin.Forms.Platform.Android\Xamarin.Forms.Platform.Android.csproj", "{0E16E70A-D6DD-4323-AD5D-363ABFF42D6A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Platform.Android", "Xamarin.Forms.Platform.Android\Xamarin.Forms.Platform.Android.csproj", "{0E16E70A-D6DD-4323-AD5D-363ABFF42D6A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Core.UnitTests", "Xamarin.Forms.Core.UnitTests\Xamarin.Forms.Core.UnitTests.csproj", "{00259593-A283-47A5-ACB7-9C3819B16364}"
EndProject
@@ -30,6 +30,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuspec", ".nuspec", "{7E12
.nuspec\Xamarin.Forms.Debug.targets = .nuspec\Xamarin.Forms.Debug.targets
.nuspec\Xamarin.Forms.DefaultItems.props = .nuspec\Xamarin.Forms.DefaultItems.props
.nuspec\Xamarin.Forms.DefaultItems.targets = .nuspec\Xamarin.Forms.DefaultItems.targets
+ .nuspec\Xamarin.Forms.DualScreen.nuspec = .nuspec\Xamarin.Forms.DualScreen.nuspec
.nuspec\Xamarin.Forms.Maps.GTK.nuspec = .nuspec\Xamarin.Forms.Maps.GTK.nuspec
.nuspec\Xamarin.Forms.Maps.nuspec = .nuspec\Xamarin.Forms.Maps.nuspec
.nuspec\Xamarin.Forms.Maps.WPF.nuspec = .nuspec\Xamarin.Forms.Maps.WPF.nuspec
@@ -61,7 +62,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Maps", "Xamar
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Maps.iOS", "Xamarin.Forms.Maps.iOS\Xamarin.Forms.Maps.iOS.csproj", "{ABA078C4-F9BB-4924-8B2B-10FE0D2F5491}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Maps.Android", "Xamarin.Forms.Maps.Android\Xamarin.Forms.Maps.Android.csproj", "{BD50B39A-EBC5-408F-9C5E-923A8EBAE473}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Maps.Android", "Xamarin.Forms.Maps.Android\Xamarin.Forms.Maps.Android.csproj", "{BD50B39A-EBC5-408F-9C5E-923A8EBAE473}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UITests", "UITests", "{D4D57221-71D6-4031-A6F4-EC66AF0929D9}"
EndProject
@@ -79,7 +80,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.ControlGaller
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Forwarders", "Forwarders", "{5A2DADBC-9510-4DD1-BE58-01501F2DF65D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Platform.Android (Forwarders)", "Stubs\Xamarin.Forms.Platform.Android\Xamarin.Forms.Platform.Android (Forwarders).csproj", "{6E53FEB1-1100-46AE-8013-17BBA35CC197}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Platform.Android (Forwarders)", "Stubs\Xamarin.Forms.Platform.Android\Xamarin.Forms.Platform.Android (Forwarders).csproj", "{6E53FEB1-1100-46AE-8013-17BBA35CC197}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Platform.iOS (Forwarders)", "Stubs\Xamarin.Forms.Platform.iOS\Xamarin.Forms.Platform.iOS (Forwarders).csproj", "{39B3457F-01D8-43D0-8E84-D8C4F73CF48D}"
EndProject
@@ -89,9 +90,9 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Xamarin.Forms.Controls.Issu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.ControlGallery.WindowsUniversal", "Xamarin.Forms.ControlGallery.WindowsUniversal\Xamarin.Forms.ControlGallery.WindowsUniversal.csproj", "{AC257966-9368-478A-9DF4-F0D28E320FE3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Platform.UAP", "Xamarin.Forms.Platform.UAP\Xamarin.Forms.Platform.UAP.csproj", "{00D8D049-FFAA-4759-8FC9-1ECA30777F72}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Platform.UAP", "Xamarin.Forms.Platform.UAP\Xamarin.Forms.Platform.UAP.csproj", "{00D8D049-FFAA-4759-8FC9-1ECA30777F72}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Maps.UWP", "Xamarin.Forms.Maps.UWP\Xamarin.Forms.Maps.UWP.csproj", "{04D89A60-78EF-4A32-AE17-87E47E0233A5}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Maps.UWP", "Xamarin.Forms.Maps.UWP\Xamarin.Forms.Maps.UWP.csproj", "{04D89A60-78EF-4A32-AE17-87E47E0233A5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Core.Windows.UITests", "Xamarin.Forms.Core.Windows.UITests\Xamarin.Forms.Core.Windows.UITests.csproj", "{0A39A74B-6F7A-4D41-84F2-B0CCDCE899DF}"
EndProject
@@ -110,7 +111,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PagesGallery.Droid", "Pages
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PagesGallery.UWP", "PagesGallery\PagesGallery.UWP\PagesGallery.UWP.csproj", "{95FEB8D4-D57E-4B96-A8D8-59D241C0501B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Platform.Android.AppLinks", "Xamarin.Forms.Platform.Android.AppLinks\Xamarin.Forms.Platform.Android.AppLinks.csproj", "{42DB052E-0909-45D2-8240-187F99F393FB}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Platform.Android.AppLinks", "Xamarin.Forms.Platform.Android.AppLinks\Xamarin.Forms.Platform.Android.AppLinks.csproj", "{42DB052E-0909-45D2-8240-187F99F393FB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Pages.Azure", "Xamarin.Forms.Pages.Azure\Xamarin.Forms.Pages.Azure.csproj", "{C9696465-7657-4843-872E-3C01891C4A9B}"
EndProject
@@ -182,10 +183,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Xamarin.Forms.Material", "X
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Material.iOS", "Xamarin.Forms.Material.iOS\Xamarin.Forms.Material.iOS.csproj", "{8A75B1DC-CEED-4B1B-8675-A7DFFD1E6DE4}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.Material.Android", "Xamarin.Forms.Material.Android\Xamarin.Forms.Material.Android.csproj", "{E1586CE6-8EAC-4388-A15A-1AABF108B5F8}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Material.Android", "Xamarin.Forms.Material.Android\Xamarin.Forms.Material.Android.csproj", "{E1586CE6-8EAC-4388-A15A-1AABF108B5F8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.Material.Tizen", "Xamarin.Forms.Material.Tizen\Xamarin.Forms.Material.Tizen.csproj", "{84B8819E-9123-43CE-A788-8CB05ABA32DA}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Xamarin.Forms.DualScreen", "Xamarin.Forms.DualScreen", "{C97E54D4-DBC5-424E-B63F-4CF2F3EE8D13}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Forms.DualScreen", "Xamarin.Forms.DualScreen\Xamarin.Forms.DualScreen.csproj", "{FB4A866A-5721-4545-9E5D-B7F7D59875A4}"
+ ProjectSection(ProjectDependencies) = postProject
+ {0E16E70A-D6DD-4323-AD5D-363ABFF42D6A} = {0E16E70A-D6DD-4323-AD5D-363ABFF42D6A}
+ {00D8D049-FFAA-4759-8FC9-1ECA30777F72} = {00D8D049-FFAA-4759-8FC9-1ECA30777F72}
+ {3B72465B-ACAE-43AE-9327-10F372FE5F80} = {3B72465B-ACAE-43AE-9327-10F372FE5F80}
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.Forms.DualScreen.UnitTests", "Xamarin.Forms.DualScreen.UnitTests\Xamarin.Forms.DualScreen.UnitTests.csproj", "{00D50743-7821-4293-92F2-7C614C256BD6}"
+EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
Xamarin.Forms.Controls.Issues\Xamarin.Forms.Controls.Issues.Shared\Xamarin.Forms.Controls.Issues.Shared.projitems*{0a39a74b-6f7a-4d41-84f2-b0ccdce899df}*SharedItemsImports = 4
@@ -1573,6 +1585,54 @@ Global
{84B8819E-9123-43CE-A788-8CB05ABA32DA}.Release|x64.Build.0 = Release|Any CPU
{84B8819E-9123-43CE-A788-8CB05ABA32DA}.Release|x86.ActiveCfg = Release|Any CPU
{84B8819E-9123-43CE-A788-8CB05ABA32DA}.Release|x86.Build.0 = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|ARM.Build.0 = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|x64.Build.0 = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Debug|x86.Build.0 = Debug|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|ARM.ActiveCfg = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|ARM.Build.0 = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|iPhone.Build.0 = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|x64.ActiveCfg = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|x64.Build.0 = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|x86.ActiveCfg = Release|Any CPU
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4}.Release|x86.Build.0 = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|ARM.Build.0 = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|x64.Build.0 = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Debug|x86.Build.0 = Debug|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|ARM.ActiveCfg = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|ARM.Build.0 = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|iPhone.Build.0 = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|x64.ActiveCfg = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|x64.Build.0 = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|x86.ActiveCfg = Release|Any CPU
+ {00D50743-7821-4293-92F2-7C614C256BD6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1640,6 +1700,8 @@ Global
{8A75B1DC-CEED-4B1B-8675-A7DFFD1E6DE4} = {F6F6A11F-CA66-4C7E-AB84-CD21D817C2CD}
{E1586CE6-8EAC-4388-A15A-1AABF108B5F8} = {F6F6A11F-CA66-4C7E-AB84-CD21D817C2CD}
{84B8819E-9123-43CE-A788-8CB05ABA32DA} = {F6F6A11F-CA66-4C7E-AB84-CD21D817C2CD}
+ {FB4A866A-5721-4545-9E5D-B7F7D59875A4} = {C97E54D4-DBC5-424E-B63F-4CF2F3EE8D13}
+ {00D50743-7821-4293-92F2-7C614C256BD6} = {C97E54D4-DBC5-424E-B63F-4CF2F3EE8D13}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {650AE971-2F29-46A8-822C-FB4FCDC6A9A0}
diff --git a/build/steps/build-windows.yml b/build/steps/build-windows.yml
index 1bbe90e00..056f6cea1 100644
--- a/build/steps/build-windows.yml
+++ b/build/steps/build-windows.yml
@@ -91,6 +91,7 @@ jobs:
inputs:
testAssemblyVer2: |
**/bin/$(BuildConfiguration)/Xamarin.Forms.Core.UnitTests.dll
+ **/bin/$(BuildConfiguration)/**/Xamarin.Forms.DualScreen.UnitTests.dll
**/bin/$(BuildConfiguration)/Xamarin.Forms.Pages.UnitTests.dll
**/bin/$(BuildConfiguration)/**/Xamarin.Forms.Xaml.UnitTests.dll
searchFolder: ${{ parameters.nunitTestFolder }}
@@ -169,6 +170,10 @@ jobs:
Microsoft.XamlStandard/bin/$(BuildConfiguration)/**/*.dll
Microsoft.XamlStandard/bin/$(BuildConfiguration)/**/*.mdb
Microsoft.XamlStandard/bin/$(BuildConfiguration)/**/*.pdb
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.dll
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.pdb
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.mdb
+ Xamarin.Forms.DualScreen.UnitTests/bin/$(BuildConfiguration)/**/*.dll
**/*.binlog
TargetFolder: ${{ parameters.artifactsTargetFolder }}
@@ -188,6 +193,10 @@ jobs:
Xamarin.Forms.Maps.UWP/bin/$(BuildConfiguration)/**/*.pdb
Xamarin.Forms.Maps.UWP/bin/$(BuildConfiguration)/**/*.pri
Xamarin.Forms.Maps.UWP/bin/$(BuildConfiguration)/**/*.xbf
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.dll
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.pdb
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.mdb
+ Xamarin.Forms.DualScreen.UnitTests/bin/$(BuildConfiguration)/**/*.dll
TargetFolder: ${{ parameters.artifactsTargetFolder }}
@@ -209,6 +218,10 @@ jobs:
Xamarin.Forms.Platform.Android.AppLinks/bin/$(BuildConfiguration)/**/*.dll
Xamarin.Forms.Platform.Android.AppLinks/bin/$(BuildConfiguration)/**/*.pdb
Xamarin.Forms.Platform.Android.AppLinks/bin/$(BuildConfiguration)/**/*.mdb
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.dll
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.pdb
+ Xamarin.Forms.DualScreen/bin/$(BuildConfiguration)/**/*.mdb
+ Xamarin.Forms.DualScreen.UnitTests/bin/$(BuildConfiguration)/**/*.dll
**/*.binlog
TargetFolder: ${{ parameters.artifactsTargetFolder }}