-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathSFSCollectionMenuController.m
More file actions
428 lines (363 loc) · 17.6 KB
/
SFSCollectionMenuController.m
File metadata and controls
428 lines (363 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
//
// SFSCollectionMenuController.m
// SFSCollectionMenu
//
// Created by BJ Miller on 9/7/13.
// Copyright (c) 2013 Six Five Software, LLC. All rights reserved.
//
#import "SFSCollectionMenuController.h"
#import "SFSMenuCell.h"
#import "SFSCircleLayout.h"
#import <AVFoundation/AVSpeechSynthesis.h>
#import "UIImage+ImageEffects.h"
#define CELL_REUSE_ID @"Cell Reuse ID"
#define MAX_CELLS 6
#define IPHONE_LABEL_OFFSET 200
#define IPAD_LABEL_OFFSET 275
@interface SFSCollectionMenuController () <UICollectionViewDataSource, UICollectionViewDelegate>
@property (nonatomic, strong) UIView *viewDisplayingMenu;
@property (nonatomic, strong) SFSCircleLayout *circleLayout;
@property (nonatomic, assign, getter = isVisible) BOOL visible;
@property (nonatomic, strong) UIImageView *collectionViewBackgroundImageView;
@property (nonatomic) UIInterfaceOrientation currentOrientation;
@property (nonatomic, strong) UIButton *closeButton;
@end
@implementation SFSCollectionMenuController
@synthesize collectionView = _collectionView;
#pragma mark - Initializer method
- (instancetype)initWithDelegate:(id<SFSCollectionMenuDelegate>)delegate {
self = [self initWithCollectionViewLayout:[self circleLayout]];
if (self) {
_delegate = delegate;
self.visible = NO;
}
return self;
}
- (void)setupCollectionView {
self.viewDisplayingMenu = ([self.delegate respondsToSelector:@selector(viewForMenu)] ? [self.delegate viewForMenu] : self.view );
if (_collectionView) {
[self.collectionView setFrame:[self frameForViewForCurrentOrientation]];
} else {
_collectionView = [[UICollectionView alloc] initWithFrame:[self frameForViewForCurrentOrientation] collectionViewLayout:[self circleLayout]];
[self.collectionView setDelegate:self];
[self.collectionView setDataSource:self];
[self.collectionView registerClass:[SFSMenuCell class] forCellWithReuseIdentifier:CELL_REUSE_ID];
[self.collectionView setBackgroundColor:[UIColor clearColor]];
// set the Accessibility View to modal so views below it are not read by VoiceOver
[self.collectionView setAccessibilityViewIsModal:YES];
// set the label
if (self.delegate) {
if ([self.delegate respondsToSelector:@selector(labelTextForMenu)]) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.text = [self.delegate labelTextForMenu];
[label setFont:[UIFont fontWithName:@"Verdana" size:25.0f]];
[label sizeToFit];
if ([self.delegate respondsToSelector:@selector(colorForLabelText)]) {
[label setTextColor:[self.delegate colorForLabelText]];
} else {
[label setTextColor:[UIColor whiteColor]];
}
CGSize labelSize = label.frame.size;
CGPoint labelOrigin = CGPointZero;
CGFloat x, y;
x = self.collectionView.center.x - (labelSize.width / 2);
y = self.collectionView.center.y - (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) ? IPHONE_LABEL_OFFSET : IPAD_LABEL_OFFSET);
labelOrigin.x = x;
labelOrigin.y = y;
[label setFrame:CGRectMake(labelOrigin.x, labelOrigin.y, labelSize.width, labelSize.height)];
// set accessibility label and hint for label
if ([self.delegate respondsToSelector:@selector(accessibilityLabelForMenuLabel)]) {
[label setAccessibilityLabel:[self.delegate accessibilityLabelForMenuLabel]];
}
if ([self.delegate respondsToSelector:@selector(accessibilityHintForMenuLabel)]) {
[label setAccessibilityHint:[self.delegate accessibilityHintForMenuLabel]];
}
// add to collectionView
[self.collectionView addSubview:label];
}
}
// register for Accessibility notification for changes in VoiceOver
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(voiceOverChanged) name:UIAccessibilityVoiceOverStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] init];
[tapGesture setNumberOfTapsRequired:1];
[tapGesture setNumberOfTouchesRequired:1];
[tapGesture addTarget:self action:@selector(handleSingleTap:)];
[self.collectionView addGestureRecognizer:tapGesture];
}
}
#pragma mark - Accessibility
- (void)speakSelected {
if (UIAccessibilityIsVoiceOverRunning()) {
AVSpeechSynthesizer *synthesizer = [[AVSpeechSynthesizer alloc] init];
AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:@"button selected"];
[synthesizer speakUtterance:utterance];
}
}
- (void)voiceOverChanged {
[self showCloseButton:UIAccessibilityIsVoiceOverRunning()];
}
- (void)showCloseButton:(BOOL)showButton {
if (showButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.closeButton setAccessibilityLabel:@"Close"];
[self.closeButton setAccessibilityHint:@"Closes the menu"];
[self.closeButton addTarget:self action:@selector(closeButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[self.closeButton setIsAccessibilityElement:YES];
if (self.delegate) {
if ([self.delegate respondsToSelector:@selector(imageForCloseButton)]) {
UIImage *closeImage = [self.delegate imageForCloseButton];
[self.closeButton setImage:closeImage forState:UIControlStateNormal];
CGPoint centerPoint = self.collectionView.center;
[self.closeButton setFrame:CGRectMake(centerPoint.x - (closeImage.size.width / 2.0),
centerPoint.y - (closeImage.size.height / 2.0),
closeImage.size.width,
closeImage.size.height)];
}
}
[self.collectionView addSubview:self.closeButton];
} else {
[self.closeButton removeFromSuperview];
self.closeButton = nil;
}
}
- (void)closeButtonTapped {
[self dismissMenuWithCompletion:^{
AVSpeechSynthesizer *synth = [[AVSpeechSynthesizer alloc] init];
AVSpeechUtterance *utter = [AVSpeechUtterance speechUtteranceWithString:@"Menu closed."];
[synth speakUtterance:utter];
}];
}
#pragma mark - Orientation
// check for orientation type. the UIDeviceOrientationDidChangeNotification sends notifications even for accelerometer changes, like tilting.
// this method makes sure it's only an interface orientation change.
- (void)orientationChanged:(NSNotification *)notification {
UIInterfaceOrientation newOrientation = [[UIApplication sharedApplication] statusBarOrientation];
if (self.isVisible && (newOrientation != self.currentOrientation)) {
[self dismissMenuWithCompletion:^{
NSLog(@"dismissed menu from orientationChanged:");
self.currentOrientation = newOrientation;
}];
}
}
- (CGRect)frameForViewForCurrentOrientation {
CGRect frame = CGRectZero;
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if (orientation == UIDeviceOrientationPortrait || orientation == UIDeviceOrientationPortraitUpsideDown) {
frame = self.viewDisplayingMenu.frame;
} else if (orientation == UIDeviceOrientationLandscapeLeft || orientation == UIDeviceOrientationLandscapeRight) {
frame = CGRectMake(self.viewDisplayingMenu.frame.origin.x,
self.viewDisplayingMenu.frame.origin.y,
self.viewDisplayingMenu.frame.size.height,
self.viewDisplayingMenu.frame.size.width);
}
return frame;
}
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer {
if (self.isVisible) {
CGPoint touch = [gestureRecognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:touch];
if (indexPath) {
[self collectionView:self.collectionView didSelectItemAtIndexPath:indexPath];
} else {
[self dismissMenuWithCompletion:^{
NSLog(@"dismissed menu from handleSingleTap:");
self.currentOrientation = [[UIApplication sharedApplication] statusBarOrientation];
}];
}
}
}
#pragma mark - Menu methods
- (BOOL)isVisible {
return _visible;
}
- (SFSCircleLayout *)circleLayout {
if (_circleLayout) return _circleLayout;
_circleLayout = [[SFSCircleLayout alloc] init];
return _circleLayout;
}
- (UIImage *)blurredImageFromContextWithLightEffect:(SFSLightEffectType)lightEffect {
CGRect bounds = [self frameForViewForCurrentOrientation];
CGSize size = bounds.size;
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
[self.viewDisplayingMenu drawViewHierarchyInRect:CGRectMake(0, 0, size.width, size.height) afterScreenUpdates:YES];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// apply effect
UIImage *blurredImage = nil;
switch (lightEffect) {
case SFSLightEffectTypeDark:
blurredImage = [newImage applyDarkEffect];
break;
case SFSLightEffectTypeExtraLight:
blurredImage = [newImage applyExtraLightEffect];
break;
case SFSLightEffectTypeLight:
blurredImage = [newImage applyLightEffect];
break;
case SFSLightEffectTypeMediumLight:
blurredImage = [newImage applyMediumLightEffect];
break;
default:
blurredImage = [newImage applyLightEffect];
break;
}
return blurredImage;
}
- (void)setBackgroundViewForCollectionWithImage:(UIImage *)image {
if (!_collectionViewBackgroundImageView) {
_collectionViewBackgroundImageView = [[UIImageView alloc] initWithImage:image];
} else {
[self.collectionViewBackgroundImageView setFrame:[self frameForViewForCurrentOrientation]];
[self.collectionViewBackgroundImageView setImage:image];
}
}
- (void)showMenuWithLightEffect:(SFSLightEffectType)lightEffect {
if (!self.isVisible) {
[self setupCollectionView];
self.currentOrientation = [[UIApplication sharedApplication] statusBarOrientation];
// blur background
//
// grab view context and set to image
UIImage *lightImage = [self blurredImageFromContextWithLightEffect:lightEffect];
// set blurred image to custom image view
[self setBackgroundViewForCollectionWithImage:lightImage];
// animate display of blur and menu
[self.collectionViewBackgroundImageView setAlpha:0.0];
[self.collectionView setAlpha:0.0];
[self.viewDisplayingMenu addSubview:self.collectionViewBackgroundImageView];
[self.viewDisplayingMenu addSubview:self.collectionView];
[UIView animateWithDuration:0.1 animations:^{
[self.collectionViewBackgroundImageView setAlpha:1.0];
[self.collectionView setAlpha:1.0];
} completion:^(BOOL finished) {
[self voiceOverChanged];
self.visible = YES;
[self.collectionView reloadData];
[self isAccessibilityElement];
[self setAccessibilityViewIsModal:YES];
}];
}
}
- (void)dismissMenu {
[self dismissMenuWithCompletion:^{
NSLog(@"dismissed menu.");
}];
}
- (void)dismissMenuWithCompletion:(void (^)(void))completion {
if (self.isVisible) {
[UIView animateWithDuration:0.2 animations:^{
[self.collectionView setAlpha:0.0];
[self.collectionViewBackgroundImageView setAlpha:0.0];
} completion:^(BOOL finished) {
[self voiceOverChanged];
[self.collectionView removeFromSuperview];
[self.collectionViewBackgroundImageView removeFromSuperview];
[self.viewDisplayingMenu.window setTintAdjustmentMode:UIViewTintAdjustmentModeNormal];
self.collectionViewBackgroundImageView = nil;
self.collectionView = nil; // this is to make sure menu does not initially draw in previous coordinate then awkwardly shift to right place
self.visible = NO;
if (completion) completion();
}];
}
}
#pragma mark - UICollectionView delegate methods
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (self.delegate) {
if ([self.delegate respondsToSelector:@selector(numberOfButtonsInMenuController:)]) {
NSInteger numCells = [self.delegate numberOfButtonsInMenuController:self];
if (numCells > MAX_CELLS) {
numCells = MAX_CELLS;
}
return numCells;
}
}
return 0;
}
- (BOOL)isButtonEnabledAtIndexPath:(NSIndexPath *)indexPath
{
BOOL isButtonEnabled = YES;
if ([self.delegate respondsToSelector:@selector(isButtonEnabledAtIndexPath:inMenuController:)]) {
isButtonEnabled = [self.delegate isButtonEnabledAtIndexPath:indexPath inMenuController:self];
}
return isButtonEnabled;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
SFSMenuCell *cell = (SFSMenuCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CELL_REUSE_ID forIndexPath:indexPath];
if (self.delegate) {
// is button enabled
BOOL isButtonEnabled = [self isButtonEnabledAtIndexPath:indexPath];
//background image
if ([self.delegate respondsToSelector:@selector(backgroundImageForButtonAtIndexPath:)]) {
[cell setBackgroundImageForCell:[self.delegate backgroundImageForButtonAtIndexPath:indexPath]];
}
if ([self.delegate respondsToSelector:@selector(backgroundColorForButtonAtIndexPath:)]) {
[cell setBackgroundImageForCell:nil];
[cell setBackgroundColorForCell:[self.delegate backgroundColorForButtonAtIndexPath:indexPath]];
}
// foreground image
if ([self.delegate respondsToSelector:@selector(imageForButtonAtIndexPath:)]) {
[cell setImageForCell:[self.delegate imageForButtonAtIndexPath:indexPath]];
if (!isButtonEnabled) {
[cell.imageView setAlpha:0.5f];
} else {
[cell.imageView setAlpha:1.0f];
}
}
// Accessibility label
if ([self.delegate respondsToSelector:@selector(accessibilityLabelForButtonAtIndexPath:)]) {
[cell setAccessibilityLabel:[self.delegate accessibilityLabelForButtonAtIndexPath:indexPath]];
}
// Accessibility hint
if ([self.delegate respondsToSelector:@selector(accessibilityHintForButtonAtIndexPath:)]) {
[cell setAccessibilityHint:[self.delegate accessibilityHintForButtonAtIndexPath:indexPath]];
}
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
// if button is not enabled, just return
BOOL isButtonEnabled = [self isButtonEnabledAtIndexPath:indexPath];
if (!isButtonEnabled) {
return;
}
// remove close button first so it does not display over top of animated button
if (self.closeButton.window) {
[self showCloseButton:NO];
}
NSMutableArray *indexPathsForButtons = [[self.collectionView indexPathsForVisibleItems] mutableCopy];
[indexPathsForButtons removeObject:indexPath];
SFSMenuCell *selectedCell = (SFSMenuCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
NSMutableArray *unselectedCells = [[self.collectionView visibleCells] mutableCopy];
[unselectedCells removeObject:selectedCell];
CGRect selectedCellOriginalRect = selectedCell.frame;
[UIView animateWithDuration:0.2 animations:^{
selectedCell.center = self.collectionView.center;
[unselectedCells enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[obj setAlpha:0.0];
}];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.05 delay:0.15 options:UIViewAnimationOptionCurveLinear animations:^{
[selectedCell setAlpha:0.25];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.05 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
[selectedCell setAlpha:1.0];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[selectedCell setAlpha:0.0];
} completion:^(BOOL finished) {
if ([self.delegate respondsToSelector:@selector(controller:didTapButtonAtIndexPath:)]) {
[self.delegate controller:self didTapButtonAtIndexPath:indexPath];
}
[self speakSelected];
[self dismissMenu];
[selectedCell setFrame:selectedCellOriginalRect];
}];
}];
}];
}];
}
@end