TOScrollBar.m 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. //
  2. // TOScrollBar.m
  3. //
  4. // Copyright 2016-2017 Timothy Oliver. All rights reserved.
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to
  8. // deal in the Software without restriction, including without limitation the
  9. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  10. // sell copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
  21. // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. #import "TOScrollBar.h"
  23. #import "UIScrollView+TOScrollBar.h"
  24. #import "TOScrollBarGestureRecognizer.h"
  25. /** Default values for the scroll bar */
  26. static const CGFloat kTOScrollBarTrackWidth = 2.0f; // The default width of the scrollable space indicator
  27. static const CGFloat kTOScrollBarHandleWidth = 4.0f; // The default width of the handle control
  28. static const CGFloat kTOScrollBarEdgeInset = 7.5f; // The distance from the edge of the view to the center of the track
  29. static const CGFloat kTOScrollBarHandleMinHeight = 64.0f; // The minimum usable size to which the handle can shrink
  30. static const CGFloat kTOScrollBarWidth = 20.0f; // The width of this control (44 is minimum recommended tapping space) TWS how handleWidth
  31. static const CGFloat kTOScrollBarVerticalPadding = 10.0f; // The default padding at the top and bottom of the view
  32. static const CGFloat kTOScrollBarMinimumContentScale = 5.0f; // The minimum scale of the content view before showing the scroll view is necessary
  33. /************************************************************************/
  34. // A struct to hold the scroll view's previous state before this bar was applied
  35. struct TOScrollBarScrollViewState {
  36. BOOL showsVerticalScrollIndicator;
  37. };
  38. typedef struct TOScrollBarScrollViewState TOScrollBarScrollViewState;
  39. /************************************************************************/
  40. // Private interface exposure for scroll view category
  41. @interface UIScrollView () //TOScrollBar
  42. - (void)setTo_scrollBar:(TOScrollBar *)scrollBar;
  43. @end
  44. /************************************************************************/
  45. @interface TOScrollBar () <UIGestureRecognizerDelegate> {
  46. TOScrollBarScrollViewState _scrollViewState;
  47. }
  48. @property (nonatomic, weak, readwrite) UIScrollView *scrollView; // The parent scroll view in which we belong
  49. @property (nonatomic, assign) BOOL userHidden; // View was explicitly hidden by the user as opposed to us
  50. @property (nonatomic, strong) UIImageView *trackView; // The track indicating the scrollable distance
  51. @property (nonatomic, strong) UIImageView *handleView; // The handle that may be dragged in the scroll bar
  52. @property (nonatomic, assign, readwrite) BOOL dragging; // The user is presently dragging the handle
  53. @property (nonatomic, assign) CGFloat yOffset; // The offset from the center of the thumb
  54. @property (nonatomic, assign) CGFloat originalYOffset; // The original placement of the scroll bar when the user started dragging
  55. @property (nonatomic, assign) CGFloat originalHeight; // The original height of the scroll bar when the user started dragging
  56. @property (nonatomic, assign) CGFloat originalTopInset; // The original safe area inset of the scroll bar when the user started dragging
  57. @property (nonatomic, assign) CGFloat horizontalOffset; // The horizontal offset when the edge inset is too small for the touch region
  58. @property (nonatomic, assign) BOOL disabled; // Disabled when there's not enough scroll content to merit showing this
  59. @property (nonatomic, strong) UIImpactFeedbackGenerator *feedbackGenerator; // Taptic feedback for iPhone 7 and above
  60. @property (nonatomic, strong) TOScrollBarGestureRecognizer *gestureRecognizer; // Our custom recognizer for handling user interactions with the scroll bar
  61. @end
  62. /************************************************************************/
  63. @implementation TOScrollBar
  64. #pragma mark - Class Creation -
  65. - (instancetype)initWithStyle:(TOScrollBarStyle)style
  66. {
  67. if (self = [super initWithFrame:CGRectZero]) {
  68. _style = style;
  69. [self setUpInitialProperties];
  70. }
  71. return self;
  72. }
  73. - (instancetype)initWithFrame:(CGRect)frame
  74. {
  75. if (self = [super initWithFrame:frame]) {
  76. [self setUpInitialProperties];
  77. }
  78. return self;
  79. }
  80. - (instancetype)initWithCoder:(NSCoder *)aDecoder
  81. {
  82. if (self = [super initWithCoder:aDecoder]) {
  83. [self setUpInitialProperties];
  84. }
  85. return self;
  86. }
  87. #pragma mark - Set-up -
  88. - (void)setUpInitialProperties
  89. {
  90. _trackWidth = kTOScrollBarTrackWidth;
  91. _handleWidth = kTOScrollBarHandleWidth;
  92. _edgeInset = kTOScrollBarEdgeInset;
  93. _handleMinimiumHeight = kTOScrollBarHandleMinHeight;
  94. _minimumContentHeightScale = kTOScrollBarMinimumContentScale;
  95. _verticalInset = UIEdgeInsetsMake(kTOScrollBarVerticalPadding, 0.0f, kTOScrollBarVerticalPadding, 0.0f);
  96. _feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
  97. _gestureRecognizer = [[TOScrollBarGestureRecognizer alloc] initWithTarget:self action:@selector(scrollBarGestureRecognized:)];
  98. }
  99. - (void)setUpViews
  100. {
  101. if (self.trackView || self.handleView) {
  102. return;
  103. }
  104. self.backgroundColor = [UIColor clearColor];
  105. // Create and add the track view
  106. self.trackView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.trackWidth]];
  107. [self addSubview:self.trackView];
  108. // Add the handle view
  109. self.handleView = [[UIImageView alloc] initWithImage:[TOScrollBar verticalCapsuleImageWithWidth:self.handleWidth]];
  110. [self addSubview:self.handleView];
  111. // Add the initial styling
  112. [self configureViewsForStyle:self.style];
  113. // Add gesture recognizer
  114. [self addGestureRecognizer:self.gestureRecognizer];
  115. }
  116. - (void)configureViewsForStyle:(TOScrollBarStyle)style
  117. {
  118. BOOL dark = (style == TOScrollBarStyleDark);
  119. CGFloat whiteColor = 0.0f;
  120. if (dark) {
  121. whiteColor = 1.0f;
  122. }
  123. self.trackView.tintColor = [UIColor colorWithWhite:whiteColor alpha:0.1f];
  124. }
  125. - (void)dealloc
  126. {
  127. [self restoreScrollView:self.scrollView];
  128. }
  129. - (void)configureScrollView:(UIScrollView *)scrollView
  130. {
  131. if (scrollView == nil) {
  132. return;
  133. }
  134. // Make a copy of the scroll view's state and then configure
  135. _scrollViewState.showsVerticalScrollIndicator = self.scrollView.showsVerticalScrollIndicator;
  136. scrollView.showsVerticalScrollIndicator = NO;
  137. //Key-value Observers
  138. [scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
  139. [scrollView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
  140. }
  141. - (void)restoreScrollView:(UIScrollView *)scrollView
  142. {
  143. if (scrollView == nil) {
  144. return;
  145. }
  146. // Restore the scroll view's state
  147. scrollView.showsVerticalScrollIndicator = _scrollView.showsVerticalScrollIndicator;
  148. // Remove the observers
  149. [scrollView removeObserver:self forKeyPath:@"contentOffset"];
  150. [scrollView removeObserver:self forKeyPath:@"contentSize"];
  151. }
  152. - (void)willMoveToSuperview:(UIView *)newSuperview
  153. {
  154. [super willMoveToSuperview:newSuperview];
  155. [self setUpViews];
  156. }
  157. #pragma mark - Content Layout -
  158. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
  159. change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
  160. {
  161. [self updateStateForScrollView];
  162. if (self.hidden) { return; }
  163. [self layoutInScrollView];
  164. [self setNeedsLayout];
  165. }
  166. - (CGFloat)heightOfHandleForContentSize
  167. {
  168. if (_scrollView == nil) {
  169. return _handleMinimiumHeight;
  170. }
  171. CGFloat heightRatio = self.scrollView.frame.size.height / self.scrollView.contentSize.height;
  172. CGFloat height = self.frame.size.height * heightRatio;
  173. return MAX(floorf(height), _handleMinimiumHeight);
  174. }
  175. - (void)updateStateForScrollView
  176. {
  177. CGRect frame = _scrollView.frame;
  178. CGSize contentSize = _scrollView.contentSize;
  179. self.disabled = (contentSize.height / frame.size.height) < _minimumContentHeightScale;
  180. [self setHidden:(self.disabled || self.userHidden) animated:NO];
  181. }
  182. - (void)layoutInScrollView
  183. {
  184. CGRect scrollViewFrame = _scrollView.frame;
  185. UIEdgeInsets insets = _scrollView.contentInset;
  186. CGPoint contentOffset = _scrollView.contentOffset;
  187. CGFloat halfWidth = (kTOScrollBarWidth * 0.5f);
  188. if (@available(iOS 11.0, *)) {
  189. insets = _scrollView.adjustedContentInset;
  190. }
  191. // Contract the usable space by the scroll view's content inset (eg navigation/tool bars)
  192. scrollViewFrame.size.height -= (insets.top + insets.bottom);
  193. CGFloat largeTitleDelta = 0.0f;
  194. if (_insetForLargeTitles) {
  195. largeTitleDelta = fabs(MIN(insets.top + contentOffset.y, 0.0f));
  196. }
  197. // Work out the final height be further contracting by the padding
  198. CGFloat height = (scrollViewFrame.size.height - (_verticalInset.top + _verticalInset.bottom)) - largeTitleDelta;
  199. // Work out how much we have to offset the track by to make sure all of the parent view
  200. // is visible at the edge of the screen (Or else we'll be unable to tap properly)
  201. CGFloat horizontalOffset = halfWidth - _edgeInset;
  202. self.horizontalOffset = (horizontalOffset > 0.0f) ? horizontalOffset : 0.0f;
  203. // Work out the frame for the scroll view
  204. CGRect frame = CGRectZero;
  205. // Size
  206. frame.size.width = kTOScrollBarWidth;
  207. frame.size.height = (_dragging ? _originalHeight : height);
  208. // Horizontal placement
  209. frame.origin.x = scrollViewFrame.size.width - (_edgeInset + halfWidth);
  210. if (@available(iOS 11.0, *)) { frame.origin.x -= _scrollView.safeAreaInsets.right; }
  211. frame.origin.x = MIN(frame.origin.x, scrollViewFrame.size.width - kTOScrollBarWidth);
  212. // Vertical placement in scroll view
  213. if (_dragging) {
  214. frame.origin.y = _originalYOffset;
  215. }
  216. else {
  217. frame.origin.y = _verticalInset.top;
  218. frame.origin.y += insets.top;
  219. frame.origin.y += largeTitleDelta;
  220. }
  221. frame.origin.y += contentOffset.y;
  222. // Set the frame
  223. self.frame = frame;
  224. // Bring the scroll bar to the front in case other subviews were subsequently added over it
  225. [self.superview bringSubviewToFront:self];
  226. }
  227. - (void)layoutSubviews
  228. {
  229. CGRect frame = self.frame;
  230. // The frame of the track
  231. CGRect trackFrame = CGRectZero;
  232. trackFrame.size.width = _trackWidth;
  233. trackFrame.size.height = frame.size.height;
  234. trackFrame.origin.x = ceilf(((frame.size.width - _trackWidth) * 0.5f) + _horizontalOffset);
  235. self.trackView.frame = CGRectIntegral(trackFrame);
  236. // Don't handle automatic layout when dragging; we'll do that manually elsewhere
  237. if (self.dragging || self.disabled) {
  238. return;
  239. }
  240. // The frame of the handle
  241. CGRect handleFrame = CGRectZero;
  242. handleFrame.size.width = _handleWidth;
  243. handleFrame.size.height = [self heightOfHandleForContentSize];
  244. handleFrame.origin.x = ceilf(((frame.size.width - _handleWidth) * 0.5f) + _horizontalOffset);
  245. // Work out the y offset of the handle
  246. UIEdgeInsets contentInset = _scrollView.contentInset;
  247. if (@available(iOS 11.0, *)) {
  248. contentInset = _scrollView.safeAreaInsets;
  249. }
  250. CGPoint contentOffset = _scrollView.contentOffset;
  251. CGSize contentSize = _scrollView.contentSize;
  252. CGRect scrollViewFrame = _scrollView.frame;
  253. CGFloat scrollableHeight = (contentSize.height + contentInset.top + contentInset.bottom) - scrollViewFrame.size.height;
  254. CGFloat scrollProgress = (contentOffset.y + contentInset.top) / scrollableHeight;
  255. handleFrame.origin.y = (frame.size.height - handleFrame.size.height) * scrollProgress;
  256. // If the scroll view expanded beyond its scrollable range, shrink the handle to match the rubber band effect
  257. if (contentOffset.y < -contentInset.top) { // The top
  258. handleFrame.size.height -= (-contentOffset.y - contentInset.top);
  259. handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2));
  260. }
  261. else if (contentOffset.y + scrollViewFrame.size.height > contentSize.height + contentInset.bottom) { // The bottom
  262. CGFloat adjustedContentOffset = contentOffset.y + scrollViewFrame.size.height;
  263. CGFloat delta = adjustedContentOffset - (contentSize.height + contentInset.bottom);
  264. handleFrame.size.height -= delta;
  265. handleFrame.size.height = MAX(handleFrame.size.height, (_trackWidth * 2 + 2));
  266. handleFrame.origin.y = frame.size.height - handleFrame.size.height;
  267. }
  268. // Clamp to the bounds of the frame
  269. handleFrame.origin.y = MAX(handleFrame.origin.y, 0.0f);
  270. handleFrame.origin.y = MIN(handleFrame.origin.y, (frame.size.height - handleFrame.size.height));
  271. self.handleView.frame = handleFrame;
  272. }
  273. - (void)setScrollYOffsetForHandleYOffset:(CGFloat)yOffset animated:(BOOL)animated
  274. {
  275. CGFloat heightRange = _trackView.frame.size.height - _handleView.frame.size.height;
  276. yOffset = MAX(0.0f, yOffset);
  277. yOffset = MIN(heightRange, yOffset);
  278. CGFloat positionRatio = yOffset / heightRange;
  279. CGRect frame = _scrollView.frame;
  280. UIEdgeInsets inset = _scrollView.contentInset;
  281. CGSize contentSize = _scrollView.contentSize;
  282. if (@available(iOS 11.0, *)) {
  283. inset = _scrollView.adjustedContentInset;
  284. }
  285. inset.top = _originalTopInset;
  286. CGFloat totalScrollSize = (contentSize.height + inset.top + inset.bottom) - frame.size.height;
  287. CGFloat scrollOffset = totalScrollSize * positionRatio;
  288. scrollOffset -= inset.top;
  289. CGPoint contentOffset = _scrollView.contentOffset;
  290. contentOffset.y = scrollOffset;
  291. // Animate to help coax the large title navigation bar to behave
  292. if (@available(iOS 11.0, *)) {
  293. [UIView animateWithDuration:animated ? 0.1f : 0.00001f animations:^{
  294. [self.scrollView setContentOffset:contentOffset animated:NO];
  295. }];
  296. }
  297. else {
  298. [self.scrollView setContentOffset:contentOffset animated:NO];
  299. }
  300. }
  301. #pragma mark - Scroll View Integration -
  302. - (void)addToScrollView:(UIScrollView *)scrollView
  303. {
  304. if (scrollView == self.scrollView) {
  305. return;
  306. }
  307. // Restore the previous scroll view
  308. [self restoreScrollView:self.scrollView];
  309. // Assign the new scroll view
  310. self.scrollView = scrollView;
  311. // Apply the observers/settings to the new scroll view
  312. [self configureScrollView:scrollView];
  313. // Add the scroll bar to the scroll view's content view
  314. [self.scrollView addSubview:self];
  315. // Add ourselves as a property of the scroll view
  316. [self.scrollView setTo_scrollBar:self];
  317. // Begin layout
  318. [self layoutInScrollView];
  319. }
  320. - (void)removeFromScrollView
  321. {
  322. [self restoreScrollView:self.scrollView];
  323. [self removeFromSuperview];
  324. [self.scrollView setTo_scrollBar:nil];
  325. self.scrollView = nil;
  326. }
  327. - (UIEdgeInsets)adjustedTableViewSeparatorInsetForInset:(UIEdgeInsets)inset
  328. {
  329. inset.right = _edgeInset * 2.0f;
  330. return inset;
  331. }
  332. - (UIEdgeInsets)adjustedTableViewCellLayoutMarginsForMargins:(UIEdgeInsets)layoutMargins manualOffset:(CGFloat)offset
  333. {
  334. layoutMargins.right = (_edgeInset * 2.0f) + 15.0f; // Magic system number is 20, but we can't infer that from here on time
  335. layoutMargins.right += offset;
  336. return layoutMargins;
  337. }
  338. #pragma mark - User Interaction -
  339. - (void)scrollBarGestureRecognized:(TOScrollBarGestureRecognizer *)recognizer
  340. {
  341. CGPoint touchPoint = [recognizer locationInView:self];
  342. switch (recognizer.state) {
  343. case UIGestureRecognizerStateBegan:
  344. [self gestureBeganAtPoint:touchPoint];
  345. break;
  346. case UIGestureRecognizerStateChanged:
  347. [self gestureMovedToPoint:touchPoint];
  348. break;
  349. case UIGestureRecognizerStateEnded:
  350. case UIGestureRecognizerStateCancelled:
  351. [self gestureEnded];
  352. break;
  353. default:
  354. break;
  355. }
  356. }
  357. - (void)gestureBeganAtPoint:(CGPoint)touchPoint
  358. {
  359. if (self.disabled) {
  360. return;
  361. }
  362. // Warm-up the feedback generator
  363. [_feedbackGenerator prepare];
  364. self.scrollView.scrollEnabled = NO;
  365. self.dragging = YES;
  366. // Capture the original position
  367. self.originalHeight = self.frame.size.height;
  368. self.originalYOffset = self.frame.origin.y - self.scrollView.contentOffset.y;
  369. if (@available(iOS 11.0, *)) {
  370. self.originalTopInset = _scrollView.adjustedContentInset.top;
  371. } else {
  372. self.originalTopInset = _scrollView.contentInset.top;
  373. }
  374. // Check if the user tapped inside the handle
  375. CGRect handleFrame = self.handleView.frame;
  376. if (touchPoint.y > (handleFrame.origin.y - 20) &&
  377. touchPoint.y < handleFrame.origin.y + (handleFrame.size.height + 20))
  378. {
  379. self.yOffset = (touchPoint.y - handleFrame.origin.y);
  380. return;
  381. }
  382. if (!self.handleExclusiveInteractionEnabled) {
  383. // User tapped somewhere else, animate the handle to that point
  384. CGFloat halfHeight = (handleFrame.size.height * 0.5f);
  385. CGFloat destinationYOffset = touchPoint.y - halfHeight;
  386. destinationYOffset = MAX(0.0f, destinationYOffset);
  387. destinationYOffset = MIN(self.frame.size.height - halfHeight, destinationYOffset);
  388. self.yOffset = (touchPoint.y - destinationYOffset);
  389. handleFrame.origin.y = destinationYOffset;
  390. [UIView animateWithDuration:0.2f
  391. delay:0.0f
  392. usingSpringWithDamping:1.0f
  393. initialSpringVelocity:0.1f options:UIViewAnimationOptionBeginFromCurrentState
  394. animations:^{
  395. self.handleView.frame = handleFrame;
  396. } completion:nil];
  397. [self setScrollYOffsetForHandleYOffset:floorf(destinationYOffset) animated:NO];
  398. }
  399. }
  400. - (void)gestureMovedToPoint:(CGPoint)touchPoint
  401. {
  402. if (self.disabled) {
  403. return;
  404. }
  405. CGFloat delta = 0.0f;
  406. CGRect handleFrame = _handleView.frame;
  407. CGRect trackFrame = _trackView.frame;
  408. CGFloat minimumY = 0.0f;
  409. CGFloat maximumY = trackFrame.size.height - handleFrame.size.height;
  410. if (self.handleExclusiveInteractionEnabled) {
  411. if (touchPoint.y < (handleFrame.origin.y - 20) ||
  412. touchPoint.y > handleFrame.origin.y + (handleFrame.size.height + 20))
  413. {
  414. // This touch is not on the handle; eject.
  415. return;
  416. }
  417. }
  418. // Apply the updated Y value plus the previous offset
  419. delta = handleFrame.origin.y;
  420. handleFrame.origin.y = touchPoint.y - _yOffset;
  421. //Clamp the handle, and adjust the y offset to counter going outside the bounds
  422. if (handleFrame.origin.y < minimumY) {
  423. _yOffset += handleFrame.origin.y;
  424. _yOffset = MAX(minimumY, _yOffset);
  425. handleFrame.origin.y = minimumY;
  426. }
  427. else if (handleFrame.origin.y > maximumY) {
  428. CGFloat handleOverflow = CGRectGetMaxY(handleFrame) - trackFrame.size.height;
  429. _yOffset += handleOverflow;
  430. _yOffset = MIN(self.yOffset, handleFrame.size.height);
  431. handleFrame.origin.y = MIN(handleFrame.origin.y, maximumY);
  432. }
  433. _handleView.frame = handleFrame;
  434. delta -= handleFrame.origin.y;
  435. delta = fabs(delta);
  436. // If the delta is not 0.0, but we're at either extreme,
  437. // this is first frame we've since reaching that point.
  438. // Play a taptic feedback impact
  439. if (delta > FLT_EPSILON && (CGRectGetMinY(handleFrame) < FLT_EPSILON || CGRectGetMinY(handleFrame) >= maximumY - FLT_EPSILON)) {
  440. [_feedbackGenerator impactOccurred];
  441. }
  442. // If the user is doing really granualar swipes, add a subtle amount
  443. // of vertical animation so the scroll view isn't jumping on each frame
  444. [self setScrollYOffsetForHandleYOffset:floorf(handleFrame.origin.y) animated:NO]; //(delta < 0.51f)
  445. }
  446. - (void)gestureEnded
  447. {
  448. self.scrollView.scrollEnabled = YES;
  449. self.dragging = NO;
  450. [UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:1.0f initialSpringVelocity:0.5f options:0 animations:^{
  451. [self layoutInScrollView];
  452. [self layoutIfNeeded];
  453. } completion:nil];
  454. }
  455. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  456. {
  457. if (!self.handleExclusiveInteractionEnabled) {
  458. return [super pointInside:point withEvent:event];
  459. }
  460. else {
  461. CGFloat handleMinY = CGRectGetMinY(self.handleView.frame);
  462. CGFloat handleMaxY = CGRectGetMaxY(self.handleView.frame);
  463. return (0 <= point.x) && (handleMinY <= point.y) && (point.y <= handleMaxY);
  464. }
  465. }
  466. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
  467. {
  468. UIView *result = [super hitTest:point withEvent:event];
  469. if (self.disabled || self.dragging) {
  470. return result;
  471. }
  472. // If the user contacts the screen in a swiping motion,
  473. // the scroll view will automatically highjack the touch
  474. // event unless we explicitly override it here.
  475. self.scrollView.scrollEnabled = (result != self);
  476. return result;
  477. }
  478. #pragma mark - Accessors -
  479. - (void)setStyle:(TOScrollBarStyle)style
  480. {
  481. _style = style;
  482. [self configureViewsForStyle:style];
  483. }
  484. - (UIColor *)trackTintColor { return self.trackView.tintColor; }
  485. - (void)setTrackTintColor:(UIColor *)trackTintColor
  486. {
  487. self.trackView.tintColor = trackTintColor;
  488. }
  489. - (UIColor *)handleTintColor { return self.handleView.tintColor; }
  490. - (void)setHandleTintColor:(UIColor *)handleTintColor
  491. {
  492. self.handleView.tintColor = handleTintColor;
  493. }
  494. - (void)setHidden:(BOOL)hidden
  495. {
  496. self.userHidden = hidden;
  497. [self setHidden:hidden animated:NO];
  498. }
  499. - (void)setHidden:(BOOL)hidden animated:(BOOL)animated
  500. {
  501. // Override. It cannot be shown if it's disabled
  502. if (_disabled) {
  503. super.hidden = YES;
  504. return;
  505. }
  506. // Simply show or hide it if we're not animating
  507. if (animated == NO) {
  508. super.hidden = hidden;
  509. return;
  510. }
  511. // Show it if we're going to animate it
  512. if (self.hidden && hidden == NO) {
  513. super.hidden = NO;
  514. [self layoutInScrollView];
  515. [self setNeedsLayout];
  516. }
  517. CGRect fromFrame = self.frame;
  518. CGRect toFrame = self.frame;
  519. CGFloat widestElement = MAX(_trackWidth, _handleWidth);
  520. CGFloat hiddenOffset = fromFrame.origin.x + _edgeInset + (widestElement * 2.0f);
  521. if (hidden == NO) {
  522. fromFrame.origin.x = hiddenOffset;
  523. }
  524. else {
  525. toFrame.origin.x = hiddenOffset;
  526. }
  527. self.frame = fromFrame;
  528. [UIView animateWithDuration:0.3f
  529. delay:0.0f
  530. usingSpringWithDamping:1.0f
  531. initialSpringVelocity:0.1f
  532. options:UIViewAnimationOptionBeginFromCurrentState
  533. animations:^{
  534. self.frame = toFrame;
  535. } completion:^(BOOL finished) {
  536. super.hidden = hidden;
  537. }];
  538. }
  539. #pragma mark - Image Generation -
  540. + (UIImage *)verticalCapsuleImageWithWidth:(CGFloat)width
  541. {
  542. UIImage *image = nil;
  543. CGFloat radius = width * 0.5f;
  544. CGRect frame = (CGRect){0, 0, width+1, width+1};
  545. UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f);
  546. [[UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius] fill];
  547. image = UIGraphicsGetImageFromCurrentImageContext();
  548. UIGraphicsEndImageContext();
  549. image = [image resizableImageWithCapInsets:UIEdgeInsetsMake(radius, radius, radius, radius) resizingMode:UIImageResizingModeStretch];
  550. image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  551. return image;
  552. }
  553. @end