OLD | NEW |
| (Empty) |
1 // Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE.md file. | |
4 | |
5 #import "SlidingWindowPresenter.h" | |
6 | |
7 @interface SlidingWindowPresenter () | |
8 | |
9 @property id<CellPresenter> cellPresenter; | |
10 @property UITableView* tableView; | |
11 | |
12 // Is the TableView of this sliding window currently scrolling. | |
13 @property bool scrolling; | |
14 | |
15 // Internal properties for updating the sliding-window display range. | |
16 // These are not "presentation state" but are rather mostly-constant values | |
17 // representing the physical dimensions of the screen. | |
18 | |
19 // Number of items in the sliding-window display. | |
20 // Must be > 2 * bufferAdvance + |visible items on screen|. | |
21 @property int bufferCount; | |
22 | |
23 // Access to an item within 'slack' distance of the start or end of | |
24 // the sliding-window triggers a sliding-window shift. | |
25 // Must be > 1 (a zero value will result in no shifting). | |
26 @property int bufferSlack; | |
27 | |
28 // Number of items to shift the buffer by when shifting. | |
29 // Must be >= bufferSlack. | |
30 @property int bufferAdvance; | |
31 | |
32 @property SlidingWindowNode* root; | |
33 | |
34 @end | |
35 | |
36 @implementation SlidingWindowPresenter | |
37 | |
38 - (id)initWithCellPresenter:(id<CellPresenter>)presenter | |
39 tableView:(UITableView*)tableView { | |
40 self.scrolling = false; | |
41 self.cellPresenter = presenter; | |
42 self.tableView = tableView; | |
43 [self setBufferParametersBasedOnViewSize]; | |
44 return self; | |
45 } | |
46 | |
47 - (void)setBufferParametersBasedOnViewSize { | |
48 CGFloat rowHeight = self.cellPresenter.minimumCellHeight; | |
49 CGFloat tableHeight = self.tableView.bounds.size.height; | |
50 int cellCount = (int) (tableHeight / rowHeight); | |
51 | |
52 self.bufferSlack = 1; | |
53 self.bufferAdvance = cellCount; | |
54 self.bufferCount = 3 * self.bufferAdvance + cellCount; | |
55 } | |
56 | |
57 - (void)presentSlidingWindow:(SlidingWindowNode*)node { | |
58 [self checkDisplayWindow:node]; | |
59 dispatch_async(dispatch_get_main_queue(), ^{ | |
60 [self presentOnMainThread:node]; | |
61 }); | |
62 } | |
63 | |
64 - (void)patchSlidingWindow:(SlidingWindowPatch*)patch { | |
65 assert(patch.updated); | |
66 [self checkDisplayWindow:patch.current]; | |
67 dispatch_async(dispatch_get_main_queue(), ^{ | |
68 [self patchOnMainThread:patch]; | |
69 }); | |
70 } | |
71 | |
72 - (void)checkDisplayWindow:(SlidingWindowNode*)node { | |
73 if (node.window.count == 0) { | |
74 node.display(0, self.bufferCount); | |
75 } | |
76 } | |
77 | |
78 - (NSInteger)tableView:(UITableView*)tableView | |
79 numberOfRowsInSection:(NSInteger)section { | |
80 return self.root == nil ? 0 : self.root.minimumCount; | |
81 } | |
82 | |
83 - (UITableViewCell*)tableView:(UITableView*)tableView | |
84 cellForRowAtIndexPath:(NSIndexPath*)indexPath { | |
85 Node* node = [self itemAtIndex:indexPath.row]; | |
86 return [self.cellPresenter tableView:tableView | |
87 indexPath:indexPath | |
88 present:node]; | |
89 } | |
90 | |
91 // To track what items are visible on screen we rely on the fact that only | |
92 // visible items are accessed by cellForRowAtIndexPath. When accessing an index | |
93 // that is in the proximity of either the start or the end of the sliding | |
94 // window, we shift the window. | |
95 - (id)itemAtIndex:(int)index { | |
96 assert(self.root != nil); | |
97 if (index < self.windowStart + self.bufferSlack) { | |
98 [self shiftDown:index]; | |
99 } else if (index + self.bufferSlack >= self.windowEnd) { | |
100 [self shiftUp:index]; | |
101 } | |
102 int adjusted = [self windowIndex:index]; | |
103 // Return nil if the adjusted index is outside the sliding window. | |
104 return (adjusted < 0) ? nil : [self.root.window objectAtIndex:adjusted]; | |
105 } | |
106 | |
107 - (void)shiftDown:(int)index { | |
108 int start = (index > self.bufferAdvance) ? index - self.bufferAdvance : 0; | |
109 if (start == self.windowStart) return; | |
110 [self refreshDisplayStart:start end:start + self.bufferCount]; | |
111 } | |
112 | |
113 - (void)shiftUp:(int)index { | |
114 int end = index + self.bufferAdvance + 1; | |
115 if (end > self.maximumCount) end = self.maximumCount; | |
116 if (end == self.windowEnd) return; | |
117 if (end > self.bufferCount) { | |
118 [self refreshDisplayStart:end - self.bufferCount end:end]; | |
119 } else { | |
120 [self refreshDisplayStart:0 end:self.bufferCount]; | |
121 } | |
122 } | |
123 | |
124 - (void)refreshDisplayStart:(int)start end:(int)end { | |
125 self.root.display(start, end); | |
126 } | |
127 | |
128 // Adjust the scroll position if the visible rows are outside the window buffer. | |
129 // Returns true if it is or if scroll position was adjusted. | |
130 - (bool)adjustScrollPosition { | |
131 // TODO(zerny): Identify "scroll to top" as scrolling and enable this again. | |
132 return false; | |
133 // TODO(zerny): Properly track the scroll position. | |
134 // If the current view is outside the visible view adjust the visible view. | |
135 if (!self.scrolling && self.tableView.indexPathsForVisibleRows.count > 0) { | |
136 int start = self.windowStart; | |
137 int end = self.windowEnd; | |
138 int row = [[self.tableView.indexPathsForVisibleRows objectAtIndex:0] row]; | |
139 if (row < start || end <= row) { | |
140 // Adjust the start by buffer slack so we don't trigger a window shift. | |
141 if (start != 0) start += self.bufferSlack + 1; | |
142 NSIndexPath* path = [NSIndexPath indexPathForRow:start inSection:0]; | |
143 [self.tableView reloadData]; | |
144 if (start < end) { | |
145 [self.tableView scrollToRowAtIndexPath:path | |
146 atScrollPosition:UITableViewScrollPositionTop | |
147 animated:NO]; | |
148 } | |
149 return true; | |
150 } | |
151 } | |
152 return false; | |
153 } | |
154 | |
155 - (void)presentOnMainThread:(SlidingWindowNode*)node { | |
156 self.root = node; | |
157 if ([self adjustScrollPosition]) return; | |
158 [self.tableView reloadData]; | |
159 } | |
160 | |
161 - (void)patchOnMainThread:(SlidingWindowPatch*)patch { | |
162 self.root = patch.current; | |
163 if ([self adjustScrollPosition]) return; | |
164 | |
165 int previousCount = patch.previous.minimumCount; | |
166 int currentCount = patch.current.minimumCount; | |
167 assert(previousCount == [self.tableView numberOfRowsInSection:0]); | |
168 | |
169 // The stable range is positions in the view both before and after the patch. | |
170 int stableCount = MIN(previousCount, currentCount); | |
171 | |
172 // Independently track if insert or removes have been made. | |
173 bool containsInserts = false; | |
174 bool containsRemoves = false; | |
175 | |
176 // Find an update ranges: | |
177 NSMutableArray* updatePaths = [[NSMutableArray alloc] init]; | |
178 for (int i = 0; i < patch.window.regions.count; ++i) { | |
179 ListRegionPatch* region = patch.window.regions[i]; | |
180 if (!region.isUpdate) { | |
181 containsInserts = containsInserts || region.isInsert; | |
182 containsRemoves = containsRemoves || region.isRemove; | |
183 continue; | |
184 } | |
185 ListRegionUpdatePatch* update = (id)region; | |
186 for (int j = 0; j < update.updates.count; ++j) { | |
187 int position = [self windowIndexToTableIndex:update.index + j]; | |
188 if (position >= stableCount) continue; | |
189 [updatePaths addObject:[NSIndexPath indexPathForRow:position inSection:0]]
; | |
190 } | |
191 } | |
192 | |
193 // This patch routine assumes that the diff algorithm will not produce | |
194 // both an insertion and a deletion region in the same patch. | |
195 assert(!containsInserts || !containsRemoves); | |
196 | |
197 // Find either the insert or the remove positions: | |
198 NSMutableArray* insertPaths; | |
199 NSMutableArray* removePaths; | |
200 if (stableCount < currentCount) { | |
201 insertPaths = [[NSMutableArray alloc] init]; | |
202 for (int i = stableCount; i < currentCount; ++i) { | |
203 [insertPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; | |
204 } | |
205 } else if (stableCount < previousCount) { | |
206 removePaths = [[NSMutableArray alloc] init]; | |
207 for (int i = stableCount; i < previousCount; ++i) { | |
208 [removePaths addObject:[NSIndexPath indexPathForRow:i inSection:0]]; | |
209 } | |
210 } | |
211 | |
212 // Batch notify the table view of the changes. | |
213 [self.tableView beginUpdates]; | |
214 [self.tableView reloadRowsAtIndexPaths:updatePaths | |
215 withRowAnimation:UITableViewRowAnimationNone]; | |
216 if (insertPaths != nil) { | |
217 [self.tableView insertRowsAtIndexPaths:insertPaths | |
218 withRowAnimation:UITableViewRowAnimationNone]; | |
219 } | |
220 if (removePaths != nil) { | |
221 [self.tableView deleteRowsAtIndexPaths:removePaths | |
222 withRowAnimation:UITableViewRowAnimationNone]; | |
223 } | |
224 [self.tableView endUpdates]; | |
225 | |
226 assert(currentCount == [self.tableView numberOfRowsInSection:0]); | |
227 } | |
228 | |
229 - (int)windowIndexToTableIndex:(int)index { | |
230 int indexDelta = index - self.root.windowOffset; | |
231 if (indexDelta < 0) indexDelta += self.root.window.count; | |
232 return [self windowStart] + indexDelta; | |
233 } | |
234 | |
235 - (int)windowIndex:(int)index { | |
236 assert(self.root != nil); | |
237 if (index < self.windowStart || self.windowEnd <= index) return -1; | |
238 int i = self.root.windowOffset + index - self.windowStart; | |
239 return i % self.windowCount; | |
240 } | |
241 | |
242 // The maximum number of items that can be in the list. | |
243 - (int)maximumCount { | |
244 return self.root.maximumCount < 0 ? INT_MAX : self.root.maximumCount; | |
245 } | |
246 | |
247 - (int)windowCount { | |
248 return self.root.window.count; | |
249 } | |
250 | |
251 - (int)windowEnd { | |
252 return self.root.startOffset + self.windowCount; | |
253 } | |
254 | |
255 - (int)windowStart { | |
256 return self.root.startOffset; | |
257 } | |
258 | |
259 - (void)tableView:(UITableView*)tableView | |
260 didSelectRowAtIndexPath:(NSIndexPath*)indexPath { | |
261 self.root.toggle(indexPath.row); | |
262 } | |
263 | |
264 - (CGFloat)tableView:(UITableView*)tableView | |
265 heightForRowAtIndexPath:(NSIndexPath*)indexPath { | |
266 return [self.cellPresenter tableView:tableView | |
267 heightForRowAtIndexPath:indexPath]; | |
268 } | |
269 | |
270 - (CGFloat)tableView:(UITableView*)tableView | |
271 estimatedHeightForRowAtIndexPath:(NSIndexPath*)indexPath { | |
272 return [self.cellPresenter tableView:tableView | |
273 estimatedHeightForRowAtIndexPath:indexPath]; | |
274 } | |
275 | |
276 - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { | |
277 self.scrolling = true; | |
278 } | |
279 | |
280 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL
)decelerate { | |
281 self.scrolling = false; | |
282 } | |
283 | |
284 @end | |
OLD | NEW |