REMenu.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. //
  2. // REMenu.m
  3. // REMenu
  4. //
  5. // Copyright (c) 2013 Roman Efimov (https://github.com/romaonthego)
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. //
  25. #import "REMenu.h"
  26. #import "REMenuItem.h"
  27. #import "REMenuItemView.h"
  28. @interface REMenuItem ()
  29. @property (assign, readwrite, nonatomic) REMenuItemView *itemView;
  30. @end
  31. @interface REMenu ()
  32. @property (strong, readwrite, nonatomic) UIView *menuWrapperView;
  33. @property (strong, readwrite, nonatomic) REMenuContainerView *containerView;
  34. @property (strong, readwrite, nonatomic) UIButton *backgroundButton;
  35. @property (assign, readwrite, nonatomic) BOOL isOpen;
  36. @property (assign, readwrite, nonatomic) BOOL isAnimating;
  37. @property (strong, readwrite, nonatomic) NSMutableArray *itemViews;
  38. @property (weak, readwrite, nonatomic) UINavigationBar *navigationBar;
  39. @property (strong, readwrite, nonatomic) UIToolbar *toolbar;
  40. @end
  41. @implementation REMenu
  42. - (id)init
  43. {
  44. self = [super init];
  45. if (self) {
  46. _imageAlignment = REMenuImageAlignmentLeft;
  47. _closeOnSelection = YES;
  48. _itemHeight = 48.0;
  49. _separatorHeight = 2.0;
  50. _separatorOffset = CGSizeMake(0.0, 0.0);
  51. _waitUntilAnimationIsComplete = YES;
  52. _textOffset = CGSizeMake(0, 0);
  53. _subtitleTextOffset = CGSizeMake(0, 0);
  54. _font = [UIFont boldSystemFontOfSize:21.0];
  55. _subtitleFont = [UIFont systemFontOfSize:14.0];
  56. _backgroundAlpha = 1.0;
  57. _backgroundColor = [UIColor colorWithRed:53/255.0 green:53/255.0 blue:52/255.0 alpha:1.0];
  58. _separatorColor = [UIColor colorWithPatternImage:self.separatorImage];
  59. _textColor = [UIColor colorWithRed:128/255.0 green:126/255.0 blue:124/255.0 alpha:1.0];
  60. _textShadowColor = [UIColor blackColor];
  61. _textShadowOffset = CGSizeMake(0, -1.0);
  62. _textAlignment = NSTextAlignmentCenter;
  63. _highlightedBackgroundColor = [UIColor colorWithRed:28/255.0 green:28/255.0 blue:27/255.0 alpha:1.0];
  64. _highlightedSeparatorColor = [UIColor colorWithRed:28/255.0 green:28/255.0 blue:27/255.0 alpha:1.0];
  65. _highlightedTextColor = [UIColor colorWithRed:128/255.0 green:126/255.0 blue:124/255.0 alpha:1.0];
  66. _highlightedTextShadowColor = [UIColor blackColor];
  67. _highlightedTextShadowOffset = CGSizeMake(0, -1.0);
  68. _subtitleTextColor = [UIColor colorWithWhite:0.425 alpha:1.000];
  69. _subtitleTextShadowColor = [UIColor blackColor];
  70. _subtitleTextShadowOffset = CGSizeMake(0, -1.0);
  71. _subtitleHighlightedTextColor = [UIColor colorWithRed:0.389 green:0.384 blue:0.379 alpha:1.000];
  72. _subtitleHighlightedTextShadowColor = [UIColor blackColor];
  73. _subtitleHighlightedTextShadowOffset = CGSizeMake(0, -1.0);
  74. _subtitleTextAlignment = NSTextAlignmentCenter;
  75. _borderWidth = 1.0;
  76. _borderColor = [UIColor colorWithRed:28/255.0 green:28/255.0 blue:27/255.0 alpha:1.0];
  77. _animationDuration = 0.3;
  78. _closeAnimationDuration = 0.2;
  79. _bounce = YES;
  80. _bounceAnimationDuration = 0.2;
  81. _appearsBehindNavigationBar = REUIKitIsFlatMode() ? YES : NO;
  82. }
  83. return self;
  84. }
  85. - (id)initWithItems:(NSArray *)items
  86. {
  87. self = [self init];
  88. if (self) {
  89. _items = items;
  90. }
  91. return self;
  92. }
  93. - (void)showFromRect:(CGRect)rect inView:(UIView *)view
  94. {
  95. if (self.isAnimating) {
  96. return;
  97. }
  98. self.isOpen = YES;
  99. self.isAnimating = YES;
  100. // Create views
  101. //
  102. self.containerView = ({
  103. REMenuContainerView *view = [[REMenuContainerView alloc] init];
  104. view.clipsToBounds = YES;
  105. view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  106. if (self.backgroundView) {
  107. self.backgroundView.alpha = 0;
  108. [view addSubview:self.backgroundView];
  109. }
  110. view;
  111. });
  112. self.menuView = ({
  113. UIView *view = [[UIView alloc] init];
  114. if (!self.liveBlur || !REUIKitIsFlatMode()) {
  115. view.backgroundColor = self.backgroundColor;
  116. }
  117. view.layer.cornerRadius = self.cornerRadius;
  118. view.layer.borderColor = self.borderColor.CGColor;
  119. view.layer.borderWidth = self.borderWidth;
  120. view.layer.masksToBounds = YES;
  121. view.layer.shouldRasterize = YES;
  122. view.layer.rasterizationScale = [UIScreen mainScreen].scale;
  123. view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  124. view;
  125. });
  126. if (REUIKitIsFlatMode()) {
  127. self.toolbar = ({
  128. UIToolbar *toolbar = [[UIToolbar alloc] init];
  129. toolbar.barStyle = (UIBarStyle)self.liveBlurBackgroundStyle;
  130. if ([toolbar respondsToSelector:@selector(setBarTintColor:)])
  131. [toolbar performSelector:@selector(setBarTintColor:) withObject:self.liveBlurTintColor];
  132. toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  133. toolbar.layer.cornerRadius = self.cornerRadius;
  134. toolbar.layer.borderColor = self.borderColor.CGColor;
  135. toolbar.layer.borderWidth = self.borderWidth;
  136. toolbar.layer.masksToBounds = YES;
  137. toolbar;
  138. });
  139. }
  140. self.menuWrapperView = ({
  141. UIView *view = [[UIView alloc] init];
  142. view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  143. if (!self.liveBlur || !REUIKitIsFlatMode()) {
  144. view.layer.shadowColor = self.shadowColor.CGColor;
  145. view.layer.shadowOffset = self.shadowOffset;
  146. view.layer.shadowOpacity = self.shadowOpacity;
  147. view.layer.shadowRadius = self.shadowRadius;
  148. view.layer.shouldRasterize = YES;
  149. view.layer.rasterizationScale = [UIScreen mainScreen].scale;
  150. }
  151. view;
  152. });
  153. self.backgroundButton = ({
  154. UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
  155. button.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  156. button.accessibilityLabel = NSLocalizedString(@"Menu background", @"Menu background");
  157. button.accessibilityHint = NSLocalizedString(@"Double tap to close", @"Double tap to close");
  158. [button addTarget:self action:@selector(close) forControlEvents:UIControlEventTouchUpInside];
  159. button;
  160. });
  161. CGFloat navigationBarOffset = [self computeNavigationBarOffset];
  162. // Append new item views to REMenuView
  163. //
  164. for (REMenuItem *item in self.items) {
  165. NSInteger index = [self.items indexOfObject:item];
  166. CGFloat itemHeight = self.itemHeight;
  167. if (index == self.items.count - 1)
  168. itemHeight += self.cornerRadius;
  169. UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(self.separatorOffset.width,
  170. index * self.itemHeight + index * self.separatorHeight + 40.0 + navigationBarOffset + self.separatorOffset.height,
  171. rect.size.width - self.separatorOffset.width,
  172. self.separatorHeight)];
  173. separatorView.backgroundColor = self.separatorColor;
  174. separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  175. [self.menuView addSubview:separatorView];
  176. REMenuItemView *itemView = [[REMenuItemView alloc] initWithFrame:CGRectMake(0,
  177. index * self.itemHeight + (index + 1.0) * self.separatorHeight + 40.0 + navigationBarOffset,
  178. rect.size.width,
  179. itemHeight)
  180. menu:self item:item
  181. hasSubtitle:item.subtitle.length > 0];
  182. itemView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  183. item.itemView = itemView;
  184. itemView.separatorView = separatorView;
  185. itemView.autoresizesSubviews = YES;
  186. if (item.customView) {
  187. item.customView.frame = itemView.bounds;
  188. [itemView addSubview:item.customView];
  189. }
  190. [self.menuView addSubview:itemView];
  191. }
  192. // Set up frames
  193. //
  194. self.menuWrapperView.frame = CGRectMake(0, -self.combinedHeight - navigationBarOffset, rect.size.width, self.combinedHeight + navigationBarOffset);
  195. self.menuView.frame = self.menuWrapperView.bounds;
  196. if (REUIKitIsFlatMode() && self.liveBlur) {
  197. self.toolbar.frame = self.menuWrapperView.bounds;
  198. }
  199. self.containerView.frame = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
  200. self.backgroundButton.frame = self.containerView.bounds;
  201. // Add subviews
  202. //
  203. if (REUIKitIsFlatMode() && self.liveBlur) {
  204. [self.menuWrapperView addSubview:self.toolbar];
  205. }
  206. [self.menuWrapperView addSubview:self.menuView];
  207. [self.containerView addSubview:self.backgroundButton];
  208. [self.containerView addSubview:self.menuWrapperView];
  209. [view addSubview:self.containerView];
  210. // Animate appearance
  211. //
  212. if (self.bounce) {
  213. self.isAnimating = YES;
  214. if ([UIView respondsToSelector:@selector(animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)]) {
  215. [UIView animateWithDuration:self.animationDuration+self.bounceAnimationDuration
  216. delay:0.0
  217. usingSpringWithDamping:0.6
  218. initialSpringVelocity:4.0
  219. options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionCurveEaseInOut
  220. animations:^{
  221. self.backgroundView.alpha = self.backgroundAlpha;
  222. CGRect frame = self.menuView.frame;
  223. frame.origin.y = -40.0 - self.separatorHeight;
  224. self.menuWrapperView.frame = frame;
  225. } completion:^(BOOL finished) {
  226. self.isAnimating = NO;
  227. }];
  228. } else {
  229. [UIView animateWithDuration:self.animationDuration
  230. delay:0.0
  231. options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionCurveEaseInOut
  232. animations:^{
  233. self.backgroundView.alpha = self.backgroundAlpha;
  234. CGRect frame = self.menuView.frame;
  235. frame.origin.y = -40.0 - self.separatorHeight;
  236. self.menuWrapperView.frame = frame;
  237. } completion:^(BOOL finished) {
  238. self.isAnimating = NO;
  239. }];
  240. }
  241. } else {
  242. [UIView animateWithDuration:self.animationDuration
  243. delay:0.0
  244. options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionCurveEaseInOut
  245. animations:^{
  246. self.backgroundView.alpha = self.backgroundAlpha;
  247. CGRect frame = self.menuView.frame;
  248. frame.origin.y = -40.0 - self.separatorHeight;
  249. self.menuWrapperView.frame = frame;
  250. } completion:^(BOOL finished) {
  251. self.isAnimating = NO;
  252. }];
  253. }
  254. }
  255. - (void)showInView:(UIView *)view
  256. {
  257. [self showFromRect:view.bounds inView:view];
  258. }
  259. -(void)showFromNavigationController:(UINavigationController *)navigationController
  260. {
  261. [self showFromNavigationController:navigationController offsetX:0 width:navigationController.navigationBar.frame.size.width];
  262. }
  263. - (void)showFromNavigationController:(UINavigationController *)navigationController offsetX:(CGFloat)offsetX width:(CGFloat)width
  264. {
  265. if (self.isAnimating) {
  266. return;
  267. }
  268. self.navigationBar = navigationController.navigationBar;
  269. [self showFromRect:CGRectMake(offsetX, 0, width, navigationController.view.frame.size.height) inView:navigationController.view];
  270. self.containerView.appearsBehindNavigationBar = self.appearsBehindNavigationBar;
  271. self.containerView.navigationBar = navigationController.navigationBar;
  272. if (self.appearsBehindNavigationBar) {
  273. [navigationController.view bringSubviewToFront:navigationController.navigationBar];
  274. }
  275. if(width != navigationController.navigationBar.frame.size.width)
  276. {
  277. self.containerView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  278. }
  279. }
  280. - (void)closeWithCompletion:(void (^)(void))completion
  281. {
  282. if (self.isAnimating) return;
  283. self.isAnimating = YES;
  284. CGFloat navigationBarOffset = [self computeNavigationBarOffset];
  285. void (^closeMenu)(void) = ^{
  286. [UIView animateWithDuration:self.closeAnimationDuration
  287. delay:0.0
  288. options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionCurveEaseInOut
  289. animations:^ {
  290. CGRect frame = self.menuView.frame;
  291. frame.origin.y = - self.combinedHeight - navigationBarOffset;
  292. self.menuWrapperView.frame = frame;
  293. self.backgroundView.alpha = 0;
  294. } completion:^(BOOL finished) {
  295. self.isOpen = NO;
  296. self.isAnimating = NO;
  297. [self.menuView removeFromSuperview];
  298. [self.menuWrapperView removeFromSuperview];
  299. [self.backgroundButton removeFromSuperview];
  300. [self.backgroundView removeFromSuperview];
  301. [self.containerView removeFromSuperview];
  302. if (completion) {
  303. completion();
  304. }
  305. if (self.closeCompletionHandler) {
  306. self.closeCompletionHandler();
  307. }
  308. }];
  309. };
  310. if (self.closePreparationBlock) {
  311. self.closePreparationBlock();
  312. }
  313. if (self.bounce) {
  314. [UIView animateWithDuration:self.bounceAnimationDuration animations:^{
  315. CGRect frame = self.menuView.frame;
  316. frame.origin.y = -20.0;
  317. self.menuWrapperView.frame = frame;
  318. } completion:^(BOOL finished) {
  319. closeMenu();
  320. }];
  321. } else {
  322. closeMenu();
  323. }
  324. }
  325. - (void)close
  326. {
  327. [self closeWithCompletion:nil];
  328. }
  329. - (CGFloat)combinedHeight
  330. {
  331. return self.items.count * self.itemHeight + self.items.count * self.separatorHeight + 40.0 + self.cornerRadius;
  332. }
  333. - (void)setNeedsLayout
  334. {
  335. [UIView animateWithDuration:0.35 animations:^{
  336. [self.containerView layoutSubviews];
  337. }];
  338. }
  339. #pragma mark -
  340. #pragma mark Setting style
  341. - (UIImage *)separatorImage
  342. {
  343. UIGraphicsBeginImageContext(CGSizeMake(1, 4.0));
  344. CGContextRef context = UIGraphicsGetCurrentContext();
  345. UIGraphicsPushContext(context);
  346. CGContextSetFillColorWithColor(context, [UIColor colorWithRed:28/255.0 green:28/255.0 blue:27/255.0 alpha:1.0].CGColor);
  347. CGContextFillRect(context, CGRectMake(0, 0, 1.0, 2.0));
  348. CGContextSetFillColorWithColor(context, [UIColor colorWithRed:79/255.0 green:79/255.0 blue:77/255.0 alpha:1.0].CGColor);
  349. CGContextFillRect(context, CGRectMake(0, 3.0, 1.0, 2.0));
  350. UIGraphicsPopContext();
  351. UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
  352. UIGraphicsEndImageContext();
  353. return [UIImage imageWithCGImage:outputImage.CGImage scale:2.0 orientation:UIImageOrientationUp];
  354. }
  355. - (CGFloat)computeNavigationBarOffset
  356. {
  357. //TWS
  358. //CGFloat navigationBarOffset = self.appearsBehindNavigationBar && self.navigationBar ? ([UIApplication sharedApplication].statusBarHidden ? 44 : 64) : 0;
  359. CGFloat offsetY = 0;
  360. if (@available(iOS 11, *)) {
  361. UIEdgeInsets insets = [UIApplication sharedApplication].delegate.window.safeAreaInsets;
  362. if (insets.bottom > 0)
  363. offsetY = 20;
  364. }
  365. CGFloat navigationBarOffset = self.appearsBehindNavigationBar && self.navigationBar ? ([UIApplication sharedApplication].statusBarHidden ? 44+offsetY : 64+offsetY) : 0;
  366. return navigationBarOffset;
  367. }
  368. @end