I have problem related with question
I've set contentsScale and after that text looking good but if I apply the 3d rotation transformation the text comes out blurry.
image here
initialization code
// init text
textLayer_ = [CATextLayer layer];
…
textLayer_.contentsScale = [[UIScreen mainScreen] scale];
// init body path
pathLayer_ = [CAShapeLayer layer];
…
[pathLayer_ addSublayer:textLayer_];
rotation code
// make the mirror
pathLayer_.transform = CATransform3DRotate(pathLayer_.transform, M_PI, 0, 1, 0);
textLayer_.transform = CATransform3DRotate(textLayer_.transform, M_PI, 0, 1, 0);
[textLayer_ setNeedsDisplay];
For test i've rotated text separately during the initialization.
// init text
textLayer_ = [CATextLayer layer];
…
textLayer_.transform = CATransform3DRotate(textLayer_.transform, M_PI, 0, 1, 0);
textLayer_.contentsScale = [[UIScreen mainScreen] scale];
Text can be rotated and remains clear
image here
Rasterizing
What's probably happening here is that it decides it has to render the textLayer to pixels. Note the warning for shouldRasterize in the CALayer Class Reference:
When the value of this property is NO, the layer is composited directly into the destination whenever possible. The layer may still be rasterized prior to compositing if certain features of the compositing model (such as the inclusion of filters) require it.
So, CATextLayer may suddenly decide to rasterize. It decides to rasterize if it's a sublayer of a rotated layer. So, don't make that happen.
Single-Sided Layers
That takes you back to your solution that causes the reversed text. You can prevent this by turning off doubleSided on the text layers. Your signs will now be blank on the far side, so add a second text layer, rotated 180 degrees relative to the first.
Declare two text layers:
#property (retain) CAShapeLayer *pathLayer;
#property (retain) CATextLayer *textLayerFront;
#property (retain) CATextLayer *textLayerBack;
Then, initialize them to be single-sided, with the back layer rotated 180 degrees:
CAShapeLayer *pathLayer = [CAShapeLayer layer];
// Also need to store a UIBezierPath in the pathLayer.
CATextLayer *textLayerFront = [CATextLayer layer];
textLayerFront.doubleSided = NO;
textLayerFront.string = #"Front";
textLayerFront.contentsScale = [[UIScreen mainScreen] scale];
CATextLayer *textLayerBack = [CATextLayer layer];
textLayerBack.doubleSided = NO;
// Eventually both sides will have the same text, but for demonstration purposes we will label them differently.
textLayerBack.string = #"Back";
// Rotate the back layer 180 degrees relative to the front layer.
textLayerBack.transform = CATransform3DRotate(textLayerBack.transform, M_PI, 0, 1, 0);
textLayerBack.contentsScale = [[UIScreen mainScreen] scale];
// Make all the layers siblings. These means they must all be rotated independently of each other.
// The layers can flicker if their Z position is close to the background, so move them forward.
// This will not work if the main layer has a perspective transform on it.
textLayerFront.zPosition = 256;
textLayerBack.zPosition = 256;
// It would make sense to make the text layers siblings of the path layer, but this seems to mean they get pre-rendered, blurring them.
[self.layer addSublayer:pathLayer];
[self.layer addSublayer:textLayerBack];
[self.layer addSublayer:textLayerFront];
// Store the layers constructed at this time for later use.
[self setTextLayerFront:textLayerFront];
[self setTextLayerBack:textLayerBack];
[self setPathLayer:pathLayer];
You can then rotate the layers. They will appear correct as long as you always rotate by the same amount.
CGFloat angle = M_PI;
self.pathLayer.transform = CATransform3DRotate(self.pathLayer.transform, angle, 0, 1, 0);
self.textLayerFront.transform = CATransform3DRotate(self.textLayerFront.transform, angle, 0, 1, 0);
self.textLayerBack.transform = CATransform3DRotate(self.textLayerBack.transform, angle, 0, 1, 0);
You should then find that you can rotate your sign to any angle while the text remains sharp.
Text to Path
There is an alternative, if you really need to manipulate your text display in ways that cause CATextLayer to rasterize: convert the text to a UIBezierPath representation. This can then be placed in a CAShapeLayer. Doing so requires delving deep into Core Text, but the results are powerful. For example, you can animate the text being drawn.
// - (UIBezierPath*) bezierPathWithString:(NSString*) string font:(UIFont*) font inRect:(CGRect) rect;
// Requires CoreText.framework
// This creates a graphical version of the input screen, line wrapped to the input rect.
// Core Text involves a whole hierarchy of objects, all requiring manual management.
- (UIBezierPath*) bezierPathWithString:(NSString*) string font:(UIFont*) font inRect:(CGRect) rect;
{
UIBezierPath *combinedGlyphsPath = nil;
CGMutablePathRef combinedGlyphsPathRef = CGPathCreateMutable();
if (combinedGlyphsPathRef)
{
// It would be easy to wrap the text into a different shape, including arbitrary bezier paths, if needed.
UIBezierPath *frameShape = [UIBezierPath bezierPathWithRect:rect];
// If the font name wasn't found while creating the font object, the result is a crash.
// Avoid this by falling back to the system font.
CTFontRef fontRef;
if ([font fontName])
fontRef = CTFontCreateWithName((__bridge CFStringRef) [font fontName], [font pointSize], NULL);
else if (font)
fontRef = CTFontCreateUIFontForLanguage(kCTFontUserFontType, [font pointSize], NULL);
else
fontRef = CTFontCreateUIFontForLanguage(kCTFontUserFontType, [UIFont systemFontSize], NULL);
if (fontRef)
{
CGPoint basePoint = CGPointMake(0, CTFontGetAscent(fontRef));
CFStringRef keys[] = { kCTFontAttributeName };
CFTypeRef values[] = { fontRef };
CFDictionaryRef attributesRef = CFDictionaryCreate(NULL, (const void **)&keys, (const void **)&values,
sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
if (attributesRef)
{
CFAttributedStringRef attributedStringRef = CFAttributedStringCreate(NULL, (__bridge CFStringRef) string, attributesRef);
if (attributedStringRef)
{
CTFramesetterRef frameSetterRef = CTFramesetterCreateWithAttributedString(attributedStringRef);
if (frameSetterRef)
{
CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetterRef, CFRangeMake(0,0), [frameShape CGPath], NULL);
if (frameRef)
{
CFArrayRef lines = CTFrameGetLines(frameRef);
CFIndex lineCount = CFArrayGetCount(lines);
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, lineCount), lineOrigins);
for (CFIndex lineIndex = 0; lineIndex<lineCount; lineIndex++)
{
CTLineRef lineRef = CFArrayGetValueAtIndex(lines, lineIndex);
CGPoint lineOrigin = lineOrigins[lineIndex];
CFArrayRef runs = CTLineGetGlyphRuns(lineRef);
CFIndex runCount = CFArrayGetCount(runs);
for (CFIndex runIndex = 0; runIndex<runCount; runIndex++)
{
CTRunRef runRef = CFArrayGetValueAtIndex(runs, runIndex);
CFIndex glyphCount = CTRunGetGlyphCount(runRef);
CGGlyph glyphs[glyphCount];
CGSize glyphAdvances[glyphCount];
CGPoint glyphPositions[glyphCount];
CFRange runRange = CFRangeMake(0, glyphCount);
CTRunGetGlyphs(runRef, CFRangeMake(0, glyphCount), glyphs);
CTRunGetPositions(runRef, runRange, glyphPositions);
CTFontGetAdvancesForGlyphs(fontRef, kCTFontDefaultOrientation, glyphs, glyphAdvances, glyphCount);
for (CFIndex glyphIndex = 0; glyphIndex<glyphCount; glyphIndex++)
{
CGGlyph glyph = glyphs[glyphIndex];
// For regular UIBezierPath drawing, we need to invert around the y axis.
CGAffineTransform glyphTransform = CGAffineTransformMakeTranslation(lineOrigin.x+glyphPositions[glyphIndex].x, rect.size.height-lineOrigin.y-glyphPositions[glyphIndex].y);
glyphTransform = CGAffineTransformScale(glyphTransform, 1, -1);
CGPathRef glyphPathRef = CTFontCreatePathForGlyph(fontRef, glyph, &glyphTransform);
if (glyphPathRef)
{
// Finally carry out the appending.
CGPathAddPath(combinedGlyphsPathRef, NULL, glyphPathRef);
CFRelease(glyphPathRef);
}
basePoint.x += glyphAdvances[glyphIndex].width;
basePoint.y += glyphAdvances[glyphIndex].height;
}
}
basePoint.x = 0;
basePoint.y += CTFontGetAscent(fontRef) + CTFontGetDescent(fontRef) + CTFontGetLeading(fontRef);
}
CFRelease(frameRef);
}
CFRelease(frameSetterRef);
}
CFRelease(attributedStringRef);
}
CFRelease(attributesRef);
}
CFRelease(fontRef);
}
// Casting a CGMutablePathRef to a CGPathRef seems to be the only way to convert what was just built into a UIBezierPath.
combinedGlyphsPath = [UIBezierPath bezierPathWithCGPath:(CGPathRef) combinedGlyphsPathRef];
CGPathRelease(combinedGlyphsPathRef);
}
return combinedGlyphsPath;
}
Here is rotating outlined text, created with the method above. It was also possible to add perspective without the z positions of the text layers becoming apparent.
This worked for me:
myTextLayer.contentsScale = UIScreen.mainScreen.scale;
The text will render crisp even when transformed.
Related
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 am decomposing a multiline string into word boundaries on iOS. My solution centers around the boundingRectForGlyphRange method of NSLayoutManager. It ALMOST works, except that the rect for each word is a few pixels off to the right. In other words NSLayoutManager seems to be adding a leading space / indent on each line and I cannot find any way to override this behavior.
I tried using NSLayoutManager.usesFontLeading as well as NSParagraphStyle.headIndent but without any results:
NSLayoutManager* layout = [NSLayoutManager new];
layout.usesFontLeading = NO;
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.headIndent = paragraphStyle.firstLineHeadIndent = 0;
NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:self attributes:#{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
layout.textStorage = textStorage;
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:size];
[layout addTextContainer:textContainer];
// compute bounding rect for each word range
for (NSValue* wordRangeValue in wordRanges)
{
NSRange wordRange = [wordRangeValue rangeValue];
NSRange wordGlyphRange = [layout glyphRangeForCharacterRange:wordRange actualCharacterRange:NULL];
CGRect wordBounds = [layout boundingRectForGlyphRange:wordGlyphRange inTextContainer:textContainer];
}
Screenshot: the gray rectangles represent label bounds, red rectangles represent text rect for each label and computed word boundaries from the [boundingRectForGlyphRange:] method above. Notice that the computed word boundaries are off by a few pixels.
I am also open to other methods for computing word boundaries, but boundingRectForGlyphRange seems very convenient for my purpose.
To ignore the left margin, use:
textContainer.lineFragmentPadding = 0;
I was not able to force NSLayoutManager to omit the left margin. If anyone knows how to get NSLayoutManager to ignore the left margin, let me know.
My workaround was to use Core Text instead. This was MUCH more difficult and involved. My solution does not lend itself to pasting into a single code excerpt, but this should give you a good reference if you want to go the same route:
- (NSArray*) coreTextLayoutsForCharacterRanges:(NSArray*)ranges withFont:(UIFont *)font constrainedToSize:(CGSize)size{
// initialization: make frame setter
NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; // watch out - need to specify line wrapping to use multi line layout!
paragraphStyle.minimumLineHeight = font.lineHeight; // watch out - custom fonts do not compute properly without this!
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:self attributes:#{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFRange wholeString = CFRangeMake(0, self.length);
CGRect bounds = CGRectMake(0, 0, size.width, size.height);
CGMutablePathRef boundsPath = CGPathCreateMutable();
CGPathAddRect(boundsPath, NULL, bounds);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, wholeString, boundsPath, NULL);
CFRelease(boundsPath);
// extract lines
CFArrayRef lines = CTFrameGetLines(frame);
int lineCount = CFArrayGetCount(lines);
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);
NSMutableArray* lineLayouts = [NSMutableArray arrayWithCapacity:lineCount];
CGFloat h = size.height;
for (int i = 0; i < lineCount; ++i){
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
CGPoint lineOrigin = lineOrigins[i]; // in Core Graphics coordinates! let's convert.
lineOrigin.y = h - lineOrigin.y;
TextLayout* lineLayout = [[CTLineLayout alloc] initWithString:self line:line lineOrigin:lineOrigin];
[lineLayouts addObject:lineLayout];
}
// got line layouts. now we iterate through the word ranges to find the appropriate line for each word and compute its layout using the corresponding CTLine.
Another important part is how to get the bounding rect of a word in a line by using CTLine. I factored this into a CTLineLayout module, but the gist is this (the 'origin' variable refers to the line origin computed in the code sample above):
CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat top = origin.y - ascent;
CGFloat bottom = origin.y + descent;
CGRect result = CGRectMake(origin.x, top, width, bottom - top); // frame of entire line (for now)
CGFloat left = CTLineGetOffsetForStringIndex(line, stringRange.location, NULL);
CGFloat right = CTLineGetOffsetForStringIndex(line, NSMaxRange(stringRange), NULL);
result.origin.x = left;
result.size.width = right - left; // frame of target word in UIKit coordinates
The above is a rough excerpt - I factored CTLine to compute the line's bounds once in the initializer, then compute only the left + right endpoints when getting the frame of a word.
Whew!
Hey i need a help with building an app. I build an art app, this app is working with interferencies. Thats the reason i need to draw many lines in this app. More lines are better for the interefernces. I think the problem is the iPad can't handle too many lines, because the speed or the performance is too slow.
I don't know how can i speed up my code for more performance on the iPad. Should i use Open GL or something else...
What can i do?
Here are the Draw.m
#import "Draw.h"
#implementation Draw
- (IBAction) sliderValueChanged:(UISlider *)sender {
label.text = [NSString stringWithFormat:#"%f", slider.value];
//NSLog(#"slider value = %f", sender.value);
[self setNeedsDisplay];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
//NSLog(#"slider value = %f", self.bounds.size.width);
CGMutablePathRef cgpath = CGPathCreateMutable();
CGPathMoveToPoint(cgpath, NULL, 0, 500);
CGMutablePathRef cgpath2 = CGPathCreateMutable();
CGPathMoveToPoint(cgpath2, NULL, 0, 500);
UIBezierPath *uipath = [[UIBezierPath alloc] init];
[uipath moveToPoint:CGPointMake(0, 0)];
int step = 5;
int iterations = self.bounds.size.width/step;
for (int i = 0; i < iterations+1; i++){
//CGPathAddCurveToPoint(cgpath, NULL, 1+i, 0, 1+i, 0, 1+i ,0);
CGPathAddLineToPoint ( cgpath, NULL, 0, 0 );
CGPathAddLineToPoint ( cgpath, NULL, 0, 768 );
CGPathAddLineToPoint ( cgpath, NULL, step*i-slider.value*2, 768 );
CGPathAddLineToPoint ( cgpath, NULL, step*i, 0 );
CGPathAddLineToPoint ( cgpath, NULL, (step*i)+step, 0 );
[[UIColor blackColor] setStroke];
CGContextAddPath(ctx, cgpath);
[self strokeUIBezierPath:uipath];
CGPathRelease(cgpath);
}
- (void)strokeContext:(CGContextRef)context
{
CGContextStrokePath(context);
}
- (void)strokeUIBezierPath:(UIBezierPath*)path
{
[path stroke];
}
#end
image http://img17.imageshack.us/img17/375/53178410200339197475308.jpg
the problem with bezier paths is, that they can be quite 'calculation heavy'.
You could either use straight lines ( CGContextAddLineToPoint(context, point.x, point.y);)
Or you use graphics acceleration.
You could either dive directly into OpenGL or use a game engine to help you with some of the code.
One of the most popular ones ( and as I think quite easy to use ) is cocos2d.
You should be getting better performance using a pattern to fill the screen. There is an entire section with sample code in the Quartz Programming Guide.
In your case you could create a very small pattern cell (height = 1) with just one black pixel to the very left followed by the same number of white pixels as the distance to the next line.
I have a view in which I want to draw a text with Text Core (on the iPad). When text grown up I'd like to increase a height of the view, but I don't know how to calculate needed height of frame.
I use it to draw a text in drawRect method:
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)currentTexts);
CGMutablePathRef textPath = CGPathCreateMutable();
CGRect textRect = CGRectMake(PADDING, PADDING, self.frame.size.width - 2 * PADDING, self.frame.size.height - 2 * PADDING);
CGPathAddRect(textPath, NULL, textRect);
CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), textPath, NULL);
CTFrameDraw(textFrame, context);
CFRelease(textFrame);
CGPathRelease(textPath);
CFRelease(framesetter);
I tried to get a height of text using sizeWithFont and also that:
- (CGSize) measureFrame: (CTFrameRef) frame
{
CGPathRef framePath = CTFrameGetPath(frame);
CGRect frameRect = CGPathGetBoundingBox(framePath);
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex numLines = CFArrayGetCount(lines);
CGFloat maxWidth = 0;
CGFloat textHeight = 0;
// Now run through each line determining the maximum width of all the lines.
// We special case the last line of text. While we've got it's descent handy,
// we'll use it to calculate the typographic height of the text as well.
CFIndex lastLineIndex = numLines - 1;
for(CFIndex index = 0; index < numLines; index++)
{
CGFloat ascent, descent, leading, width;
CTLineRef line = (CTLineRef) CFArrayGetValueAtIndex(lines, index);
width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
if(width > maxWidth)
{
maxWidth = width;
}
if(index == lastLineIndex)
{
// Get the origin of the last line. We add the descent to this
// (below) to get the bottom edge of the last line of text.
CGPoint lastLineOrigin;
CTFrameGetLineOrigins(frame, CFRangeMake(lastLineIndex, 1), &lastLineOrigin);
// The height needed to draw the text is from the bottom of the last line
// to the top of the frame.
textHeight = CGRectGetMaxY(frameRect) - lastLineOrigin.y + descent;
}
}
// For some text the exact typographic bounds is a fraction of a point too
// small to fit the text when it is put into a context. We go ahead and round
// the returned drawing area up to the nearest point. This takes care of the
// discrepencies.
return CGSizeMake(ceil(maxWidth), ceil(textHeight));
}
I use that to create an attrubuted string:
CTParagraphStyleSetting setting[1] = {
{kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &minimumLineSpacing}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(setting, 1);
NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:
(id)textColor.CGColor, kCTForegroundColorAttributeName,
(id)currentFont, kCTFontAttributeName,
(id)paragraphStyle, kCTParagraphStyleAttributeName,
nil];
When I use sizeWithFont, at the begin everything is ok, but when text has more lines, the frame is bigger and bigger than a text and I want it to fit exactly the text. How can I make it?
To calculate the height of the text, have you tried using CTFramesetterSuggestFrameSizeWithConstraints ?
http://foobarpig.com/iphone/using-ctframesettersuggestframesizewithconstraints-and-sizewithfont-to-calculate-text-height.html
I am a developing an iPad application in which i have to use CTRunDelegate. I have defined all the the callbacks that are required viz CTRunDelegateGetAscentCallback , CTRunDelegateGetDescentCallback , CTRunDelegateGetWidthCallback. I dont know how to use CTRunDelegateRef object that I am creating. Right now what is happening is that my callbacks are not getting called.
Any pointers in this regard will be highly appreciated.
Thanx in advance.
You should add your run delegate as an attribute for a range of characters in your attributed string. See Core Text String Attributes. When drawing, Core Text will call your callbacks to get the sizing of that characters.
Update
This is a sample code for a view drawing a simple text (Note that there's no memory management code here).
#implementation View
/* Callbacks */
void MyDeallocationCallback( void* refCon ){
}
CGFloat MyGetAscentCallback( void *refCon ){
return 10.0;
}
CGFloat MyGetDescentCallback( void *refCon ){
return 4.0;
}
CGFloat MyGetWidthCallback( void* refCon ){
return 125;
}
- (void)drawRect:(CGRect)rect {
// create an attributed string
NSMutableAttributedString * attrString = [[NSMutableAttributedString alloc] initWithString:#"This is my delegate space"];
// create the delegate
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateVersion1;
callbacks.dealloc = MyDeallocationCallback;
callbacks.getAscent = MyGetAscentCallback;
callbacks.getDescent = MyGetDescentCallback;
callbacks.getWidth = MyGetWidthCallback;
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, NULL);
// set the delegate as an attribute
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attrString, CFRangeMake(19, 1), kCTRunDelegateAttributeName, delegate);
// create a frame and draw the text
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, rect);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attrString.length), path, NULL);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextSetTextPosition(context, 0.0, 0.0);
CTFrameDraw(frame, context);
}
#end
The size of the space character between "delegate" and "space" in the text are controlled by the run delegate.