Skip to content
This repository was archived by the owner on May 1, 2024. It is now read-only.

Commit 780efca

Browse files
techdugguDurgesh Khandal
andauthored
[Enhancement] ListView ScrollTo with empty groups - Android (#8310)f ixes #8279
* Fixed - 8279 - [Enhancement] ListView ScrollTo with empty groups * Added UI Tests for 8279 * Update Issue8279.cs * Fixed build error - finally * Added reset button to fix UI test * Added meaningful local variables for better code readability * Added meaningful names to local variables and removed unused condition Co-authored-by: Durgesh Khandal <[email protected]>
1 parent 6b816f7 commit 780efca

File tree

2 files changed

+266
-12
lines changed

2 files changed

+266
-12
lines changed
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.ComponentModel;
5+
using System.Linq;
6+
7+
using Xamarin.Forms.CustomAttributes;
8+
using Xamarin.Forms.Internals;
9+
#if UITEST
10+
using Xamarin.Forms.Core.UITests;
11+
using Xamarin.UITest;
12+
using NUnit.Framework;
13+
#endif
14+
15+
namespace Xamarin.Forms.Controls.Issues
16+
{
17+
[Preserve (AllMembers=true)]
18+
[Issue (IssueTracker.Github, 8279, "[Feature requested] ListView do not ScrollTo a group when there is no child of this group", PlatformAffected.Android)]
19+
public class Issue8279 : TestContentPage
20+
{
21+
public static ListView List { get; set; }
22+
public static List<MyGroup> Data { get; set; }
23+
const string ScrollWithNoItemButGroup = "ScrollWithNoItemButGroup";
24+
const string ScrollWithItemButNoGroup = "ScrollWithItemButNoGroup";
25+
const string ScrollWithItemWithGroup = "ScrollWithItemWithGroup";
26+
const string ScrollWithNoItemNoGroup = "ScrollWithNoItemNoGroup";
27+
const string ScrollWithNoItemEmptyGroup = "ScrollWithNoItemEmptyGroup";
28+
const string Reset = "Reset";
29+
30+
protected override void Init()
31+
{
32+
33+
}
34+
35+
public Issue8279()
36+
{
37+
#if APP
38+
Data = new List<MyGroup>();
39+
Data.Add(new MyGroup(){Headertitle = "Header 1"});
40+
Data.First().Add(new MyData(){Title = "title 1"});
41+
Data.First().Add(new MyData() { Title = "title 2" });
42+
Data.First().Add(new MyData() { Title = "title 3" });
43+
Data.First().Add(new MyData() { Title = "title 4" });
44+
Data.First().Add(new MyData() { Title = "title 5" });
45+
Data.First().Add(new MyData() { Title = "title 6" });
46+
Data.First().Add(new MyData() { Title = "title 7" });
47+
Data.First().Add(new MyData() { Title = "title 8" });
48+
Data.Add(new MyGroup() { Headertitle = "Header 2" });
49+
Data.Add(new MyGroup() { Headertitle = "Header 3" });
50+
Data.Last().Add(new MyData() { Title = "title 3a" });
51+
Data.Last().Add(new MyData() { Title = "title 3b" });
52+
Data.Last().Add(new MyData() { Title = "title 3b" });
53+
Data.Last().Add(new MyData() { Title = "title 3b" });
54+
55+
List = new ListView();
56+
List.HorizontalOptions = LayoutOptions.FillAndExpand;
57+
List.VerticalOptions = LayoutOptions.FillAndExpand;
58+
List.BackgroundColor = Color.Yellow;
59+
List.ItemTemplate = new DataTemplate(typeof (VCTest));
60+
List.GroupHeaderTemplate = new DataTemplate(typeof(VCHeader));
61+
List.IsGroupingEnabled = true;
62+
List.ItemsSource = Data;
63+
64+
var lastGroup = Data.Last();
65+
var lastItem = lastGroup.First();
66+
var firstGroup = Data.First();
67+
var firstItem = firstGroup.First();
68+
var emptyGroup = Data[1];
69+
70+
var button1 = new Button()
71+
{
72+
Text = "Scroll with no item but group",
73+
AutomationId = ScrollWithNoItemButGroup,
74+
Command = new Command(()=> List.ScrollTo(null, lastGroup, ScrollToPosition.MakeVisible, true))
75+
};
76+
77+
var button2 = new Button()
78+
{
79+
Text = "Scroll with item but no group",
80+
AutomationId = ScrollWithItemButNoGroup,
81+
Command = new Command(() => List.ScrollTo(firstItem, ScrollToPosition.MakeVisible, true))
82+
};
83+
84+
var button3 = new Button()
85+
{
86+
Text = "Scroll with item with group",
87+
AutomationId = ScrollWithItemWithGroup,
88+
Command = new Command(() => List.ScrollTo(firstItem, lastGroup, ScrollToPosition.MakeVisible, true))
89+
};
90+
91+
var button4 = new Button()
92+
{
93+
Text = "Scroll with no item no group",
94+
AutomationId = ScrollWithNoItemNoGroup,
95+
Command = new Command(() => List.ScrollTo(null, null, ScrollToPosition.MakeVisible, true))
96+
};
97+
98+
var button5 = new Button()
99+
{
100+
Text = "Scroll with no item but empty group",
101+
AutomationId = ScrollWithNoItemEmptyGroup,
102+
Command = new Command(() => List.ScrollTo(null, emptyGroup, ScrollToPosition.MakeVisible, true))
103+
};
104+
105+
var resetButton = new Button()
106+
{
107+
Text = "Reset",
108+
AutomationId = Reset,
109+
Command = new Command(() => List.ScrollTo(null, firstGroup, ScrollToPosition.Center, true))
110+
};
111+
112+
Content = new StackLayout () {
113+
VerticalOptions = LayoutOptions.StartAndExpand,
114+
HorizontalOptions = LayoutOptions.FillAndExpand,
115+
Children = { resetButton, button1, button2, button3, button4, button5, List },
116+
};
117+
#endif
118+
}
119+
120+
#if UITEST
121+
[Test]
122+
public void ScrollWithNoItemButGroupTest()
123+
{
124+
RunningApp.WaitForElement(Reset);
125+
RunningApp.Tap(Reset);
126+
RunningApp.WaitForElement(ScrollWithNoItemButGroup);
127+
RunningApp.Tap(ScrollWithNoItemButGroup);
128+
// This will fail if the list didn't scroll. If it did scroll, it will succeed
129+
RunningApp.WaitForElement(q => q.Marked("Header 3"), timeout: TimeSpan.FromSeconds(2));
130+
}
131+
132+
[Test]
133+
public void ScrollWithItemButNoGroupTest()
134+
{
135+
RunningApp.WaitForElement(Reset);
136+
RunningApp.Tap(Reset);
137+
RunningApp.WaitForElement(ScrollWithItemButNoGroup);
138+
RunningApp.Tap(ScrollWithItemButNoGroup);
139+
// This will fail if the list didn't scroll. If it did scroll, it will succeed
140+
RunningApp.WaitForElement(q => q.Marked("title 1"), timeout: TimeSpan.FromSeconds(2));
141+
}
142+
143+
[Test]
144+
public void ScrollWithItemWithGroupTest()
145+
{
146+
RunningApp.WaitForElement(Reset);
147+
RunningApp.Tap(Reset);
148+
RunningApp.WaitForElement(ScrollWithItemWithGroup);
149+
RunningApp.Tap(ScrollWithItemWithGroup);
150+
// This will fail if the list didn't scroll. If it did scroll, it will succeed
151+
RunningApp.WaitForElement(q => q.Marked("Header 3"), timeout: TimeSpan.FromSeconds(2));
152+
}
153+
154+
[Test]
155+
public void ScrollWithNoItemNoGroupTest()
156+
{
157+
RunningApp.WaitForElement(Reset);
158+
RunningApp.Tap(Reset);
159+
RunningApp.WaitForElement(ScrollWithNoItemNoGroup);
160+
RunningApp.Tap(ScrollWithNoItemNoGroup);
161+
// This will pass if the list didn't scroll and remain on the same state
162+
RunningApp.WaitForElement(q => q.Marked("Header 1"), timeout: TimeSpan.FromSeconds(2));
163+
}
164+
165+
[Test]
166+
public void ScrollWithNoItemEmptyGroupTest()
167+
{
168+
RunningApp.WaitForElement(Reset);
169+
RunningApp.Tap(Reset);
170+
RunningApp.WaitForElement(ScrollWithNoItemEmptyGroup);
171+
RunningApp.Tap(ScrollWithNoItemEmptyGroup);
172+
// This will fail if the list didn't scroll. If it did scroll, it will succeed
173+
RunningApp.WaitForElement(q => q.Marked("Header 2"), timeout: TimeSpan.FromSeconds(2));
174+
}
175+
#endif
176+
177+
[Preserve(AllMembers = true)]
178+
public class MyData : INotifyPropertyChanged
179+
{
180+
public event PropertyChangedEventHandler PropertyChanged;
181+
182+
string _title;
183+
184+
public const string PropTitle = "Title";
185+
186+
public string Title
187+
{
188+
get { return _title; }
189+
set
190+
{
191+
if (value.Equals(_title, StringComparison.Ordinal))
192+
return;
193+
_title = value;
194+
OnPropertyChanged(new PropertyChangedEventArgs(PropTitle));
195+
}
196+
}
197+
198+
public void OnPropertyChanged(PropertyChangedEventArgs e)
199+
{
200+
if (PropertyChanged != null)
201+
PropertyChanged(this, e);
202+
}
203+
}
204+
[Preserve(AllMembers = true)]
205+
public class MyGroup : ObservableCollection<MyData>, INotifyPropertyChanged
206+
{
207+
string _headertitle;
208+
209+
public const string PropHeadertitle = "Headertitle";
210+
211+
public string Headertitle
212+
{
213+
get { return _headertitle; }
214+
set
215+
{
216+
if (value.Equals(_headertitle, StringComparison.Ordinal))
217+
return;
218+
_headertitle = value;
219+
OnPropertyChanged(new PropertyChangedEventArgs(PropHeadertitle));
220+
}
221+
}
222+
}
223+
[Preserve(AllMembers = true)]
224+
internal class VCTest : ViewCell
225+
{
226+
public VCTest()
227+
{
228+
var label = new Label();
229+
label.SetBinding(Label.TextProperty, "Title");
230+
View = label;
231+
}
232+
}
233+
[Preserve(AllMembers = true)]
234+
internal class VCHeader : ViewCell
235+
{
236+
public VCHeader()
237+
{
238+
var label = new Label();
239+
label.SetBinding(Label.TextProperty, "Headertitle");
240+
View = label;
241+
}
242+
}
243+
}
244+
245+
}

Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,32 +273,41 @@ void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e)
273273
}
274274

