How can I animate the center of a radial gradient as drawn in a custom CA Layer:
- (instancetype)init
{
self = [super init];
if (self) {
[self setNeedsDisplay];
}
return self;
}
- (void)drawInContext:(CGContextRef)ctx
{
size_t gradLocationsNum = 2;
CGFloat gradLocations[2] = {0.0f, 1.0f};
CGFloat gradColors[8] = {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.5f};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, gradColors, gradLocations, gradLocationsNum);
CGColorSpaceRelease(colorSpace);
CGPoint gradCenter= CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
float gradRadius = MIN(self.bounds.size.width , self.bounds.size.height) ;
CGContextDrawRadialGradient (ctx, gradient, gradCenter, 0, gradCenter, gradRadius, kCGGradientDrawsAfterEndLocation);
CGGradientRelease(gradient);
}
(from https://stackoverflow.com/a/26924839/668518)
Is there any way to move the center of a radial gradient drawn in this fashion?
You may generate a timer or rather a display link to trigger a refresh method on every frame.
When you start animating you should save the current point to some property (self.animationStartPoint) and save the target point to some property (self.animationEndPoint). Then save the 2 dates you want the animation between like self.animationStartDate = [NSDate date] and self.animationDate = [self.animationStartDate byAddingTimeInterval:animationDuration]. Now when timer or display link fires you need to only call [self setNeedsDisplay].
In draw method you now need to interpolate the two points. First we need to check where in animation time are we:
CGFloat scale = [[NSDate date] timeIntervalSince:self.animationStartDate] / [self.animationEndDate timeIntervalSince:self.animationStartDate];
While animating the scale should be between 0 and 1. If it is less than the animation is still waiting (if you use "after delay" procedure). If it is greater than the animation is over:
if(scale < 0.0) return;
else if(scale > 1.0) {
scale = 1.0;
// TODO: report animation completed, invalidate timer or display link
}
Then use the scale to find the current point:
CGPoint gradCenter = CGPointMake(self.animationStartPoint.x + (self.animationEndPoint.x - self.animationStartPoint.x)*scale, self.animationStartPoint.y + (self.animationEndPoint.y - self.animationStartPoint.y)*scale);
Related
I'm trying to apply an angle gradient to the dashes created with the code I've written inside a custom UIView class, as below. Although it needs tweaking, I'm happy with the results it produces so far.
Given the input parameters in the view initialisation (below), and a frame of 768 * 768 on an iPad Air2 in portrait mode, it produces the following gauge:
First gauge
What I'd like to do is to cause each of the dashes to step through a user-defined gradient, e.g. green to red, much like this (kludged in Photoshop):
Gauge with colours
I've searched high and low, and cannot find anything to achieve this. The only things that come close use different drawing methods, and I want to keep my drawing routine.
As far as I'm concerned, I should simply be able to call:
CGContextSetStrokeColorWithColor(myContext, [gradient color goes here])
inside the draw loop, and that's it, but I don't know how to create the relevant color array/gradient, and change the line drawing color according to an index into that array.
Any help would be much appreciated.
- (void)drawRect:(CGRect)rect {
myContext = UIGraphicsGetCurrentContext();
UIImage *gaugeImage = [self radials:300 andSteps:3 andLineWidth:10.0];
UIImageView *gaugeImageView = [[UIImageView alloc] initWithImage:gaugeImage];
[self addSubview:gaugeImageView];
}
-(UIImage *)radials:(NSInteger)degrees andSteps:(NSInteger)steps andLineWidth:(CGFloat)lineWidth{
UIGraphicsBeginImageContext(self.bounds.size);
myContext = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(myContext, lineWidth);
CGContextSetStrokeColorWithColor(myContext, [[UIColor blackColor] CGColor]);
CGPoint center = CGPointMake(self.bounds.origin.x+(self.bounds.size.width/2), self.bounds.origin.y+(self.bounds.size.height/2));
CGFloat r1 = center.x * 0.87f;
CGFloat r2 = center.x * 0.95f;
CGContextTranslateCTM(myContext, center.x, center.y);
CGContextBeginPath(myContext);
CGFloat offset = 0;
if(degrees < 360){
offset = (360-degrees) / 2;
}
for(int lp = offset + 0 ; lp < offset + degrees+1 ; lp+=steps){
CGFloat theta = lp * (2 * M_PI / 360);
CGContextMoveToPoint(myContext, 0, 0);
r1 = center.x * 0.87f;
if(lp % 10 == 0){
r1 = center.x * 0.81f;
}
CGContextMoveToPoint(myContext, sin(theta) * r1, cos(theta) * r1);
CGContextAddLineToPoint(myContext, sin(theta) * r2, cos(theta) * r2);
CGContextStrokePath(myContext);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
So, you want something like this:
First, a couple of gentle suggestions:
Don't add subviews inside drawRect:. What if drawRect: gets called a second time, if for example the view's size changes?
Here's what the View Programming Guide for iOS says about implementing drawRect::
The implementation of your drawRect: method should do exactly one thing: draw your content. This method is not the place to be updating your application’s data structures or performing any tasks not related to drawing. It should configure the drawing environment, draw your content, and exit as quickly as possible. And if your drawRect: method might be called frequently, you should do everything you can to optimize your drawing code and draw as little as possible each time the method is called.
If you need to add or remove subviews, you should do that when the view is initialized, or in layoutSubviews at the latest.
There's no need to draw into an image or use an image view at all. The whole point of drawRect: is to draw into the current graphics context, which UIKit has already set up to target the view's backing store.
Those suggestions aside, there is no support for angular gradients in Core Graphics. However, for your graphic, you can set the color for each tick mark separately and get a pretty good approximation, which is how I created the image above. Use +[UIColor colorWithHue:saturation:brightness:alpha:] to create your color, calculating the hue parameter based on the tick angle.
If you factor out the drawing code into a separate class, it's easy to use it to draw either directly to a view (in drawRect:), or to an image if you need to. Here's the interface:
#interface RainbowGaugeAppearance: NSObject
#property (nonatomic) CGFloat startDegrees;
#property (nonatomic) CGFloat endDegrees;
#property (nonatomic) CGFloat degreesPerMajorTick;
#property (nonatomic) int subdivisionsPerMajorTick;
#property (nonatomic) CGFloat tickThickness;
#property (nonatomic) CGFloat startHue;
#property (nonatomic) CGFloat endHue;
#property (nonatomic) CGFloat outerRadiusFraction;
#property (nonatomic) CGFloat minorInnerRadiusFraction;
#property (nonatomic) CGFloat majorInnerRadiusFraction;
- (instancetype _Nonnull)init;
- (void)drawInRect:(CGRect)rect;
#end
And the implementation:
#implementation RainbowGaugeAppearance
static CGFloat radiansForDegrees(CGFloat degrees) { return degrees * M_PI / 180; }
- (instancetype _Nonnull)init {
if (self = [super init]) {
_startDegrees = 120;
_endDegrees = _startDegrees + 300;
_degreesPerMajorTick = 30;
_subdivisionsPerMajorTick = 10;
_tickThickness = 4;
_outerRadiusFraction = 0.95;
_minorInnerRadiusFraction = 0.87;
_majorInnerRadiusFraction = 0.81;
_startHue = 1/ 3.0;
_endHue = 0;
}
return self;
}
- (void)drawInRect:(CGRect)rect {
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc); {
CGContextTranslateCTM(gc, CGRectGetMidX(rect), CGRectGetMidY(rect));
CGContextSetLineWidth(gc, self.tickThickness);
CGContextSetLineCap(gc, kCGLineCapButt);
CGFloat outerRadius = _outerRadiusFraction / 2 * rect.size.width;
CGFloat minorInnerRadius = _minorInnerRadiusFraction / 2 * rect.size.width;
CGFloat majorInnerRadius = _majorInnerRadiusFraction / 2 * rect.size.width;
CGFloat degreesPerTick = _degreesPerMajorTick / _subdivisionsPerMajorTick;
for (int i = 0; ; ++i) {
CGFloat degrees = _startDegrees + i * degreesPerTick;
if (degrees > _endDegrees) { break; }
CGFloat t = (degrees - _startDegrees) / (_endDegrees - _startDegrees);
CGFloat hue = _startHue + t * (_endHue - _startHue);
CGContextSetStrokeColorWithColor(gc, [UIColor colorWithHue:hue saturation:0.8 brightness:1 alpha:1].CGColor);
CGFloat sine = sin(radiansForDegrees(degrees));
CGFloat cosine = cos(radiansForDegrees(degrees));
CGFloat innerRadius = (i % _subdivisionsPerMajorTick == 0) ? majorInnerRadius : minorInnerRadius;
CGContextMoveToPoint(gc, outerRadius * cosine, outerRadius * sine);
CGContextAddLineToPoint(gc, innerRadius * cosine, innerRadius * sine);
CGContextStrokePath(gc);
}
} CGContextRestoreGState(gc);
}
#end
Using it to draw a view is then trivial:
#implementation RainbowGaugeView {
RainbowGaugeAppearance *_appearance;
}
- (RainbowGaugeAppearance *_Nonnull)appearance {
if (_appearance == nil) { _appearance = [[RainbowGaugeAppearance alloc] init]; }
return _appearance;
}
- (void)drawRect:(CGRect)rect {
[self.appearance drawInRect:self.bounds];
}
#end
As far as I'm concerned, I should simply be able to call CGContextSetStrokeColorWithColor
Reality, however, is not interested in "as far as you're concerned". You are describing an angle gradient. The reality is that there is no built-in Core Graphics facility for creating an angle gradient.
However, you can do it easily with a good library such as AngleGradientLayer. It is then a simple matter to draw the angle gradient and use your gauge drawing as a mask.
In that way, I got this — not kludged in Photoshop, but done entirely live, in iOS, using AngleGradientLayer, plus your radials:andSteps:andLineWidth: method just copied and pasted in and used to generate the mask:
Here's the only code I had to write. First, generating the angle gradient layer:
+ (Class)layerClass {
return [AngleGradientLayer class];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
AngleGradientLayer *l = (AngleGradientLayer *)self.layer;
l.colors = [NSArray arrayWithObjects:
(id)[UIColor colorWithRed:1 green:0 blue:0 alpha:1].CGColor,
(id)[UIColor colorWithRed:0 green:1 blue:0 alpha:1].CGColor,
nil];
l.startAngle = M_PI/2.0;
}
return self;
}
Second, the mask (this part is in Swift, but that's irrelevant):
let im = self.v.radials(300, andSteps: 3, andLineWidth: 10)
let iv = UIImageView(image:im)
self.v.mask = iv
To sum up my question beforehand: I'm trying to determine where on the slider I can place the image based upon knowing only the UISlider's duration, and having an array of times to loop through, placing the images accordingly.
I've been reading through the Apple Docs on UISlider, and it appears that there is no native way to add "Tick marks" on a UISlider based upon an array of floats. "Tick marks" meaning lines upon a slider, such as those used to place advertisements on scrubbers. Here is a visualization:
Now, I have an array full of floats; Floats in which I will use to drop the tick marks based upon the UISlider's value. The values of the floats in the array will be different every time. I would like to loop through the .value property of my UISlider, dropping the UIImages accordingly. The UIImage's are the tick marks that are just little png's assets I created. What I cannot figure out is the logic behind looping through the .value property of the UISlider and placing the UIImage in accordance with the UISlider's future position. The values of the floats in the array will be different every time, so I can't place them statically. Does anyone know where to start? I'm still a little new to Objective-C programming.
I know that it may be possible utilize retrieving the slider's beginning X coordinate on the screen, like so:
- (float)xPositionFromSliderValue:(UISlider *)aSlider;
{
float sliderRange = aSlider.frame.size.width - aSlider.currentThumbImage.size.width;
float sliderOrigin = aSlider.frame.origin.x + (aSlider.currentThumbImage.size.width / 2.0);
float sliderValueToPixels = (((aSlider.value-aSlider.minimumValue)/(aSlider.maximumValue-aSlider.minimumValue)) * sliderRange) + sliderOrigin);
return sliderValueToPixels;
}
Maybe I could add in a calculation in the for loop to place the image in accordance to that instead. I'm just not too sure where even to begin here...
The methods trackRectForBounds and thumbRectForBounds are provided for subclassing UISlider, but you can call them directly, and they will get your tick centers up front.
- (float)sliderThumbCenter:(UISlider *)slider forValue:(float)value{
CGRect trackRect = [slider trackRectForBounds:slider.bounds];
CGRect thumbRect = [slider thumbRectForBounds:slider.bounds trackRect:trackRect value:value];
CGFloat centerThumb = CGRectGetMidX(thumbRect);
return centerThumb;
}
And it might be easier to do a custom view to draw the track rather than Image views, then just put the slider on top of it and hide the track. Just make the slider frame equal to the TickView's bounds. Really I suppose a UISlider subclass would be better, but this works!
#interface TickView : UIView
#property UIColor *tickColor;
#property int tickCount;
#property CGFloat tickHeight;
#property (weak) UISlider *slider;
#property float *ticks;
-(void)setTicks:(float *)ticks count:(int)tickCount;
#end
#implementation TickView{
__weak UISlider *_slider;
}
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.tickColor = [UIColor grayColor];
self.backgroundColor = [UIColor clearColor];
self.tickCount = 7;
self.ticks = malloc(sizeof(float) * self.tickCount);
self.tickHeight = 10;
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, self.tickColor.CGColor);
CGContextBeginPath(context);
CGFloat centerY = rect.size.height / 2;
CGContextMoveToPoint(context, 0, centerY);
CGContextAddLineToPoint(context, rect.size.width, centerY);
CGFloat tickTop = centerY - self.tickHeight / 2;
CGFloat tickBottom = centerY + self.tickHeight / 2;
CGFloat tickX = 0;
if (self.slider) {
for (int i = 0; i < self.tickCount; i++) {
tickX = [self sliderThumbCenter:self.slider forValue:self.ticks[i]];
CGContextMoveToPoint(context, tickX, tickTop);
CGContextAddLineToPoint(context, tickX, tickBottom);
}
}
else{
CGFloat tickSpacing = rect.size.width / (self.tickCount - 1);
for (int i = 0; i < self.tickCount; i++) {
CGContextMoveToPoint(context, tickX, tickTop);
CGContextAddLineToPoint(context, tickX, tickBottom);
tickX += tickSpacing;
}
}
CGContextStrokePath(context);
}
-(void)setTicks:(float *)ticks count:(int)tickCount{
free(_ticks);
_ticks = malloc(sizeof(float) * tickCount);
memcpy(_ticks, ticks, sizeof(float) * tickCount);
_tickCount = tickCount;
[self setNeedsDisplay];
}
- (float)sliderThumbCenter:(UISlider *)slider forValue:(float)value{
CGRect trackRect = [slider trackRectForBounds:slider.bounds];
CGRect thumbRect = [slider thumbRectForBounds:slider.bounds trackRect:trackRect value:value];
CGFloat centerThumb = CGRectGetMidX(thumbRect);
return centerThumb;
}
-(void)setSlider:(UISlider *)slider{
_slider = slider;
}
-(UISlider *)slider{
return _slider;
}
-(void)dealloc{
free(_ticks);
}
#end
I think you will have trouble positioning the tick marks. However, if the parent view of your UISlider is "view", you add a subview like this:
[view addSubView:myTickView];
The position of the added subview is determined by its frame property, which is in the parent's view coordinate space.
To remove a view, you do this:
[myTickView removeFromSuperView];
You can also loop through your tick views and change there frames, but these changes will be animated, so the ticks will appear to slide if you do that, unless you turn animations off.
I am using the following core graphics code to draw a waveform of audio data as I am recording. I apologize for tons of code, but here it is (I found it here):
//
// WaveformView.m
//
// Created by Edward Majcher on 7/17/14.
//
#import "WaveformView.h"
//Gain applied to incoming samples
static CGFloat kGain = 10.;
//Number of samples displayed
static int kMaxWaveforms = 80.;
#interface WaveformView ()
#property (nonatomic) BOOL addToBuffer;
//Holds kMaxWaveforms number of incoming samples,
//80 is based on half the width of iPhone, adding a 1 pixel line between samples
#property (strong, nonatomic) NSMutableArray* bufferArray;
+ (float)RMS:(float *)buffer length:(int)bufferSize;
#end
#implementation WaveformView
- (void)awakeFromNib
{
[super awakeFromNib];
self.bufferArray = [NSMutableArray array];
}
-(void)updateBuffer:(float *)buffer withBufferSize:(UInt32)bufferSize
{
if (!self.addToBuffer) {
self.addToBuffer = YES;
return;
} else {
self.addToBuffer = NO;
}
float rms = [WaveformView RMS:buffer length:bufferSize];
if ([self.bufferArray count] == kMaxWaveforms) {
//##################################################
// [self.bufferArray removeObjectAtIndex:0];
}
[self.bufferArray addObject:#(rms * kGain)];
[self setNeedsDisplay];
}
+ (float)RMS:(float *)buffer length:(int)bufferSize {
float sum = 0.0;
for(int i = 0; i < bufferSize; i++) {
sum += buffer[i] * buffer[i];
}
return sqrtf( sum / bufferSize );
}
// *****************************************************
- (void)drawRect:(CGRect)rect
{
CGFloat midX = CGRectGetMidX(rect);
CGFloat maxX = CGRectGetMaxX(rect);
CGFloat midY = CGRectGetMidY(rect);
CGContextRef context = UIGraphicsGetCurrentContext();
// Draw out center line
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(context, 1.);
CGContextMoveToPoint(context, 0., midY);
CGContextAddLineToPoint(context, maxX, midY);
CGContextStrokePath(context);
CGFloat x = 0.;
for (NSNumber* n in self.bufferArray) {
CGFloat height = 20 * [n floatValue];
CGContextMoveToPoint(context, x, midY - height);
CGContextAddLineToPoint(context, x, midY + height);
CGContextStrokePath(context);
x += 2;
}
if ([self.bufferArray count] >= kMaxWaveforms) {
[self addMarkerInContext:context forX:midX forRect:rect];
} else {
[self addMarkerInContext:context forX:x forRect:rect];
}
}
- (void)addMarkerInContext:(CGContextRef)context forX:(CGFloat)x forRect:(CGRect)rect
{
CGFloat maxY = CGRectGetMaxY(rect);
CGContextSetStrokeColorWithColor(context, [UIColor greenColor].CGColor);
CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(x - 1.5, 0, 3, 3));
CGContextMoveToPoint(context, x, 0 + 3);
CGContextAddLineToPoint(context, x, maxY - 3);
CGContextStrokePath(context);
CGContextFillEllipseInRect(context, CGRectMake(x - 1.5, maxY - 3, 3, 3));
}
#end
So as I am recording audio, the waveform drawing gets more and more jittery, kind of like a game that has bad frame rates. I tried contacting the owner of this piece of code, but no luck. I have never used core graphics, so I'm trying to figure out why performance is so bad. Performance starts to degrade at around 2-3 seconds worth of audio (the waveform doesn't even fill the screen).
My first question is, is this redrawing the entire audio history every time drawRect is called? If you look in the drawRect function (marked by asterisks), there is a variable called CGRect x. This seems to affect the position at which the waveform is being drawn (if you set it to 60 instead of 0, it starts at x=60 pixels instead of x=0 pixels).
From my viewController, I pass in the audioData which gets stored in the self.bufferArray property. So when that loop goes through to draw the data, it seems like it's starting at zero and working its way up every time drawRect is getting called, which means that for every new piece of audio data added, drawRect gets called, and it redraws the entire waveform plus the new piece of audio data.
If that is the problem, does anyone know how I can optimize this piece of code? I tried emptying the bufferArray after the loop so that it contained only new data, but that didn't work.
If this is not the problem, are there any core graphics experts that can figure out what the problem is?
I should also mention that I commented out a piece of code (marked with ### at signs) because I need the entire waveform. I don't want it to remove pieces of the waveform at the beginning. The iOS Voice Memos app can hold a waveform of audio without performance degradation.
I am new to Core Graphics and trying to understand why text labels I draw in CGRect form an elliptical arc when images I draw using the same coordinates form a circular arc.
The original code by Arthur Knopper creates circles wherever the screen is touched. By removing the touches method, I have been able to generate a series of small circles (dots) along a circular arc (uber circle). Each dot is centred on the perimeter of the uber circle (as shown below).
In order to label each dot I use the same point coordinates I used for placing the dot. However text labels form an elliptical arc even though dots form a circular arc (as shown below). Labels are also hidden by the dots when dots are filled. The reason for this is a complete mystery.
As a novice I am probably missing something basic in Core Graphics. If anyone could explain what that is and what I need to do to make both arcs circular and place labels on top of the dots I’d be most grateful.
Thanks.
Here is the code.
circleView.h
NSMutableArray *totalCircles;
int dotCount, limit;
float uberX, uberY, uberRadius, uberAngle, labelX,
labelY,dotRadius, dotsFilled, sectors, x, y;
CGPoint dotPosition;
CGRect boxBoundary;
CGContextRef context;
}
- (void)demo;
#end
And ...
-#implementation iOSCircleView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
totalCircles = [[NSMutableArray alloc] init];
// Set background color
self.backgroundColor = [UIColor whiteColor];
}
return self;
} // frame a view for drawing
- (void)drawRect:(CGRect)rect {
[self demo];
}
- (void)demo {
context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.5);
uberX = 120;
uberY = 160;
uberRadius = 30;
sectors = 16;
uberAngle = (2.0 * PI) / sectors;
dotRadius = 20;
dotsFilled = FALSE;
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // make new point for each dot
dotPosition = CGPointMake(x,y); // create each dot
NSLog(#"Circle%i: %#", dotCount, NSStringFromCGPoint(dotPosition));
[self autoLabel]; // text hides behind the dots
newCircle.circleCentre = dotPosition; // place each dot on the frame
[totalCircles addObject:newCircle];
[self setNeedsDisplay];
}
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
dotCount = 1;
for (iOSCircle *circle in totalCircles) {
CGContextAddArc(context, circle.circleCentre.x, circle.circleCentre.y, circle.circleRadius, 0.0, M_PI * 2.0, YES); // draw the circles
NSLog(#"Dot %i Filled %i ", dotCount, dotsFilled);
switch (dotsFilled) {
case 1:
CGContextSetFillColorWithColor(context, [[UIColor cyanColor] CGColor]);
//CGContextDrawPath(context, kCGPathFillStroke);
break;
default:
//CGContextStrokePath(context); // draw dot outline
break;
}
dotCount++;
}
} // draw circular dots in circular patterns
- (void)setSectorDotCoordinates {
x = uberX + (uberRadius * cos(uberAngle *dotCount) * 2);
y = uberY + (uberRadius * sin(uberAngle *dotCount) * 2);
} // calculate dot coordinates along a circular arc
- (void)autoLabel {
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
boxBoundary = CGRectMake(x-dotRadius, y-dotRadius, x+dotRadius, y+dotRadius);
[[NSString stringWithFormat:#"%i",dotCount] drawInRect:boxBoundary withFont:[UIFont systemFontOfSize:24] lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
Change the boxBoundary in autoLabel, CGRectMake creates a rectangle with one point coordinates and width and height, not two points:
(void)autoLabel {
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
boxBoundary = CGRectMake(x-dotRadius, y-dotRadius, dotRadius*2, dotRadius*2);
[[NSString stringWithFormat:#"%i",dotCount] drawInRect:boxBoundary
withFont:[UIFont systemFontOfSize:24]
lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
In your code the "boxes" containing the texts where bigger and bigger when you where going to the right. (the width and height were not fixed)
My updated code show labels that match the drawing order but text is still hidden when dots are filled.
I suspect I need to construct a path to write text in front of the dots and it’s already apparent that something like CGPathMoveToPoint is needed to start drawing from the 12 O'clock position.
Here’s the updated code. The first part draws and renders the dots
context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.5);
uberX = 160;
uberY = 240;
uberRadius = 52;
sectors = 16;
uberAngle = ((2.0 * PI) / sectors);
NSLog(#"%f %f %f %f", uberX, uberY, uberRadius, uberAngle);
dotRadius = 20;
dotsFilled = FALSE;
textOffset = 4; // add to y to centre the label
for (dotCount = 1; dotCount <= 4 /*sectors*/; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // create a new point for each dot
dotPosition = CGPointMake(x,y); // create each dot
NSLog(#"Circle%i: %#", dotCount, NSStringFromCGPoint(dotPosition));
newCircle.circleCentre = dotPosition; // place each dot on the frame
[totalCircles addObject:newCircle];
[self setNeedsDisplay];
}
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
dotCount = 1;
for (iOSCircle *circle in totalCircles) {
CGContextAddArc(context, circle.circleCentre.x, circle.circleCentre.y, circle.circleRadius, 0.0, M_PI * 2.0, YES);
// draw the circles
NSLog(#"Dot %i Filled %i ", dotCount, dotsFilled);
switch (dotsFilled) {
case 1:
CGContextSetFillColorWithColor(context, [[UIColor cyanColor] CGColor]);
CGContextDrawPath(context, kCGPathFillStroke);
break;
default:
CGContextStrokePath(context); // draw dot outline
break;
}
[self setSectorDotCoordinates]; // find point coordinates for each dot
dotCount++;
}
The code that draws the labels follow immediately afterwards.
// draw labels
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self setSectorDotCoordinates]; // find point coordinates for each dot
dotPosition = CGPointMake(x,y); // use point coordinates for label
[self autoLabel]; // THIS SHOWS TEXT BEHIND THE DOTS
}
I have an UIImageView that I want to transform inside a callback (specifically: using OpenCV processImage). Inside the callback I calculate an angle at which it should be transformed and set the transform to that angle. It works only the first time, but won't update (I'm logging the angle and have verified that it changes).
However, If I attach a button to the same code I can apply the transform multiple times with no problems. I can also mix the two actions (applying rotate inside the processImage callback and applying rotate on button tap) but rotation only takes affect on the button tap.
Is there some kind of redraw method I'm missing to tell the UIImageView to redraw itself that gets called automatically on button tap?
This actionRotate works fine and applies the rotation every time I tap the button.
- (IBAction)actionRotate:(id)sender {
if (!self.angle) {
self.angle = 0.5f;
} else {
self.angle += 0.1f;
}
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / -500;
self.image.layer.transform = CATransform3DRotate(rotationAndPerspectiveTransform, self.angle, 0.0f, 1.0f, 0.0f);
self.image.layer.zPosition = 1000;
NSLog(#"actionRotate angle: %f", self.angle);
}
- (void)viewDidLoad {
[super viewDidLoad];
// init camera
self.videoCamera = [[CvVideoCamera alloc] initWithParentView:self.output];
self.videoCamera.delegate = self;
self.videoCamera.defaultAVCaptureDevicePosition = AVCaptureDevicePositionBack;
self.videoCamera.defaultAVCaptureSessionPreset = AVCaptureSessionPresetMedium;
self.videoCamera.defaultAVCaptureVideoOrientation = AVCaptureVideoOrientationPortrait;
// 1 fps to slow down log output for debugging
self.videoCamera.defaultFPS = 1;
self.output.frame = CGRectMake(0, 0, 360, 480);
[self.videoCamera start];
}
This processImage does not work. The angle gets incremented correctly, but the transform doesn't get applied. Even if I call [self actionRotate:nil] to be absolutely sure it's the same code running.
- (void)processImage:(Mat&)image;
{
if (!self.angle) {
self.angle = 0.5f;
} else {
self.angle += 0.1f;
}
CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
rotationAndPerspectiveTransform.m34 = 1.0 / -500;
self.image.layer.transform = CATransform3DRotate(rotationAndPerspectiveTransform, self.angle, 0.0f, 1.0f, 0.0f);
self.image.layer.zPosition = 1000;
NSLog(#"processImage angle: %f", self.angle);
}
I discovered that it wasn't running in the main queue, so had to change the code doing the transform in processImage
dispatch_async(dispatch_get_main_queue(), ^{
self.deviation.layer.transform = CATransform3DRotate(rotationAndPerspectiveTransform, self.angle, 0.0f, 1.0f, 0.0f);
self.deviation.layer.zPosition = 1000;
});