Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to draw a non-rectangle UITextView?

I just want to create a UITextView like this (not two textviews, the empty area is a uiimage)

enter image description here

like image 377
ericyue Avatar asked Feb 14 '12 02:02

ericyue


1 Answers

There's no built-in UIView subclass that does this (except UIWebView if you write the proper HTML and CSS), but it's quite easy to do using Core Text. I've put my test project in my ShapedLabel github repository, and here's what it looks like:

ShapedLabel screen shot

The project has a UIView subclass called ShapedLabel. Here's how it works.

Create a UIView subclass called ShapedLabel. Give it these properties:

@property (nonatomic, copy) NSString *text;
@property (nonatomic) UITextAlignment textAlignment;
@property (nonatomic, copy) NSString *fontName;
@property (nonatomic) CGFloat fontSize;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *shapeColor;
@property (nonatomic, copy) UIBezierPath *path;

You'll want to override each property setter method to send setNeedsDisplay, like this for example:

- (void)setFontName:(NSString *)fontName {
    _fontName = [fontName copy];
    [self setNeedsDisplay];
}

I'm relying on ARC to worry about releasing the old value of _fontName. If you're not using ARC... start. It's so much easier and it's supported since iOS 4.0.

Anyway, then you'll need to implement drawRect:, where the real work gets done. First, we'll fill in the shape with the shapeColor if it's set:

- (void)drawRect:(CGRect)rect
{
    if (!_path)
        return;

    if (_shapeColor) {
        [_shapeColor setFill];
        [_path fill];
    }

We check to make sure we have all the other parameters we need:

    if (!_text || !_textColor || !_fontName || _fontSize <= 0)
        return;

Next we handle the textAligment property:

    CTTextAlignment textAlignment = NO ? 0
        : _textAlignment == UITextAlignmentCenter ? kCTCenterTextAlignment
        : _textAlignment == UITextAlignmentRight ? kCTRightTextAlignment
        : kCTLeftTextAlignment;
    CTParagraphStyleSetting paragraphStyleSettings[] = {
        {
            .spec = kCTParagraphStyleSpecifierAlignment,
            .valueSize = sizeof textAlignment,
            .value = &textAlignment
        }
    };
    CTParagraphStyleRef style = CTParagraphStyleCreate(paragraphStyleSettings, sizeof paragraphStyleSettings / sizeof *paragraphStyleSettings);

We create the CTFont next. Note that this is different than a CGFont or a UIFont. You can convert a CGFont to a CTFont using CTFontCreateWithGraphicsFont, but you cannot easily convert a UIFont to a CTFont. Anyway we just create the CTFont directly:

    CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)_fontName, _fontSize, NULL);

We create the attributes dictionary that defines all of the style attributes we want to see:

    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
        (__bridge id)font, kCTFontAttributeName,
        _textColor.CGColor, kCTForegroundColorAttributeName,
        style, kCTParagraphStyleAttributeName,
        nil];
    CFRelease(font);
    CFRelease(style);

Once we have the attributes dictionary, we can create the attributed string that attaches the attributes dictionary to the text string. This is what Core Text uses:

    CFAttributedStringRef trib = CFAttributedStringCreate(NULL, (__bridge CFStringRef)_text, (__bridge CFDictionaryRef)attributes);

We create a Core Text framesetter that will lay out the text from the attributed string:

    CTFramesetterRef setter = CTFramesetterCreateWithAttributedString(trib);
    CFRelease(trib);

Core Text assumes that the graphics context will have the “standard” Core Graphics coordinate system with the origin at the lower left. But UIKit changes the context to put the origin at the upper left. We'll assume that the path was created with that in mind. So we need a transform that flips the coordinate system vertically:

    // Core Text lays out text using the default Core Graphics coordinate system, with the origin at the lower left.  We need to compensate for that, both when laying out the text and when drawing it.
    CGAffineTransform textMatrix = CGAffineTransformIdentity;
    textMatrix = CGAffineTransformTranslate(textMatrix, 0, self.bounds.size.height);
    textMatrix = CGAffineTransformScale(textMatrix, 1, -1);

We can then create a flipped copy of the path:

    CGPathRef flippedPath = CGPathCreateCopyByTransformingPath(_path.CGPath, &textMatrix);

At last we can ask the framesetter to lay out a frame of text. This is what actually fits the text inside the shape defined by the path property:

    CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), flippedPath, NULL);
    CFRelease(flippedPath);
    CFRelease(setter);

Finally we draw the text. We need to again

    CGContextRef gc = UIGraphicsGetCurrentContext();
    CGContextSaveGState(gc); {
        CGContextConcatCTM(gc, textMatrix);
        CTFrameDraw(frame, gc);
    } CGContextRestoreGState(gc);
    CFRelease(frame);
}

