-
Notifications
You must be signed in to change notification settings - Fork 0
[Windows] Fix CollectionView auto-scroll when on-screen keyboard appears #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
138797f
397a2e8
2f47fee
f3b39cd
3d75b12
31c2088
e7b32d1
37290d1
d489294
8e8d630
1338f24
750d65c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,10 +9,13 @@ | |
| using Microsoft.Maui.Controls.Platform; | ||
| using Microsoft.Maui.Graphics; | ||
| using Microsoft.Maui.Handlers; | ||
| using Microsoft.Maui.Platform; | ||
| using Microsoft.UI.Xaml; | ||
| using Microsoft.UI.Xaml.Controls; | ||
| using Microsoft.UI.Xaml.Controls.Primitives; | ||
| using Microsoft.UI.Xaml.Data; | ||
| using Microsoft.UI.Xaml.Input; | ||
| using Windows.UI.ViewManagement; | ||
| using WASDKApp = Microsoft.UI.Xaml.Application; | ||
| using WASDKDataTemplate = Microsoft.UI.Xaml.DataTemplate; | ||
| using WASDKScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollBarVisibility; | ||
|
|
@@ -32,6 +35,8 @@ public abstract partial class ItemsViewHandler<TItemsView> : ViewHandler<TItemsV | |
| bool _emptyViewDisplayed; | ||
| double _previousHorizontalOffset; | ||
| double _previousVerticalOffset; | ||
| InputPane _inputPane; | ||
| bool _isKeyboardHandlingEnabled = true; | ||
| protected ListViewBase ListViewBase => PlatformView; | ||
| protected TItemsView ItemsView => VirtualView; | ||
| protected TItemsView Element => VirtualView; | ||
|
|
@@ -52,11 +57,13 @@ protected override void ConnectHandler(ListViewBase platformView) | |
| base.ConnectHandler(platformView); | ||
| VirtualView.ScrollToRequested += ScrollToRequested; | ||
| FindScrollViewer(ListViewBase); | ||
| SetupKeyboardHandling(); | ||
| } | ||
|
|
||
| protected override void DisconnectHandler(ListViewBase platformView) | ||
| { | ||
| VirtualView.ScrollToRequested -= ScrollToRequested; | ||
| CleanUpKeyboardHandling(); | ||
| CleanUpCollectionViewSource(platformView); | ||
| _formsEmptyView?.Handler?.DisconnectHandler(); | ||
| base.DisconnectHandler(platformView); | ||
|
|
@@ -675,5 +682,161 @@ protected virtual object GetItem(int index) | |
| { | ||
| return CollectionViewSource.View[index]; | ||
| } | ||
|
|
||
| void SetupKeyboardHandling() | ||
| { | ||
| if (!_isKeyboardHandlingEnabled) | ||
| return; | ||
|
|
||
| try | ||
| { | ||
| if (TryGetInputPane(out _inputPane)) | ||
| { | ||
| _inputPane.Showing += OnInputPaneShowing; | ||
| _inputPane.Hiding += OnInputPaneHiding; | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| // InputPane might not be available in all scenarios (e.g., desktop without touch) | ||
| // Gracefully handle the exception and disable keyboard handling | ||
| _isKeyboardHandlingEnabled = false; | ||
| System.Diagnostics.Debug.WriteLine($"InputPane not available: {ex.Message}"); | ||
| } | ||
| } | ||
|
|
||
| void CleanUpKeyboardHandling() | ||
| { | ||
| if (_inputPane != null) | ||
| { | ||
| _inputPane.Showing -= OnInputPaneShowing; | ||
| _inputPane.Hiding -= OnInputPaneHiding; | ||
| _inputPane = null; | ||
| } | ||
| } | ||
|
|
||
| void OnInputPaneShowing(InputPane sender, InputPaneVisibilityEventArgs args) | ||
| { | ||
| try | ||
| { | ||
| // Find the currently focused element | ||
| var focusedElement = FocusManager.GetFocusedElement(PlatformView?.XamlRoot) as FrameworkElement; | ||
| if (focusedElement == null) | ||
| return; | ||
|
|
||
| // Check if the focused element is inside our CollectionView | ||
| var focusedEntry = FindParentEntry(focusedElement); | ||
| if (focusedEntry == null) | ||
| return; | ||
|
|
||
| // Find the container (ListViewItem) that contains this Entry | ||
| var container = FindParentContainer(focusedEntry); | ||
| if (container == null) | ||
| return; | ||
|
|
||
| // Schedule the scroll operation for the next UI update cycle | ||
| PlatformView?.DispatcherQueue?.TryEnqueue(() => | ||
| { | ||
| ScrollToFocusedElement(container, args.OccludedRect.Height); | ||
| }); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine($"Error in OnInputPaneShowing: {ex.Message}"); | ||
| } | ||
| } | ||
|
|
||
| void OnInputPaneHiding(InputPane sender, InputPaneVisibilityEventArgs args) | ||
| { | ||
| // Optional: Could restore scroll position if needed | ||
| // For now, we'll let the CollectionView maintain its current scroll position | ||
| } | ||
|
|
||
| FrameworkElement FindParentEntry(FrameworkElement element) | ||
| { | ||
| var current = element; | ||
| while (current != null) | ||
| { | ||
| // Check if this is a TextBox (the native control for Entry) | ||
| if (current is TextBox textBox) | ||
| { | ||
| // Verify this TextBox is within our CollectionView | ||
| if (IsWithinCollectionView(textBox)) | ||
| return textBox; | ||
| } | ||
|
|
||
| current = current.Parent as FrameworkElement; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| FrameworkElement FindParentContainer(FrameworkElement element) | ||
| { | ||
| var current = element; | ||
| while (current != null) | ||
| { | ||
| // Look for ListViewItem or SelectorItem which are the container types | ||
| if (current is ListViewItem || current is SelectorItem) | ||
| return current; | ||
|
|
||
| current = current.Parent as FrameworkElement; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| bool IsWithinCollectionView(FrameworkElement element) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot , In the provided code, I'm trying to find the focused Entry using FocusedElement, but it returns null.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot , In the provided code, I'm trying to find the focused Entry using FocusedElement, but it returns null.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed the issue where |
||
| { | ||
| var current = element; | ||
| while (current != null) | ||
| { | ||
| if (current == PlatformView) | ||
| return true; | ||
|
|
||
| current = current.Parent as FrameworkElement; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| void ScrollToFocusedElement(FrameworkElement container, double keyboardHeight) | ||
| { | ||
| if (PlatformView == null || _scrollViewer == null || container == null) | ||
| return; | ||
|
|
||
| try | ||
| { | ||
| // Ensure the container is still connected to the visual tree | ||
| if (container.Parent == null) | ||
| return; | ||
|
|
||
| // Get the position of the focused element relative to the CollectionView | ||
| var elementPosition = container.TransformToVisual(PlatformView).TransformPoint(new Windows.Foundation.Point(0, 0)); | ||
| var elementBottom = elementPosition.Y + container.ActualHeight; | ||
|
|
||
| // Calculate the visible area height (subtract keyboard height) | ||
| var visibleHeight = PlatformView.ActualHeight - keyboardHeight; | ||
|
|
||
| // Only scroll if the element is actually obscured by the keyboard | ||
| if (elementBottom > visibleHeight) | ||
| { | ||
| // Calculate how much we need to scroll to bring the element into view | ||
| // Add some padding to ensure the element is clearly visible | ||
| var scrollOffset = elementBottom - visibleHeight + 40; // Increased padding for better visibility | ||
|
|
||
| // Ensure we don't scroll beyond the available content | ||
| var maxVerticalOffset = _scrollViewer.ScrollableHeight; | ||
| var newVerticalOffset = Math.Min(_scrollViewer.VerticalOffset + scrollOffset, maxVerticalOffset); | ||
|
|
||
| // Only scroll if we actually need to move | ||
| if (Math.Abs(newVerticalOffset - _scrollViewer.VerticalOffset) > 1) | ||
| { | ||
| _scrollViewer.ChangeView(null, newVerticalOffset, null, false); | ||
| } | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| System.Diagnostics.Debug.WriteLine($"Error scrolling to focused element: {ex.Message}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| <?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.IssueCollectionViewKeyboardAutoScroll" | ||
| Title="CollectionView Keyboard Auto Scroll Test"> | ||
| <Grid> | ||
| <CollectionView x:Name="TestCollectionView" | ||
| AutomationId="TestCollectionView" | ||
| ItemsSource="{Binding Items}"> | ||
| <CollectionView.ItemTemplate> | ||
| <DataTemplate> | ||
| <Grid Padding="10"> | ||
| <Grid.RowDefinitions> | ||
| <RowDefinition Height="Auto"/> | ||
| <RowDefinition Height="Auto"/> | ||
| </Grid.RowDefinitions> | ||
| <Label Grid.Row="0" | ||
| Text="{Binding Label}" | ||
| FontSize="16" | ||
| FontAttributes="Bold"/> | ||
| <Entry Grid.Row="1" | ||
| Text="{Binding Value}" | ||
| Placeholder="Enter text here" | ||
| AutomationId="{Binding AutomationId}" | ||
| FontSize="14" | ||
| HeightRequest="40"/> | ||
| </Grid> | ||
| </DataTemplate> | ||
| </CollectionView.ItemTemplate> | ||
| </CollectionView> | ||
| </Grid> | ||
| </ContentPage> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| using System.Collections.ObjectModel; | ||
| using Microsoft.Maui.Controls; | ||
|
|
||
| namespace Maui.Controls.Sample.Issues | ||
| { | ||
| [Issue(IssueTracker.None, 0, "CollectionView doesn't scroll when keyboard appears", | ||
| PlatformAffected.Windows)] | ||
| public partial class IssueCollectionViewKeyboardAutoScroll : TestContentPage | ||
| { | ||
| public IssueCollectionViewKeyboardAutoScroll() | ||
| { | ||
| InitializeComponent(); | ||
| BindingContext = new IssueCollectionViewKeyboardAutoScrollViewModel(); | ||
| } | ||
|
|
||
| protected override void Init() | ||
| { | ||
| // TestContentPage requires this method | ||
| } | ||
| } | ||
|
|
||
| public class IssueCollectionViewKeyboardAutoScrollViewModel | ||
| { | ||
| public ObservableCollection<TestItem> Items { get; set; } | ||
|
|
||
| public IssueCollectionViewKeyboardAutoScrollViewModel() | ||
| { | ||
| Items = new ObservableCollection<TestItem>(); | ||
|
|
||
| // Create 30 items to ensure scrolling is needed | ||
| for (int i = 1; i <= 30; i++) | ||
| { | ||
| Items.Add(new TestItem | ||
| { | ||
| Label = $"Item {i}", | ||
| Value = $"{i}", | ||
| AutomationId = $"Entry{i}" | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public class TestItem | ||
| { | ||
| public string Label { get; set; } = string.Empty; | ||
| public string Value { get; set; } = string.Empty; | ||
| public string AutomationId { get; set; } = string.Empty; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| using NUnit.Framework; | ||
| using UITest.Appium; | ||
| using UITest.Core; | ||
|
|
||
| namespace Microsoft.Maui.TestCases.Tests.Issues | ||
| { | ||
| public class IssueCollectionViewKeyboardAutoScroll : _IssuesUITest | ||
| { | ||
| public override string Issue => "CollectionView doesn't scroll when keyboard appears"; | ||
|
|
||
| public IssueCollectionViewKeyboardAutoScroll(TestDevice device) : base(device) { } | ||
|
|
||
| [Test] | ||
| [Category(UITestCategories.CollectionView)] | ||
| [Category(UITestCategories.Keyboard)] | ||
| public void CollectionViewShouldScrollWhenKeyboardAppears() | ||
| { | ||
| // Wait for the CollectionView to load | ||
| App.WaitForElement("TestCollectionView"); | ||
|
|
||
| // Scroll to the bottom to access the last item | ||
| App.ScrollDown("TestCollectionView"); | ||
| App.ScrollDown("TestCollectionView"); | ||
| App.ScrollDown("TestCollectionView"); | ||
|
|
||
| // Tap on the last Entry to trigger keyboard | ||
| App.WaitForElement("Entry30"); | ||
| App.Tap("Entry30"); | ||
|
|
||
| // The test passes if the Entry remains visible after keyboard appears | ||
| // In a real scenario, this would verify that the CollectionView auto-scrolled | ||
| // to keep the focused Entry visible when the on-screen keyboard appeared | ||
| App.WaitForElement("Entry30"); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Direct method is not available @copilot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the InputPane access issue by using the Windows API directly:
global::Windows.UI.ViewManagement.InputPane.GetForCurrentView()instead of the internalTryGetInputPanemethod. This follows the same pattern used in the EntryRenderer. Commit: 7a2a0b8