Merge branch 'main' into merge-main-net9

# Conflicts:
#	eng/Version.Details.xml
#	src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs
This commit is contained in:
Rui Marinho 2024-09-24 12:06:48 +01:00
Родитель c6ca951a38 a4d2290ef8
Коммит eb3ec76450
46 изменённых файлов: 1610 добавлений и 118 удалений

Просмотреть файл

@ -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))
{

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 150 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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);

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 35 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 5.6 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 181 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 60 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 110 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 99 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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;
}