Я думаю, вам будет проще, если вы создадите представление для каждого среза и используете UIPinchGestureRecognizer
. Вот как.
Во-первых, нам нужен подкласс UIView
, который рисует один срез. Также следует переопределить pointInside:withEvent:
, чтобы игнорировать касание, которое попадает за пределы среза (даже если касание находится внутри прямоугольных границ вида).
Итак, мы создадим класс с именем SliceView
. Для рисования среза используется CAShapeLayer
:
@interface SliceView : UIView
@property (nonatomic) CGFloat padding;
@property (nonatomic) CGFloat startRadians;
@property (nonatomic) CGFloat endRadians;
@property (nonatomic, strong) UIColor *fillColor;
@end
@implementation SliceView
@synthesize padding = _padding;
@synthesize startRadians = _startRadians;
@synthesize endRadians = _endRadians;
@synthesize fillColor = _fillColor;
Мы говорим ему использовать CAShapeLayer
вместо простого CALayer
, переопределяя метод layerClass
. Мы также добавим удобный метод, который возвращает слой вида как CAShapeLayer
.
+ (Class)layerClass {
return [CAShapeLayer class];
}
- (CAShapeLayer *)shapeLayer {
return (CAShapeLayer *)self.layer;
}
Мы вычислим путь среза в layoutSubviews
, потому что представление получает сообщение layoutSubviews
каждый раз, когда изменяется его размер.
Мы собираемся выложить каждый вид среза, чтобы покрыть весь пирог, но только нарисуем его клин пирога. Рамка каждого среза будет покрывать весь экран (если круговая диаграмма полноэкранная). Это означает, что вид среза знает, что центр его дуги находится в центре его границ. Но затем мы используем небольшую тригонометрию, чтобы вставить отступ между смежными срезами.
Мы также корректируем опорную точку слоя; это точка в слое, которая не перемещается при масштабировании или повороте слоя. Мы хотим, чтобы точка привязки находилась в углу среза, ближайшего к центру.
- (void)layoutSubviews {
CAShapeLayer *layer = self.shapeLayer;
CGRect bounds = self.bounds;
CGFloat radius = MIN(bounds.size.width, bounds.size.height) / 2 - 2 * _padding;
CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGFloat sine = sinf((_startRadians + _endRadians) * 0.5f);
CGFloat cosine = cosf((_startRadians + _endRadians) * 0.5f);
center.x += _padding * cosine;
center.y += _padding * sine;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:center];
[path addArcWithCenter:center radius:radius startAngle:_startRadians endAngle:_endRadians clockwise:YES];
[path closePath];
layer.path = path.CGPath;
// Move my anchor point to the corner of my path so scaling will leave the corner in the same place.
CGPoint cornerInSuperview = [self convertPoint:center toView:self.superview];
layer.anchorPoint = CGPointMake(center.x / bounds.size.width, center.y / bounds.size.height);
self.center = cornerInSuperview;
}
Когда любое из свойств представления, относящихся к срезу, изменяется, нам нужно пересчитать путь, очерчивающий срез. И когда цвет заливки среза изменяется, мы должны передать это изменение слою. Поэтому мы переопределим установщики свойств.
- (void)setPadding:(CGFloat)padding {
_padding = padding;
[self setNeedsLayout];
}
- (void)setStartRadians:(CGFloat)startRadians {
_startRadians = startRadians;
[self setNeedsLayout];
}
- (void)setEndRadians:(CGFloat)endRadians {
_endRadians = endRadians;
[self setNeedsLayout];
}
- (void)setFillColor:(UIColor *)color {
_fillColor = color;
self.shapeLayer.fillColor = color.CGColor;
}
Наконец, мы переопределяем pointInside:withEvent:
, так что тестирование попаданий назначит касание только виду среза, если касание действительно находится внутри пути среза. Это очень важно, поскольку все виды срезов будут иметь рамку, охватывающую весь экран.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return CGPathContainsPoint(self.shapeLayer.path, NULL, point, NO);
}
@end
Теперь, когда у нас есть удобный класс SliceView
, мы можем использовать его для рисования круговой диаграммы с масштабируемыми срезами. Трудно втиснуть два пальца в срез на экране iPhone, поэтому мы позволим пользователю коснуться среза, чтобы выбрать его, и ущипнуть в любом месте для масштабирования выбранного среза. (Этот интерфейс также делает его тестируемым в симуляторе.)
@implementation ViewController {
__weak SliceView *_selectedSlice;
}
Мы нарисуем невыбранные кусочки красным, а выделенный кусочек синим.
+ (UIColor *)unselectedSliceFillColor {
return UIColor.redColor;
}
+ (UIColor *)selectedSliceFillColor {
return UIColor.blueColor;
}
Когда пользователь нажимает на срез, нам нужно изменить цвета предыдущего и нового выделения и записать новый выбор.
- (IBAction)sliceWasTapped:(UITapGestureRecognizer *)tapper {
_selectedSlice.fillColor = self.class.unselectedSliceFillColor;
_selectedSlice = (SliceView *)tapper.view;
_selectedSlice.fillColor = self.class.selectedSliceFillColor;
}
Когда пользователь сжимает, мы настраиваем преобразование выбранного среза, если он есть.
- (IBAction)pinched:(UIPinchGestureRecognizer *)pincher {
if (!_selectedSlice)
return;
CGFloat scale = pincher.scale;
pincher.scale = 1;
_selectedSlice.transform = CGAffineTransformScale(_selectedSlice.transform, scale, scale);
}
Наконец, нам нужно создать виды слайсов и распознаватели жестов. Мы создаем один распознаватель касаний для каждого среза и один «глобальный» распознаватель пинчей, прикрепленный к фоновому виду.
- (void)viewDidLoad {
static int const SliceCount = 12;
CGRect bounds = self.view.bounds;
for (int i = 0; i < SliceCount; ++i) {
SliceView *slice = [[SliceView alloc] initWithFrame:bounds];
slice.startRadians = 2 * M_PI * i / SliceCount;
slice.endRadians = 2 * M_PI * (i + 1) / SliceCount;
slice.padding = 4;
slice.fillColor = self.class.unselectedSliceFillColor;
slice.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:slice];
UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(sliceWasTapped:)];
[slice addGestureRecognizer:tapper];
}
UIPinchGestureRecognizer *pincher = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinched:)];
[self.view addGestureRecognizer:pincher];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
@end
А вот как это выглядит:
Вы можете скачать мой тестовый проект здесь: http://dl.dropbox.com/u/26919672/pie.zip
UPDATE
В ответ на ваш комментарий с вопросом об ограничении масштаба я бы предложил добавить еще несколько свойств к SliceView
:
@property (nonatomic) CGFloat minScale;
@property (nonatomic) CGFloat maxScale;
@property (nonatomic) CGFloat scale;
Важно: Вам нужно будет инициализировать все три свойства: 1 в initWithFrame:
и initWithCoder:
.
Затем реализуйте установщик scale
, чтобы фактически применить ограничения и установить масштаб:
- (void)setScale:(CGFloat)scale {
_scale = MAX(minScale, MIN(scale, maxScale));
self.transform = CGAffineTransformMakeScale(_scale, _scale);
}
В pinched:
вы обновляете свойство scale
представления вместо прямой установки свойства transform
представления:
- (IBAction)pinched:(UIPinchGestureRecognizer *)pincher {
if (!_selectedSlice)
return;
CGFloat scale = pincher.scale;
pincher.scale = 1;
_selectedSlice.scale = _selectedSlice.scale * scale;
}