Improve keyboard scrolling with editors, contentInsets, and different keyboards (#24589)

* Changes to improve keyboard scrolling with editors, contentInsets, and different keyboards

* picker6 screenshot

* Add entry7 screenshot test

* reset test state

* fix the distance between keyboard and cursor and remove null forgiving

* bottomBoundary should have the TextDistanceFromBottom when the bottom is not the keyboard

* FireAndForget

* new entry7 screenshot

* add back the distance from bottom in the insets

* Allow partial scrolling one direction and then moving the next scrollview the opposite direction

* Adjust insets on UITextView and other scrollviews

* we dont need more inset if no keyboard intersection

* add edge case where we have small editor
This commit is contained in:
TJ Lambert 2024-09-19 06:16:32 -05:00 коммит произвёл GitHub
Родитель 60076be75d
Коммит 14c77194c5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
23 изменённых файлов: 1130 добавлений и 99 удалений

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

@ -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,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,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,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

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

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

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

После

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

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

После

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

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

После

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

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

@ -7,7 +7,6 @@
using System;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CoreGraphics;
using Foundation;
@ -30,11 +29,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 +51,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 +107,7 @@ public static class KeyboardAutoManagerScroll
IsKeyboardAutoScrollHandling = false;
}
static async void DidUITextBeginEditing(NSNotification notification)
static void DidUITextBeginEditing(NSNotification notification)
{
IsKeyboardAutoScrollHandling = true;
@ -127,12 +125,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 +139,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 +183,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 +221,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 +247,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 +267,9 @@ public static class KeyboardAutoManagerScroll
// example of passed in description: "NSRect: {{0, 586}, {430, 346}}"
if (description is null)
{
return null;
}
// remove everything except for numbers and commas
var temp = Regex.Replace(description, @"[^0-9,]", "");
@ -274,24 +291,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();
}
}
}
@ -307,7 +323,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;
@ -344,12 +362,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)
{
@ -357,70 +373,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)
@ -429,14 +468,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();
@ -462,13 +507,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)
{
@ -510,27 +570,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);
@ -543,7 +610,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, () =>
{
@ -552,9 +620,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,
@ -563,7 +635,9 @@ public static class KeyboardAutoManagerScroll
var amountNotScrolled = requestedMove - actualScrolledAmount;
if (prefersLargeTitles && amountNotScrolled > 1)
{
ShouldScrollAgain = true;
}
}
else
@ -573,8 +647,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
@ -587,26 +669,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), () => { });
}
}
@ -622,6 +702,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);
}
}
}
@ -643,15 +730,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;
@ -669,15 +762,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>();
}
@ -685,10 +839,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);
@ -706,10 +865,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;
@ -726,12 +886,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;
}
@ -750,14 +913,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;
}
@ -771,16 +940,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())
{