Dibuja segmentos de un círculo o donut

He estado tratando de encontrar una forma de dibujar segmentos como se ilustra en la siguiente imagen:

enter image description here

Me gustaría:

  1. dibujar el segmento
  2. incluir degradados
  3. incluir sombras
  4. animar el dibujo de 0 a n ángulo

He estado intentando hacer esto con CGContextAddArc y llamadas similares, pero sin llegar muy lejos.

Alguien puede ayudar ?

Hay muchas partes para su pregunta.

Obteniendo el camino

Crear la ruta para un segmento así no debería ser demasiado difícil. Hay dos arcos y dos líneas rectas. Anteriormente le expliqué cómo puede analizar un camino así, así que no lo haré aquí. En cambio, voy a ser elegante y crear el camino acariciando otro camino. Por supuesto, puede leer el desglose y construir el camino usted mismo. El arco al que me refiero es el arco naranja dentro del resultado final gris punteado.

Camino a ser acariciado

Para acariciar el camino, primero lo necesitamos. eso es básicamente tan simple como moverse al punto de inicio y dibujar un arco alrededor del centro desde el ángulo actual al ángulo que desea que cubra el segmento.

 CGMutablePathRef arc = CGPathCreateMutable(); CGPathMoveToPoint(arc, NULL, startPoint.x, startPoint.y); CGPathAddArc(arc, NULL, centerPoint.x, centerPoint.y, radius, startAngle, endAngle, YES); 

Luego, cuando tenga esa ruta (arco simple), puede crear el nuevo segmento acariciándolo con un ancho determinado. La ruta resultante tendrá las dos líneas rectas y los dos arcos. El golpe ocurre desde el centro a igual distancia hacia adentro y hacia afuera.

 CGFloat lineWidth = 10.0; CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL, lineWidth, kCGLineCapButt, kCGLineJoinMiter, // the default 10); // 10 is default miter limit 

Dibujo

El siguiente es dibujar y generalmente hay dos opciones principales: Gráficos básicos en drawRect: o capas de formas con Core Animation. Core Graphics te dará el dibujo más poderoso, pero Core Animation te dará el mejor rendimiento de animación. Como las rutas están involucradas, la Animación Cora pura no funcionará. Terminarás con artefactos extraños. Sin embargo, podemos usar una combinación de capas y Core Graphics dibujando el contexto gráfico de la capa.

Llenar y acariciar el segmento

Ya tenemos la forma básica, pero antes de agregarle gradientes y sombras, haré un relleno básico y un trazo (tiene un trazo negro en su imagen).

 CGContextRef c = UIGraphicsGetCurrentContext(); CGContextAddPath(c, strokedArc); CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor); CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor); CGContextDrawPath(c, kCGPathFillStroke); 

Eso pondrá algo como esto en la pantalla

Forma llena y acariciada

Añadiendo sombras

Voy a cambiar el orden y hacer la sombra antes del gradiente. Para dibujar la sombra necesitamos configurar una sombra para el contexto y dibujar llenar la forma para dibujarlo con la sombra. Luego tenemos que restaurar el contexto (antes de la sombra) y acariciar la forma nuevamente.

 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); 

En este punto, el resultado es algo como esto

enter image description here

Dibujando el degradado

Para el degradado, necesitamos una capa de degradado. Estoy haciendo un gradiente de dos colores muy simple aquí, pero puedes personalizarlo todo lo que quieras. Para crear el degradado necesitamos obtener los colores y el espacio de color adecuado. Luego podemos dibujar el degradado sobre el relleno (pero antes del trazo). También necesitamos enmascarar el degradado en el mismo camino que antes. Para hacer esto, recortamos el camino.

 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); 

Esto termina el sorteo ya que actualmente tenemos este resultado

Gradiente enmascarado

Animación

Cuando se trata de la animación de la forma, todo se ha escrito antes: Animación de sectores de tarta con un CALayer personalizado . Si intentas hacer el dibujo simplemente animando la propiedad de la ruta, verás una deformación realmente funky de la ruta durante la animación. La sombra y el degradado se han dejado intactos con fines ilustrativos en la imagen siguiente.

