123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545 |
- //
- // ReaderThumbsView.m
- // Reader v2.8.6
- //
- // Created by Julius Oklamcak on 2011-09-01.
- // Copyright © 2011-2015 Julius Oklamcak. All rights reserved.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights to
- // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
- // of the Software, and to permit persons to whom the Software is furnished to
- // do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in all
- // copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
- // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- //
- #import "ReaderThumbsView.h"
- @interface ReaderThumbsView () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
- @end
- @implementation ReaderThumbsView
- {
- CGPoint lastContentOffset;
- ReaderThumbView *touchedCell;
- NSMutableArray *thumbCellsQueue;
- NSMutableArray *thumbCellsVisible;
- NSInteger _thumbsX, _thumbsY, _thumbX;
- CGSize _thumbSize, _lastViewSize;
- NSUInteger _thumbCount;
- BOOL canUpdate;
- }
- #pragma mark - Properties
- @synthesize delegate;
- #pragma mark - ReaderThumbsView instance methods
- - (instancetype)initWithFrame:(CGRect)frame
- {
- if ((self = [super initWithFrame:frame]))
- {
- self.scrollsToTop = NO;
- self.autoresizesSubviews = NO;
- self.delaysContentTouches = NO;
- self.alwaysBounceVertical = YES;
- self.contentMode = UIViewContentModeRedraw;
- self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
- self.backgroundColor = [UIColor clearColor];
- [super setDelegate:self]; // Set the superclass UIScrollView delegate
- thumbCellsQueue = [NSMutableArray new]; thumbCellsVisible = [NSMutableArray new]; // Cell management arrays
- UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
- //tapGesture.numberOfTouchesRequired = 1; tapGesture.numberOfTapsRequired = 1; tapGesture.delegate = self;
- [self addGestureRecognizer:tapGesture];
- UILongPressGestureRecognizer *pressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePressGesture:)];
- pressGesture.minimumPressDuration = 0.8; //pressGesture.numberOfTouchesRequired = 1; pressGesture.delegate = self;
- [self addGestureRecognizer:pressGesture];
- lastContentOffset = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
- }
- return self;
- }
- - (void)requeueThumbCell:(ReaderThumbView *)tvCell
- {
- [thumbCellsQueue addObject:tvCell];
- [thumbCellsVisible removeObject:tvCell];
- tvCell.tag = NSIntegerMin; tvCell.hidden = YES;
- [tvCell reuse]; // Reuse the cell
- }
- - (void)requeueAllThumbCells
- {
- if (thumbCellsVisible.count > 0)
- {
- NSArray *visible = [thumbCellsVisible copy];
- for (ReaderThumbView *tvCell in visible)
- {
- [self requeueThumbCell:tvCell];
- }
- }
- }
- - (ReaderThumbView *)dequeueThumbCellWithFrame:(CGRect)frame
- {
- ReaderThumbView *theCell = nil;
- if (thumbCellsQueue.count > 0) // Reuse existing cell
- {
- theCell = [thumbCellsQueue objectAtIndex:0];
- [thumbCellsQueue removeObjectAtIndex:0]; // Dequeue it
- theCell.frame = frame; // Position the reused cell
- }
- else // Allocate a brand new thumb cell subclass for our use
- {
- theCell = [delegate thumbsView:self thumbCellWithFrame:frame];
- //assert([theCell isKindOfClass:[ReaderThumbView class]]);
- theCell.tag = NSIntegerMin; theCell.hidden = YES;
- [self insertSubview:theCell atIndex:0]; // Add
- }
- [thumbCellsVisible addObject:theCell];
- return theCell;
- }
- - (NSMutableIndexSet *)visibleIndexSetForContentOffset
- {
- CGFloat minY = self.contentOffset.y; // Content offset
- CGFloat maxY = (minY + self.bounds.size.height - 1.0f);
- NSInteger startRow = (minY / _thumbSize.height); // Start row
- NSInteger finalRow = (maxY / _thumbSize.height); // Final row
- NSInteger startIndex = (startRow * _thumbsX); // Start index
- NSInteger finalIndex = (finalRow * _thumbsX); // Final index
- finalIndex += (_thumbsX - 1); // Last index value in last row
- NSInteger maximumIndex = (_thumbCount - 1); // Maximum index value
- if (finalIndex > maximumIndex) finalIndex = maximumIndex; // Limit it
- NSRange indexRange = NSMakeRange(startIndex, (finalIndex - startIndex + 1));
- return [NSMutableIndexSet indexSetWithIndexesInRange:indexRange];
- }
- - (ReaderThumbView *)thumbCellContainingPoint:(CGPoint)point
- {
- ReaderThumbView *theCell = nil;
- for (ReaderThumbView *tvCell in thumbCellsVisible)
- {
- if (CGRectContainsPoint(tvCell.frame, point) == true)
- {
- theCell = tvCell; break; // Found it
- }
- }
- return theCell;
- }
- - (CGRect)thumbCellFrameForIndex:(NSInteger)index
- {
- CGRect thumbRect; thumbRect.size = _thumbSize;
- NSInteger thumbY = ((index / _thumbsX) * _thumbSize.height); // X, Y
- NSInteger thumbX = (((index % _thumbsX) * _thumbSize.width) + _thumbX);
- thumbRect.origin.x = thumbX; thumbRect.origin.y = thumbY;
- return thumbRect;
- }
- - (void)updateContentSize:(NSUInteger)thumbCount
- {
- canUpdate = NO; // Disable updates
- if (thumbCount > 0) // Have some thumbs
- {
- CGFloat bw = self.bounds.size.width;
- _thumbsX = (bw / _thumbSize.width);
- if (_thumbsX < 1) _thumbsX = 1;
- _thumbsY = (thumbCount / _thumbsX);
- if ((_thumbsX * _thumbsY) < thumbCount) _thumbsY++;
- CGFloat tw = (_thumbsX * _thumbSize.width);
- CGFloat th = (_thumbsY * _thumbSize.height);
- if (tw < bw)
- _thumbX = ((bw - tw) * 0.5f);
- else
- _thumbX = 0; // Reset
- if (tw < bw) tw = bw; // Limit
- [self setContentSize:CGSizeMake(tw, th)];
- }
- else // Zero (0) thumbs
- {
- [self setContentSize:CGSizeZero];
- }
- canUpdate = YES; // Enable updates
- }
- - (void)layoutSubviews
- {
- if (CGSizeEqualToSize(_lastViewSize, CGSizeZero) == true)
- {
- _lastViewSize = self.bounds.size; // Initial view size
- }
- else
- if (CGSizeEqualToSize(_lastViewSize, self.bounds.size) == false)
- {
- _lastViewSize = self.bounds.size; // Track the view size
- [self updateContentSize:_thumbCount]; // Update the content size
- NSMutableArray *requeueCells = [NSMutableArray array]; // Requeue cell list
- NSMutableIndexSet *visibleIndexSet = [self visibleIndexSetForContentOffset];
- for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
- {
- NSInteger index = tvCell.tag; // Get the cell's index value
- if ([visibleIndexSet containsIndex:index] == YES) // Visible cell
- {
- tvCell.frame = [self thumbCellFrameForIndex:index]; // Frame
- [visibleIndexSet removeIndex:index]; // Remove from set
- }
- else // Add it to the list of cells to requeue
- {
- [requeueCells addObject:tvCell];
- }
- }
- for (ReaderThumbView *tvCell in requeueCells) // Enumerate requeue cells
- {
- [self requeueThumbCell:tvCell]; // Requeue the thumb cell
- }
- [visibleIndexSet enumerateIndexesUsingBlock: // Enumerate visible indexes
- ^(NSUInteger index, BOOL *stop)
- {
- CGRect thumbRect = [self thumbCellFrameForIndex:index]; // Frame
- ReaderThumbView *tvCell = [self dequeueThumbCellWithFrame:thumbRect];
- [delegate thumbsView:self updateThumbCell:tvCell forIndex:index];
- tvCell.tag = index; tvCell.hidden = NO; // Tag and show it
- }
- ];
- }
- }
- - (void)setThumbSize:(CGSize)thumbSize
- {
- if (CGSizeEqualToSize(_thumbSize, CGSizeZero) == true)
- {
- if (CGSizeEqualToSize(thumbSize, CGSizeZero) == false)
- {
- _thumbSize = thumbSize; // Set the maximum thumb size
- }
- }
- }
- - (void)reloadThumbsCenterOnIndex:(NSInteger)index
- {
- assert(delegate != nil); // Check delegate
- assert(CGSizeEqualToSize(_thumbSize, CGSizeZero) == false);
- if (self.decelerating == YES) // Stop scroll view movement
- {
- [self setContentOffset:self.contentOffset animated:NO];
- }
- CGPoint newContentOffset = CGPointZero; // At top
- lastContentOffset = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
- [self requeueAllThumbCells]; // Start off fresh
- _thumbCount = 0; // Reset the thumb count to zero
- NSUInteger thumbCount = [delegate numberOfThumbsInThumbsView:self];
- [self updateContentSize:thumbCount]; _thumbCount = thumbCount;
- if (thumbCount > 0) // Have some thumbs
- {
- NSInteger boundsHeight = self.bounds.size.height;
- NSInteger maxY = (self.contentSize.height - boundsHeight);
- NSInteger minY = 0; maxY--; if (maxY < minY) maxY = minY; // Limits
- if (index < 0) index = 0; else if (index > thumbCount) index = (thumbCount - 1);
- NSInteger thumbY = ((index / _thumbsX) * _thumbSize.height); // Thumb Y
- NSInteger offsetY = (thumbY - (boundsHeight / 2) + (_thumbSize.height / 2));
- if (offsetY < minY) offsetY = minY; else if (offsetY > maxY) offsetY = maxY;
- newContentOffset.y = offsetY; // Calculated content offset Y position
- }
- newContentOffset.y -= self.contentInset.top; // Content inset adjust
- if (CGPointEqualToPoint(self.contentOffset, newContentOffset) == false)
- [self setContentOffset:newContentOffset animated:NO];
- else
- [self scrollViewDidScroll:self];
- [self flashScrollIndicators];
- }
- - (void)reloadThumbsContentOffset:(CGPoint)newContentOffset
- {
- assert(delegate != nil); // Check delegate
- assert(CGSizeEqualToSize(_thumbSize, CGSizeZero) == false);
- if (self.decelerating == YES) // Stop scroll view movement
- {
- [self setContentOffset:self.contentOffset animated:NO];
- }
- lastContentOffset = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
- [self requeueAllThumbCells]; // Start off fresh
- _thumbCount = 0; // Reset the thumb count to zero
- NSUInteger thumbCount = [delegate numberOfThumbsInThumbsView:self];
- [self updateContentSize:thumbCount]; _thumbCount = thumbCount;
- if (thumbCount > 0) // Have some thumbs
- {
- NSInteger boundsHeight = self.bounds.size.height;
- NSInteger maxY = (self.contentSize.height - boundsHeight);
- NSInteger minY = 0; maxY--; if (maxY < minY) maxY = minY; // Limits
- NSInteger offsetY = newContentOffset.y; // Requested content offset Y
- if (offsetY < minY) offsetY = minY; else if (offsetY > maxY) offsetY = maxY;
- newContentOffset.y = offsetY; newContentOffset.x = 0.0f; // Validated
- }
- else // Zero (0) thumbs
- {
- newContentOffset = CGPointZero;
- }
- newContentOffset.y -= self.contentInset.top; // Content inset adjust
- if (CGPointEqualToPoint(self.contentOffset, newContentOffset) == false)
- [self setContentOffset:newContentOffset animated:NO];
- else
- [self scrollViewDidScroll:self];
- [self flashScrollIndicators];
- }
- - (void)refreshThumbWithIndex:(NSInteger)index
- {
- for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
- {
- if (tvCell.tag == index) // Found a visible thumb cell with the index value
- {
- if ([delegate respondsToSelector:@selector(thumbsView:refreshThumbCell:forIndex:)])
- {
- [delegate thumbsView:self refreshThumbCell:tvCell forIndex:index]; // Refresh
- }
- break;
- }
- }
- }
- - (void)refreshVisibleThumbs
- {
- for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
- {
- if ([delegate respondsToSelector:@selector(thumbsView:refreshThumbCell:forIndex:)])
- {
- [delegate thumbsView:self refreshThumbCell:tvCell forIndex:tvCell.tag]; // Refresh
- }
- }
- }
- - (CGPoint)insetContentOffset
- {
- CGPoint insetContentOffset = self.contentOffset; // Offset
- insetContentOffset.y += self.contentInset.top; // Inset adjust
- return insetContentOffset; // Adjusted content offset
- }
- #pragma mark - UIGestureRecognizer action methods
- - (void)handleTapGesture:(UITapGestureRecognizer *)recognizer
- {
- if (recognizer.state == UIGestureRecognizerStateRecognized) // Handle the tap
- {
- CGPoint point = [recognizer locationInView:recognizer.view]; // Tap location
- ReaderThumbView *tvCell = [self thumbCellContainingPoint:point]; // Look for cell
- if (tvCell != nil) [delegate thumbsView:self didSelectThumbWithIndex:tvCell.tag];
- }
- }
- - (void)handlePressGesture:(UILongPressGestureRecognizer *)recognizer
- {
- if (recognizer.state == UIGestureRecognizerStateBegan) // Handle the press
- {
- if ([delegate respondsToSelector:@selector(thumbsView:didPressThumbWithIndex:)])
- {
- CGPoint point = [recognizer locationInView:recognizer.view]; // Press location
- ReaderThumbView *tvCell = [self thumbCellContainingPoint:point]; // Look for cell
- if (tvCell != nil) [delegate thumbsView:self didPressThumbWithIndex:tvCell.tag];
- }
- }
- }
- #pragma mark - UIScrollViewDelegate methods
- - (void)scrollViewDidScroll:(UIScrollView *)scrollView
- {
- if ((canUpdate == YES) && (_thumbCount > 0)) // Check flag and thumb count
- {
- if (CGPointEqualToPoint(scrollView.contentOffset, lastContentOffset) == false)
- {
- lastContentOffset = scrollView.contentOffset; // Work around a 'feature'
- CGRect visibleBounds = self.bounds; // Visible bounds in the scroll view
- NSMutableArray *requeueCells = [NSMutableArray array]; // Requeue cell list
- NSMutableIndexSet *visibleCellSet = [NSMutableIndexSet indexSet]; // Visible set
- for (ReaderThumbView *tvCell in thumbCellsVisible) // Enumerate visible cells
- {
- if (CGRectIntersectsRect(tvCell.frame, visibleBounds) == true)
- [visibleCellSet addIndex:tvCell.tag];
- else
- [requeueCells addObject:tvCell];
- }
- for (ReaderThumbView *tvCell in requeueCells) // Enumerate requeue cells
- {
- [self requeueThumbCell:tvCell]; // Requeue the thumb cell
- }
- NSMutableIndexSet *visibleIndexSet = [self visibleIndexSetForContentOffset];
- [visibleIndexSet enumerateIndexesUsingBlock: // Enumerate visible indexes
- ^(NSUInteger index, BOOL *stop)
- {
- if ([visibleCellSet containsIndex:index] == NO) // Index not visible
- {
- CGRect thumbRect = [self thumbCellFrameForIndex:index]; // Frame
- ReaderThumbView *tvCell = [self dequeueThumbCellWithFrame:thumbRect];
- [delegate thumbsView:self updateThumbCell:tvCell forIndex:index];
- tvCell.tag = index; tvCell.hidden = NO; // Tag and show it
- }
- }
- ];
- }
- }
- }
- #pragma mark - UIResponder instance methods
- - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- {
- [super touchesBegan:touches withEvent:event]; // Message superclass
- if (touchedCell != nil) { [touchedCell showTouched:NO]; touchedCell = nil; }
- if (touches.count == 1) // Show selection on single touch
- {
- UITouch *touch = [touches anyObject]; // Get touch from set
- CGPoint point = [touch locationInView:touch.view]; // Touch location
- ReaderThumbView *tvCell = [self thumbCellContainingPoint:point]; // Look for cell
- if (tvCell != nil) { touchedCell = tvCell; [touchedCell showTouched:YES]; }
- }
- }
- - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
- {
- [super touchesCancelled:touches withEvent:event]; // Message superclass
- if (touchedCell != nil) { [touchedCell showTouched:NO]; touchedCell = nil; }
- }
- - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- {
- [super touchesEnded:touches withEvent:event]; // Message superclass
- if (touchedCell != nil) { [touchedCell showTouched:NO]; touchedCell = nil; }
- }
- - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- {
- [super touchesMoved:touches withEvent:event]; // Message superclass
- }
- @end
|