Skip to content

Commit 19d6fec

Browse files
Merge pull request #34 from Unity3dAzure/iss29
∞ Infinite scrolling for Unity TSTableView. Resolves #29
2 parents ea0a9ae + 1082fd8 commit 19d6fec

File tree

7 files changed

+163
-73
lines changed

7 files changed

+163
-73
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
UWP/
99
Assets/*.pfx
1010

11+
# Builds
12+
*.unitypackage
13+
14+
# Android
15+
*.apk
16+
1117
# Autogenerated VS/MD solution and project files
1218
*.csproj
1319
*.unityproj
@@ -18,6 +24,10 @@ Assets/*.pfx
1824
*.userprefs
1925
*.pidb
2026
*.booproj
27+
*.svd
28+
29+
# Source control temp files
30+
*.orig
2131

2232
# Unity3D generated meta files in directories
2333
*.pidb.meta

Assets/AppServices/table/query/CustomQuery.cs

Lines changed: 59 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
using UnityEngine;
44

55
/// <summary>
6-
/// Query records operation https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx
6+
/// Implemention of Query records operation https://msdn.microsoft.com/en-us/library/azure/jj677199.aspx
77
/// There is a maximum of 50 records returned in a query - use top and skip params to return additional pages of results.
8-
/// NB: `$inlinecount` (which returns count of all items without paging applied) is not set here as it changes the data model and the way the REST decode callback works makes it intangible to decode.
9-
/// Rather the '$inlinecount=allpages' param is automically set when using the table's Query method and wrapping your data model with the NestedResults object wrapper.
8+
/// NB: `$inlinecount` (which returns count of all items without paging applied) is not set here as it changes the data model shape and the way the REST decode callback works makes it non-trival to decode.
9+
/// Rather the '$inlinecount=allpages' param is automatically set when using the table's Query method and wrapping your data model with the NestedResults object wrapper.
1010
/// </summary>
1111
namespace Unity3dAzure.AppServices
1212
{
@@ -20,74 +20,75 @@ public enum MobileServiceSystemProperty
2020
deleted = 0x8
2121
}
2222

23-
[CLSCompliant(false)]
24-
public class CustomQuery
25-
{
23+
[CLSCompliant (false)]
24+
public class CustomQuery
25+
{
2626
// query option parameters defined by the Open Data Protocol (OData)
27-
private string _filter;
28-
private string _orderBy;
29-
private uint _top;
30-
private uint _skip;
31-
private string _select;
27+
public string Filter;
28+
public string OrderBy;
29+
public uint Top;
30+
public uint Skip;
31+
public string Select;
3232
// other params
33-
private MobileServiceSystemProperty _systemProperties;
34-
private bool _includeDeleted;
33+
public MobileServiceSystemProperty SystemProperties;
34+
public bool IncludeDeleted;
3535

36-
public CustomQuery(string filter, string orderBy=null, uint top=0, uint skip=0, string select=null, MobileServiceSystemProperty systemProperties=MobileServiceSystemProperty.nil, bool includeDeleted=false)
37-
{
38-
_filter = filter; // return only rows that satisfy the specified filter predicate
39-
_orderBy = orderBy; // sort column by one or more columns: order can be specified in 'desc' or 'asc' order ('asc' is default)
40-
_top = top; // return the top n entities for any query
41-
_skip = skip; // the n of records to skip (used for paging results)
42-
_select = select; // defines new projection of data by specifying the columns
43-
_systemProperties = systemProperties; // list of system properties to be included in the response
44-
_includeDeleted = includeDeleted; // if table has soft delete enabled then deleted records will be included in the results
45-
}
36+
public CustomQuery (string filter = "", string orderBy = null, uint top = 0, uint skip = 0, string select = null, MobileServiceSystemProperty systemProperties = MobileServiceSystemProperty.nil, bool includeDeleted = false)
37+
{
38+
this.Filter = filter; // return only rows that satisfy the specified filter predicate
39+
this.OrderBy = orderBy; // sort column by one or more columns: order can be specified in 'desc' or 'asc' order ('asc' is default)
40+
this.Top = top; // return the top n entities for any query
41+
this.Skip = skip; // the n of records to skip (used for paging results)
42+
this.Select = select; // defines new projection of data by specifying the columns
43+
this.SystemProperties = systemProperties; // list of system properties to be included in the response
44+
this.IncludeDeleted = includeDeleted; // if table has soft delete enabled then deleted records will be included in the results
45+
}
4646

47-
public static CustomQuery OrderBy(string orderBy) {
48-
return new CustomQuery("", orderBy);
47+
public static CustomQuery CreateWithOrderBy (string orderBy)
48+
{
49+
return new CustomQuery ("", orderBy);
4950
}
5051

51-
public override string ToString()
52-
{
53-
string queryString = "";
54-
string q = "?";
55-
if (!string.IsNullOrEmpty(_filter)) {
56-
queryString += string.Format("{0}$filter=({1})", q, _filter);
57-
q = "&";
58-
}
59-
if (!string.IsNullOrEmpty(_orderBy)) {
60-
queryString += string.Format("{0}$orderby={1}", q, _orderBy);
61-
q = "&";
62-
}
63-
if (_top > 0) {
64-
queryString += string.Format("{0}$top={1}", q, _top.ToString());
52+
public override string ToString ()
53+
{
54+
string queryString = "";
55+
string q = "?";
56+
if (!string.IsNullOrEmpty (this.Filter)) {
57+
queryString += string.Format ("{0}$filter=({1})", q, this.Filter);
58+
q = "&";
59+
}
60+
if (!string.IsNullOrEmpty (this.OrderBy)) {
61+
queryString += string.Format ("{0}$orderby={1}", q, this.OrderBy);
6562
q = "&";
6663
}
67-
if (_skip > 0) {
68-
queryString += string.Format("{0}$skip={1}", q, _skip.ToString());
64+
if (this.Top > 0) {
65+
queryString += string.Format ("{0}$top={1}", q, this.Top.ToString ());
6966
q = "&";
7067
}
71-
if (!string.IsNullOrEmpty(_select)) {
72-
queryString += string.Format("{0}$select={1}", q, _select);
68+
if (this.Skip > 0) {
69+
queryString += string.Format ("{0}$skip={1}", q, this.Skip.ToString ());
7370
q = "&";
7471
}
75-
if (_systemProperties!=MobileServiceSystemProperty.nil) {
72+
if (!string.IsNullOrEmpty (this.Select)) {
73+
queryString += string.Format ("{0}$select={1}", q, this.Select);
74+
q = "&";
75+
}
76+
if (this.SystemProperties != MobileServiceSystemProperty.nil) {
7677
// NB: setting __systemproperties param doesn't seem to do anything different as these properties are all included by default, but we can append values to the 'select' param.
77-
if (!string.IsNullOrEmpty (_select)) {
78-
queryString += string.Format (",{0}", SystemPropertiesValues (_systemProperties));
78+
if (!string.IsNullOrEmpty (this.Select)) {
79+
queryString += string.Format (",{0}", SystemPropertiesValues (this.SystemProperties));
7980
}
80-
queryString += string.Format("{0}__systemproperties={1}", q, SystemPropertiesValues(_systemProperties));
81+
queryString += string.Format ("{0}__systemproperties={1}", q, SystemPropertiesValues (this.SystemProperties));
8182
q = "&";
8283
}
83-
if (_includeDeleted) {
84-
queryString += string.Format("{0}__includeDeleted=true", q);
84+
if (this.IncludeDeleted) {
85+
queryString += string.Format ("{0}__includeDeleted=true", q);
8586
}
86-
return EscapeURL(queryString);
87-
}
87+
return EscapeURL (queryString);
88+
}
8889

89-
private string EscapeURL(string query)
90-
{
90+
private string EscapeURL (string query)
91+
{
9192
string q = WWW.EscapeURL (query);
9293
StringBuilder sb = new StringBuilder (q);
9394
sb.Replace ("+", "%20"); // NB: replace space with '%20' instead of '+'
@@ -96,15 +97,15 @@ private string EscapeURL(string query)
9697
sb.Replace ("%26", "&");
9798
sb.Replace ("%3d", "=");
9899
sb.Replace ("%24", "$");
99-
return sb.ToString();
100-
}
100+
return sb.ToString ();
101+
}
101102

102-
private string SystemPropertiesValues(MobileServiceSystemProperty systemProperties)
103+
private string SystemPropertiesValues (MobileServiceSystemProperty systemProperties)
103104
{
104105
if (systemProperties == MobileServiceSystemProperty.nil) {
105106
return "";
106107
}
107-
return systemProperties.ToString().Replace(" ",""); // remove spaces from string
108+
return systemProperties.ToString ().Replace (" ", ""); // remove spaces from string
108109
}
109-
}
110+
}
110111
}

Assets/Scenes/HighscoresDemo.unity

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3254,7 +3254,7 @@ MonoBehaviour:
32543254
m_HandleRect: {fileID: 1064423869}
32553255
m_Direction: 0
32563256
m_Value: 0
3257-
m_Size: 1
3257+
m_Size: 0.9999999
32583258
m_NumberOfSteps: 0
32593259
m_OnValueChanged:
32603260
m_PersistentCalls:

Assets/Scripts/HighscoresDemo.cs

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ public class HighscoresDemo : MonoBehaviour, ITableViewDataSource
4646
private ScoreCell _cellPrefab;
4747
bool HasNewData = false; // to reload table view when data has changed
4848

49+
// infinite scroll vars
50+
private bool _isPaginated = false; // only enable infinite scrolling for paginated results
51+
private const float _infiniteScrollSize = 0.2f;
52+
private bool _isLoadingNextPage = false; // load once when scroll buffer is hit
53+
private const uint _noPageResults = 50;
54+
private uint _skip = 0; // no of records to skip
55+
private uint _totalCount = 0; // count value should be > 0 to paginate
56+
4957
[Space(10)]
5058
[SerializeField]
5159
private ModalAlert _modalAlert;
@@ -79,7 +87,9 @@ void Update ()
7987
// Only update table when there is new data
8088
if (HasNewData) {
8189
Debug.Log ("Refresh Table Data");
90+
SetInteractableScrollbars (false);
8291
_tableView.ReloadData ();
92+
SetInteractableScrollbars (true);
8393
HasNewData = false;
8494
}
8595
// Display new score details
@@ -193,6 +203,7 @@ private void OnReadCompleted(IRestResponse<List<Highscore>> response)
193203
Debug.Log("OnReadCompleted data: " + response.ResponseUri +" data: "+ response.Content);
194204
List<Highscore> items = response.Data;
195205
Debug.Log("Read items count: " + items.Count);
206+
_isPaginated = false; // default query has max. of 50 records and is not paginated so disable infinite scroll
196207
_scores = items;
197208
HasNewData = true;
198209
}
@@ -208,26 +219,39 @@ private void OnReadNestedResultsCompleted(IRestResponse<NestedResults<Highscore>
208219
{
209220
Debug.Log("OnReadNestedResultsCompleted: " + response.ResponseUri +" data: "+ response.Content);
210221
List<Highscore> items = response.Data.results;
222+
_totalCount = response.Data.count;
211223
Debug.Log("Read items count: " + items.Count + "/" + response.Data.count);
212-
_scores = items;
224+
_isPaginated = true; // nested query will support pagination
225+
if (_skip != 0) {
226+
_scores.AddRange (items); // append results for paginated results
227+
} else {
228+
_scores = items; // set for first page of results
229+
}
213230
HasNewData = true;
214231
}
215232
else
216233
{
217234
Debug.Log("Read Nested Results Error Status:" + response.StatusCode + " Uri: "+response.ResponseUri );
218235
}
236+
_isLoadingNextPage = false; // allows next page to be loaded
219237
}
220238

221239
public void GetAllHighscores()
222240
{
223-
CustomQuery query = new CustomQuery ("", "score desc", 50, 0, "id,username,score"); //CustomQuery.OrderBy ("score desc");
224-
_table.Query<NestedResults<Highscore>>(query, OnReadNestedResultsCompleted); //Query(query);
241+
// reset
242+
_skip = 0;
243+
GetPageHighscores();
225244
}
226245

246+
private void GetPageHighscores()
247+
{
248+
CustomQuery query = new CustomQuery ("", "score desc", _noPageResults, _skip, "id,username,score"); //CustomQuery.OrderBy ("score desc");
249+
_table.Query<NestedResults<Highscore>>(query, OnReadNestedResultsCompleted); //Query(query);
250+
}
227251

228252
public void GetTopHighscores()
229253
{
230-
DateTime today = DateTime.Today.AddDays(-1);
254+
DateTime today = DateTime.Today;
231255
string day = today.ToString("s");
232256
string filter = string.Format("createdAt gt '{0}Z'", day); //string.Format("score gt {0}", 999);
233257
Debug.Log ("filter:" + filter);
@@ -439,7 +463,7 @@ private void UpdateUI()
439463

440464
#endregion
441465

442-
#region ITableViewDataSource
466+
#region TSTableView ITableViewDataSource
443467

444468
public int GetNumberOfRowsForTableView(TableView tableView)
445469
{
@@ -481,6 +505,56 @@ public void OnSelectedRow(Button button) {
481505
_score = score; // update editor with selected item
482506
}
483507

508+
#region Infinite Scroll private methods for Highscores
509+
510+
void OnEnable()
511+
{
512+
_tableView.GetComponent<ScrollRect>().onValueChanged.AddListener(OnScrollValueChanged);
513+
}
514+
515+
void OnDisable()
516+
{
517+
_tableView.GetComponent<ScrollRect>().onValueChanged.RemoveListener(OnScrollValueChanged);
518+
}
519+
520+
private void OnScrollValueChanged(Vector2 newScrollValue)
521+
{
522+
// skip if not paginated results, or if no items, or if busy already loading next page of results
523+
if (!_isPaginated || _totalCount==0 || _isLoadingNextPage)
524+
{
525+
return;
526+
}
527+
//Debug.Log (string.Format("Scroll y:{0} table view scroll: {1}/{2}", newScrollValue.y, _tableView.scrollY, _tableView.scrollableHeight));
528+
float scrollY = _tableView.scrollableHeight - _tableView.scrollY;
529+
float scrollBuffer = _infiniteScrollSize * _tableView.scrollableHeight;
530+
// scrollY is still at 'top' and so no need to load anything at this point
531+
if (scrollY > scrollBuffer)
532+
{
533+
return;
534+
}
535+
// scrollY has reached 'bottom' minus buffer size
536+
// only trigger request if there are more records to load
537+
if (_skip < _totalCount)
538+
{
539+
_isLoadingNextPage = true;
540+
_skip += _noPageResults;
541+
//Debug.Log (string.Format("Load next page @{0} scroll: {1}<{2}", _skip, scrollY, scrollBuffer));
542+
GetPageHighscores ();
543+
}
544+
}
545+
546+
// Tip: When infinite scrolling and using TSTableView's ReloadData() method I prefer to wrap "disable and enable scrollbar" calls around it to help prevent jumpy behaviour when continously dragging the scrollbar thumb.
547+
private void SetInteractableScrollbars(bool isInteractable)
548+
{
549+
Scrollbar[] scrollbars = _tableView.GetComponentsInChildren<Scrollbar> ();
550+
foreach (Scrollbar scrollbar in scrollbars) {
551+
//Debug.Log (string.Format("Scrollbar {0} is {1}",scrollbar.name, isInteractable));
552+
scrollbar.interactable = isInteractable;
553+
}
554+
}
555+
556+
#endregion
557+
484558
/// <summary>
485559
/// Handler to go to next scene
486560
/// </summary>

Assets/TSTableView/TableView.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ public void ReloadData() {
7272
this.isEmpty = m_rowHeights.Length == 0;
7373
ClearAllRows();
7474
if (this.isEmpty) {
75+
// reset content size when empty
76+
var rect = m_scrollRect.content.GetComponent<RectTransform> ();
77+
rect.localPosition = Vector2.zero;
78+
rect.sizeDelta = Vector2.zero;
79+
m_requiresReload = false;
7580
return;
7681
}
7782
m_cumulativeRowHeights = new float[m_rowHeights.Length];

ProjectSettings/GraphicsSettings.asset

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ GraphicsSettings:
4444
useReflectionProbeBoxProjection: 0
4545
useReflectionProbeBlending: 0
4646
m_ShaderSettings_Tier2:
47-
useCascadedShadowMaps: 1
48-
standardShaderQuality: 2
49-
useReflectionProbeBoxProjection: 1
50-
useReflectionProbeBlending: 1
47+
useCascadedShadowMaps: 0
48+
standardShaderQuality: 1
49+
useReflectionProbeBoxProjection: 0
50+
useReflectionProbeBlending: 0
5151
m_ShaderSettings_Tier3:
52-
useCascadedShadowMaps: 1
53-
standardShaderQuality: 2
54-
useReflectionProbeBoxProjection: 1
55-
useReflectionProbeBlending: 1
52+
useCascadedShadowMaps: 0
53+
standardShaderQuality: 1
54+
useReflectionProbeBoxProjection: 0
55+
useReflectionProbeBlending: 0
5656
m_BuildTargetShaderSettings: []
5757
m_LightmapStripping: 0
5858
m_FogStripping: 0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Contains a Unity 5 project featuring two demo scenes for Azure App Services (pre
88
* Client-directed login with Facebook
99
* Insert Highscore
1010
* Update Highscore
11-
* Read list of Highscores (hall of fame)
11+
* Read list of Highscores using infinite scrolling (hall of fame)
1212
* Query for today's top ten highscores (daily leaderboard)
1313
* Query for username (user's scores)
1414

0 commit comments

Comments
 (0)