ReaderThumbsView.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. //
  2. // ReaderThumbsView.m
  3. // Reader v2.8.6
  4. //
  5. // Created by Julius Oklamcak on 2011-09-01.
  6. // Copyright © 2011-2015 Julius Oklamcak. All rights reserved.
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy
  9. // of this software and associated documentation files (the "Software"), to deal
  10. // in the Software without restriction, including without limitation the rights to
  11. // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  12. // of the Software, and to permit persons to whom the Software is furnished to
  13. // do so, subject to the following conditions:
  14. //
  15. // The above copyright notice and this permission notice shall be included in all
  16. // copies or substantial portions of the Software.
  17. //
  18. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  19. // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  20. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  21. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  22. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  23. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  24. //
  25. #import "ReaderThumbsView.h"
  26. @interface ReaderThumbsView () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
  27. @end
  28. @implementation ReaderThumbsView
  29. {
  30. CGPoint lastContentOffset;
  31. ReaderThumbView *touchedCell;
  32. NSMutableArray *thumbCellsQueue;
  33. NSMutableArray *thumbCellsVisible;
  34. NSInteger _thumbsX, _thumbsY, _thumbX;
  35. CGSize _thumbSize, _lastViewSize;
  36. NSUInteger _thumbCount;
  37. BOOL canUpdate;
  38. }
  39. #pragma mark - Properties
  40. @synthesize delegate;
  41. #pragma mark - ReaderThumbsView instance methods
  42. - (instancetype)initWithFrame:(CGRect)frame
  43. {
  44. if ((self = [super initWithFrame:frame]))
  45. {
  46. self.scrollsToTop = NO;
  47. self.autoresizesSubviews = NO;
  48. self.delaysContentTouches = NO;
  49. self.alwaysBounceVertical = YES;
  50. self.contentMode = UIViewContentModeRedraw;
  51. self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  52. self.backgroundColor = [UIColor clearColor];
  53. [super setDelegate:self]; // Set the superclass UIScrollView delegate
  54. thumbCellsQueue = [NSMutableArray new]; thumbCellsVisible = [NSMutableArray new]; // Cell management arrays
  55. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
  56. //tapGesture.numberOfTouchesRequired = 1; tapGesture.numberOfTapsRequired = 1; tapGesture.delegate = self;
  57. [self addGestureRecognizer:tapGesture];
  58. UILongPressGestureRecognizer *pressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)];
  59. pressGesture.minimumPressDuration = 0.8; //pressGesture.numberOfTouchesRequired = 1; pressGesture.delegate = self;
  60. [self addGestureRecognizer:pressGesture];
  61. lastContentOffset = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
  62. }
  63. return self;
  64. }
  65. - (void)requeueThumbCell:(ReaderThumbView *)tvCell
  66. {
  67. [thumbCellsQueue addObject:tvCell];
  68. [thumbCellsVisible removeObject:tvCell];
  69. tvCell.tag = NSIntegerMin; tvCell.hidden = YES;
  70. [tvCell reuse]; // Reuse the cell
  71. }
  72. - (void)requeueAllThumbCells
  73. {
  74. if (thumbCellsVisible.count > 0)
  75. {
  76. NSArray *visible = [thumbCellsVisible copy];
  77. for (ReaderThumbView *tvCell in visible)
  78. {
  79. [self requeueThumbCell:tvCell];
  80. }
  81. }
  82. }
  83. - (ReaderThumbView *)dequeueThumbCellWithFrame:(CGRect)frame
  84. {
  85. ReaderThumbView *theCell = nil;
  86. if (thumbCellsQueue.count > 0) // Reuse existing cell
  87. {
  88. theCell = [thumbCellsQueue objectAtIndex:0];
  89. [thumbCellsQueue removeObjectAtIndex:0]; // Dequeue it
  90. theCell.frame = frame; // Position the reused cell
  91. }
  92. else // Allocate a brand new thumb cell subclass for our use
  93. {
  94. theCell = [delegate thumbsView:self thumbCellWithFrame:frame];
  95. //assert([theCell isKindOfClass:[ReaderThumbView class]]);
  96. theCell.tag = NSIntegerMin; theCell.hidden = YES;
  97. [self insertSubview:theCell atIndex:0]; // Add
  98. }
  99. [thumbCellsVisible addObject:theCell];
  100. return theCell;
  101. }
  102. - (NSMutableIndexSet *)visibleIndexSetForContentOffset
  103. {
  104. CGFloat minY = self.contentOffset.y; // Content offset
  105. CGFloat maxY = (minY + self.bounds.size.height - 1.0f);
  106. NSInteger startRow = (minY / _thumbSize.height); // Start row
  107. NSInteger finalRow = (maxY / _thumbSize.height); // Final row
  108. NSInteger startIndex = (startRow * _thumbsX); // Start index
  109. NSInteger finalIndex = (finalRow * _thumbsX); // Final index
  110. finalIndex += (_thumbsX - 1); // Last index value in last row
  111. NSInteger maximumIndex = (_thumbCount - 1); // Maximum index value
  112. if (finalIndex > maximumIndex) finalIndex = maximumIndex; // Limit it
  113. NSRange indexRange = NSMakeRange(startIndex, (finalIndex - startIndex + 1));
  114. return [NSMutableIndexSet indexSetWithIndexesInRange:indexRange];
  115. }
  116. - (ReaderThumbView *)thumbCellContainingPoint:(CGPoint)point
  117. {
  118. ReaderThumbView *theCell = nil;
  119. for (ReaderThumbView *tvCell in thumbCellsVisible)
  120. {
  121. if (CGRectContainsPoint(tvCell.frame, point) == true)
  122. {
  123. theCell = tvCell; break; // Found it
  124. }
  125. }
  126. return theCell;
  127. }
  128. - (CGRect)thumbCellFrameForIndex:(NSInteger)index
  129. {
  130. CGRect thumbRect; thumbRect.size = _thumbSize;
  131. NSInteger thumbY = ((index / _thumbsX) * _thumbSize.height); // X, Y
  132. NSInteger thumbX = (((index % _thumbsX) * _thumbSize.width) + _thumbX);
  133. thumbRect.origin.x = thumbX; thumbRect.origin.y = thumbY;
  134. return thumbRect;
  135. }
  136. - (void)updateContentSize:(NSUInteger)thumbCount
  137. {
  138. canUpdate = NO; // Disable updates
  139. if (thumbCount > 0) // Have some thumbs
  140. {
  141. CGFloat bw = self.bounds.size.width;
  142. _thumbsX = (bw / _thumbSize.width);
  143. if (_thumbsX < 1) _thumbsX = 1;
  144. _thumbsY = (thumbCount / _thumbsX);
  145. if ((_thumbsX * _thumbsY) < thumbCount) _thumbsY++;
  146. CGFloat tw = (_thumbsX * _thumbSize.width);
  147. CGFloat th = (_thumbsY * _thumbSize.height);
  148. if (tw < bw)
  149. _thumbX = ((bw - tw) * 0.5f);
  150. else
  151. _thumbX = 0; // Reset
  152. if (tw < bw) tw = bw; // Limit
  153. [self setContentSize:CGSizeMake(tw, th)];
  154. }
  155. else // Zero (0) thumbs
  156. {
  157. [self setContentSize:CGSizeZero];
  158. }
  159. canUpdate = YES; // Enable updates
  160. }
  161. - (void)layoutSubviews
  162. {
  163. if (CGSizeEqualToSize(_lastViewSize, CGSizeZero) == true)
  164. {
  165. _lastViewSize = self.bounds.size; // Initial view size
  166. }
  167. else
  168. if (CGSizeEqualToSize(_lastViewSize, self.bounds.size) == false)
  169. {
  170. _lastViewSize = self.bounds.size; // Track the view size
  171. [self updateContentSize:_thumbCount]; // Update the content size
  172. NSMutableArray *requeueCells = [NSMutableArray array]; // Requeue cell list
  173. NSMutableIndexSet *visibleIndexSet = [self visibleIndexSetForContentOffset];
  174. for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
  175. {
  176. NSInteger index = tvCell.tag; // Get the cell's index value
  177. if ([visibleIndexSet containsIndex:index] == YES) // Visible cell
  178. {
  179. tvCell.frame = [self thumbCellFrameForIndex:index]; // Frame
  180. [visibleIndexSet removeIndex:index]; // Remove from set
  181. }
  182. else // Add it to the list of cells to requeue
  183. {
  184. [requeueCells addObject:tvCell];
  185. }
  186. }
  187. for (ReaderThumbView *tvCell in requeueCells) // Enumerate requeue cells
  188. {
  189. [self requeueThumbCell:tvCell]; // Requeue the thumb cell
  190. }
  191. [visibleIndexSet enumerateIndexesUsingBlock: // Enumerate visible indexes
  192. ^(NSUInteger index, BOOL *stop)
  193. {
  194. CGRect thumbRect = [self thumbCellFrameForIndex:index]; // Frame
  195. ReaderThumbView *tvCell = [self dequeueThumbCellWithFrame:thumbRect];
  196. [delegate thumbsView:self updateThumbCell:tvCell forIndex:index];
  197. tvCell.tag = index; tvCell.hidden = NO; // Tag and show it
  198. }
  199. ];
  200. }
  201. }
  202. - (void)setThumbSize:(CGSize)thumbSize
  203. {
  204. if (CGSizeEqualToSize(_thumbSize, CGSizeZero) == true)
  205. {
  206. if (CGSizeEqualToSize(thumbSize, CGSizeZero) == false)
  207. {
  208. _thumbSize = thumbSize; // Set the maximum thumb size
  209. }
  210. }
  211. }
  212. - (void)reloadThumbsCenterOnIndex:(NSInteger)index
  213. {
  214. assert(delegate != nil); // Check delegate
  215. assert(CGSizeEqualToSize(_thumbSize, CGSizeZero) == false);
  216. if (self.decelerating == YES) // Stop scroll view movement
  217. {
  218. [self setContentOffset:self.contentOffset animated:NO];
  219. }
  220. CGPoint newContentOffset = CGPointZero; // At top
  221. lastContentOffset = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
  222. [self requeueAllThumbCells]; // Start off fresh
  223. _thumbCount = 0; // Reset the thumb count to zero
  224. NSUInteger thumbCount = [delegate numberOfThumbsInThumbsView:self];
  225. [self updateContentSize:thumbCount]; _thumbCount = thumbCount;
  226. if (thumbCount > 0) // Have some thumbs
  227. {
  228. NSInteger boundsHeight = self.bounds.size.height;
  229. NSInteger maxY = (self.contentSize.height - boundsHeight);
  230. NSInteger minY = 0; maxY--; if (maxY < minY) maxY = minY; // Limits
  231. if (index < 0) index = 0; else if (index > thumbCount) index = (thumbCount - 1);
  232. NSInteger thumbY = ((index / _thumbsX) * _thumbSize.height); // Thumb Y
  233. NSInteger offsetY = (thumbY - (boundsHeight / 2) + (_thumbSize.height / 2));
  234. if (offsetY < minY) offsetY = minY; else if (offsetY > maxY) offsetY = maxY;
  235. newContentOffset.y = offsetY; // Calculated content offset Y position
  236. }
  237. newContentOffset.y -= self.contentInset.top; // Content inset adjust
  238. if (CGPointEqualToPoint(self.contentOffset, newContentOffset) == false)
  239. [self setContentOffset:newContentOffset animated:NO];
  240. else
  241. [self scrollViewDidScroll:self];
  242. [self flashScrollIndicators];
  243. }
  244. - (void)reloadThumbsContentOffset:(CGPoint)newContentOffset
  245. {
  246. assert(delegate != nil); // Check delegate
  247. assert(CGSizeEqualToSize(_thumbSize, CGSizeZero) == false);
  248. if (self.decelerating == YES) // Stop scroll view movement
  249. {
  250. [self setContentOffset:self.contentOffset animated:NO];
  251. }
  252. lastContentOffset = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
  253. [self requeueAllThumbCells]; // Start off fresh
  254. _thumbCount = 0; // Reset the thumb count to zero
  255. NSUInteger thumbCount = [delegate numberOfThumbsInThumbsView:self];
  256. [self updateContentSize:thumbCount]; _thumbCount = thumbCount;
  257. if (thumbCount > 0) // Have some thumbs
  258. {
  259. NSInteger boundsHeight = self.bounds.size.height;
  260. NSInteger maxY = (self.contentSize.height - boundsHeight);
  261. NSInteger minY = 0; maxY--; if (maxY < minY) maxY = minY; // Limits
  262. NSInteger offsetY = newContentOffset.y; // Requested content offset Y
  263. if (offsetY < minY) offsetY = minY; else if (offsetY > maxY) offsetY = maxY;
  264. newContentOffset.y = offsetY; newContentOffset.x = 0.0f; // Validated
  265. }
  266. else // Zero (0) thumbs
  267. {
  268. newContentOffset = CGPointZero;
  269. }
  270. newContentOffset.y -= self.contentInset.top; // Content inset adjust
  271. if (CGPointEqualToPoint(self.contentOffset, newContentOffset) == false)
  272. [self setContentOffset:newContentOffset animated:NO];
  273. else
  274. [self scrollViewDidScroll:self];
  275. [self flashScrollIndicators];
  276. }
  277. - (void)refreshThumbWithIndex:(NSInteger)index
  278. {
  279. for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
  280. {
  281. if (tvCell.tag == index) // Found a visible thumb cell with the index value
  282. {
  283. if ([delegate respondsToSelector:@selector(thumbsView:refreshThumbCell:forIndex:)])
  284. {
  285. [delegate thumbsView:self refreshThumbCell:tvCell forIndex:index]; // Refresh
  286. }
  287. break;
  288. }
  289. }
  290. }
  291. - (void)refreshVisibleThumbs
  292. {
  293. for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
  294. {
  295. if ([delegate respondsToSelector:@selector(thumbsView:refreshThumbCell:forIndex:)])
  296. {
  297. [delegate thumbsView:self refreshThumbCell:tvCell forIndex:tvCell.tag]; // Refresh
  298. }
  299. }
  300. }
  301. - (CGPoint)insetContentOffset
  302. {
  303. CGPoint insetContentOffset = self.contentOffset; // Offset
  304. insetContentOffset.y += self.contentInset.top; // Inset adjust
  305. return insetContentOffset; // Adjusted content offset
  306. }
  307. #pragma mark - UIGestureRecognizer action methods
  308. - (void)handleTapGesture:(UITapGestureRecognizer *)recognizer
  309. {
  310. if (recognizer.state == UIGestureRecognizerStateRecognized) // Handle the tap
  311. {
  312. CGPoint point = [recognizer locationInView:recognizer.view]; // Tap location
  313. ReaderThumbView *tvCell = [self thumbCellContainingPoint:point]; // Look for cell
  314. if (tvCell != nil) [delegate thumbsView:self didSelectThumbWithIndex:tvCell.tag];
  315. }
  316. }
  317. - (void)handlePressGesture:(UILongPressGestureRecognizer *)recognizer
  318. {
  319. if (recognizer.state == UIGestureRecognizerStateBegan) // Handle the press
  320. {
  321. if ([delegate respondsToSelector:@selector(thumbsView:didPressThumbWithIndex:)])
  322. {
  323. CGPoint point = [recognizer locationInView:recognizer.view]; // Press location
  324. ReaderThumbView *tvCell = [self thumbCellContainingPoint:point]; // Look for cell
  325. if (tvCell != nil) [delegate thumbsView:self didPressThumbWithIndex:tvCell.tag];
  326. }
  327. }
  328. }
  329. #pragma mark - UIScrollViewDelegate methods
  330. - (void)scrollViewDidScroll:(UIScrollView *)scrollView
  331. {
  332. if ((canUpdate == YES) && (_thumbCount > 0)) // Check flag and thumb count
  333. {
  334. if (CGPointEqualToPoint(scrollView.contentOffset, lastContentOffset) == false)
  335. {
  336. lastContentOffset = scrollView.contentOffset; // Work around a 'feature'
  337. CGRect visibleBounds = self.bounds; // Visible bounds in the scroll view
  338. NSMutableArray *requeueCells = [NSMutableArray array]; // Requeue cell list
  339. NSMutableIndexSet *visibleCellSet = [NSMutableIndexSet indexSet]; // Visible set
  340. for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
  341. {
  342. if (CGRectIntersectsRect(tvCell.frame, visibleBounds) == true)
  343. [visibleCellSet addIndex:tvCell.tag];
  344. else
  345. [requeueCells addObject:tvCell];
  346. }
  347. for (ReaderThumbView *tvCell in requeueCells) // Enumerate requeue cells
  348. {
  349. [self requeueThumbCell:tvCell]; // Requeue the thumb cell
  350. }
  351. NSMutableIndexSet *visibleIndexSet = [self visibleIndexSetForContentOffset];
  352. [visibleIndexSet enumerateIndexesUsingBlock: // Enumerate visible indexes
  353. ^(NSUInteger index, BOOL *stop)
  354. {
  355. if ([visibleCellSet containsIndex:index] == NO) // Index not visible
  356. {
  357. CGRect thumbRect = [self thumbCellFrameForIndex:index]; // Frame
  358. ReaderThumbView *tvCell = [self dequeueThumbCellWithFrame:thumbRect];
  359. [delegate thumbsView:self updateThumbCell:tvCell forIndex:index];
  360. tvCell.tag = index; tvCell.hidden = NO; // Tag and show it
  361. }
  362. }
  363. ];
  364. }
  365. }
  366. }
  367. #pragma mark - UIResponder instance methods
  368. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  369. {
  370. [super touchesBegan:touches withEvent:event]; // Message superclass
  371. if (touchedCell != nil) { [touchedCell showTouched:NO]; touchedCell = nil; }
  372. if (touches.count == 1) // Show selection on single touch
  373. {
  374. UITouch *touch = [touches anyObject]; // Get touch from set
  375. CGPoint point = [touch locationInView:touch.view]; // Touch location
  376. ReaderThumbView *tvCell = [self thumbCellContainingPoint:point]; // Look for cell
  377. if (tvCell != nil) { touchedCell = tvCell; [touchedCell showTouched:YES]; }
  378. }
  379. }
  380. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
  381. {
  382. [super touchesCancelled:touches withEvent:event]; // Message superclass
  383. if (touchedCell != nil) { [touchedCell showTouched:NO]; touchedCell = nil; }
  384. }
  385. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  386. {
  387. [super touchesEnded:touches withEvent:event]; // Message superclass
  388. if (touchedCell != nil) { [touchedCell showTouched:NO]; touchedCell = nil; }
  389. }
  390. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  391. {
  392. [super touchesMoved:touches withEvent:event]; // Message superclass
  393. }
  394. @end