275275
Cell cell;
276-
int position;
276+
int scrollPosition;
277277
var scrollArgs = (ITemplatedItemsListScrollToRequestedEventArgs)e;
278278

279279
var templatedItems = TemplatedItemsView.TemplatedItems;
280280
if (Element.IsGroupingEnabled)
281281
{
282282
var results = templatedItems.GetGroupAndIndexOfItem(scrollArgs.Group, scrollArgs.Item);
283-
if (results.Item1 == -1 || results.Item2 == -1)
283+
int indexOfGroup = results.Item1;
284+
int indexOfItem = results.Item2;
285+
286+
if (indexOfGroup == -1)
284287
return;
285288

286-
var group = templatedItems.GetGroup(results.Item1);
287-
cell = group[results.Item2];
289+
int itemIndex = indexOfItem == -1 ? 0 : indexOfItem;
290+
291+
var group = templatedItems.GetGroup(indexOfGroup);
292+
if (group.Count == 0)
293+
cell = group.HeaderContent;
294+
else
295+
cell = group[itemIndex];
288296

289-
position = templatedItems.GetGlobalIndexForGroup(group) + results.Item2 + 1;
297+
//Increment Scroll Position by 1 when Grouping is enabled. Android offsets position of cells when using header.
298+
scrollPosition = templatedItems.GetGlobalIndexForGroup(group) + itemIndex + 1;
290299
}
291300
else
292301
{
293-
position = templatedItems.GetGlobalIndexOfItem(scrollArgs.Item);
294-
if (position == -1)
302+
scrollPosition = templatedItems.GetGlobalIndexOfItem(scrollArgs.Item);
303+
if (scrollPosition == -1)
295304
return;
296305

297-
cell = templatedItems[position];
306+
cell = templatedItems[scrollPosition];
298307
}
299308

300309
//Android offsets position of cells when using header
301-
int realPositionWithHeader = position + 1;
310+
int realPositionWithHeader = scrollPosition + 1;
302311

303312
if (e.Position == ScrollToPosition.MakeVisible)
304313
{
@@ -314,11 +323,11 @@ void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e)
314323
if (cellHeight == -1)
315324
{
316325
int first = Control.FirstVisiblePosition;
317-
if (first <= position && position <= Control.LastVisiblePosition)
318-
cellHeight = Control.GetChildAt(position - first).Height;
326+
if (first <= scrollPosition && scrollPosition <= Control.LastVisiblePosition)
327+
cellHeight = Control.GetChildAt(scrollPosition - first).Height;
319328
else
320329
{
321-
AView view = _adapter.GetView(position, null, null);
330+
AView view = _adapter.GetView(scrollPosition, null, null);
322331
view.Measure(MeasureSpecFactory.MakeMeasureSpec(Control.Width, MeasureSpecMode.AtMost), MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified));
323332
cellHeight = view.MeasuredHeight;
324333
}

0 commit comments

Comments
 (0)