Нарисуйте сегменты из круга или пончик
Я пытался найти способ рисовать сегменты, как показано на следующем рисунке:
Я бы хотел:
- нарисовать отрезок
- включить градиенты
- включать тени
- анимировать рисунок под углом от 0 до n
Я пытался сделать это с CGContextAddArc
и подобные звонки, но не очень далеко.
Кто-нибудь может помочь?
3 ответа
Есть много частей на ваш вопрос.
Получение пути
Создание пути для такого сегмента не должно быть слишком сложным. Есть две дуги и две прямые. Ранее я объяснил, как вы можете сломать такой путь, поэтому я не буду делать это здесь. Вместо этого я собираюсь проявить фантазию и создать путь, поглаживая другой путь. Конечно, вы можете прочитать разбивку и построить путь самостоятельно. Дуга, о которой я говорю, - это оранжевая дуга внутри серого пунктирного конечного результата.
Чтобы пройти путь, нам это сначала нужно. это в основном так же просто, как перейти к начальной точке и нарисовать дугу вокруг центра от текущего угла до угла, который вы хотите охватить сегментом.
CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
YES);
Затем, когда у вас есть этот путь (одна дуга), вы можете создать новый сегмент, обводя его определенной шириной. Получившийся путь будет иметь две прямые линии и две дуги. Ход от центра происходит на равном расстоянии внутрь и наружу.
CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
Рисование
Далее идет рисование, и обычно есть два основных варианта: Базовая графика в drawRect:
или формировать слои с помощью Core Animation. Core Graphics даст вам более мощный рисунок, но Core Animation даст вам лучшую производительность анимации. Поскольку пути задействованы, чистая анимация Cora не будет работать. Вы получите странные артефакты. Однако мы можем использовать комбинацию слоев и Core Graphics, рисуя графический контекст слоя.
Заполнение и поглаживание сегмента
У нас уже есть базовая форма, но прежде чем добавить к ней градиенты и тени, я сделаю базовую заливку и обводку (у вас на изображении черный обводка).
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke);
Это выведет что-то подобное на экран
Добавление теней
Я собираюсь изменить порядок и сделать тень до градиента. Чтобы нарисовать тень, нам нужно настроить тень для контекста и нарисовать заливку формы, чтобы нарисовать ее с тенью. Затем нам нужно восстановить контекст (до тени) и снова обвести форму.
CGColorRef shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75].CGColor;
CGContextSaveGState(c);
CGContextSetShadowWithColor(c,
CGSizeMake(0, 2), // Offset
3.0, // Radius
shadowColor);
CGContextFillPath(c);
CGContextRestoreGState(c);
// Note that filling the path "consumes it" so we add it again
CGContextAddPath(c, strokedArc);
CGContextStrokePath(c);
На данный момент результат примерно такой
Рисование градиента
Для градиента нам нужен градиентный слой. Здесь я делаю очень простой двухцветный градиент, но вы можете настроить его по своему усмотрению. Чтобы создать градиент, нам нужно получить цвета и подходящее цветовое пространство. Затем мы можем нарисовать градиент поверх заливки (но до обводки). Нам также нужно замаскировать градиент к тому же пути, что и раньше. Для этого мы обрезаем путь.
CGFloat colors [] = {
0.75, 1.0, // light gray (fully opaque)
0.90, 1.0 // lighter gray (fully opaque)
};
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceGray(); // gray colors want gray color space
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;
CGContextSaveGState(c);
CGContextAddPath(c, strokedArc);
CGContextClip(c);
CGRect boundingBox = CGPathGetBoundingBox(strokedArc);
CGPoint gradientStart = CGPointMake(0, CGRectGetMinY(boundingBox));
CGPoint gradientEnd = CGPointMake(0, CGRectGetMaxY(boundingBox));
CGContextDrawLinearGradient(c, gradient, gradientStart, gradientEnd, 0);
CGGradientRelease(gradient), gradient = NULL;
CGContextRestoreGState(c);
Это завершает рисунок, так как у нас есть этот результат
Анимация
Когда дело доходит до анимации фигуры, все это было написано ранее: Анимирование кусочков пирога с использованием пользовательского CALayer. Если вы попытаетесь нарисовать, просто анимируя свойство пути, вы увидите действительно забавную деформацию пути во время анимации. Тень и градиент были оставлены нетронутыми для иллюстративных целей на изображении ниже.
Я предлагаю взять код рисования, который я разместил в этом ответе, и применить его к коду анимации из этой статьи. Тогда вы должны в конечном итоге с тем, что вы просите.
Для справки: тот же рисунок с использованием Core Animation
Простая форма
CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = [UIColor lightGrayColor].CGColor;
segment.strokeColor = [UIColor blackColor].CGColor;
segment.lineWidth = 1.0;
segment.path = strokedArc;
[self.view.layer addSublayer:segment];
Добавление теней
Слой имеет некоторые свойства, связанные с тенями, которые вы можете настроить. Как бы то ни было, вы должны установить shadowPath
свойство для улучшения производительности.
segment.shadowColor = [UIColor blackColor].CGColor;
segment.shadowOffset = CGSizeMake(0, 2);
segment.shadowOpacity = 0.75;
segment.shadowRadius = 3.0;
segment.shadowPath = segment.path; // Important for performance
Рисование градиента
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.colors = @[(id)[UIColor colorWithWhite:0.75 alpha:1.0].CGColor, // light gray
(id)[UIColor colorWithWhite:0.90 alpha:1.0].CGColor]; // lighter gray
gradient.frame = CGPathGetBoundingBox(segment.path);
Если бы мы нарисовали градиент сейчас, он был бы поверх фигуры, а не внутри нее. Нет, у нас не может быть градиентной заливки формы (я знаю, что вы думали об этом). Нам нужно замаскировать градиент так, чтобы он выходил за пределы сегмента. Для этого мы создаем еще один слой, который будет маской этого сегмента. Это должен быть другой уровень, в документации ясно, что поведение "неопределено", если маска является частью иерархии уровней. Поскольку система координат маски будет такой же, как и у подслоев градиента, нам нужно будет перевести форму сегмента перед ее настройкой.
CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
-CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
&translation);
gradient.mask = mask;
Все, что вам нужно, описано в Руководстве по программированию Quartz 2D. Я предлагаю вам просмотреть это.
Однако может быть трудно собрать все это вместе, поэтому я проведу вас через это. Мы напишем функцию, которая принимает размер и возвращает изображение, которое выглядит примерно как один из ваших сегментов:
Мы начинаем определение функции следующим образом:
static UIImage *imageWithSize(CGSize size) {
Нам понадобится константа для толщины сегмента:
static CGFloat const kThickness = 20;
и константа для ширины линии, очерчивающей сегмент:
static CGFloat const kLineWidth = 1;
и константа для размера тени:
static CGFloat const kShadowWidth = 8;
Далее нам нужно создать контекст изображения для рисования:
UIGraphicsBeginImageContextWithOptions(size, NO, 0); {
Я ставлю левую скобку в конце этой строки, потому что мне нравится дополнительный уровень отступа, чтобы напомнить мне позвонить UIGraphicsEndImageContext
потом.
Поскольку многие функции, которые нам нужно вызвать, являются функциями Core Graphics (или Quartz 2D), а не функциями UIKit, нам нужно получить CGContext
:
CGContextRef gc = UIGraphicsGetCurrentContext();
Теперь мы готовы начать. Сначала мы добавляем дугу к пути. Дуга проходит вдоль центра отрезка, который мы хотим нарисовать:
CGContextAddArc(gc, size.width / 2, size.height / 2,
(size.width - kThickness - kLineWidth) / 2,
-M_PI / 4, -3 * M_PI / 4, YES);
Теперь мы попросим Core Graphics заменить путь "заштрихованной" версией, которая обрисовывает контур. Сначала мы устанавливаем толщину обводки на толщину, которую мы хотим, чтобы сегмент имел:
CGContextSetLineWidth(gc, kThickness);
и мы установили стиль линии колпачка на "встык", чтобы у нас были квадраты с квадратами:
CGContextSetLineCap(gc, kCGLineCapButt);
Затем мы можем попросить Core Graphics заменить путь штриховой версией:
CGContextReplacePathWithStrokedPath(gc);
Чтобы заполнить этот путь линейным градиентом, мы должны указать Core Graphics обрезать все операции внутри пути. Это заставит Core Graphics сбросить путь, но позже нам понадобится путь, чтобы нарисовать черную линию по краю. Итак, мы скопируем путь здесь:
CGPathRef path = CGContextCopyPath(gc);
Так как мы хотим, чтобы сегмент отбрасывал тень, мы установим параметры тени, прежде чем делать какие-либо рисунки:
CGContextSetShadowWithColor(gc,
CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2,
[UIColor colorWithWhite:0 alpha:0.3].CGColor);
Мы собираемся заполнить сегмент (градиентом) и обвести его (чтобы нарисовать черный контур). Мы хотим одну тень для обеих операций. Мы сообщаем Core Graphics, что, начиная слой прозрачности:
CGContextBeginTransparencyLayer(gc, 0); {
Я ставлю левую скобку в конце этой строки, потому что мне нравится иметь дополнительный уровень отступа, чтобы напомнить мне позвонить CGContextEndTransparencyLayer
потом.
Так как мы собираемся изменить область клипа контекста для заполнения, но мы не хотим обрезать, когда позже будем обводить контур, нам нужно сохранить состояние графики:
CGContextSaveGState(gc); {
Я ставлю левую скобку в конце этой строки, потому что мне нравится иметь дополнительный уровень отступа, чтобы напомнить мне позвонить CGContextRestoreGState
потом.
Чтобы заполнить путь градиентом, нам нужно создать объект градиента:
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColors(rgb, (__bridge CFArrayRef)@[
(__bridge id)[UIColor grayColor].CGColor,
(__bridge id)[UIColor whiteColor].CGColor
], (CGFloat[]){ 0.0f, 1.0f });
CGColorSpaceRelease(rgb);
Нам также нужно определить начальную точку и конечную точку для градиента. Мы будем использовать ограничивающий путь путь:
CGRect bbox = CGContextGetPathBoundingBox(gc);
CGPoint start = bbox.origin;
CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox));
и мы заставим градиент рисовать горизонтально или вертикально, в зависимости от того, что длиннее:
if (bbox.size.width > bbox.size.height) {
end.y = start.y;
} else {
end.x = start.x;
}
Теперь у нас наконец есть все, что нужно для рисования градиента. Сначала мы обрезаем путь:
CGContextClip(gc);
Затем мы рисуем градиент:
CGContextDrawLinearGradient(gc, gradient, start, end, 0);
Затем мы можем освободить градиент и восстановить сохраненное графическое состояние:
CGGradientRelease(gradient);
} CGContextRestoreGState(gc);
Когда мы позвонили CGContextClip
, Core Graphics сбрасывает путь контекста. Путь не является частью сохраненного графического состояния; Вот почему мы сделали копию ранее. Теперь пришло время использовать эту копию, чтобы снова установить путь в контексте:
CGContextAddPath(gc, path);
CGPathRelease(path);
Теперь мы можем обвести путь, нарисовать черный контур сегмента:
CGContextSetLineWidth(gc, kLineWidth);
CGContextSetLineJoin(gc, kCGLineJoinMiter);
[[UIColor blackColor] setStroke];
CGContextStrokePath(gc);
Далее мы говорим Core Graphics, чтобы закончить слой прозрачности. Это позволит взглянуть на то, что мы нарисовали, и добавить тень под ним:
} CGContextEndTransparencyLayer(gc);
Теперь мы все закончили рисовать. Мы просим UIKit создать UIImage
из контекста изображения, затем уничтожьте контекст и верните изображение:
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Вы можете найти код все вместе в этой сути.
Это Swift 3 версия ответа Роба Мейоффа. Просто посмотрите, насколько эффективнее этот язык! Это может быть содержимое файла MView.swift:
import UIKit
class MView: UIView {
var size = CGSize.zero
override init(frame: CGRect) {
super.init(frame: frame)
size = frame.size
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var niceImage: UIImage {
let kThickness = CGFloat(20)
let kLineWidth = CGFloat(1)
let kShadowWidth = CGFloat(8)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let gc = UIGraphicsGetCurrentContext()!
gc.addArc(center: CGPoint(x: size.width/2, y: size.height/2),
radius: (size.width - kThickness - kLineWidth)/2,
startAngle: -45°,
endAngle: -135°,
clockwise: true)
gc.setLineWidth(kThickness)
gc.setLineCap(.butt)
gc.replacePathWithStrokedPath()
let path = gc.path!
gc.setShadow(
offset: CGSize(width: 0, height: kShadowWidth/2),
blur: kShadowWidth/2,
color: UIColor.gray.cgColor
)
gc.beginTransparencyLayer(auxiliaryInfo: nil)
gc.saveGState()
let rgb = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(
colorsSpace: rgb,
colors: [UIColor.gray.cgColor, UIColor.white.cgColor] as CFArray,
locations: [CGFloat(0), CGFloat(1)])!
let bbox = path.boundingBox
let startP = bbox.origin
var endP = CGPoint(x: bbox.maxX, y: bbox.maxY);
if (bbox.size.width > bbox.size.height) {
endP.y = startP.y
} else {
endP.x = startP.x
}
gc.clip()
gc.drawLinearGradient(gradient, start: startP, end: endP,
options: CGGradientDrawingOptions(rawValue: 0))
gc.restoreGState()
gc.addPath(path)
gc.setLineWidth(kLineWidth)
gc.setLineJoin(.miter)
UIColor.black.setStroke()
gc.strokePath()
gc.endTransparencyLayer()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
override func draw(_ rect: CGRect) {
niceImage.draw(at:.zero)
}
}
Вызовите его из viewController следующим образом:
let vi = MView(frame: self.view.bounds)
self.view.addSubview(vi)
Для преобразования градусов в радианы я создал оператор ° postfix. Так что теперь вы можете использовать, например, 45°, и это делает преобразование из 45 градусов в радианы. Этот пример для Ints, расширяйте их также для типов Float, если у вас есть необходимость:
postfix operator °
protocol IntegerInitializable: ExpressibleByIntegerLiteral {
init (_: Int)
}
extension Int: IntegerInitializable {
postfix public static func °(lhs: Int) -> CGFloat {
return CGFloat(lhs) * .pi / 180
}
}
Поместите этот код в файл утилиты Swift.