That's pretty much it. You can now put a nice shaped label on the screen.

For posterity (in case I delete the test project), here's the complete source for the ShapedLabel class.

ShapedLabel.h

#import <UIKit/UIKit.h>

@interface ShapedLabel : UIView

@property (nonatomic, copy) NSString *text;
@property (nonatomic) UITextAlignment textAlignment;
@property (nonatomic, copy) NSString *fontName;
@property (nonatomic) CGFloat fontSize;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *shapeColor;
@property (nonatomic, copy) UIBezierPath *path;

@end

ShapedLabel.m

#import "ShapedLabel.h"
#import <CoreText/CoreText.h>

@implementation ShapedLabel

@synthesize fontName = _fontName;
@synthesize fontSize = _fontSize;
@synthesize path = _path;
@synthesize text = _text;
@synthesize textColor = _textColor;
@synthesize shapeColor = _shapeColor;
@synthesize textAlignment = _textAlignment;

- (void)commonInit {
    _text = @"";
    _fontSize = UIFont.systemFontSize;
    // There is no API for just getting the system font name, grr...
    UIFont *uiFont = [UIFont systemFontOfSize:_fontSize];
    _fontName = [uiFont.fontName copy];
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)setFontName:(NSString *)fontName {
    _fontName = [fontName copy];
    [self setNeedsDisplay];
}

- (void)setFontSize:(CGFloat)fontSize {
    _fontSize = fontSize;
    [self setNeedsDisplay];
}

- (void)setPath:(UIBezierPath *)path {
    _path = [path copy];
    [self setNeedsDisplay];
}

- (void)setText:(NSString *)text {
    _text = [text copy];
    [self setNeedsDisplay];
}

- (void)setTextColor:(UIColor *)textColor {
    _textColor = textColor;
    [self setNeedsDisplay];
}

- (void)setTextAlignment:(UITextAlignment)textAlignment {
    _textAlignment = textAlignment;
    [self setNeedsDisplay];
}

- (void)setShapeColor:(UIColor *)shapeColor {
    _shapeColor = shapeColor;
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
    if (!_path)
        return;

    if (_shapeColor) {
        [_shapeColor setFill];
        [_path fill];
    }

    if (!_text || !_textColor || !_fontName || _fontSize <= 0)
        return;

    CTTextAlignment textAlignment = NO ? 0
    : _textAlignment == UITextAlignmentCenter ? kCTCenterTextAlignment
    : _textAlignment == UITextAlignmentRight ? kCTRightTextAlignment
    : kCTLeftTextAlignment;
    CTParagraphStyleSetting paragraphStyleSettings[] = {
        {
            .spec = kCTParagraphStyleSpecifierAlignment,
            .valueSize = sizeof textAlignment,
            .value = &textAlignment
        }
    };
    CTParagraphStyleRef style = CTParagraphStyleCreate(paragraphStyleSettings, sizeof paragraphStyleSettings / sizeof *paragraphStyleSettings);

    CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)_fontName, _fontSize, NULL);

    NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
                                (__bridge id)font, kCTFontAttributeName,
                                _textColor.CGColor, kCTForegroundColorAttributeName,
                                style, kCTParagraphStyleAttributeName,
                                nil];
    CFRelease(font);
    CFRelease(style);

    CFAttributedStringRef trib = CFAttributedStringCreate(NULL, (__bridge CFStringRef)_text, (__bridge CFDictionaryRef)attributes);
    CTFramesetterRef setter = CTFramesetterCreateWithAttributedString(trib);
    CFRelease(trib);

    // Core Text lays out text using the default Core Graphics coordinate system, with the origin at the lower left.  We need to compensate for that, both when laying out the text and when drawing it.
    CGAffineTransform textMatrix = CGAffineTransformIdentity;
    textMatrix = CGAffineTransformTranslate(textMatrix, 0, self.bounds.size.height);
    textMatrix = CGAffineTransformScale(textMatrix, 1, -1);

    CGPathRef flippedPath = CGPathCreateCopyByTransformingPath(_path.CGPath, &textMatrix);
    CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), flippedPath, NULL);
    CFRelease(flippedPath);
    CFRelease(setter);

    CGContextRef gc = UIGraphicsGetCurrentContext();
    CGContextSaveGState(gc); {
        CGContextConcatCTM(gc, textMatrix);
        CTFrameDraw(frame, gc);
    } CGContextRestoreGState(gc);
    CFRelease(frame);
}

@end
like image 72
rob mayoff Avatar answered Sep 28 '22 01:09

rob mayoff