| 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 |