Skip to content
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue30081.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 30081, "[Android] ScrollView scroll position changes unexpectedly when Orientation is set to Horizontal and FlowDirection is RTL at runtime", PlatformAffected.Android)]

public class Issue30081 : ContentPage
{
readonly ScrollViewViewModel _viewModel;

public Issue30081()
{
_viewModel = new ScrollViewViewModel();
BindingContext = _viewModel;

// Create Grid (same as XAML structure)
var grid = new Grid
{
RowDefinitions =
{
new RowDefinition { Height = GridLength.Star },
new RowDefinition { Height = GridLength.Auto }
}
};

// Create ContentView and bind its Content
var scrollViewContent = new ContentView
{
AutomationId = "ScrollViewContent"
};
scrollViewContent.SetBinding(ContentView.ContentProperty, nameof(ScrollViewViewModel.Content));

// Create ScrollView and bind its Orientation
var scrollView = new ScrollView
{
Content = scrollViewContent,
FlowDirection = FlowDirection.RightToLeft
};
scrollView.SetBinding(ScrollView.OrientationProperty, nameof(ScrollViewViewModel.Orientation));

// Add ScrollView to Grid row 0
grid.Add(scrollView);
Grid.SetRow(scrollView, 0);

// Create Button
var button = new Button
{
Text = "Toggle Orientation",
AutomationId = "ToggleOrientationButton"
};
button.Clicked += Button_Clicked;

// Add Button to Grid row 1
grid.Add(button);
Grid.SetRow(button, 1);

// Set grid as the page content
Content = grid;
}

void Button_Clicked(object sender, EventArgs e)
{
if (_viewModel.Orientation == ScrollOrientation.Vertical)
_viewModel.Orientation = ScrollOrientation.Horizontal;
else
_viewModel.Orientation = ScrollOrientation.Vertical;
}
}

