V8HorizontalPickerView.m 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. //
  2. // V8HorizontalPickerView.m
  3. //
  4. // Created by Shawn Veader on 9/17/10.
  5. // Copyright 2010 V8 Labs, LLC. All rights reserved.
  6. //
  7. #import "V8HorizontalPickerView.h"
  8. #import "UIView+Additions.h"
  9. #pragma mark - Internal Method Interface
  10. @implementation V8LabelNode
  11. @end
  12. @interface V8HorizontalPickerView () {
  13. UIScrollView *_scrollView;
  14. }
  15. // collection of widths of each element.
  16. @property (nonatomic, strong) NSMutableArray *elementWidths;
  17. @property (nonatomic, assign) NSInteger elementPadding;
  18. // state keepers
  19. @property (nonatomic, assign) BOOL dataHasBeenLoaded;
  20. @property (nonatomic, assign) BOOL scrollSizeHasBeenSet;
  21. @property (nonatomic, assign) BOOL scrollingBasedOnUserInteraction;
  22. // keep track of which elements are visible for tiling
  23. @property (nonatomic, assign) NSInteger firstVisibleElement;
  24. @property (nonatomic, assign) NSInteger lastVisibleElement;
  25. @end
  26. #pragma mark - Implementation
  27. @implementation V8HorizontalPickerView : UIView
  28. #pragma mark - Init/Dealloc
  29. - (id)initWithFrame:(CGRect)frame {
  30. self = [super initWithFrame:frame];
  31. if (self) {
  32. [self initSetup];
  33. }
  34. return self;
  35. }
  36. - (id)initWithCoder:(NSCoder *)theCoder {
  37. self = [super initWithCoder:theCoder];
  38. if (self) {
  39. [self initSetup];
  40. }
  41. return self;
  42. }
  43. - (void)dealloc {
  44. _scrollView.delegate = nil;
  45. self.delegate = nil;
  46. self.dataSource = nil;
  47. }
  48. - (void)initSetup {
  49. self.elementWidths = [NSMutableArray array];
  50. [self addScrollView];
  51. self.textColor = [UIColor blackColor];
  52. self.elementFont = [UIFont systemFontOfSize:12.0f];
  53. _currentSelectedIndex = -1; // nothing is selected yet
  54. _numberOfElements = 0;
  55. self.elementPadding = 0;
  56. self.dataHasBeenLoaded = NO;
  57. self.scrollSizeHasBeenSet = NO;
  58. self.scrollingBasedOnUserInteraction = NO;
  59. self.selectionPoint = CGPointZero;
  60. self.indicatorPosition = V8HorizontalPickerIndicatorBottom;
  61. self.firstVisibleElement = -1;
  62. self.lastVisibleElement = -1;
  63. self.scrollEdgeViewPadding = 0.0f;
  64. self.autoresizesSubviews = YES;
  65. }
  66. #pragma mark - LayoutSubViews
  67. - (void)layoutSubviews {
  68. [super layoutSubviews];
  69. BOOL adjustWhenFinished = NO;
  70. if (CGPointEqualToPoint(self.selectionPoint, CGPointZero)) {
  71. // default to the center
  72. self.selectionPoint = CGPointMake(self.frame.size.width / 2, 0.0f);
  73. }
  74. if (!self.dataHasBeenLoaded) {
  75. [self collectData];
  76. }
  77. if (!self.scrollSizeHasBeenSet) {
  78. adjustWhenFinished = YES;
  79. [self updateScrollContentInset];
  80. [self setTotalWidthOfScrollContent];
  81. }
  82. SEL titleForElementSelector = @selector(horizontalPickerView:titleForElementAtIndex:);
  83. SEL viewForElementSelector = @selector(horizontalPickerView:viewForElementAtIndex:);
  84. SEL setSelectedSelector = @selector(setSelectedElement:);
  85. CGRect visibleBounds = [self bounds];
  86. CGRect scaledViewFrame = CGRectZero;
  87. // remove any subviews that are no longer visible
  88. for (UIView *view in [_scrollView subviews]) {
  89. scaledViewFrame = [_scrollView convertRect:[view frame] toView:self];
  90. // if the view doesn't intersect, it's not visible, so we can recycle it
  91. if (!CGRectIntersectsRect(scaledViewFrame, visibleBounds)) {
  92. [view removeFromSuperview];
  93. } else { // if it is still visible, update it's selected state
  94. // view's tag is it's index
  95. BOOL isSelected = (self.currentSelectedIndex == [self indexForElement:view]);
  96. if (isSelected) {
  97. // if this view is set to be selected, make sure it is over the selection point
  98. // NSInteger currentIndex = [self nearestElementToCenter];
  99. // isSelected = (currentIndex == self.currentSelectedIndex);
  100. if (_selectedMaskView != nil) {
  101. [view addSubview:_selectedMaskView];
  102. _selectedMaskView.center = CGPointMake(view.width/2, view.height/2);
  103. }
  104. } else {
  105. [view removeAllSubViews];
  106. }
  107. if ([view respondsToSelector:setSelectedSelector]) {
  108. // casting to V8HorizontalPickerLabel so we can call this without all the NSInvocation jazz
  109. [(V8HorizontalPickerLabel *)view setSelectedElement:isSelected];
  110. }
  111. }
  112. }
  113. // find needed elements by looking at left and right edges of frame
  114. CGPoint offset = _scrollView.contentOffset;
  115. NSInteger firstNeededElement = [self nearestElementToPoint:CGPointMake(offset.x, 0.0f)];
  116. NSInteger lastNeededElement = [self nearestElementToPoint:CGPointMake(offset.x + visibleBounds.size.width, 0.0f)];
  117. // add any views that have become visible
  118. UIView *view = nil;
  119. CGRect tmpViewFrame = CGRectZero;
  120. CGPoint itemViewCenter = CGPointZero;
  121. for (NSInteger i = firstNeededElement; i <= lastNeededElement; i++) {
  122. view = nil; // paranoia
  123. view = [_scrollView viewWithTag:[self tagForElementAtIndex:i]];
  124. if (!view) {
  125. if (i < self.numberOfElements) { // make sure we are not requesting data out of range
  126. if (self.delegate && [self.delegate respondsToSelector:titleForElementSelector]) {
  127. NSString *title = [self.delegate horizontalPickerView:self titleForElementAtIndex:i];
  128. view = [self labelForForElementAtIndex:i withTitle:title];
  129. } else if (self.delegate && [self.delegate respondsToSelector:viewForElementSelector]) {
  130. view = [self.delegate horizontalPickerView:self viewForElementAtIndex:i];
  131. // move view's center to the center of item's ideal frame
  132. tmpViewFrame = [self frameForElementAtIndex:i];
  133. itemViewCenter = CGPointMake((tmpViewFrame.size.width / 2.0f) + tmpViewFrame.origin.x, (tmpViewFrame.size.height / 2.0f));
  134. view.center = itemViewCenter;
  135. }
  136. if (view) {
  137. // use the index as the tag so we can find it later
  138. view.tag = [self tagForElementAtIndex:i];
  139. [_scrollView addSubview:view];
  140. }
  141. }
  142. }
  143. }
  144. // add the left or right edge views if visible
  145. CGRect viewFrame = CGRectZero;
  146. if (self.leftScrollEdgeView) {
  147. viewFrame = [self frameForLeftScrollEdgeView];
  148. scaledViewFrame = [_scrollView convertRect:viewFrame toView:self];
  149. if (CGRectIntersectsRect(scaledViewFrame, visibleBounds) && ![self.leftScrollEdgeView isDescendantOfView:_scrollView]) {
  150. self.leftScrollEdgeView.frame = viewFrame;
  151. [_scrollView addSubview:self.leftScrollEdgeView];
  152. }
  153. }
  154. if (self.rightScrollEdgeView) {
  155. viewFrame = [self frameForRightScrollEdgeView];
  156. scaledViewFrame = [_scrollView convertRect:viewFrame toView:self];
  157. if (CGRectIntersectsRect(scaledViewFrame, visibleBounds) && ![self.rightScrollEdgeView isDescendantOfView:_scrollView]) {
  158. self.rightScrollEdgeView.frame = viewFrame;
  159. [_scrollView addSubview:self.rightScrollEdgeView];
  160. }
  161. }
  162. // save off what's visible now
  163. self.firstVisibleElement = firstNeededElement;
  164. self.lastVisibleElement = lastNeededElement;
  165. // determine if scroll view needs to shift in response to resizing?
  166. if (self.currentSelectedIndex > -1 && [self centerOfElementAtIndex:self.currentSelectedIndex] != [self currentCenter].x) {
  167. if (adjustWhenFinished) {
  168. [self scrollToElement:self.currentSelectedIndex animated:NO];
  169. } else if (self.numberOfElements <= self.currentSelectedIndex) {
  170. // if currentSelectedIndex no longer exists, select what is currently centered
  171. _currentSelectedIndex = [self nearestElementToCenter];
  172. [self scrollToElement:self.currentSelectedIndex animated:NO];
  173. }
  174. }
  175. }
  176. #pragma mark - Getters and Setters
  177. - (void)setDelegate:(id)newDelegate {
  178. if (self.delegate != newDelegate) {
  179. _delegate = newDelegate;
  180. [self collectData];
  181. }
  182. }
  183. - (void)setDataSource:(id)newDataSource {
  184. if (self.dataSource != newDataSource) {
  185. _dataSource = newDataSource;
  186. [self collectData];
  187. }
  188. }
  189. - (void)setSelectionPoint:(CGPoint)point {
  190. if (!CGPointEqualToPoint(point, self.selectionPoint)) {
  191. _selectionPoint = point;
  192. [self updateScrollContentInset];
  193. }
  194. }
  195. // allow the setting of this views background color to change the scroll view
  196. - (void)setBackgroundColor:(UIColor *)newColor {
  197. [super setBackgroundColor:newColor];
  198. _scrollView.backgroundColor = newColor;
  199. for (UIView *view in [_scrollView subviews]) {
  200. view.backgroundColor = newColor;
  201. }
  202. }
  203. - (void)setIndicatorPosition:(V8HorizontalPickerIndicatorPosition)position {
  204. if (self.indicatorPosition != position) {
  205. _indicatorPosition = position;
  206. [self drawPositionIndicator];
  207. }
  208. }
  209. - (void)setSelectionIndicatorView:(UIView *)indicatorView {
  210. if (self.selectionIndicatorView != indicatorView) {
  211. if (self.selectionIndicatorView) {
  212. [self.selectionIndicatorView removeFromSuperview];
  213. }
  214. _selectionIndicatorView = indicatorView;
  215. [self drawPositionIndicator];
  216. }
  217. }
  218. - (void)setLeftEdgeView:(UIView *)leftView {
  219. if (self.leftEdgeView != leftView) {
  220. if (self.leftEdgeView) {
  221. [self.leftEdgeView removeFromSuperview];
  222. }
  223. _leftEdgeView = leftView;
  224. CGRect tmpFrame = self.leftEdgeView.frame;
  225. tmpFrame.origin.x = 0.0f;
  226. tmpFrame.origin.y = 0.0f;
  227. self.leftEdgeView.frame = tmpFrame;
  228. [self addSubview:self.leftEdgeView];
  229. }
  230. }
  231. - (void)setRightEdgeView:(UIView *)rightView {
  232. if (self.rightEdgeView != rightView) {
  233. if (self.rightEdgeView) {
  234. [self.rightEdgeView removeFromSuperview];
  235. }
  236. _rightEdgeView = rightView;
  237. CGRect tmpFrame = self.rightEdgeView.frame;
  238. tmpFrame.origin.x = self.frame.size.width - tmpFrame.size.width;
  239. tmpFrame.origin.y = 0.0f;
  240. self.rightEdgeView.frame = tmpFrame;
  241. [self addSubview:self.rightEdgeView];
  242. }
  243. }
  244. - (void)setLeftScrollEdgeView:(UIView *)leftView {
  245. if (self.leftScrollEdgeView != leftView) {
  246. if (self.leftScrollEdgeView) {
  247. [self.leftScrollEdgeView removeFromSuperview];
  248. }
  249. _leftScrollEdgeView = leftView;
  250. self.scrollSizeHasBeenSet = NO;
  251. [self setNeedsLayout];
  252. }
  253. }
  254. - (void)setRightScrollEdgeView:(UIView *)rightView {
  255. if (self.rightScrollEdgeView != rightView) {
  256. if (self.rightScrollEdgeView) {
  257. [self.rightScrollEdgeView removeFromSuperview];
  258. }
  259. _rightScrollEdgeView = rightView;
  260. self.scrollSizeHasBeenSet = NO;
  261. [self setNeedsLayout];
  262. }
  263. }
  264. - (void)setFrame:(CGRect)newFrame {
  265. if (!CGRectEqualToRect(self.frame, newFrame)) {
  266. // causes recalulation of offsets, etc based on new size
  267. self.scrollSizeHasBeenSet = NO;
  268. }
  269. [super setFrame:newFrame];
  270. }
  271. #pragma mark - Data Fetching Methods
  272. - (void)reloadData {
  273. // remove all scrollview subviews and "recycle" them
  274. for (UIView *view in [_scrollView subviews]) {
  275. [view removeFromSuperview];
  276. }
  277. self.firstVisibleElement = NSIntegerMax;
  278. self.lastVisibleElement = NSIntegerMin;
  279. [self collectData];
  280. }
  281. - (void)collectData {
  282. self.scrollSizeHasBeenSet = NO;
  283. self.dataHasBeenLoaded = NO;
  284. [self getNumberOfElementsFromDataSource];
  285. [self getElementWidthsFromDelegate];
  286. [self setTotalWidthOfScrollContent];
  287. [self updateScrollContentInset];
  288. self.dataHasBeenLoaded = YES;
  289. [self setNeedsLayout];
  290. }
  291. #pragma mark - Scroll To Element Method
  292. - (void)scrollToElement:(NSInteger)index animated:(BOOL)animate {
  293. _currentSelectedIndex = index;
  294. // int x = [self centerOfElementAtIndex:index] - self.selectionPoint.x;
  295. if (index == 0) {
  296. [_scrollView setContentOffset:CGPointMake(0, 0) animated:animate];
  297. }
  298. // notify delegate of the selected index
  299. SEL delegateCall = @selector(horizontalPickerView:didSelectElementAtIndex:);
  300. if (self.delegate && [self.delegate respondsToSelector:delegateCall]) {
  301. [self.delegate horizontalPickerView:self didSelectElementAtIndex:index];
  302. }
  303. #if (__IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_4_3)
  304. [self setNeedsLayout];
  305. #endif
  306. }
  307. #pragma mark - UIScrollViewDelegate Methods
  308. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  309. if (self.scrollingBasedOnUserInteraction) {
  310. // NOTE: sizing and/or changing orientation of control might cause scrolling
  311. // not initiated by user. do not update current selection in these
  312. // cases so that the view state is properly preserved.
  313. // set the current item under the center to "highlighted" or current
  314. //_currentSelectedIndex = [self nearestElementToCenter];
  315. }
  316. #if (__IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_4_3)
  317. [self setNeedsLayout];
  318. #endif
  319. }
  320. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  321. self.scrollingBasedOnUserInteraction = YES;
  322. }
  323. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  324. // only do this if we aren't decelerating
  325. if (!decelerate) {
  326. [self scrollToElementNearestToCenter];
  327. }
  328. }
  329. //- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView { }
  330. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  331. [self scrollToElementNearestToCenter];
  332. }
  333. - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
  334. self.scrollingBasedOnUserInteraction = NO;
  335. }
  336. #pragma mark - View Creation Methods (Internal Methods)
  337. - (void)addScrollView {
  338. if (_scrollView == nil) {
  339. _scrollView = [[UIScrollView alloc] initWithFrame:self.bounds];
  340. _scrollView.delegate = self;
  341. _scrollView.scrollEnabled = YES;
  342. _scrollView.scrollsToTop = NO;
  343. _scrollView.showsVerticalScrollIndicator = NO;
  344. _scrollView.showsHorizontalScrollIndicator = NO;
  345. _scrollView.bouncesZoom = NO;
  346. _scrollView.alwaysBounceHorizontal = YES;
  347. _scrollView.alwaysBounceVertical = NO;
  348. _scrollView.minimumZoomScale = 1.0; // setting min/max the same disables zooming
  349. _scrollView.maximumZoomScale = 1.0;
  350. _scrollView.contentInset = UIEdgeInsetsZero;
  351. _scrollView.decelerationRate = 0.1; //UIScrollViewDecelerationRateNormal;
  352. _scrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  353. _scrollView.autoresizesSubviews = YES;
  354. UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTapped:)];
  355. [_scrollView addGestureRecognizer:tapRecognizer];
  356. [self addSubview:_scrollView];
  357. }
  358. }
  359. - (void)drawPositionIndicator {
  360. CGRect indicatorFrame = self.selectionIndicatorView.frame;
  361. CGFloat x = self.selectionPoint.x - (indicatorFrame.size.width / 2);
  362. CGFloat y;
  363. switch (self.indicatorPosition) {
  364. case V8HorizontalPickerIndicatorTop: {
  365. y = 0.0f;
  366. break;
  367. }
  368. case V8HorizontalPickerIndicatorBottom: {
  369. y = self.frame.size.height - indicatorFrame.size.height;
  370. break;
  371. }
  372. default:
  373. break;
  374. }
  375. // properly place indicator image in view relative to selection point
  376. CGRect tmpFrame = CGRectMake(x, y, indicatorFrame.size.width, indicatorFrame.size.height);
  377. self.selectionIndicatorView.frame = tmpFrame;
  378. [self addSubview:self.selectionIndicatorView];
  379. }
  380. // create a UILabel for this element.
  381. - (V8HorizontalPickerLabel *)labelForForElementAtIndex:(NSInteger)index withTitle:(NSString *)title {
  382. CGRect labelFrame = [self frameForElementAtIndex:index];
  383. V8HorizontalPickerLabel *elementLabel = [[V8HorizontalPickerLabel alloc] initWithFrame:labelFrame];
  384. elementLabel.textAlignment = NSTextAlignmentCenter;
  385. elementLabel.backgroundColor = self.backgroundColor;
  386. elementLabel.text = title;
  387. elementLabel.font = self.elementFont;
  388. elementLabel.normalStateColor = self.textColor;
  389. elementLabel.selectedStateColor = self.selectedTextColor;
  390. // show selected status if this element is the selected one and is currently over selectionPoint
  391. NSInteger currentIndex = [self nearestElementToCenter];
  392. elementLabel.selectedElement = (self.currentSelectedIndex == index) && (currentIndex == self.currentSelectedIndex);
  393. return elementLabel;
  394. }
  395. #pragma mark - DataSource Calling Method (Internal Method)
  396. - (void)getNumberOfElementsFromDataSource {
  397. SEL dataSourceCall = @selector(numberOfElementsInHorizontalPickerView:);
  398. if (self.dataSource && [self.dataSource respondsToSelector:dataSourceCall]) {
  399. _numberOfElements = [self.dataSource numberOfElementsInHorizontalPickerView:self];
  400. } else {
  401. _numberOfElements = 0;
  402. }
  403. }
  404. #pragma mark - Delegate Calling Method (Internal Method)
  405. - (void)getElementWidthsFromDelegate {
  406. SEL delegateCall = @selector(horizontalPickerView:widthForElementAtIndex:);
  407. [self.elementWidths removeAllObjects];
  408. for (int i = 0; i < self.numberOfElements; i++) {
  409. if (self.delegate && [self.delegate respondsToSelector:delegateCall]) {
  410. NSInteger width = [self.delegate horizontalPickerView:self widthForElementAtIndex:i];
  411. [self.elementWidths addObject:[NSNumber numberWithInteger:width]];
  412. }
  413. }
  414. }
  415. #pragma mark - View Calculation and Manipulation Methods (Internal Methods)
  416. // what is the total width of the content area?
  417. - (void)setTotalWidthOfScrollContent {
  418. NSInteger totalWidth = 0;
  419. totalWidth += [self leftScrollEdgeWidth];
  420. totalWidth += [self rightScrollEdgeWidth];
  421. // sum the width of all elements
  422. for (NSNumber *width in self.elementWidths) {
  423. totalWidth += [width intValue];
  424. totalWidth += self.elementPadding;
  425. }
  426. // TODO: is this necessary?
  427. totalWidth -= self.elementPadding; // we add "one too many" in for loop
  428. if (_scrollView) {
  429. // create our scroll view as wide as all the elements to be included
  430. _scrollView.contentSize = CGSizeMake(totalWidth, self.bounds.size.height);
  431. self.scrollSizeHasBeenSet = YES;
  432. }
  433. }
  434. // reset the content inset of the scroll view based on centering first and last elements.
  435. - (void)updateScrollContentInset {
  436. // update content inset if we have element widths
  437. if ([self.elementWidths count] != 0) {
  438. CGFloat scrollerWidth = _scrollView.frame.size.width;
  439. CGFloat halfFirstWidth = 0.0f;
  440. CGFloat halfLastWidth = 0.0f;
  441. if ( [self.elementWidths count] > 0 ) {
  442. halfFirstWidth = [[self.elementWidths objectAtIndex:0] floatValue] / 2.0;
  443. halfLastWidth = [[self.elementWidths lastObject] floatValue] / 2.0;
  444. }
  445. // calculating the inset so that the bouncing on the ends happens more smooothly
  446. // - first inset is the distance from the left edge to the left edge of the
  447. // first element when that element is centered under the selection point.
  448. // - represented below as the # area
  449. // - last inset is the distance from the right edge to the right edge of
  450. // the last element when that element is centered under the selection point.
  451. // - represented below as the * area
  452. //
  453. // Selection
  454. // +---------|---------------+
  455. // |####| Element |**********| << UIScrollView
  456. // +-------------------------+
  457. CGFloat firstInset = self.selectionPoint.x - halfFirstWidth;
  458. firstInset -= [self leftScrollEdgeWidth];
  459. CGFloat lastInset = (scrollerWidth - self.selectionPoint.x) - halfLastWidth;
  460. lastInset -= [self rightScrollEdgeWidth];
  461. _scrollView.contentInset = UIEdgeInsetsMake(0, firstInset, 0, lastInset);
  462. }
  463. }
  464. // what is the left-most edge of the element at the given index?
  465. - (NSInteger)offsetForElementAtIndex:(NSInteger)index {
  466. NSInteger offset = 0;
  467. if (index >= [self.elementWidths count]) {
  468. return 0;
  469. }
  470. offset += [self leftScrollEdgeWidth];
  471. for (int i = 0; i < index && i < [self.elementWidths count]; i++) {
  472. offset += [[self.elementWidths objectAtIndex:i] intValue];
  473. offset += self.elementPadding;
  474. }
  475. return offset;
  476. }
  477. // return the tag for an element at a given index
  478. - (NSInteger)tagForElementAtIndex:(NSInteger)index {
  479. return (index + 1) * 10;
  480. }
  481. // return the index given an element's tag
  482. - (NSInteger)indexForElement:(UIView *)element {
  483. return (element.tag / 10) - 1;
  484. }
  485. // what is the center of the element at the given index?
  486. - (NSInteger)centerOfElementAtIndex:(NSInteger)index {
  487. if (index >= [self.elementWidths count]) {
  488. return 0;
  489. }
  490. NSInteger elementOffset = [self offsetForElementAtIndex:index];
  491. NSInteger elementWidth = [[self.elementWidths objectAtIndex:index] intValue] / 2;
  492. return elementOffset + elementWidth;
  493. }
  494. // what is the frame for the element at the given index?
  495. - (CGRect)frameForElementAtIndex:(NSInteger)index {
  496. CGFloat width = 0.0f;
  497. if ([self.elementWidths count] > index) {
  498. width = [[self.elementWidths objectAtIndex:index] intValue];
  499. }
  500. return CGRectMake([self offsetForElementAtIndex:index], 0.0f, width, self.frame.size.height);
  501. }
  502. // what is the frame for the left scroll edge view?
  503. - (CGRect)frameForLeftScrollEdgeView {
  504. if (self.leftScrollEdgeView) {
  505. CGFloat scrollHeight = _scrollView.contentSize.height;
  506. CGFloat viewHeight = self.leftScrollEdgeView.frame.size.height;
  507. return CGRectMake(0.0f, ((scrollHeight / 2.0f) - (viewHeight / 2.0f)),
  508. self.leftScrollEdgeView.frame.size.width, viewHeight);
  509. } else {
  510. return CGRectZero;
  511. }
  512. }
  513. // what is the width of the left edge of the scroll area?
  514. - (CGFloat)leftScrollEdgeWidth {
  515. if (self.leftScrollEdgeView) {
  516. CGFloat width = self.leftScrollEdgeView.frame.size.width;
  517. width += self.scrollEdgeViewPadding;
  518. return width;
  519. }
  520. return 0.0f;
  521. }
  522. // what is the frame for the right scroll edge view?
  523. - (CGRect)frameForRightScrollEdgeView {
  524. if (self.rightScrollEdgeView) {
  525. CGFloat scrollWidth = _scrollView.contentSize.width;
  526. CGFloat scrollHeight = _scrollView.contentSize.height;
  527. CGFloat viewWidth = self.rightScrollEdgeView.frame.size.width;
  528. CGFloat viewHeight = self.rightScrollEdgeView.frame.size.height;
  529. return CGRectMake(scrollWidth - viewWidth, ((scrollHeight / 2.0f) - (viewHeight / 2.0f)),
  530. viewWidth, viewHeight);
  531. } else {
  532. return CGRectZero;
  533. }
  534. }
  535. // what is the width of the right edge of the scroll area?
  536. - (CGFloat)rightScrollEdgeWidth {
  537. if (self.rightScrollEdgeView) {
  538. CGFloat width = self.rightScrollEdgeView.frame.size.width;
  539. width += self.scrollEdgeViewPadding;
  540. return width;
  541. }
  542. return 0.0f;
  543. }
  544. // what is the "center", relative to the content offset and adjusted to selection point?
  545. - (CGPoint)currentCenter {
  546. CGFloat x = _scrollView.contentOffset.x + self.selectionPoint.x;
  547. return CGPointMake(x, 0.0f);
  548. }
  549. // what is the element nearest to the center of the view?
  550. - (NSInteger)nearestElementToCenter {
  551. return [self nearestElementToPoint:[self currentCenter]];
  552. }
  553. // what is the element nearest to the given point?
  554. - (NSInteger)nearestElementToPoint:(CGPoint)point {
  555. for (int i = 0; i < self.numberOfElements; i++) {
  556. CGRect frame = [self frameForElementAtIndex:i];
  557. if (CGRectContainsPoint(frame, point)) {
  558. return i;
  559. } else if (point.x < frame.origin.x) {
  560. // if the center is before this element, go back to last one,
  561. // unless we're at the beginning
  562. if (i > 0) {
  563. return i - 1;
  564. } else {
  565. return 0;
  566. }
  567. break;
  568. } else if (point.x > frame.origin.y) {
  569. // if the center is past the last element, scroll to it
  570. if (i == self.numberOfElements - 1) {
  571. return i;
  572. }
  573. }
  574. }
  575. return 0;
  576. }
  577. // similar to nearestElementToPoint: however, this method does not look past beginning/end
  578. - (NSInteger)elementContainingPoint:(CGPoint)point {
  579. for (int i = 0; i < self.numberOfElements; i++) {
  580. CGRect frame = [self frameForElementAtIndex:i];
  581. if (CGRectContainsPoint(frame, point)) {
  582. return i;
  583. }
  584. }
  585. return -1;
  586. }
  587. // move scroll view to position nearest element under the center
  588. - (void)scrollToElementNearestToCenter {
  589. // [self scrollToElement:[self nearestElementToCenter] animated:YES];
  590. }
  591. #pragma mark - Tap Gesture Recognizer Handler Method
  592. // use the gesture recognizer to slide to element under tap
  593. - (void)scrollViewTapped:(UITapGestureRecognizer *)recognizer {
  594. if (recognizer.state == UIGestureRecognizerStateRecognized) {
  595. CGPoint tapLocation = [recognizer locationInView:_scrollView];
  596. NSInteger elementIndex = [self elementContainingPoint:tapLocation];
  597. if (elementIndex != -1) { // point not in element
  598. [self scrollToElement:elementIndex animated:YES];
  599. }
  600. }
  601. }
  602. @end
  603. // ------------------------------------------------------------------------
  604. #pragma mark - Picker Label Implementation
  605. @implementation V8HorizontalPickerLabel : UILabel
  606. - (void)setSelectedElement:(BOOL)selected {
  607. if (self.selectedElement != selected) {
  608. if (selected) {
  609. self.textColor = self.selectedStateColor;
  610. } else {
  611. self.textColor = self.normalStateColor;
  612. }
  613. _selectedElement = selected;
  614. [self setNeedsLayout];
  615. }
  616. }
  617. - (void)setNormalStateColor:(UIColor *)color {
  618. if (self.normalStateColor != color) {
  619. _normalStateColor = color;
  620. self.textColor = self.normalStateColor;
  621. [self setNeedsLayout];
  622. }
  623. }
  624. @end