Funky warping de camino

Sugiero que tome el código de dibujo que he publicado en esta respuesta y lo adopte al código de animación de ese artículo. Entonces deberías terminar con lo que estás pidiendo.


Para referencia: el mismo dibujo usando Core Animation

Forma simple

 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]; 

Añadiendo sombras

La capa tiene algunas propiedades relacionadas con la sombra que depende de usted personalizar. Sin embargo , debes establecer la propiedad shadowPath para un mejor rendimiento.

 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 

Dibujando el degradado

 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); 

Si dibujáramos el degradado ahora, estaría encima de la forma y no dentro de ella. No, no podemos tener un degradado relleno de la forma (sé que estabas pensando en eso). Necesitamos enmascarar el gradiente para que salga del segmento. Para hacer eso, creamos otra capa para que sea la máscara de ese segmento. Tiene que ser otra capa, la documentación es clara de que el comportamiento es “indefinido” si la máscara es parte de la jerarquía de capas. Como el sistema de coordenadas de la máscara va a ser el mismo que el de las subcapas al degradado, tendremos que traducir la forma del segmento antes de configurarlo.

 CAShapeLayer *mask = [CAShapeLayer layer]; CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame), -CGRectGetMinY(gradient.frame)); mask.path = CGPathCreateCopyByTransformingPath(segment.path, &translation); gradient.mask = mask; 

Todo lo que necesita está cubierto en la Guía de progtwigción 2D de Quartz . Te sugiero que mires a través de él.

Sin embargo, puede ser difícil juntarlo todo, así que lo guiaré a través de él. Escribiremos una función que toma un tamaño y devuelve una imagen que se parece más o menos a uno de sus segmentos:

arco con contorno, degradado y sombra

