I just saw this image and it's interesting to me, how to create such type of animation in Swift:
So, I have many gray teeth in circle and when I set the angle, for example 45degree it will fill these gray teeth into blue within 0..45 degree.
You can just explain me the right way of doing it or you can show different snippets(it would be great). And later I will search or read about it.
Thanks in advance!
If you only need the individual 'teeth' to change color, instead of using the teeth as masks for a solid fill, you can use Core Graphics instead of Core Animation (although Core Animation is generally preferred). So in order to do this, we should be doing the following:
Subclass UIView to insert our drawing code
Create an array of path objects, wrapped in UIBezierPath
Setup a timer to update a progress value and setNeedsDisplay
In drawRect:, draw the paths and assign a fill to each depending on the progress
First of all, lets define the variables we're going to be working with in this UIView subclass.
class TeethLoaderView : UIView {
let numberOfTeeth = UInt(60) // Number of teeth to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
var progress = NSTimeInterval(0.0) // The progress of the loader
var paths = [UIBezierPath]() // The array containing the UIBezier paths
var displayLink = CADisplayLink() // The display link to update the progress
var teethHighlighted = UInt(0) // Number of teeth highlighted
...
Now let's add a function to create our paths.
func getPaths(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> [UIBezierPath] {
let halfHeight = size.height*0.5;
let halfWidth = size.width*0.5;
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil);
var pathArray = [UIBezierPath]()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight);
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
let pathCopy = CGPathCreateCopyByTransformingPath(p, &rotate)!
pathArray.append(UIBezierPath(CGPath: pathCopy)) // Populate the array
}
return pathArray
}
This is fairly simple. We just create a path for a single 'tooth' and then copy this path for how many teeth we need, translating and rotating the path for each one.
Next we want to setup our view. I'm going to a CADisplayLink for the timer so that the animation performs at the same speed on all devices.
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
private func commonSetup() {
self.backgroundColor = UIColor.whiteColor()
paths = getPaths(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire));
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
Here we just set the background color, as well as setup our timer and initialise the paths we're going to be using. Next we want to setup a function to change the progress of the view when the CADisplayLink fires.
func displayLinkDidFire() {
progress += displayLink.duration/animationDuration
if (progress > 1) {
progress -= 1
}
let t = teethHighlighted
teethHighlighted = UInt(round(progress*NSTimeInterval(numberOfTeeth))) // Calculate the number of teeth to highlight
if (t != teethHighlighted) { // Only call setNeedsDisplay if the teethHighlighted changed
setNeedsDisplay()
}
}
Nothing complicated here, we just update the progress and teethHighlighted and call setNeedsDisplay() to redraw the view, if teethHighlighted changed.
Finally, we want to draw the view.
override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
CGContextScaleCTM(ctx, -1, -1) // Flip the context to the correct orientation
CGContextTranslateCTM(ctx, -rect.size.width, -rect.size.height)
for (index, path) in paths.enumerate() { // Draw each 'tooth'
CGContextAddPath(ctx, path.CGPath);
let fillColor = (UInt(index) <= teethHighlighted) ? highlightColor:inactiveColor;
CGContextSetFillColorWithColor(ctx, fillColor.CGColor)
CGContextFillPath(ctx)
}
}
If you wanted to go down the Core Animation path, I adapted this code into a Core Animation layer
Final Result
Full project: https://github.com/hamishknight/Circle-Loader
Well, in the spirit of "go big or go home" (and because I'm actually having some fun doing this), I created a Core Animation version of my Core Graphics answer. It's quite a bit less code and animates smoother, so I'd actually prefer to use this.
First off, let's subclass a UIView again (this isn't strictly necessary, but it's nice to contain everything in a single view) and define our variables:
class TeethLoaderViewCA : UIView {
let numberOfTeeth = UInt(60) // Number of teetch to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
let shapeLayer = CAShapeLayer() // The teeth shape layer
let drawLayer = CAShapeLayer() // The arc fill layer
let anim = CABasicAnimation(keyPath: "strokeEnd") // The stroke animation
...
This is mostly the same as the Core Graphics version, but with a couple of Core Animation objects and without the timing logic. Next, we can pretty much copy the getPaths function we created in the other version, except with a few tweaks.
func getPathMask(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> CGPathRef? {
let halfHeight = size.height*0.5
let halfWidth = size.width*0.5
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil)
let returnPath = CGPathCreateMutable()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight)
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
CGPathAddPath(returnPath, &rotate, p)
}
return CGPathCreateCopy(returnPath)
}
This time, all the paths are grouped into one big path and the function returns that path.
Finally, we just have to create our layer objects & setup the animation.
private func commonSetup() {
// set your background color
self.backgroundColor = UIColor.whiteColor()
// Get the group of paths we created.
shapeLayer.path = getPathMask(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
let halfWidth = frame.size.width*0.5
let halfHeight = frame.size.height*0.5
let halfDeltaAngle = CGFloat(M_PI/Double(numberOfTeeth))
// Creates an arc path, with a given offset to allow it to be presented nicely
drawLayer.path = UIBezierPath(arcCenter: CGPointMake(halfWidth, halfHeight), radius: halfWidth, startAngle: CGFloat(-M_PI_2)-halfDeltaAngle, endAngle: CGFloat(M_PI*1.5)+halfDeltaAngle, clockwise: true).CGPath
drawLayer.frame = frame
drawLayer.fillColor = inactiveColor.CGColor
drawLayer.strokeColor = highlightColor.CGColor
drawLayer.strokeEnd = 0
drawLayer.lineWidth = halfWidth
drawLayer.mask = shapeLayer
layer.addSublayer(drawLayer)
// Optional, but looks nice
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
}
All we're doing here is assigning the group of paths to a CAShapeLayer, which we will use as a mask over the drawLayer, which we will be animating around the view (using a stroke on an arch path).
Final Result
Full project: https://github.com/hamishknight/Circle-Loader
Related
I'm using this approach to cut out a rounded rect "window" from a background view:
override func draw(_ rect: CGRect) {
guard let rectsArray = rectsArray else {
return
}
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
if let context = UIGraphicsGetCurrentContext() {
let roundedWindow = UIBezierPath(roundedRect: holeRect, cornerRadius: 15.0)
if holeRectIntersection.intersects(rect) {
context.addPath(roundedWindow.cgPath)
context.clip()
context.clear(holeRectIntersection)
context.setFillColor(UIColor.clear.cgColor)
context.fill(holeRectIntersection)
}
}
}
}
In layoutSubviews() I update the background colour add my "window frame" rect:
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = self.baseMoodColour
isOpaque = false
self.rectsArray?.removeAll()
self.rectsArray = [dragAreaView.frame]
}
I'm adding the rect here because layoutSubviews() updates the size of the "window frame" (i.e., the rect changes after layoutSubviews() runs).
The basic mechanism works as expected, however, if I change the background colour, the cutout window fills with black. So I'm wondering how I can animate a background colour change with this kind of setup? That is, I want to animate the colour of the area outside the cutout window (the window remains clear).
I've tried updating backgroundColor directly, and also using didSet in the accessor of a custom colour variable in my UIView subclass, but both cause the same filling-in of the "window".
var baseMoodColour: UIColor {
didSet {
self.backgroundColor = baseMoodColour
self.setNeedsDisplay()
}
}
Try to use UIView.animate, you can check it here
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveEaseOut], animations: {
self.backgroundColor = someNewColour
//Generally
//myView.backgroundColor = someNewColor
}, nil)
The problem in the short run is that that is simply what clear does if the background color is opaque. Just give your background color some transparency — even a tiny bit of transparency, so tiny that the human eye cannot perceive it — and now clear will cut a hole in the view.
For example, your code works fine if you set the view's background color to UIColor.green.withAlphaComponent(0.99).
By the way, you should delete the lines about UIColor.clear; that's a red herring. You should also cut the lines about the backgroundColor; you should not be repainting the background color into your context. They are two different things.
The problem in the long run is that what you're doing is not how to punch a hole in a view. You should be using a mask instead. That's the only way you're going to get the animation while maintaining the hole.
Answering my own question, based on #matt's suggestion (and linked example), I did it with a CAShapeLayer. There was an extra "hitch" in my requirements, since I have a couple of views on top of the one I needed to mask out. So, I did the masking like this:
func cutOutWindow() {
// maskedBackgroundView is an additional view, inserted ONLY for the mask
let r = self.maskedBackgroundView.bounds
// Adjust frame for dragAreaView's border
var dragSize = self.dragAreaView.frame.size
var dragPosition = self.dragAreaView.frame.origin
dragSize.width -= 6.0
dragSize.height -= 6.0
dragPosition.x += 3.0
dragPosition.y += 3.0
let r2 = CGRect(x: dragPosition.x, y: dragPosition.y, width: dragSize.width, height: dragSize.height)
let roundedWindow = UIBezierPath(roundedRect: r2, cornerRadius: 15.0)
let mask = CAShapeLayer()
let path = CGMutablePath()
path.addPath(roundedWindow.cgPath)
path.addRect(r)
mask.path = path
mask.fillRule = kCAFillRuleEvenOdd
self.maskedBackgroundView.layer.mask = mask
}
Then I had to apply the colour change to maskedBackgroundView.layer.backgroundColor (i.e., to the layer, not the view). With that in place, I get the cutout I need, with animatable colour changes. Thanks #matt for pointing me in the right direction.
I'm creating a subclass of AVPlayerController, that observes the player and handles player's states. If AVPlayerItemStatus is .failed, I add a custom UIView over the player's frame. At present, the important parts of my custom view's code looks like this:
class NoiseView: UIView {
...
var timer: Timer?
func animate() {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
func stopAnimating() {
timer?.invalidate()
}
#objc func timerAction() {
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!
let h = rect.height
let w = rect.width
var val: CGFloat!
var color: UIColor!
for i in 0...Int(w-1) {
for j in 0...Int(h-1) {
val = .random
color = UIColor(red: val, green: val, blue: val, alpha: 1.0)
context.setFillColor(color.cgColor)
context.fill(CGRect(x: i, y: j, width: 1, height: 1))
}
}
context.flush()
}
}
I'm calling the method animate() from other ViewController that keeps the NoiseView object.
The thing is, it's working as it's supposed to work (view is animating and creating a white noise) but the app starts to work slowly. What should be a proper approach to redraw the view without causing such a performance drop?
By the way, the drop happened with 0.1s interval (10 FPS). What I want to accomplish is having a constant white noise with at least 30 FPS to look like a legit tv noise.
There are a number of strategies you can try to optimize the drawing code, but to me the most obvious one is that you should pre-calculate the CGRects you need outside the drawing code.
The loops you're running require w^h iterations for each frame of animation to calculate all the CGRects you need. That's a lot, and totally unnecessary, because the CGRects will always be the same so long as the width and height are unchanged. Instead, you should do something like this:
var cachedRects = [CGRect]()
override var frame: CGRect {
didSet {
calculateRects()
}
}
func calculateRects() {
cachedRects = []
let w = frame.width
let h = frame.height
for i in 0...Int(w-1) {
for j in 0...Int(h-1) {
let rect = CGRect(x: i, y: j, width: 1, height: 1)
cachedRects += [rect]
}
}
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let context = UIGraphicsGetCurrentContext()!
var val: CGFloat!
var color: UIColor!
for rect in cachedRects {
val = .random
color = UIColor(red: val, green: val, blue: val, alpha: 1.0)
context.setFillColor(color.cgColor)
context.fill(rect)
}
context.flush()
}
By pre-caching the rects, you only have to do (w * h) number of iterations, which is a vast improvement.
If that isn't enough to improve performance, you can further optimize using similar pre-caching strategies, like instead of randomizing the each pixel, pre-calculate tiles of random colors outside the draw code, and then randomly assemble them in drawRect(). If the randomizer is what's the performance problem, this would cut down on the amount of work it would have to.
The key strategy is to try and minimize the amount of work your drawRect() method has to do, because it run on each frame of animation.
Good luck!
Instead of using the timer and calling setNeedsDisplay which in result will keep calling draw method, hence slowing the app.
Use CAAnimation and create CALayer or CAreplicator layer and animate that.
If you need code you can check this link Color Picker or I can post some demo code after sometime, Its fifa time :-p.
CAReplicatorlayer CALayer
Custom draw(rect:) methods can cause performance hits.
I suggest looking into adding a CAAnimation or CAAnimationGroup to your white noise view.
CABasicAnimation Documentation
CAAnimationGroup StackOverflow Post
Core-Animation treats angles as described in this image:
(image from http://btk.tillnagel.com/tutorials/rotation-translation-matrix.html)
EDIT: Adding an animated gif to explain better what I'm needing:
I need to animate a slice to grow wider, starting at 300:315 degrees, and ending 300:060.
To create each slice I'm using this function:
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
func createSlice(angle1:CGFloat, angle2:CGFloat) -> UIBezierPath! {
let path: UIBezierPath = UIBezierPath()
let width: CGFloat = self.frame.size.width/2
let height: CGFloat = self.frame.size.height/2
let centerToOrigin: CGFloat = sqrt((height)*(height)+(width)*(width));
let ctr: CGPoint = CGPoint(x: width, y: height)
path.move(to: ctr)
path.addArc( withCenter: ctr,
radius: centerToOrigin,
startAngle: CGFloat(angle1).toRadians(),
endAngle: CGFloat(angle2).toRadians(),
clockwise: true
)
path.close()
return path
}
I can now create the two slices and a sublayer with the smaller one, but I can't find how to proceed from this point:
func doStuff() {
path1 = self.createSlice(angle1: 300,angle2: 315)
path2 = self.createSlice(angle1: 300,angle2: 60)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path1.cgPath
shapeLayer.fillColor = UIColor.cyan.cgColor
self.layer.addSublayer(shapeLayer)
I would highly appreciate any help here!
Only a single color
If you want to animate the angle of a solid color filled pie segment like the one in your question, then you can do it by animating the strokeEnd of a CAShapeLayer.
The "trick" here is to make a very wide line. More specifically, you can create a path that is just an arc (the dashed line in the animation below) at half of the intended radius and then giving it the full radius as its line width. When you animate stroking that line it looks like the orange segment below:
Depending on your use case, you can either:
create a path from one angle to the other angle and animate stroke end from 0 to 1
create a path for a full circle, set stroke start and stroke end to some fraction of the circle, and animate stroke end from the start fraction to the end fraction.
If your drawing is just a single color like this, then this will be the smallest solution to your problem.
However, if your drawing is more complex (e.g. also stroking the pie segment) then this solutions simply won't work and you'll have to do something more complex.
Custom drawing / Custom animations
If your drawing of the pie segment is any more complex, then you'll quickly find yourself having to create a layer subclass with custom animatable properties. Doing so is a bit more code - some of which might look a bit unusual1 - but not as scary as it might sound.
This might be one of those things that is still more convenient to do in Objective-C.
Dynamic properties
First, create a layer subclass with the properties you're going to need. In Objective-C parlance these properties should be #dynamic, i.e. not synthesized. This isn't the same as dynamic in Swift. Instead we have to use #NSManaged.
class PieSegmentLayer : CALayer {
#NSManaged var startAngle, endAngle, strokeWidth: CGFloat
#NSManaged var fillColor, strokeColor: UIColor?
// More to come here ...
}
This allows Core Animation to handle these properties dynamically allowing it to track changes and integrate them into the animation system.
Note: a good rule of thumb is that these properties should all be related to drawing / visual presentation of the layer. If they aren't then it's quite likely that they don't belong on the layer. Instead they could be added to a view that in turn uses the layer for its drawing.
Copying layers
During the custom animation, Core Animation is going to want to create and render different layer configurations for different frames. Unlike most of Apple's other frameworks, this happens using the copy constructor init(layer:). For the above five properties to be copied along, we need to override init(layer:) and copy over their values.
In Swift we also have to override the plain init() and init?(coder).
override init(layer: Any) {
super.init(layer: layer)
guard let other = layer as? PieSegmentLayer else { return }
fillColor = other.fillColor
strokeColor = other.strokeColor
startAngle = other.startAngle
endAngle = other.endAngle
strokeWidth = other.strokeWidth
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
return nil
}
Reacting to change
Core Animation is in many ways built for performance. One of the ways it achieves this is by avoiding unnecessary work. By default, a layer won't redraw itself when a property changes. But these properties is used for drawing, and we want the layer to redraw when any of them changes. To do that, we need to override needsDisplay(forKey:) and return true if the key was one of these properties.
override class func needsDisplay(forKey key: String) -> Bool {
switch key {
case #keyPath(startAngle), #keyPath(endAngle),
#keyPath(strokeWidth),
#keyPath(fillColor), #keyPath(strokeColor):
return true
default:
return super.needsDisplay(forKey: key)
}
}
Additionally, If we want the layers default implicit animations for these properties, we need to override action(forKey:) to return a partially configured animation object. If we only want some properties (e.g. the angles) to implicitly animate, then we only need to return an animation for those properties. Unless we need something very custom, it's good to just return a basic animation with the fromValue set to the current presentation value:
override func action(forKey key: String) -> CAAction? {
switch key {
case #keyPath(startAngle), #keyPath(endAngle):
let anim = CABasicAnimation(keyPath: key)
anim.fromValue = presentation()?.value(forKeyPath: key)
return anim
default:
return super.action(forKey: key)
}
}
Drawing
The last piece of a custom animation is the custom drawing. This is done by overriding draw(in:) and using the supplied context to draw the layer:
override func draw(in ctx: CGContext) {
let center = CGPoint(x: bounds.midX, y: bounds.midY)
// subtract half the stroke width to avoid clipping the stroke
let radius = min(center.x, center.y) - strokeWidth / 2
// The two angle properties are in degrees but CG wants them in radians.
let start = startAngle * .pi / 180
let end = endAngle * .pi / 180
ctx.beginPath()
ctx.move(to: center)
ctx.addLine(to: CGPoint(x: center.x + radius * cos(start),
y: center.y + radius * sin(start)))
ctx.addArc(center: center, radius: radius,
startAngle: start, endAngle: end,
clockwise: start > end)
ctx.closePath()
// Configure the graphics context
if let fillCGColor = fillColor?.cgColor {
ctx.setFillColor(fillCGColor)
}
if let strokeCGColor = strokeColor?.cgColor {
ctx.setStrokeColor(strokeCGColor)
}
ctx.setLineWidth(strokeWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
// Draw
ctx.drawPath(using: .fillStroke)
}
Here I've filled and stroked a pie segment that extends from the center of the layer to the nearest edge. You should replace this with your custom drawing.
A custom animation in action
With all that code in place, we now have a custom layer subclass whose properties can be animated both implicitly (just by changing them) and explicitly (by adding a CAAnimation for their key). The results looks something like this:
Final words
It might not be obvious with the frame rate of those animations but one strong benefit from leveraging Core Animation (in different ways) in both these solutions is that it decouples the drawing of a single state from the timing of an animations.
That means that the layer doesn't know and doesn't have to know about the duration, delays, timing curves, etc. These can all be configured and controlled externally.
So at last I have found a solution. It took me time to understand that there is indeed no way to animate the fill of the shape, but we can trick CA engine by creating a filled circle by making the stroke (i.e. the border of the arc) extremely wide, so that it fills the whole circle!
extension CGFloat {
func toRadians() -> CGFloat {
return self * CGFloat(Double.pi) / 180.0
}
}
import UIKit
class SliceView: UIView {
let circleLayer = CAShapeLayer()
var fromAngle:CGFloat = 30
var toAngle:CGFloat = 150
var color:UIColor = UIColor.magenta
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init(frame:CGRect, fromAngle:CGFloat, toAngle:CGFloat, color:UIColor) {
self.init(frame:frame)
self.fromAngle = fromAngle
self.toAngle = toAngle
self.color = color
}
func setup() {
circleLayer.strokeColor = color.cgColor
circleLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(circleLayer)
layer.backgroundColor = UIColor.brown.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
let startAngle:CGFloat = (fromAngle-90).toRadians()
let endAngle:CGFloat = (toAngle-90).toRadians()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 4
let path = UIBezierPath(arcCenter: CGPoint(x: 0,y :0), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleLayer.position = center
circleLayer.lineWidth = radius*2
circleLayer.path = path.cgPath
}
public func animate() {
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 3.0;
pathAnimation.fromValue = 0.0;
pathAnimation.toValue = 1.0;
circleLayer.add(pathAnimation, forKey: "strokeEndAnimation")
}
}
So, now we can add it into our view controller and run the animation. In my case - I'm bridging it into Objecive-C but you can easily adapt it to swift.
I simply can't believe that in 2017 it was still not possible to find a ready solution for this simple task. It took me days to have that done. I really hope it will help others!
Here is how I'm using my class:
#implementation ViewController
{
SliceView *sv_;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.grayColor;
CGFloat width = 240.0;
CGFloat height = 160.0;
CGRect r = CGRectMake(
self.view.frame.size.width/2 - width/2,
self.view.frame.size.height/2 - height/2,
width, height);
sv_ = [[SliceView alloc] initWithFrame:r fromAngle:150 toAngle:30 color:[UIColor yellowColor] ];
[self.view addSubview:sv_];
}
- (IBAction)pressedGo:(id)sender {
[sv_ animate];
}
I'm adding a slight improvement for David's class. (David - you are welcome to copy into your book-quality answer!)
You can add the following init function:
convenience init(frame:CGRect, startAngle:CGFloat, endAngle:CGFloat, fillColor:UIColor,
strokeColor:UIColor, strokeWidth:CGFloat) {
self.init()
self.frame = frame
self.startAngle = startAngle
self.endAngle = endAngle
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
}
and then call it like this (Objective-C in my case):
PieSegmentLayer *sliceLayer = [[PieSegmentLayer alloc] initWithFrame:r startAngle:30 endAngle:180 fillColor:[UIColor cyanColor] strokeColor:[UIColor redColor] strokeWidth:4];
[self.view.layer addSublayer:sliceLayer];
I'm trying to draw a series of vertical lines inside of an arc but I'm having trouble being able to do this. I'm trying to do this using CAShapeLayers The end result is something that looks like this.
I know how to draw the curved arc and the line segments using CAShapeLayers but what I can't seem to figure out is how to draw the vertical lines inside the CAShapeLayer
My initial approach is to subclass CAShapeLayer and in the subclass, attempt to draw the vertical lines. However, I'm not getting the desired results. Here is my code for adding a line to a bezier point and attempting to add the sub layers.
class CustomLayer : CAShapeLayer {
override init() {
super.init()
}
func drawDividerLayer(){
print("Init has been called in custom layer")
print("The bounds of the custom layer is: \(bounds)")
print("The frame of the custom layer is: \(frame)")
let bezierPath = UIBezierPath()
let dividerShapeLayer = CAShapeLayer()
dividerShapeLayer.strokeColor = UIColor.redColor().CGColor
dividerShapeLayer.lineWidth = 1
let startPoint = CGPointMake(5, 0)
let endPoint = CGPointMake(5, 8)
let convertedStart = convertPoint(startPoint, toLayer: dividerShapeLayer)
let convertedEndPoint = convertPoint(endPoint, toLayer: dividerShapeLayer)
bezierPath.moveToPoint(convertedStart)
bezierPath.addLineToPoint(convertedEndPoint)
dividerShapeLayer.path = bezierPath.CGPath
addSublayer(dividerShapeLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class DrawView : UIView {
var customDrawLayer : CAShapeLayer!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//drawLayers()
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
}
func drawLayers() {
let bezierPath = UIBezierPath()
let startPoint = CGPointMake(5, 35)
let endPoint = CGPointMake(100, 35)
bezierPath.moveToPoint(startPoint)
bezierPath.addLineToPoint(endPoint)
let customLayer = CustomLayer()
customLayer.frame = CGPathGetBoundingBox(bezierPath.CGPath)
customLayer.drawDividerLayer()
customLayer.strokeColor = UIColor.blackColor().CGColor
customLayer.opacity = 0.5
customLayer.lineWidth = 8
customLayer.fillColor = UIColor.clearColor().CGColor
layer.addSublayer(customLayer)
customLayer.path = bezierPath.CGPath
}
However this code produces this image:
It definitely seems that I have a coordinate space problem/bounds/frame issue but I'm not quite sure. The way I want this to work is to draw from the top of the superLayer to the bottom of the superLayer inside of the CustomLayer class. But not only that, this must work using the bezier path addArcWithCenter: method which I haven't gotten to yet because I'm trying to solve this problem first. Any help would be appreciated.
The easiest way to draw an arc that consists of lines is to use lineDashPattern:
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: false)
let arc = CAShapeLayer()
arc.path = path.CGPath
arc.lineWidth = 50
arc.lineDashPattern = [4,15]
arc.strokeColor = UIColor.lightGrayColor().CGColor
arc.fillColor = UIColor.clearColor().CGColor
view.layer.addSublayer(arc)
So this is a blue arc underneath the dashed arc shown above. Obviously, I enlarged it for the sake of visibility, but it illustrates the idea.
I have a spritekit application written in swift and I want to get the color on the pixel that my finger is touching.
I have seen multiple post regarding this and tried them all out but can't seam to get it to work for me. Accourding to other post it should be possible to get the color from a UIView and as a SKScene has a SKIView that inherits from UIView it should be possible to get the color from there.
So to make the question easy and understandable I have an example.
Create a new spritekit application and add a image to it.
In my case I created a png image 200x200 pixels with a lot of different colors in it.
This is the GameScene.swift file, it is the only file I have changes from the auto generated:
import SpriteKit
extension UIView {
func getColorFromPoint(point:CGPoint) -> SKColor {
var pixelData:[UInt8] = [0,0,0,0]
let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.toRaw())
let context = CGBitmapContextCreate(&pixelData, 1, 1, 8, 4, colorSpace, bitmapInfo)
CGContextTranslateCTM(context, -point.x, -point.y);
self.layer.renderInContext(context)
var red:CGFloat = CGFloat(pixelData[0])/CGFloat(255.0)
var green:CGFloat = CGFloat(pixelData[1])/CGFloat(255.0)
var blue:CGFloat = CGFloat(pixelData[2])/CGFloat(255.0)
var alpha:CGFloat = CGFloat(pixelData[3])/CGFloat(255.0)
var color:SKColor = SKColor(red: red, green: green, blue: blue, alpha: alpha)
return color
}
}
class GameScene: SKScene {
var myColorWheel:SKSpriteNode!
override func didMoveToView(view: SKView) {
let recognizerTap = UITapGestureRecognizer(target: self, action:Selector("handleTap:"))
view.addGestureRecognizer(recognizerTap)
myColorWheel = SKSpriteNode(imageNamed: "ColorWheel.png")
myColorWheel.anchorPoint = CGPoint(x: 0, y: 0)
myColorWheel.position = CGPoint(x: 200, y: 200)
self.addChild(myColorWheel)
}
func handleTap(recognizer : UITapGestureRecognizer)
{
let location : CGPoint = self.convertPointFromView(recognizer.locationInView(self.view))
if(myColorWheel.containsPoint(location))
{
let color = self.view?.getColorFromPoint(location)
println(color)
}
}
}
It don't matter where I press on the image on the display, the result is always:
Optional(UIDeviceRGBColorSpace 0 0 0 0)
Have you tried to take a snapshot first using:
- (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates
Then picking the colours from that view?
Not sure how the system renders the .layer in a SKView.
Hope that helps.
Cheers