How to draw an oval speech bubble programmatically

2020-05-21 05:45发布

问题:

The technique shown in a similar question is a rectangular bubble. How to draw one in an oval shape? i.e.:

   

回答1:

I would do it in two iterations.
First get the context and begin a path. Fill an ellipse and then a custom path that encloses a triangle with three lines. I assumed the following dimensions: 70 width, 62 height. Override draw rect in a subclass of UIView and instantiate in a subclassed UIViewController:

-(void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetRGBFillColor(ctx, 0.0, 0.0, 1.0, 1.0);
    CGContextFillEllipseInRect(ctx, CGRectMake(0.0, 0.0, 70.0, 50.0)); //oval shape
    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, 8.0, 40.0);
    CGContextAddLineToPoint(ctx, 6.0, 50.0);
    CGContextAddLineToPoint(ctx, 18.0, 45.0);
    CGContextClosePath(ctx);
    CGContextFillPath(ctx);
}

Produces this in the iPhone simulator when added against a gray backdrop:

This second code example will almost duplicate what you produced above. I implemented this using flexible sizes that could be supplied to the UIView frame when you instantiate it. Essentially, the white portion of the speech bubble is drawn with a black stroke over lay to follow.

-(void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGRect aRect = CGRectMake(2.0, 2.0, (self.bounds.size.width * 0.95f), (self.bounds.size.width * 0.60f)); // set the rect with inset.
    CGContextSetRGBFillColor(ctx, 1.0, 1.0, 1.0, 1.0); //white fill
    CGContextSetRGBStrokeColor(ctx, 0.0, 0.0, 0.0, 1.0); //black stroke
    CGContextSetLineWidth(ctx, 2.0); 


    CGContextFillEllipseInRect(ctx, aRect); 
    CGContextStrokeEllipseInRect(ctx, aRect);    

    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, (self.bounds.size.width * 0.10), (self.bounds.size.width * 0.48f));
    CGContextAddLineToPoint(ctx, 3.0, (self.bounds.size.height *0.80f));
    CGContextAddLineToPoint(ctx, 20.0, (self.bounds.size.height *0.70f));
    CGContextClosePath(ctx);
    CGContextFillPath(ctx);

    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, (self.bounds.size.width * 0.10), (self.bounds.size.width * 0.48f));
    CGContextAddLineToPoint(ctx, 3.0, (self.bounds.size.height *0.80f));
    CGContextStrokePath(ctx);

    CGContextBeginPath(ctx);
    CGContextMoveToPoint(ctx, 3.0, (self.bounds.size.height *0.80f));
    CGContextAddLineToPoint(ctx, 20.0, (self.bounds.size.height *0.70f));
    CGContextStrokePath(ctx);
 }



回答2:

I have another way - however I dont have any time to properly explain it.

However, my example was written in .NET for use in a Windows application.

My version creates the entire speach bubble as 2D Polygon Mesh and is partially customizable. It is a single drawn path instead of multiple parts.

While our platforms are not the same - the technique uses common math routines and procedural loop. I believe the technique could be translated to other programming languages or platforms.

 Private Sub Generate(ByVal Resolution As Integer, Optional ByVal SpeachPointerAngle As Integer = (45 * 3), Optional ByVal PointerBend As Decimal = 15)

        'Generated the same way we create vector (wireframe) mesh for an Ellipse but...
        '... at a predefined defined angle we create 

        'the size of the Pointer TIP/Corner portion of the speach bubble
        'in relation to the EDGE of the ELLIPSE (average)
        Dim SpeachPointerSize As Integer = 30

        If PointerBend > 10 Then PointerBend = 10
        If PointerBend < -10 Then PointerBend = -10

        'as a variable offset that should be limited to max +/-   -15 to 15 degrees relative to current angle as a safe range
        '- were speach pointer angle determins which side the the pointer appears
        Dim PointerOffsetValue As Decimal = PointerBend
        Dim ActualPointerAngle As Decimal

        'SpeachPointerAngle = 360 - SpeachPointerAngle ' adjust orientation so that 0 degrees is SOUTH

        'Ellipse Size:
        Dim Size_X As Decimal = 80
        Dim Size_Y As Decimal = 50

        If Resolution < 30 Then Resolution = 30

        Dim Result As Vector2()

        'size of each angle step based on resolution (number of vectors ) - Mesh Quality in otherwords.
        Dim _Step As Decimal = 360 / Resolution

        'Our current angle as we step through  the loop
        Dim _CurrentAngle As Decimal = 0

        'rounded values
        Dim _LastAngle As Decimal = 0
        Dim _NextAngle As Decimal = _Step

        Dim SpeachDrawn As Boolean = False ' prevent creating more than 1 point to be safe

        Dim I2 As Integer = 0 'need a stepper because of skipped IDS 

        'build the ellipse mesh 
        For i = 0 To Resolution - 1

            _LastAngle = _CurrentAngle - 15
            _NextAngle = _CurrentAngle + 15

            ActualPointerAngle = _CurrentAngle 'side
            ActualPointerAngle += PointerOffsetValue ' acual angle of point

            Dim EX As Decimal = System.Math.Cos(Math.Deg2Rad(_CurrentAngle)) * Size_X
            Dim EY As Decimal = System.Math.Sin(Math.Deg2Rad(_CurrentAngle)) * Size_Y

            'Point extrusion size ( trying to be even size all around )
            Dim ExtrudeX As Decimal = System.Math.Cos(Math.Deg2Rad(_CurrentAngle)) * (Size_X + SpeachPointerSize)
            Dim ExtrudeY As Decimal = System.Math.Sin(Math.Deg2Rad(_CurrentAngle)) * (Size_Y + SpeachPointerSize)

            'is Pointer angle between Last and Next?
            If SpeachPointerAngle > _LastAngle And SpeachPointerAngle < _NextAngle Then
                If (SpeachDrawn = False) Then
                    ' the point for the speachbubble tip
                    Array.Resize(Result, I2 + 1)

                    Result(I2) = New Vector2(ExtrudeX, ExtrudeY)
                    SpeachDrawn = True
                    I2 += 1
                Else
                    'skip
                End If
            Else
                'normal ellipse vector
                Array.Resize(Result, I2 + 1)
                Result(I2) = New Vector2(EX, EY)
                I2 += 1
            End If

            _CurrentAngle += _Step

        Next

        _Vectors = Result
    End Sub

The above code generated this - drawn to a bitmap using GDI+ [DrawPolygon/FillPolygon]: https://fbcdn-sphotos-a.akamaihd.net/hphotos-ak-ash4/380262_10151202393414692_590995900_n.jpg

(Sorry - I can't post the image here directly as I have never posted here before. I don't have the reputation yet )

This is a Primitive in a Graphics Assembly I am developing for .NET which uses my own Vector2.

This speach bubble supports transparency when drawn - as it is a single polygon shape instead of multiple shapes.

Basically we draw an ellipse programatically and then extrude a speach point out on a desired side of the ellipse.

A similar approach could be applied using PointF structures instead.

All shapes in the code are generated around Origin 0,0.

Arrays are also resized incrementally as vectors are added prevent gaps in the array.

EG - the center of the speach bubble is Origin 0.0.

I apologize for not explaining my code properly - I just don't have the time. But it probably isnt too hard to understand.