Comenzamos la definición de la función de esta manera:

 static UIImage *imageWithSize(CGSize size) { 

Necesitaremos una constante para el grosor del segmento:

  static CGFloat const kThickness = 20; 

y una constante para el ancho de la línea que delinea el segmento:

  static CGFloat const kLineWidth = 1; 

y una constante para el tamaño de la sombra:

  static CGFloat const kShadowWidth = 8; 

A continuación, necesitamos crear un contexto de imagen para dibujar:

  UIGraphicsBeginImageContextWithOptions(size, NO, 0); { 

Puse un corchete izquierdo al final de esa línea porque me gusta un nivel extra de sangría para recordarme que llame a UIGraphicsEndImageContext más tarde.

Dado que muchas de las funciones que necesitamos llamar son funciones de Core Graphics (también conocido como Quartz 2D), no funciones de UIKit, necesitamos obtener el CGContext :

  CGContextRef gc = UIGraphicsGetCurrentContext(); 

Ahora estamos listos para realmente comenzar. Primero agregamos un arco a la ruta. El arco corre a lo largo del centro del segmento que queremos dibujar:

  CGContextAddArc(gc, size.width / 2, size.height / 2, (size.width - kThickness - kLineWidth) / 2, -M_PI / 4, -3 * M_PI / 4, YES); 

Ahora le pediremos a Core Graphics que reemplace la ruta con una versión “acariciada” que describa la ruta. Primero establecemos el grosor del trazo al grosor que queremos que tenga el segmento:

  CGContextSetLineWidth(gc, kThickness); 

y establecemos el estilo de límite de línea en “extremo”, así tendremos extremos cuadrados :

  CGContextSetLineCap(gc, kCGLineCapButt); 

Entonces podemos pedirle a Core Graphics que reemplace la ruta con una versión cargada:

  CGContextReplacePathWithStrokedPath(gc); 

Para llenar este camino con un degradado lineal, debemos decirle a Core Graphics que recorte todas las operaciones al interior de la ruta. Si lo hace, Core Graphics reiniciará la ruta, pero luego necesitaremos la ruta para dibujar la línea negra alrededor del borde. Así que copiaremos el camino aquí:

  CGPathRef path = CGContextCopyPath(gc); 

Como queremos que el segmento arroje una sombra, estableceremos los parámetros de sombra antes de hacer cualquier dibujo:

  CGContextSetShadowWithColor(gc, CGSizeMake(0, kShadowWidth / 2), kShadowWidth / 2, [UIColor colorWithWhite:0 alpha:0.3].CGColor); 

Vamos a llenar el segmento (con un degradado) y acariciarlo (para dibujar el contorno negro). Queremos una sola sombra para ambas operaciones. Le decimos a Core Graphics que al comenzar una capa de transparencia:

  CGContextBeginTransparencyLayer(gc, 0); { 

CGContextEndTransparencyLayer izquierdo al final de esa línea porque me gusta tener un nivel extra de sangría para recordarme que llame a CGContextEndTransparencyLayer más tarde.

Como vamos a cambiar la región del clip del contexto para el llenado, pero no querremos recortar cuando trazamos el contorno más tarde, tenemos que guardar el estado de los gráficos:

  CGContextSaveGState(gc); { 

Puse un corsé izquierdo al final de esa línea porque me gusta tener un nivel extra de sangría para recordarme que llame a CGContextRestoreGState más tarde.

Para llenar el camino con un degradado, debemos crear un objeto degradado:

  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); 

También necesitamos descubrir un punto de inicio y un punto final para el gradiente. Usaremos el cuadro delimitador de ruta:

  CGRect bbox = CGContextGetPathBoundingBox(gc); CGPoint start = bbox.origin; CGPoint end = CGPointMake(CGRectGetMaxX(bbox), CGRectGetMaxY(bbox)); 

y forzaremos que el degradado se dibuje horizontal o verticalmente, lo que sea más largo:

  if (bbox.size.width > bbox.size.height) { end.y = start.y; } else { end.x = start.x; } 

Ahora finalmente tenemos todo lo que necesitamos para dibujar el degradado. Primero, clip a la ruta:

  CGContextClip(gc); 

Luego dibujamos el degradado:

  CGContextDrawLinearGradient(gc, gradient, start, end, 0); 

Luego podemos liberar el degradado y restaurar el estado de gráficos guardados:

  CGGradientRelease(gradient); } CGContextRestoreGState(gc); 

Cuando llamamos a CGContextClip , Core Graphics restablece la ruta del contexto. La ruta no es parte del estado de gráficos guardado; es por eso que hicimos una copia antes. Ahora es el momento de usar esa copia para establecer la ruta en el contexto nuevamente:

  CGContextAddPath(gc, path); CGPathRelease(path); 

Ahora podemos trazar el camino, dibujar el contorno negro del segmento:

  CGContextSetLineWidth(gc, kLineWidth); CGContextSetLineJoin(gc, kCGLineJoinMiter); [[UIColor blackColor] setStroke]; CGContextStrokePath(gc); 

A continuación, le diremos a Core Graphics que finalice la capa de transparencia. Esto hará que mire lo que hemos dibujado y agregue la sombra debajo:

  } CGContextEndTransparencyLayer(gc); 

Ahora hemos terminado de dibujar. Le pedimos a UIKit que cree un UIImage desde el contexto de la imagen, luego destruya el contexto y devuelva la imagen:

  UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; } 

Puedes encontrar el código todos juntos en esta esencia .

Esta es una versión de Swift 3 de la respuesta de Rob Mayoff. ¡Vea cuánto más eficiente es este lenguaje! Este podría ser el contenido de un archivo 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.restreGState() 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) } } 

Llámalo desde un viewController como este:

 let vi = MView(frame: self.view.bounds) self.view.addSubview(vi) 

Para hacer las conversiones de grados a radianes, he creado el operador ° postfix. Entonces ahora puedes usar, por ejemplo, 45 ° y esto hace la conversión de 45 grados a radianes. Este ejemplo es para Ints, amplíe estos también para los tipos de Float si tiene la necesidad:

 postfix operator ° protocol IntegerInitializable: ExpressibleByIntegerLiteral { init (_: Int) } extension Int: IntegerInitializable { postfix public static func °(lhs: Int) -> CGFloat { return CGFloat(lhs) * .pi / 180 } } 

Coloque este código en un archivo rápido de utilidades.