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