uno
uno copied to clipboard
Scrolling a combobox with touch swipes keeps resetting the scrolling position.
Current behavior
On an Android device or in the simulator, scrolling a combobox list down with touch swipes (or simulated touch with the mouse) frequently resets the scroll position, seemingly in an effort to bring the selected item back into view (for no reason). This makes scrolling a combobox challenging if not entirely impossible on an Android mobile device. This seems to have regressed recently as I had not noticed the issue before a recent update to latest released Uno.
Expected behavior
Scrolling to work correctly in the ComboBox list on Android.
How to reproduce it (as minimally and precisely as possible)
Run attached sample app. Main page contains a ComboBox with a few dozen items:
- Ensure first item is selected
- scroll down with a swipe from the bottom of the list up
- selected first item is now out of the viewport
- tap again toward the bottom of the list to start another swipe up
- first item scrolls back into view, cancelling the scroll
This can be repro-ed on Android in the emulator or on a device. I have not been able to repro this on WASM on a touch laptop.
Sample app: UnoApp20.zip
Video of the repro: https://github.com/unoplatform/uno/assets/15091389/4a43016a-ce24-4869-90e0-9b85800829b9
Workaround
None found so far
Works on UWP/WinUI
Yes
Environment
No response
NuGet package version(s)
Affected platforms
Android
IDE
Visual Studio 2022
IDE version
17.9.3
Relevant plugins
No response
Anything else we need to know?
I have not tested iOS.
Looks like if some pointer events are not flagged as handled properly and the popup is being dismissed.
tl;dr: to repro, scroll until selection is out of view, press-and-hold on any item container
cant repro with flick scrolling
use old-man scrolling to repro: (press,stop,drag,stop,release)x2
__^ actually, scroll however you want, stop, then press-and-hold on any item container and it'll BringIntoView the current selection
____^ you need to click on the item container; around the flyout or between item doesnt work; the faintly grayed parts on this picture:
if you dont hesitate/wait between press-(here)-drag, the issue never occur
Thanks @Xiaoy312 for looking into the repro. I can confirm that:
- Press and hold on any item with the selection out of view will repro the problem.
- Clicking/tapping outside of an item container does not repro the problem.
- Swipes without hesitation at the start (i.e., hold) do not repro the problem.
Also, I had Uno 5.1.0-dev.469 before and I was not seeing the issue then. I then upgraded to 5.1.80 and I believe that is when the problem started. I can spend some time double checking this if that is of interest.
So, the reason why the scroll resets is because we are sending two layout calls to the ScrollViewer, with different sizes. And, this causes the SV to reset on android. While the exact mechanism/purpose of this android ScrollViewer behavior is unclear, we can prevent this from the uno side by keeping size the same. This difference in size are due to the margin of ComboBox Flyout, where one calls took it into consideration and the other didn't.
In the meantime, as a workaround, you can override the default ComboBox style, and remove this line: https://github.com/unoplatform/uno/blob/5.2.80/src/Uno.UI/UI/Xaml/Style/Generic/Generic.xaml#L785 Or, clear the margin of PopupBorder from the .Loaded event of the ComboBox:
#if __ANDROID__
var combo = ...;
combo.Loaded += (s, e) =>
{
var border = combo.GetTemplateChild("PopupBorder");
border.Margin = new Thickness(0);
}
#endif
debug note
> https://github.com/unoplatform/uno/issues/16074
> Scrolling a combobox with touch swipes keeps resetting the scrolling position.
can repro on emulator:
cant repro with flick scrolling
use _old-man_ scrolling to repro: (press,drag,stop,release)x2
^ scroll however you want,stop,then press-and-hold anywhere on the combo flyout
^ and it'll BringIntoView current selection
if you dont hesitate/wait between press-(*here*)-drag, the issue never occur
# tree
Border#PopupBorder > SV > Border > SCP > NativeSCP > ItemsPresenter >
ContentControl
CarouselPanel > N*(ComboBoxItem > Grid > CP > ImplicitTextBlock)
ContentControl
Border#PopupBorder // LTRB=547,155,741,929, Actual=73.9047619047619x294.85714285714283, HV=Stretch/Stretch, CornerRadius=0, Margin=[0,-1], Padding=0, Opacity=1, Visibility=Visible
ScrollViewer#ScrollViewer // LTRB=3,3,191,771, Actual=71.9047619047619x292.85714285714283, HV=Stretch/Stretch, Offset=(0,0), Viewport=(292.95238095238096,72), Extent=(1594.952380952381,72), CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
Border // LTRB=0,0,189,769, Actual=71.9047619047619x292.85714285714283, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
ScrollContentPresenter#ScrollContentPresenter // LTRB=0,0,189,769, Actual=71.9047619047619x292.85714285714283, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
NativeScrollContentPresenter // LTRB=0,0,189,769, Margin=0, Opacity=1, Visibility=Visible
ItemsPresenter // LTRB=0,18,189,4168, Actual=72x1580.952380952381, HV=Stretch/Stretch, Margin=[0,7], Padding=0, Opacity=1, Visibility=Visible
ContentControl // LTRB=0,0,189,0, Actual=72x0, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
CarouselPanel // LTRB=0,0,189,4150, Actual=72x1580.9523809523819, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
ComboBoxItem // LTRB=0,0,189,83, Actual=72x31.61904761904762, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=[11,5,11,7], Opacity=1, Visibility=Visible
Grid#LayoutRoot // LTRB=0,0,189,83, Actual=72x31.61904761904762, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
ContentPresenter#ContentPresenter // LTRB=29,13,160,64, Actual=50x19.428571428571427, HV=Stretch/Center, CornerRadius=0, Margin=[11,5,11,7], Padding=0, Opacity=1, Visibility=Visible
ImplicitTextBlock // LTRB=0,0,131,51, Actual=39.61904761904762x19.428571428571427, HV=Stretch/Stretch, Margin=0, Padding=0, Opacity=1, Visibility=Visible
ComboBoxItem // LTRB=0,4067,189,4150, Actual=72x31.61904761904762, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=[11,5,11,7], Opacity=1, Visibility=Visible
Grid#LayoutRoot // LTRB=0,0,189,83, Actual=72x31.61904761904762, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
ContentPresenter#ContentPresenter // LTRB=29,13,160,64, Actual=50x19.428571428571427, HV=Stretch/Center, CornerRadius=0, Margin=[11,5,11,7], Padding=0, Opacity=1, Visibility=Visible
ImplicitTextBlock // LTRB=0,0,131,51, Actual=47.61904761904762x19.428571428571427, HV=Stretch/Stretch, Margin=0, Padding=0, Opacity=1, Visibility=Visible
ContentControl // LTRB=0,4150,189,4150, Actual=72x0, HV=Stretch/Stretch, CornerRadius=0, Margin=0, Padding=0, Opacity=1, Visibility=Visible
# debugging
nothing obvious from ComboBox,Selector,ScrollViewer is happening
none of Selector events trigger when it happens
SCP::OnNativeScroll fires, but we no context of the caller as it was lost crossing android-uno barrier
nooping ComboBoxItem's Pressed&PointerOver visual-states doesnt help neither
SV::BringIntoViewOnFocusChange is true // lead to nowhere?
FocusManager::GotFocus only procs once on open when focus changes between ComboBox to selected ComboBoxItem
the scrolling can be traced back from layout
there are always two layouts calls for NativeSCP every time the reset occurs
[Rect [email protected],0.0] [Rect [email protected],0.0]
^ blocking one of them prevents the issue from happening
^ the scrolling is due to a change in the viewport..?
logs:
@xy ComboBox::Arange: 411.4x659.4, [Rect [email protected],0.0], 73.9x292.9
@xy child.Arrange: "Border", [Rect [email protected],60.0]
@xy Layouter::ArrangeChildOverride // view=NativeSCP#924873582, frame=[Rect [email protected],0.0]
@xy Layouter::ArrangeChildOverride // view=NativeSCP#924873582, frame=[Rect [email protected],0.0]
Uno.UI.ViewHelper.Scale: 2.625
both layout calls are originated from
DropDownLayouter::Arrange ->
UIElement::Arrange
if (this is not FrameworkElement fwe) return; // this=Border#PopupBorder
var layouter = ((ILayouterElement)fwe).Layouter;
layouter.Arrange(finalRect); // 290
layouter.ArrangeChild(fwe, finalRect); // 292
^ the difference between 290 and 292 is from the border's margin of [0,-1]
logs:
@ margin = 0
@xy ComboBox::Arange: 411.4x659.4, [Rect [email protected],0.0], 73.9x294.9
@xy child.Arrange: "Border", [Rect [email protected],60.0]
@xy Layouter::ArrangeChildOverride // view=NativeSCP#408604404, frame=[Rect [email protected],0.0]
---
@ border-thickness = 0
@xy ComboBox::Arange: 411.4x659.4, [Rect [email protected],0.0], 73.9x292.9
@xy child.Arrange: "Border", [Rect [email protected],60.0]
@xy Layouter::ArrangeChildOverride // view=NativeSCP#929768762, frame=[Rect [email protected],0.0] // <-- X
@xy Layouter::ArrangeChildOverride // view=NativeSCP#929768762, frame=[Rect [email protected],0.0] // <-- O
^ border-thickness always takes in effect, margin is only applied to one of them
# idea
void: follow selectionchanged to trace up pointer events entry points
and from there debug pointer event(s) that deals with long-press, but not click
and see if what they dispatch
void: diff tree-graphs of two layout calls: 290 vs 292
comparison of Arrange and ArrangeChild: 16074.callstack.diff.txt