-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathJDCarouselControl.m
369 lines (290 loc) · 14.1 KB
/
JDCarouselControl.m
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
#import "JDCarouselControl.h"
#define INNER_PROPORTION 0.6
#define VIEW_RADIUS_PLACEMENT_PROPORTION 0.8
#define VIEW_SCALING_FACTOR 0.3
#define DISABLED_ALPHA 0.5
@interface JDCarouselControl ()
typedef struct margins {
CGFloat hMargin;
CGFloat vMargin;
} Margins;
@property (nonatomic) NSMutableArray *items;
@property (nonatomic) CGFloat diameter;
@property (nonatomic) Margins margins;
@property (nonatomic) CGFloat innerRadius;
@property (nonatomic) CGFloat innerDiameter;
@property (nonatomic) Margins innerMargins;
@property (nonatomic) float arcLength;
@property (nonatomic) int prevNumberOfIndices;
@property (nonatomic, readwrite) NSInteger numberOfSegments;
@property (nonatomic, readwrite) NSInteger selectedSegmentIndex;
@property (nonatomic, readwrite) NSInteger previousIndex;
@property (nonatomic) NSMutableSet *indicesToFillImagesAt;
@end
@implementation JDCarouselControl
#pragma mark Initialization
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self _initialSetup];
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self _initialSetup];
}
return self;
}
-(void)_initialSetup {
self.selectedSegmentIndex = 0;
self.color = self.tintColor; // default color set to tintColor
self.textColor = [UIColor blackColor]; // default selected text color is black
self.backgroundColor = [UIColor clearColor];
self.indicesToFillImagesAt = [[NSMutableSet alloc] initWithCapacity:2];
}
- (void)drawRect:(CGRect)rect
{
// Update the dimensions so we know exactly what to draw
[self _updateDimensions];
CGContextRef context = UIGraphicsGetCurrentContext();
// Define the CGRects for inner & outer circles
CGRect outerCircleRect = CGRectMake(self.margins.hMargin, self.margins.vMargin, self.diameter, self.diameter);
CGRect innerCircleRect = CGRectMake(self.innerMargins.hMargin, self.innerMargins.vMargin, self.innerDiameter, self.innerDiameter);
// Just the stroke, for the outer circle
CGContextSetStrokeColorWithColor(context, self.color.CGColor);
CGContextStrokeEllipseInRect(context, outerCircleRect);
// And now the inner
CGContextStrokeEllipseInRect(context, innerCircleRect);
// And the arcs
[self _drawSegments];
}
-(void)_drawSegments {
CGPoint centerd = {self.frame.size.width/2, self.frame.size.height/2};
if (self.numberOfSegments < 1) {
// don't draw any radial lines
return;
}
// We'll start from the conventional 0º and head ccw
CGContextRef context = UIGraphicsGetCurrentContext();
// Stroke Paths
CGContextBeginPath(context);
CGContextSetStrokeColorWithColor(context, self.color.CGColor);
for (int i = 0; i < self.numberOfSegments; i++) {
CGContextMoveToPoint(context, centerd.x, centerd.y);
CGContextMoveToPoint(context, centerd.x + cos(i*self.arcLength)*self.innerRadius, centerd.y + sin(i*self.arcLength)*self.innerRadius);
CGContextAddLineToPoint(context, centerd.x + cos(i*self.arcLength)*self.radius, centerd.y + sin(i*self.arcLength)*self.radius);
}
CGContextStrokePath(context);
// Fill selected segment index
CGContextBeginPath(context);
CGContextMoveToPoint(context, centerd.x + cos(self.selectedSegmentIndex*self.arcLength)*self.innerRadius, centerd.y + sin(self.selectedSegmentIndex*self.arcLength)*self.innerRadius);
CGContextAddLineToPoint(context, centerd.x + cos(self.selectedSegmentIndex*self.arcLength)*self.radius, centerd.y + sin(self.selectedSegmentIndex*self.arcLength)*self.radius);
CGContextAddArc(context, centerd.x, centerd.y, self.radius, self.arcLength*self.selectedSegmentIndex, self.arcLength*(self.selectedSegmentIndex + 1), 0);
CGContextAddLineToPoint(context, centerd.x + cos((self.selectedSegmentIndex + 1)*self.arcLength)*self.innerRadius, centerd.y + sin((self.selectedSegmentIndex + 1)*self.arcLength)*self.innerRadius);
CGContextAddArc(context, centerd.x, centerd.y, self.innerRadius, self.arcLength*(self.selectedSegmentIndex + 1), self.arcLength*self.selectedSegmentIndex, 1);
CGContextSetFillColorWithColor(context, (self.enabled ? self.color.CGColor : [self.color colorWithAlphaComponent:DISABLED_ALPHA].CGColor));
CGContextFillPath(context);
}
-(void)setNeedsLayout {
if (self.prevNumberOfIndices != self.numberOfSegments) {
[self setNeedsDisplay];
[self _updateDimensions];
[self _layoutSegments];
}
self.prevNumberOfIndices = self.numberOfSegments;
}
-(void)setRadius:(CGFloat)radius {
if (!(radius > self.frame.size.width/2 || radius > self.frame.size.height/2)) {
_radius = radius;
self.diameter = radius*2;
}
}
-(void)setInnerRadius:(CGFloat)innerRadius {
if (self.innerRadius >= self.radius)
return;
_innerRadius = innerRadius;
self.innerDiameter = innerRadius*2;
}
// Segment inserting -- always calls a common private method _insertSegment
- (void)insertSegments:(NSArray *)segments {
for (id item in segments) {
[self _insertSegment:item atIndex:self.numberOfSegments];
}
}
- (void)insertSegmentWithTitle:(NSString *)title {
[self _insertSegment:title atIndex:self.numberOfSegments];
}
- (void)insertSegmentWithImage:(UIImage *)image
{
[self _insertSegment:image atIndex:self.numberOfSegments];
}
-(void)insertSegmentWithTitle:(NSString *)title atIndex:(NSUInteger)segment {
[self _insertSegment:title atIndex:segment];
}
- (void)insertSegmentWithImage:(UIImage *)image atIndex:(NSUInteger)segment
{
[self _insertSegment:image atIndex:segment];
}
-(void)removeSegmentAtIndex:(NSUInteger)index {
if (index >= [self.items count]) return;
[[self.items objectAtIndex:index] removeFromSuperview];
[self.items removeObjectAtIndex:index];
[self setNeedsLayout];
}
-(int)numberOfSegments {
return [_items count];
}
#pragma mark Touch Handling
-(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
[self sendActionsForControlEvents:UIControlEventTouchDown];
return YES;
}
-(void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [touch locationInView:self];
NSInteger tappedIndex = [self _tappedSegmentIndex:location];
if (tappedIndex != -1) {
// Handle Select
[self setNeedsDisplay];
self.previousIndex = self.selectedSegmentIndex;
[self sendActionsForControlEvents:UIControlEventTouchUpInside];
if (tappedIndex != self.selectedSegmentIndex) {
[self.indicesToFillImagesAt addObject:[NSNumber numberWithInt:tappedIndex]];
[self.indicesToFillImagesAt addObject:[NSNumber numberWithInt:self.previousIndex]];
self.selectedSegmentIndex = tappedIndex;
[self _updateLabelColors];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
}
}
-(NSInteger)_tappedSegmentIndex:(CGPoint)tapLocation {
/* In order to determine the tapped segment index, we'll have to use a polar co-ordinate system – first determine the distance between the tapped location and the center (r), and then the angle of that point from 0º (theta). We can then do the following tests:
1. Is r between innerRadius and radius?
If it is, we know it's a valid touch and HAS to be mapped to a segment index. Proceed to (2)
If it isn't, we know it is not a valid touch and CANNOT be mapped to a segment index (return -1)
2. Divide the theta value by the arc length of the individual segments, cast it to an integer. This will be the segment index tapped.
But how do we get the r and theta values? It should be a simple conversion, but we need to find the rectangular co-ordinates with respect to the center first.
x is (center.x - tapLocation.x)
y is (center.y - tapLocation.y)
Now use Py. to find r, and theta is simply atan(y/x)
*/
CGPoint centerd = {self.frame.size.width/2, self.frame.size.height/2};
float x = tapLocation.x - centerd.x;
float y = tapLocation.y - centerd.y;
float R = sqrtf(x*x + y*y);
float thetaR = atanf(y/x); // This is the 'raw theta'; it's positive in Q2 and Q4 and negative in Q1 and Q3. It's not an absolute angle relative to 0 (CLOCKWISE)
float theta;
if (x >= 0 && y < 0) // Q1
theta = 2*M_PI + thetaR;
else if (x >= 0 && y >= 0) // Q4
theta = thetaR;
else if (x < 0 && y < 0) // Q2
theta = M_PI + thetaR;
else if (x < 0 && y >= 0) // Q3
theta = M_PI + thetaR;
else return -1;
if (R > self.radius || R < self.innerRadius)
return -1;
return (int)(theta/self.arcLength);
}
#pragma mark View
-(void)_updateDimensions {
// First deal with the larger circle
self.radius = ((self.frame.size.width < self.frame.size.height) ? self.frame.size.width/2.2 : self.frame.size.height/2.2);
CGFloat vMargins = self.frame.size.height - self.diameter;
CGFloat hMargins = self.frame.size.width - self.diameter;
Margins margins = {hMargins/2, vMargins/2};
self.margins = margins;
// Now we'll define the dimensions of the smaller circle with respect to the bounds
self.innerRadius = self.radius * INNER_PROPORTION;
CGFloat vMarginsInner = self.frame.size.height - self.innerDiameter;
CGFloat hMarginsInner = self.frame.size.width - self.innerDiameter;
Margins innerMargins = {hMarginsInner/2, vMarginsInner/2};
self.innerMargins = innerMargins;
// Now we'll deal with getting the angles of the segments
self.arcLength = (self.numberOfSegments ? (2*M_PI / self.numberOfSegments) : 2*M_PI);
}
-(void)_layoutSegments {
/* The challenge here is placing each view such that it's within and centred within each circular segment. We'll do this by applying the following 'transformation' starting from the point (center.x + radius*cos(arcLength), center.y + radius*sin(arcLength)) – the end point of each radial line drawn in drawRect
1. Translate arcLength/2 clockwise (in polar co-ordinates, on the theta-axis)
2. Scale radius by VIEW_RADIUS_PLACEMENT_PROPORTION (in polar co-ordinates, on the r-axis)
*/
CGPoint centerd = {self.frame.size.width/2, self.frame.size.height/2};
const float size_x = self.radius*VIEW_SCALING_FACTOR;
const float size_y = self.radius*VIEW_SCALING_FACTOR;
const float scalingFactor = VIEW_RADIUS_PLACEMENT_PROPORTION;
for (int i = 0; i < self.numberOfSegments; i++) {
[[self.items objectAtIndex:i] setFrame:CGRectMake(centerd.x + cos(i*self.arcLength + self.arcLength/2)*self.radius*scalingFactor - size_x/2, centerd.y + sin(i*self.arcLength + self.arcLength/2)*self.radius*scalingFactor - size_y/2, size_x, size_y)];
[self _updateLabelColors];
}
}
-(void)_updateLabelColors {
for (int i = 0; i < self.numberOfSegments; i++) {
if ([[[self.items objectAtIndex:i] viewWithTag:1] isKindOfClass:[UILabel class]]) {
UILabel *textLabel = (UILabel*)[[self.items objectAtIndex:i] viewWithTag:1];
if (i == self.selectedSegmentIndex) {
textLabel.textColor = self.textColor;
}
else {
textLabel.textColor = self.color;
}
}
else if ([[[self.items objectAtIndex:i] viewWithTag:1] isKindOfClass:[UIImageView class]]) {
// Check to see if we need to reload this image, otherwise skip over it
if ([self.indicesToFillImagesAt member:[NSNumber numberWithInt:i]]) {
UIImageView *imgView = (UIImageView*)[[self.items objectAtIndex:i] viewWithTag:1];
if (i == self.selectedSegmentIndex) {
imgView.image = [self _fillImage:imgView.image withColor:self.textColor];
}
else {
imgView.image = [self _fillImage:imgView.image withColor:self.color];
}
}
}
}
[self.indicesToFillImagesAt removeAllObjects];
}
-(void)_insertSegment:(id)content atIndex:(NSUInteger)index {
if (!_items) _items = [[NSMutableArray alloc] init];
UIView *item = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
item.userInteractionEnabled = NO;
if ([content isKindOfClass:[NSString class]]) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0,0,self.radius*VIEW_SCALING_FACTOR,self.radius*VIEW_SCALING_FACTOR)];
label.text = content;
label.textAlignment = NSTextAlignmentCenter;
label.textColor = self.color;
label.adjustsFontSizeToFitWidth = YES;
label.minimumScaleFactor = 0.1f;
label.tag = 1;
[item addSubview:label];
}
else if ([content isKindOfClass:[UIImage class]]) {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0,0,self.radius*VIEW_SCALING_FACTOR,self.radius*VIEW_SCALING_FACTOR)];
imageView.image = [self _fillImage:content withColor:self.color];
imageView.tag = 1;
[item addSubview:imageView];
}
[self addSubview:item];
if (index >= self.items.count) [self.items addObject:item];
else [self.items insertObject:item atIndex:index];
[self.indicesToFillImagesAt addObject:[NSNumber numberWithInt:index]];
[self setNeedsLayout];
}
-(UIImage *)_fillImage:(UIImage *)mask withColor:(UIColor *)color {
CGImageRef maskImage = mask.CGImage;
CGFloat width = mask.size.width;
CGFloat height = mask.size.height;
CGRect bounds = CGRectMake(0,0,width,height);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bitmapContext = CGBitmapContextCreate(NULL, width, height, 8, 0, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
CGContextClipToMask(bitmapContext, bounds, maskImage);
CGContextSetFillColorWithColor(bitmapContext, color.CGColor);
CGContextFillRect(bitmapContext, bounds);
CGImageRef mainViewContentBitmapContext = CGBitmapContextCreateImage(bitmapContext);
CGContextRelease(bitmapContext);
UIImage *result = [UIImage imageWithCGImage:mainViewContentBitmapContext];
return result;
}
@end