7

I am working on a children's book app and would like to dynamically populate speech bubble(s) for character dialogues as shown in attached screenshots. Screenshots are from Cinderella app which was developed using Cocos2D (or 3D) I guess. However, I would like to achieve this functionality using objective-c and iOS sdk.

One approach would be:

Calculate the size of the text given a font and size (using the NSString method sizeWithFont:constrainedToSize:), then draw a rounded rectangle (using CALayer) to enclose that size.

For drawing rounded rectangle, set up a CALayer with a background color, border width and border color and color radius, and add that to a UIView object. That would give a speech bubble effect very easily.

Now, how would I get a corner that pointed to the character's mouth? How do I pop-out and pop-in the CALayer from character's mouth, how would I implement this animation? My main scene (illustration) would be an UIImageview and this dialogue pop-op should appear from character's mouth in a animated way and after few seconds it should disappear as if its going back in character's mouth. You suggestions would be greatly appreciated.

If you are aware of some sample code/article some place, please route me accordingly.

Here is a video link of the app which shows how character dialogues pop-in and out as speech bubble: http://www.youtube.com/watch?v=umfNJLyrrNg

enter image description here

Alex
  • 833
  • 3
  • 15
  • 28

3 Answers3

15

here is my implementation by modifying this apple tutorial:

https://developer.apple.com/library/ios/samplecode/quartzdemo/Listings/QuartzDemo_QuartzCurves_m.html

- (void)drawRect:(CGRect)rect
{
    CGContextRef context=UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, .5f);
    CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor);
    CGContextSetRGBFillColor(context, 1, 1, 1, 1);

    rect.origin.y++;
    CGFloat radius = cornerRadius;

    CGFloat minx = CGRectGetMinX(rect), midx = CGRectGetMidX(rect), maxx = CGRectGetMaxX(rect);
    CGFloat miny = CGRectGetMinY(rect), midy = CGRectGetMidY(rect), maxy = CGRectGetMaxY(rect);

    CGMutablePathRef outlinePath = CGPathCreateMutable();

    if (![User isCurrentUser:message.user])
    {
        minx += 5;
    
        CGPathMoveToPoint(outlinePath, nil, midx, miny);
        CGPathAddArcToPoint(outlinePath, nil, maxx, miny, maxx, midy, radius);
        CGPathAddArcToPoint(outlinePath, nil, maxx, maxy, midx, maxy, radius);
        CGPathAddArcToPoint(outlinePath, nil, minx, maxy, minx, midy, radius);
        CGPathAddLineToPoint(outlinePath, nil, minx, miny + 20);
        CGPathAddLineToPoint(outlinePath, nil, minx - 5, miny + 15);
        CGPathAddLineToPoint(outlinePath, nil, minx, miny + 10);
        CGPathAddArcToPoint(outlinePath, nil, minx, miny, midx, miny, radius);
        CGPathCloseSubpath(outlinePath);
    
        CGContextSetShadowWithColor(context, CGSizeMake(0,1), 1, [UIColor lightGrayColor].CGColor);
        CGContextAddPath(context, outlinePath);
        CGContextFillPath(context);
    
        CGContextAddPath(context, outlinePath);
        CGContextClip(context);
        CGPoint start = CGPointMake(rect.origin.x, rect.origin.y);
        CGPoint end = CGPointMake(rect.origin.x, rect.size.height);
        CGContextDrawLinearGradient(context, [self normalGradient], start, end, 0);
    }
    else
    {
        maxx-=5;
        CGPathMoveToPoint(outlinePath, nil, midx, miny);
        CGPathAddArcToPoint(outlinePath, nil, minx, miny, minx, midy, radius);
        CGPathAddArcToPoint(outlinePath, nil, minx, maxy, midx, maxy, radius);
        CGPathAddArcToPoint(outlinePath, nil, maxx, maxy, maxx, midy, radius);
        CGPathAddLineToPoint(outlinePath, nil, maxx, miny + 20);
        CGPathAddLineToPoint(outlinePath, nil, maxx + 5, miny + 15);
        CGPathAddLineToPoint(outlinePath, nil, maxx, miny + 10);
        CGPathAddArcToPoint(outlinePath, nil, maxx, miny, midx, miny, radius);
        CGPathCloseSubpath(outlinePath);
    
        CGContextSetShadowWithColor(context, CGSizeMake(0,1), 1, [UIColor lightGrayColor].CGColor);
        CGContextAddPath(context, outlinePath);
        CGContextFillPath(context);
    
        CGContextAddPath(context, outlinePath);
        CGContextClip(context);
        CGPoint start = CGPointMake(rect.origin.x, rect.origin.y);
        CGPoint end = CGPointMake(rect.origin.x, rect.size.height);
        CGContextDrawLinearGradient(context, [self greenGradient], start, end, 0);
    }


    CGContextDrawPath(context, kCGPathFillStroke);

}

