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:
Родитель
60076be75d
Коммит
14c77194c5
|
@ -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);
|
||||
|
||||
|
|
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PageShouldNotScroll.png
Normal file
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PageShouldNotScroll.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 60 KiB |
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Entry7.png
Normal file
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Entry7.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 110 KiB |
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Picker6.png
Normal file
Двоичные данные
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/PickerNewKeyboardIsAboveKeyboard_Picker6.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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())
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче