Merge branch 'main' into merge-main-net9
# Conflicts: # eng/Version.Details.xml # src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs
|
@ -21,7 +21,7 @@
|
|||
]
|
||||
},
|
||||
"microsoft.dotnet.xharness.cli": {
|
||||
"version": "10.0.0-prerelease.24466.1",
|
||||
"version": "10.0.0-prerelease.24467.4",
|
||||
"commands": [
|
||||
"xharness"
|
||||
]
|
||||
|
|
|
@ -135,17 +135,17 @@
|
|||
<Uri>https://github.com/dotnet/runtime</Uri>
|
||||
<Sha>46cfb747b4c22471242dee0d106f5c79cf9fd4c5</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="Microsoft.DotNet.XHarness.TestRunners.Common" Version="10.0.0-prerelease.24466.1">
|
||||
<Dependency Name="Microsoft.DotNet.XHarness.TestRunners.Common" Version="10.0.0-prerelease.24467.4">
|
||||
<Uri>https://github.com/dotnet/xharness</Uri>
|
||||
<Sha>f20e52f7731da99588dd6b4f4bd60119f03220a3</Sha>
|
||||
<Sha>3cfb1a3d86da666fb80ba0adb970525e88339d57</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="Microsoft.DotNet.XHarness.TestRunners.Xunit" Version="10.0.0-prerelease.24466.1">
|
||||
<Dependency Name="Microsoft.DotNet.XHarness.TestRunners.Xunit" Version="10.0.0-prerelease.24467.4">
|
||||
<Uri>https://github.com/dotnet/xharness</Uri>
|
||||
<Sha>f20e52f7731da99588dd6b4f4bd60119f03220a3</Sha>
|
||||
<Sha>3cfb1a3d86da666fb80ba0adb970525e88339d57</Sha>
|
||||
</Dependency>
|
||||
<Dependency Name="Microsoft.DotNet.XHarness.CLI" Version="10.0.0-prerelease.24466.1">
|
||||
<Dependency Name="Microsoft.DotNet.XHarness.CLI" Version="10.0.0-prerelease.24467.4">
|
||||
<Uri>https://github.com/dotnet/xharness</Uri>
|
||||
<Sha>f20e52f7731da99588dd6b4f4bd60119f03220a3</Sha>
|
||||
<Sha>3cfb1a3d86da666fb80ba0adb970525e88339d57</Sha>
|
||||
</Dependency>
|
||||
</ProductDependencies>
|
||||
<ToolsetDependencies>
|
||||
|
|
|
@ -124,9 +124,9 @@
|
|||
<_HarfBuzzSharpVersion>7.3.0.2</_HarfBuzzSharpVersion>
|
||||
<_SkiaSharpNativeAssetsVersion>0.0.0-commit.7af1d0840a381c0ce7ef2877454a88dbb2949686.1086</_SkiaSharpNativeAssetsVersion>
|
||||
<MicrosoftTemplateEngineTasksVersion>7.0.114</MicrosoftTemplateEngineTasksVersion>
|
||||
<MicrosoftDotNetXHarnessTestRunnersCommonVersion>10.0.0-prerelease.24466.1</MicrosoftDotNetXHarnessTestRunnersCommonVersion>
|
||||
<MicrosoftDotNetXHarnessTestRunnersXunitVersion>10.0.0-prerelease.24466.1</MicrosoftDotNetXHarnessTestRunnersXunitVersion>
|
||||
<MicrosoftDotNetXHarnessCLIVersion>10.0.0-prerelease.24466.1</MicrosoftDotNetXHarnessCLIVersion>
|
||||
<MicrosoftDotNetXHarnessTestRunnersCommonVersion>10.0.0-prerelease.24467.4</MicrosoftDotNetXHarnessTestRunnersCommonVersion>
|
||||
<MicrosoftDotNetXHarnessTestRunnersXunitVersion>10.0.0-prerelease.24467.4</MicrosoftDotNetXHarnessTestRunnersXunitVersion>
|
||||
<MicrosoftDotNetXHarnessCLIVersion>10.0.0-prerelease.24467.4</MicrosoftDotNetXHarnessCLIVersion>
|
||||
<TizenUIExtensionsVersion>0.9.2</TizenUIExtensionsVersion>
|
||||
<SvgSkiaPackageVersion>1.0.0.16</SvgSkiaPackageVersion>
|
||||
<FizzlerPackageVersion>1.3.0</FizzlerPackageVersion>
|
||||
|
|
|
@ -382,7 +382,17 @@ void PerformCleanupIfNeeded(bool cleanupEnabled)
|
|||
void SetAndroidEnvironmentVariables(string sdkRoot)
|
||||
{
|
||||
// Set up Android SDK environment variables and paths
|
||||
string[] paths = { $"{sdkRoot}/tools/bin", $"{sdkRoot}/cmdline-tools/latest/bin", $"{sdkRoot}/cmdline-tools/5.0/bin", $"{sdkRoot}/cmdline-tools/7.0/bin", $"{sdkRoot}/platform-tools", $"{sdkRoot}/emulator" };
|
||||
string[] paths = {
|
||||
$"{sdkRoot}/tools/bin",
|
||||
$"{sdkRoot}/cmdline-tools/latest/bin",
|
||||
$"{sdkRoot}/cmdline-tools/5.0/bin",
|
||||
$"{sdkRoot}/cmdline-tools/7.0/bin",
|
||||
$"{sdkRoot}/cmdline-tools/11.0/bin",
|
||||
$"{sdkRoot}/cmdline-tools/12.0/bin",
|
||||
$"{sdkRoot}/cmdline-tools/13.0/bin",
|
||||
$"{sdkRoot}/platform-tools",
|
||||
$"{sdkRoot}/emulator" };
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
SetEnvironmentVariable("PATH", path, prepend: true);
|
||||
|
|
|
@ -144,9 +144,10 @@ namespace Microsoft.Maui.Controls
|
|||
}
|
||||
}
|
||||
|
||||
// if we are in a scenario with unlimited width and the image is on top or bottom, let's make sure the title is not cut off by ensuring we have enough padding for the image and title
|
||||
if (padding == ButtonHandler.DefaultPadding
|
||||
&& image is not null
|
||||
// if we are in a scenario with unlimited width and the image is on top or bottom,
|
||||
// of if the horizontalOption is not fill and the image is on top or bottom,
|
||||
// let's make sure the title is not cut off by ensuring we have enough padding for the image and title.
|
||||
if (image is not null
|
||||
&& (widthConstraint == double.PositiveInfinity || button.HorizontalOptions != LayoutOptions.Fill)
|
||||
&& (layout.Position == ButtonContentLayout.ImagePosition.Top || layout.Position == ButtonContentLayout.ImagePosition.Bottom))
|
||||
{
|
||||
|
|
Двоичные данные
src/Controls/tests/TestCases.Android.Tests/snapshots/android/ButtonPaddingIsAddedWhenNeeded.png
Normal file
После Ширина: | Высота: | Размер: 150 KiB |
Двоичные данные
src/Controls/tests/TestCases.Android.Tests/snapshots/android/SelectedTabIconShouldChangeColor.png
Normal file
После Ширина: | Высота: | Размер: 17 KiB |
|
@ -0,0 +1,125 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Maui.Controls.Sample.Issues.Issue19214"
|
||||
Title="Issue19214">
|
||||
<Grid RowDefinitions="40,*,*,*" Margin="20">
|
||||
<ScrollView Background="LightBlue" Padding="10" Grid.Row="1" AutomationId="ScrollView_1">
|
||||
<VerticalStackLayout>
|
||||
<Entry Text="Top Scroll" Background="Lightgray" HeightRequest="40" AutomationId="TopEntry_1"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry1" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry2" Background="Lightgray" HeightRequest="40" AutomationId="Entry2_1"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry3" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry4" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry5" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry6" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry7" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry8" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry9" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry10" Background="Lightgray" HeightRequest="40" AutomationId="Entry10_1"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Bottom!!" Background="Green" HeightRequest="40" AutomationId="BottomEntry_1"/>
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
<ScrollView Background="Lightgreen" Padding="10" Grid.Row="2" AutomationId="ScrollView_2">
|
||||
<VerticalStackLayout>
|
||||
<Entry Text="Top Scroll" Background="Lightgray" HeightRequest="40" AutomationId="TopEntry_2"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry1" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry2" Background="Lightgray" HeightRequest="40" AutomationId="Entry2_2"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry3" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry4" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry5" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry6" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry7" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry8" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry9" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry10" Background="Lightgray" HeightRequest="40" AutomationId="Entry10_2"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Bottom!!" Background="Green" HeightRequest="40" AutomationId="BottomEntry_2"/>
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
<ScrollView Background="pink" Padding="10" Grid.Row="3" AutomationId="ScrollView_3">
|
||||
<VerticalStackLayout>
|
||||
<Entry Text="Top Scroll" Background="Lightgray" HeightRequest="40" AutomationId="TopEntry_3"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry1" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry2" Background="Lightgray" HeightRequest="40" AutomationId="Entry2_3"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry3" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry4" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry5" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry6" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry7" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry8" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry9" Background="Lightgray" HeightRequest="40"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Entry10" Background="Lightgray" HeightRequest="40" AutomationId="Entry10_3"/>
|
||||
<BoxView HeightRequest="40" />
|
||||
|
||||
<Entry Text="Bottom!!" Background="Green" HeightRequest="40" AutomationId="BottomEntry_3"/>
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</Grid>
|
||||
</ContentPage>
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
|
||||
namespace Maui.Controls.Sample.Issues;
|
||||
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.Github, 19214, "iOS Keyboard Scrolling ContentInset Tests", PlatformAffected.iOS)]
|
||||
public partial class Issue19214 : ContentPage
|
||||
{
|
||||
public Issue19214()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Maui.Controls.Sample.Issues.Issue19214_2"
|
||||
Title="Issue19214_2">
|
||||
<Grid RowDefinitions="50, 50, *, 50" Margin="30">
|
||||
<Entry Text="Content before" AutomationId="EntryBefore" FontSize="Large" ReturnType="Next" BackgroundColor="Aquamarine" Grid.Row="0" />
|
||||
<Label x:Name="CursorHeightTracker" Text="0" AutomationId="CursorHeightTracker" FontSize="Large" BackgroundColor="Aquamarine" Grid.Row="1" />
|
||||
<Editor x:Name="editor" AutomationId="IssueEditor" FontSize="Large" BackgroundColor="Orange" Grid.Row="2" TextChanged="Editor_TextChanged" />
|
||||
<Button Text="Erase" Clicked="Button_Clicked" Grid.Row="3"/>
|
||||
</Grid>
|
||||
</ContentPage>
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
|
||||
namespace Maui.Controls.Sample.Issues;
|
||||
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.Github, 19214_2, "iOS Editor Cursor stays above keyboard - Top level Grid", PlatformAffected.iOS)]
|
||||
public partial class Issue19214_2 : ContentPage
|
||||
{
|
||||
public Issue19214_2()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Button_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
editor.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void Editor_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (sender is Editor editor)
|
||||
{
|
||||
AddCursorHeightToLabel(editor);
|
||||
}
|
||||
}
|
||||
|
||||
void AddCursorHeightToLabel (Editor editor)
|
||||
{
|
||||
#if IOS
|
||||
var textInput = editor.Handler.PlatformView as UIKit.UITextView;
|
||||
var selectedTextRange = textInput?.SelectedTextRange;
|
||||
var localCursor = selectedTextRange is not null ? textInput?.GetCaretRectForPosition(selectedTextRange.Start) : null;
|
||||
|
||||
if (localCursor is CoreGraphics.CGRect local && textInput is not null)
|
||||
{
|
||||
var container = GetContainerView(textInput);
|
||||
var cursorInContainer = container.ConvertRectFromView(local, textInput);
|
||||
var cursorInWindow = container.ConvertRectToView(cursorInContainer, null);
|
||||
|
||||
CursorHeightTracker.Text = cursorInWindow.Y.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UIKit.UIView GetContainerView(UIKit.UIView startingPoint)
|
||||
{
|
||||
var rootView = FindResponder<Microsoft.Maui.Platform.ContainerViewController>(startingPoint)?.View;
|
||||
|
||||
if (rootView is not null)
|
||||
{
|
||||
return rootView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
T FindResponder<T>(UIKit.UIView view) where T : UIKit.UIResponder
|
||||
{
|
||||
var nextResponder = view as UIKit.UIResponder;
|
||||
while (nextResponder is not null)
|
||||
{
|
||||
nextResponder = nextResponder.NextResponder;
|
||||
|
||||
if (nextResponder is T responder)
|
||||
return responder;
|
||||
}
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Maui.Controls.Sample.Issues.Issue19214_3"
|
||||
Title="Issue19214_3">
|
||||
<ScrollView Margin="30">
|
||||
<VerticalStackLayout>
|
||||
<Entry Text="Content before" AutomationId="EntryBefore" FontSize="Large" ReturnType="Next" BackgroundColor="Aquamarine"/>
|
||||
<Label x:Name="CursorHeightTracker" Text="0" AutomationId="CursorHeightTracker" FontSize="Large" BackgroundColor="Aquamarine" />
|
||||
<Editor x:Name="editor" HeightRequest="650" AutomationId="IssueEditor" FontSize="Large" BackgroundColor="Orange" TextChanged="Editor_TextChanged" />
|
||||
<Button Text="Erase" Clicked="Button_Clicked" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
|
||||
namespace Maui.Controls.Sample.Issues;
|
||||
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.Github, 19214_3, "iOS Editor Cursor stays above keyboard - Top level ScrollView", PlatformAffected.iOS)]
|
||||
public partial class Issue19214_3 : ContentPage
|
||||
{
|
||||
public Issue19214_3()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Button_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
editor.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void Editor_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (sender is Editor editor)
|
||||
{
|
||||
AddCursorHeightToLabel(editor);
|
||||
}
|
||||
}
|
||||
|
||||
void AddCursorHeightToLabel (Editor editor)
|
||||
{
|
||||
#if IOS
|
||||
var textInput = editor.Handler.PlatformView as UIKit.UITextView;
|
||||
var selectedTextRange = textInput?.SelectedTextRange;
|
||||
var localCursor = selectedTextRange is not null ? textInput?.GetCaretRectForPosition(selectedTextRange.Start) : null;
|
||||
|
||||
if (localCursor is CoreGraphics.CGRect local && textInput is not null)
|
||||
{
|
||||
var container = GetContainerView(textInput);
|
||||
var cursorInContainer = container.ConvertRectFromView(local, textInput);
|
||||
var cursorInWindow = container.ConvertRectToView(cursorInContainer, null);
|
||||
|
||||
CursorHeightTracker.Text = cursorInWindow.Y.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UIKit.UIView GetContainerView(UIKit.UIView startingPoint)
|
||||
{
|
||||
var rootView = FindResponder<Microsoft.Maui.Platform.ContainerViewController>(startingPoint)?.View;
|
||||
|
||||
if (rootView is not null)
|
||||
{
|
||||
return rootView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
T FindResponder<T>(UIKit.UIView view) where T : UIKit.UIResponder
|
||||
{
|
||||
var nextResponder = view as UIKit.UIResponder;
|
||||
while (nextResponder is not null)
|
||||
{
|
||||
nextResponder = nextResponder.NextResponder;
|
||||
|
||||
if (nextResponder is T responder)
|
||||
return responder;
|
||||
}
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<TabbedPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Maui.Controls.Sample.Issues.Issue22032"
|
||||
SelectedTabColor="Red"
|
||||
UnselectedTabColor="Green">
|
||||
<ContentPage Title="Tab1">
|
||||
<ContentPage.IconImageSource>
|
||||
<FontImageSource
|
||||
FontFamily="OpenSansRegular"
|
||||
Glyph="₪"
|
||||
Size="15" />
|
||||
</ContentPage.IconImageSource>
|
||||
<Button Text="Switch page"
|
||||
Clicked="Button_Clicked"
|
||||
AutomationId="button"/>
|
||||
</ContentPage>
|
||||
<ContentPage x:Name="tab2" Title="Tab2">
|
||||
<ContentPage.IconImageSource>
|
||||
<FontImageSource
|
||||
FontFamily="OpenSansRegular"
|
||||
Glyph="¼"
|
||||
Size="15" />
|
||||
</ContentPage.IconImageSource>
|
||||
<Label
|
||||
Text="Hello, Maui!"
|
||||
TextColor="White"
|
||||
AutomationId="label"/>
|
||||
</ContentPage>
|
||||
</TabbedPage>
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
|
||||
namespace Maui.Controls.Sample.Issues
|
||||
{
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.Github, 22032, "Shell FlyoutItem Tab Selected Icon Color not changing if using Font icons", PlatformAffected.iOS)]
|
||||
public partial class Issue22032 : TabbedPage
|
||||
{
|
||||
public Issue22032()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
void Button_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
CurrentPage = tab2;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Maui.Controls.Sample.Issues.Issue22715"
|
||||
Title="Issue22715"
|
||||
Loaded="OnPageLoaded"
|
||||
AutomationId="contentPage">
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout>
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto, Auto, Auto"
|
||||
ColumnDefinitions="300">
|
||||
|
||||
<Border Grid.Row="0" StrokeThickness="2" Stroke="Black" >
|
||||
<Label
|
||||
AutomationId="TopLabel"
|
||||
BackgroundColor="LightGray"
|
||||
HeightRequest="50"
|
||||
TextColor="Black"
|
||||
FontSize="20"
|
||||
Text="Enter a number" />
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" StrokeThickness="2" Stroke="Black" >
|
||||
<Entry
|
||||
x:Name="EntNumber"
|
||||
AutomationId="EntNumber"
|
||||
BackgroundColor="LightBlue"
|
||||
Keyboard="Numeric"
|
||||
ReturnType="Next"
|
||||
HorizontalOptions="Fill"
|
||||
Focused="EntNumber_Focused"
|
||||
TextColor="Black" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
|
||||
namespace Maui.Controls.Sample.Issues;
|
||||
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.Github, 22715, "Page should not scroll when focusing element above keyboard", PlatformAffected.iOS)]
|
||||
public partial class Issue22715 : ContentPage
|
||||
{
|
||||
public Issue22715()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnPageLoaded(object sender, EventArgs e)
|
||||
{
|
||||
EntNumber.Focus();
|
||||
}
|
||||
|
||||
void EntNumber_Focused(object sender, FocusEventArgs e)
|
||||
{
|
||||
#if IOS
|
||||
var entry = (Entry)sender;
|
||||
var field = entry.Handler?.PlatformView as UIKit.UITextField;
|
||||
if (field is not null)
|
||||
{
|
||||
field.TintColor = UIKit.UIColor.Clear;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Maui.Controls.Sample.Issues.Issue24496"
|
||||
Title="Issue24496">
|
||||
<ContentPage.Resources>
|
||||
<Style TargetType="Entry">
|
||||
<Style.Triggers>
|
||||
<Trigger
|
||||
TargetType="Entry"
|
||||
Property="IsFocused"
|
||||
Value="True">
|
||||
<Setter
|
||||
Property="BackgroundColor"
|
||||
Value="Yellow" />
|
||||
</Trigger>
|
||||
|
||||
<Trigger
|
||||
TargetType="Entry"
|
||||
Property="IsFocused"
|
||||
Value="False">
|
||||
<Setter
|
||||
Property="BackgroundColor"
|
||||
Value="LightPink" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Picker">
|
||||
<Style.Triggers>
|
||||
<Trigger
|
||||
TargetType="Picker"
|
||||
Property="IsFocused"
|
||||
Value="True">
|
||||
<Setter
|
||||
Property="BackgroundColor"
|
||||
Value="Yellow" />
|
||||
</Trigger>
|
||||
<Trigger
|
||||
TargetType="Picker"
|
||||
Property="IsFocused"
|
||||
Value="False">
|
||||
<Setter
|
||||
Property="BackgroundColor"
|
||||
Value="LightGreen" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<x:Array x:Key="pickerInputs" Type="{x:Type x:String}">
|
||||
<x:String>Test 1</x:String>
|
||||
<x:String>Test 2</x:String>
|
||||
<x:String>Test 3</x:String>
|
||||
<x:String>Test 4</x:String>
|
||||
</x:Array>
|
||||
</ContentPage.Resources>
|
||||
<ScrollView>
|
||||
|
||||
<Grid
|
||||
Padding="{x:OnPlatform Default='30,0', iOS='30,0,30,34'}"
|
||||
ColumnDefinitions="*,*">
|
||||
<VerticalStackLayout
|
||||
Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Fill"
|
||||
Padding="5"
|
||||
Spacing="25">
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}" AutomationId="Picker6"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry" AutomationId="Entry7" Focused="Entry_Focused"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
<Entry BackgroundColor="LightPink" Placeholder="Entry"/>
|
||||
<Picker BackgroundColor="LightGreen" ItemsSource="{x:StaticResource pickerInputs}"/>
|
||||
|
||||
</VerticalStackLayout>
|
||||
<VerticalStackLayout
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
HorizontalOptions="Fill"
|
||||
Padding="5"
|
||||
Spacing="25">
|
||||
|
||||
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
|
||||
</ScrollView>
|
||||
</ContentPage>
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
|
||||
namespace Maui.Controls.Sample.Issues
|
||||
{
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.Github, 24496, "Pickers scroll to bottom and new keyboard types rekick the scrolling", PlatformAffected.iOS)]
|
||||
public partial class Issue24496 : ContentPage
|
||||
{
|
||||
public Issue24496()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
void Entry_Focused(object sender, FocusEventArgs e)
|
||||
{
|
||||
#if IOS
|
||||
var entry = (Entry)sender;
|
||||
var field = entry.Handler?.PlatformView as UIKit.UITextField;
|
||||
if (field is not null)
|
||||
{
|
||||
field.TintColor = UIKit.UIColor.Clear;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Maui.Controls.Sample.Issues.Issue24746"
|
||||
Title="Issue24746">
|
||||
<VerticalStackLayout>
|
||||
<HorizontalStackLayout>
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png"
|
||||
Background="lightgray" ContentLayout="Top,0" AutomationId="TopButton"/>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<HorizontalStackLayout>
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png"
|
||||
Background="lightgreen" ContentLayout="Top,0" Padding="0" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<HorizontalStackLayout>
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png"
|
||||
Background="lightblue" ContentLayout="Top,0" Padding="5" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<HorizontalStackLayout>
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png"
|
||||
Background="purple" ContentLayout="Top,0" Padding="50,10" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png"
|
||||
Background="lightgray" ContentLayout="Top,0" HorizontalOptions="Center" />
|
||||
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png" Padding="0"
|
||||
Background="lightgreen" ContentLayout="Top,0" HorizontalOptions="Center" />
|
||||
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png" Padding="5"
|
||||
Background="lightblue" ContentLayout="Top,0" HorizontalOptions="Center" />
|
||||
|
||||
<Button Text="Hello, longer world!" ImageSource="dotnet_bot_resized2.png" Padding="50,10"
|
||||
Background="purple" ContentLayout="Top,0" HorizontalOptions="Center" />
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ContentPage>
|
|
@ -0,0 +1,11 @@
|
|||
namespace Maui.Controls.Sample.Issues;
|
||||
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.Github, 24746, "iOS button padding is increased if needed", PlatformAffected.All)]
|
||||
public partial class Issue24746 : ContentPage
|
||||
{
|
||||
public Issue24746()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:issues="clr-namespace:Maui.Controls.Sample.Issues"
|
||||
x:Class="Maui.Controls.Sample.Issues.RenderingPerformance"
|
||||
Title="RenderingPerformance">
|
||||
|
||||
<Grid Padding="24" RowDefinitions="Auto,Auto,*" RowSpacing="8">
|
||||
<Button x:Name="StartButton" Clicked="ButtonClicked" Text="Start" AutomationId="StartButton" />
|
||||
<Label Grid.Row="1" Text="Do not press 'Start' more than once, just wait." />
|
||||
|
||||
<ScrollView Grid.Row="2">
|
||||
<ContentView>
|
||||
<VerticalStackLayout x:Name="BindableContainer" BindableLayout.ItemsSource="{Binding Models}">
|
||||
<BindableLayout.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="0,16" Padding="8" StrokeShape="RoundRectangle 8" Background="LightBlue">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||
<Label Text="{Binding Header}" />
|
||||
<Label Text="{Binding Content}" Grid.Row="1" />
|
||||
<VerticalStackLayout Grid.Row="2" BindableLayout.ItemsSource="{Binding SubModels}" Margin="16,0">
|
||||
<BindableLayout.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentView>
|
||||
<VerticalStackLayout Margin="0,8">
|
||||
<Label Text="{Binding Header}" />
|
||||
<issues:MeasuredLabel IsMeasured="{Binding IsMeasured}" Text="{Binding Content}" />
|
||||
</VerticalStackLayout>
|
||||
</ContentView>
|
||||
</DataTemplate>
|
||||
</BindableLayout.ItemTemplate>
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</BindableLayout.ItemTemplate>
|
||||
</VerticalStackLayout>
|
||||
</ContentView>
|
||||
</ScrollView>
|
||||
</Grid>
|
||||
</ContentPage>
|
|
@ -0,0 +1,220 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Microsoft.Maui;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Controls.Xaml;
|
||||
using ILayout = Microsoft.Maui.ILayout;
|
||||
|
||||
namespace Maui.Controls.Sample.Issues;
|
||||
|
||||
public class MeasuredLabel : Label
|
||||
{
|
||||
private static readonly TimeSpan ArrangedThreshold = TimeSpan.FromSeconds(1);
|
||||
public static readonly BindableProperty IsMeasuredProperty = BindableProperty.Create(nameof(IsMeasured), typeof(bool), typeof(MeasuredLabel), false);
|
||||
|
||||
public bool IsMeasured
|
||||
{
|
||||
get => (bool)GetValue(IsMeasuredProperty);
|
||||
set => SetValue(IsMeasuredProperty, value);
|
||||
}
|
||||
|
||||
public long? LastArrangedTicks { get; set; }
|
||||
|
||||
public long? GetArrangeTicks() {
|
||||
if (LastArrangedTicks is { } ticks)
|
||||
{
|
||||
var elapsed = Stopwatch.GetElapsedTime(ticks);
|
||||
if (elapsed > ArrangedThreshold)
|
||||
{
|
||||
return ticks;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RenderingPerformanceExtensions
|
||||
{
|
||||
public static MauiAppBuilder RenderingPerformanceAddMappers(this MauiAppBuilder builder)
|
||||
{
|
||||
builder.ConfigureMauiHandlers(handlers =>
|
||||
{
|
||||
Microsoft.Maui.Handlers.LabelHandler.CommandMapper.AppendToMapping(nameof(IView.Frame), (handler, view, arg) =>
|
||||
{
|
||||
if (view is MeasuredLabel { IsMeasured: true } measuredLabel)
|
||||
{
|
||||
measuredLabel.LastArrangedTicks = Stopwatch.GetTimestamp();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
[XamlCompilation(XamlCompilationOptions.Compile)]
|
||||
[Issue(IssueTracker.None, 0, "Rendering performance", PlatformAffected.All)]
|
||||
public partial class RenderingPerformance : ContentPage
|
||||
{
|
||||
bool _firstRun = true;
|
||||
|
||||
public List<ViewModelStub> Models { get; set; }
|
||||
|
||||
public RenderingPerformance()
|
||||
{
|
||||
Models = GenerateMeasuredItem();
|
||||
BindingContext = this;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void ButtonClicked(object sender, EventArgs e)
|
||||
{
|
||||
var capturedTimes = new List<int[]>();
|
||||
|
||||
// Generate view models so that only the last NestedViewModelStub of the last ViewModelStub is measured
|
||||
// First time we generate 40 * 10 + 1 = 401 items
|
||||
// This causes the creation of (40 * 5) + (40 * 10 * 4) + (1 * 5) + (1 * 4) = ~1800 platform views
|
||||
var test1Models = GenerateItems(40, "First");
|
||||
// Second time we generate 20 * 10 + 1 = 201 items
|
||||
// This causes (20 * 5) + (20 * 10 * 4) = ~900 binding context changes
|
||||
// and other ~900 platform views removals
|
||||
var test2Models = GenerateItems(20, "Second");
|
||||
// Third time we manually clear the BindableContainer and reset the models to the initial state (1 measured item)
|
||||
var resetModel = GenerateMeasuredItem();
|
||||
|
||||
// This enables us to measure the time it takes to:
|
||||
// - Create platform views
|
||||
// - Bind the new view models
|
||||
// - Remove platform views
|
||||
// - Clear platform views
|
||||
|
||||
// Views include frequently used components like `ContentView` (legacy layout), `Border`, `VerticalStackLayout`, `Grid`, `Label`.
|
||||
// Measurement happens by tracking IView.Frame mapping which happens right after the platform has arranged the view in the container view.
|
||||
|
||||
// Clear the first measure (happened while rendering the page for the first time)
|
||||
if (_firstRun)
|
||||
{
|
||||
_firstRun = false;
|
||||
await GetArrangeTicksAsync();
|
||||
}
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
await Task.Delay(200);
|
||||
|
||||
Models = test1Models;
|
||||
var startTicks = Stopwatch.GetTimestamp();
|
||||
OnPropertyChanged(nameof(Models));
|
||||
var endTicks = await Task.Run(GetArrangeTicksAsync);
|
||||
var t1 = (int)Stopwatch.GetElapsedTime(startTicks, endTicks).TotalMilliseconds;
|
||||
|
||||
await Task.Delay(200);
|
||||
|
||||
Models = test2Models;
|
||||
startTicks = Stopwatch.GetTimestamp();
|
||||
OnPropertyChanged(nameof(Models));
|
||||
endTicks = await Task.Run(GetArrangeTicksAsync);
|
||||
var t2 = (int)Stopwatch.GetElapsedTime(startTicks, endTicks).TotalMilliseconds;
|
||||
|
||||
await Task.Delay(200);
|
||||
|
||||
startTicks = Stopwatch.GetTimestamp();
|
||||
BindableContainer.Clear();
|
||||
Models = resetModel;
|
||||
OnPropertyChanged(nameof(Models));
|
||||
endTicks = await Task.Run(GetArrangeTicksAsync);
|
||||
var t3 = (int)Stopwatch.GetElapsedTime(startTicks, endTicks).TotalMilliseconds;
|
||||
|
||||
capturedTimes.Add([t1, t2, t3]);
|
||||
}
|
||||
|
||||
var avg1 = (int)capturedTimes.Average(t => t[0]);
|
||||
var avg2 = (int)capturedTimes.Average(t => t[1]);
|
||||
var avg3 = (int)capturedTimes.Average(t => t[2]);
|
||||
StartButton.Text = $"{avg1},{avg2},{avg3}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Traverse the visual tree to find the last MeasuredLabel and return its arrange ticks when found
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
async Task<long> GetArrangeTicksAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
IView view = BindableContainer;
|
||||
while (true)
|
||||
{
|
||||
if (view is ILayout { Count: > 0 } layout)
|
||||
{
|
||||
view = layout[^1];
|
||||
}
|
||||
else if (view is IContentView contentView)
|
||||
{
|
||||
view = (IView)contentView.Content;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (view is MeasuredLabel measuredLabel && measuredLabel.GetArrangeTicks() is { } arrangeTicks)
|
||||
{
|
||||
measuredLabel.LastArrangedTicks = null;
|
||||
return arrangeTicks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static List<ViewModelStub> GenerateItems(int count, string prefix)
|
||||
{
|
||||
return
|
||||
[
|
||||
..Enumerable.Range(0, count).Select(i => new ViewModelStub
|
||||
{
|
||||
Content = $"{prefix} Content {i}",
|
||||
Header = $"Header {i}",
|
||||
SubModels = Enumerable.Range(0, 10).Select(j => new NestedViewModelStub
|
||||
{
|
||||
Content = $"{prefix} SubContent {j}", Header = $"{prefix} SubHeader {j}"
|
||||
}).ToArray()
|
||||
}),
|
||||
..GenerateMeasuredItem()
|
||||
];
|
||||
}
|
||||
|
||||
static List<ViewModelStub> GenerateMeasuredItem()
|
||||
{
|
||||
return
|
||||
[
|
||||
new ViewModelStub
|
||||
{
|
||||
Content = "Measured Content",
|
||||
Header = "Measured Header",
|
||||
SubModels =
|
||||
[
|
||||
new NestedViewModelStub { Content = "Measured SubContent", Header = "Measured SubHeader", IsMeasured = true }
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public class ViewModelStub
|
||||
{
|
||||
public string Header { get; set; }
|
||||
public string Content { get; set; }
|
||||
public NestedViewModelStub[] SubModels { get; set; }
|
||||
}
|
||||
|
||||
public class NestedViewModelStub
|
||||
{
|
||||
public string Header { get; set; }
|
||||
public string Content { get; set; }
|
||||
public bool IsMeasured { get; set; }
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ namespace Maui.Controls.Sample
|
|||
fonts.AddFont("FontAwesome.ttf", "FA");
|
||||
fonts.AddFont("ionicons.ttf", "Ion");
|
||||
})
|
||||
.RenderingPerformanceAddMappers()
|
||||
.Issue21109AddMappers()
|
||||
.Issue18720AddMappers()
|
||||
.Issue18720EditorAddMappers()
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
#if IOS
|
||||
using System.Drawing;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using OpenQA.Selenium.Appium.Interactions;
|
||||
using OpenQA.Selenium.Appium.MultiTouch;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues;
|
||||
public class Issue19214 : _IssuesUITest
|
||||
{
|
||||
public Issue19214(TestDevice device) : base(device) { }
|
||||
|
||||
public override string Issue => "iOS Keyboard Scrolling ContentInset Tests";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.Entry)]
|
||||
public void TestMultipleScrollViews ()
|
||||
{
|
||||
var app = App as AppiumApp;
|
||||
if (app is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var topRectY = app.WaitForElement("TopEntry_1").GetRect().Y;
|
||||
var bottomRectY = app.WaitForElement("Entry2_3").GetRect().Y;
|
||||
|
||||
for (int i = 1; i < 4; i++)
|
||||
{
|
||||
var topEntry = $"TopEntry_{i}";
|
||||
var bottomEntry = $"BottomEntry_{i}";
|
||||
var entryTwo = $"Entry2_{i}";
|
||||
var entryTen = $"Entry10_{i}";
|
||||
var scrollView = $"ScrollView_{i}";
|
||||
|
||||
CheckInsets(app, topEntry, topEntry, bottomEntry, scrollView);
|
||||
CheckInsets(app, entryTwo, topEntry, bottomEntry, scrollView);
|
||||
CheckInsets(app, entryTen, topEntry, bottomEntry, scrollView, true);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckInsets (AppiumApp app, string queryEntry, string topEntry, string bottomEntry, string scrollView, bool startFromBottom = false)
|
||||
{
|
||||
if (startFromBottom)
|
||||
{
|
||||
var startRect = app.WaitForElement(topEntry).GetRect();
|
||||
ScrollScrollView(app, startRect);
|
||||
}
|
||||
|
||||
var queryRect = app.WaitForElement(queryEntry).GetRect();
|
||||
ClassicAssert.NotNull(queryRect, "Could not find the initial entry.");
|
||||
|
||||
app.Click(queryEntry);
|
||||
queryRect = app.WaitForElement(queryEntry).GetRect();
|
||||
KeyboardScrolling.CheckIfViewAboveKeyboard(app, queryEntry, false);
|
||||
|
||||
// Make sure we can scroll up to the top entry
|
||||
ScrollScrollView(app, queryRect, false);
|
||||
var topRect = app.WaitForElement(topEntry).GetRect();
|
||||
|
||||
ConfirmVisible (app, topRect, scrollView, topEntry, true);
|
||||
|
||||
// Scroll to the bottom of the ScrollView
|
||||
ScrollScrollView(app, topRect);
|
||||
|
||||
// Make sure we get to the bottom of the ScrollView
|
||||
var bottomRect = app.WaitForElement(bottomEntry).GetRect();
|
||||
ConfirmVisible (app, bottomRect, scrollView, bottomEntry, false);
|
||||
|
||||
// Scroll back up and make sure we can get all the way up
|
||||
ScrollScrollView(app, bottomRect, false);
|
||||
topRect = app.WaitForElement(topEntry).GetRect();
|
||||
ConfirmVisible (app, topRect, scrollView, topEntry, true);
|
||||
|
||||
// Hide the keyboard
|
||||
KeyboardScrolling.HideKeyboard(app, app.Driver, false);
|
||||
}
|
||||
|
||||
void ConfirmVisible (AppiumApp app, Rectangle rect, string scrollView, string entry, bool isTopField)
|
||||
{
|
||||
var scrollViewRect = app.WaitForElement(scrollView).GetRect();
|
||||
KeyboardScrolling.CheckIfViewAboveKeyboard(app, entry, false);
|
||||
// ClassicAssert.True(rect.Y > scrollViewRect.Y && rect.Bottom < scrollViewRect.Bottom, $"{entry} was not visible in {scrollView}");
|
||||
if (isTopField)
|
||||
{
|
||||
ClassicAssert.Greater(rect.Y, scrollViewRect.Y, $"rect.Y: {rect.Y} was not greater than scrollViewRect.Y: {scrollViewRect.Y}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ClassicAssert.Less(rect.Bottom, scrollViewRect.Bottom, $"rect.Bottom: {rect.Bottom} was not less than scrollViewRect.Bottom: {scrollViewRect.Bottom}");
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollScrollView (AppiumApp app, Rectangle rect, bool scrollsDown = true)
|
||||
{
|
||||
var newY = scrollsDown ? rect.Y - 5000 : rect.Y + 5000;
|
||||
|
||||
OpenQA.Selenium.Appium.Interactions.PointerInputDevice touchDevice = new OpenQA.Selenium.Appium.Interactions.PointerInputDevice(PointerKind.Touch);
|
||||
var scrollSequence = new ActionSequence(touchDevice, 0);
|
||||
if (scrollsDown)
|
||||
{
|
||||
scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, rect.Left - 5, rect.Y, TimeSpan.Zero));
|
||||
}
|
||||
else
|
||||
{
|
||||
scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, rect.Left - 5, rect.Bottom, TimeSpan.Zero));
|
||||
}
|
||||
scrollSequence.AddAction(touchDevice.CreatePointerDown(PointerButton.TouchContact));
|
||||
scrollSequence.AddAction(touchDevice.CreatePause(TimeSpan.FromMilliseconds(500)));
|
||||
scrollSequence.AddAction(touchDevice.CreatePointerMove(CoordinateOrigin.Viewport, rect.Left - 5, newY, TimeSpan.FromMilliseconds(250)));
|
||||
scrollSequence.AddAction(touchDevice.CreatePointerUp(PointerButton.TouchContact));
|
||||
app.Driver.PerformActions([scrollSequence]);
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,77 @@
|
|||
#if IOS
|
||||
using System.Drawing;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using OpenQA.Selenium.Appium.Interactions;
|
||||
using OpenQA.Selenium.Appium.MultiTouch;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
using System.Text;
|
||||
using OpenQA.Selenium;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues;
|
||||
public class Issue19214_2 : _IssuesUITest
|
||||
{
|
||||
public Issue19214_2(TestDevice device) : base(device) { }
|
||||
|
||||
public override string Issue => "iOS Editor Cursor stays above keyboard - Top level Grid";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.Entry)]
|
||||
public void KeepEditorCursorAboveKeyboardInGrid ()
|
||||
{
|
||||
var app = App as AppiumApp;
|
||||
if (app is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var editorRect = app.WaitForElement("IssueEditor").GetRect();
|
||||
app.Click("IssueEditor");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 1; i <= 30; i++)
|
||||
{
|
||||
sb.Append($"\n{i}");
|
||||
}
|
||||
|
||||
app.EnterText("IssueEditor", sb.ToString());
|
||||
|
||||
var keyboardLocation = KeyboardScrolling.FindiOSKeyboardLocation(app.Driver);
|
||||
|
||||
var cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
|
||||
|
||||
var cursorHeight1 = Convert.ToDouble(cursorLabel);
|
||||
KeyboardScrolling.HideKeyboard(app, app.Driver, true);
|
||||
|
||||
// Click a low spot on the editor
|
||||
var lowSpotY = editorRect.Y + editorRect.Height - 100;
|
||||
app.TapCoordinates(editorRect.X + 10, lowSpotY);
|
||||
|
||||
app.EnterText("IssueEditor", "A");
|
||||
|
||||
cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
|
||||
var cursorHeight2 = Convert.ToDouble(cursorLabel);
|
||||
|
||||
app.EnterText("IssueEditor", sb.ToString());
|
||||
|
||||
cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
|
||||
var cursorHeight3 = Convert.ToDouble(cursorLabel);
|
||||
|
||||
if (keyboardLocation is Point keyboardPoint)
|
||||
{
|
||||
ClassicAssert.True(cursorHeight1 > 0);
|
||||
ClassicAssert.True(cursorHeight2 > 0);
|
||||
ClassicAssert.True(cursorHeight3 > 0);
|
||||
ClassicAssert.True(cursorHeight1 < keyboardPoint.Y);
|
||||
ClassicAssert.True(cursorHeight2 < keyboardPoint.Y);
|
||||
ClassicAssert.True(cursorHeight3 < keyboardPoint.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClassicAssert.Fail("keyboardLocation is null");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,77 @@
|
|||
#if IOS
|
||||
using System.Drawing;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using OpenQA.Selenium.Appium.Interactions;
|
||||
using OpenQA.Selenium.Appium.MultiTouch;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
using System.Text;
|
||||
using OpenQA.Selenium;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues;
|
||||
public class Issue19214_3 : _IssuesUITest
|
||||
{
|
||||
public Issue19214_3(TestDevice device) : base(device) { }
|
||||
|
||||
public override string Issue => "iOS Editor Cursor stays above keyboard - Top level ScrollView";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.Entry)]
|
||||
public void KeepEditorCursorAboveKeyboardInScrollView ()
|
||||
{
|
||||
var app = App as AppiumApp;
|
||||
if (app is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var editorRect = app.WaitForElement("IssueEditor").GetRect();
|
||||
app.Click("IssueEditor");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 1; i <= 30; i++)
|
||||
{
|
||||
sb.Append($"\n{i}");
|
||||
}
|
||||
|
||||
app.EnterText("IssueEditor", sb.ToString());
|
||||
|
||||
var keyboardLocation = KeyboardScrolling.FindiOSKeyboardLocation(app.Driver);
|
||||
|
||||
var cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
|
||||
|
||||
var cursorHeight1 = Convert.ToDouble(cursorLabel);
|
||||
KeyboardScrolling.HideKeyboard(app, app.Driver, true);
|
||||
|
||||
// Click a low spot on the editor
|
||||
var lowSpotY = editorRect.Y + editorRect.Height - 100;
|
||||
app.TapCoordinates(editorRect.X + 10, lowSpotY);
|
||||
|
||||
app.EnterText("IssueEditor", "A");
|
||||
|
||||
cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
|
||||
var cursorHeight2 = Convert.ToDouble(cursorLabel);
|
||||
|
||||
app.EnterText("IssueEditor", sb.ToString());
|
||||
|
||||
cursorLabel = app.WaitForElement("CursorHeightTracker").GetText();
|
||||
var cursorHeight3 = Convert.ToDouble(cursorLabel);
|
||||
|
||||
if (keyboardLocation is Point keyboardPoint)
|
||||
{
|
||||
ClassicAssert.True(cursorHeight1 > 0);
|
||||
ClassicAssert.True(cursorHeight2 > 0);
|
||||
ClassicAssert.True(cursorHeight3 > 0);
|
||||
ClassicAssert.True(cursorHeight1 < keyboardPoint.Y);
|
||||
ClassicAssert.True(cursorHeight2 < keyboardPoint.Y);
|
||||
ClassicAssert.True(cursorHeight3 < keyboardPoint.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClassicAssert.Fail("keyboardLocation is null");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -55,20 +55,31 @@ public class Issue19956: _IssuesUITest
|
|||
{
|
||||
var app = App as AppiumApp;
|
||||
if (app is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
App.Tap("Entry5");
|
||||
ScrollToBottom(app);
|
||||
CheckForBottomEntry(app);
|
||||
KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
|
||||
try
|
||||
{
|
||||
App.Tap("Entry5");
|
||||
ScrollToBottom(app);
|
||||
CheckForBottomEntry(app);
|
||||
KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
|
||||
|
||||
App.Tap("Entry10");
|
||||
ScrollToBottom(app);
|
||||
CheckForBottomEntry(app);
|
||||
KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
|
||||
App.Tap("Entry10");
|
||||
ScrollToBottom(app);
|
||||
CheckForBottomEntry(app);
|
||||
KeyboardScrolling.NextiOSKeyboardPress(app.Driver);
|
||||
|
||||
ScrollToBottom(app);
|
||||
CheckForBottomEntry(app);
|
||||
ScrollToBottom(app);
|
||||
CheckForBottomEntry(app);
|
||||
}
|
||||
finally
|
||||
{
|
||||
//Reset the app so other UITest is in a clean state
|
||||
Reset();
|
||||
FixtureSetup();
|
||||
}
|
||||
}
|
||||
|
||||
static void ScrollToBottom(AppiumApp app)
|
||||
|
@ -92,4 +103,4 @@ public class Issue19956: _IssuesUITest
|
|||
ClassicAssert.Less(bottomEntryRect.Bottom, keyboardPosition!.Value.Y);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
using NUnit.Framework;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues
|
||||
{
|
||||
public class Issue22032 : _IssuesUITest
|
||||
{
|
||||
public Issue22032(TestDevice device) : base(device) { }
|
||||
|
||||
public override string Issue => "Shell FlyoutItem Tab Selected Icon Color not changing if using Font icons";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.TabbedPage)]
|
||||
public void SelectedTabIconShouldChangeColor()
|
||||
{
|
||||
App.WaitForElement("button");
|
||||
App.Click("button");
|
||||
App.WaitForElement("label");
|
||||
|
||||
// The test passes if tab1 icon is green and tab2 red
|
||||
VerifyScreenshot();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
#if IOS
|
||||
using System.Drawing;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using OpenQA.Selenium.Appium.Interactions;
|
||||
using OpenQA.Selenium.Appium.MultiTouch;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues;
|
||||
public class Issue22715 : _IssuesUITest
|
||||
{
|
||||
public Issue22715(TestDevice device) : base(device) { }
|
||||
|
||||
public override string Issue => "Page should not scroll when focusing element above keyboard";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.Entry)]
|
||||
public void PageShouldNotScroll ()
|
||||
{
|
||||
App.WaitForElement("EntNumber").GetRect();
|
||||
App.WaitForElement("TopLabel").GetRect();
|
||||
VerifyScreenshot();
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,28 @@
|
|||
#if IOS
|
||||
using NUnit.Framework;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues
|
||||
{
|
||||
public class Issue24496 : _IssuesUITest
|
||||
{
|
||||
public Issue24496(TestDevice testDevice) : base(testDevice)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Issue => "Pickers scroll to bottom and new keyboard types rekick the scrolling";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.Entry)]
|
||||
public void PickerNewKeyboardIsAboveKeyboard()
|
||||
{
|
||||
App.WaitForElement("Picker6");
|
||||
App.Tap("Picker6");
|
||||
VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Picker6");
|
||||
App.Tap("Entry7");
|
||||
VerifyScreenshot(TestContext.CurrentContext.Test.MethodName + "_Entry7");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,22 @@
|
|||
using NUnit.Framework;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues
|
||||
{
|
||||
public class Issue24746 : _IssuesUITest
|
||||
{
|
||||
public Issue24746(TestDevice testDevice) : base(testDevice)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Issue => "iOS button padding is increased if needed";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.Button)]
|
||||
public void ButtonPaddingIsAddedWhenNeeded()
|
||||
{
|
||||
VerifyScreenshot();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using UITest.Appium;
|
||||
using UITest.Core;
|
||||
|
||||
namespace Microsoft.Maui.TestCases.Tests.Issues
|
||||
{
|
||||
public class RenderingPerformance : _IssuesUITest
|
||||
{
|
||||
public RenderingPerformance(TestDevice device) : base(device) { }
|
||||
|
||||
public override string Issue => "Rendering performance";
|
||||
|
||||
[Test]
|
||||
[Category(UITestCategories.Performance)]
|
||||
public async Task RenderingPerformanceRun()
|
||||
{
|
||||
const string automationId = "StartButton";
|
||||
|
||||
var button = App.WaitForElement(automationId);
|
||||
App.Tap(automationId);
|
||||
|
||||
var timeout = TimeSpan.FromMinutes(5); // MACCATALYST takes a long time to run this test
|
||||
App.WaitForTextToBePresentInElement(automationId, ",", timeout);
|
||||
|
||||
var times = button.GetText()?.Split(',') ?? throw new ArgumentNullException("StartButton text is null");
|
||||
|
||||
var logMessage = @$"RenderingPerformance: [{times[0]}, {times[1]}, {times[2]}]";
|
||||
TestContext.WriteLine(logMessage);
|
||||
|
||||
// Write the log to a file and attach it to the test results for ADO
|
||||
var logFile = Path.Combine(Path.GetTempPath(), "RenderingPerformance.log");
|
||||
await File.WriteAllTextAsync(logFile, logMessage);
|
||||
TestContext.AddTestAttachment(logFile, "RenderingPerformance.log");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -71,7 +71,7 @@ namespace Microsoft.Maui.TestCases.Tests
|
|||
}
|
||||
|
||||
// will return a bool showing if the view is visible
|
||||
static bool CheckIfViewAboveKeyboard(IApp app, string marked, bool isEditor)
|
||||
internal static bool CheckIfViewAboveKeyboard(IApp app, string marked, bool isEditor)
|
||||
{
|
||||
var views = app.WaitForElement(marked);
|
||||
|
||||
|
|
Двоичные данные
src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/ButtonPaddingIsAddedWhenNeeded.png
Normal file
После Ширина: | Высота: | Размер: 35 KiB |
Двоичные данные
src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/SelectedTabIconShouldChangeColor.png
Normal file
После Ширина: | Высота: | Размер: 5.6 KiB |
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/ButtonPaddingIsAddedWhenNeeded.png
Normal file
После Ширина: | Высота: | Размер: 181 KiB |
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PageShouldNotScroll.png
Normal file
После Ширина: | Высота: | Размер: 60 KiB |
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Entry7.png
Normal file
После Ширина: | Высота: | Размер: 110 KiB |
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Picker6.png
Normal file
После Ширина: | Высота: | Размер: 99 KiB |
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SelectedTabIconShouldChangeColor.png
Normal file
После Ширина: | Высота: | Размер: 50 KiB |
|
@ -48,7 +48,7 @@ namespace Microsoft.Maui
|
|||
var image = UIGraphics.GetImageFromCurrentImageContext();
|
||||
UIGraphics.EndImageContext();
|
||||
|
||||
return image.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
|
||||
return image.ImageWithRenderingMode(UIImageRenderingMode.Automatic);
|
||||
}
|
||||
|
||||
internal static UIImage? GetPlatformImage(this IFileImageSource imageSource)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
|
@ -30,11 +31,8 @@ public static class KeyboardAutoManagerScroll
|
|||
static UIView? View;
|
||||
static UIView? ContainerView;
|
||||
static CGRect? CursorRect;
|
||||
static CGRect? StartingContainerViewFrame;
|
||||
internal static bool IsKeyboardShowing;
|
||||
static int TextViewDistanceFromBottom = 20;
|
||||
static int TextViewDistanceFromTop = 5;
|
||||
static int DebounceCount;
|
||||
static NSObject? WillShowToken;
|
||||
static NSObject? WillHideToken;
|
||||
static NSObject? DidHideToken;
|
||||
|
@ -55,7 +53,9 @@ public static class KeyboardAutoManagerScroll
|
|||
public static void Connect()
|
||||
{
|
||||
if (TextFieldToken is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextFieldToken = NSNotificationCenter.DefaultCenter.AddObserver(new NSString("UITextFieldTextDidBeginEditingNotification"), DidUITextBeginEditing);
|
||||
|
||||
|
@ -109,7 +109,7 @@ public static class KeyboardAutoManagerScroll
|
|||
IsKeyboardAutoScrollHandling = false;
|
||||
}
|
||||
|
||||
static async void DidUITextBeginEditing(NSNotification notification)
|
||||
static void DidUITextBeginEditing(NSNotification notification)
|
||||
{
|
||||
IsKeyboardAutoScrollHandling = true;
|
||||
|
||||
|
@ -127,12 +127,7 @@ public static class KeyboardAutoManagerScroll
|
|||
|
||||
ContainerView = View.GetContainerView();
|
||||
|
||||
// Grab the starting position of the ContainerView so we can track if
|
||||
// there is any external scrolling going on
|
||||
if (ContainerView is not null)
|
||||
StartingContainerViewFrame = ContainerView.ConvertRectToView(ContainerView.Bounds, null);
|
||||
|
||||
await AdjustPositionDebounce();
|
||||
AdjustPositionDebounce().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,30 +141,42 @@ public static class KeyboardAutoManagerScroll
|
|||
internal static CGRect? FindCursorPosition()
|
||||
{
|
||||
var localCursor = FindLocalCursorPosition();
|
||||
if (localCursor is CGRect local)
|
||||
return View?.ConvertRectToView(local, null);
|
||||
if (localCursor is CGRect local && ContainerView is not null)
|
||||
{
|
||||
var cursorInContainer = ContainerView.ConvertRectFromView(local, View);
|
||||
var cursorInWindow = ContainerView.ConvertRectToView(cursorInContainer, null);
|
||||
return cursorInWindow;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async void WillKeyboardShow(NSNotification notification)
|
||||
static void WillKeyboardShow(NSNotification notification)
|
||||
{
|
||||
var userInfo = notification.UserInfo;
|
||||
var oldKeyboardFrame = KeyboardFrame;
|
||||
|
||||
if (userInfo is not null)
|
||||
{
|
||||
var frameSize = userInfo.FindValue("UIKeyboardFrameEndUserInfoKey");
|
||||
var frameSizeRect = DescriptionToCGRect(frameSize?.Description);
|
||||
if (frameSizeRect is not null)
|
||||
{
|
||||
KeyboardFrame = (CGRect)frameSizeRect;
|
||||
}
|
||||
|
||||
userInfo.SetAnimationDuration();
|
||||
}
|
||||
|
||||
if (!IsKeyboardShowing)
|
||||
{
|
||||
await AdjustPositionDebounce();
|
||||
IsKeyboardShowing = true;
|
||||
AdjustPositionDebounce().FireAndForget();
|
||||
}
|
||||
else if (oldKeyboardFrame != KeyboardFrame && IsKeyboardShowing)
|
||||
{
|
||||
// this could be the case if the keyboard is already showing but type of keyboard changes
|
||||
AdjustPositionDebounce().FireAndForget();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,10 +185,14 @@ public static class KeyboardAutoManagerScroll
|
|||
notification.UserInfo?.SetAnimationDuration();
|
||||
|
||||
if (LastScrollView is not null)
|
||||
{
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, AnimateHidingKeyboard, () => { });
|
||||
}
|
||||
|
||||
if (IsKeyboardShowing)
|
||||
{
|
||||
RestorePosition();
|
||||
}
|
||||
|
||||
IsKeyboardShowing = false;
|
||||
View = null;
|
||||
|
@ -212,7 +223,9 @@ public static class KeyboardAutoManagerScroll
|
|||
var durationNum = (NSNumber)NSObject.FromObject(durationObj);
|
||||
var num = (double)durationNum;
|
||||
if (num != 0)
|
||||
{
|
||||
AnimationDuration = num;
|
||||
}
|
||||
}
|
||||
|
||||
static void AnimateHidingKeyboard()
|
||||
|
@ -236,9 +249,13 @@ public static class KeyboardAutoManagerScroll
|
|||
if (!superScrollView.ContentOffset.Equals(newContentOffset))
|
||||
{
|
||||
if (View?.Superview is UIStackView)
|
||||
{
|
||||
superScrollView.SetContentOffset(newContentOffset, UIView.AnimationsEnabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
superScrollView.ContentOffset = newContentOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
superScrollView = superScrollView.FindResponder<UIScrollView>();
|
||||
|
@ -252,7 +269,9 @@ public static class KeyboardAutoManagerScroll
|
|||
// example of passed in description: "NSRect: {{0, 586}, {430, 346}}"
|
||||
|
||||
if (description is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var temp = RemoveEverythingExceptForNumbersAndCommas(description);
|
||||
var dimensions = temp.Split(',');
|
||||
|
@ -286,24 +305,23 @@ public static class KeyboardAutoManagerScroll
|
|||
// all the fields are updated before calling AdjustPostition()
|
||||
internal static async Task AdjustPositionDebounce()
|
||||
{
|
||||
Interlocked.Increment(ref DebounceCount);
|
||||
|
||||
var entranceCount = DebounceCount;
|
||||
|
||||
// If we are going to a new view that has an InputAccessoryView
|
||||
// while we have the keyboard up, we need a delay to recalculate
|
||||
// the height of the InputAccessoryView
|
||||
if (IsKeyboardShowing && View?.InputAccessoryView is not null)
|
||||
await Task.Delay(30);
|
||||
|
||||
if (entranceCount == DebounceCount)
|
||||
if (IsKeyboardShowing)
|
||||
{
|
||||
// If we are going to a new view that has an InputAccessoryView
|
||||
// while we have the keyboard up, we need a delay to recalculate
|
||||
// the height of the InputAccessoryView
|
||||
if (View?.InputAccessoryView is not null)
|
||||
{
|
||||
await Task.Delay(30);
|
||||
}
|
||||
AdjustPosition();
|
||||
|
||||
// See if the layout requests to scroll again after our initial scroll
|
||||
await Task.Delay(5);
|
||||
if (ShouldScrollAgain)
|
||||
{
|
||||
AdjustPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,7 +337,9 @@ public static class KeyboardAutoManagerScroll
|
|||
}
|
||||
|
||||
if (TopViewBeginOrigin == InvalidPoint)
|
||||
{
|
||||
TopViewBeginOrigin = new CGPoint(ContainerView.Frame.X, ContainerView.Frame.Y);
|
||||
}
|
||||
|
||||
var rootViewOrigin = new CGPoint(ContainerView.Frame.GetMinX(), ContainerView.Frame.GetMinY());
|
||||
var window = ContainerView.Window;
|
||||
|
@ -356,12 +376,10 @@ public static class KeyboardAutoManagerScroll
|
|||
navigationBarAreaHeight = statusBarHeight;
|
||||
}
|
||||
|
||||
var topLayoutGuide = Math.Max(navigationBarAreaHeight, ContainerView.LayoutMargins.Top) + TextViewDistanceFromTop;
|
||||
var topLayoutGuide = Math.Max(navigationBarAreaHeight, ContainerView.LayoutMargins.Top);
|
||||
|
||||
// calculate the cursor rect
|
||||
var localCursor = FindLocalCursorPosition();
|
||||
if (localCursor is CGRect local)
|
||||
CursorRect = View.ConvertRectToView(local, null);
|
||||
CursorRect = FindCursorPosition();
|
||||
|
||||
if (CursorRect is null)
|
||||
{
|
||||
|
@ -369,70 +387,93 @@ public static class KeyboardAutoManagerScroll
|
|||
return;
|
||||
}
|
||||
|
||||
var viewRectInWindow = View.ConvertRectToView(View.Bounds, window);
|
||||
var cursorRect = (CGRect)CursorRect;
|
||||
|
||||
// give a small offset of 20 plus the cursor.Height for the distance
|
||||
// between the selected text and the keyboard
|
||||
TextViewDistanceFromBottom = ((int?)localCursor?.Height ?? 0) + 20;
|
||||
var viewRectInContainer = ContainerView.ConvertRectFromView(View.Frame, View.Superview);
|
||||
var viewRectInWindow = ContainerView.ConvertRectToView(viewRectInContainer, null);
|
||||
|
||||
// since the cursorRect does not have a height for Pickers, we can assign the height of the picker as the cursor height
|
||||
if (cursorRect.Height == 0)
|
||||
{
|
||||
cursorRect.Height = View.Bounds.Height;
|
||||
}
|
||||
|
||||
var keyboardYPosition = window.Frame.Height - kbSize.Height - TextViewDistanceFromBottom;
|
||||
|
||||
// readjust contentInset when the textView height is too large for the screen
|
||||
var rootSuperViewFrameInWindow = window.Frame;
|
||||
if (ContainerView.Superview is UIView v)
|
||||
{
|
||||
rootSuperViewFrameInWindow = v.ConvertRectToView(v.Bounds, window);
|
||||
|
||||
var cursorRect = (CGRect)CursorRect;
|
||||
}
|
||||
|
||||
nfloat cursorNotInViewScroll = 0;
|
||||
nfloat move = 0;
|
||||
bool cursorTooHigh = false;
|
||||
bool cursorTooLow = false;
|
||||
|
||||
// Find the next parent ScrollView that is scrollable
|
||||
var superView = View.FindResponder<UIScrollView>();
|
||||
// Find the next parent ScrollView that is scrollable or use the current View if it is a ScrollView
|
||||
var superView = View.FindResponder<UIScrollView>() ?? View as UIScrollView;
|
||||
var superScrollView = FindParentScroll(superView);
|
||||
|
||||
CGRect? superScrollViewRect = null;
|
||||
var topBoundary = topLayoutGuide;
|
||||
var bottomBoundary = (double)keyboardYPosition;
|
||||
|
||||
if (superScrollView is not null){
|
||||
superScrollViewRect = superScrollView.ConvertRectToView(superScrollView.Bounds, window);
|
||||
topBoundary = Math.Max(topBoundary, superScrollViewRect.Value.Top + TextViewDistanceFromTop);
|
||||
bottomBoundary = Math.Min(bottomBoundary, superScrollViewRect.Value.Bottom - TextViewDistanceFromBottom);
|
||||
if (superScrollView is not null)
|
||||
{
|
||||
var superScrollInContainer = ContainerView.ConvertRectFromView(superScrollView.Frame, superScrollView.Superview);
|
||||
superScrollViewRect = ContainerView.ConvertRectToView(superScrollInContainer, null);
|
||||
topBoundary = Math.Max(topBoundary, superScrollViewRect.Value.Top);
|
||||
var superScrollViewBottom = superScrollViewRect.Value.Bottom - TextViewDistanceFromBottom;
|
||||
|
||||
// if the superScrollView is a small editor, it may not make sense to scroll the entire screen if cursor is visible
|
||||
if (superScrollView is UITextView && superScrollViewRect.Value.Bottom - TextViewDistanceFromBottom < cursorRect.Bottom)
|
||||
{
|
||||
superScrollViewBottom = superScrollViewRect.Value.Bottom;
|
||||
}
|
||||
|
||||
bottomBoundary = Math.Min(bottomBoundary, superScrollViewBottom);
|
||||
}
|
||||
|
||||
bool forceSetContentInsets = true;
|
||||
|
||||
// scenario where we go into an editor with the "Next" keyboard button,
|
||||
// but the cursor location on the editor is scrolled below the visible section
|
||||
if (View is UITextView && cursorRect.Y >= viewRectInWindow.GetMaxY())
|
||||
if (View is UITextView && IsKeyboardShowing && cursorRect.Bottom >= viewRectInWindow.GetMaxY())
|
||||
{
|
||||
cursorNotInViewScroll = viewRectInWindow.GetMaxY() - cursorRect.GetMaxY();
|
||||
move = cursorRect.Y - (nfloat)bottomBoundary + cursorNotInViewScroll;
|
||||
cursorTooLow = true;
|
||||
move = viewRectInWindow.Bottom - (nfloat)bottomBoundary;
|
||||
}
|
||||
|
||||
// scenario where we go into an editor with the "Next" keyboard button,
|
||||
// but the cursor location on the editor is scrolled above the visible section
|
||||
else if (View is UITextView && cursorRect.Y < viewRectInWindow.GetMinY())
|
||||
else if (View is UITextView && IsKeyboardShowing && cursorRect.Y < viewRectInWindow.GetMinY())
|
||||
{
|
||||
cursorNotInViewScroll = viewRectInWindow.GetMinY() - cursorRect.Y;
|
||||
move = cursorRect.Y - (nfloat)bottomBoundary + cursorNotInViewScroll;
|
||||
cursorTooHigh = true;
|
||||
move = viewRectInWindow.Top - (nfloat)bottomBoundary;
|
||||
|
||||
// no need to move the screen down if we can already see the view
|
||||
if (move < 0)
|
||||
{
|
||||
move = 0;
|
||||
}
|
||||
}
|
||||
|
||||
else if (cursorRect.Y >= topBoundary && cursorRect.Y < bottomBoundary)
|
||||
return;
|
||||
else if (cursorRect.Bottom > bottomBoundary && cursorRect.Y > topBoundary)
|
||||
{
|
||||
move = cursorRect.Bottom - (nfloat)bottomBoundary;
|
||||
}
|
||||
|
||||
else if (cursorRect.Y > bottomBoundary)
|
||||
move = cursorRect.Y - (nfloat)bottomBoundary;
|
||||
|
||||
else if (cursorRect.Y <= topBoundary)
|
||||
else if (cursorRect.Y <= topBoundary && cursorRect.Bottom <= bottomBoundary)
|
||||
{
|
||||
move = cursorRect.Y - (nfloat)topBoundary;
|
||||
}
|
||||
|
||||
else if (cursorRect.Y <= topBoundary && cursorRect.Bottom >= bottomBoundary)
|
||||
{
|
||||
cursorNotInViewScroll = viewRectInWindow.GetMinY() - cursorRect.Y;
|
||||
move = cursorRect.Bottom - (nfloat)bottomBoundary - cursorNotInViewScroll;
|
||||
cursorTooHigh = true;
|
||||
}
|
||||
|
||||
// This is the case when the keyboard is already showing and we click another editor/entry
|
||||
if (LastScrollView is not null)
|
||||
|
@ -441,14 +482,20 @@ public static class KeyboardAutoManagerScroll
|
|||
if (superScrollView is null)
|
||||
{
|
||||
if (LastScrollView.ContentInset != StartingContentInsets)
|
||||
{
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, AnimateStartingLastScrollView, () => { });
|
||||
}
|
||||
|
||||
if (!LastScrollView.ContentOffset.Equals(StartingContentOffset))
|
||||
{
|
||||
if (View.FindResponder<UIStackView>() is UIStackView)
|
||||
{
|
||||
LastScrollView.SetContentOffset(StartingContentOffset, UIView.AnimationsEnabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
LastScrollView.ContentOffset = StartingContentOffset;
|
||||
}
|
||||
}
|
||||
|
||||
StartingContentInsets = new UIEdgeInsets();
|
||||
|
@ -474,13 +521,28 @@ public static class KeyboardAutoManagerScroll
|
|||
var lastView = View;
|
||||
superScrollView = LastScrollView;
|
||||
nfloat innerScrollValue = 0;
|
||||
nfloat tempMove = 0;
|
||||
|
||||
while (superScrollView is not null)
|
||||
{
|
||||
var shouldContinue = false;
|
||||
|
||||
if (move > 0)
|
||||
// if we have an innerScrollValue, let's move with this value first and then do the move
|
||||
if (cursorNotInViewScroll != 0)
|
||||
{
|
||||
tempMove = move;
|
||||
move = cursorNotInViewScroll;
|
||||
shouldContinue = true;
|
||||
}
|
||||
|
||||
else if (move > 0 || tempMove > 0)
|
||||
{
|
||||
if (move == 0)
|
||||
{
|
||||
move = tempMove;
|
||||
}
|
||||
shouldContinue = move > -superScrollView.ContentOffset.Y - superScrollView.ContentInset.Top;
|
||||
}
|
||||
|
||||
else if (superScrollView.FindResponder<UITableView>() is UITableView tableView)
|
||||
{
|
||||
|
@ -522,27 +584,34 @@ public static class KeyboardAutoManagerScroll
|
|||
{
|
||||
shouldContinue = !(innerScrollValue == 0
|
||||
&& cursorRect.Y + cursorNotInViewScroll >= topBoundary
|
||||
&& cursorRect.Y + cursorNotInViewScroll <= bottomBoundary);
|
||||
&& cursorRect.Bottom + cursorNotInViewScroll <= bottomBoundary);
|
||||
|
||||
if (cursorRect.Y - innerScrollValue < topBoundary && !cursorTooHigh)
|
||||
{
|
||||
move = cursorRect.Y - innerScrollValue - (nfloat)topBoundary;
|
||||
}
|
||||
else if (cursorRect.Y - innerScrollValue > bottomBoundary && !cursorTooLow)
|
||||
{
|
||||
move = cursorRect.Y - innerScrollValue - (nfloat)bottomBoundary;
|
||||
}
|
||||
}
|
||||
|
||||
// Go up the hierarchy and look for other scrollViews until we reach the UIWindow
|
||||
if (shouldContinue)
|
||||
{
|
||||
forceSetContentInsets = false;
|
||||
|
||||
var tempScrollView = superScrollView.FindResponder<UIScrollView>();
|
||||
var nextScrollView = FindParentScroll(tempScrollView);
|
||||
|
||||
// if PrefersLargeTitles is true, we may need additional logic to
|
||||
// handle the collapsable navbar
|
||||
// if PrefersLargeTitles is true, we may need additional logic to handle the collapsable navbar
|
||||
var navController = View?.FindResponder<UINavigationController>();
|
||||
var prefersLargeTitles = navController?.NavigationBar.PrefersLargeTitles ?? false;
|
||||
|
||||
if (prefersLargeTitles)
|
||||
{
|
||||
move = AdjustForLargeTitles(move, superScrollView, navController!);
|
||||
}
|
||||
|
||||
var origContentOffsetY = superScrollView.ContentOffset.Y;
|
||||
var shouldOffsetY = superScrollView.ContentOffset.Y - Math.Min(superScrollView.ContentOffset.Y, -move);
|
||||
|
@ -555,7 +624,8 @@ public static class KeyboardAutoManagerScroll
|
|||
|
||||
if ((!superScrollView.ContentOffset.Equals(newContentOffset) || innerScrollValue != 0) && superScrollViewRect is not null)
|
||||
{
|
||||
if (nextScrollView is null && superScrollViewRect.Value.Y < bottomBoundary)
|
||||
if ((nextScrollView is null && superScrollViewRect.Value.Y + cursorRect.Height + TextViewDistanceFromBottom < bottomBoundary) ||
|
||||
cursorNotInViewScroll != 0)
|
||||
{
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () =>
|
||||
{
|
||||
|
@ -564,9 +634,13 @@ public static class KeyboardAutoManagerScroll
|
|||
ScrolledView = superScrollView;
|
||||
|
||||
if (View?.FindResponder<UIStackView>() is not null)
|
||||
{
|
||||
superScrollView.SetContentOffset(newContentOffset, UIView.AnimationsEnabled);
|
||||
}
|
||||
else
|
||||
{
|
||||
superScrollView.ContentOffset = newContentOffset;
|
||||
}
|
||||
}, () => { });
|
||||
|
||||
// after this scroll finishes, there is an edge case where if we have Large Titles,
|
||||
|
@ -575,7 +649,9 @@ public static class KeyboardAutoManagerScroll
|
|||
var amountNotScrolled = requestedMove - actualScrolledAmount;
|
||||
|
||||
if (prefersLargeTitles && amountNotScrolled > 1)
|
||||
{
|
||||
ShouldScrollAgain = true;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
|
@ -585,8 +661,16 @@ public static class KeyboardAutoManagerScroll
|
|||
}
|
||||
}
|
||||
|
||||
lastView = superScrollView;
|
||||
superScrollView = nextScrollView;
|
||||
// if we needed to scroll for cursorNotInViewScroll first, use the same superScrollView and handle the move now
|
||||
if (cursorNotInViewScroll != 0)
|
||||
{
|
||||
cursorNotInViewScroll = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastView = superScrollView;
|
||||
superScrollView = nextScrollView;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
|
@ -599,26 +683,24 @@ public static class KeyboardAutoManagerScroll
|
|||
|
||||
move += innerScrollValue;
|
||||
|
||||
// ContentInset logic
|
||||
if (ScrolledView is not null)
|
||||
// Adjust the parent's ContentInset.Bottom so we can still scroll to the top with the keyboard showing
|
||||
if (forceSetContentInsets && superScrollView is not null)
|
||||
{
|
||||
var bottomInset = kbSize.Height;
|
||||
var bottomScrollIndicatorInset = bottomInset - TextViewDistanceFromBottom;
|
||||
|
||||
bottomInset = nfloat.Max(StartingContentInsets.Bottom, bottomInset);
|
||||
bottomScrollIndicatorInset = nfloat.Max(StartingScrollIndicatorInsets.Bottom, bottomScrollIndicatorInset);
|
||||
|
||||
if (OperatingSystem.IsIOSVersionAtLeast(11, 0))
|
||||
ApplyContentInset(superScrollView, LastScrollView, false, false);
|
||||
// if our View is an editor, we can adjust the ContentInset.Bottom so that the text cursor will stay above the keyboard
|
||||
if (superScrollView != View && View is UITextView textView)
|
||||
{
|
||||
bottomInset -= ScrolledView.SafeAreaInsets.Bottom;
|
||||
bottomScrollIndicatorInset -= ScrolledView.SafeAreaInsets.Bottom;
|
||||
ApplyContentInset(textView, textView, false, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyContentInset (ScrolledView, LastScrollView, true, false);
|
||||
// if our View is an editor, we can adjust the ContentInset.Bottom so that the text cursor will stay above the keyboard
|
||||
if (ScrolledView != View && View is UITextView textView)
|
||||
{
|
||||
ApplyContentInset(textView, textView, true, true);
|
||||
}
|
||||
|
||||
var movedInsets = ScrolledView.ContentInset;
|
||||
movedInsets.Bottom = bottomInset;
|
||||
|
||||
if (LastScrollView.ContentInset != movedInsets)
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(ScrolledView, movedInsets, bottomScrollIndicatorInset), () => { });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -634,6 +716,13 @@ public static class KeyboardAutoManagerScroll
|
|||
rect.Y = rootViewOrigin.Y;
|
||||
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateRootView(rect), () => { });
|
||||
|
||||
// this is the scenario where there is a scrollview, but the whole scrollview is below
|
||||
// where the keyboard will be. We need to scroll the ContainerView and add ContentInsets to the scrollview.
|
||||
if (LastScrollView is not null)
|
||||
{
|
||||
ApplyContentInset(LastScrollView, LastScrollView, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -655,15 +744,21 @@ public static class KeyboardAutoManagerScroll
|
|||
static void AnimateInset(UIScrollView? scrollView, UIEdgeInsets movedInsets, nfloat bottomScrollIndicatorInset)
|
||||
{
|
||||
if (scrollView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
scrollView.ContentInset = movedInsets;
|
||||
UIEdgeInsets newscrollIndicatorInset;
|
||||
|
||||
if (OperatingSystem.IsIOSVersionAtLeast(11, 0))
|
||||
{
|
||||
newscrollIndicatorInset = scrollView.VerticalScrollIndicatorInsets;
|
||||
}
|
||||
else
|
||||
{
|
||||
newscrollIndicatorInset = scrollView.ScrollIndicatorInsets;
|
||||
}
|
||||
|
||||
newscrollIndicatorInset.Bottom = bottomScrollIndicatorInset;
|
||||
scrollView.ScrollIndicatorInsets = newscrollIndicatorInset;
|
||||
|
@ -681,15 +776,76 @@ public static class KeyboardAutoManagerScroll
|
|||
static void AnimateRootView(CGRect rect)
|
||||
{
|
||||
if (ContainerView is not null)
|
||||
{
|
||||
ContainerView.Frame = rect;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjusts the ContentInset of our view that Scrolled so that we can still scroll to the top and bottom with the keyboard showing.
|
||||
static void ApplyContentInset(UIScrollView? scrolledView, UIScrollView? lastScrollView, bool didMove, bool isInnerEditor)
|
||||
{
|
||||
if (scrolledView is null || lastScrollView is null || ContainerView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var frameInContainer = ContainerView.ConvertRectFromView(scrolledView.Frame, scrolledView.Superview);
|
||||
var frameInWindow = ContainerView.ConvertRectToView(frameInContainer, null);
|
||||
|
||||
var keyboardIntersect = CGRect.Intersect(KeyboardFrame, frameInWindow);
|
||||
|
||||
var bottomInset = keyboardIntersect.Height;
|
||||
|
||||
// For new lines in an editor, we want the cursor to stay right above the keyboard.
|
||||
// When adding contentInsets for a scrollview, it is nice to have a little extra padding.
|
||||
if (scrolledView is not UITextView && keyboardIntersect.Height > 0)
|
||||
{
|
||||
bottomInset += TextViewDistanceFromBottom;
|
||||
}
|
||||
|
||||
var bottomScrollIndicatorInset = bottomInset;
|
||||
|
||||
bottomInset = nfloat.Max(StartingContentInsets.Bottom, bottomInset);
|
||||
bottomScrollIndicatorInset = nfloat.Max(StartingScrollIndicatorInsets.Bottom, bottomScrollIndicatorInset);
|
||||
|
||||
if (OperatingSystem.IsIOSVersionAtLeast(11, 0))
|
||||
{
|
||||
bottomInset -= scrolledView.SafeAreaInsets.Bottom;
|
||||
bottomScrollIndicatorInset -= scrolledView.SafeAreaInsets.Bottom;
|
||||
}
|
||||
|
||||
var movedInsets = scrolledView.ContentInset;
|
||||
movedInsets.Bottom = bottomInset;
|
||||
|
||||
// if we are in an editor that is inside a scrollView and are below where the keyboard will appear,
|
||||
// the outer scrollview will put the cursor above the keyboard and we will
|
||||
// need to add a bottom inset to the inner editor so that the cursor will
|
||||
// stay above the keyboard when we add new lines.
|
||||
if (didMove && isInnerEditor && scrolledView is UITextView textView)
|
||||
{
|
||||
var cursorRect = FindCursorPosition();
|
||||
if (cursorRect is CGRect cursor)
|
||||
{
|
||||
var editorBottomInset = frameInWindow.Bottom - cursor.Bottom - TextViewDistanceFromBottom;
|
||||
movedInsets.Bottom = nfloat.Max(0, editorBottomInset);
|
||||
bottomScrollIndicatorInset = nfloat.Max(0, editorBottomInset);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastScrollView.ContentInset != movedInsets)
|
||||
{
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(scrolledView, movedInsets, bottomScrollIndicatorInset), () => { });
|
||||
}
|
||||
}
|
||||
|
||||
static UIScrollView? FindParentScroll(UIScrollView? view)
|
||||
{
|
||||
while (view is not null)
|
||||
{
|
||||
if (view.ScrollEnabled)
|
||||
if (view.ScrollEnabled && !IsHorizontalCollectionView(view))
|
||||
{
|
||||
return view;
|
||||
}
|
||||
|
||||
view = view.FindResponder<UIScrollView>();
|
||||
}
|
||||
|
@ -697,10 +853,15 @@ public static class KeyboardAutoManagerScroll
|
|||
return null;
|
||||
}
|
||||
|
||||
static bool IsHorizontalCollectionView(UIView collectionView)
|
||||
=> collectionView is UICollectionView { CollectionViewLayout: UICollectionViewFlowLayout { ScrollDirection: UICollectionViewScrollDirection.Horizontal }};
|
||||
|
||||
internal static nfloat FindKeyboardHeight()
|
||||
{
|
||||
if (ContainerView is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var window = ContainerView.Window;
|
||||
var intersectRect = CGRect.Intersect(KeyboardFrame, window.Frame);
|
||||
|
@ -718,10 +879,11 @@ public static class KeyboardAutoManagerScroll
|
|||
// so skip if we are not in those scenarios.
|
||||
if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone
|
||||
&& (UIDevice.CurrentDevice.Orientation == UIDeviceOrientation.LandscapeLeft || UIDevice.CurrentDevice.Orientation == UIDeviceOrientation.LandscapeRight))
|
||||
{
|
||||
return move;
|
||||
}
|
||||
|
||||
// These values are not publicly available but can be tested.
|
||||
// It is possible that these can change in the future.
|
||||
// These values are not publicly available but can be tested. It is possible that these can change in the future.
|
||||
var navBarCollapsedHeight = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone ? 44 : 50;
|
||||
var navBarExpandedHeight = navController.NavigationBar.SizeThatFits(new CGSize(0, 0)).Height;
|
||||
|
||||
|
@ -738,12 +900,15 @@ public static class KeyboardAutoManagerScroll
|
|||
// to the minimum amount that will cause the collapse or else
|
||||
// we will not see our view
|
||||
if (move - navBarCollapseDifference < amountLeftToCollapseNavBar)
|
||||
{
|
||||
return amountLeftToCollapseNavBar;
|
||||
}
|
||||
|
||||
// else the navBar will collapse and we want to subtract
|
||||
// the navBarCollapseDifference to account for it
|
||||
// else the navBar will collapse and we want to subtract the navBarCollapseDifference to account for it
|
||||
else
|
||||
{
|
||||
return move - navBarCollapseDifference;
|
||||
}
|
||||
}
|
||||
return move;
|
||||
}
|
||||
|
@ -762,14 +927,20 @@ public static class KeyboardAutoManagerScroll
|
|||
}
|
||||
|
||||
if (ScrolledView is not null && ScrolledView.ContentInset != UIEdgeInsets.Zero)
|
||||
{
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(ScrolledView, UIEdgeInsets.Zero, 0), () => { });
|
||||
}
|
||||
|
||||
if (View is not null && View is UIScrollView editorScrollView && editorScrollView.ContentInset != UIEdgeInsets.Zero && View is UITextView textView)
|
||||
{
|
||||
UIView.Animate(AnimationDuration, 0, UIViewAnimationOptions.CurveEaseOut, () => AnimateInset(editorScrollView, UIEdgeInsets.Zero, 0), () => { });
|
||||
}
|
||||
|
||||
ScrolledView = null;
|
||||
View = null;
|
||||
ContainerView = null;
|
||||
TopViewBeginOrigin = InvalidPoint;
|
||||
CursorRect = null;
|
||||
StartingContainerViewFrame = null;
|
||||
ShouldIgnoreSafeAreaAdjustment = false;
|
||||
ShouldScrollAgain = false;
|
||||
}
|
||||
|
@ -783,16 +954,26 @@ public static class KeyboardAutoManagerScroll
|
|||
{
|
||||
previousSection -= 1;
|
||||
if (previousSection >= 0 && scrollView is UICollectionView collectionView)
|
||||
{
|
||||
previousRow = (int)(collectionView.NumberOfItemsInSection(previousSection) - 1);
|
||||
}
|
||||
else if (previousSection >= 0 && scrollView is UITableView tableView)
|
||||
{
|
||||
previousRow = (int)(tableView.NumberOfRowsInSection(previousSection) - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (previousRow >= 0 && previousSection >= 0)
|
||||
{
|
||||
return NSIndexPath.FromRowSection(previousRow, previousSection);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,7 +171,7 @@ namespace Microsoft.Maui.Platform
|
|||
{
|
||||
Maui.TextAlignment.Center => new CGPoint(0, -Math.Max(1, availableSpace / 2)),
|
||||
Maui.TextAlignment.End => new CGPoint(0, -Math.Max(1, availableSpace)),
|
||||
_ => new CGPoint(0, 0),
|
||||
_ => ContentOffset,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,9 @@ namespace Microsoft.Maui.Platform
|
|||
protected CGRect AdjustForSafeArea(CGRect bounds)
|
||||
{
|
||||
if (KeyboardAutoManagerScroll.ShouldIgnoreSafeAreaAdjustment)
|
||||
{
|
||||
KeyboardAutoManagerScroll.ShouldScrollAgain = true;
|
||||
}
|
||||
|
||||
if (View is not ISafeAreaView sav || sav.IgnoreSafeArea || !RespondsToSafeArea())
|
||||
{
|
||||
|
|
|
@ -401,11 +401,10 @@ namespace UITest.Appium
|
|||
Wait(query, i => i is null, timeoutMessage, timeout, retryFrequency);
|
||||
}
|
||||
|
||||
public static bool WaitForTextToBePresentInElement(this IApp app, string automationId, string text)
|
||||
public static bool WaitForTextToBePresentInElement(this IApp app, string automationId, string text, TimeSpan? timeout = null)
|
||||
{
|
||||
TimeSpan timeout = DefaultTimeout;
|
||||
timeout ??= DefaultTimeout;
|
||||
TimeSpan retryFrequency = TimeSpan.FromMilliseconds(500);
|
||||
string timeoutMessage = $"Timed out on {nameof(WaitForTextToBePresentInElement)}.";
|
||||
|
||||
DateTime start = DateTime.Now;
|
||||
|
||||
|
@ -418,9 +417,9 @@ namespace UITest.Appium
|
|||
}
|
||||
|
||||
long elapsed = DateTime.Now.Subtract(start).Ticks;
|
||||
if (elapsed >= timeout.Ticks)
|
||||
if (elapsed >= timeout.Value.Ticks)
|
||||
{
|
||||
Debug.WriteLine($">>>>> {elapsed} ticks elapsed, timeout value is {timeout.Ticks}");
|
||||
Debug.WriteLine($">>>>> {elapsed} ticks elapsed, timeout value is {timeout.Value.Ticks}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|