- (CGGradientRef)normalGradient
{

    NSMutableArray *normalGradientLocations = [NSMutableArray arrayWithObjects:
                                               [NSNumber numberWithFloat:0.0f],
                                               [NSNumber numberWithFloat:1.0f],
                                               nil];


    NSMutableArray *colors = [NSMutableArray arrayWithCapacity:2];

    UIColor *color = [UIColor whiteColor];
    [colors addObject:(id)[color CGColor]];
    color = [StyleView lightColorFromColor:[UIColor cloudsColor]];
    [colors addObject:(id)[color CGColor]];
    NSMutableArray  *normalGradientColors = colors;

    int locCount = [normalGradientLocations count];
    CGFloat locations[locCount];
    for (int i = 0; i < [normalGradientLocations count]; i++)
    {
        NSNumber *location = [normalGradientLocations objectAtIndex:i];
        locations[i] = [location floatValue];
    }
    CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();

    CGGradientRef normalGradient = CGGradientCreateWithColors(space, (__bridge CFArrayRef)normalGradientColors, locations);
    CGColorSpaceRelease(space);

    return normalGradient;
}

Which results in this... enter image description here

Community
  • 1
  • 1
Misha
  • 693
  • 5
  • 14
  • 1
    This looks nice, but can you add a comment on usage. I.e., how do you get from wanting to put some text into the bubble, to actually having it in the bubble – ChrisC Dec 13 '14 at 00:12
  • 1
    @user2011985 you would create a subclass a UILabel or UITextField and add the drawRect method. I'm modifying the code a bit to make that more apparent. – Misha Dec 19 '14 at 20:21
  • can u provide demo in swift ? @Misha – amit gupta Sep 15 '15 at 13:16
  • @Misha . Can you please share this UILabel subclass with properties you have used as well? comment and color schemes? Or a demo? That will be much appreciated. – iAhmed Jan 13 '16 at 08:59
  • I have found, at least with IOS 11, when I tried this that the text if left aligned is off the edge of the speech bubble, so I had to indent it using an overridden method - (void)drawTextInRect:(CGRect)rect { UIEdgeInsets insets = {0, 10, 0, 10}; [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)]; } – ammianus Dec 09 '17 at 19:46
  • One other comment, it's not totally clear to me what values are acceptable for the "radius" in your code from looking at Apple's developer documentation. From experimentation radius values between 5.0 and 10.0 seemed to look reasonable, again on IOS 11 – ammianus Dec 09 '17 at 19:47
  • Oh and link to the Quartz Demo is now broken, I can't find it on Apple's site. Does anyone have the correct link? – ammianus Dec 09 '17 at 19:48
7

Another way of doing this is through the use of images with cap insets. Take a look at the UIImage method:

resizableImageWithCapInsets:

Basically you can create an image of a minimum sized bubble, and have it stretch indefinitely while maintaining the look of the 'bubble' borders.

The code below specifies an image where 12 pixels on the top, left, bottom and right be preserved when stretching/resizing the image:

UIImage *bubble = [[UIImage imageNamed:@"bubble.png"] 
                            resizableImageWithCapInsets:UIEdgeInsetsMake(12, 12, 12, 12)];
UIImageView *imgView = [[[UIImageView alloc] initWithImage:bubble] autorelease];
imgView.frame = CGRectZero;

Animating the size change of a UIImageView comes for free:

[UIView animateWithDuration:0.3
                 animations:^(void) {
                     imgView.frame = CGRectMake(50, 50, 200, 100);
                 } completion:^(BOOL finished) {

                 }];
hundreth
  • 841
  • 4
  • 8
  • I see this a lot lately. Is -stretchableImageWithLeftCapWidth:topCapHeight: deprecated? – Nicolas Miari Jun 25 '12 at 20:05
  • Thanks. How would I speech bubble corner that pointed to the character's mouth using resizableImageWithCapInsets:? And How would I animate the opening and closing of speech bubble? – Alex Jun 25 '12 at 20:42
  • I guess the pointed corner would be part of the image which I would be stretching. How can I animate (on tap or automatically) the opening and closing of speech bubble? – Alex Jun 25 '12 at 20:51
  • 1
    Yes Alex, the pointed corner would be part of the image. See my clarifications above. – hundreth Jun 25 '12 at 20:52
  • Thx much. How and where would I print the actual dialogue text? Can I use a NSString and/or UILabel? Dialogue should be printed inside the speech bubble. – Alex Jun 25 '12 at 20:58
  • Build a UILabel with dimensions you calculated from your NSString, add the UILabel view on top of your bubble image view. – hundreth Jun 25 '12 at 21:01
  • In answer to Alex's question "where would I print the actual dialogue text" I found this posting helpful http://stackoverflow.com/questions/6992830/how-to-write-text-on-image-in-objective-c-iphone. – Michael Osofsky Aug 24 '14 at 03:52
3

The three20 framework (actually, Three20Style) has got a shaped window class called TTSpeechBubbleShape, which is basically what you are looking for.

Now, you have two options, as I see it:

  1. either use Three20; or

  2. analyze the TTSpeechBubbleShape class to see how this is implemented and do the same in your app.

sergio
  • 68,819
  • 11
  • 102
  • 123
  • Thanks. I hope there won't be any issues with my app acceptance by apple if I implement using TTSpeechBubbleShape? How would I animate the opening and closing of speech bubble? – Alex Jun 25 '12 at 20:37
  • I think you will have no issues with approval; an app of mine using speech bubble got approved some time ago... try with animating the frame size, it will do. – sergio Jun 26 '12 at 07:33
  • @sergio Hey, I am trying to implement this, but the SpeechBubble class is derived from TTShape which is derived from NSObject. How do I turn this NSObject into a view – GangstaGraham Apr 11 '13 at 21:46
  • Can't open the link or find TTSpeechBubbleShape by search. – kelin Jul 29 '22 at 10:15