// // SMPageControl.m // SMPageControl // // Created by Jerry Jones on 10/13/12. // Copyright (c) 2012 Spaceman Labs. All rights reserved. // #import "SMPageControl.h" #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif #define DEFAULT_INDICATOR_WIDTH 6.0f #define DEFAULT_INDICATOR_MARGIN 10.0f #define DEFAULT_MIN_HEIGHT 36.0f #define DEFAULT_INDICATOR_WIDTH_LARGE 7.0f #define DEFAULT_INDICATOR_MARGIN_LARGE 9.0f #define DEFAULT_MIN_HEIGHT_LARGE 36.0f typedef NS_ENUM(NSUInteger, SMPageControlImageType) { SMPageControlImageTypeNormal = 1, SMPageControlImageTypeCurrent, SMPageControlImageTypeMask }; typedef NS_ENUM(NSUInteger, SMPageControlStyleDefaults) { SMPageControlDefaultStyleClassic = 0, SMPageControlDefaultStyleModern }; static SMPageControlStyleDefaults _defaultStyleForSystemVersion; @interface SMPageControl () @property (strong, readonly, nonatomic) NSMutableDictionary *pageNames; @property (strong, readonly, nonatomic) NSMutableDictionary *pageImages; @property (strong, readonly, nonatomic) NSMutableDictionary *currentPageImages; @property (strong, readonly, nonatomic) NSMutableDictionary *pageImageMasks; @property (strong, readonly, nonatomic) NSMutableDictionary *cgImageMasks; @property (strong, readwrite, nonatomic) NSArray *pageRects; // Page Control used for stealing page number localizations for accessibility labels // I'm not sure I love this technique, but it's the best way to get exact translations for all the languages // that Apple supports out of the box @property (nonatomic, strong) UIPageControl *accessibilityPageControl; @end @implementation SMPageControl { @private NSInteger _displayedPage; CGFloat _measuredIndicatorWidth; CGFloat _measuredIndicatorHeight; CGImageRef _pageImageMask; } @synthesize pageNames = _pageNames; @synthesize pageImages = _pageImages; @synthesize currentPageImages = _currentPageImages; @synthesize pageImageMasks = _pageImageMasks; @synthesize cgImageMasks = _cgImageMasks; + (void)initialize { NSString *reqSysVer = @"7.0"; NSString *currSysVer = [[UIDevice currentDevice] systemVersion]; if ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending) { _defaultStyleForSystemVersion = SMPageControlDefaultStyleModern; } else { _defaultStyleForSystemVersion = SMPageControlDefaultStyleClassic; } } - (void)_initialize { _numberOfPages = 0; _tapBehavior = SMPageControlTapBehaviorStep; self.backgroundColor = [UIColor clearColor]; // If the app wasn't linked against iOS 7 or newer, always use the classic style // otherwise, use the style of the current OS. #ifdef __IPHONE_7_0 [self setStyleWithDefaults:_defaultStyleForSystemVersion]; #else [self setStyleWithDefaults:SMPageControlDefaultStyleClassic]; #endif _alignment = SMPageControlAlignmentCenter; _verticalAlignment = SMPageControlVerticalAlignmentMiddle; self.isAccessibilityElement = YES; self.accessibilityTraits = UIAccessibilityTraitUpdatesFrequently; self.accessibilityPageControl = [[UIPageControl alloc] init]; self.contentMode = UIViewContentModeRedraw; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (nil == self) { return nil; } [self _initialize]; return self; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (nil == self) { return nil; } [self _initialize]; return self; } - (void)dealloc { if (_pageImageMask) { CGImageRelease(_pageImageMask); } } - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); [self _renderPages:context rect:rect]; } - (void)_renderPages:(CGContextRef)context rect:(CGRect)rect { NSMutableArray *pageRects = [NSMutableArray arrayWithCapacity:self.numberOfPages]; if (_numberOfPages < 2 && _hidesForSinglePage) { return; } CGFloat left = [self _leftOffset]; CGFloat xOffset = left; CGFloat yOffset = 0.0f; UIColor *fillColor = nil; UIImage *image = nil; CGImageRef maskingImage = nil; CGSize maskSize = CGSizeZero; for (NSInteger i = 0; i < _numberOfPages; i++) { NSNumber *indexNumber = @(i); if (i == _displayedPage) { fillColor = _currentPageIndicatorTintColor ? _currentPageIndicatorTintColor : [UIColor whiteColor]; image = _currentPageImages[indexNumber]; if (nil == image) { image = _currentPageIndicatorImage; } } else { fillColor = _pageIndicatorTintColor ? _pageIndicatorTintColor : [[UIColor whiteColor] colorWithAlphaComponent:0.3f]; image = _pageImages[indexNumber]; if (nil == image) { image = _pageIndicatorImage; } } // If no finished images have been set, try a masking image if (nil == image) { maskingImage = (__bridge CGImageRef)_cgImageMasks[indexNumber]; UIImage *originalImage = _pageImageMasks[indexNumber]; maskSize = originalImage.size; // If no per page mask is set, try for a global page mask! if (nil == maskingImage) { maskingImage = _pageImageMask; maskSize = _pageIndicatorMaskImage.size; } } [fillColor set]; CGRect indicatorRect; if (image) { yOffset = [self _topOffsetForHeight:image.size.height rect:rect]; CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - image.size.width) / 2.0f); [image drawAtPoint:CGPointMake(centeredXOffset, yOffset)]; indicatorRect = CGRectMake(centeredXOffset, yOffset, image.size.width, image.size.height); } else if (maskingImage) { yOffset = [self _topOffsetForHeight:maskSize.height rect:rect]; CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - maskSize.width) / 2.0f); indicatorRect = CGRectMake(centeredXOffset, yOffset, maskSize.width, maskSize.height); CGContextDrawImage(context, indicatorRect, maskingImage); } else { yOffset = [self _topOffsetForHeight:_indicatorDiameter rect:rect]; CGFloat centeredXOffset = xOffset + floorf((_measuredIndicatorWidth - _indicatorDiameter) / 2.0f); indicatorRect = CGRectMake(centeredXOffset, yOffset, _indicatorDiameter, _indicatorDiameter); CGContextFillEllipseInRect(context, indicatorRect); } [pageRects addObject:[NSValue valueWithCGRect:indicatorRect]]; maskingImage = NULL; xOffset += _measuredIndicatorWidth + _indicatorMargin; } self.pageRects = pageRects; } - (CGFloat)_leftOffset { CGRect rect = self.bounds; CGSize size = [self sizeForNumberOfPages:self.numberOfPages]; CGFloat left = 0.0f; switch (_alignment) { case SMPageControlAlignmentCenter: left = ceilf(CGRectGetMidX(rect) - (size.width / 2.0f)); break; case SMPageControlAlignmentRight: left = CGRectGetMaxX(rect) - size.width; break; default: break; } return left; } - (CGFloat)_topOffsetForHeight:(CGFloat)height rect:(CGRect)rect { CGFloat top = 0.0f; switch (_verticalAlignment) { case SMPageControlVerticalAlignmentMiddle: top = CGRectGetMidY(rect) - (height / 2.0f); break; case SMPageControlVerticalAlignmentBottom: top = CGRectGetMaxY(rect) - height; break; default: break; } return top; } - (void)updateCurrentPageDisplay { _displayedPage = _currentPage; [self setNeedsDisplay]; } - (CGSize)sizeForNumberOfPages:(NSInteger)pageCount { CGFloat marginSpace = MAX(0, pageCount - 1) * _indicatorMargin; CGFloat indicatorSpace = pageCount * _measuredIndicatorWidth; CGSize size = CGSizeMake(marginSpace + indicatorSpace, _measuredIndicatorHeight); return size; } - (CGRect)rectForPageIndicator:(NSInteger)pageIndex { if (pageIndex < 0 || pageIndex >= _numberOfPages) { return CGRectZero; } CGFloat left = [self _leftOffset]; CGSize size = [self sizeForNumberOfPages:pageIndex + 1]; CGRect rect = CGRectMake(left + size.width - _measuredIndicatorWidth, 0, _measuredIndicatorWidth, _measuredIndicatorWidth); return rect; } - (void)_setImage:(UIImage *)image forPage:(NSInteger)pageIndex type:(SMPageControlImageType)type { if (pageIndex < 0 || pageIndex >= _numberOfPages) { return; } NSMutableDictionary *dictionary = nil; switch (type) { case SMPageControlImageTypeCurrent: dictionary = self.currentPageImages; break; case SMPageControlImageTypeNormal: dictionary = self.pageImages; break; case SMPageControlImageTypeMask: dictionary = self.pageImageMasks; break; default: break; } if (image) { dictionary[@(pageIndex)] = image; } else { [dictionary removeObjectForKey:@(pageIndex)]; } } - (void)setImage:(UIImage *)image forPage:(NSInteger)pageIndex { [self _setImage:image forPage:pageIndex type:SMPageControlImageTypeNormal]; [self _updateMeasuredIndicatorSizes]; } - (void)setCurrentImage:(UIImage *)image forPage:(NSInteger)pageIndex { [self _setImage:image forPage:pageIndex type:SMPageControlImageTypeCurrent];; [self _updateMeasuredIndicatorSizes]; } - (void)setImageMask:(UIImage *)image forPage:(NSInteger)pageIndex { [self _setImage:image forPage:pageIndex type:SMPageControlImageTypeMask]; if (nil == image) { [self.cgImageMasks removeObjectForKey:@(pageIndex)]; return; } CGImageRef maskImage = [self createMaskForImage:image]; if (maskImage) { self.cgImageMasks[@(pageIndex)] = (__bridge id)maskImage; CGImageRelease(maskImage); [self _updateMeasuredIndicatorSizeWithSize:image.size]; [self setNeedsDisplay]; } } - (id)_imageForPage:(NSInteger)pageIndex type:(SMPageControlImageType)type { if (pageIndex < 0 || pageIndex >= _numberOfPages) { return nil; } NSDictionary *dictionary = nil; switch (type) { case SMPageControlImageTypeCurrent: dictionary = _currentPageImages; break; case SMPageControlImageTypeNormal: dictionary = _pageImages; break; case SMPageControlImageTypeMask: dictionary = _pageImageMasks; break; default: break; } return dictionary[@(pageIndex)]; } - (UIImage *)imageForPage:(NSInteger)pageIndex { return [self _imageForPage:pageIndex type:SMPageControlImageTypeNormal]; } - (UIImage *)currentImageForPage:(NSInteger)pageIndex { return [self _imageForPage:pageIndex type:SMPageControlImageTypeCurrent]; } - (UIImage *)imageMaskForPage:(NSInteger)pageIndex { return [self _imageForPage:pageIndex type:SMPageControlImageTypeMask]; } - (CGSize)sizeThatFits:(CGSize)size { CGSize sizeThatFits = [self sizeForNumberOfPages:self.numberOfPages]; sizeThatFits.height = MAX(sizeThatFits.height, _minHeight); return sizeThatFits; } - (CGSize)intrinsicContentSize { if (_numberOfPages < 1 || (_numberOfPages < 2 && _hidesForSinglePage)) { return CGSizeMake(UIViewNoIntrinsicMetric, 0.0f); } CGSize intrinsicContentSize = CGSizeMake(UIViewNoIntrinsicMetric, MAX(_measuredIndicatorHeight, _minHeight)); return intrinsicContentSize; } - (void)updatePageNumberForScrollView:(UIScrollView *)scrollView { NSInteger page = (int)floorf(scrollView.contentOffset.x / scrollView.bounds.size.width); self.currentPage = page; } - (void)setScrollViewContentOffsetForCurrentPage:(UIScrollView *)scrollView animated:(BOOL)animated { CGPoint offset = scrollView.contentOffset; offset.x = scrollView.bounds.size.width * self.currentPage; [scrollView setContentOffset:offset animated:animated]; } - (void)setStyleWithDefaults:(SMPageControlStyleDefaults)defaultStyle { switch (defaultStyle) { case SMPageControlDefaultStyleModern: self.indicatorDiameter = DEFAULT_INDICATOR_WIDTH_LARGE; self.indicatorMargin = DEFAULT_INDICATOR_MARGIN_LARGE; self.pageIndicatorTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.2f]; self.minHeight = DEFAULT_MIN_HEIGHT_LARGE; break; case SMPageControlDefaultStyleClassic: default: self.indicatorDiameter = DEFAULT_INDICATOR_WIDTH; self.indicatorMargin = DEFAULT_INDICATOR_MARGIN; self.pageIndicatorTintColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3f]; self.minHeight = DEFAULT_MIN_HEIGHT; break; } } #pragma mark - - (CGImageRef)createMaskForImage:(UIImage *)image CF_RETURNS_RETAINED { size_t pixelsWide = image.size.width * image.scale; size_t pixelsHigh = image.size.height * image.scale; size_t bitmapBytesPerRow = (pixelsWide * 1); CGContextRef context = CGBitmapContextCreate(NULL, pixelsWide, pixelsHigh, CGImageGetBitsPerComponent(image.CGImage), bitmapBytesPerRow, NULL, (CGBitmapInfo)kCGImageAlphaOnly); CGContextTranslateCTM(context, 0.f, pixelsHigh); CGContextScaleCTM(context, 1.0f, -1.0f); CGContextDrawImage(context, CGRectMake(0, 0, pixelsWide, pixelsHigh), image.CGImage); CGImageRef maskImage = CGBitmapContextCreateImage(context); CGContextRelease(context); return maskImage; } - (void)_updateMeasuredIndicatorSizeWithSize:(CGSize)size { _measuredIndicatorWidth = MAX(_measuredIndicatorWidth, size.width); _measuredIndicatorHeight = MAX(_measuredIndicatorHeight, size.height); } - (void)_updateMeasuredIndicatorSizes { _measuredIndicatorWidth = _indicatorDiameter; _measuredIndicatorHeight = _indicatorDiameter; // If we're only using images, ignore the _indicatorDiameter if ( (self.pageIndicatorImage || self.pageIndicatorMaskImage) && self.currentPageIndicatorImage ) { _measuredIndicatorWidth = 0; _measuredIndicatorHeight = 0; } if (self.pageIndicatorImage) { [self _updateMeasuredIndicatorSizeWithSize:self.pageIndicatorImage.size]; } if (self.currentPageIndicatorImage) { [self _updateMeasuredIndicatorSizeWithSize:self.currentPageIndicatorImage.size]; } if (self.pageIndicatorMaskImage) { [self _updateMeasuredIndicatorSizeWithSize:self.pageIndicatorMaskImage.size]; } if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { [self invalidateIntrinsicContentSize]; } } #pragma mark - Tap Gesture // We're using touchesEnded: because we want to mimick UIPageControl as close as possible // As of iOS 6, UIPageControl still (as far as we know) does not use a tap gesture recognizer. This means that actions like // touching down, sliding around, and releasing, still results in the page incrementing or decrementing. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self]; if (SMPageControlTapBehaviorJump == self.tapBehavior) { __block NSInteger tappedIndicatorIndex = NSNotFound; [self.pageRects enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger index, BOOL *stop) { CGRect indicatorRect = [value CGRectValue]; if (CGRectContainsPoint(indicatorRect, point)) { tappedIndicatorIndex = index; *stop = YES; } }]; if (NSNotFound != tappedIndicatorIndex) { [self setCurrentPage:tappedIndicatorIndex sendEvent:YES canDefer:YES]; return; } } CGSize size = [self sizeForNumberOfPages:self.numberOfPages]; CGFloat left = [self _leftOffset]; CGFloat middle = left + (size.width / 2.0f); if (point.x < middle) { [self setCurrentPage:self.currentPage - 1 sendEvent:YES canDefer:YES]; } else { [self setCurrentPage:self.currentPage + 1 sendEvent:YES canDefer:YES]; } } #pragma mark - Accessors - (void)setFrame:(CGRect)frame { [super setFrame:frame]; [self setNeedsDisplay]; } - (void)setIndicatorDiameter:(CGFloat)indicatorDiameter { if (indicatorDiameter == _indicatorDiameter) { return; } _indicatorDiameter = indicatorDiameter; // Absolute minimum height of the control is the indicator diameter if (_minHeight < indicatorDiameter) { self.minHeight = indicatorDiameter; } [self _updateMeasuredIndicatorSizes]; [self setNeedsDisplay]; } - (void)setIndicatorMargin:(CGFloat)indicatorMargin { if (indicatorMargin == _indicatorMargin) { return; } _indicatorMargin = indicatorMargin; [self setNeedsDisplay]; } - (void)setMinHeight:(CGFloat)minHeight { if (minHeight == _minHeight) { return; } // Absolute minimum height of the control is the indicator diameter if (minHeight < _indicatorDiameter) { minHeight = _indicatorDiameter; } _minHeight = minHeight; if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { [self invalidateIntrinsicContentSize]; } [self setNeedsLayout]; } - (void)setNumberOfPages:(NSInteger)numberOfPages { if (numberOfPages == _numberOfPages) { return; } self.accessibilityPageControl.numberOfPages = numberOfPages; _numberOfPages = MAX(0, numberOfPages); if ([self respondsToSelector:@selector(invalidateIntrinsicContentSize)]) { [self invalidateIntrinsicContentSize]; } [self updateAccessibilityValue]; [self setNeedsDisplay]; } - (void)setCurrentPage:(NSInteger)currentPage { [self setCurrentPage:currentPage sendEvent:NO canDefer:NO]; } - (void)setCurrentPage:(NSInteger)currentPage sendEvent:(BOOL)sendEvent canDefer:(BOOL)defer { _currentPage = MIN(MAX(0, currentPage), _numberOfPages - 1); self.accessibilityPageControl.currentPage = self.currentPage; [self updateAccessibilityValue]; if (NO == self.defersCurrentPageDisplay || NO == defer) { _displayedPage = _currentPage; [self setNeedsDisplay]; } if (sendEvent) { [self sendActionsForControlEvents:UIControlEventValueChanged]; } } - (void)setCurrentPageIndicatorImage:(UIImage *)currentPageIndicatorImage { if ([currentPageIndicatorImage isEqual:_currentPageIndicatorImage]) { return; } _currentPageIndicatorImage = currentPageIndicatorImage; [self _updateMeasuredIndicatorSizes]; [self setNeedsDisplay]; } - (void)setPageIndicatorImage:(UIImage *)pageIndicatorImage { if ([pageIndicatorImage isEqual:_pageIndicatorImage]) { return; } _pageIndicatorImage = pageIndicatorImage; [self _updateMeasuredIndicatorSizes]; [self setNeedsDisplay]; } - (void)setPageIndicatorMaskImage:(UIImage *)pageIndicatorMaskImage { if ([pageIndicatorMaskImage isEqual:_pageIndicatorMaskImage]) { return; } _pageIndicatorMaskImage = pageIndicatorMaskImage; if (_pageImageMask) { CGImageRelease(_pageImageMask); } _pageImageMask = [self createMaskForImage:_pageIndicatorMaskImage]; [self _updateMeasuredIndicatorSizes]; [self setNeedsDisplay]; } - (NSMutableDictionary *)pageNames { if (nil != _pageNames) { return _pageNames; } _pageNames = [[NSMutableDictionary alloc] init]; return _pageNames; } - (NSMutableDictionary *)pageImages { if (nil != _pageImages) { return _pageImages; } _pageImages = [[NSMutableDictionary alloc] init]; return _pageImages; } - (NSMutableDictionary *)currentPageImages { if (nil != _currentPageImages) { return _currentPageImages; } _currentPageImages = [[NSMutableDictionary alloc] init]; return _currentPageImages; } - (NSMutableDictionary *)pageImageMasks { if (nil != _pageImageMasks) { return _pageImageMasks; } _pageImageMasks = [[NSMutableDictionary alloc] init]; return _pageImageMasks; } - (NSMutableDictionary *)cgImageMasks { if (nil != _cgImageMasks) { return _cgImageMasks; } _cgImageMasks = [[NSMutableDictionary alloc] init]; return _cgImageMasks; } #pragma mark - UIAccessibility - (void)setName:(NSString *)name forPage:(NSInteger)pageIndex { if (pageIndex < 0 || pageIndex >= _numberOfPages) { return; } self.pageNames[@(pageIndex)] = name; } - (NSString *)nameForPage:(NSInteger)pageIndex { if (pageIndex < 0 || pageIndex >= _numberOfPages) { return nil; } return self.pageNames[@(pageIndex)]; } - (void)updateAccessibilityValue { NSString *pageName = [self nameForPage:self.currentPage]; NSString *accessibilityValue = self.accessibilityPageControl.accessibilityValue; if (pageName) { self.accessibilityValue = [NSString stringWithFormat:@"%@ - %@", pageName, accessibilityValue]; } else { self.accessibilityValue = accessibilityValue; } } @end