public class ScrollViewViewModel : INotifyPropertyChanged
{
string _contentText;
ScrollOrientation _orientation = ScrollOrientation.Vertical;

public ScrollOrientation Orientation
{
get => _orientation;
set
{
if (_orientation != value)
{
_orientation = value;
OnPropertyChanged();
}
}
}

View _content;

public ScrollViewViewModel()
{
_contentText = string.Empty;
Content = new Label
{
Text = string.Join(Environment.NewLine, Enumerable.Range(1, 100).Select(i => $"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, urna eu tincidunt consectetur, nisi nisl aliquam enim, eget facilisis enim nisl nec elit . Sed euismod, urna eu tincidunt consectetur, nisi nisl aliquam enim Eget facilisis enim nisl nec elit Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae. Nullam ac erat at dui laoreet aliquet. Praesent euismod, justo at dictum facilisis, urna erat dictum enim. {i}")),
FontSize = 18,
Padding = 10
};
}

public string ContentText
{
get => _contentText;
set
{
if (_contentText != value)
{
_contentText = value;
Content = new Label { Text = _contentText }; // Update Content when ContentText changes
OnPropertyChanged();
}
}
}

public View Content
{
get => _content;
set
{
if (_content != value)
{
_content = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged([CallerMemberName] string propertyName = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST // More Info: https://github.com/dotnet/maui/issues/32271
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue30081 : _IssuesUITest
{
public Issue30081(TestDevice device) : base(device) { }

public override string Issue => "[Android] ScrollView scroll position changes unexpectedly when Orientation is set to Horizontal and FlowDirection is RTL at runtime";
[Test]
[Category(UITestCategories.ScrollView)]
public void VerifyHorizontalScrollViewPositionAtRuntime()
{
App.WaitForElement("ToggleOrientationButton");
App.Tap("ToggleOrientationButton");
VerifyScreenshot();
}
}
#endif
8 changes: 8 additions & 0 deletions src/Core/src/Handlers/ScrollView/ScrollViewHandler.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ public static void MapOrientation(IScrollViewHandler handler, IScrollView scroll
handler.PlatformView.SetOrientation(scrollView.Orientation);
}

internal static void MapFlowDirection(IScrollViewHandler handler, IScrollView scrollView)
{
if (handler.PlatformView is MauiScrollView mauiScrollView && scrollView is IView view)
{
mauiScrollView.UpdateFlowDirection(view);
}
}

public static void MapRequestScrollTo(IScrollViewHandler handler, IScrollView scrollView, object? args)
{
if (args is not ScrollToRequest request)
Expand Down
3 changes: 3 additions & 0 deletions src/Core/src/Handlers/ScrollView/ScrollViewHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public partial class ScrollViewHandler : IScrollViewHandler
[nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility,
[nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility,
[nameof(IScrollView.Orientation)] = MapOrientation,
#if ANDROID
[nameof(IView.FlowDirection)] = MapFlowDirection,
#endif
#if __IOS__
[nameof(IScrollView.IsEnabled)] = MapIsEnabled,
#endif
Expand Down
47 changes: 47 additions & 0 deletions src/Core/src/Platform/Android/MauiScrollView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Android.Widget;
using AndroidX.Core.View;
using AndroidX.Core.Widget;
using ALayoutDirection = Android.Views.LayoutDirection;

namespace Microsoft.Maui.Platform
{
Expand All @@ -26,6 +27,8 @@ public class MauiScrollView : NestedScrollView, IScrollBarView, NestedScrollView
bool _didSafeAreaEdgeConfigurationChange = true;
bool _isInsetListenerSet;
Java.Lang.IRunnable? _setAppBarLiftTargetRunnable;
ALayoutDirection _prevLayoutDirection = ALayoutDirection.Ltr;
bool _checkedForRtlScroll;

internal float LastX { get; set; }
internal float LastY { get; set; }
Expand Down Expand Up @@ -224,6 +227,12 @@ public void SetOrientation(ScrollOrientation orientation)
bool orientationChanged = _scrollOrientation != orientation;
_scrollOrientation = orientation;

// Reset RTL tracking when orientation changes
if (orientationChanged)
{
_checkedForRtlScroll = false;
}

if (orientation == ScrollOrientation.Horizontal || orientation == ScrollOrientation.Both)
{
if (_hScrollView == null)
Expand Down Expand Up @@ -268,6 +277,28 @@ public void SetOrientation(ScrollOrientation orientation)
}
}

internal void UpdateFlowDirection(IView view)
{
var layoutDirection = ViewExtensions.GetLayoutDirection(view);

// Handle FlowDirection specifically for horizontal scroll view
if (_hScrollView != null && _scrollOrientation == ScrollOrientation.Horizontal)
{
if (_prevLayoutDirection != layoutDirection)
{
_prevLayoutDirection = layoutDirection;
_hScrollView.LayoutDirection = layoutDirection;
_checkedForRtlScroll = false; // Reset to allow re-evaluation
}
}
else
{
// Fallback to default mechanism for other cases (vertical scroll or no horizontal scroll)
// Use the common ViewExtensions logic for standard FlowDirection handling
this.LayoutDirection = layoutDirection;
}
}

public override bool OnInterceptTouchEvent(MotionEvent? ev)
{
// See also MauiHorizontalScrollView notes in OnInterceptTouchEvent
Expand Down Expand Up @@ -369,6 +400,21 @@ protected override void OnLayout(bool changed, int left, int top, int right, int
_hScrollView.Layout(0, 0, hScrollViewWidth, hScrollViewHeight);
}

// Handle RTL initial positioning
if (!_checkedForRtlScroll && _hScrollView != null && _scrollOrientation == ScrollOrientation.Horizontal)
{
if (_hScrollView.LayoutDirection == ALayoutDirection.Rtl)
{
Post(() =>
{
// Scroll to the right end for RTL
_hScrollView?.ScrollTo(_hScrollView?.GetChildAt(0)?.Width ?? 0, 0);
});
}
}

_checkedForRtlScroll = true;

if (_didSafeAreaEdgeConfigurationChange && _isInsetListenerSet)
{
ViewCompat.RequestApplyInsets(this);
Expand Down Expand Up @@ -473,6 +519,7 @@ void SmoothScrollTo(int x, int y, Action finished)
void IOnScrollChangeListener.OnScrollChange(NestedScrollView? v, int scrollX, int scrollY, int oldScrollX, int oldScrollY)
#pragma warning restore CA1822
{
_checkedForRtlScroll = true;
OnScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Core/src/Platform/Android/ViewExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ public static void UpdateFlowDirection(this AView platformView, IView view)
platformView.LayoutDirection = GetLayoutDirection(view);
}

static ALayoutDirection GetLayoutDirection(IView view)
internal static ALayoutDirection GetLayoutDirection(IView view)
{
return view.FlowDirection switch
{
Expand Down
Loading