GTScrollNavigationBar.m 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /* This file is adapted from GTScrollNavigationBar - https://github.com/luugiathuy/GTScrollNavigationBar
  2. * 3-clause BSD License
  3. *
  4. * Copyright (c) 2013, Luu Gia Thuy
  5. *
  6. * Redistribution and use in source and binary forms, with or without modification,
  7. * are permitted provided that the following conditions are met:
  8. *
  9. * * Redistributions of source code must retain the above copyright notice, this
  10. * list of conditions and the following disclaimer.
  11. *
  12. * * Redistributions in binary form must reproduce the above copyright notice, this
  13. * list of conditions and the following disclaimer in the documentation and/or
  14. * other materials provided with the distribution.
  15. *
  16. * * Neither the name of the {organization} nor the names of its
  17. * contributors may be used to endorse or promote products derived from
  18. * this software without specific prior written permission.
  19. *
  20. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  21. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  24. * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  25. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  26. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  27. * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  29. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
  30. #import "GTScrollNavigationBar.h"
  31. #define kNearZero 0.000001f
  32. @interface GTScrollNavigationBar () <UIGestureRecognizerDelegate>
  33. @property (strong, nonatomic) UIPanGestureRecognizer* panGesture;
  34. @property (assign, nonatomic) CGFloat lastContentOffsetY;
  35. @end
  36. @implementation GTScrollNavigationBar
  37. - (id)initWithCoder:(NSCoder *)aDecoder {
  38. self = [super initWithCoder:aDecoder];
  39. if (self) {
  40. [self setup];
  41. }
  42. return self;
  43. }
  44. - (id)initWithFrame:(CGRect)frame
  45. {
  46. self = [super initWithFrame:frame];
  47. if (self) {
  48. [self setup];
  49. }
  50. return self;
  51. }
  52. - (void)setup
  53. {
  54. self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self
  55. action:@selector(handlePan:)];
  56. self.panGesture.delegate = self;
  57. self.panGesture.cancelsTouchesInView = NO;
  58. NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
  59. [defaultCenter addObserver:self
  60. selector:@selector(applicationDidBecomeActive)
  61. name:UIApplicationDidBecomeActiveNotification
  62. object:nil];
  63. [defaultCenter addObserver:self
  64. selector:@selector(statusBarOrientationDidChange)
  65. name:UIApplicationDidChangeStatusBarOrientationNotification
  66. object:nil];
  67. }
  68. - (void)dealloc
  69. {
  70. if (!SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"9.0")) {
  71. [[NSNotificationCenter defaultCenter] removeObserver:self];
  72. }
  73. }
  74. #pragma mark - Properties
  75. - (void)setScrollView:(UIScrollView*)scrollView
  76. {
  77. [self resetToDefaultPositionWithAnimation:NO];
  78. _scrollView = scrollView;
  79. // remove gesture from current panGesture's view
  80. if (self.panGesture.view) {
  81. [self.panGesture.view removeGestureRecognizer:self.panGesture];
  82. }
  83. if (scrollView) {
  84. [scrollView addGestureRecognizer:self.panGesture];
  85. }
  86. }
  87. #pragma mark - Public methods
  88. - (void)resetToDefaultPositionWithAnimation:(BOOL)animated
  89. {
  90. self.scrollState = GTScrollNavigationBarStateNone;
  91. CGRect frame = self.frame;
  92. frame.origin.y = [self statusBarTopOffset];
  93. [self setFrame:frame alpha:1.0f animated:animated];
  94. }
  95. #pragma mark - Notifications
  96. - (void)statusBarOrientationDidChange
  97. {
  98. [self resetToDefaultPositionWithAnimation:NO];
  99. }
  100. - (void)applicationDidBecomeActive
  101. {
  102. [self resetToDefaultPositionWithAnimation:NO];
  103. }
  104. #pragma mark - UIGestureRecognizerDelegate
  105. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
  106. shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
  107. {
  108. return YES;
  109. }
  110. #pragma mark - panGesture handler
  111. - (void)handlePan:(UIPanGestureRecognizer*)gesture
  112. {
  113. if (!self.scrollView || gesture.view != self.scrollView) {
  114. return;
  115. }
  116. // Don't try to scroll navigation bar if there's not enough room
  117. if (self.scrollView.frame.size.height + (self.bounds.size.height * 2) >=
  118. self.scrollView.contentSize.height) {
  119. return;
  120. }
  121. CGFloat contentOffsetY = self.scrollView.contentOffset.y;
  122. // Reset scrollState when the gesture began
  123. if (gesture.state == UIGestureRecognizerStateBegan) {
  124. self.scrollState = GTScrollNavigationBarStateNone;
  125. self.lastContentOffsetY = contentOffsetY;
  126. return;
  127. }
  128. CGFloat deltaY = contentOffsetY - self.lastContentOffsetY;
  129. if (deltaY < 0.0f) {
  130. self.scrollState = GTScrollNavigationBarStateScrollingDown;
  131. } else if (deltaY > 0.0f) {
  132. self.scrollState = GTScrollNavigationBarStateScrollingUp;
  133. }
  134. CGRect frame = self.frame;
  135. CGFloat alpha = 1.0f;
  136. CGFloat maxY = [self statusBarTopOffset];
  137. CGFloat minY = maxY - CGRectGetHeight(frame) + 1.0f;
  138. // NOTE: plus 1px to prevent the navigation bar disappears in iOS < 7
  139. CGFloat contentInsetTop = self.scrollView.contentInset.top;
  140. bool isBouncePastTopEdge = contentOffsetY < -contentInsetTop;
  141. if (isBouncePastTopEdge && CGRectGetMinY(frame) == maxY) {
  142. self.lastContentOffsetY = contentOffsetY;
  143. return;
  144. }
  145. bool isScrolling = (self.scrollState == GTScrollNavigationBarStateScrollingUp ||
  146. self.scrollState == GTScrollNavigationBarStateScrollingDown);
  147. bool gestureIsActive = (gesture.state != UIGestureRecognizerStateEnded &&
  148. gesture.state != UIGestureRecognizerStateCancelled);
  149. if (isScrolling && !gestureIsActive) {
  150. // Animate navigation bar to end position
  151. if (self.scrollState == GTScrollNavigationBarStateScrollingDown) {
  152. frame.origin.y = maxY;
  153. alpha = 1.0f;
  154. }
  155. else if (self.scrollState == GTScrollNavigationBarStateScrollingUp) {
  156. frame.origin.y = minY;
  157. alpha = kNearZero;
  158. }
  159. [self setFrame:frame alpha:alpha animated:YES];
  160. }
  161. // When panning down at beginning of scrollView and the bar is expanding, do not update lastContentOffsetY
  162. if (!isBouncePastTopEdge && CGRectGetMinY(frame) == maxY) {
  163. self.lastContentOffsetY = contentOffsetY;
  164. }
  165. }
  166. #pragma mark - helper methods
  167. - (CGFloat)statusBarTopOffset
  168. {
  169. CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
  170. CGFloat topOffset = MIN(CGRectGetMaxX(statusBarFrame), CGRectGetMaxY(statusBarFrame));
  171. bool isInCallStatusBar = topOffset == 40.0f;
  172. if (isInCallStatusBar) {
  173. topOffset -= 20.0f;
  174. }
  175. return topOffset;
  176. }
  177. - (void)setFrame:(CGRect)frame alpha:(CGFloat)alpha animated:(BOOL)animated
  178. {
  179. if (animated) {
  180. [UIView beginAnimations:@"GTScrollNavigationBarAnimation" context:nil];
  181. }
  182. CGFloat offsetY = CGRectGetMinY(frame) - CGRectGetMinY(self.frame);
  183. UIView *firstView = [self.subviews firstObject];
  184. for (UIView *view in self.subviews) {
  185. bool isBackgroundView = view == firstView;
  186. bool isViewHidden = view.hidden || view.alpha == 0.0f;
  187. if (isBackgroundView || isViewHidden)
  188. continue;
  189. view.alpha = alpha;
  190. }
  191. self.frame = frame;
  192. if (self.scrollView) {
  193. CGRect parentViewFrame = self.scrollView.superview.frame;
  194. parentViewFrame.origin.y += offsetY;
  195. parentViewFrame.size.height -= offsetY;
  196. self.scrollView.superview.frame = parentViewFrame;
  197. }
  198. if (animated) {
  199. [UIView commitAnimations];
  200. }
  201. }
  202. @end
  203. @implementation UINavigationController (GTScrollNavigationBarAdditions)
  204. @dynamic scrollNavigationBar;
  205. - (GTScrollNavigationBar *)scrollNavigationBar
  206. {
  207. return (GTScrollNavigationBar *)self.navigationBar;
  208. }
  209. @end